feat: implement geo-filtering with whitelist and blacklist support

- Added support for geo-filtering in the proxy pool, allowing configuration of allowed and blocked countries via environment variables.
- Updated `.env.example` and `docker-compose.yml` to include `ALLOWED_COUNTRIES` for whitelist functionality.
- Enhanced `CLAUDE.md`, `GEO_FILTER.md`, and `README.md` to document the new geo-filtering features and usage instructions.
- Modified proxy validation logic to prioritize whitelist over blacklist during admission checks.
- Improved WebUI to allow dynamic configuration of geo-filter settings.
This commit is contained in:
isboyjc
2026-04-01 21:45:09 +08:00
parent dfe71d0390
commit a06be637e7
14 changed files with 307 additions and 65 deletions

View File

@@ -379,6 +379,22 @@ tr:hover{background:var(--gray-2);box-shadow:inset 0 0 20px rgba(0,255,65,0.05)}
</div>
</div>
<div class="form-section">
<div class="form-section-title" data-i18n="config.section_geo_filter">地理过滤</div>
<div class="form-grid">
<div class="form-group">
<label data-i18n="config.allowed_countries">允许国家(白名单)</label>
<input type="text" id="cfg-allowed-countries" placeholder="US,JP,KR,SG">
<div class="form-help" data-i18n="config.allowed_countries_help">非空时仅允许这些国家入池,忽略黑名单</div>
</div>
<div class="form-group">
<label data-i18n="config.blocked_countries">屏蔽国家(黑名单)</label>
<input type="text" id="cfg-blocked-countries" placeholder="CN,RU,KP">
<div class="form-help" data-i18n="config.blocked_countries_help">白名单为空时生效</div>
</div>
</div>
</div>
<div class="modal-actions">
<button class="btn btn-secondary" onclick="closeSettings()" data-i18n="config.cancel">取消</button>
<button class="btn" onclick="saveConfig()" data-i18n="config.save">保存配置</button>
@@ -453,6 +469,11 @@ const i18n = {
'config.optimize_interval': '优化间隔(分钟)',
'config.replace_threshold': '替换阈值',
'config.replace_threshold_help': '新代理需快30%',
'config.section_geo_filter': '地理过滤',
'config.allowed_countries': '允许国家(白名单)',
'config.allowed_countries_help': '非空时仅允许这些国家入池,忽略黑名单',
'config.blocked_countries': '屏蔽国家(黑名单)',
'config.blocked_countries_help': '白名单为空时生效',
'config.cancel': '取消',
'config.save': '保存配置',
'msg.fetch_confirm': '确定开始抓取代理吗?',
@@ -527,6 +548,11 @@ const i18n = {
'config.optimize_interval': 'Optimize Interval (min)',
'config.replace_threshold': 'Replace Threshold',
'config.replace_threshold_help': 'New proxy must be 30% faster',
'config.section_geo_filter': 'Geo Filter',
'config.allowed_countries': 'Allowed Countries (Whitelist)',
'config.allowed_countries_help': 'When set, only these countries are allowed; blacklist is ignored',
'config.blocked_countries': 'Blocked Countries (Blacklist)',
'config.blocked_countries_help': 'Effective only when whitelist is empty',
'config.cancel': 'Cancel',
'config.save': 'Save Configuration',
'msg.fetch_confirm': 'Start proxy fetch?',
@@ -877,6 +903,8 @@ async function openSettings() {
document.getElementById('cfg-health-batch').value = cfg.health_check_batch_size;
document.getElementById('cfg-optimize-interval').value = cfg.optimize_interval;
document.getElementById('cfg-replace-threshold').value = cfg.replace_threshold;
document.getElementById('cfg-blocked-countries').value = (cfg.blocked_countries || []).join(',');
document.getElementById('cfg-allowed-countries').value = (cfg.allowed_countries || []).join(',');
document.getElementById('settings-modal').classList.add('show');
}
@@ -899,6 +927,8 @@ async function saveConfig() {
health_check_batch_size: parseInt(document.getElementById('cfg-health-batch').value),
optimize_interval: parseInt(document.getElementById('cfg-optimize-interval').value),
replace_threshold: parseFloat(document.getElementById('cfg-replace-threshold').value),
blocked_countries: document.getElementById('cfg-blocked-countries').value.split(',').map(s => s.trim().toUpperCase()).filter(s => s),
allowed_countries: document.getElementById('cfg-allowed-countries').value.split(',').map(s => s.trim().toUpperCase()).filter(s => s),
};
const result = await api('/api/config/save', {

View File

@@ -336,23 +336,27 @@ func (s *Server) apiConfig(w http.ResponseWriter, r *http.Request) {
"pool_min_per_protocol": cfg.PoolMinPerProtocol,
"pool_http_slots": httpSlots,
"pool_socks5_slots": socks5Slots,
// 延迟配置
"max_latency_ms": cfg.MaxLatencyMs,
"max_latency_emergency": cfg.MaxLatencyEmergency,
"max_latency_healthy": cfg.MaxLatencyHealthy,
// 验证配置
"validate_concurrency": cfg.ValidateConcurrency,
"validate_timeout": cfg.ValidateTimeout,
// 健康检查配置
"health_check_interval": cfg.HealthCheckInterval,
"health_check_batch_size": cfg.HealthCheckBatchSize,
// 优化配置
"optimize_interval": cfg.OptimizeInterval,
"replace_threshold": cfg.ReplaceThreshold,
// 地理过滤配置
"blocked_countries": cfg.BlockedCountries,
"allowed_countries": cfg.AllowedCountries,
})
}
@@ -364,18 +368,20 @@ func (s *Server) apiConfigSave(w http.ResponseWriter, r *http.Request) {
}
var req struct {
PoolMaxSize int `json:"pool_max_size"`
PoolHTTPRatio float64 `json:"pool_http_ratio"`
PoolMinPerProtocol int `json:"pool_min_per_protocol"`
MaxLatencyMs int `json:"max_latency_ms"`
MaxLatencyEmergency int `json:"max_latency_emergency"`
MaxLatencyHealthy int `json:"max_latency_healthy"`
ValidateConcurrency int `json:"validate_concurrency"`
ValidateTimeout int `json:"validate_timeout"`
HealthCheckInterval int `json:"health_check_interval"`
HealthCheckBatchSize int `json:"health_check_batch_size"`
OptimizeInterval int `json:"optimize_interval"`
ReplaceThreshold float64 `json:"replace_threshold"`
PoolMaxSize int `json:"pool_max_size"`
PoolHTTPRatio float64 `json:"pool_http_ratio"`
PoolMinPerProtocol int `json:"pool_min_per_protocol"`
MaxLatencyMs int `json:"max_latency_ms"`
MaxLatencyEmergency int `json:"max_latency_emergency"`
MaxLatencyHealthy int `json:"max_latency_healthy"`
ValidateConcurrency int `json:"validate_concurrency"`
ValidateTimeout int `json:"validate_timeout"`
HealthCheckInterval int `json:"health_check_interval"`
HealthCheckBatchSize int `json:"health_check_batch_size"`
OptimizeInterval int `json:"optimize_interval"`
ReplaceThreshold float64 `json:"replace_threshold"`
BlockedCountries []string `json:"blocked_countries"`
AllowedCountries []string `json:"allowed_countries"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
@@ -408,6 +414,8 @@ func (s *Server) apiConfigSave(w http.ResponseWriter, r *http.Request) {
newCfg.HealthCheckBatchSize = req.HealthCheckBatchSize
newCfg.OptimizeInterval = req.OptimizeInterval
newCfg.ReplaceThreshold = req.ReplaceThreshold
newCfg.BlockedCountries = req.BlockedCountries
newCfg.AllowedCountries = req.AllowedCountries
if err := config.Save(&newCfg); err != nil {
jsonError(w, "save config error: "+err.Error(), http.StatusInternalServerError)