first version

This commit is contained in:
2021-08-01 00:03:25 +03:00
parent 231ca061ac
commit 5e38debcfd
13 changed files with 854 additions and 0 deletions

118
internal/action/action.go Normal file
View File

@ -0,0 +1,118 @@
package action
import (
"kf2-antiddos/internal/output"
"os"
"os/exec"
"strings"
"time"
)
type Action struct {
ticker *time.Ticker
ips map[string]bool // map[IP]readyToUnban
allowAction string
denyAction string
shell string
quit chan struct{}
banChan *chan string
resetChan *chan string
workerID uint
}
func New(workerID uint, denyTime uint, shell, allowAction, denyAction string, banChan, resetChan *chan string) *Action {
return &Action{
ticker: time.NewTicker(time.Duration(denyTime) * time.Second),
ips: make(map[string]bool),
allowAction: allowAction,
denyAction: denyAction,
shell: shell,
quit: make(chan struct{}),
banChan: banChan,
resetChan: resetChan,
workerID: workerID,
}
}
func (a *Action) Do() {
go func() {
for {
select {
case ip := <-*a.banChan:
a.deny(ip)
case <-a.ticker.C:
a.allow(false)
case <-a.quit:
a.ticker.Stop()
a.allow(true)
return
}
}
}()
}
func (a *Action) Stop() {
close(a.quit)
}
func (a *Action) allow(unbanAll bool) {
unban := make([]string, 0)
for ip := range a.ips {
if unbanAll || bool(a.ips[ip]) { // aka if readyToUnban
unban = append(unban, ip)
} else {
a.ips[ip] = true // mark readyToUnban next time
}
}
for _, ip := range unban {
delete(a.ips, ip)
}
if len(unban) != 0 {
for _, ip := range unban {
*a.resetChan <- ip
}
output.Printf("Allow: %s", strings.Join(unban, ", "))
if err := a.execCmd(a.allowAction, unban); err != nil {
output.Error(err.Error())
return
}
}
}
func (a *Action) deny(ip string) {
a.ips[ip] = false
output.Printf("Ban: %s", ip)
if err := a.execCmd(a.denyAction, []string{ip}); err != nil {
output.Error(err.Error())
return
}
}
func (a *Action) execCmd(command string, args []string) error {
WorkingDir, err := os.Getwd()
if err != nil {
WorkingDir = ""
}
cmd := &exec.Cmd{
Path: a.shell,
Args: append([]string{a.shell, command}, args...),
Stdout: output.StdoutWriter(),
Stderr: output.StderrWriter(),
Dir: WorkingDir,
}
if err := cmd.Start(); err != nil {
return err
}
if err := cmd.Wait(); err != nil {
return err
}
return nil
}

18
internal/common/common.go Normal file
View File

@ -0,0 +1,18 @@
package common
type Worker interface {
Do()
Stop()
}
type RawEvent struct {
LineNum byte
Text string
}
type Event struct {
LineNum byte
ConnectIP string
PlayerStartIP string
PlayerEndIP string
}

82
internal/config/config.go Normal file
View File

@ -0,0 +1,82 @@
package config
import (
"fmt"
"os"
"runtime"
"kf2-antiddos/internal/output"
)
const (
OT_Proxy = "proxy"
OT_Self = "self"
OT_All = "all"
OT_Quiet = "quiet"
)
type Config struct {
Shell string
DenyAction string
AllowAction string
Jobs uint
OutputMode string
DenyTime uint
MaxConn uint
ShowVersion bool
ShowHelp bool
}
func (cfg Config) IsValid() bool {
errs := make([]string, 0)
if cfg.Shell == "" {
errs = append(errs, "shell can not be empty")
} else if _, err := os.Stat(cfg.Shell); os.IsNotExist(err) {
errs = append(errs, fmt.Sprintf("shell %s not found", cfg.Shell))
}
if cfg.AllowAction == "" {
errs = append(errs, "allow_action can not be empty")
} else if _, err := os.Stat(cfg.AllowAction); os.IsNotExist(err) {
errs = append(errs, fmt.Sprintf("allow_action file %s not found", cfg.AllowAction))
}
if cfg.DenyAction == "" {
errs = append(errs, "deny_action can not be empty")
} else if _, err := os.Stat(cfg.DenyAction); os.IsNotExist(err) {
errs = append(errs, fmt.Sprintf("deny_action file %s not found", cfg.DenyAction))
}
switch cfg.OutputMode {
case OT_Proxy:
case OT_Self:
case OT_All:
case OT_Quiet:
case "":
default:
errs = append(errs, fmt.Sprintf("Unknown output_type: %s", cfg.OutputMode))
}
for _, err := range errs {
output.Errorln(err)
}
return len(errs) == 0
}
func (cfg *Config) SetEmptyArgs() {
if cfg.Jobs == 0 {
cfg.Jobs = uint(runtime.NumCPU())
}
if cfg.MaxConn == 0 {
cfg.MaxConn = 10
}
if cfg.OutputMode == "" {
cfg.OutputMode = OT_Self
}
if cfg.DenyTime == 0 {
cfg.DenyTime = 20 * 60
}
}

