diff --git a/frontend/src/pages/settings/SubscriptionFormatsTab.tsx b/frontend/src/pages/settings/SubscriptionFormatsTab.tsx index 8d96aef14..935e27577 100644 --- a/frontend/src/pages/settings/SubscriptionFormatsTab.tsx +++ b/frontend/src/pages/settings/SubscriptionFormatsTab.tsx @@ -8,6 +8,7 @@ import { RocketOutlined, SendOutlined, SettingOutlined, + StopOutlined, } from '@ant-design/icons'; import type { AllSetting } from '@/models/setting'; import { onNumber } from '@/utils/onNumber'; @@ -31,10 +32,17 @@ const DEFAULT_MUX = { xudpConcurrency: 16, xudpProxyUDP443: 'reject', }; -const DEFAULT_RULES: { type: string; outboundTag: string; domain?: string[]; ip?: string[] }[] = [ + +type SubJsonRule = { type: string; outboundTag: string; domain?: string[]; ip?: string[] }; + +const DEFAULT_DIRECT_RULES: SubJsonRule[] = [ { type: 'field', outboundTag: 'direct', domain: ['geosite:category-ir'] }, { type: 'field', outboundTag: 'direct', ip: ['geoip:private', 'geoip:ir'] }, ]; +const DEFAULT_BLOCK_RULES: SubJsonRule[] = [ + { type: 'field', outboundTag: 'block', domain: ['geosite:category-ads-all'] }, +]; +const BLOCK_IP_RULE: SubJsonRule = { type: 'field', outboundTag: 'block', ip: [] }; const directIPsOptions = [ { label: 'Private IP', value: 'geoip:private' }, @@ -57,6 +65,10 @@ const directDomainsOptions = [ { label: 'Meta', value: 'geosite:meta' }, { label: 'Google', value: 'geosite:google' }, ]; +const blockDomainsOptions = [ + { label: 'Ads All', value: 'geosite:category-ads-all' }, + { label: 'Adult +18', value: 'geosite:category-porn' }, +]; function readJson(raw: string, fallback: T): T { try { @@ -67,6 +79,11 @@ function readJson(raw: string, fallback: T): T { } } +function readRules(raw: string): SubJsonRule[] { + const parsed = readJson(raw, null); + return Array.isArray(parsed) ? (parsed as SubJsonRule[]) : []; +} + export default function SubscriptionFormatsTab({ allSetting, updateSetting, @@ -75,7 +92,6 @@ export default function SubscriptionFormatsTab({ const { isMobile } = useMediaQuery(); const muxEnabled = allSetting.subJsonMux !== ''; - const directEnabled = allSetting.subJsonRules !== ''; const muxObj = useMemo( () => @@ -92,57 +108,48 @@ export default function SubscriptionFormatsTab({ updateSetting({ subJsonMux: JSON.stringify(next) }); } - const ruleArray = useMemo(() => { - if (!directEnabled) return null; - return readJson(allSetting.subJsonRules, null); - }, [allSetting.subJsonRules, directEnabled]); + const ruleArray = useMemo(() => readRules(allSetting.subJsonRules), [allSetting.subJsonRules]); + const directEnabled = ruleArray.some((r) => r.outboundTag === 'direct'); + const blockEnabled = ruleArray.some((r) => r.outboundTag === 'block'); - const directIPs = useMemo(() => { - if (!ruleArray) return []; - const ipRule = ruleArray.find((r) => r.ip); - return ipRule?.ip ?? []; - }, [ruleArray]); + const ruleValues = (tag: string, key: 'ip' | 'domain') => + ruleArray.find((r) => r.outboundTag === tag && r[key])?.[key] ?? []; - const directDomains = useMemo(() => { - if (!ruleArray) return []; - const dRule = ruleArray.find((r) => r.domain); - return dRule?.domain ?? []; - }, [ruleArray]); - - function setDirectEnabled(v: boolean) { - updateSetting({ subJsonRules: v ? JSON.stringify(DEFAULT_RULES) : '' }); + function writeRules(rules: SubJsonRule[]) { + updateSetting({ subJsonRules: rules.length > 0 ? JSON.stringify(rules) : '' }); } - function setDirectIPs(value: string[]) { - if (!ruleArray) return; - let rules = [...ruleArray]; - if (value.length === 0) { - rules = rules.filter((r) => !r.ip); - } else { - let idx = rules.findIndex((r) => r.ip); - if (idx === -1) { - rules.push({ ...DEFAULT_RULES[1] }); - idx = rules.length - 1; - } - rules[idx] = { ...rules[idx], ip: [...value] }; + function setTagEnabled(tag: string, defaults: SubJsonRule[], enabled: boolean) { + const rest = ruleArray.filter((r) => r.outboundTag !== tag); + if (!enabled) { + // Turning off the last managed tag also drops foreign-tag leftovers so + // the panel still has a path back to an empty subJsonRules. + const hasManaged = rest.some((r) => r.outboundTag === 'direct' || r.outboundTag === 'block'); + writeRules(hasManaged ? rest : []); + return; } - updateSetting({ subJsonRules: JSON.stringify(rules) }); + // Prepend block defaults so ads match before direct; never re-sort the rest. + writeRules(tag === 'block' ? [...defaults, ...rest] : [...rest, ...defaults]); } - function setDirectDomains(value: string[]) { - if (!ruleArray) return; + function setRuleValues( + tag: string, + key: 'ip' | 'domain', + template: SubJsonRule, + value: string[], + ) { let rules = [...ruleArray]; if (value.length === 0) { - rules = rules.filter((r) => !r.domain); + rules = rules.filter((r) => !(r.outboundTag === tag && r[key])); } else { - let idx = rules.findIndex((r) => r.domain); - if (idx === -1) { - rules.push({ ...DEFAULT_RULES[0] }); - idx = rules.length - 1; + let index = rules.findIndex((r) => r.outboundTag === tag && r[key]); + if (index === -1) { + rules.push({ ...template }); + index = rules.length - 1; } - rules[idx] = { ...rules[idx], domain: [...value] }; + rules[index] = { ...rules[index], [key]: [...value] }; } - updateSetting({ subJsonRules: JSON.stringify(rules) }); + writeRules(rules); } return ( @@ -385,16 +392,19 @@ export default function SubscriptionFormatsTab({ title={t('pages.settings.direct')} description={t('pages.settings.directDesc')} > - + setTagEnabled('direct', DEFAULT_DIRECT_RULES, v)} + /> {directEnabled && (
{t('pages.settings.direct')} IPs}> + setRuleValues('direct', 'domain', DEFAULT_DIRECT_RULES[0], v) + } options={directDomainsOptions} /> @@ -419,6 +431,46 @@ export default function SubscriptionFormatsTab({ ), }, + { + key: '5', + label: catTabLabel(, t('pages.settings.block'), isMobile), + children: ( + <> + + setTagEnabled('block', DEFAULT_BLOCK_RULES, v)} + /> + + {blockEnabled && ( +
+ + setRuleValues('block', 'ip', BLOCK_IP_RULE, v)} + options={directIPsOptions} + /> + +
+ )} + + ), + }, ]} /> ); diff --git a/internal/web/translation/ar-EG.json b/internal/web/translation/ar-EG.json index cb08f38ac..f4130adc1 100644 --- a/internal/web/translation/ar-EG.json +++ b/internal/web/translation/ar-EG.json @@ -1293,6 +1293,8 @@ "muxSett": "إعدادات MUX", "direct": "اتصال مباشر", "directDesc": "ينشئ اتصال مباشر مع الدومينات أو نطاقات IP لدولة معينة.", + "block": "حظر الاتصال", + "blockDesc": "يحظر الاتصالات بالنطاقات أو نطاقات IP المحددة باستخدام مسار blackhole.", "notifications": "الإشعارات", "certs": "الشهادات", "externalTraffic": "الترافيك الخارجي", diff --git a/internal/web/translation/en-US.json b/internal/web/translation/en-US.json index 902b3104a..b4aedfa87 100644 --- a/internal/web/translation/en-US.json +++ b/internal/web/translation/en-US.json @@ -1415,6 +1415,8 @@ "muxSett": "Mux Settings", "direct": "Direct Connection", "directDesc": "Directly establishes connections with domains or IP ranges of a specific country.", + "block": "Block Connection", + "blockDesc": "Block connections to selected domains or IP ranges using the blackhole outbound.", "notifications": "Notifications", "certs": "Certificates", "externalTraffic": "External Traffic", diff --git a/internal/web/translation/es-ES.json b/internal/web/translation/es-ES.json index 51af14c25..d86cc7b5a 100644 --- a/internal/web/translation/es-ES.json +++ b/internal/web/translation/es-ES.json @@ -1293,6 +1293,8 @@ "muxSett": "Configuración Mux", "direct": "Conexión Directa", "directDesc": "Establece conexiones directas con dominios o rangos de IP de un país específico.", + "block": "Bloquear conexión", + "blockDesc": "Bloquea conexiones a dominios o rangos de IP seleccionados usando el outbound blackhole.", "notifications": "Notificaciones", "certs": "Certificados", "externalTraffic": "Tráfico Externo", diff --git a/internal/web/translation/fa-IR.json b/internal/web/translation/fa-IR.json index d8e78ad07..c2f693588 100644 --- a/internal/web/translation/fa-IR.json +++ b/internal/web/translation/fa-IR.json @@ -1297,6 +1297,8 @@ "muxSett": "تنظیمات ماکس", "direct": "اتصال مستقیم", "directDesc": "به طور مستقیم با دامنه ها یا محدوده آی‌پی یک کشور خاص ارتباط برقرار می کند", + "block": "مسدود کردن اتصال", + "blockDesc": "اتصالات به دامنه‌ها یا محدوده‌های IP انتخاب‌شده را با outbound بلک‌هول مسدود می‌کند.", "notifications": "اعلان‌ها", "certs": "گواهی‌ها", "externalTraffic": "ترافیک خارجی", diff --git a/internal/web/translation/id-ID.json b/internal/web/translation/id-ID.json index 8776c6df1..b76b75204 100644 --- a/internal/web/translation/id-ID.json +++ b/internal/web/translation/id-ID.json @@ -1293,6 +1293,8 @@ "muxSett": "Pengaturan Mux", "direct": "Koneksi langsung", "directDesc": "Secara langsung membuat koneksi dengan domain atau rentang IP negara tertentu.", + "block": "Blokir Koneksi", + "blockDesc": "Memblokir koneksi ke domain atau rentang IP yang dipilih menggunakan outbound blackhole.", "notifications": "Notifikasi", "certs": "Sertifikat", "externalTraffic": "Lalu Lintas Eksternal", diff --git a/internal/web/translation/ja-JP.json b/internal/web/translation/ja-JP.json index 88bfa681c..49bd79c6a 100644 --- a/internal/web/translation/ja-JP.json +++ b/internal/web/translation/ja-JP.json @@ -1293,6 +1293,8 @@ "muxSett": "マルチプレクサ設定", "direct": "直接接続", "directDesc": "特定の国のドメインまたはIP範囲に直接接続する", + "block": "接続をブロック", + "blockDesc": "blackholeアウトバウンドを使用して、選択したドメインまたはIP範囲への接続をブロックします。", "notifications": "通知", "certs": "証明書", "externalTraffic": "外部トラフィック", diff --git a/internal/web/translation/pt-BR.json b/internal/web/translation/pt-BR.json index 6020d6c85..dee36629a 100644 --- a/internal/web/translation/pt-BR.json +++ b/internal/web/translation/pt-BR.json @@ -1293,6 +1293,8 @@ "muxSett": "Configurações de Mux", "direct": "Conexão Direta", "directDesc": "Estabelece conexões diretamente com domínios ou intervalos de IP de um país específico.", + "block": "Bloquear conexão", + "blockDesc": "Bloqueia conexões para domínios ou intervalos de IP selecionados usando o outbound blackhole.", "notifications": "Notificações", "certs": "Certificados", "externalTraffic": "Tráfego Externo", diff --git a/internal/web/translation/ru-RU.json b/internal/web/translation/ru-RU.json index c37421c14..7d613f138 100644 --- a/internal/web/translation/ru-RU.json +++ b/internal/web/translation/ru-RU.json @@ -1293,6 +1293,8 @@ "muxSett": "Настройки Mux", "direct": "Прямое подключение", "directDesc": "Устанавливает прямые соединения с доменами или IP-адресами определённой страны.", + "block": "Блокировка соединений", + "blockDesc": "Блокирует соединения с выбранными доменами или диапазонами IP через outbound blackhole.", "notifications": "Уведомления", "certs": "Сертификаты", "externalTraffic": "Внешний трафик", diff --git a/internal/web/translation/tr-TR.json b/internal/web/translation/tr-TR.json index cb21e49ab..eb25e0eef 100644 --- a/internal/web/translation/tr-TR.json +++ b/internal/web/translation/tr-TR.json @@ -1293,6 +1293,8 @@ "muxSett": "Mux Ayarları", "direct": "Doğrudan Bağlantı", "directDesc": "Belirli bir ülkenin alan adları veya IP aralıkları ile doğrudan bağlantı kurar.", + "block": "Bağlantıyı Engelle", + "blockDesc": "Seçilen alan adlarına veya IP aralıklarına giden bağlantıları blackhole outbound ile engeller.", "notifications": "Bildirimler", "certs": "Sertifikalar", "externalTraffic": "Harici Trafik", diff --git a/internal/web/translation/uk-UA.json b/internal/web/translation/uk-UA.json index 6e2170470..687327810 100644 --- a/internal/web/translation/uk-UA.json +++ b/internal/web/translation/uk-UA.json @@ -1293,6 +1293,8 @@ "muxSett": "Налаштування Mux", "direct": "Пряме підключення", "directDesc": "Безпосередньо встановлює з’єднання з доменами або діапазонами IP певної країни.", + "block": "Блокування з’єднань", + "blockDesc": "Блокує з’єднання з вибраними доменами або діапазонами IP через outbound blackhole.", "notifications": "Сповіщення", "certs": "Сертифікати", "externalTraffic": "Зовнішній трафік", diff --git a/internal/web/translation/vi-VN.json b/internal/web/translation/vi-VN.json index 691560ef9..fbbba2c6b 100644 --- a/internal/web/translation/vi-VN.json +++ b/internal/web/translation/vi-VN.json @@ -1293,6 +1293,8 @@ "muxSett": "Mux Cài đặt", "direct": "Kết nối trực tiếp", "directDesc": "Trực tiếp thiết lập kết nối với tên miền hoặc dải IP của một quốc gia cụ thể.", + "block": "Chặn kết nối", + "blockDesc": "Chặn kết nối tới các tên miền hoặc dải IP đã chọn bằng outbound blackhole.", "notifications": "Thông báo", "certs": "Chứng chỉ", "externalTraffic": "Lưu lượng bên ngoài", diff --git a/internal/web/translation/zh-CN.json b/internal/web/translation/zh-CN.json index 1562ffad2..b28d3c070 100644 --- a/internal/web/translation/zh-CN.json +++ b/internal/web/translation/zh-CN.json @@ -1293,6 +1293,8 @@ "muxSett": "复用器设置", "direct": "直接连接", "directDesc": "直接与特定国家的域或 IP 范围建立连接", + "block": "阻止连接", + "blockDesc": "使用 blackhole 出站阻止对所选域名或 IP 范围的连接。", "notifications": "通知", "certs": "证书", "externalTraffic": "外部流量", diff --git a/internal/web/translation/zh-TW.json b/internal/web/translation/zh-TW.json index fa38613a7..e54923b38 100644 --- a/internal/web/translation/zh-TW.json +++ b/internal/web/translation/zh-TW.json @@ -1293,6 +1293,8 @@ "muxSett": "複用器設定", "direct": "直接連線", "directDesc": "直接與特定國家的域或 IP 範圍建立連線", + "block": "封鎖連線", + "blockDesc": "使用 blackhole 出站封鎖對所選網域或 IP 範圍的連線。", "notifications": "通知", "certs": "證書", "externalTraffic": "外部流量",