mirror of https://github.com/ginuerzh/gost
19 changed files with 395 additions and 68 deletions
@ -0,0 +1,122 @@ |
|||||
|
package gost |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"net" |
||||
|
"regexp" |
||||
|
"strings" |
||||
|
"sync" |
||||
|
"sync/atomic" |
||||
|
"time" |
||||
|
|
||||
|
"github.com/go-log/log" |
||||
|
) |
||||
|
|
||||
|
// 25 端口 465 和 587
|
||||
|
var mailPorts = []string{"25", "465", "587"} |
||||
|
|
||||
|
type EmailACL struct { |
||||
|
emails map[string]struct{} |
||||
|
domains map[string]struct{} |
||||
|
regex []*regexp.Regexp |
||||
|
} |
||||
|
|
||||
|
var emailACL atomic.Value |
||||
|
|
||||
|
func LoadEmailACL(list []string, regexList []string) error { |
||||
|
acl := &EmailACL{ |
||||
|
emails: map[string]struct{}{}, |
||||
|
domains: map[string]struct{}{}, |
||||
|
} |
||||
|
for _, v := range list { |
||||
|
v = strings.ToLower(strings.TrimSpace(v)) |
||||
|
if strings.HasPrefix(v, "@") { |
||||
|
domain := strings.TrimPrefix(v, "@") |
||||
|
acl.domains[domain] = struct{}{} |
||||
|
} else { |
||||
|
acl.emails[v] = struct{}{} |
||||
|
} |
||||
|
} |
||||
|
for _, r := range regexList { |
||||
|
re, err := regexp.Compile(r) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
acl.regex = append(acl.regex, re) |
||||
|
} |
||||
|
emailACL.Store(acl) |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func IsEmailAllowed(email string) bool { |
||||
|
acl := emailACL.Load().(*EmailACL) |
||||
|
email = strings.ToLower(strings.TrimSpace(email)) |
||||
|
// 白名单为空 → 拒绝所有
|
||||
|
if len(acl.emails) == 0 && len(acl.domains) == 0 && len(acl.regex) == 0 { |
||||
|
return false |
||||
|
} |
||||
|
// 精确匹配
|
||||
|
if _, ok := acl.emails[email]; ok { |
||||
|
return true |
||||
|
} |
||||
|
// domain 匹配
|
||||
|
parts := strings.Split(email, "@") |
||||
|
if len(parts) == 2 { |
||||
|
domain := parts[1] |
||||
|
if _, ok := acl.domains[domain]; ok { |
||||
|
return true |
||||
|
} |
||||
|
} |
||||
|
// regex
|
||||
|
for _, r := range acl.regex { |
||||
|
if r.MatchString(email) { |
||||
|
return true |
||||
|
} |
||||
|
} |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
func CheckMailFrom(email string) error { |
||||
|
if !IsEmailAllowed(email) { |
||||
|
log.Logf("smtp blocked email: %s", email) |
||||
|
return fmt.Errorf("550 sender not allowed") |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
type RateLimit struct { |
||||
|
count int |
||||
|
lastTime time.Time |
||||
|
} |
||||
|
|
||||
|
var rateLimitMap sync.Map // key: ip/user, value: *RateLimit
|
||||
|
|
||||
|
func CheckRateLimit(ip net.IP, user string, maxPerMinute int) bool { |
||||
|
|
||||
|
key := ip.String() + ":" + user |
||||
|
now := time.Now() |
||||
|
|
||||
|
v, _ := rateLimitMap.LoadOrStore(key, &RateLimit{ |
||||
|
count: 0, |
||||
|
lastTime: now, |
||||
|
}) |
||||
|
|
||||
|
rl := v.(*RateLimit) |
||||
|
|
||||
|
// 超过一分钟窗口 → 重置计数
|
||||
|
if now.Sub(rl.lastTime) > time.Minute { |
||||
|
rl.count = 0 |
||||
|
rl.lastTime = now |
||||
|
} |
||||
|
|
||||
|
if rl.count >= maxPerMinute { |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
rl.count++ |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
// if !CheckRateLimit(clientIP, username, 50) {
|
||||
|
// return fmt.Errorf("451 Too many messages, rate limit exceeded")
|
||||
|
// }
|
||||
@ -0,0 +1,65 @@ |
|||||
|
package gost |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"net" |
||||
|
"strings" |
||||
|
"sync/atomic" |
||||
|
) |
||||
|
|
||||
|
var whiteList atomic.Value |
||||
|
|
||||
|
type IPWhiteList struct { |
||||
|
networks []*net.IPNet |
||||
|
} |
||||
|
|
||||
|
func NewIPWhiteList(list []string) (*IPWhiteList, error) { |
||||
|
wl := &IPWhiteList{} |
||||
|
for _, item := range list { |
||||
|
if strings.Contains(item, "/") { |
||||
|
_, netw, err := net.ParseCIDR(item) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
wl.networks = append(wl.networks, netw) |
||||
|
} else { |
||||
|
ip := net.ParseIP(item) |
||||
|
if ip == nil { |
||||
|
return nil, fmt.Errorf("invalid ip: %s", item) |
||||
|
} |
||||
|
mask := net.CIDRMask(32, 32) |
||||
|
wl.networks = append(wl.networks, &net.IPNet{ |
||||
|
IP: ip, |
||||
|
Mask: mask, |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return wl, nil |
||||
|
} |
||||
|
|
||||
|
func (w *IPWhiteList) Contains(ip net.IP) bool { |
||||
|
if ip == nil { |
||||
|
return false |
||||
|
} |
||||
|
for _, netw := range w.networks { |
||||
|
if netw.Contains(ip) { |
||||
|
return true |
||||
|
} |
||||
|
} |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
func LoadIPWhiteList(list []string) error { |
||||
|
wl, err := NewIPWhiteList(list) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
whiteList.Store(wl) |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func isWhiteIP(ip net.IP) bool { |
||||
|
wl := whiteList.Load().(*IPWhiteList) |
||||
|
return wl.Contains(ip) |
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
[auth] |
||||
|
#动态口令周期 (s) 600=10分钟 |
||||
|
dynamic_period = 600 |
||||
|
#时间漂移 |
||||
|
dynamic_skew = 1 |
||||
|
|
||||
|
ip_whitelist = [ |
||||
|
"198.144.184.47", |
||||
|
"108.174.48.108", |
||||
|
"23.94.205.145", |
||||
|
"108.174.48.102", |
||||
|
"198.144.184.108", |
||||
|
"198.144.184.57", |
||||
|
"198.144.184.125", |
||||
|
] |
||||
|
|
||||
|
#动态密钥 |
||||
|
secret = "&&4sg123g[]/~" |
||||
|
|
||||
|
# SMTP 发信白名单 |
||||
|
email_whitelist = ["[email protected]", "@example.com"] |
||||
|
|
||||
|
email_regex = ["^test[0-9][email protected]$"] |
||||
@ -0,0 +1,30 @@ |
|||||
|
package gost |
||||
|
|
||||
|
import ( |
||||
|
"github.com/BurntSushi/toml" |
||||
|
"github.com/go-log/log" |
||||
|
) |
||||
|
|
||||
|
type Config struct { |
||||
|
Auth struct { |
||||
|
DynamicPeriod int64 `toml:"dynamic_period"` |
||||
|
IPWhiteList []string `toml:"ip_whitelist"` |
||||
|
EmailWhiteList []string `toml:"email_whitelist"` |
||||
|
EmailRegWhiteList []string `toml:"email_regex"` |
||||
|
DynamicSkew int `toml:"dynamic_skew"` |
||||
|
Secret string `toml:"secret"` |
||||
|
} `toml:"auth"` |
||||
|
} |
||||
|
|
||||
|
var config Config |
||||
|
|
||||
|
func LoadAuthConfig() { |
||||
|
|
||||
|
_, err := toml.DecodeFile("auth.toml", &config) |
||||
|
if err != nil { |
||||
|
log.Log("not found auth.toml", err) |
||||
|
} |
||||
|
|
||||
|
LoadIPWhiteList(config.Auth.IPWhiteList) |
||||
|
LoadEmailACL(config.Auth.EmailWhiteList, config.Auth.EmailRegWhiteList) |
||||
|
} |
||||
@ -0,0 +1,72 @@ |
|||||
|
package gost |
||||
|
|
||||
|
import ( |
||||
|
"crypto/hmac" |
||||
|
"crypto/sha1" |
||||
|
"crypto/sha256" |
||||
|
"encoding/binary" |
||||
|
"encoding/hex" |
||||
|
"fmt" |
||||
|
"time" |
||||
|
) |
||||
|
|
||||
|
func VerifyOTP(secret, pass string) bool { |
||||
|
now := time.Now().UTC().Unix() |
||||
|
skew := config.Auth.DynamicSkew |
||||
|
period := config.Auth.DynamicPeriod |
||||
|
|
||||
|
for i := -skew; i <= skew; i++ { |
||||
|
|
||||
|
t := (now / period) + int64(i) |
||||
|
|
||||
|
code := generateTOTP(secret, t) |
||||
|
|
||||
|
if code == pass { |
||||
|
return true |
||||
|
} |
||||
|
} |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
func generateTOTP(secret string, counter int64) string { |
||||
|
key := []byte(secret) |
||||
|
var buf [8]byte |
||||
|
binary.BigEndian.PutUint64(buf[:], uint64(counter)) |
||||
|
h := hmac.New(sha1.New, key) |
||||
|
h.Write(buf[:]) |
||||
|
hash := h.Sum(nil) |
||||
|
offset := hash[len(hash)-1] & 0x0f |
||||
|
truncated := |
||||
|
(binary.BigEndian.Uint32(hash[offset:offset+4]) & 0x7fffffff) % 1000000 |
||||
|
return fmt.Sprintf("%06d", truncated) |
||||
|
} |
||||
|
|
||||
|
func generateSecret(ip, user string) string { |
||||
|
src := ip + ":" + user + ":" + config.Auth.Secret |
||||
|
hash := sha256.Sum256([]byte(src)) |
||||
|
return hex.EncodeToString(hash[:]) |
||||
|
} |
||||
|
|
||||
|
func verifyOTP(secret, pass string) (bool, int64) { |
||||
|
now := time.Now().UTC().Unix() |
||||
|
skew := config.Auth.DynamicSkew |
||||
|
period := config.Auth.DynamicPeriod |
||||
|
|
||||
|
for i := -skew; i <= skew; i++ { |
||||
|
counter := (now / period) + int64(i) |
||||
|
code := generateTOTP(secret, counter) |
||||
|
if code == pass { |
||||
|
return true, counter |
||||
|
} |
||||
|
} |
||||
|
return false, 0 |
||||
|
} |
||||
|
|
||||
|
func GeneratePassword(ip, user string) string { |
||||
|
src := ip + user + config.Auth.Secret |
||||
|
hash := sha256.New() |
||||
|
hash.Write([]byte(src)) |
||||
|
hashedSrc := hash.Sum(nil) |
||||
|
hashedSrcHex := hex.EncodeToString(hashedSrc) |
||||
|
return hashedSrcHex |
||||
|
} |
||||
Loading…
Reference in new issue