103
internal/history/history.go Normal file
View File

@ -0,0 +1,103 @@
package history
import (
"kf2-antiddos/internal/common"
)
type History struct {
quit chan struct{}
eventChan *chan common.Event
banChan *chan string
resetChan *chan string
head byte
history map[byte]common.Event
ips map[string]uint // map[ip]conn_count
whitelist map[string]struct{}
banned map[string]struct{}
maxConn uint
workerID uint
}
func New(workerID uint, eventChan *chan common.Event, banChan *chan string, resetChan *chan string, maxConn uint) *History {
return &History{
quit: make(chan struct{}),
ips: make(map[string]uint, 0),
history: make(map[byte]common.Event, 0),
whitelist: make(map[string]struct{}, 0),
banned: make(map[string]struct{}, 0),
eventChan: eventChan,
banChan: banChan,
resetChan: resetChan,
head: 0,
maxConn: maxConn,
workerID: workerID,
}
}
func (h *History) Do() {
go func() {
for {
select {
case event := <-*h.eventChan:
h.registerEvent(event)
case ip := <-*h.resetChan:
h.resetIp(ip)
case <-h.quit:
return
}
}
}()
}
func (h *History) Stop() {
close(h.quit)
}
func (h *History) registerEvent(e common.Event) {
h.history[e.LineNum] = e
for {
nextEvent, nextEventExists := h.history[h.head+1]
if nextEventExists {
switch {
case nextEvent.ConnectIP != "":
h.registerConnect(nextEvent.ConnectIP)
case nextEvent.PlayerEndIP != "":
h.registerEndPlayer(nextEvent.PlayerEndIP)
case nextEvent.PlayerStartIP != "":
h.registerNewPlayer(nextEvent.PlayerEndIP)
}
delete(h.history, h.head+1)
h.head++
} else {
break
}
}
}
func (h *History) registerConnect(ip string) {
h.ips[ip]++
if h.ips[ip] > h.maxConn {
_, whitelisted := h.whitelist[ip]
_, banned := h.banned[ip]
if !whitelisted && !banned {
h.banned[ip] = struct{}{}
*h.banChan <- ip
}
}
}
func (h *History) registerNewPlayer(ip string) {
h.whitelist[ip] = struct{}{}
}
func (h *History) registerEndPlayer(ip string) {
delete(h.whitelist, ip)
delete(h.ips, ip)
delete(h.banned, ip)
}
func (h *History) resetIp(ip string) {
delete(h.ips, ip)
delete(h.banned, ip)
}

111
internal/output/output.go Normal file
View File

@ -0,0 +1,111 @@
package output
import (
"fmt"
"io"
"io/ioutil"
"log"
"os"
"runtime"
)
const (
AppName = "[kf2-antiddos] "
)
var (
endOfLine string = "\n"
devNull *log.Logger = log.New(ioutil.Discard, "", 0)
stdout *log.Logger = log.New(os.Stdout, "", 0)
stderr *log.Logger = log.New(os.Stderr, "", 0)
proxy *log.Logger = log.New(os.Stdout, "", 0)
)
func ProxyMode() {
stdout = devNull
stderr = devNull
proxy = log.New(os.Stdout, "", 0)
}
func SelfMode() {
proxy = devNull
stdout = log.New(os.Stdout, "", 0)
stderr = log.New(os.Stderr, "", 0)
}
func AllMode() {
stdout = log.New(os.Stdout, AppName, 0)
stderr = log.New(os.Stderr, AppName, 0)
proxy = log.New(os.Stdout, "", 0)
}
func StdoutWriter() io.Writer {
return stdout.Writer()
}
func StderrWriter() io.Writer {
return stderr.Writer()
}
func QuietMode() {
stdout = devNull
stderr = devNull
proxy = devNull
}
func SetEndOfLineNative() {
switch os := runtime.GOOS; os {
case "windows":
setEndOfLineWindows()
default:
setEndOfLineUnix()
}
}
func EOL() string {
return endOfLine
}
func setEndOfLineUnix() {
endOfLine = "\n"
}
func setEndOfLineWindows() {
endOfLine = "\r\n"
}
func Print(v ...interface{}) {
stdout.Print(v...)
}
func Printf(format string, v ...interface{}) {
stdout.Printf(format, v...)
}
func Println(v ...interface{}) {
stdout.Print(fmt.Sprint(v...) + endOfLine)
}
func Error(v ...interface{}) {
stderr.Print(v...)
}
func Errorf(format string, v ...interface{}) {
stderr.Printf(format, v...)
}
func Errorln(v ...interface{}) {
stderr.Print(fmt.Sprint(v...) + endOfLine)
}
func Proxy(v ...interface{}) {
proxy.Print(v...)
}
func Proxyf(format string, v ...interface{}) {
proxy.Printf(format, v...)
}
func Proxyln(v ...interface{}) {
proxy.Print(fmt.Sprint(v...) + endOfLine)
}

