Files
-Fonu/internal/proxy/security.go
T
mrlee 278f8267b7 chore: 初始提交 Fonu 1.0.2
- 公网反代:Nginx 状态卡片(版本 / 运行状态 / 配置校验 / 端口监听)
- 部署设置第一、二梯队(HTTPS 开关、安全头、限流、连接数、仅中国大陆 IP)
- 隧道检测兼容 frps v0.68 / v0.71(V2 与旧版端点)
- 恢复被清空的 internal/frp/store.go、internal/api/ddns_handler.go、web/src/views/DdnsView.vue
2026-09-23 09:40:59 +08:00

293 lines
7.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package proxy
import (
"encoding/json"
"fmt"
"strings"
"github.com/fonu/fonu/internal/validate"
)
type BasicAuthConfig struct {
Enabled bool `json:"enabled,omitempty"`
Username string `json:"username,omitempty"`
PasswordHash string `json:"password_hash,omitempty"` // bcrypt htpasswd line body
HasPassword bool `json:"has_password,omitempty"` // API response only
}
type RateLimitConfig struct {
Enabled bool `json:"enabled,omitempty"`
Rate int `json:"rate,omitempty"` // requests per second
Burst int `json:"burst,omitempty"` // burst count
}
type ConnLimitConfig struct {
Enabled bool `json:"enabled,omitempty"`
Max int `json:"max,omitempty"` // max concurrent connections per IP
}
type SecurityConfig struct {
IPBlacklist []string `json:"ip_blacklist,omitempty"`
IPWhitelist []string `json:"ip_whitelist,omitempty"`
IPWhitelistMode bool `json:"ip_whitelist_mode,omitempty"`
ChinaOnly bool `json:"china_only,omitempty"`
BasicAuth *BasicAuthConfig `json:"basic_auth,omitempty"`
RateLimit *RateLimitConfig `json:"rate_limit,omitempty"`
ConnLimit *ConnLimitConfig `json:"conn_limit,omitempty"`
ProxySSLVerifyOff bool `json:"proxy_ssl_verify_off,omitempty"`
ProxyHostUpstream bool `json:"proxy_host_upstream,omitempty"` // use $proxy_host instead of $host
TLSMin13Only bool `json:"tls_min_13_only,omitempty"`
SecurityHeaders bool `json:"security_headers,omitempty"`
}
type SecurityInput struct {
IPBlacklist []string
IPWhitelist []string
IPWhitelistMode *bool
ChinaOnly *bool
BasicAuth *BasicAuthInput
RateLimit *RateLimitConfig
ConnLimit *ConnLimitConfig
ProxySSLVerifyOff *bool
ProxyHostUpstream *bool
TLSMin13Only *bool
SecurityHeaders *bool
}
type BasicAuthInput struct {
Enabled *bool
Username *string
Password *string // plaintext on write only
}
// DefaultPrivateCIDRs are always exempt from china_only restrictions.
var DefaultPrivateCIDRs = []string{
"10.0.0.0/8",
"127.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"::1/128",
"fc00::/7",
}
func ChinaBypassCIDRs(extra []string) []string {
seen := make(map[string]bool, len(DefaultPrivateCIDRs)+len(extra))
out := make([]string, 0, len(DefaultPrivateCIDRs)+len(extra))
for _, cidr := range DefaultPrivateCIDRs {
if !seen[cidr] {
seen[cidr] = true
out = append(out, cidr)
}
}
for _, cidr := range extra {
cidr = strings.TrimSpace(cidr)
if cidr != "" && !seen[cidr] {
seen[cidr] = true
out = append(out, cidr)
}
}
return out
}
func DefaultSecurityConfig() SecurityConfig {
return SecurityConfig{}
}
func ParseSecurityJSON(raw string) (SecurityConfig, error) {
raw = strings.TrimSpace(raw)
if raw == "" || raw == "{}" {
return DefaultSecurityConfig(), nil
}
var cfg SecurityConfig
if err := json.Unmarshal([]byte(raw), &cfg); err != nil {
return SecurityConfig{}, fmt.Errorf("安全配置格式无效")
}
return cfg.Normalize(), nil
}
func (c SecurityConfig) Normalize() SecurityConfig {
if c.RateLimit != nil && c.RateLimit.Enabled {
if c.RateLimit.Rate <= 0 {
c.RateLimit.Rate = 10
}
if c.RateLimit.Burst <= 0 {
c.RateLimit.Burst = 20
}
}
if c.ConnLimit != nil && c.ConnLimit.Enabled {
if c.ConnLimit.Max <= 0 {
c.ConnLimit.Max = 20
}
}
return c
}
func RuleForAPI(rule Rule) Rule {
rule.Security = rule.Security.ForAPI()
return rule
}
func RulesForAPI(rules []Rule) []Rule {
out := make([]Rule, len(rules))
for i, rule := range rules {
out[i] = RuleForAPI(rule)
}
return out
}
func (c SecurityConfig) ForAPI() SecurityConfig {
out := c.Normalize()
if out.BasicAuth != nil {
hasPassword := out.BasicAuth.PasswordHash != ""
out.BasicAuth = &BasicAuthConfig{
Enabled: out.BasicAuth.Enabled,
Username: out.BasicAuth.Username,
HasPassword: hasPassword,
}
}
return out
}
func (c SecurityConfig) MarshalJSON() ([]byte, error) {
type alias SecurityConfig
return json.Marshal(alias(c.Normalize()))
}
func (c SecurityConfig) ToJSON() (string, error) {
b, err := c.MarshalJSON()
if err != nil {
return "", err
}
return string(b), nil
}
func (c SecurityConfig) BasicAuthEnabled() bool {
return c.BasicAuth != nil && c.BasicAuth.Enabled && c.BasicAuth.Username != "" && c.BasicAuth.PasswordHash != ""
}
func (c SecurityConfig) RateLimitEnabled() bool {
return c.RateLimit != nil && c.RateLimit.Enabled
}
func (c SecurityConfig) ConnLimitEnabled() bool {
return c.ConnLimit != nil && c.ConnLimit.Enabled
}
func (c SecurityConfig) WhitelistEnabled() bool {
return c.IPWhitelistMode && len(c.IPWhitelist) > 0
}
func MergeSecurity(current SecurityConfig, in SecurityInput) (SecurityConfig, error) {
out := current.Normalize()
if in.IPBlacklist != nil {
out.IPBlacklist = in.IPBlacklist
}
if in.IPWhitelist != nil {
out.IPWhitelist = in.IPWhitelist
}
if in.IPWhitelistMode != nil {
out.IPWhitelistMode = *in.IPWhitelistMode
}
if in.ChinaOnly != nil {
out.ChinaOnly = *in.ChinaOnly
}
if in.RateLimit != nil {
out.RateLimit = in.RateLimit
}
if in.ConnLimit != nil {
out.ConnLimit = in.ConnLimit
}
if in.ProxySSLVerifyOff != nil {
out.ProxySSLVerifyOff = *in.ProxySSLVerifyOff
}
if in.ProxyHostUpstream != nil {
out.ProxyHostUpstream = *in.ProxyHostUpstream
}
if in.TLSMin13Only != nil {
out.TLSMin13Only = *in.TLSMin13Only
}
if in.SecurityHeaders != nil {
out.SecurityHeaders = *in.SecurityHeaders
}
if in.BasicAuth != nil {
if in.BasicAuth.Enabled != nil {
if out.BasicAuth == nil {
out.BasicAuth = &BasicAuthConfig{}
}
out.BasicAuth.Enabled = *in.BasicAuth.Enabled
}
if in.BasicAuth.Username != nil {
if out.BasicAuth == nil {
out.BasicAuth = &BasicAuthConfig{}
}
out.BasicAuth.Username = strings.TrimSpace(*in.BasicAuth.Username)
}
if in.BasicAuth.Password != nil && strings.TrimSpace(*in.BasicAuth.Password) != "" {
if out.BasicAuth == nil {
out.BasicAuth = &BasicAuthConfig{}
}
hash, err := validate.BasicAuthPasswordHash(out.BasicAuth.Username, *in.BasicAuth.Password)
if err != nil {
return SecurityConfig{}, err
}
out.BasicAuth.PasswordHash = hash
}
}
return out, ValidateSecurity(out)
}
func ValidateSecurity(cfg SecurityConfig) error {
cfg = cfg.Normalize()
if err := validate.IPOrCIDRList(cfg.IPBlacklist); err != nil {
return fmt.Errorf("IP 黑名单:%w", err)
}
if err := validate.IPOrCIDRList(cfg.IPWhitelist); err != nil {
return fmt.Errorf("IP 白名单:%w", err)
}
if cfg.IPWhitelistMode && len(cfg.IPWhitelist) == 0 {
return fmt.Errorf("启用白名单模式时至少添加一条 IP 或 CIDR")
}
if cfg.BasicAuth != nil && cfg.BasicAuth.Enabled {
if err := validate.Username(cfg.BasicAuth.Username); err != nil {
return fmt.Errorf("Basic Auth:%w", err)
}
if cfg.BasicAuth.PasswordHash == "" {
return fmt.Errorf("Basic Auth 需要设置密码")
}
}
if cfg.RateLimit != nil && cfg.RateLimit.Enabled {
if cfg.RateLimit.Rate < 1 || cfg.RateLimit.Rate > 10000 {
return fmt.Errorf("限流速率无效(1-10000 次/秒)")
}
if cfg.RateLimit.Burst < 1 || cfg.RateLimit.Burst > 100000 {
return fmt.Errorf("限流突发值无效(1-100000)")
}
}
if cfg.ConnLimit != nil && cfg.ConnLimit.Enabled {
if cfg.ConnLimit.Max < 1 || cfg.ConnLimit.Max > 10000 {
return fmt.Errorf("连接数限制无效(1-10000)")
}
}
return nil
}
func ParseIPListText(text string) []string {
lines := strings.Split(text, "\n")
out := make([]string, 0, len(lines))
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
out = append(out, line)
}
return out
}
func FormatIPListText(list []string) string {
return strings.Join(list, "\n")
}