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