commit
4631524455
@ -0,0 +1,8 @@ |
|||||||
|
# Example AppArmor profile (/etc/apparmor.d/usr.local.bin.SYN-Scan-Firewall) |
||||||
|
/usr/local/bin/SYN-Scan-Firewall { |
||||||
|
include <abstractions/base> |
||||||
|
capability net_admin, |
||||||
|
capability net_raw, |
||||||
|
/etc/syn-firewall/config.yaml r, |
||||||
|
/var/lib/syn-firewall/** rw, |
||||||
|
} |
||||||
@ -0,0 +1,22 @@ |
|||||||
|
The MIT License |
||||||
|
|
||||||
|
Copyright (c) 2025 Robert Strutts |
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining |
||||||
|
a copy of this software and associated documentation files (the |
||||||
|
"Software"), to deal in the Software without restriction, including |
||||||
|
without limitation the rights to use, copy, modify, merge, publish, |
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to |
||||||
|
permit persons to whom the Software is furnished to do so, subject to |
||||||
|
the following conditions: |
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be |
||||||
|
included in all copies or substantial portions of the Software. |
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE |
||||||
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION |
||||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION |
||||||
|
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
||||||
@ -0,0 +1,35 @@ |
|||||||
|
# SYN-Scan-Firewall |
||||||
|
## To block the IP from port scans... |
||||||
|
``` |
||||||
|
./install.sh |
||||||
|
``` |
||||||
|
## /etc/SYN-Scan-Firewall/config.yaml |
||||||
|
``` |
||||||
|
blockDuration: 10m |
||||||
|
maxScanAttempts: 5 |
||||||
|
device: "enp2s0" # Ethernet Device name |
||||||
|
|
||||||
|
logging: |
||||||
|
filePath: "/var/log/SYN-Scan-Firewall.log" |
||||||
|
maxSizeMB: 10 # Max log size in megabytes |
||||||
|
backups: 5 # Number of backup logs to keep |
||||||
|
compressBackups: true # Whether to gzip old logs |
||||||
|
timestampFormat: "2006-01-02T15:04:05" # Go time format |
||||||
|
|
||||||
|
ignoredPorts: |
||||||
|
- 80 # HTTP |
||||||
|
- 443 # HTTPS |
||||||
|
- 9980 # php -S |
||||||
|
- 631 # CUPS (printing) |
||||||
|
- 9100 # print server ports |
||||||
|
- 53 # DNS |
||||||
|
- 123 # NTP |
||||||
|
- 68 # DHCP client |
||||||
|
# - 22 # SSH |
||||||
|
|
||||||
|
whitelistedIPs: |
||||||
|
- "192.168.10.2" # own IP |
||||||
|
- "192.168.1.100" # Example local admin |
||||||
|
- "10.0.0.50" # Example monitoring server |
||||||
|
- "127.0.0.1" # Localhost |
||||||
|
``` |
||||||
Binary file not shown.
@ -0,0 +1,486 @@ |
|||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"compress/gzip" |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"log" |
||||||
|
"os" |
||||||
|
"os/exec" |
||||||
|
"os/signal" |
||||||
|
"path/filepath" |
||||||
|
"sort" |
||||||
|
"strings" |
||||||
|
"sync" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/google/gopacket" |
||||||
|
"github.com/google/gopacket/layers" |
||||||
|
"github.com/google/gopacket/pcap" |
||||||
|
"gopkg.in/yaml.v3" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
defaultConfigFile = "/etc/SYN-Scan-Firewall/config.yaml" |
||||||
|
) |
||||||
|
|
||||||
|
// Config structures
|
||||||
|
type LoggingConfig struct { |
||||||
|
FilePath string `yaml:"filePath"` |
||||||
|
MaxSizeMB int `yaml:"maxSizeMB"` |
||||||
|
Backups int `yaml:"backups"` |
||||||
|
CompressBackups bool `yaml:"compressBackups"` |
||||||
|
TimestampFormat string `yaml:"timestampFormat"` |
||||||
|
} |
||||||
|
|
||||||
|
type Config struct { |
||||||
|
BlockDuration string `yaml:"blockDuration"` |
||||||
|
MaxScanAttempts int `yaml:"maxScanAttempts"` |
||||||
|
Device string `yaml:"device"` |
||||||
|
Logging LoggingConfig `yaml:"logging"` |
||||||
|
IgnoredPorts []int `yaml:"ignoredPorts"` |
||||||
|
WhitelistedIPs []string `yaml:"whitelistedIPs"` |
||||||
|
} |
||||||
|
|
||||||
|
type AppConfig struct { |
||||||
|
BlockDuration time.Duration |
||||||
|
MaxScanAttempts int |
||||||
|
Device string |
||||||
|
Logging LoggingConfig |
||||||
|
IgnoredPorts map[int]bool |
||||||
|
WhitelistedIPs map[string]bool |
||||||
|
} |
||||||
|
|
||||||
|
type UnblockTask struct { |
||||||
|
IP string |
||||||
|
UnblockAt time.Time |
||||||
|
} |
||||||
|
|
||||||
|
type Sniffer struct { |
||||||
|
unblockTasks []UnblockTask |
||||||
|
tracker *ScanTracker |
||||||
|
handle *pcap.Handle |
||||||
|
config *AppConfig |
||||||
|
logger *log.Logger |
||||||
|
} |
||||||
|
|
||||||
|
// ScanTracker implementation
|
||||||
|
type ScanTracker struct { |
||||||
|
sync.Mutex |
||||||
|
entries map[string]*ScanEntry |
||||||
|
} |
||||||
|
|
||||||
|
type ScanEntry struct { |
||||||
|
Count int |
||||||
|
Timestamp time.Time |
||||||
|
} |
||||||
|
|
||||||
|
// NewScanTracker creates and initializes a new ScanTracker
|
||||||
|
func NewScanTracker() *ScanTracker { |
||||||
|
return &ScanTracker{ |
||||||
|
entries: make(map[string]*ScanEntry), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// RotatingLogger implementation
|
||||||
|
type RotatingLogger struct { |
||||||
|
config LoggingConfig |
||||||
|
currentFile *os.File |
||||||
|
mu sync.Mutex |
||||||
|
} |
||||||
|
|
||||||
|
func NewRotatingLogger(config LoggingConfig) (*RotatingLogger, error) { |
||||||
|
rl := &RotatingLogger{config: config} |
||||||
|
if err := rl.openFile(); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
return rl, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (rl *RotatingLogger) openFile() error { |
||||||
|
if err := os.MkdirAll(filepath.Dir(rl.config.FilePath), 0755); err != nil { |
||||||
|
return fmt.Errorf("failed to create log directory: %v", err) |
||||||
|
} |
||||||
|
|
||||||
|
file, err := os.OpenFile(rl.config.FilePath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0640) |
||||||
|
if err != nil { |
||||||
|
return fmt.Errorf("failed to open log file: %v", err) |
||||||
|
} |
||||||
|
|
||||||
|
rl.currentFile = file |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (rl *RotatingLogger) rotate() error { |
||||||
|
rl.mu.Lock() |
||||||
|
defer rl.mu.Unlock() |
||||||
|
|
||||||
|
if err := rl.currentFile.Close(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
for i := rl.config.Backups - 1; i >= 0; i-- { |
||||||
|
src := rl.getBackupName(i) |
||||||
|
if _, err := os.Stat(src); err == nil { |
||||||
|
dst := rl.getBackupName(i + 1) |
||||||
|
if i+1 >= rl.config.Backups { |
||||||
|
os.Remove(dst) |
||||||
|
} else { |
||||||
|
if rl.config.CompressBackups && !strings.HasSuffix(src, ".gz") { |
||||||
|
if err := rl.compressFile(src); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
src += ".gz" |
||||||
|
dst += ".gz" |
||||||
|
} |
||||||
|
os.Rename(src, dst) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if err := os.Rename(rl.config.FilePath, rl.getBackupName(0)); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
return rl.openFile() |
||||||
|
} |
||||||
|
|
||||||
|
func (rl *RotatingLogger) compressFile(src string) error { |
||||||
|
in, err := os.Open(src) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
defer in.Close() |
||||||
|
|
||||||
|
out, err := os.Create(src + ".gz") |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
defer out.Close() |
||||||
|
|
||||||
|
gz := gzip.NewWriter(out) |
||||||
|
defer gz.Close() |
||||||
|
|
||||||
|
if _, err = io.Copy(gz, in); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
return os.Remove(src) |
||||||
|
} |
||||||
|
|
||||||
|
func (rl *RotatingLogger) getBackupName(index int) string { |
||||||
|
if index == 0 { |
||||||
|
return rl.config.FilePath + ".1" |
||||||
|
} |
||||||
|
return fmt.Sprintf("%s.%d", rl.config.FilePath, index+1) |
||||||
|
} |
||||||
|
|
||||||
|
func (rl *RotatingLogger) needsRotation() (bool, error) { |
||||||
|
info, err := rl.currentFile.Stat() |
||||||
|
if err != nil { |
||||||
|
return false, err |
||||||
|
} |
||||||
|
return info.Size() >= int64(rl.config.MaxSizeMB*1024*1024), nil |
||||||
|
} |
||||||
|
|
||||||
|
func (rl *RotatingLogger) Write(p []byte) (n int, err error) { |
||||||
|
if rotate, err := rl.needsRotation(); rotate && err == nil { |
||||||
|
if err := rl.rotate(); err != nil { |
||||||
|
return 0, err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
rl.mu.Lock() |
||||||
|
defer rl.mu.Unlock() |
||||||
|
return rl.currentFile.Write(p) |
||||||
|
} |
||||||
|
|
||||||
|
func (rl *RotatingLogger) Close() error { |
||||||
|
rl.mu.Lock() |
||||||
|
defer rl.mu.Unlock() |
||||||
|
return rl.currentFile.Close() |
||||||
|
} |
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
func loadConfig(path string) (*AppConfig, error) { |
||||||
|
data, err := os.ReadFile(path) |
||||||
|
if err != nil { |
||||||
|
return nil, fmt.Errorf("failed to read config file: %v", err) |
||||||
|
} |
||||||
|
|
||||||
|
var cfg Config |
||||||
|
if err := yaml.Unmarshal(data, &cfg); err != nil { |
||||||
|
return nil, fmt.Errorf("failed to parse config: %v", err) |
||||||
|
} |
||||||
|
|
||||||
|
// Set defaults
|
||||||
|
if cfg.Logging.FilePath == "" { |
||||||
|
cfg.Logging.FilePath = "/var/log/SYN-Scan-Firewall.log" |
||||||
|
} |
||||||
|
if cfg.Logging.MaxSizeMB == 0 { |
||||||
|
cfg.Logging.MaxSizeMB = 10 |
||||||
|
} |
||||||
|
if cfg.Logging.Backups == 0 { |
||||||
|
cfg.Logging.Backups = 5 |
||||||
|
} |
||||||
|
if cfg.Logging.TimestampFormat == "" { |
||||||
|
cfg.Logging.TimestampFormat = "2006-01-02 15:04:05" |
||||||
|
} |
||||||
|
|
||||||
|
blockDuration, err := time.ParseDuration(cfg.BlockDuration) |
||||||
|
if err != nil { |
||||||
|
return nil, fmt.Errorf("invalid blockDuration format: %v", err) |
||||||
|
} |
||||||
|
|
||||||
|
ignoredPorts := make(map[int]bool) |
||||||
|
for _, port := range cfg.IgnoredPorts { |
||||||
|
ignoredPorts[port] = true |
||||||
|
} |
||||||
|
|
||||||
|
whitelistedIPs := make(map[string]bool) |
||||||
|
for _, ip := range cfg.WhitelistedIPs { |
||||||
|
whitelistedIPs[ip] = true |
||||||
|
} |
||||||
|
|
||||||
|
return &AppConfig{ |
||||||
|
BlockDuration: blockDuration, |
||||||
|
MaxScanAttempts: cfg.MaxScanAttempts, |
||||||
|
Device: cfg.Device, |
||||||
|
Logging: cfg.Logging, |
||||||
|
IgnoredPorts: ignoredPorts, |
||||||
|
WhitelistedIPs: whitelistedIPs, |
||||||
|
}, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (st *ScanTracker) Add(ip string, config *AppConfig) { |
||||||
|
st.Lock() |
||||||
|
defer st.Unlock() |
||||||
|
|
||||||
|
entry, exists := st.entries[ip] |
||||||
|
if !exists { |
||||||
|
entry = &ScanEntry{} |
||||||
|
st.entries[ip] = entry |
||||||
|
} |
||||||
|
|
||||||
|
now := time.Now() |
||||||
|
if entry.Timestamp.Add(config.BlockDuration).Before(now) { |
||||||
|
entry.Count = 0 |
||||||
|
} |
||||||
|
|
||||||
|
entry.Count++ |
||||||
|
entry.Timestamp = now |
||||||
|
} |
||||||
|
|
||||||
|
func (st *ScanTracker) GetCount(ip string) int { |
||||||
|
st.Lock() |
||||||
|
defer st.Unlock() |
||||||
|
|
||||||
|
if entry, exists := st.entries[ip]; exists { |
||||||
|
return entry.Count |
||||||
|
} |
||||||
|
return 0 |
||||||
|
} |
||||||
|
|
||||||
|
func isIPBlocked(ip string) bool { |
||||||
|
cmd := exec.Command("sudo", "iptables", "-L", "-n") |
||||||
|
output, err := cmd.Output() |
||||||
|
if err != nil { |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
return strings.Contains(string(output), ip) |
||||||
|
} |
||||||
|
|
||||||
|
func blockIP(ip string, logger *log.Logger) { |
||||||
|
if isIPBlocked(ip) { |
||||||
|
logger.Printf("IP %s is already blocked", ip) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
logger.Printf("Redirecting IP: %s to port 9999 BANNER", ip) |
||||||
|
cmd := exec.Command("sudo", "iptables", "-t", "nat", "-A", "PREROUTING", "-s", ip, "-p", "tcp", "--dport", "1:65535", "-j", "REDIRECT", "--to-port", "9999") |
||||||
|
if err := cmd.Run(); err != nil { |
||||||
|
logger.Printf("Error redirecting IP %s to banner service: %v", ip, err) |
||||||
|
} |
||||||
|
|
||||||
|
logger.Printf("Blocking IP: %s", ip) |
||||||
|
cmd = exec.Command("sudo", "iptables", "-A", "INPUT", "-s", ip, "-j", "DROP") |
||||||
|
if err := cmd.Run(); err != nil { |
||||||
|
logger.Printf("Error blocking IP %s: %v", ip, err) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func unblockIP(ip string, logger *log.Logger) { |
||||||
|
logger.Printf("Unblocking IP: %s", ip) |
||||||
|
cmd := exec.Command("sudo", "iptables", "-D", "INPUT", "-s", ip, "-j", "DROP") |
||||||
|
if err := cmd.Run(); err != nil { |
||||||
|
logger.Printf("Error unblocking IP %s: %v", ip, err) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Sniffer methods
|
||||||
|
func (s *Sniffer) isWhitelisted(ip string) bool { |
||||||
|
return s.config.WhitelistedIPs[ip] |
||||||
|
} |
||||||
|
|
||||||
|
func (s *Sniffer) handlePacket(packet gopacket.Packet) { |
||||||
|
tcpLayer := packet.Layer(layers.LayerTypeTCP) |
||||||
|
if tcpLayer == nil { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
tcp, _ := tcpLayer.(*layers.TCP) |
||||||
|
|
||||||
|
if tcp.SYN && !tcp.ACK { |
||||||
|
ipLayer := packet.Layer(layers.LayerTypeIPv4) |
||||||
|
if ipLayer == nil { |
||||||
|
return |
||||||
|
} |
||||||
|
ip, _ := ipLayer.(*layers.IPv4) |
||||||
|
|
||||||
|
srcIP := ip.SrcIP.String() |
||||||
|
dstPort := int(tcp.DstPort) |
||||||
|
|
||||||
|
if s.isWhitelisted(srcIP) { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
if s.config.IgnoredPorts[dstPort] { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
s.logger.Printf("Scan detected on port %d from %s", dstPort, srcIP) |
||||||
|
|
||||||
|
s.tracker.Add(srcIP, s.config) |
||||||
|
count := s.tracker.GetCount(srcIP) |
||||||
|
|
||||||
|
if count > s.config.MaxScanAttempts { |
||||||
|
s.logger.Printf("IP %s exceeded scan limit (%d attempts), blocking for %.0f minutes", |
||||||
|
srcIP, s.config.MaxScanAttempts, s.config.BlockDuration.Minutes()) |
||||||
|
blockIP(srcIP, s.logger) |
||||||
|
unblockTime := time.Now().Add(s.config.BlockDuration) |
||||||
|
s.logger.Printf("IP %s will be unblocked at %s", srcIP, unblockTime.Format(s.config.Logging.TimestampFormat)) |
||||||
|
s.unblockTasks = append(s.unblockTasks, UnblockTask{ |
||||||
|
IP: srcIP, |
||||||
|
UnblockAt: unblockTime, |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (s *Sniffer) unblockExpiredIPs() { |
||||||
|
now := time.Now() |
||||||
|
var remainingTasks []UnblockTask |
||||||
|
|
||||||
|
for _, task := range s.unblockTasks { |
||||||
|
if now.After(task.UnblockAt) || now.Equal(task.UnblockAt) { |
||||||
|
unblockIP(task.IP, s.logger) |
||||||
|
} else { |
||||||
|
remainingTasks = append(remainingTasks, task) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
s.unblockTasks = remainingTasks |
||||||
|
} |
||||||
|
|
||||||
|
func (s *Sniffer) StartSniffing() error { |
||||||
|
s.logger.Printf("Starting port scan detection") |
||||||
|
s.logger.Printf("Configuration:") |
||||||
|
s.logger.Printf(" Device: %s", s.config.Device) |
||||||
|
s.logger.Printf(" Ignored ports: %v", s.getSortedPorts()) |
||||||
|
s.logger.Printf(" Whitelisted IPs: %v", s.getSortedIPs()) |
||||||
|
s.logger.Printf(" Block duration: %.0f minutes", s.config.BlockDuration.Minutes()) |
||||||
|
s.logger.Printf(" Max scan attempts before blocking: %d", s.config.MaxScanAttempts) |
||||||
|
|
||||||
|
handle, err := pcap.OpenLive(s.config.Device, 1600, false, pcap.BlockForever) |
||||||
|
if err != nil { |
||||||
|
return fmt.Errorf("error opening device %s: %v", s.config.Device, err) |
||||||
|
} |
||||||
|
s.handle = handle |
||||||
|
|
||||||
|
err = handle.SetBPFFilter("tcp") |
||||||
|
if err != nil { |
||||||
|
return fmt.Errorf("error setting filter: %v", err) |
||||||
|
} |
||||||
|
|
||||||
|
packetSource := gopacket.NewPacketSource(handle, handle.LinkType()) |
||||||
|
for packet := range packetSource.Packets() { |
||||||
|
s.handlePacket(packet) |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (s *Sniffer) getSortedPorts() []int { |
||||||
|
ports := make([]int, 0, len(s.config.IgnoredPorts)) |
||||||
|
for port := range s.config.IgnoredPorts { |
||||||
|
ports = append(ports, port) |
||||||
|
} |
||||||
|
sort.Ints(ports) |
||||||
|
return ports |
||||||
|
} |
||||||
|
|
||||||
|
func (s *Sniffer) getSortedIPs() []string { |
||||||
|
ips := make([]string, 0, len(s.config.WhitelistedIPs)) |
||||||
|
for ip := range s.config.WhitelistedIPs { |
||||||
|
ips = append(ips, ip) |
||||||
|
} |
||||||
|
sort.Strings(ips) |
||||||
|
return ips |
||||||
|
} |
||||||
|
|
||||||
|
// Main program
|
||||||
|
func main() { |
||||||
|
if os.Geteuid() != 0 { |
||||||
|
log.Fatal("This program must be run as root (sudo)") |
||||||
|
} |
||||||
|
|
||||||
|
config, err := loadConfig(defaultConfigFile) |
||||||
|
if err != nil { |
||||||
|
log.Fatalf("Configuration error: %v", err) |
||||||
|
} |
||||||
|
|
||||||
|
rl, err := NewRotatingLogger(config.Logging) |
||||||
|
if err != nil { |
||||||
|
log.Fatalf("Failed to initialize logger: %v", err) |
||||||
|
} |
||||||
|
defer rl.Close() |
||||||
|
|
||||||
|
logger := log.New(rl, "", 0) |
||||||
|
logger.SetPrefix(fmt.Sprintf("[%s] ", time.Now().Format(config.Logging.TimestampFormat))) |
||||||
|
|
||||||
|
tracker := NewScanTracker() |
||||||
|
sniffer := &Sniffer{ |
||||||
|
tracker: tracker, |
||||||
|
config: config, |
||||||
|
logger: logger, |
||||||
|
} |
||||||
|
sniffer.tracker.entries = make(map[string]*ScanEntry) |
||||||
|
|
||||||
|
go func() { |
||||||
|
if err := sniffer.StartSniffing(); err != nil { |
||||||
|
logger.Fatalf("Sniffer error: %v", err) |
||||||
|
} |
||||||
|
}() |
||||||
|
|
||||||
|
ticker := time.NewTicker(5 * time.Second) |
||||||
|
defer ticker.Stop() |
||||||
|
|
||||||
|
sigChan := make(chan os.Signal, 1) |
||||||
|
signal.Notify(sigChan, os.Interrupt) |
||||||
|
|
||||||
|
logger.Println("Running... Press Ctrl+C to stop") |
||||||
|
|
||||||
|
for { |
||||||
|
select { |
||||||
|
case <-ticker.C: |
||||||
|
sniffer.unblockExpiredIPs() |
||||||
|
case <-sigChan: |
||||||
|
logger.Println("Stopping...") |
||||||
|
if sniffer.handle != nil { |
||||||
|
sniffer.handle.Close() |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,66 @@ |
|||||||
|
[Unit] |
||||||
|
Description=SYN Scan Detection Firewall |
||||||
|
Documentation=https:// |
||||||
|
After=network.target network-online.target |
||||||
|
Requires=network-online.target |
||||||
|
ConditionPathExists=/usr/local/bin/SYN-Scan-Firewall |
||||||
|
AssertPathExists=/etc/SYN-Scan-firewall/config.yaml |
||||||
|
AssertFileIsExecutable=/usr/local/bin/SYN-Scan-Firewall |
||||||
|
|
||||||
|
[Service] |
||||||
|
Type=notify |
||||||
|
User=synfirewall |
||||||
|
Group=synfirewall |
||||||
|
WorkingDirectory=/var/lib/syn-firewall |
||||||
|
|
||||||
|
# Hardened execution |
||||||
|
ExecStart=/usr/local/bin/SYN-Scan-Firewall |
||||||
|
ExecReload=/bin/kill -HUP $MAINPID |
||||||
|
Restart=on-failure |
||||||
|
RestartSec=5s |
||||||
|
TimeoutStopSec=30s |
||||||
|
KillSignal=SIGTERM |
||||||
|
KillMode=process |
||||||
|
|
||||||
|
# Capabilities (minimal) |
||||||
|
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW |
||||||
|
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW |
||||||
|
|
||||||
|
# Security confinement |
||||||
|
NoNewPrivileges=true |
||||||
|
ProtectSystem=strict |
||||||
|
ProtectHome=read-only |
||||||
|
PrivateTmp=true |
||||||
|
PrivateDevices=true |
||||||
|
PrivateUsers=true |
||||||
|
ProtectHostname=true |
||||||
|
ProtectKernelTunables=true |
||||||
|
ProtectKernelModules=true |
||||||
|
ProtectControlGroups=true |
||||||
|
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 |
||||||
|
RestrictNamespaces=true |
||||||
|
RestrictRealtime=true |
||||||
|
RestrictSUIDSGID=true |
||||||
|
LockPersonality=true |
||||||
|
RemoveIPC=true |
||||||
|
|
||||||
|
# Memory protection |
||||||
|
MemoryDenyWriteExecute=true |
||||||
|
SystemCallFilter=@system-service @network-io @signal |
||||||
|
SystemCallArchitectures=native |
||||||
|
UMask=0077 |
||||||
|
|
||||||
|
# Network restrictions |
||||||
|
IPAddressDeny=any |
||||||
|
IPAddressAllow=localhost |
||||||
|
IPAddressAllow=127.0.0.1 |
||||||
|
IPAddressAllow=::1 |
||||||
|
|
||||||
|
# Resource limits |
||||||
|
LimitNOFILE=4096 |
||||||
|
LimitNPROC=64 |
||||||
|
LimitMEMLOCK=64K |
||||||
|
LimitSTACK=8M |
||||||
|
|
||||||
|
[Install] |
||||||
|
WantedBy=multi-user.target |
||||||
@ -0,0 +1,41 @@ |
|||||||
|
[Unit] |
||||||
|
Description=Portscan Firewall Banner Service |
||||||
|
After=network.target |
||||||
|
ConditionPathExists=/usr/local/bin/banner_service |
||||||
|
ConditionCapability=CAP_NET_BIND_SERVICE |
||||||
|
|
||||||
|
[Service] |
||||||
|
Type=simple |
||||||
|
User=bannersvc |
||||||
|
Group=bannersvc |
||||||
|
WorkingDirectory=/var/lib/banner-service |
||||||
|
ExecStart=/usr/local/bin/banner_service |
||||||
|
AmbientCapabilities=CAP_NET_BIND_SERVICE |
||||||
|
CapabilityBoundingSet=CAP_NET_BIND_SERVICE |
||||||
|
NoNewPrivileges=true |
||||||
|
ProtectSystem=strict |
||||||
|
ProtectHome=true |
||||||
|
PrivateTmp=true |
||||||
|
PrivateDevices=true |
||||||
|
ProtectKernelTunables=true |
||||||
|
ProtectKernelModules=true |
||||||
|
ProtectControlGroups=true |
||||||
|
RestrictAddressFamilies=AF_INET AF_INET6 |
||||||
|
RestrictNamespaces=true |
||||||
|
RestrictRealtime=true |
||||||
|
RestrictSUIDSGID=true |
||||||
|
MemoryDenyWriteExecute=true |
||||||
|
LockPersonality=true |
||||||
|
SystemCallFilter=@system-service |
||||||
|
SystemCallArchitectures=native |
||||||
|
IPAddressDeny=any |
||||||
|
IPAddressAllow=localhost |
||||||
|
IPAddressAllow=127.0.0.1 |
||||||
|
IPAddressAllow=::1 |
||||||
|
|
||||||
|
# Connection rate limiting |
||||||
|
LimitNOFILE=1024 |
||||||
|
LimitNPROC=8 |
||||||
|
|
||||||
|
[Install] |
||||||
|
WantedBy=multi-user.target |
||||||
Binary file not shown.
@ -0,0 +1,34 @@ |
|||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"net" |
||||||
|
"os" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
banner = "\n*** UNAUTHORIZED ACCESS PROHIBITED ***\n*** YOUR CONNECTION ATTEMPT HAS BEEN LOGGED ***\n\n" |
||||||
|
port = "9999" |
||||||
|
) |
||||||
|
|
||||||
|
func handleConnection(conn net.Conn) { |
||||||
|
conn.Write([]byte(banner)) |
||||||
|
conn.Close() |
||||||
|
} |
||||||
|
|
||||||
|
func main() { |
||||||
|
ln, err := net.Listen("tcp", ":"+port) |
||||||
|
if err != nil { |
||||||
|
fmt.Println("Error starting banner service:", err) |
||||||
|
os.Exit(1) |
||||||
|
} |
||||||
|
defer ln.Close() |
||||||
|
|
||||||
|
for { |
||||||
|
conn, err := ln.Accept() |
||||||
|
if err != nil { |
||||||
|
continue |
||||||
|
} |
||||||
|
go handleConnection(conn) |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,28 @@ |
|||||||
|
# config.yaml |
||||||
|
blockDuration: 10m |
||||||
|
maxScanAttempts: 5 |
||||||
|
device: "enp2s0" |
||||||
|
|
||||||
|
logging: |
||||||
|
filePath: "/var/log/SYN-Scan-Firewall.log" |
||||||
|
maxSizeMB: 10 # Max log size in megabytes |
||||||
|
backups: 5 # Number of backup logs to keep |
||||||
|
compressBackups: true # Whether to gzip old logs |
||||||
|
timestampFormat: "2006-01-02T15:04:05" # Go time format |
||||||
|
|
||||||
|
ignoredPorts: |
||||||
|
- 80 # HTTP |
||||||
|
- 443 # HTTPS |
||||||
|
- 9980 # php -S |
||||||
|
- 631 # CUPS (printing) |
||||||
|
- 9100 # print server ports |
||||||
|
- 53 # DNS |
||||||
|
- 123 # NTP |
||||||
|
- 68 # DHCP client |
||||||
|
# - 22 # SSH |
||||||
|
|
||||||
|
whitelistedIPs: |
||||||
|
- "192.168.10.2" # own IP |
||||||
|
- "192.168.1.100" # Example local admin |
||||||
|
# - "10.0.0.50" # Example monitoring server |
||||||
|
- "127.0.0.1" # Localhost |
||||||
@ -0,0 +1,8 @@ |
|||||||
|
module SYN-Scan-Firewall |
||||||
|
|
||||||
|
go 1.24.3 |
||||||
|
|
||||||
|
require ( |
||||||
|
github.com/google/gopacket v1.1.19 // indirect |
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect |
||||||
|
) |
||||||
@ -0,0 +1,17 @@ |
|||||||
|
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= |
||||||
|
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= |
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= |
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= |
||||||
|
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= |
||||||
|
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= |
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= |
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= |
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= |
||||||
|
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= |
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= |
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= |
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |
||||||
@ -0,0 +1,72 @@ |
|||||||
|
#!/bin/bash |
||||||
|
/usr/bin/echo "Installing libpcap-dev" |
||||||
|
/usr/bin/sudo /usr/bin/touch /var/log/SYN-Scan-Firewall.log |
||||||
|
/usr/bin/sudo /usr/bin/chmod 640 /var/log/SYN-Scan-Firewall.log |
||||||
|
/usr/bin/sudo /usr/bin/apt update |
||||||
|
/usr/bin/sudo /usr/bin/apt install -y libpcap-dev |
||||||
|
|
||||||
|
/usr/bin/echo "Create the service account for Banner" |
||||||
|
/usr/bin/sudo /usr/sbin/groupadd bannersvc |
||||||
|
/usr/bin/sudo /usr/sbin/useradd -r -g bannersvc -s /usr/sbin/nologin -d /var/lib/banner-service bannersvc |
||||||
|
/usr/bin/sudo /usr/bin/mkdir -p /var/lib/banner-service |
||||||
|
/usr/bin/sudo /usr/bin/chown bannersvc:bannersvc /var/lib/banner-service |
||||||
|
/usr/bin/sudo /usr/bin/chmod 750 /var/lib/banner-service |
||||||
|
|
||||||
|
# Force rebuild of packages, Remove file system paths from executable, Reduces binary size and removes debug info, Enables ASLR (Address Space Layout Randomization), and Use Go's native DNS resolver. |
||||||
|
/usr/bin/echo "Building Banner Service..." |
||||||
|
/usr/local/bin/go build \ |
||||||
|
-a \ |
||||||
|
-trimpath \ |
||||||
|
-ldflags="-s -w -extldflags=-z,now,-z,relro" \ |
||||||
|
-buildmode=pie \ |
||||||
|
-tags=netgo \ |
||||||
|
-o banner_service \ |
||||||
|
banner_service.go |
||||||
|
|
||||||
|
/usr/bin/sudo /usr/bin/cp banner_service /usr/local/bin/ |
||||||
|
/usr/bin/sudo /usr/bin/chown root:bannersvc /usr/local/bin/banner_service |
||||||
|
/usr/bin/sudo /usr/bin/chmod 750 /usr/local/bin/banner_service |
||||||
|
|
||||||
|
/usr/bin/echo "Set capabilities (for binding to port 9999 without root)" |
||||||
|
/usr/bin/sudo /usr/sbin/setcap 'cap_net_bind_service=+ep' /usr/local/bin/banner_service |
||||||
|
|
||||||
|
/usr/bin/echo "Copy over Service Files" |
||||||
|
/usr/bin/sudo /usr/bin/cp banner.service /etc/systemd/system/banner.service |
||||||
|
/usr/bin/sudo /usr/bin/chmod 644 /etc/systemd/system/banner.service |
||||||
|
/usr/bin/sudo /usr/bin/mkdir -p /etc/systemd/system/banner.service.d |
||||||
|
/usr/bin/sudo /usr/bin/cp seccomp.conf /etc/systemd/system/banner.service.d/seccomp.conf |
||||||
|
/usr/bin/sudo /usr/bin/chmod 644 /etc/systemd/system/banner.service |
||||||
|
|
||||||
|
/usr/bin/echo "Enable the service for Banner" |
||||||
|
/usr/bin/sudo /usr/bin/systemctl daemon-reload |
||||||
|
/usr/bin/sudo /usr/bin/systemctl enable --now banner.service |
||||||
|
|
||||||
|
/usr/bin/echo "Create the service account for synfirewall" |
||||||
|
sudo groupadd synfirewall |
||||||
|
sudo useradd -r -g synfirewall -s /usr/sbin/nologin \ |
||||||
|
-d /var/lib/syn-firewall -c "SYN Scan Firewall" synfirewall |
||||||
|
|
||||||
|
/usr/bin/echo "Making config.yaml" |
||||||
|
/usr/bin/sudo /usr/bin/mkdir -p /etc/SYN-Scan-Firewall |
||||||
|
/usr/bin/sudo /usr/bin/chown synfirewall:synfirewall /etc/SYN-Scan-Firewall |
||||||
|
/usr/bin/sudo /usr/bin/chmod 750 /etc/SYN-Scan-Firewall |
||||||
|
/usr/bin/sudo /usr/bin/cp config-example.yaml /etc/SYN-Scan-Firewall/config.yaml |
||||||
|
/usr/bin/chmod 640 /etc/SYN-Scan-Firewall/config.yaml |
||||||
|
/usr/bin/sudo /usr/bin/nano /etc/SYN-Scan-Firewall/config.yaml |
||||||
|
|
||||||
|
/usr/bin/echo "Making lib dir..." |
||||||
|
sudo mkdir -p /var/lib/syn-firewall |
||||||
|
sudo chown synfirewall:synfirewall /var/lib/syn-firewall |
||||||
|
sudo chmod 750 /var/lib/syn-firewall |
||||||
|
./reBuild.sh |
||||||
|
|
||||||
|
/usr/bin/echo "Copy over Service Files for SYN-Scan-Firewall" |
||||||
|
/usr/bin/sudo /usr/bin/cp SYN-Scan-Firewall.service /etc/systemd/system/ |
||||||
|
/usr/bin/sudo /usr/bin/chmod 644 /etc/systemd/system/SYN-Scan-Firewall.service |
||||||
|
|
||||||
|
/usr/bin/echo "Adding AppArmor policy file..." |
||||||
|
/usr/bin/sudo /usr/bin/cp AppArmor.policy /etc/apparmor.d/usr.local.bin.SYN-Scan-Firewall |
||||||
|
|
||||||
|
#/usr/bin/echo "Enable the service for SYN-Scan-Firewall" |
||||||
|
#sudo systemctl daemon-reload |
||||||
|
#sudo systemctl enable --now SYN-Scan-Firewall.service |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
#!/bin/bash |
||||||
|
/usr/bin/echo "Building SYN-Scan-Firewall..." |
||||||
|
go build -buildmode=pie -ldflags="-s -w -extldflags=-z,now,-z,relro" -tags=netgo -o SYN-Scan-Firewall SYN-Scan-Firewall.go |
||||||
|
|
||||||
|
/usr/bin/echo "Setting up local bin..." |
||||||
|
/usr/bin/sudo /usr/bin/cp SYN-Scan-Firewall /usr/local/bin/ |
||||||
|
/usr/bin/sudo /usr/bin/chown root:synfirewall /usr/local/bin/SYN-Scan-Firewall |
||||||
|
/usr/bin/sudo /usr/bin/chmod 750 /usr/local/bin/SYN-Scan-Firewall |
||||||
|
/usr/bin/sudo /usr/sbin/setcap 'cap_net_admin,cap_net_raw+ep' /usr/local/bin/SYN-Scan-Firewall |
||||||
@ -0,0 +1,3 @@ |
|||||||
|
[Service] |
||||||
|
SystemCallFilter=~@clock @debug @module @mount @obsolete @reboot @swap |
||||||
|
SystemCallErrorNumber=EPERM |
||||||
Loading…
Reference in new issue