Skip to content

Commit 36859ac

Browse files
committed
feat: core IP security feature with dynamic scoring and middleware
1 parent 62775ab commit 36859ac

File tree

12 files changed

+2958
-385
lines changed

12 files changed

+2958
-385
lines changed

README.md

Lines changed: 434 additions & 385 deletions
Large diffs are not rendered by default.

banManager.go

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package golangIPGuardian
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"net/smtp"
8+
"os"
9+
"sync"
10+
"time"
11+
12+
"github.com/redis/go-redis/v9"
13+
)
14+
15+
type BanItem struct {
16+
IP string `json:"ip"`
17+
Reason string `json:"reason"`
18+
AddedAt int64 `json:"added_at"`
19+
}
20+
21+
type BanManager struct {
22+
Logger *Logger
23+
Config *Config
24+
Redis *redis.Client
25+
Context context.Context
26+
Parameter *Parameter
27+
Mutex sync.RWMutex
28+
Cache map[string]*BanItem
29+
}
30+
31+
func (i *IPGuardian) newBanManager() *BanManager {
32+
manager := &BanManager{
33+
Logger: i.Logger,
34+
Config: i.Config,
35+
Redis: i.Redis,
36+
Context: i.Context,
37+
Parameter: &i.Config.Parameter,
38+
Cache: make(map[string]*BanItem),
39+
}
40+
41+
err := manager.load()
42+
if err != nil {
43+
i.Logger.error("Failed to load ban list from file", err.Error())
44+
}
45+
46+
return manager
47+
}
48+
49+
func (m *BanManager) load() error {
50+
m.Mutex.Lock()
51+
defer m.Mutex.Unlock()
52+
53+
path := ".ban.json"
54+
55+
if _, err := os.Stat(path); os.IsNotExist(err) {
56+
return nil
57+
}
58+
59+
data, err := os.ReadFile(path)
60+
if err != nil {
61+
return err
62+
}
63+
64+
var list []BanItem
65+
if err := json.Unmarshal(data, &list); err != nil {
66+
return err
67+
}
68+
69+
pipe := m.Redis.Pipeline()
70+
71+
for _, item := range list {
72+
m.Cache[item.IP] = &item
73+
74+
data, err := json.Marshal(item)
75+
if err != nil {
76+
continue
77+
}
78+
79+
key := fmt.Sprintf(redisBan, item.IP)
80+
pipe.Set(m.Context, key, data, 0)
81+
}
82+
83+
_, err = pipe.Exec(m.Context)
84+
if err != nil {
85+
return m.Logger.error("Failed to load ban list to redis", err.Error())
86+
}
87+
88+
return nil
89+
}
90+
91+
func (m *BanManager) check(ip string) bool {
92+
key := fmt.Sprintf(redisBan, ip)
93+
exist, err := m.Redis.Exists(m.Context, key).Result()
94+
if err == nil && exist > 0 {
95+
return true
96+
}
97+
98+
m.Mutex.RLock()
99+
defer m.Mutex.RUnlock()
100+
101+
_, cache := m.Cache[ip]
102+
103+
return cache
104+
}
105+
106+
func (m *BanManager) save() error {
107+
path := ".ban.json"
108+
109+
var list []BanItem
110+
for _, item := range m.Cache {
111+
list = append(list, *item)
112+
}
113+
114+
data, err := json.MarshalIndent(list, "", " ")
115+
if err != nil {
116+
return err
117+
}
118+
119+
return os.WriteFile(path, data, 0644)
120+
}
121+
122+
func (m *BanManager) sendEmail(ip string, reason string) {
123+
if m.Config.Email == nil {
124+
return
125+
}
126+
127+
subject := fmt.Sprintf("[IP Guardian] IP %s has been blacklisted", ip)
128+
body := ""
129+
msg := fmt.Sprintf("Subject: %s\r\n\r\n%s", subject, body)
130+
131+
auth := smtp.PlainAuth("", m.Config.Email.Username, m.Config.Email.Password, m.Config.Email.Host)
132+
addr := fmt.Sprintf("%s:%d", m.Config.Email.Host, m.Config.Email.Port)
133+
134+
err := smtp.SendMail(addr, auth, m.Config.Email.From, m.Config.Email.To, []byte(msg))
135+
if err != nil {
136+
m.Logger.error("Failed to send email", err.Error())
137+
}
138+
}
139+
140+
// * public
141+
func (m *BanManager) Add(ip, reason string) error {
142+
m.Mutex.Lock()
143+
defer m.Mutex.Unlock()
144+
145+
item := &BanItem{
146+
IP: ip,
147+
Reason: reason,
148+
AddedAt: time.Now().Unix(),
149+
}
150+
151+
m.Cache[ip] = item
152+
153+
data, err := json.Marshal(item)
154+
if err != nil {
155+
return m.Logger.error("Failed to parse ban item", err.Error())
156+
}
157+
158+
key := fmt.Sprintf(redisBan, ip)
159+
if err := m.Redis.Set(m.Context, key, data, 0).Err(); err != nil {
160+
return m.Logger.error("Failed to save ban item to redis", err.Error())
161+
}
162+
163+
if err := m.save(); err != nil {
164+
return m.Logger.error("Failed to save ban item to file", err.Error())
165+
}
166+
167+
if m.Config != nil {
168+
go m.sendEmail(ip, reason)
169+
}
170+
171+
return nil
172+
}