75
internal/parser/parser.go Normal file
View File

@ -0,0 +1,75 @@
package parser
import (
"kf2-antiddos/internal/common"
"regexp"
)
const (
ngConnectIP = "ConnectIP"
ngPlayerStartIP = "PlayerStartIP"
ngPlayerEndIP = "PlayerEndIP"
rxIP = `\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}`
rxConnect = `NetComeGo:\sOpen\sTheWorld\s+(?P<` + ngConnectIP + `>` + rxIP + `){1}`
rxPlayerStart = `DevOnline:\sVerifyClientAuthSession:\sClientIP:\s(?P<` + ngPlayerStartIP + `>` + rxIP + `){1}`
rxPlayerEnd = `DevOnline:\sEndRemoteClientAuthSession:\sClientAddr:\s(?P<` + ngPlayerEndIP + `>` + rxIP + `){1}`
rxValue = rxConnect + `|` + rxPlayerStart + `|` + rxPlayerEnd
)
var (
rxKFLog *regexp.Regexp = regexp.MustCompile(rxValue)
)
type Parser struct {
quit chan struct{}
inputChan *chan common.RawEvent
outputChan *chan common.Event
workerID uint
}
func New(workerID uint, inputChan *chan common.RawEvent, outputChan *chan common.Event) *Parser {
return &Parser{
inputChan: inputChan,
outputChan: outputChan,
quit: make(chan struct{}),
workerID: workerID,
}
}
func (p *Parser) Do() {
go func() {
for {
select {
case rawEvent := <-*p.inputChan:
*p.outputChan <- p.parse(rawEvent)
case <-p.quit:
return
}
}
}()
}
func (p *Parser) Stop() {
close(p.quit)
}
func (p *Parser) parse(rawEvent common.RawEvent) common.Event {
res := common.Event{
LineNum: rawEvent.LineNum,
}
match := rxKFLog.FindStringSubmatch(rawEvent.Text)
for i, name := range rxKFLog.SubexpNames() {
if i != 0 && name != "" && i <= len(match) && match[i] != "" {
switch name {
case ngConnectIP:
res.ConnectIP = match[i]
case ngPlayerStartIP:
res.PlayerStartIP = match[i]
case ngPlayerEndIP:
res.PlayerEndIP = match[i]
}
}
}
return res
}

59
internal/reader/reader.go Normal file
View File

@ -0,0 +1,59 @@
package reader
import (
"kf2-antiddos/internal/common"
"kf2-antiddos/internal/output"
"bufio"
"os"
)
type Reader struct {
quit chan struct{}
outputChan *chan common.RawEvent
workerID uint
}
func New(workerID uint, outputChan *chan common.RawEvent) *Reader {
return &Reader{
outputChan: outputChan,
quit: make(chan struct{}),
workerID: workerID,
}
}
func (r *Reader) Do() {
go func() {
stdin := bufio.NewScanner(os.Stdin)
stdin.Split(bufio.ScanLines)
for {
select {
case <-r.quit: // check quit if there are no input
return
default:
for lineNum := byte(1); stdin.Scan(); lineNum++ { // byte overflow it's not a bug, but a feature
select {
case <-r.quit: // check quit if there are input
return
default:
}
text := stdin.Text()
output.Proxyln(text)
*r.outputChan <- common.RawEvent{
LineNum: lineNum,
Text: text,
}
}
if err := stdin.Err(); err != nil {
output.Errorln(err.Error())
}
}
}
}()
}
func (r *Reader) Stop() {
close(r.quit)
}