blockManager.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package golangIPGuardian
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"time"
8+
9+
"github.com/redis/go-redis/v9"
10+
)
11+
12+
type BlockItem struct {
13+
IP string `json:"ip"`
14+
Reason string `json:"reason"`
15+
AddedAt int64 `json:"added_at"`
16+
Count int `json:"count"`
17+
Last int64 `json:"last"`
18+
}
19+
20+
type BlockManager struct {
21+
Logger *Logger
22+
Config *Config
23+
Redis *redis.Client
24+
Context context.Context
25+
Parameter *Parameter
26+
}
27+
28+
func (i *IPGuardian) newBlockManager() *BlockManager {
29+
return &BlockManager{
30+
Logger: i.Logger,
31+
Config: i.Config,
32+
Redis: i.Redis,
33+
Context: i.Context,
34+
Parameter: &i.Config.Parameter,
35+
}
36+
}
37+
38+
func (m *BlockManager) check(ip string) bool {
39+
key := fmt.Sprintf(redisBlock, ip)
40+
41+
exist, err := m.Redis.Exists(m.Context, key).Result()
42+
if err == nil && exist > 0 {
43+
return true
44+
}
45+
46+
return false
47+
}
48+
49+
func (m *BlockManager) checkBlockItem(ip string) (bool, *BlockItem, error) {
50+
key := fmt.Sprintf(redisBlock, ip)
51+
52+
exists, err := m.Redis.Exists(m.Context, key).Result()
53+
if err != nil {
54+
return false, nil, err
55+
}
56+
57+
if exists == 0 {
58+
return false, nil, nil
59+
}
60+
61+
data, err := m.Redis.Get(m.Context, key).Result()
62+
if err != nil {
63+
return true, nil, err
64+
}
65+
66+
var item BlockItem
67+
if err := json.Unmarshal([]byte(data), &item); err != nil {
68+
return true, nil, err
69+
}
70+
71+
return true, &item, nil
72+
}
73+
74+
// * public
75+
func (m *BlockManager) Add(ip string, reason string) error {
76+
key := fmt.Sprintf(redisBlock, ip)
77+
now := time.Now().Unix()
78+
79+
var item *BlockItem
80+
81+
isBlock, item, err := m.checkBlockItem(ip)
82+
if err != nil {
83+
return err
84+
}
85+
86+
var duration time.Duration = time.Duration(m.Parameter.BlockTimeMin)
87+
88+
if isBlock && item != nil {
89+
item.Reason += "\n" + reason
90+
item.Count++
91+
item.Last = now
92+
93+
duration = time.Duration(1<<item.Count) * time.Duration(m.Parameter.BlockTimeMin) // * 指數增長封鎖時間
94+
if duration > time.Duration(m.Parameter.BlockTimeMax) {
95+
duration = time.Duration(m.Parameter.BlockTimeMax)
96+
}
97+
} else {
98+
item = &BlockItem{
99+
IP: ip,
100+
Reason: reason,
101+
AddedAt: now,
102+
Count: 1,
103+
Last: now,
104+
}
105+
}
106+
107+
data, err := json.Marshal(item)
108+
if err != nil {
109+
return m.Logger.error("Failed to parse block item", err.Error())
110+
}
111+
112+
if err := m.Redis.Set(m.Context, key, data, duration).Err(); err != nil {
113+
return m.Logger.error("Failed to update block item in redis", err.Error())
114+
}
115+
116+
return nil
117+
}

0 commit comments

Comments
 (0)