Compare commits
6 Commits
38e0c100be
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 1c8d49a636 | |||
| 71b2b4e6e5 | |||
| 89b9a125c8 | |||
| 6b5480586a | |||
| 2e9aeba3c0 | |||
| 95230c6349 |
408
config-clash/solar/solar.yaml
Normal file
408
config-clash/solar/solar.yaml
Normal file
@@ -0,0 +1,408 @@
|
|||||||
|
anchors:
|
||||||
|
default-rule-provider-config: &default_rule_provider_config
|
||||||
|
type: http
|
||||||
|
behavior: classical
|
||||||
|
interval: 86400
|
||||||
|
|
||||||
|
# # ———————————————————————————————— health checks ———————————————————————————————— #
|
||||||
|
proxy_provider_substore: &proxy_provider_substore
|
||||||
|
type: http
|
||||||
|
interval: 3600
|
||||||
|
proxy: DIRECT
|
||||||
|
|
||||||
|
# # ———————————————————————————————— health checks ———————————————————————————————— #
|
||||||
|
health-check-providers: &health_check_providers
|
||||||
|
health-check:
|
||||||
|
enable: true
|
||||||
|
interval: 1200
|
||||||
|
expected-status: 204
|
||||||
|
timeout: 5000
|
||||||
|
url: https://www.gstatic.com/generate_204
|
||||||
|
|
||||||
|
health-check-groups: &health_check_groups
|
||||||
|
health-check:
|
||||||
|
enable: true
|
||||||
|
interval: 600
|
||||||
|
expected-status: 204
|
||||||
|
timeout: 5000
|
||||||
|
url: https://www.gstatic.com/generate_204
|
||||||
|
|
||||||
|
# # ————————————————————————————————— proxy lists ————————————————————————————————— #
|
||||||
|
use-all: &use_all
|
||||||
|
use:
|
||||||
|
- 🐦 fallback package
|
||||||
|
- 🚪 local tunnels
|
||||||
|
- 🫂 neighborhood tunnels
|
||||||
|
- 📺 youtube tunnels
|
||||||
|
- 🕊️ clear tunnels
|
||||||
|
- 🪨 default package / 📺
|
||||||
|
- 🪨 default package / 👠
|
||||||
|
- 🪨 default package
|
||||||
|
- 💎 premium package / 📺
|
||||||
|
- 💎 premium package / 👠
|
||||||
|
- 💎 premium package
|
||||||
|
- 🌉 private relays
|
||||||
|
- ♨️ private vpns
|
||||||
|
|
||||||
|
# # ————————————————————————————————— proxy types ————————————————————————————————— #
|
||||||
|
p-selector-udp: &p_selector_udp
|
||||||
|
type: select
|
||||||
|
disable-udp: false
|
||||||
|
|
||||||
|
# ————————————————————————————————————————————————————— LOCAL PROXY —————————————————————————————————————————————————————vs
|
||||||
|
port: 7890
|
||||||
|
socks-port: 7891
|
||||||
|
redir-port: 7892
|
||||||
|
tproxy-port: 7893
|
||||||
|
mixed-port: 7894
|
||||||
|
|
||||||
|
allow-lan: true
|
||||||
|
lan-allowed-ips:
|
||||||
|
- 0.0.0.0/0
|
||||||
|
bind-address: "*"
|
||||||
|
|
||||||
|
# ————————————————————————————————————————————————— EXTERNAL CONTROLLER —————————————————————————————————————————————————
|
||||||
|
|
||||||
|
external-controller: 0.0.0.0:9090
|
||||||
|
external-controller-tls: 0.0.0.0:9443
|
||||||
|
secret: '314159271828'
|
||||||
|
external-ui: ./ui
|
||||||
|
external-ui-name: zashboard
|
||||||
|
external-ui-url: "https://github.com/Zephyruso/zashboard/releases/latest/download/dist-cdn-fonts.zip"
|
||||||
|
|
||||||
|
# ——————————————————————————————————————————————————————— GENERAL ———————————————————————————————————————————————————————
|
||||||
|
mode: rule
|
||||||
|
ipv6: false
|
||||||
|
unified-delay: true
|
||||||
|
log-level: info
|
||||||
|
disable-keep-alive: false
|
||||||
|
keep-alive-interval: 15
|
||||||
|
keep-alive-idle: 600
|
||||||
|
find-process-mode: "off" # Options: always, strict, off
|
||||||
|
interface-name: eth0 # Outbound interface name
|
||||||
|
routing-mark: 1337
|
||||||
|
# global-client-fingerprint: random # Options: chrome, firefox, safari, iOS, android, edge, 360, qq, random
|
||||||
|
# tcp-concurrent: true # Enable TCP concurrent connections, which will use all IP addresses resolved by DNS for connections, using the first successful connection.
|
||||||
|
|
||||||
|
# ————————————————— GEO DATA CONFIGURATION ————————————————— https://github.com/runetfreedom/russia-v2ray-rules-dat —————
|
||||||
|
geodata-mode: true
|
||||||
|
geodata-loader: standard
|
||||||
|
geo-auto-update: true
|
||||||
|
geo-update-interval: 24
|
||||||
|
geox-url:
|
||||||
|
geoip: https://raw.githubusercontent.com/runetfreedom/russia-v2ray-rules-dat/release/geoip.dat
|
||||||
|
geosite: https://raw.githubusercontent.com/runetfreedom/russia-v2ray-rules-dat/release/geosite.dat
|
||||||
|
mmdb: https://testingcf.jsdelivr.net/gh/alecthw/mmdb_china_ip_list@release/Country.mmdb
|
||||||
|
asn: https://testingcf.jsdelivr.net/gh/xishang0128/geoip@release/GeoLite2-ASN.mmdb
|
||||||
|
global-ua: clash.meta
|
||||||
|
etag-support: true
|
||||||
|
|
||||||
|
listeners:
|
||||||
|
- name: socks-inbound
|
||||||
|
type: socks
|
||||||
|
port: 7891
|
||||||
|
listen: 0.0.0.0
|
||||||
|
udp: true
|
||||||
|
users:
|
||||||
|
- username: testuser1
|
||||||
|
password: testuser1
|
||||||
|
- username: testuser2
|
||||||
|
password: testuser2
|
||||||
|
- username: testuser3
|
||||||
|
password: testuser3
|
||||||
|
- username: testuser4
|
||||||
|
password: testuser4
|
||||||
|
|
||||||
|
sniffer:
|
||||||
|
enable: true
|
||||||
|
parse-pure-ip: true
|
||||||
|
override-destination: true
|
||||||
|
sniff:
|
||||||
|
HTTP:
|
||||||
|
ports: [80, 8080-8880]
|
||||||
|
override-destination: true
|
||||||
|
TLS:
|
||||||
|
ports: [443, 8443]
|
||||||
|
QUIC:
|
||||||
|
ports: [443, 8443]
|
||||||
|
skip-domain:
|
||||||
|
- '+.dts'
|
||||||
|
- '+.webway.dts'
|
||||||
|
- '+.netbird.selfhosted'
|
||||||
|
- '+.shamanlanding.org'
|
||||||
|
- '+.shamanlanding.com'
|
||||||
|
|
||||||
|
- "Mijia Cloud" # Xiaomi Smart Home (Mijia). Uses non-standard TLS headers.
|
||||||
|
- "dlg.io.mi.com" # Xiaomi IoT logging/telemetry.
|
||||||
|
- "+.push.apple.com" # Apple Push Notification Service (APNS). Critical for iOS.
|
||||||
|
- "+.apple.com" # (Optional) Broader Apple bypass. Safer for iCloud sync.
|
||||||
|
|
||||||
|
dns:
|
||||||
|
enable: true
|
||||||
|
enhanced-mode: fake-ip
|
||||||
|
cache-algorithm: arc
|
||||||
|
ipv6: false
|
||||||
|
listen: 0.0.0.0:53
|
||||||
|
prefer-h3: false
|
||||||
|
respect-rules: true
|
||||||
|
use-hosts: true
|
||||||
|
use-system-hosts: false
|
||||||
|
|
||||||
|
fake-ip-range: 198.18.0.1/16
|
||||||
|
fake-ip-filter-mode: blacklist
|
||||||
|
fake-ip-filter:
|
||||||
|
# ———————————————————— self-hosted domains ———————————————————
|
||||||
|
- '*.lan'
|
||||||
|
- '*.local'
|
||||||
|
- '+.dts'
|
||||||
|
- '+.webway.dts'
|
||||||
|
- '+.netbird.selfhosted'
|
||||||
|
- '+.shamanlanding.org'
|
||||||
|
# ————————————————————————— ru domains ———————————————————————
|
||||||
|
- '+.ru'
|
||||||
|
- '+.рф'
|
||||||
|
- '+.su'
|
||||||
|
- '+.ntp.org'
|
||||||
|
- '+.pool.ntp.org'
|
||||||
|
- 'time.apple.com'
|
||||||
|
- 'time.nist.gov'
|
||||||
|
- 'time.windows.com'
|
||||||
|
- 'time.google.com'
|
||||||
|
# ————————————————————— connectivity checks ——————————————————
|
||||||
|
- 'dns.msftncsi.com'
|
||||||
|
- 'www.msftncsi.com'
|
||||||
|
- 'www.msftconnecttest.com'
|
||||||
|
- 'connectivitycheck.gstatic.com'
|
||||||
|
- 'connectivitycheck.android.com'
|
||||||
|
- 'clients3.google.com'
|
||||||
|
- 'captive.apple.com'
|
||||||
|
- '+.hotspot.msn.com'
|
||||||
|
default-nameserver: # Resolving the domain names of DNS servers.
|
||||||
|
- 1.1.1.1
|
||||||
|
- 1.0.0.1
|
||||||
|
- 8.8.8.8
|
||||||
|
- 8.8.4.4
|
||||||
|
- 9.9.9.9
|
||||||
|
- 208.67.222.222
|
||||||
|
- 208.67.220.220
|
||||||
|
nameserver: # Default domain name resolution server.
|
||||||
|
- 'tls://kavanah.shamanlanding.org'
|
||||||
|
# - https://d.adguard-dns.com/dns-query/5ffb7de2
|
||||||
|
proxy-server-nameserver: # Resolving the domain names of proxy nodes.
|
||||||
|
- 'tls://kavanah.shamanlanding.org'
|
||||||
|
# - https://d.adguard-dns.com/dns-query/5ffb7de2
|
||||||
|
|
||||||
|
hosts:
|
||||||
|
# 'solar.shamanlanding.org': 192.168.25.8
|
||||||
|
#
|
||||||
|
# 'battlescribe.shamanlanding.org': 192.168.25.8
|
||||||
|
# 'kavanah.shamanlanding.org': 192.168.25.8
|
||||||
|
# 'loremaster.shamanlanding.org': 192.168.25.8
|
||||||
|
# 'omnissiah.shamanlanding.org': 192.168.25.8
|
||||||
|
# 'sanctum.shamanlanding.org': 192.168.25.8
|
||||||
|
# 'tesseract.shamanlanding.org': 192.168.25.8
|
||||||
|
# 'synaxis.shamanlanding.org': 192.168.25.8
|
||||||
|
#
|
||||||
|
# '+.solar.shamanlanding.org': 192.168.25.8
|
||||||
|
|
||||||
|
proxy-providers:
|
||||||
|
🐦 fallback package:
|
||||||
|
url: "https://synaxis.shamanlanding.org/webway-subscription-provider/download/collection/fallback"
|
||||||
|
path: "./proxy_provider/fallback.txt"
|
||||||
|
<<: [*health_check_providers, *proxy_provider_substore]
|
||||||
|
|
||||||
|
# ———————————————————————————————— tunnels ———————————————————————————————— #
|
||||||
|
🚪 local tunnels:
|
||||||
|
url: "https://synaxis.shamanlanding.org/webway-subscription-provider/download/collection/own-package-solar"
|
||||||
|
path: "./proxy_provider/webway-local-tunnels.txt"
|
||||||
|
filter: "🚪"
|
||||||
|
exclude-filter: "✨"
|
||||||
|
<<: [*health_check_providers, *proxy_provider_substore]
|
||||||
|
|
||||||
|
🫂 neighborhood tunnels:
|
||||||
|
url: "https://synaxis.shamanlanding.org/webway-subscription-provider/download/collection/own-package-solar"
|
||||||
|
path: "./proxy_provider/webway-neighborhood-tunnels.txt"
|
||||||
|
filter: "🫂"
|
||||||
|
exclude-filter: "✨"
|
||||||
|
<<: [*health_check_providers, *proxy_provider_substore]
|
||||||
|
|
||||||
|
📺 youtube tunnels:
|
||||||
|
url: "https://synaxis.shamanlanding.org/webway-subscription-provider/download/collection/own-package-solar"
|
||||||
|
path: "./proxy_provider/webway-tunnels-youtube.txt"
|
||||||
|
filter: "📺"
|
||||||
|
exclude-filter: "✨"
|
||||||
|
<<: [*health_check_providers, *proxy_provider_substore]
|
||||||
|
|
||||||
|
🕊️ clear tunnels:
|
||||||
|
url: "https://synaxis.shamanlanding.org/webway-subscription-provider/download/collection/own-package-solar"
|
||||||
|
path: "./proxy_provider/webway-tunnels-clear.txt"
|
||||||
|
filter: "🕊️"
|
||||||
|
exclude-filter: "✨"
|
||||||
|
<<: [*health_check_providers, *proxy_provider_substore]
|
||||||
|
|
||||||
|
# ———————————————————————————————— левые впнки ———————————————————————————————— #
|
||||||
|
🪨 default package:
|
||||||
|
url: "https://synaxis.shamanlanding.org/webway-subscription-provider/download/collection/ext-package-solar"
|
||||||
|
path: "./proxy_provider/webway-class-b.txt"
|
||||||
|
exclude-filter: "📺|👠"
|
||||||
|
<<: [*health_check_providers, *proxy_provider_substore]
|
||||||
|
|
||||||
|
🪨 default package / 📺:
|
||||||
|
url: "https://synaxis.shamanlanding.org/webway-subscription-provider/download/collection/ext-package-solar"
|
||||||
|
path: "./proxy_provider/webway-class-b-youtube.txt"
|
||||||
|
filter: "📺"
|
||||||
|
<<: [*health_check_providers, *proxy_provider_substore]
|
||||||
|
|
||||||
|
🪨 default package / 👠:
|
||||||
|
url: "https://synaxis.shamanlanding.org/webway-subscription-provider/download/collection/ext-package-solar"
|
||||||
|
path: "./proxy_provider/webway-class-b-capri.txt"
|
||||||
|
filter: "👠"
|
||||||
|
<<: [*health_check_providers, *proxy_provider_substore]
|
||||||
|
|
||||||
|
💎 premium package:
|
||||||
|
url: "https://synaxis.shamanlanding.org/webway-subscription-provider/download/collection/prm-package-solar"
|
||||||
|
path: "./proxy_provider/webway-class-a.txt"
|
||||||
|
exclude-filter: "📺|👠"
|
||||||
|
<<: [*health_check_providers, *proxy_provider_substore]
|
||||||
|
|
||||||
|
💎 premium package / 📺:
|
||||||
|
url: "https://synaxis.shamanlanding.org/webway-subscription-provider/download/collection/prm-package-solar"
|
||||||
|
path: "./proxy_provider/webway-class-a-youtube.txt"
|
||||||
|
filter: "📺"
|
||||||
|
<<: [*health_check_providers, *proxy_provider_substore]
|
||||||
|
|
||||||
|
💎 premium package / 👠:
|
||||||
|
url: "https://synaxis.shamanlanding.org/webway-subscription-provider/download/collection/prm-package-solar"
|
||||||
|
path: "./proxy_provider/webway-class-a-capri.txt"
|
||||||
|
filter: "👠"
|
||||||
|
<<: [*health_check_providers, *proxy_provider_substore]
|
||||||
|
|
||||||
|
# ———————————————————————————————— хорошие впнки ———————————————————————————————— #
|
||||||
|
♨️ private vpns:
|
||||||
|
url: "https://synaxis.shamanlanding.org/webway-subscription-provider/download/collection/own-package-solar"
|
||||||
|
path: "./proxy_provider/webway-private-vpns.txt"
|
||||||
|
filter: "♨️"
|
||||||
|
<<: [*health_check_providers, *proxy_provider_substore]
|
||||||
|
|
||||||
|
🌉 private relays:
|
||||||
|
url: "https://synaxis.shamanlanding.org/webway-subscription-provider/download/collection/own-package-solar"
|
||||||
|
path: "./proxy_provider/webway-private-relays.txt"
|
||||||
|
filter: "🌉"
|
||||||
|
<<: [*health_check_providers, *proxy_provider_substore]
|
||||||
|
|
||||||
|
proxy-groups:
|
||||||
|
- name: RU-зона локально
|
||||||
|
proxies:
|
||||||
|
- DIRECT
|
||||||
|
- PASS
|
||||||
|
- REJECT
|
||||||
|
- REJECT-DROP
|
||||||
|
<<: [*p_selector_udp]
|
||||||
|
- name: RU-зона через webway
|
||||||
|
proxies:
|
||||||
|
- REJECT
|
||||||
|
- REJECT-DROP
|
||||||
|
- DIRECT
|
||||||
|
- PASS
|
||||||
|
<<: [*p_selector_udp]
|
||||||
|
|
||||||
|
- name: Testzone A
|
||||||
|
filter: ""
|
||||||
|
exclude-filter: ""
|
||||||
|
exclude-type: ""
|
||||||
|
proxies:
|
||||||
|
- PASS
|
||||||
|
- Заблокированные сайты
|
||||||
|
- Личный список
|
||||||
|
<<: [*health_check_groups, *use_all, *p_selector_udp]
|
||||||
|
- name: Testzone B
|
||||||
|
filter: ""
|
||||||
|
exclude-filter: ""
|
||||||
|
exclude-type: ""
|
||||||
|
proxies:
|
||||||
|
- PASS
|
||||||
|
- Заблокированные сайты
|
||||||
|
- Личный список
|
||||||
|
<<: [*health_check_groups, *use_all, *p_selector_udp]
|
||||||
|
- name: Заблокированные сайты
|
||||||
|
filter: ""
|
||||||
|
exclude-filter: ""
|
||||||
|
exclude-type: ""
|
||||||
|
<<: [*health_check_groups, *use_all, *p_selector_udp]
|
||||||
|
- name: Личный список
|
||||||
|
filter: ""
|
||||||
|
exclude-filter: ""
|
||||||
|
exclude-type: ""
|
||||||
|
<<: [*health_check_groups, *use_all, *p_selector_udp]
|
||||||
|
|
||||||
|
rule-providers:
|
||||||
|
📃 Solar Proxy Domain List:
|
||||||
|
url: https://antifilter.solar.shamanlanding.org/proxy-domain.yaml
|
||||||
|
path: "./rule_provider/consolidated-lists-private/adaptation-solar-domain-proxy.yaml"
|
||||||
|
<<: *default_rule_provider_config
|
||||||
|
📃 Solar Proxy IP List:
|
||||||
|
url: https://antifilter.solar.shamanlanding.org/proxy-ip.yaml
|
||||||
|
path: "./rule_provider/consolidated-lists-private/adaptation-solar-ip-proxy.yaml"
|
||||||
|
<<: *default_rule_provider_config
|
||||||
|
📃 Shared Proxy Domain List:
|
||||||
|
url: https://antifilter.scarus.shamanlanding.org/proxy-domain.yaml
|
||||||
|
path: "./rule_provider/consolidated-lists-private/adaptation-scarus-domain-proxy.yaml"
|
||||||
|
<<: *default_rule_provider_config
|
||||||
|
📃 Shared Proxy IP List:
|
||||||
|
url: https://antifilter.scarus.shamanlanding.org/proxy-ip.yaml
|
||||||
|
path: "./rule_provider/consolidated-lists-private/adaptation-scarus-ip-proxy.yaml"
|
||||||
|
<<: *default_rule_provider_config
|
||||||
|
|
||||||
|
🛝 Testzone A:
|
||||||
|
url: https://gitea.shamanlanding.org/DaTekShaman/clash-rules/raw/branch/main/rule-provider/consolidated-lists-private/testzone-a.yaml
|
||||||
|
path: "./rule_provider/services/consolidated-lists-private/testzone-a.yaml"
|
||||||
|
<<: *default_rule_provider_config
|
||||||
|
🛝 Testzone B:
|
||||||
|
url: https://gitea.shamanlanding.org/DaTekShaman/clash-rules/raw/branch/main/rule-provider/consolidated-lists-private/testzone-b.yaml
|
||||||
|
path: "./rule_provider/services/consolidated-lists-private/testzone-b.yaml"
|
||||||
|
<<: *default_rule_provider_config
|
||||||
|
🛜 Webway Unprivileged:
|
||||||
|
url: https://gitea.shamanlanding.org/DaTekShaman/clash-rules/raw/branch/main/rule-provider/consolidated-lists-private/webway-unprivileged.yaml
|
||||||
|
path: "./rule_provider/services/consolidated-lists-private/webway-unprivileged.yaml"
|
||||||
|
<<: *default_rule_provider_config
|
||||||
|
🛜 VLAN10:
|
||||||
|
url: https://gitea.shamanlanding.org/DaTekShaman/clash-rules/raw/branch/main/rule-provider/consolidated-lists-private/vlan10.yaml
|
||||||
|
path: "./rule_provider/services/consolidated-lists-private/vlan10.yaml"
|
||||||
|
<<: *default_rule_provider_config
|
||||||
|
🛜 VLAN40:
|
||||||
|
url: https://gitea.shamanlanding.org/DaTekShaman/clash-rules/raw/branch/main/rule-provider/consolidated-lists-private/vlan40.yaml
|
||||||
|
path: "./rule_provider/services/consolidated-lists-private/vlan40.yaml"
|
||||||
|
<<: *default_rule_provider_config
|
||||||
|
|
||||||
|
👥 Current Antifilter/Refilter:
|
||||||
|
url: https://gitea.shamanlanding.org/DaTekShaman/clash-rules/raw/branch/main/rule-provider/consolidated-lists-public/current-public-set.yaml
|
||||||
|
path: "./rule_provider/consolidated-lists-public/current-public-set.yaml"
|
||||||
|
<<: *default_rule_provider_config
|
||||||
|
|
||||||
|
📦 RU Services Manual:
|
||||||
|
url: https://gitea.shamanlanding.org/DaTekShaman/clash-rules/raw/branch/main/rule-provider/consolidated-services/ru-services.yaml
|
||||||
|
path: "./rule_provider/consolidated-services/ru-services.yaml"
|
||||||
|
<<: *default_rule_provider_config
|
||||||
|
|
||||||
|
rules:
|
||||||
|
- SUB-RULE,(OR,((RULE-SET,📦 RU Services Manual),(GEOIP,RU),(GEOSITE,category-ru))),russian_internet
|
||||||
|
|
||||||
|
- RULE-SET,🛝 Testzone A,Testzone A
|
||||||
|
- RULE-SET,🛝 Testzone B,Testzone B
|
||||||
|
|
||||||
|
- RULE-SET,📃 Solar Proxy Domain List,Личный список
|
||||||
|
- RULE-SET,📃 Solar Proxy IP List,Личный список,no-resolve
|
||||||
|
|
||||||
|
- RULE-SET,📃 Shared Proxy Domain List,Заблокированные сайты
|
||||||
|
- RULE-SET,📃 Shared Proxy IP List,Заблокированные сайты,no-resolve
|
||||||
|
- RULE-SET,👥 Current Antifilter/Refilter,Заблокированные сайты
|
||||||
|
|
||||||
|
- MATCH,DIRECT
|
||||||
|
|
||||||
|
sub-rules:
|
||||||
|
russian_internet:
|
||||||
|
- DOMAIN-SUFFIX,shamanlanding.org,DIRECT
|
||||||
|
- SRC-IP-CIDR,100.98.0.0/16,RU-зона через webway
|
||||||
|
- SRC-IP-CIDR,10.10.0.0/16,RU-зона локально
|
||||||
|
- SRC-IP-CIDR,10.40.0.0/16,RU-зона локально
|
||||||
|
- MATCH,REJECT
|
||||||
@@ -1,233 +0,0 @@
|
|||||||
/**
|
|
||||||
* SUB STORE YAML ASSEMBLER (v4: Fix Duplicate Headers)
|
|
||||||
* * Arguments:
|
|
||||||
* - clear-comments=true
|
|
||||||
* - clear-manifest=true
|
|
||||||
* - clear-replacements=true
|
|
||||||
* * Requires: Header "# @file: filename.yaml" in input files.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// --- OPTIONS PARSING ---
|
|
||||||
function normalizeOptions() {
|
|
||||||
const args = (typeof $arguments !== "undefined" && $arguments) ? $arguments : {};
|
|
||||||
const asBool = (v, def = false) => {
|
|
||||||
if (v === undefined || v === null || v === "") return def;
|
|
||||||
if (typeof v === "boolean") return v;
|
|
||||||
const s = String(v).toLowerCase().trim();
|
|
||||||
if (["1", "true", "yes", "y", "on"].includes(s)) return true;
|
|
||||||
if (["0", "false", "no", "n", "off"].includes(s)) return false;
|
|
||||||
return def;
|
|
||||||
};
|
|
||||||
return {
|
|
||||||
clearComments: asBool(args['clear-comments'], false),
|
|
||||||
clearManifest: asBool(args['clear-manifest'], false),
|
|
||||||
clearReplacements: asBool(args['clear-replacements'], false),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- UTILS ---
|
|
||||||
function normalizeFiles(rawFiles) {
|
|
||||||
const map = new Map();
|
|
||||||
if (!rawFiles || !Array.isArray(rawFiles)) return map;
|
|
||||||
rawFiles.forEach((content) => {
|
|
||||||
if (typeof content !== 'string') return;
|
|
||||||
const match = content.match(/^#\s*@file:\s*(.+?)(\s|$)/m);
|
|
||||||
if (match) map.set(match[1].trim(), content);
|
|
||||||
});
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanText(text) {
|
|
||||||
return String(text || "").replace(/\r\n/g, "\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractName(block) {
|
|
||||||
const match = block.match(/^ {4}name:\s*(?:["']?)(.*?)(?:["']?)\s*(?:#.*)?$/m);
|
|
||||||
return match ? match[1].trim() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractKey(block, indentLevel) {
|
|
||||||
const spaceStr = " ".repeat(indentLevel);
|
|
||||||
const re = new RegExp(`^${spaceStr}([^#\\s][^:]+):`);
|
|
||||||
const match = block.match(re);
|
|
||||||
return match ? match[1].trim().replace(/['"]/g, "") : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function stripFullLineComments(text) {
|
|
||||||
return text.split('\n')
|
|
||||||
.filter(line => !line.trim().startsWith('#'))
|
|
||||||
.join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
function splitBlocks(text, type) {
|
|
||||||
const lines = cleanText(text).split("\n");
|
|
||||||
const blocks = [];
|
|
||||||
let currentBuf = [];
|
|
||||||
|
|
||||||
const isListStart = (l) => l.match(/^ {2}-\s/);
|
|
||||||
const isMapStart2 = (l) => l.match(/^ {2}[^ \-#][^:]*:/);
|
|
||||||
const isMapStart0 = (l) => l.match(/^[^ \-#][^:]*:/);
|
|
||||||
|
|
||||||
const isStart = (l) => {
|
|
||||||
if (type === 'list') return isListStart(l);
|
|
||||||
if (type === 'map2') return isMapStart2(l);
|
|
||||||
if (type === 'map0') return isMapStart0(l);
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
const line = lines[i];
|
|
||||||
if (line.match(/^#\s*@file:/)) continue;
|
|
||||||
|
|
||||||
if (isStart(line)) {
|
|
||||||
if (currentBuf.length > 0) {
|
|
||||||
blocks.push(currentBuf.join("\n"));
|
|
||||||
currentBuf = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (currentBuf.length === 0 && !line.trim()) continue;
|
|
||||||
currentBuf.push(line);
|
|
||||||
}
|
|
||||||
if (currentBuf.length > 0) blocks.push(currentBuf.join("\n"));
|
|
||||||
return blocks;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- MERGE LOGIC ---
|
|
||||||
function processSection(sectionName, manifestEntries, fileMap, opts) {
|
|
||||||
let sectionOutput = [];
|
|
||||||
const seenKeys = new Set();
|
|
||||||
|
|
||||||
// Добавляем заголовок секции
|
|
||||||
if (!['root', 'x-substore'].includes(sectionName)) {
|
|
||||||
sectionOutput.push(`${sectionName}:`);
|
|
||||||
}
|
|
||||||
if (sectionName === 'x-substore') {
|
|
||||||
sectionOutput.push(`x-substore:`);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const entry of manifestEntries) {
|
|
||||||
let content = fileMap.get(entry.file);
|
|
||||||
if (!content) throw new Error(`CRITICAL: File "${entry.file}" not found.`);
|
|
||||||
|
|
||||||
if (!opts.clearComments) {
|
|
||||||
sectionOutput.push(`\n# --- source: ${entry.file} | mode: ${entry.mode || "concat"} ---`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.clearComments) {
|
|
||||||
content = stripFullLineComments(content);
|
|
||||||
}
|
|
||||||
|
|
||||||
// [FIX] Удаляем дублирующий заголовок x-substore из контента файла,
|
|
||||||
// но оставляем содержимое (оно уже имеет правильный отступ 2 пробела)
|
|
||||||
if (sectionName === 'x-substore') {
|
|
||||||
content = content.replace(/^x-substore:\s*(?:#.*)?$/m, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
const mode = entry.mode || "concat";
|
|
||||||
|
|
||||||
if (mode === 'concat') {
|
|
||||||
const lines = cleanText(content).split('\n').filter(l => !l.match(/^#\s*@file:/));
|
|
||||||
sectionOutput.push(lines.join('\n'));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mode === 'first_wins') {
|
|
||||||
let blockType = 'map2';
|
|
||||||
if (sectionName === 'root') blockType = 'map0';
|
|
||||||
if (['proxies', 'proxy-groups'].includes(sectionName)) blockType = 'list';
|
|
||||||
|
|
||||||
const blocks = splitBlocks(content, blockType);
|
|
||||||
|
|
||||||
if (blocks.length === 0 && !opts.clearComments) {
|
|
||||||
sectionOutput.push(`# WARNING: No valid blocks found in ${entry.file}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const block of blocks) {
|
|
||||||
let id = null;
|
|
||||||
if (blockType === 'list') {
|
|
||||||
id = extractName(block);
|
|
||||||
} else if (blockType === 'map0') {
|
|
||||||
id = extractKey(block, 0);
|
|
||||||
} else {
|
|
||||||
id = extractKey(block, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply cleaning options for x-substore content
|
|
||||||
if (sectionName === 'x-substore' && id) {
|
|
||||||
if (opts.clearManifest && id === 'manifest') continue;
|
|
||||||
if (opts.clearReplacements && id === 'replacements') continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (id) {
|
|
||||||
if (seenKeys.has(id)) {
|
|
||||||
if (!opts.clearComments) sectionOutput.push(`# [SKIP] Duplicate "${id}" ignored`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
seenKeys.add(id);
|
|
||||||
}
|
|
||||||
sectionOutput.push(block);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return sectionOutput.join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- MAIN EXECUTION ---
|
|
||||||
try {
|
|
||||||
const opts = normalizeOptions();
|
|
||||||
const fileMap = normalizeFiles($files);
|
|
||||||
|
|
||||||
let manifestKey = null;
|
|
||||||
for (const k of fileMap.keys()) {
|
|
||||||
if (k.startsWith("00-manifest")) {
|
|
||||||
manifestKey = k;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!manifestKey) throw new Error("Manifest file (00-manifest-...) not found.");
|
|
||||||
|
|
||||||
const manifestRaw = fileMap.get(manifestKey);
|
|
||||||
const manifestObj = ProxyUtils.yaml.safeLoad(manifestRaw);
|
|
||||||
|
|
||||||
if (!manifestObj?.['x-substore']?.manifest) {
|
|
||||||
throw new Error("Invalid Manifest structure.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const manifestList = manifestObj['x-substore'].manifest;
|
|
||||||
const replacements = manifestObj['x-substore'].replacements || [];
|
|
||||||
|
|
||||||
const sectionOrder = [
|
|
||||||
"x-substore", "root", "hosts", "sniffer", "tun", "dns",
|
|
||||||
"proxies", "proxy-providers", "proxy-groups", "rule-providers", "rules"
|
|
||||||
];
|
|
||||||
|
|
||||||
const plan = {};
|
|
||||||
sectionOrder.forEach(s => plan[s] = []);
|
|
||||||
manifestList.forEach(entry => {
|
|
||||||
if (!plan[entry.section]) plan[entry.section] = [];
|
|
||||||
plan[entry.section].push(entry);
|
|
||||||
});
|
|
||||||
|
|
||||||
const finalChunks = [];
|
|
||||||
for (const sec of sectionOrder) {
|
|
||||||
if (!plan[sec] || plan[sec].length === 0) continue;
|
|
||||||
const secStr = processSection(sec, plan[sec], fileMap, opts);
|
|
||||||
finalChunks.push(secStr);
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = finalChunks.join("\n\n");
|
|
||||||
|
|
||||||
if (Array.isArray(replacements)) {
|
|
||||||
replacements.forEach(rep => {
|
|
||||||
if (rep.from && rep.to) {
|
|
||||||
result = result.split(rep.from).join(rep.to);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$content = result;
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
$content = `# CRITICAL ERROR:\n# ${err.message}\n# Stack: ${err.stack}`;
|
|
||||||
}
|
|
||||||
@@ -1,239 +0,0 @@
|
|||||||
/**
|
|
||||||
* SUB STORE YAML ASSEMBLER (Content-Tag Aware)
|
|
||||||
* * Требование: Каждый файл должен начинаться с комментария:
|
|
||||||
* # @file: filename.yaml
|
|
||||||
*/
|
|
||||||
|
|
||||||
// --- UTILS ---
|
|
||||||
|
|
||||||
// Функция нормализации теперь парсит содержимое, чтобы найти имя файла
|
|
||||||
function normalizeFiles(rawFiles) {
|
|
||||||
const map = new Map();
|
|
||||||
if (!rawFiles || !Array.isArray(rawFiles)) return map;
|
|
||||||
|
|
||||||
rawFiles.forEach((content, index) => {
|
|
||||||
if (typeof content !== 'string') return;
|
|
||||||
|
|
||||||
// Ищем магический тег: # @file: filename.yaml
|
|
||||||
const match = content.match(/^#\s*@file:\s*(.+?)(\s|$)/m);
|
|
||||||
|
|
||||||
if (match) {
|
|
||||||
const filename = match[1].trim();
|
|
||||||
map.set(filename, content);
|
|
||||||
} else {
|
|
||||||
// Если тега нет, файл остается "анонимным" и недоступным через манифест,
|
|
||||||
// но мы можем логировать это.
|
|
||||||
// console.log(`File at index ${index} has no @file tag`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanText(text) {
|
|
||||||
return String(text || "").replace(/\r\n/g, "\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Извлечение name из элемента списка
|
|
||||||
function extractName(block) {
|
|
||||||
// Ищем name: value с учетом отступов (4 пробела)
|
|
||||||
const match = block.match(/^ {4}name:\s*(?:["']?)(.*?)(?:["']?)\s*(?:#.*)?$/m);
|
|
||||||
return match ? match[1].trim() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Извлечение ключа из map (0 или 2 пробела)
|
|
||||||
function extractKey(block, indentLevel) {
|
|
||||||
const spaceStr = " ".repeat(indentLevel);
|
|
||||||
const re = new RegExp(`^${spaceStr}([^#\\s][^:]+):`);
|
|
||||||
const match = block.match(re);
|
|
||||||
return match ? match[1].trim().replace(/['"]/g, "") : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Разбивка текста на логические блоки
|
|
||||||
function splitBlocks(text, type) {
|
|
||||||
const lines = cleanText(text).split("\n");
|
|
||||||
const blocks = [];
|
|
||||||
let currentBuf = [];
|
|
||||||
|
|
||||||
// Детекторы начала блока
|
|
||||||
const isListStart = (l) => l.match(/^ {2}-\s/); // " - "
|
|
||||||
const isMapStart2 = (l) => l.match(/^ {2}[^ \-#][^:]*:/); // " key:"
|
|
||||||
const isMapStart0 = (l) => l.match(/^[^ \-#][^:]*:/); // "key:" (root)
|
|
||||||
|
|
||||||
const isStart = (l) => {
|
|
||||||
if (type === 'list') return isListStart(l);
|
|
||||||
if (type === 'map2') return isMapStart2(l);
|
|
||||||
if (type === 'map0') return isMapStart0(l);
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
const line = lines[i];
|
|
||||||
|
|
||||||
// Если строка начинается с # @file:, пропускаем ее, чтобы не мусорить в конфиге
|
|
||||||
if (line.match(/^#\s*@file:/)) continue;
|
|
||||||
|
|
||||||
if (isStart(line)) {
|
|
||||||
if (currentBuf.length > 0) {
|
|
||||||
blocks.push(currentBuf.join("\n"));
|
|
||||||
currentBuf = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Пропуск пустых строк в начале, если буфер пуст
|
|
||||||
if (currentBuf.length === 0 && !line.trim()) continue;
|
|
||||||
|
|
||||||
currentBuf.push(line);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentBuf.length > 0) {
|
|
||||||
blocks.push(currentBuf.join("\n"));
|
|
||||||
}
|
|
||||||
|
|
||||||
return blocks;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- MERGE LOGIC ---
|
|
||||||
|
|
||||||
function processSection(sectionName, manifestEntries, fileMap) {
|
|
||||||
let sectionOutput = [];
|
|
||||||
const seenKeys = new Set();
|
|
||||||
|
|
||||||
// Добавляем заголовок секции (кроме root, x-substore и rules)
|
|
||||||
if (!['root', 'x-substore'].includes(sectionName)) {
|
|
||||||
sectionOutput.push(`${sectionName}:`);
|
|
||||||
}
|
|
||||||
if (sectionName === 'x-substore') {
|
|
||||||
sectionOutput.push(`x-substore:`);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const entry of manifestEntries) {
|
|
||||||
const content = fileMap.get(entry.file);
|
|
||||||
|
|
||||||
if (!content) {
|
|
||||||
// FAIL-FAST: Если файл из манифеста не найден (нет тега или не загружен)
|
|
||||||
throw new Error(`CRITICAL: File "${entry.file}" not found inside input bundle. Did you add '# @file: ${entry.file}' header?`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mode = entry.mode || "concat";
|
|
||||||
sectionOutput.push(`\n# --- source: ${entry.file} | mode: ${mode} ---`);
|
|
||||||
|
|
||||||
if (mode === 'concat') {
|
|
||||||
// Просто чистим от тега @file при вставке
|
|
||||||
const lines = cleanText(content).split('\n').filter(l => !l.match(/^#\s*@file:/));
|
|
||||||
sectionOutput.push(lines.join('\n'));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mode === 'first_wins') {
|
|
||||||
let blockType = 'map2';
|
|
||||||
if (sectionName === 'root') blockType = 'map0';
|
|
||||||
if (['proxies', 'proxy-groups'].includes(sectionName)) blockType = 'list';
|
|
||||||
|
|
||||||
const blocks = splitBlocks(content, blockType);
|
|
||||||
|
|
||||||
if (blocks.length === 0) {
|
|
||||||
sectionOutput.push(`# WARNING: No valid blocks found in ${entry.file}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const block of blocks) {
|
|
||||||
let id = null;
|
|
||||||
|
|
||||||
if (blockType === 'list') {
|
|
||||||
id = extractName(block);
|
|
||||||
} else if (blockType === 'map0') {
|
|
||||||
id = extractKey(block, 0);
|
|
||||||
} else {
|
|
||||||
id = extractKey(block, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (id) {
|
|
||||||
if (seenKeys.has(id)) {
|
|
||||||
sectionOutput.push(`# [SKIP] Duplicate "${id}" ignored`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
seenKeys.add(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
sectionOutput.push(block);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return sectionOutput.join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- MAIN EXECUTION ---
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. Создаем карту файлов на основе тегов # @file:
|
|
||||||
const fileMap = normalizeFiles($files);
|
|
||||||
|
|
||||||
// 2. Ищем Манифест
|
|
||||||
let manifestKey = null;
|
|
||||||
// Ищем файл, чье имя (из тега) начинается с 00-manifest
|
|
||||||
for (const k of fileMap.keys()) {
|
|
||||||
if (k.startsWith("00-manifest")) {
|
|
||||||
manifestKey = k;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!manifestKey) {
|
|
||||||
const foundFiles = Array.from(fileMap.keys()).join(", ");
|
|
||||||
throw new Error(`Manifest file (00-manifest-...) not found in headers. Found files: [${foundFiles}]`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Парсим Манифест
|
|
||||||
const manifestRaw = fileMap.get(manifestKey);
|
|
||||||
const manifestObj = ProxyUtils.yaml.safeLoad(manifestRaw);
|
|
||||||
|
|
||||||
if (!manifestObj?.['x-substore']?.manifest) {
|
|
||||||
throw new Error("Invalid Manifest: missing x-substore.manifest structure.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const manifestList = manifestObj['x-substore'].manifest;
|
|
||||||
const replacements = manifestObj['x-substore'].replacements || [];
|
|
||||||
|
|
||||||
// 4. Планирование порядка секций
|
|
||||||
const sectionOrder = [
|
|
||||||
"x-substore", "root", "hosts", "sniffer", "tun", "dns",
|
|
||||||
"proxies", "proxy-providers", "proxy-groups", "rule-providers", "rules"
|
|
||||||
];
|
|
||||||
|
|
||||||
const plan = {};
|
|
||||||
sectionOrder.forEach(s => plan[s] = []);
|
|
||||||
|
|
||||||
manifestList.forEach(entry => {
|
|
||||||
// Если секция в манифесте есть, а в нашем плане нет - добавляем динамически (на всякий случай)
|
|
||||||
if (!plan[entry.section]) plan[entry.section] = [];
|
|
||||||
plan[entry.section].push(entry);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 5. Сборка
|
|
||||||
const finalChunks = [];
|
|
||||||
|
|
||||||
for (const sec of sectionOrder) {
|
|
||||||
if (!plan[sec] || plan[sec].length === 0) continue;
|
|
||||||
|
|
||||||
// Обработка секции
|
|
||||||
const secStr = processSection(sec, plan[sec], fileMap);
|
|
||||||
finalChunks.push(secStr);
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = finalChunks.join("\n\n");
|
|
||||||
|
|
||||||
// 6. Replacements (Literal)
|
|
||||||
if (Array.isArray(replacements)) {
|
|
||||||
replacements.forEach(rep => {
|
|
||||||
if (rep.from && rep.to) {
|
|
||||||
result = result.split(rep.from).join(rep.to);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$content = result;
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
$content = `# CRITICAL ERROR in Assembler:\n# ${err.message}\n\n# Stack:\n${err.stack}`;
|
|
||||||
}
|
|
||||||
@@ -1,235 +0,0 @@
|
|||||||
/**********************
|
|
||||||
* Defaults (AmneziaWG)
|
|
||||||
* Если в исходнике нет параметра, берём отсюда.
|
|
||||||
* Если итоговое значение == 0, параметр пропускаем в amnezia-wg-option.
|
|
||||||
**********************/
|
|
||||||
const AMZ_DEFAULTS = {
|
|
||||||
Jc: 4,
|
|
||||||
Jmin: 10,
|
|
||||||
Jmax: 50,
|
|
||||||
S1: 110,
|
|
||||||
S2: 120,
|
|
||||||
H1: 0,
|
|
||||||
H2: 0,
|
|
||||||
H3: 0,
|
|
||||||
H4: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**********************
|
|
||||||
* Options from Sub Store
|
|
||||||
* Example URL:
|
|
||||||
* .../convert-awg-to-clash.js#dns=false&ipv6=false#noCache
|
|
||||||
*
|
|
||||||
* Требования:
|
|
||||||
* - dns=false => remote-dns-resolve: false (вне зависимости от входа)
|
|
||||||
* - ipv6=false => удалить IPv6 из allowed-ips (и вообще не добавлять ipv6-части)
|
|
||||||
**********************/
|
|
||||||
function normalizeOptions() {
|
|
||||||
const args = (typeof $arguments !== "undefined" && $arguments) ? $arguments : {};
|
|
||||||
|
|
||||||
const asBool = (v, def = true) => {
|
|
||||||
if (v === undefined || v === null || v === "") return def;
|
|
||||||
if (typeof v === "boolean") return v;
|
|
||||||
const s = String(v).toLowerCase().trim();
|
|
||||||
if (["1", "true", "yes", "y", "on"].includes(s)) return true;
|
|
||||||
if (["0", "false", "no", "n", "off"].includes(s)) return false;
|
|
||||||
return def;
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
dns: asBool(args.dns, true),
|
|
||||||
ipv6: asBool(args.ipv6, true),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**********************
|
|
||||||
* Parsing WG INI blocks
|
|
||||||
**********************/
|
|
||||||
function cleanLines(text) {
|
|
||||||
return String(text ?? "")
|
|
||||||
.replace(/\r\n/g, "\n")
|
|
||||||
.split("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Парсим один INI-фрагмент с [Interface] и [Peer] (один peer)
|
|
||||||
function parseIniOne(text) {
|
|
||||||
const lines = cleanLines(text)
|
|
||||||
.map((l) => l.trim())
|
|
||||||
.filter((l) => l.length > 0 && !l.startsWith("#") && !l.startsWith(";"));
|
|
||||||
|
|
||||||
let section = null;
|
|
||||||
const data = { Interface: {}, Peer: {} };
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
const mSec = line.match(/^\[(.+?)\]$/);
|
|
||||||
if (mSec) {
|
|
||||||
section = mSec[1];
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mKV = line.match(/^([^=]+?)\s*=\s*(.+)$/);
|
|
||||||
if (!mKV || !section) continue;
|
|
||||||
|
|
||||||
const key = mKV[1].trim();
|
|
||||||
const value = mKV[2].trim();
|
|
||||||
|
|
||||||
if (section === "Interface") data.Interface[key] = value;
|
|
||||||
else if (section === "Peer") data.Peer[key] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Разбиваем весь файл на блоки по заголовкам "##### ..."
|
|
||||||
function splitByHeaders(fullText) {
|
|
||||||
const lines = cleanLines(fullText);
|
|
||||||
|
|
||||||
const blocks = [];
|
|
||||||
let current = { name: "amz-wg", buf: [] };
|
|
||||||
|
|
||||||
const headerRe = /^#{5}\s*(.+)\s*$/;
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
const mh = line.match(headerRe);
|
|
||||||
if (mh) {
|
|
||||||
// закрываем предыдущий блок, если там что-то есть
|
|
||||||
if (current.buf.join("\n").trim().length > 0) blocks.push(current);
|
|
||||||
current = { name: mh[1].trim(), buf: [] };
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
current.buf.push(line);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (current.buf.join("\n").trim().length > 0) blocks.push(current);
|
|
||||||
|
|
||||||
return blocks;
|
|
||||||
}
|
|
||||||
|
|
||||||
function splitList(val) {
|
|
||||||
return String(val || "")
|
|
||||||
.split(",")
|
|
||||||
.map((s) => s.trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseEndpoint(endpoint) {
|
|
||||||
// Поддержка:
|
|
||||||
// - host:port
|
|
||||||
// - [ipv6]:port
|
|
||||||
const s = String(endpoint || "").trim();
|
|
||||||
const v6 = s.match(/^\[(.+?)\]:(\d+)$/);
|
|
||||||
if (v6) return { host: v6[1], port: Number(v6[2]) };
|
|
||||||
|
|
||||||
const v4 = s.match(/^(.+?):(\d+)$/);
|
|
||||||
if (v4) return { host: v4[1], port: Number(v4[2]) };
|
|
||||||
|
|
||||||
return { host: "", port: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
function toNumberOrNull(v) {
|
|
||||||
const s = String(v ?? "").trim();
|
|
||||||
if (s === "") return null;
|
|
||||||
if (/^-?\d+$/.test(s)) return Number(s);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildAmzOptions(interfaceObj) {
|
|
||||||
// Правило:
|
|
||||||
// - если в файле есть параметр => используем его
|
|
||||||
// - иначе берём из AMZ_DEFAULTS
|
|
||||||
// - если итог == 0 => пропускаем
|
|
||||||
const out = {};
|
|
||||||
const keys = Object.keys(AMZ_DEFAULTS);
|
|
||||||
|
|
||||||
for (const K of keys) {
|
|
||||||
const fromFile = interfaceObj[K];
|
|
||||||
const fileNum = toNumberOrNull(fromFile);
|
|
||||||
const fallback = AMZ_DEFAULTS[K];
|
|
||||||
|
|
||||||
const finalVal =
|
|
||||||
fileNum !== null ? fileNum : (fallback ?? 0);
|
|
||||||
|
|
||||||
if (Number(finalVal) !== 0) {
|
|
||||||
out[K.toLowerCase()] = Number(finalVal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
function filterAllowedIPs(allowed, enableIPv6) {
|
|
||||||
if (enableIPv6) return allowed;
|
|
||||||
// выкидываем всё, что похоже на IPv6
|
|
||||||
return allowed.filter((cidr) => !cidr.includes(":"));
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildProxy(blockName, wg, options) {
|
|
||||||
const i = wg.Interface || {};
|
|
||||||
const p = wg.Peer || {};
|
|
||||||
|
|
||||||
const address = i.Address || "";
|
|
||||||
const dnsList = splitList(i.DNS);
|
|
||||||
|
|
||||||
const ep = parseEndpoint(p.Endpoint);
|
|
||||||
let allowed = splitList(p.AllowedIPs);
|
|
||||||
allowed = filterAllowedIPs(allowed, options.ipv6);
|
|
||||||
|
|
||||||
const proxy = {
|
|
||||||
name: blockName || "amz-wg",
|
|
||||||
type: "wireguard",
|
|
||||||
ip: address,
|
|
||||||
// ipv6 поле в твоём примере закомментировано, так что не добавляем вообще
|
|
||||||
"private-key": i.PrivateKey || "",
|
|
||||||
peers: [
|
|
||||||
{
|
|
||||||
server: ep.host,
|
|
||||||
port: ep.port,
|
|
||||||
"public-key": p.PublicKey || "",
|
|
||||||
...(p.PresharedKey ? { "pre-shared-key": p.PresharedKey } : {}),
|
|
||||||
"allowed-ips": allowed,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
udp: true,
|
|
||||||
// dns=false => принудительно false
|
|
||||||
"remote-dns-resolve": options.dns ? true : false,
|
|
||||||
...(dnsList.length ? { dns: dnsList } : {}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const amz = buildAmzOptions(i);
|
|
||||||
if (Object.keys(amz).length) {
|
|
||||||
proxy["amnezia-wg-option"] = amz;
|
|
||||||
}
|
|
||||||
|
|
||||||
return proxy;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**********************
|
|
||||||
* ENTRYPOINT
|
|
||||||
**********************/
|
|
||||||
const opts = normalizeOptions();
|
|
||||||
|
|
||||||
// Вход: чаще всего $content, но на всякий пожарный берём $files[0]
|
|
||||||
const input = String($content ?? ($files && $files[0]) ?? "");
|
|
||||||
|
|
||||||
// Разбиваем по заголовкам ##### ...
|
|
||||||
const blocks = splitByHeaders(input);
|
|
||||||
|
|
||||||
// Для каждого блока парсим INI и строим proxy
|
|
||||||
const proxies = [];
|
|
||||||
for (const b of blocks) {
|
|
||||||
const iniText = b.buf.join("\n").trim();
|
|
||||||
if (!iniText) continue;
|
|
||||||
|
|
||||||
const wg = parseIniOne(iniText);
|
|
||||||
|
|
||||||
// минимальная валидация: нужны ключи
|
|
||||||
if (!wg.Interface?.PrivateKey || !wg.Peer?.PublicKey || !wg.Peer?.Endpoint) {
|
|
||||||
// пропускаем мусорные блоки, чтобы не ронять весь конвертер
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
proxies.push(buildProxy(b.name, wg, opts));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Финальный YAML
|
|
||||||
$content = ProxyUtils.yaml.safeDump({ proxies });
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
// Example:
|
|
||||||
// Script Operator
|
|
||||||
// 1. backend version(>2.14.88):
|
|
||||||
$server.name = 'prefix-' + $server.name
|
|
||||||
$server.ecn = true
|
|
||||||
$server['test-url'] = 'http://1.0.0.1/generate_204'
|
|
||||||
// 2. operator function
|
|
||||||
function operator(proxies, targetPlatform, context) {
|
|
||||||
return proxies.map( proxy => {
|
|
||||||
// Change proxy information here
|
|
||||||
|
|
||||||
return proxy;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Script Filter
|
|
||||||
// 1. backend version(>2.14.119):
|
|
||||||
const port = Number($server.port)
|
|
||||||
return [80].includes(port)
|
|
||||||
|
|
||||||
// 2. filter function
|
|
||||||
function filter(proxies, targetPlatform) {
|
|
||||||
return proxies.map( proxy => {
|
|
||||||
// Return true if the current proxy is selected
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
/**
|
|
||||||
* 节点名改为花里胡哨字体,仅支持英文字符和数字
|
|
||||||
*
|
|
||||||
* 【字体】
|
|
||||||
* 可参考:https://www.dute.org/weird-fonts
|
|
||||||
* serif-bold, serif-italic, serif-bold-italic, sans-serif-regular, sans-serif-bold-italic, script-regular, script-bold, fraktur-regular, fraktur-bold, monospace-regular, double-struck-bold, circle-regular, square-regular, modifier-letter(小写没有 q, 用 ᵠ 替代. 大写缺的太多, 用小写替代)
|
|
||||||
*
|
|
||||||
* 【示例】
|
|
||||||
* 1️⃣ 设置所有格式为 "serif-bold"
|
|
||||||
* #type=serif-bold
|
|
||||||
*
|
|
||||||
* 2️⃣ 设置字母格式为 "serif-bold",数字格式为 "circle-regular"
|
|
||||||
* #type=serif-bold&num=circle-regular
|
|
||||||
*/
|
|
||||||
|
|
||||||
global.$arguments = { type: "serif-bold" };
|
|
||||||
|
|
||||||
function operator(proxies) {
|
|
||||||
const { type, num } = $arguments;
|
|
||||||
const TABLE = {
|
|
||||||
"serif-bold": ["𝟎","𝟏","𝟐","𝟑","𝟒","𝟓","𝟔","𝟕","𝟖","𝟗","𝐚","𝐛","𝐜","𝐝","𝐞","𝐟","𝐠","𝐡","𝐢","𝐣","𝐤","𝐥","𝐦","𝐧","𝐨","𝐩","𝐪","𝐫","𝐬","𝐭","𝐮","𝐯","𝐰","𝐱","𝐲","𝐳","𝐀","𝐁","𝐂","𝐃","𝐄","𝐅","𝐆","𝐇","𝐈","𝐉","𝐊","𝐋","𝐌","𝐍","𝐎","𝐏","𝐐","𝐑","𝐒","𝐓","𝐔","𝐕","𝐖","𝐗","𝐘","𝐙"] ,
|
|
||||||
"serif-italic": ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "𝑎", "𝑏", "𝑐", "𝑑", "𝑒", "𝑓", "𝑔", "ℎ", "𝑖", "𝑗", "𝑘", "𝑙", "𝑚", "𝑛", "𝑜", "𝑝", "𝑞", "𝑟", "𝑠", "𝑡", "𝑢", "𝑣", "𝑤", "𝑥", "𝑦", "𝑧", "𝐴", "𝐵", "𝐶", "𝐷", "𝐸", "𝐹", "𝐺", "𝐻", "𝐼", "𝐽", "𝐾", "𝐿", "𝑀", "𝑁", "𝑂", "𝑃", "𝑄", "𝑅", "𝑆", "𝑇", "𝑈", "𝑉", "𝑊", "𝑋", "𝑌", "𝑍"],
|
|
||||||
"serif-bold-italic": ["0","1","2","3","4","5","6","7","8","9","𝒂","𝒃","𝒄","𝒅","𝒆","𝒇","𝒈","𝒉","𝒊","𝒋","𝒌","𝒍","𝒎","𝒏","𝒐","𝒑","𝒒","𝒓","𝒔","𝒕","𝒖","𝒗","𝒘","𝒙","𝒚","𝒛","𝑨","𝑩","𝑪","𝑫","𝑬","𝑭","𝑮","𝑯","𝑰","𝑱","𝑲","𝑳","𝑴","𝑵","𝑶","𝑷","𝑸","𝑹","𝑺","𝑻","𝑼","𝑽","𝑾","𝑿","𝒀","𝒁"],
|
|
||||||
"sans-serif-regular": ["𝟢", "𝟣", "𝟤", "𝟥", "𝟦", "𝟧", "𝟨", "𝟩", "𝟪", "𝟫", "𝖺", "𝖻", "𝖼", "𝖽", "𝖾", "𝖿", "𝗀", "𝗁", "𝗂", "𝗃", "𝗄", "𝗅", "𝗆", "𝗇", "𝗈", "𝗉", "𝗊", "𝗋", "𝗌", "𝗍", "𝗎", "𝗏", "𝗐", "𝗑", "𝗒", "𝗓", "𝖠", "𝖡", "𝖢", "𝖣", "𝖤", "𝖥", "𝖦", "𝖧", "𝖨", "𝖩", "𝖪", "𝖫", "𝖬", "𝖭", "𝖮", "𝖯", "𝖰", "𝖱", "𝖲", "𝖳", "𝖴", "𝖵", "𝖶", "𝖷", "𝖸", "𝖹"],
|
|
||||||
"sans-serif-bold": ["𝟬","𝟭","𝟮","𝟯","𝟰","𝟱","𝟲","𝟳","𝟴","𝟵","𝗮","𝗯","𝗰","𝗱","𝗲","𝗳","𝗴","𝗵","𝗶","𝗷","𝗸","𝗹","𝗺","𝗻","𝗼","𝗽","𝗾","𝗿","𝘀","𝘁","𝘂","𝘃","𝘄","𝘅","𝘆","𝘇","𝗔","𝗕","𝗖","𝗗","𝗘","𝗙","𝗚","𝗛","𝗜","𝗝","𝗞","𝗟","𝗠","𝗡","𝗢","𝗣","𝗤","𝗥","𝗦","𝗧","𝗨","𝗩","𝗪","𝗫","𝗬","𝗭"],
|
|
||||||
"sans-serif-italic": ["0","1","2","3","4","5","6","7","8","9","𝘢","𝘣","𝘤","𝘥","𝘦","𝘧","𝘨","𝘩","𝘪","𝘫","𝘬","𝘭","𝘮","𝘯","𝘰","𝘱","𝘲","𝘳","𝘴","𝘵","𝘶","𝘷","𝘸","𝘹","𝘺","𝘻","𝘈","𝘉","𝘊","𝘋","𝘌","𝘍","𝘎","𝘏","𝘐","𝘑","𝘒","𝘓","𝘔","𝘕","𝘖","𝘗","𝘘","𝘙","𝘚","𝘛","𝘜","𝘝","𝘞","𝘟","𝘠","𝘡"],
|
|
||||||
"sans-serif-bold-italic": ["0","1","2","3","4","5","6","7","8","9","𝙖","𝙗","𝙘","𝙙","𝙚","𝙛","𝙜","𝙝","𝙞","𝙟","𝙠","𝙡","𝙢","𝙣","𝙤","𝙥","𝙦","𝙧","𝙨","𝙩","𝙪","𝙫","𝙬","𝙭","𝙮","𝙯","𝘼","𝘽","𝘾","𝘿","𝙀","𝙁","𝙂","𝙃","𝙄","𝙅","𝙆","𝙇","𝙈","𝙉","𝙊","𝙋","𝙌","𝙍","𝙎","𝙏","𝙐","𝙑","𝙒","𝙓","𝙔","𝙕"],
|
|
||||||
"script-regular": ["0","1","2","3","4","5","6","7","8","9","𝒶","𝒷","𝒸","𝒹","ℯ","𝒻","ℊ","𝒽","𝒾","𝒿","𝓀","𝓁","𝓂","𝓃","ℴ","𝓅","𝓆","𝓇","𝓈","𝓉","𝓊","𝓋","𝓌","𝓍","𝓎","𝓏","𝒜","ℬ","𝒞","𝒟","ℰ","ℱ","𝒢","ℋ","ℐ","𝒥","𝒦","ℒ","ℳ","𝒩","𝒪","𝒫","𝒬","ℛ","𝒮","𝒯","𝒰","𝒱","𝒲","𝒳","𝒴","𝒵"],
|
|
||||||
"script-bold": ["0","1","2","3","4","5","6","7","8","9","𝓪","𝓫","𝓬","𝓭","𝓮","𝓯","𝓰","𝓱","𝓲","𝓳","𝓴","𝓵","𝓶","𝓷","𝓸","𝓹","𝓺","𝓻","𝓼","𝓽","𝓾","𝓿","𝔀","𝔁","𝔂","𝔃","𝓐","𝓑","𝓒","𝓓","𝓔","𝓕","𝓖","𝓗","𝓘","𝓙","𝓚","𝓛","𝓜","𝓝","𝓞","𝓟","𝓠","𝓡","𝓢","𝓣","𝓤","𝓥","𝓦","𝓧","𝓨","𝓩"],
|
|
||||||
"fraktur-regular": ["0","1","2","3","4","5","6","7","8","9","𝔞","𝔟","𝔠","𝔡","𝔢","𝔣","𝔤","𝔥","𝔦","𝔧","𝔨","𝔩","𝔪","𝔫","𝔬","𝔭","𝔮","𝔯","𝔰","𝔱","𝔲","𝔳","𝔴","𝔵","𝔶","𝔷","𝔄","𝔅","ℭ","𝔇","𝔈","𝔉","𝔊","ℌ","ℑ","𝔍","𝔎","𝔏","𝔐","𝔑","𝔒","𝔓","𝔔","ℜ","𝔖","𝔗","𝔘","𝔙","𝔚","𝔛","𝔜","ℨ"],
|
|
||||||
"fraktur-bold": ["0","1","2","3","4","5","6","7","8","9","𝖆","𝖇","𝖈","𝖉","𝖊","𝖋","𝖌","𝖍","𝖎","𝖏","𝖐","𝖑","𝖒","𝖓","𝖔","𝖕","𝖖","𝖗","𝖘","𝖙","𝖚","𝖛","𝖜","𝖝","𝖞","𝖟","𝕬","𝕭","𝕮","𝕯","𝕰","𝕱","𝕲","𝕳","𝕴","𝕵","𝕶","𝕷","𝕸","𝕹","𝕺","𝕻","𝕼","𝕽","𝕾","𝕿","𝖀","𝖁","𝖂","𝖃","𝖄","𝖅"],
|
|
||||||
"monospace-regular": ["𝟶","𝟷","𝟸","𝟹","𝟺","𝟻","𝟼","𝟽","𝟾","𝟿","𝚊","𝚋","𝚌","𝚍","𝚎","𝚏","𝚐","𝚑","𝚒","𝚓","𝚔","𝚕","𝚖","𝚗","𝚘","𝚙","𝚚","𝚛","𝚜","𝚝","𝚞","𝚟","𝚠","𝚡","𝚢","𝚣","𝙰","𝙱","𝙲","𝙳","𝙴","𝙵","𝙶","𝙷","𝙸","𝙹","𝙺","𝙻","𝙼","𝙽","𝙾","𝙿","𝚀","𝚁","𝚂","𝚃","𝚄","𝚅","𝚆","𝚇","𝚈","𝚉"],
|
|
||||||
"double-struck-bold": ["𝟘","𝟙","𝟚","𝟛","𝟜","𝟝","𝟞","𝟟","𝟠","𝟡","𝕒","𝕓","𝕔","𝕕","𝕖","𝕗","𝕘","𝕙","𝕚","𝕛","𝕜","𝕝","𝕞","𝕟","𝕠","𝕡","𝕢","𝕣","𝕤","𝕥","𝕦","𝕧","𝕨","𝕩","𝕪","𝕫","𝔸","𝔹","ℂ","𝔻","𝔼","𝔽","𝔾","ℍ","𝕀","𝕁","𝕂","𝕃","𝕄","ℕ","𝕆","ℙ","ℚ","ℝ","𝕊","𝕋","𝕌","𝕍","𝕎","𝕏","𝕐","ℤ"],
|
|
||||||
"circle-regular": ["⓪","①","②","③","④","⑤","⑥","⑦","⑧","⑨","ⓐ","ⓑ","ⓒ","ⓓ","ⓔ","ⓕ","ⓖ","ⓗ","ⓘ","ⓙ","ⓚ","ⓛ","ⓜ","ⓝ","ⓞ","ⓟ","ⓠ","ⓡ","ⓢ","ⓣ","ⓤ","ⓥ","ⓦ","ⓧ","ⓨ","ⓩ","Ⓐ","Ⓑ","Ⓒ","Ⓓ","Ⓔ","Ⓕ","Ⓖ","Ⓗ","Ⓘ","Ⓙ","Ⓚ","Ⓛ","Ⓜ","Ⓝ","Ⓞ","Ⓟ","Ⓠ","Ⓡ","Ⓢ","Ⓣ","Ⓤ","Ⓥ","Ⓦ","Ⓧ","Ⓨ","Ⓩ"],
|
|
||||||
"square-regular": ["0","1","2","3","4","5","6","7","8","9","🄰","🄱","🄲","🄳","🄴","🄵","🄶","🄷","🄸","🄹","🄺","🄻","🄼","🄽","🄾","🄿","🅀","🅁","🅂","🅃","🅄","🅅","🅆","🅇","🅈","🅉","🄰","🄱","🄲","🄳","🄴","🄵","🄶","🄷","🄸","🄹","🄺","🄻","🄼","🄽","🄾","🄿","🅀","🅁","🅂","🅃","🅄","🅅","🅆","🅇","🅈","🅉"],
|
|
||||||
"modifier-letter": ["⁰", "¹", "²", "³", "⁴", "⁵", "⁶", "⁷", "⁸", "⁹", "ᵃ", "ᵇ", "ᶜ", "ᵈ", "ᵉ", "ᶠ", "ᵍ", "ʰ", "ⁱ", "ʲ", "ᵏ", "ˡ", "ᵐ", "ⁿ", "ᵒ", "ᵖ", "ᵠ", "ʳ", "ˢ", "ᵗ", "ᵘ", "ᵛ", "ʷ", "ˣ", "ʸ", "ᶻ", "ᴬ", "ᴮ", "ᶜ", "ᴰ", "ᴱ", "ᶠ", "ᴳ", "ʰ", "ᴵ", "ᴶ", "ᴷ", "ᴸ", "ᴹ", "ᴺ", "ᴼ", "ᴾ", "ᵠ", "ᴿ", "ˢ", "ᵀ", "ᵁ", "ᵛ", "ᵂ", "ˣ", "ʸ", "ᶻ"],
|
|
||||||
};
|
|
||||||
|
|
||||||
// charCode => index in `TABLE`
|
|
||||||
const INDEX = { "48": 0, "49": 1, "50": 2, "51": 3, "52": 4, "53": 5, "54": 6, "55": 7, "56": 8, "57": 9, "65": 36, "66": 37, "67": 38, "68": 39, "69": 40, "70": 41, "71": 42, "72": 43, "73": 44, "74": 45, "75": 46, "76": 47, "77": 48, "78": 49, "79": 50, "80": 51, "81": 52, "82": 53, "83": 54, "84": 55, "85": 56, "86": 57, "87": 58, "88": 59, "89": 60, "90": 61, "97": 10, "98": 11, "99": 12, "100": 13, "101": 14, "102": 15, "103": 16, "104": 17, "105": 18, "106": 19, "107": 20, "108": 21, "109": 22, "110": 23, "111": 24, "112": 25, "113": 26, "114": 27, "115": 28, "116": 29, "117": 30, "118": 31, "119": 32, "120": 33, "121": 34, "122": 35 };
|
|
||||||
|
|
||||||
return proxies.map(p => {
|
|
||||||
p.name = [...p.name].map(c => {
|
|
||||||
if (/[a-zA-Z0-9]/.test(c)) {
|
|
||||||
const code = c.charCodeAt(0);
|
|
||||||
const index = INDEX[code];
|
|
||||||
if (isNumber(code) && num) {
|
|
||||||
return TABLE[num][index];
|
|
||||||
} else {
|
|
||||||
return TABLE[type][index];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return c;
|
|
||||||
}).join("");
|
|
||||||
return p;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function isNumber(code) { return code >= 48 && code <= 57; }
|
|
||||||
@@ -1,328 +0,0 @@
|
|||||||
/**
|
|
||||||
* Sub-Store operator: Normalize + tag + country detect + per-country numbering
|
|
||||||
*
|
|
||||||
* Output format (default):
|
|
||||||
* 🇺🇸 USA-01 🔒 📺 ❻ ▫️ws/vless/443
|
|
||||||
*
|
|
||||||
* Notes:
|
|
||||||
* - Numbering is computed per-country AFTER grouping the full list.
|
|
||||||
* - Tags (icons) do NOT affect numbering order.
|
|
||||||
* - GeoIP is optional and only used when server is an IP and utils.geoip.lookup exists.
|
|
||||||
*/
|
|
||||||
|
|
||||||
///////////////////////
|
|
||||||
// CONFIG (EDIT ME)
|
|
||||||
///////////////////////
|
|
||||||
|
|
||||||
// 1) Remove these patterns (marketing noise, brackets, separators, etc.)
|
|
||||||
const NOISE_PATTERNS = [
|
|
||||||
/\[[^\]]*]/g, // [ ... ]
|
|
||||||
/\([^)]*\)/g, // ( ... )
|
|
||||||
/\{[^}]*}/g, // { ... }
|
|
||||||
/\btraffic\b/gi,
|
|
||||||
/\bfree\b/gi,
|
|
||||||
/\bwebsite\b/gi,
|
|
||||||
/\bexpire\b/gi,
|
|
||||||
/\blow\s*ping\b/gi,
|
|
||||||
/\bai\s*studio\b/gi,
|
|
||||||
/\bno\s*p2p\b/gi,
|
|
||||||
/\b10\s*gbit\b/gi,
|
|
||||||
/\bvless\b/gi, // you said you don't want it in the visible name
|
|
||||||
/\bvmess\b/gi,
|
|
||||||
/\bssr?\b/gi,
|
|
||||||
/\btrojan\b/gi,
|
|
||||||
/\bhysteria2?\b/gi,
|
|
||||||
/\btuic\b/gi,
|
|
||||||
/[|]/g,
|
|
||||||
/[_]+/g,
|
|
||||||
/[-]{2,}/g
|
|
||||||
];
|
|
||||||
|
|
||||||
// 2) Keyword -> icon tags (if found in original name, icon is added; the keyword is removed from base name)
|
|
||||||
const ICON_RULES = [
|
|
||||||
{ regex: /\bYT\b/gi, icon: "📺" },
|
|
||||||
{ regex: /\bIPv6\b/gi, icon: "❻" },
|
|
||||||
{ regex: /\bNetflix\b|\bNF\b/gi, icon: "🎬" },
|
|
||||||
{ regex: /\bDisney\+?\b|\bDSNY\b/gi, icon: "🏰" },
|
|
||||||
{ regex: /\bHBO\b/gi, icon: "📼" },
|
|
||||||
{ regex: /\bPrime\b|\bAmazon\b/gi, icon: "📦" },
|
|
||||||
{ regex: /\bChatGPT\b|\bOpenAI\b/gi, icon: "🤖" },
|
|
||||||
{ regex: /\bSteam\b/gi, icon: "🎮" },
|
|
||||||
];
|
|
||||||
|
|
||||||
// 3) Optional “network” tag rules based on NAME text (not $server.network)
|
|
||||||
// (Useful if providers shove "BGP/IPLC" into the node name)
|
|
||||||
const NAME_NETWORK_TAGS = [
|
|
||||||
{ regex: /\bIPLC\b/gi, tag: "🛰️" },
|
|
||||||
{ regex: /\bBGP\b/gi, tag: "🧭" },
|
|
||||||
{ regex: /\bAnycast\b/gi, tag: "🌍" }
|
|
||||||
];
|
|
||||||
|
|
||||||
// 4) Country detection rules by NAME (regex). First match wins (priority = lower is earlier)
|
|
||||||
const COUNTRY_RULES = [
|
|
||||||
// USA
|
|
||||||
{ regex: /\b(USA|US|UNITED\s*STATES|AMERICA|NEW\s*YORK|NYC|LOS\s*ANGELES|LA|DALLAS|CHI(CAGO)?)\b/i, iso3: "USA", flag: "🇺🇸", priority: 10 },
|
|
||||||
|
|
||||||
// Germany
|
|
||||||
{ regex: /\b(DE|DEU|GER(MANY)?|FRANKFURT|BERLIN|MUNICH|MÜNCHEN)\b/i, iso3: "DEU", flag: "🇩🇪", priority: 20 },
|
|
||||||
|
|
||||||
// Netherlands
|
|
||||||
{ regex: /\b(NL|NLD|NETHERLANDS|HOLLAND|AMSTERDAM)\b/i, iso3: "NLD", flag: "🇳🇱", priority: 30 },
|
|
||||||
|
|
||||||
// UK
|
|
||||||
{ regex: /\b(UK|GB|GBR|UNITED\s*KINGDOM|LONDON|MANCHESTER)\b/i, iso3: "GBR", flag: "🇬🇧", priority: 40 },
|
|
||||||
|
|
||||||
// France
|
|
||||||
{ regex: /\b(FR|FRA|FRANCE|PARIS|MARSEILLE)\b/i, iso3: "FRA", flag: "🇫🇷", priority: 50 },
|
|
||||||
|
|
||||||
// Poland
|
|
||||||
{ regex: /\b(PL|POL|POLAND|WARSAW|WARSZAWA)\b/i, iso3: "POL", flag: "🇵🇱", priority: 60 },
|
|
||||||
|
|
||||||
// Finland
|
|
||||||
{ regex: /\b(FI|FIN|FINLAND|HELSINKI)\b/i, iso3: "FIN", flag: "🇫🇮", priority: 70 },
|
|
||||||
|
|
||||||
// Sweden
|
|
||||||
{ regex: /\b(SE|SWE|SWEDEN|STOCKHOLM)\b/i, iso3: "SWE", flag: "🇸🇪", priority: 80 },
|
|
||||||
|
|
||||||
// Norway
|
|
||||||
{ regex: /\b(NO|NOR|NORWAY|OSLO)\b/i, iso3: "NOR", flag: "🇳🇴", priority: 90 },
|
|
||||||
|
|
||||||
// Switzerland
|
|
||||||
{ regex: /\b(CH|CHE|SWITZERLAND|ZURICH|GENEVA)\b/i, iso3: "CHE", flag: "🇨🇭", priority: 100 },
|
|
||||||
|
|
||||||
// Estonia / Latvia / Lithuania
|
|
||||||
{ regex: /\b(EE|EST|ESTONIA|TALLINN)\b/i, iso3: "EST", flag: "🇪🇪", priority: 110 },
|
|
||||||
{ regex: /\b(LV|LVA|LATVIA|RIGA)\b/i, iso3: "LVA", flag: "🇱🇻", priority: 120 },
|
|
||||||
{ regex: /\b(LT|LTU|LITHUANIA|VILNIUS)\b/i, iso3: "LTU", flag: "🇱🇹", priority: 130 },
|
|
||||||
|
|
||||||
// Turkey
|
|
||||||
{ regex: /\b(TR|TUR|TURKEY|ISTANBUL)\b/i, iso3: "TUR", flag: "🇹🇷", priority: 140 },
|
|
||||||
|
|
||||||
// Singapore / Japan / Korea / Hong Kong
|
|
||||||
{ regex: /\b(SG|SGP|SINGAPORE)\b/i, iso3: "SGP", flag: "🇸🇬", priority: 200 },
|
|
||||||
{ regex: /\b(JP|JPN|JAPAN|TOKYO|OSAKA)\b/i, iso3: "JPN", flag: "🇯🇵", priority: 210 },
|
|
||||||
{ regex: /\b(KR|KOR|KOREA|SEOUL)\b/i, iso3: "KOR", flag: "🇰🇷", priority: 220 },
|
|
||||||
{ regex: /\b(HK|HKG|HONG\s*KONG)\b/i, iso3: "HKG", flag: "🇭🇰", priority: 230 },
|
|
||||||
];
|
|
||||||
|
|
||||||
// 5) GeoIP mapping (ISO2 -> ISO3 + flag) used only if utils.geoip.lookup(ip) returns ISO2
|
|
||||||
const ISO2_TO_ISO3 = {
|
|
||||||
US: { iso3: "USA", flag: "🇺🇸" },
|
|
||||||
DE: { iso3: "DEU", flag: "🇩🇪" },
|
|
||||||
NL: { iso3: "NLD", flag: "🇳🇱" },
|
|
||||||
GB: { iso3: "GBR", flag: "🇬🇧" },
|
|
||||||
FR: { iso3: "FRA", flag: "🇫🇷" },
|
|
||||||
PL: { iso3: "POL", flag: "🇵🇱" },
|
|
||||||
FI: { iso3: "FIN", flag: "🇫🇮" },
|
|
||||||
SE: { iso3: "SWE", flag: "🇸🇪" },
|
|
||||||
NO: { iso3: "NOR", flag: "🇳🇴" },
|
|
||||||
CH: { iso3: "CHE", flag: "🇨🇭" },
|
|
||||||
EE: { iso3: "EST", flag: "🇪🇪" },
|
|
||||||
LV: { iso3: "LVA", flag: "🇱🇻" },
|
|
||||||
LT: { iso3: "LTU", flag: "🇱🇹" },
|
|
||||||
TR: { iso3: "TUR", flag: "🇹🇷" },
|
|
||||||
SG: { iso3: "SGP", flag: "🇸🇬" },
|
|
||||||
JP: { iso3: "JPN", flag: "🇯🇵" },
|
|
||||||
KR: { iso3: "KOR", flag: "🇰🇷" },
|
|
||||||
HK: { iso3: "HKG", flag: "🇭🇰" },
|
|
||||||
};
|
|
||||||
|
|
||||||
// 6) Protocol icons (based on proxy.type)
|
|
||||||
const PROTOCOL_ICONS = {
|
|
||||||
ss: "🔒",
|
|
||||||
ssr: "☂️",
|
|
||||||
vmess: "🪁",
|
|
||||||
vless: "🌌",
|
|
||||||
trojan: "🐎",
|
|
||||||
http: "🌐",
|
|
||||||
socks5: "🧦",
|
|
||||||
snell: "🐌",
|
|
||||||
wireguard: "🐲",
|
|
||||||
hysteria: "🤪",
|
|
||||||
hysteria2: "⚡",
|
|
||||||
tuic: "🚅"
|
|
||||||
};
|
|
||||||
|
|
||||||
///////////////////////
|
|
||||||
// HELPERS
|
|
||||||
///////////////////////
|
|
||||||
|
|
||||||
function isIPv4(str) {
|
|
||||||
if (typeof str !== "string") return false;
|
|
||||||
const m = str.match(/^(\d{1,3})(\.\d{1,3}){3}$/);
|
|
||||||
if (!m) return false;
|
|
||||||
return str.split(".").every(oct => {
|
|
||||||
const n = Number(oct);
|
|
||||||
return n >= 0 && n <= 255 && String(n) === oct.replace(/^0+(\d)/, "$1"); // avoids "001" weirdness
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function uniq(arr) {
|
|
||||||
return [...new Set(arr.filter(Boolean))];
|
|
||||||
}
|
|
||||||
|
|
||||||
function sanitizeBaseName(name) {
|
|
||||||
let s = String(name || "");
|
|
||||||
|
|
||||||
// Remove noise patterns
|
|
||||||
for (const re of NOISE_PATTERNS) s = s.replace(re, " ");
|
|
||||||
|
|
||||||
// Collapse spaces
|
|
||||||
s = s.replace(/\s+/g, " ").trim();
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractIconTagsAndStrip(name) {
|
|
||||||
let s = String(name || "");
|
|
||||||
const tags = [];
|
|
||||||
|
|
||||||
for (const r of ICON_RULES) {
|
|
||||||
if (r.regex.test(s)) {
|
|
||||||
tags.push(r.icon);
|
|
||||||
s = s.replace(r.regex, " ");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const t of NAME_NETWORK_TAGS) {
|
|
||||||
if (t.regex.test(s)) {
|
|
||||||
tags.push(t.tag);
|
|
||||||
s = s.replace(t.regex, " ");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { stripped: s.replace(/\s+/g, " ").trim(), tags: uniq(tags) };
|
|
||||||
}
|
|
||||||
|
|
||||||
function detectCountryByName(name) {
|
|
||||||
const n = String(name || "");
|
|
||||||
// Order by priority, then first match wins
|
|
||||||
const sorted = COUNTRY_RULES.slice().sort((a, b) => a.priority - b.priority);
|
|
||||||
for (const c of sorted) {
|
|
||||||
if (c.regex.test(n)) return { iso3: c.iso3, flag: c.flag, priority: c.priority, source: "name" };
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function detectCountryByGeoIP(server, utils) {
|
|
||||||
if (!isIPv4(server)) return null;
|
|
||||||
if (!utils || !utils.geoip || typeof utils.geoip.lookup !== "function") return null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const geo = utils.geoip.lookup(server);
|
|
||||||
const iso2 = geo && (geo.country || geo.country_code || geo.iso_code);
|
|
||||||
if (!iso2 || typeof iso2 !== "string") return null;
|
|
||||||
|
|
||||||
const key = iso2.toUpperCase();
|
|
||||||
const mapped = ISO2_TO_ISO3[key];
|
|
||||||
if (mapped) return { iso3: mapped.iso3, flag: mapped.flag, priority: 900, source: "geoip" };
|
|
||||||
|
|
||||||
// Unknown ISO2: keep something sane
|
|
||||||
return { iso3: key, flag: "🏳️", priority: 950, source: "geoip" };
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function pad2(n) {
|
|
||||||
const x = Number(n);
|
|
||||||
return x < 10 ? `0${x}` : String(x);
|
|
||||||
}
|
|
||||||
|
|
||||||
function safeStr(v) {
|
|
||||||
return (v === undefined || v === null) ? "" : String(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////
|
|
||||||
// OPERATOR
|
|
||||||
///////////////////////
|
|
||||||
|
|
||||||
function operator(proxies, targetPlatform, utils) {
|
|
||||||
// Sub-Store sometimes passes utils as global $utils; sometimes as 3rd arg; sometimes not at all.
|
|
||||||
// We'll accept any of them without whining.
|
|
||||||
const U = utils || (typeof $utils !== "undefined" ? $utils : null);
|
|
||||||
|
|
||||||
const buckets = Object.create(null);
|
|
||||||
|
|
||||||
for (const proxy of proxies) {
|
|
||||||
const originalName = safeStr(proxy && proxy.name);
|
|
||||||
|
|
||||||
// 1) Extract tags (icons) from ORIGINAL name, then strip those keywords out
|
|
||||||
const iconStage = extractIconTagsAndStrip(originalName);
|
|
||||||
|
|
||||||
// 2) Sanitize remaining base name (remove marketing trash, brackets, etc.)
|
|
||||||
const cleanBase = sanitizeBaseName(iconStage.stripped);
|
|
||||||
|
|
||||||
// 3) Detect country (name first, then GeoIP)
|
|
||||||
const byName = detectCountryByName(originalName);
|
|
||||||
const byGeo = detectCountryByGeoIP(proxy && proxy.server, U);
|
|
||||||
const country = byName || byGeo || { iso3: "UNK", flag: "🏴☠️", priority: 9999, source: "fallback" };
|
|
||||||
|
|
||||||
// 4) Protocol icon (based on type)
|
|
||||||
const proto = PROTOCOL_ICONS[(proxy && proxy.type) || ""] || "🔌";
|
|
||||||
|
|
||||||
// 5) Network/type/port tag (from proxy fields)
|
|
||||||
const net = safeStr(proxy && proxy.network) || "net?";
|
|
||||||
const typ = safeStr(proxy && proxy.type) || "type?";
|
|
||||||
const port = safeStr(proxy && proxy.port) || "port?";
|
|
||||||
const metaTag = `▫️${net}/${typ}/${port}`;
|
|
||||||
|
|
||||||
// 6) Prepare bucket key
|
|
||||||
const key = country.iso3;
|
|
||||||
|
|
||||||
if (!buckets[key]) {
|
|
||||||
buckets[key] = {
|
|
||||||
country,
|
|
||||||
list: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep meta used for sorting and final formatting
|
|
||||||
buckets[key].list.push({
|
|
||||||
proxy,
|
|
||||||
_meta: {
|
|
||||||
cleanBase,
|
|
||||||
iconTags: iconStage.tags,
|
|
||||||
proto,
|
|
||||||
metaTag
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 7) Sort buckets by priority
|
|
||||||
const bucketKeys = Object.keys(buckets).sort((a, b) => {
|
|
||||||
return (buckets[a].country.priority || 9999) - (buckets[b].country.priority || 9999);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 8) Sort inside each country bucket and rename with per-country numbering
|
|
||||||
const result = [];
|
|
||||||
|
|
||||||
for (const key of bucketKeys) {
|
|
||||||
const group = buckets[key];
|
|
||||||
|
|
||||||
group.list.sort((A, B) => {
|
|
||||||
// Tags do not affect numbering: sort only by sanitized base + server:port as tie-breaker
|
|
||||||
const an = A._meta.cleanBase.toLowerCase();
|
|
||||||
const bn = B._meta.cleanBase.toLowerCase();
|
|
||||||
if (an !== bn) return an.localeCompare(bn);
|
|
||||||
|
|
||||||
const as = `${safeStr(A.proxy.server)}:${safeStr(A.proxy.port)}`;
|
|
||||||
const bs = `${safeStr(B.proxy.server)}:${safeStr(B.proxy.port)}`;
|
|
||||||
return as.localeCompare(bs);
|
|
||||||
});
|
|
||||||
|
|
||||||
for (let i = 0; i < group.list.length; i++) {
|
|
||||||
const item = group.list[i];
|
|
||||||
const p = item.proxy;
|
|
||||||
const num = pad2(i + 1);
|
|
||||||
|
|
||||||
const tagStr = item._meta.iconTags.length ? ` ${item._meta.iconTags.join(" ")}` : "";
|
|
||||||
// Final name format:
|
|
||||||
// 🇩🇪 DEU-03 🌌 📺 ❻ ▫️ws/vless/443
|
|
||||||
p.name = `${group.country.flag} ${group.country.iso3}-${num} ${item._meta.proto}${tagStr} ${item._meta.metaTag}`.replace(/\s+/g, " ").trim();
|
|
||||||
|
|
||||||
result.push(p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
@@ -1,540 +0,0 @@
|
|||||||
/**
|
|
||||||
* Sub-Store operator: Normalize + tag + country detect + per-country numbering
|
|
||||||
*
|
|
||||||
* Output format (default):
|
|
||||||
* 🇺🇸 USA-01 🔒 📺 ❻ ▫️ws/vless/443
|
|
||||||
*
|
|
||||||
* Notes:
|
|
||||||
* - Numbering is computed per-country AFTER grouping the full list.
|
|
||||||
* - Tags (icons) do NOT affect numbering order.
|
|
||||||
* - GeoIP is optional and only used when server is an IP and utils.geoip.lookup exists.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// --- OPTIONS PARSING ---
|
|
||||||
function normalizeOptions() {
|
|
||||||
const args = (typeof $arguments !== "undefined" && $arguments) ? $arguments : {};
|
|
||||||
const asBool = (v, def = false) => {
|
|
||||||
if (v === undefined || v === null || v === "") return def;
|
|
||||||
if (typeof v === "boolean") return v;
|
|
||||||
const s = String(v).toLowerCase().trim();
|
|
||||||
if (["1", "true", "yes", "y", "on"].includes(s)) return true;
|
|
||||||
if (["0", "false", "no", "n", "off"].includes(s)) return false;
|
|
||||||
return def;
|
|
||||||
};
|
|
||||||
return {
|
|
||||||
appendOriginalName: asBool(args['append-original'], false),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////
|
|
||||||
// CONFIG (EDIT ME)
|
|
||||||
///////////////////////
|
|
||||||
|
|
||||||
const DEBUG_APPEND_ORIGINAL_NAME = false; // set true to enable debug mode (appends original name as comment)
|
|
||||||
|
|
||||||
// 1) Remove these patterns (marketing noise, brackets, separators, etc.)
|
|
||||||
const NOISE_PATTERNS = [
|
|
||||||
/\[[^\]]*]/g, // [ ... ]
|
|
||||||
/\([^)]*\)/g, // ( ... )
|
|
||||||
/\{[^}]*}/g, // { ... }
|
|
||||||
/\btraffic\b/gi,
|
|
||||||
/\bfree\b/gi,
|
|
||||||
/\bwebsite\b/gi,
|
|
||||||
/\bexpire\b/gi,
|
|
||||||
/\blow\s*ping\b/gi,
|
|
||||||
/\bai\s*studio\b/gi,
|
|
||||||
/\bno\s*p2p\b/gi,
|
|
||||||
/\b10\s*gbit\b/gi,
|
|
||||||
/\bvless\b/gi,
|
|
||||||
/\bvmess\b/gi,
|
|
||||||
/\bssr?\b/gi,
|
|
||||||
/\btrojan\b/gi,
|
|
||||||
/\bhysteria2?\b/gi,
|
|
||||||
/\btuic\b/gi,
|
|
||||||
/[|]/g,
|
|
||||||
/[_]+/g,
|
|
||||||
/[-]{2,}/g
|
|
||||||
];
|
|
||||||
|
|
||||||
// 2) Keyword -> icon tags (if found in original name, icon is added; the keyword is removed from base name)
|
|
||||||
// 🇫🇿 🇺🇳 🇩🇻 🇻🇿 🇵🇷 🇦🇿 🇬🇺🇦🇷🇩
|
|
||||||
// 🌀 - double hop
|
|
||||||
const ICON_RULES = [
|
|
||||||
{ regex: /TEST/gi, icon: "🧪" },
|
|
||||||
{ regex: uWordBoundaryGroup("Low Ping"), icon: "⚡️" },
|
|
||||||
{ regex: uWordBoundaryGroup("YT|Russia|Россия"), icon: "📺" },
|
|
||||||
{ regex: uWordBoundaryGroup("IPv6"), icon: "🎱" },
|
|
||||||
{ regex: uWordBoundaryGroup("Gemini|AI Studio"), icon: "🤖" },
|
|
||||||
{ regex: uWordBoundaryGroup("Torrent|P2P|P2P-Torrents"), icon: "🧲" },
|
|
||||||
|
|
||||||
{ regex: uWordBoundaryGroup("local"), icon: "🚪" },
|
|
||||||
{ regex: uWordBoundaryGroup("neighbourhood"), icon: "🫂" },
|
|
||||||
|
|
||||||
{ regex: uWordBoundaryGroup("🌀|Мост⚡|Мост|-Мост⚡"), icon: "🌀" },
|
|
||||||
{ regex: uWordBoundaryGroup("Авто|Balance"), icon: "⚖️" },
|
|
||||||
|
|
||||||
|
|
||||||
{ regex: uWordBoundaryGroup("xfizz|x-fizz"), icon: " 🇫" },
|
|
||||||
{ regex: uWordBoundaryGroup("uncd|unicade"), icon: " 🇺" },
|
|
||||||
{ regex: uWordBoundaryGroup("vzdh|vezdehod"), icon: " 🇻" },
|
|
||||||
{ regex: uWordBoundaryGroup("dvpn|d-vpn"), icon: " 🇩" },
|
|
||||||
{ regex: uWordBoundaryGroup("ovsc|oversecure"), icon: " 🇴" },
|
|
||||||
{ regex: uWordBoundaryGroup("snow|snowy") , icon: " 🇸" },
|
|
||||||
{ regex: uWordBoundaryGroup("proton"), icon: " 🇵" },
|
|
||||||
{ regex: uWordBoundaryGroup("amnezia"), icon: " 🇦" },
|
|
||||||
{ regex: uWordBoundaryGroup("adguard"), icon: " 🇬" },
|
|
||||||
];
|
|
||||||
|
|
||||||
// 3) Optional “network” tag rules based on NAME text (not $server.network)
|
|
||||||
// (Useful if providers shove "BGP/IPLC" into the node name)
|
|
||||||
const NAME_NETWORK_TAGS = [
|
|
||||||
{ regex: uWordBoundaryGroup("IPLC"), tag: "🛰️" },
|
|
||||||
{ regex: uWordBoundaryGroup("BGP"), tag: "🧭" },
|
|
||||||
{ regex: uWordBoundaryGroup("Anycast"), tag: "🌍" }
|
|
||||||
];
|
|
||||||
|
|
||||||
// 4) Country detection rules by NAME (regex). First match wins (priority = lower is earlier)
|
|
||||||
const COUNTRY_RULES = [
|
|
||||||
{ regex: uWordBoundaryGroup("(Аргентина|Argentina|AR|ARG|ARGENTINA|BUENOS\s*AIRES)"), iso3: "ARG", flag: "🇦🇷", priority: 100 }, // Argentina
|
|
||||||
{ regex: uWordBoundaryGroup("(Australia|AU|AUS|AUSTRALIA|SYDNEY)"), iso3: "AUS", flag: "🇦🇺", priority: 110 }, // Australia
|
|
||||||
{ regex: uWordBoundaryGroup("(Austria|AT|AUT|AUSTRIA|VIENNA)"), iso3: "AUT", flag: "🇦🇹", priority: 120 }, // Austria
|
|
||||||
{ regex: uWordBoundaryGroup("(Беларусь|Белоруссия|BELARUS)"), iso3: "BLR", flag: "🇧🇾", priority: 130 }, // Belarus
|
|
||||||
{ regex: uWordBoundaryGroup("(Brazil|BR|BRA|BRAZIL|SAO\s*PAULO)"), iso3: "BRA", flag: "🇧🇷", priority: 140 }, // Brazil
|
|
||||||
{ regex: uWordBoundaryGroup("(Bulgaria|BG|BGR|BULGARIA|SOFIA)"), iso3: "BGR", flag: "🇧🇬", priority: 150 }, // Bulgaria
|
|
||||||
{ regex: uWordBoundaryGroup("(Canada|CA|CAN|CANADA|TORONTO)"), iso3: "CAN", flag: "🇨🇦", priority: 160 }, // Canada
|
|
||||||
{ regex: uWordBoundaryGroup("(КИТАЙ|China)"), iso3: "CHN", flag: "🇨🇳", priority: 170 }, // China
|
|
||||||
{ regex: uWordBoundaryGroup("(Czech\s*Republic|CZ|CZE|CZECH|PRAGUE)"), iso3: "CZE", flag: "🇨🇿", priority: 180 }, // Czech Republic
|
|
||||||
{ regex: uWordBoundaryGroup("(Denmark|DK|DNK|DENMARK|COPENHAGEN)"), iso3: "DNK", flag: "🇩🇰", priority: 190 }, // Denmark
|
|
||||||
{ regex: uWordBoundaryGroup("(Egypt|EG|EGY|EGYPT|CAIRO)"), iso3: "EGY", flag: "🇪🇬", priority: 200 }, // Egypt
|
|
||||||
{ regex: uWordBoundaryGroup("(Эстония|EE|EST|ESTONIA|TALLINN)"), iso3: "EST", flag: "🇪🇪", priority: 210 }, // Estonia
|
|
||||||
{ regex: uWordBoundaryGroup("(Финляндия|FI|FIN|FINLAND|HELSINKI)"), iso3: "FIN", flag: "🇫🇮", priority: 220 }, // Finland
|
|
||||||
{ regex: uWordBoundaryGroup("(Франция|FR|FRA|FRANCE|PARIS|MARSEILLE)"), iso3: "FRA", flag: "🇫🇷", priority: 230 }, // France
|
|
||||||
{ regex: uWordBoundaryGroup("(Georgia|GE|GEO|GEORGIA|TBILISI)"), iso3: "GEO", flag: "🇬🇪", priority: 240 }, // Georgia
|
|
||||||
{ regex: uWordBoundaryGroup("(Германия|DE|DEU|GER(MANY)?|FRANKFURT|BERLIN|MUNICH)"), iso3: "DEU", flag: "🇩🇪", priority: 250 }, // Germany
|
|
||||||
{ regex: uWordBoundaryGroup("(Гонконг|HK|HKG|HONG\s*KONG)"), iso3: "HKG", flag: "🇭🇰", priority: 260 }, // Hong Kong
|
|
||||||
{ regex: uWordBoundaryGroup("(India|IN|IND|INDIA|MUMBAI)"), iso3: "IND", flag: "🇮🇳", priority: 270 }, // India
|
|
||||||
{ regex: uWordBoundaryGroup("(Ireland|IE|IRL|IRELAND|DUBLIN)"), iso3: "IRL", flag: "🇮🇪", priority: 280 }, // Ireland
|
|
||||||
{ regex: uWordBoundaryGroup("(Israel|IL|ISR|ISRAEL|TEL\s*AVIV)"), iso3: "ISR", flag: "🇮🇱", priority: 290 }, // Israel
|
|
||||||
{ regex: uWordBoundaryGroup("(Italy|IT|ITA|ITALY|ROME)"), iso3: "ITA", flag: "🇮🇹", priority: 300 }, // Italy
|
|
||||||
{ regex: uWordBoundaryGroup("(Япония|JP|JPN|JAPAN|TOKYO|OSAKA)"), iso3: "JPN", flag: "🇯🇵", priority: 310 }, // Japan
|
|
||||||
{ regex: uWordBoundaryGroup("(Kazakhstan|KZ|KAZ|KAZAKHSTAN|ALMATY)"), iso3: "KAZ", flag: "🇰🇿", priority: 320 }, // Kazakhstan
|
|
||||||
{ regex: uWordBoundaryGroup("(Латвия|LV|LVA|LATVIA|RIGA)"), iso3: "LVA", flag: "🇱🇻", priority: 330 }, // Latvia
|
|
||||||
{ regex: uWordBoundaryGroup("(Литва|LT|LTU|LITHUANIA|VILNIUS)"), iso3: "LTU", flag: "🇱🇹", priority: 340 }, // Lithuania
|
|
||||||
{ regex: uWordBoundaryGroup("(Malaysia|MY|MYS|MALAYSIA|KUALA\s*LUMPUR)"), iso3: "MYS", flag: "🇲🇾", priority: 350 }, // Malaysia
|
|
||||||
{ regex: uWordBoundaryGroup("(Moldova|MD|MDA|MOLDOVA|CHISINAU)"), iso3: "MDA", flag: "🇲🇩", priority: 360 }, // Moldova
|
|
||||||
{ regex: uWordBoundaryGroup("(Нидерланды|NL|NLD|NETHERLANDS|HOLLAND|AMSTERDAM)"), iso3: "NLD", flag: "🇳🇱", priority: 370 }, // Netherlands
|
|
||||||
{ regex: uWordBoundaryGroup("(Nigeria|NG|NGA|NIGERIA|LAGOS)"), iso3: "NGA", flag: "🇳🇬", priority: 380 }, // Nigeria
|
|
||||||
{ regex: uWordBoundaryGroup("(Норвегия|NO|NOR|NORWAY|OSLO)"), iso3: "NOR", flag: "🇳🇴", priority: 390 }, // Norway
|
|
||||||
{ regex: uWordBoundaryGroup("(Philippines|PH|PHL|PHILIPPINES|MANILA)"), iso3: "PHL", flag: "🇵🇭", priority: 400 }, // Philippines
|
|
||||||
{ regex: uWordBoundaryGroup("(Польша|PL|POL|POLAND|WARSAW|WARSZAWA)"), iso3: "POL", flag: "🇵🇱", priority: 410 }, // Poland
|
|
||||||
{ regex: uWordBoundaryGroup("(Portugal|PT|PRT|PORTUGAL|LISBON)"), iso3: "PRT", flag: "🇵🇹", priority: 420 }, // Portugal
|
|
||||||
{ regex: uWordBoundaryGroup("(Romania|RO|ROU|ROMANIA|BUCHAREST)"), iso3: "ROU", flag: "🇷🇴", priority: 430 }, // Romania
|
|
||||||
{ regex: uWordBoundaryGroup("(Russia|RU|RUS|RUSSIA|MOSCOW)"), iso3: "RUS", flag: "🇷🇺", priority: 440 }, // Russia
|
|
||||||
{ regex: uWordBoundaryGroup("(Сингапур|SG|SGP|SINGAPORE)"), iso3: "SGP", flag: "🇸🇬", priority: 200 }, // Singapore
|
|
||||||
{ regex: uWordBoundaryGroup("(South Korea|Корея|KR|KOR|KOREA|SEOUL)"), iso3: "KOR", flag: "🇰🇷", priority: 450 }, // South Korea
|
|
||||||
{ regex: uWordBoundaryGroup("(Spain|ES|ESP|SPAIN|MADRID)"), iso3: "ESP", flag: "🇪🇸", priority: 460 }, // Spain
|
|
||||||
{ regex: uWordBoundaryGroup("(Швеция|SE|SWE|SWEDEN|STOCKHOLM)"), iso3: "SWE", flag: "🇸🇪", priority: 470 }, // Sweden
|
|
||||||
{ regex: uWordBoundaryGroup("(Швейцария|CH|CHE|SWITZERLAND|Switzerl)"), iso3: "CHE", flag: "🇨🇭", priority: 480 }, // Switzerland
|
|
||||||
{ regex: uWordBoundaryGroup("(Taiwan|TW|TWN|TAIWAN|TAIPEI)"), iso3: "TWN", flag: "🇹🇼", priority: 490 }, // Taiwan
|
|
||||||
{ regex: uWordBoundaryGroup("(Thailand|TH|THA|THAILAND|BANGKOK)"), iso3: "THA", flag: "🇹🇭", priority: 500 }, // Thailand
|
|
||||||
{ regex: uWordBoundaryGroup("(Турция|TR|TUR|TURKEY|ISTANBUL)"), iso3: "TUR", flag: "🇹🇷", priority: 510 }, // Turkey
|
|
||||||
{ regex: uWordBoundaryGroup("(UAE|United\s*Arab\s*Emirates|AE|ARE|DUBAI)"), iso3: "ARE", flag: "🇦🇪", priority: 520 }, // UAE
|
|
||||||
{ regex: uWordBoundaryGroup("(Великобритания|Англия|England|UK|GB|GBR|UNITED\s*KINGDOM)"), iso3: "GBR", flag: "🇬🇧", priority: 530 }, // UK
|
|
||||||
{ regex: uWordBoundaryGroup("(США|USA|US|UNITED\s*STATES|AMERICA|NEW\s*YORK|NYC)"), iso3: "USA", flag: "🇺🇸", priority: 540 }, // USA
|
|
||||||
{ regex: uWordBoundaryGroup("(Vietnam|VN|VNM|VIETNAM|HANOI)"), iso3: "VNM", flag: "🇻🇳", priority: 500 } // Vietnam
|
|
||||||
];
|
|
||||||
|
|
||||||
// 5) GeoIP mapping (ISO2 -> ISO3 + flag) used only if utils.geoip.lookup(ip) returns ISO2
|
|
||||||
const ISO2_TO_ISO3 = {
|
|
||||||
US: { iso3: "USA", flag: "🇺🇸" },
|
|
||||||
DE: { iso3: "DEU", flag: "🇩🇪" },
|
|
||||||
NL: { iso3: "NLD", flag: "🇳🇱" },
|
|
||||||
GB: { iso3: "GBR", flag: "🇬🇧" },
|
|
||||||
FR: { iso3: "FRA", flag: "🇫🇷" },
|
|
||||||
PL: { iso3: "POL", flag: "🇵🇱" },
|
|
||||||
FI: { iso3: "FIN", flag: "🇫🇮" },
|
|
||||||
SE: { iso3: "SWE", flag: "🇸🇪" },
|
|
||||||
NO: { iso3: "NOR", flag: "🇳🇴" },
|
|
||||||
CH: { iso3: "CHE", flag: "🇨🇭" },
|
|
||||||
EE: { iso3: "EST", flag: "🇪🇪" },
|
|
||||||
LV: { iso3: "LVA", flag: "🇱🇻" },
|
|
||||||
LT: { iso3: "LTU", flag: "🇱🇹" },
|
|
||||||
TR: { iso3: "TUR", flag: "🇹🇷" },
|
|
||||||
SG: { iso3: "SGP", flag: "🇸🇬" },
|
|
||||||
JP: { iso3: "JPN", flag: "🇯🇵" },
|
|
||||||
KR: { iso3: "KOR", flag: "🇰🇷" },
|
|
||||||
HK: { iso3: "HKG", flag: "🇭🇰" },
|
|
||||||
};
|
|
||||||
|
|
||||||
// 6) Protocol icons (based on proxy.type)
|
|
||||||
const PROTOCOL_ICONS = {
|
|
||||||
ss: "",
|
|
||||||
ssr: "",
|
|
||||||
vmess: "",
|
|
||||||
vless: "",
|
|
||||||
trojan: "",
|
|
||||||
http: "",
|
|
||||||
socks5: "",
|
|
||||||
snell: "",
|
|
||||||
wireguard: "",
|
|
||||||
hysteria: "",
|
|
||||||
hysteria2: "",
|
|
||||||
tuic: ""
|
|
||||||
};
|
|
||||||
|
|
||||||
const STANDARD_PORTS_BY_TYPE = {
|
|
||||||
wireguard: new Set(["51820"]),
|
|
||||||
vless: new Set(["443"]),
|
|
||||||
trojan: new Set(["443"]),
|
|
||||||
ss: new Set(["443"]),
|
|
||||||
};
|
|
||||||
|
|
||||||
const PROTOCOL_ICON_DEFAULT = ""; // fallback icon if type is unknown
|
|
||||||
|
|
||||||
|
|
||||||
const METATAG_RULES = {
|
|
||||||
// Keys are "network/type" OR "/type" (network-agnostic) OR "network/" (type-agnostic)
|
|
||||||
// Matching priority: exact "network/type" -> "/type" -> "network/" -> default
|
|
||||||
// 🅶🆃 🆃🆂 🆃🆅 🆆🆅 🆇🆅 🆆🅶 🅽🅸
|
|
||||||
pairMap: {
|
|
||||||
"grpc/trojan": "🅶🆃",
|
|
||||||
"tcp/trojan": "🆃🆃",
|
|
||||||
"tcp/ss": "🆃🆂",
|
|
||||||
"tcp/vless": "🆃🆅",
|
|
||||||
"ws/vless": "🆆🆅",
|
|
||||||
"xhttp/vless": "🆇🆅",
|
|
||||||
"grpc/vless": "🅶🆅",
|
|
||||||
|
|
||||||
"/wireguard": "🆆🅶",
|
|
||||||
"/naive": "🅽🅸",
|
|
||||||
},
|
|
||||||
|
|
||||||
defaultPair: "▫️", // fallback if nothing matches
|
|
||||||
includeFallbackText: false, // if true, append "(net/type)" when defaultPair is used
|
|
||||||
};
|
|
||||||
|
|
||||||
// Port formatting: superscript digits with left padding to 4 chars
|
|
||||||
// 𝟎𝟏𝟐𝟑𝟒𝟓𝟔𝟕𝟖𝟗
|
|
||||||
const PORT_FORMAT = {
|
|
||||||
padLeftTo: 3,
|
|
||||||
padChar: "0",
|
|
||||||
fancy: {
|
|
||||||
"0": "𝟎", "1": "𝟏", "2": "𝟐", "3": "𝟑", "4": "𝟒", "5": "𝟓", "6": "𝟔", "7": "𝟕", "8": "𝟖", "9": "𝟗",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
///////////////////////
|
|
||||||
// HELPERS
|
|
||||||
///////////////////////
|
|
||||||
|
|
||||||
function normalizeToken(s) {
|
|
||||||
return String(s || "").trim().toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
function uWordBoundaryGroup(inner) {
|
|
||||||
// Match if surrounded by non-letter/non-digit (Unicode-aware)
|
|
||||||
// We don't use lookbehind for max compatibility.
|
|
||||||
return new RegExp(`(?:^|[^\\p{L}\\p{N}])(?:${inner})(?=$|[^\\p{L}\\p{N}])`, "iu");
|
|
||||||
}
|
|
||||||
|
|
||||||
function portToFancy(port, type) {
|
|
||||||
let p = String(port ?? "").trim();
|
|
||||||
p = p.replace(/[^\d]/g, "");
|
|
||||||
if (!p) return "";
|
|
||||||
|
|
||||||
if (STANDARD_PORTS_BY_TYPE[type]?.has(p)) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// left pad to fixed width
|
|
||||||
//if (PORT_FORMAT.padLeftTo && p.length < PORT_FORMAT.padLeftTo) {
|
|
||||||
// p = p.padStart(PORT_FORMAT.padLeftTo, PORT_FORMAT.padChar);
|
|
||||||
//}
|
|
||||||
|
|
||||||
// map digits
|
|
||||||
//let out = "";
|
|
||||||
//for (const ch of p) out += PORT_FORMAT.fancy[ch] ?? ch;
|
|
||||||
out = "✳️"
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildMetaTag(proxy) {
|
|
||||||
const net = safeStr(proxy && proxy.network) || "";
|
|
||||||
const typ = safeStr(proxy && proxy.type) || "";
|
|
||||||
const port = safeStr(proxy && proxy.port);
|
|
||||||
|
|
||||||
const { icon, matched } = metaPairIcon(net, typ);
|
|
||||||
const portSup = portToFancy(port, typ);
|
|
||||||
|
|
||||||
if (icon === METATAG_RULES.defaultPair && METATAG_RULES.includeFallbackText) {
|
|
||||||
return `${icon}(${normalizeToken(net)}/${normalizeToken(typ)})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${icon}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function metaPairIcon(network, type) {
|
|
||||||
const net = normalizeToken(network);
|
|
||||||
const typ = normalizeToken(type);
|
|
||||||
|
|
||||||
const exact = `${net}/${typ}`;
|
|
||||||
const typeOnly = `/${typ}`;
|
|
||||||
const netOnly = `${net}/`;
|
|
||||||
|
|
||||||
const m = METATAG_RULES.pairMap;
|
|
||||||
|
|
||||||
if (m[exact]) return { icon: m[exact], matched: exact };
|
|
||||||
if (m[typeOnly]) return { icon: m[typeOnly], matched: typeOnly };
|
|
||||||
if (m[netOnly]) return { icon: m[netOnly], matched: netOnly };
|
|
||||||
|
|
||||||
return { icon: METATAG_RULES.defaultPair, matched: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
function isIPv4(str) {
|
|
||||||
if (typeof str !== "string") return false;
|
|
||||||
const m = str.match(/^(\d{1,3})(\.\d{1,3}){3}$/);
|
|
||||||
if (!m) return false;
|
|
||||||
return str.split(".").every(oct => {
|
|
||||||
const n = Number(oct);
|
|
||||||
return n >= 0 && n <= 255 && String(n) === oct.replace(/^0+(\d)/, "$1"); // avoids "001" weirdness
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function uniq(arr) {
|
|
||||||
return [...new Set(arr.filter(Boolean))];
|
|
||||||
}
|
|
||||||
|
|
||||||
function sanitizeBaseName(name) {
|
|
||||||
let s = String(name || "");
|
|
||||||
|
|
||||||
// Remove noise patterns
|
|
||||||
for (const re of NOISE_PATTERNS) s = s.replace(re, " ");
|
|
||||||
|
|
||||||
// Collapse spaces
|
|
||||||
s = s.replace(/\s+/g, " ").trim();
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractIconTagsAndStrip(name, proxy) {
|
|
||||||
let s = String(name || "");
|
|
||||||
const tags = [];
|
|
||||||
|
|
||||||
const typ = safeStr(proxy && proxy.type) || "";
|
|
||||||
const port = safeStr(proxy && proxy.port);
|
|
||||||
tags.push(portToFancy(port, typ))
|
|
||||||
|
|
||||||
for (const r of ICON_RULES) {
|
|
||||||
if (r.regex.test(s)) {
|
|
||||||
tags.push(r.icon);
|
|
||||||
s = s.replace(r.regex, " ");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const t of NAME_NETWORK_TAGS) {
|
|
||||||
if (t.regex.test(s)) {
|
|
||||||
tags.push(t.tag);
|
|
||||||
s = s.replace(t.regex, " ");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { stripped: s.replace(/\s+/g, " ").trim(), tags: uniq(tags) };
|
|
||||||
}
|
|
||||||
|
|
||||||
function detectCountryByName(name) {
|
|
||||||
const n = String(name || "");
|
|
||||||
// Order by priority, then first match wins
|
|
||||||
|
|
||||||
// Fast path: flag emoji
|
|
||||||
if (n.includes("🇦🇪")) return { iso3: "ARE", flag: "🇦🇪", priority: 1, source: "flag" };
|
|
||||||
if (n.includes("🇦🇱")) return { iso3: "ALB", flag: "🇦🇱", priority: 2, source: "flag" };
|
|
||||||
if (n.includes("🇦🇲")) return { iso3: "ARM", flag: "🇦🇲", priority: 2, source: "flag" };
|
|
||||||
if (n.includes("🇦🇷")) return { iso3: "ARG", flag: "🇦🇷", priority: 2, source: "flag" };
|
|
||||||
if (n.includes("🇦🇹")) return { iso3: "AUT", flag: "🇦🇹", priority: 3, source: "flag" };
|
|
||||||
if (n.includes("🇦🇺")) return { iso3: "AUS", flag: "🇦🇺", priority: 4, source: "flag" };
|
|
||||||
if (n.includes("🇧🇪")) return { iso3: "BEL", flag: "🇧🇪", priority: 5, source: "flag" };
|
|
||||||
if (n.includes("🇧🇬")) return { iso3: "BGR", flag: "🇧🇬", priority: 5, source: "flag" };
|
|
||||||
if (n.includes("🇧🇾")) return { iso3: "BLR", flag: "🇧🇾", priority: 6, source: "flag" };
|
|
||||||
if (n.includes("🇧🇷")) return { iso3: "BRA", flag: "🇧🇷", priority: 7, source: "flag" };
|
|
||||||
if (n.includes("🇨🇦")) return { iso3: "CAN", flag: "🇨🇦", priority: 8, source: "flag" };
|
|
||||||
if (n.includes("🇨🇭")) return { iso3: "CHE", flag: "🇨🇭", priority: 9, source: "flag" };
|
|
||||||
if (n.includes("🇨🇳")) return { iso3: "CHN", flag: "🇨🇳", priority: 10, source: "flag" };
|
|
||||||
if (n.includes("🇨🇾")) return { iso3: "CYP", flag: "🇨🇾", priority: 11, source: "flag" };
|
|
||||||
if (n.includes("🇨🇿")) return { iso3: "CZE", flag: "🇨🇿", priority: 11, source: "flag" };
|
|
||||||
if (n.includes("🇩🇪")) return { iso3: "DEU", flag: "🇩🇪", priority: 12, source: "flag" };
|
|
||||||
if (n.includes("🇩🇰")) return { iso3: "DNK", flag: "🇩🇰", priority: 13, source: "flag" };
|
|
||||||
if (n.includes("🇪🇪")) return { iso3: "EST", flag: "🇪🇪", priority: 14, source: "flag" };
|
|
||||||
if (n.includes("🇪🇬")) return { iso3: "EGY", flag: "🇪🇬", priority: 15, source: "flag" };
|
|
||||||
if (n.includes("🇪🇸")) return { iso3: "ESP", flag: "🇪🇸", priority: 16, source: "flag" };
|
|
||||||
if (n.includes("🇫🇮")) return { iso3: "FIN", flag: "🇫🇮", priority: 17, source: "flag" };
|
|
||||||
if (n.includes("🇫🇷")) return { iso3: "FRA", flag: "🇫🇷", priority: 18, source: "flag" };
|
|
||||||
if (n.includes("🇬🇧")) return { iso3: "GBR", flag: "🇬🇧", priority: 19, source: "flag" };
|
|
||||||
if (n.includes("🇬🇪")) return { iso3: "GEO", flag: "🇬🇪", priority: 20, source: "flag" };
|
|
||||||
if (n.includes("🇬🇷")) return { iso3: "GRC", flag: "🇬🇷", priority: 2, source: "flag" };
|
|
||||||
if (n.includes("🇭🇰")) return { iso3: "HKG", flag: "🇭🇰", priority: 21, source: "flag" };
|
|
||||||
if (n.includes("🇭🇷")) return { iso3: "HRV", flag: "🇭🇷", priority: 21, source: "flag" };
|
|
||||||
if (n.includes("🇭🇺")) return { iso3: "HUN", flag: "🇭🇺", priority: 1, source: "flag" };
|
|
||||||
if (n.includes("🇮🇪")) return { iso3: "IRL", flag: "🇮🇪", priority: 22, source: "flag" };
|
|
||||||
if (n.includes("🇮🇱")) return { iso3: "ISR", flag: "🇮🇱", priority: 23, source: "flag" };
|
|
||||||
if (n.includes("🇮🇳")) return { iso3: "IND", flag: "🇮🇳", priority: 24, source: "flag" };
|
|
||||||
if (n.includes("🇮🇹")) return { iso3: "ITA", flag: "🇮🇹", priority: 25, source: "flag" };
|
|
||||||
if (n.includes("🇮🇸")) return { iso3: "ISL", flag: "🇮🇸", priority: 1, source: "flag" };
|
|
||||||
if (n.includes("🇯🇵")) return { iso3: "JPN", flag: "🇯🇵", priority: 26, source: "flag" };
|
|
||||||
if (n.includes("🇰🇷")) return { iso3: "KOR", flag: "🇰🇷", priority: 27, source: "flag" };
|
|
||||||
if (n.includes("🇰🇿")) return { iso3: "KAZ", flag: "🇰🇿", priority: 28, source: "flag" };
|
|
||||||
if (n.includes("🇱🇹")) return { iso3: "LTU", flag: "🇱🇹", priority: 29, source: "flag" };
|
|
||||||
if (n.includes("🇱🇻")) return { iso3: "LVA", flag: "🇱🇻", priority: 30, source: "flag" };
|
|
||||||
if (n.includes("🇲🇩")) return { iso3: "MDA", flag: "🇲🇩", priority: 31, source: "flag" };
|
|
||||||
if (n.includes("🇲🇰")) return { iso3: "MKD", flag: "🇲🇰", priority: 2, source: "flag" };
|
|
||||||
if (n.includes("🇲🇾")) return { iso3: "MYS", flag: "🇲🇾", priority: 32, source: "flag" };
|
|
||||||
if (n.includes("🇳🇬")) return { iso3: "NGA", flag: "🇳🇬", priority: 33, source: "flag" };
|
|
||||||
if (n.includes("🇳🇱")) return { iso3: "NLD", flag: "🇳🇱", priority: 34, source: "flag" };
|
|
||||||
if (n.includes("🇳🇴")) return { iso3: "NOR", flag: "🇳🇴", priority: 35, source: "flag" };
|
|
||||||
if (n.includes("🇵🇭")) return { iso3: "PHL", flag: "🇵🇭", priority: 36, source: "flag" };
|
|
||||||
if (n.includes("🇵🇱")) return { iso3: "POL", flag: "🇵🇱", priority: 37, source: "flag" };
|
|
||||||
if (n.includes("🇵🇹")) return { iso3: "PRT", flag: "🇵🇹", priority: 38, source: "flag" };
|
|
||||||
if (n.includes("🇷🇴")) return { iso3: "ROU", flag: "🇷🇴", priority: 39, source: "flag" };
|
|
||||||
if (n.includes("🇷🇺")) return { iso3: "RUS", flag: "🇷🇺", priority: 40, source: "flag" };
|
|
||||||
if (n.includes("🇸🇪")) return { iso3: "SWE", flag: "🇸🇪", priority: 41, source: "flag" };
|
|
||||||
if (n.includes("🇸🇬")) return { iso3: "SGP", flag: "🇸🇬", priority: 42, source: "flag" };
|
|
||||||
if (n.includes("🇹🇭")) return { iso3: "THA", flag: "🇹🇭", priority: 43, source: "flag" };
|
|
||||||
if (n.includes("🇹🇷")) return { iso3: "TUR", flag: "🇹🇷", priority: 44, source: "flag" };
|
|
||||||
if (n.includes("🇹🇼")) return { iso3: "TWN", flag: "🇹🇼", priority: 45, source: "flag" };
|
|
||||||
if (n.includes("🇺🇦")) return { iso3: "UKR", flag: "🇺🇦", priority: 2, source: "flag" };
|
|
||||||
if (n.includes("🇺🇸")) return { iso3: "USA", flag: "🇺🇸", priority: 46, source: "flag" };
|
|
||||||
if (n.includes("🇻🇳")) return { iso3: "VNM", flag: "🇻🇳", priority: 47, source: "flag" };
|
|
||||||
|
|
||||||
const sorted = COUNTRY_RULES.slice().sort((a, b) => a.priority - b.priority);
|
|
||||||
for (const c of sorted) {
|
|
||||||
if (c.regex.test(n)) return { iso3: c.iso3, flag: c.flag, priority: c.priority, source: "name" };
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function detectCountryByGeoIP(server, utils) {
|
|
||||||
if (!isIPv4(server)) return null;
|
|
||||||
if (!utils || !utils.geoip || typeof utils.geoip.lookup !== "function") return null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const geo = utils.geoip.lookup(server);
|
|
||||||
const iso2 = geo && (geo.country || geo.country_code || geo.iso_code);
|
|
||||||
if (!iso2 || typeof iso2 !== "string") return null;
|
|
||||||
|
|
||||||
const key = iso2.toUpperCase();
|
|
||||||
const mapped = ISO2_TO_ISO3[key];
|
|
||||||
if (mapped) return { iso3: mapped.iso3, flag: mapped.flag, priority: 900, source: "geoip" };
|
|
||||||
|
|
||||||
// Unknown ISO2: keep something sane
|
|
||||||
return { iso3: key, flag: "🏳️", priority: 950, source: "geoip" };
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function pad2(n) {
|
|
||||||
const x = Number(n);
|
|
||||||
return x < 10 ? `0${x}` : String(x);
|
|
||||||
}
|
|
||||||
|
|
||||||
function safeStr(v) {
|
|
||||||
return (v === undefined || v === null) ? "" : String(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////
|
|
||||||
// OPERATOR
|
|
||||||
///////////////////////
|
|
||||||
|
|
||||||
function operator(proxies, targetPlatform, utils) {
|
|
||||||
// Sub-Store sometimes passes utils as global $utils; sometimes as 3rd arg; sometimes not at all.
|
|
||||||
// We'll accept any of them without whining.
|
|
||||||
const opts = normalizeOptions();
|
|
||||||
|
|
||||||
const U = utils || (typeof $utils !== "undefined" ? $utils : null);
|
|
||||||
|
|
||||||
const buckets = Object.create(null);
|
|
||||||
|
|
||||||
for (const proxy of proxies) {
|
|
||||||
const originalName = safeStr(proxy && proxy.name);
|
|
||||||
|
|
||||||
// 1) Extract tags (icons) from ORIGINAL name, then strip those keywords out
|
|
||||||
const iconStage = extractIconTagsAndStrip(originalName, proxy);
|
|
||||||
|
|
||||||
// 2) Sanitize remaining base name (remove marketing trash, brackets, etc.)
|
|
||||||
const cleanBase = sanitizeBaseName(iconStage.stripped);
|
|
||||||
|
|
||||||
// 3) Detect country (name first, then GeoIP)
|
|
||||||
const byName = detectCountryByName(originalName);
|
|
||||||
const byGeo = detectCountryByGeoIP(proxy && proxy.server, U);
|
|
||||||
const country = byName || byGeo || { iso3: "UNK", flag: "🏴☠️", priority: 9999, source: "fallback" };
|
|
||||||
|
|
||||||
// 4) Protocol icon (based on type)
|
|
||||||
const proto = PROTOCOL_ICONS[(proxy && proxy.type) || ""] || PROTOCOL_ICON_DEFAULT;
|
|
||||||
|
|
||||||
// 5) Network/type/port tag (from proxy fields)
|
|
||||||
const metaTag = buildMetaTag(proxy);
|
|
||||||
|
|
||||||
// 6) Prepare bucket key
|
|
||||||
const key = country.iso3;
|
|
||||||
|
|
||||||
if (!buckets[key]) {
|
|
||||||
buckets[key] = {
|
|
||||||
country,
|
|
||||||
list: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep meta used for sorting and final formatting
|
|
||||||
buckets[key].list.push({
|
|
||||||
proxy,
|
|
||||||
_meta: {
|
|
||||||
originalName,
|
|
||||||
cleanBase,
|
|
||||||
iconTags: iconStage.tags,
|
|
||||||
proto,
|
|
||||||
metaTag
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 7) Sort buckets by priority
|
|
||||||
const bucketKeys = Object.keys(buckets).sort((a, b) => {
|
|
||||||
return (buckets[a].country.priority || 9999) - (buckets[b].country.priority || 9999);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 8) Sort inside each country bucket and rename with per-country numbering
|
|
||||||
const result = [];
|
|
||||||
|
|
||||||
for (const key of bucketKeys) {
|
|
||||||
const group = buckets[key];
|
|
||||||
|
|
||||||
group.list.sort((A, B) => {
|
|
||||||
// Tags do not affect numbering: sort only by sanitized base + server:port as tie-breaker
|
|
||||||
const an = A._meta.cleanBase.toLowerCase();
|
|
||||||
const bn = B._meta.cleanBase.toLowerCase();
|
|
||||||
if (an !== bn) return an.localeCompare(bn);
|
|
||||||
|
|
||||||
const as = `${safeStr(A.proxy.server)}:${safeStr(A.proxy.port)}`;
|
|
||||||
const bs = `${safeStr(B.proxy.server)}:${safeStr(B.proxy.port)}`;
|
|
||||||
return as.localeCompare(bs);
|
|
||||||
});
|
|
||||||
|
|
||||||
for (let i = 0; i < group.list.length; i++) {
|
|
||||||
const item = group.list[i];
|
|
||||||
const p = item.proxy;
|
|
||||||
const num = pad2(i + 1);
|
|
||||||
|
|
||||||
const debugSuffix = opts.appendOriginalName
|
|
||||||
? ` ⟦${item._meta.originalName}⟧`
|
|
||||||
: "";
|
|
||||||
|
|
||||||
const tagStr = item._meta.iconTags.length ? ` ${item._meta.iconTags.join("")}` : "";
|
|
||||||
|
|
||||||
p.name = `${group.country.flag}${item._meta.metaTag} ${group.country.iso3}-${num} ${item._meta.proto}${tagStr} ${debugSuffix}`
|
|
||||||
.replace(/\s+/g, " ")
|
|
||||||
.trim();
|
|
||||||
|
|
||||||
result.push(p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
@@ -1,516 +0,0 @@
|
|||||||
/**
|
|
||||||
* Sub-Store operator: Normalize + tag + country detect + per-country numbering
|
|
||||||
*
|
|
||||||
* Output format (default):
|
|
||||||
* 🇺🇸 USA-01 🔒 📺 ❻ ▫️ws/vless/443
|
|
||||||
*
|
|
||||||
* Notes:
|
|
||||||
* - Numbering is computed per-country AFTER grouping the full list.
|
|
||||||
* - Tags (icons) do NOT affect numbering order.
|
|
||||||
* - GeoIP is optional and only used when server is an IP and utils.geoip.lookup exists.
|
|
||||||
*/
|
|
||||||
|
|
||||||
///////////////////////
|
|
||||||
// CONFIG (EDIT ME)
|
|
||||||
///////////////////////
|
|
||||||
|
|
||||||
const DEBUG_APPEND_ORIGINAL_NAME = false; // set true to enable debug mode (appends original name as comment)
|
|
||||||
|
|
||||||
// 1) Remove these patterns (marketing noise, brackets, separators, etc.)
|
|
||||||
const NOISE_PATTERNS = [
|
|
||||||
/\[[^\]]*]/g, // [ ... ]
|
|
||||||
/\([^)]*\)/g, // ( ... )
|
|
||||||
/\{[^}]*}/g, // { ... }
|
|
||||||
/\btraffic\b/gi,
|
|
||||||
/\bfree\b/gi,
|
|
||||||
/\bwebsite\b/gi,
|
|
||||||
/\bexpire\b/gi,
|
|
||||||
/\blow\s*ping\b/gi,
|
|
||||||
/\bai\s*studio\b/gi,
|
|
||||||
/\bno\s*p2p\b/gi,
|
|
||||||
/\b10\s*gbit\b/gi,
|
|
||||||
/\bvless\b/gi,
|
|
||||||
/\bvmess\b/gi,
|
|
||||||
/\bssr?\b/gi,
|
|
||||||
/\btrojan\b/gi,
|
|
||||||
/\bhysteria2?\b/gi,
|
|
||||||
/\btuic\b/gi,
|
|
||||||
/[|]/g,
|
|
||||||
/[_]+/g,
|
|
||||||
/[-]{2,}/g
|
|
||||||
];
|
|
||||||
|
|
||||||
// 2) Keyword -> icon tags (if found in original name, icon is added; the keyword is removed from base name)
|
|
||||||
// 🇫🇿 🇺🇳 🇩🇻 🇻🇿 🇵🇷 🇦🇿 🇬🇺🇦🇷🇩
|
|
||||||
const ICON_RULES = [
|
|
||||||
{ regex: /TEST/gi, icon: "🧪" },
|
|
||||||
{ regex: uWordBoundaryGroup("Low Ping|⚡|Быстрое"), icon: "⚡️" },
|
|
||||||
{ regex: uWordBoundaryGroup("10 Gbit|20 Гбит/c"), icon: "🛤️" },
|
|
||||||
{ regex: uWordBoundaryGroup("YT|YouTube|Russia|Россия|Saint Petersburg|Moscow"), icon: "📺" },
|
|
||||||
{ regex: uWordBoundaryGroup("IPv6"), icon: "🎱" },
|
|
||||||
{ regex: uWordBoundaryGroup("Gemini|AI Studio"), icon: "🤖" },
|
|
||||||
{ regex: uWordBoundaryGroup("Torrent|P2P|P2P-Torrents"), icon: "🧲" },
|
|
||||||
{ regex: uWordBoundaryGroup("Мегафон|MTS|Yota|T2|Все операторы|Обход"), icon: "📃" },
|
|
||||||
{ regex: uWordBoundaryGroup("Мост"), icon: "🌉" },
|
|
||||||
{ regex: uWordBoundaryGroup("Сильные блокировки"), icon: "🚧" },
|
|
||||||
|
|
||||||
{ regex: uWordBoundaryGroup("local"), icon: "🚪" },
|
|
||||||
{ regex: uWordBoundaryGroup("neighbourhood"), icon: "🫂" },
|
|
||||||
|
|
||||||
{ regex: uWordBoundaryGroup("xfizz|x-fizz"), icon: "🇫" },
|
|
||||||
{ regex: uWordBoundaryGroup("unicade|uncd"), icon: "🇺" },
|
|
||||||
{ regex: uWordBoundaryGroup("vzdh|vezdehod"), icon: "🇻" },
|
|
||||||
{ regex: uWordBoundaryGroup("dvpn|d-vpn"), icon: "🇩" },
|
|
||||||
{ regex: uWordBoundaryGroup("proton"), icon: "🇵" },
|
|
||||||
{ regex: uWordBoundaryGroup("amnezia"), icon: "🇦" },
|
|
||||||
{ regex: uWordBoundaryGroup("adguard"), icon: "🇬" },
|
|
||||||
{ regex: uWordBoundaryGroup("snow"), icon: "🇸" },
|
|
||||||
{ regex: uWordBoundaryGroup("ovsc"), icon: "🇴" },
|
|
||||||
];
|
|
||||||
|
|
||||||
// 3) Optional “network” tag rules based on NAME text (not $server.network)
|
|
||||||
// (Useful if providers shove "BGP/IPLC" into the node name)
|
|
||||||
const NAME_NETWORK_TAGS = [
|
|
||||||
{ regex: uWordBoundaryGroup("IPLC"), tag: "🛰️" },
|
|
||||||
{ regex: uWordBoundaryGroup("BGP"), tag: "🧭" },
|
|
||||||
{ regex: uWordBoundaryGroup("Anycast"), tag: "🌍" }
|
|
||||||
];
|
|
||||||
|
|
||||||
// 4) Country detection rules by NAME (regex). First match wins (priority = lower is earlier)
|
|
||||||
const COUNTRY_RULES = [
|
|
||||||
{ regex: uWordBoundaryGroup("(Аргентина|Argentina|AR|ARG|ARGENTINA|BUENOS\s*AIRES)"), iso3: "ARG", flag: "🇦🇷", priority: 100 }, // Argentina
|
|
||||||
{ regex: uWordBoundaryGroup("(Australia|AU|AUS|AUSTRALIA|SYDNEY)"), iso3: "AUS", flag: "🇦🇺", priority: 110 }, // Australia
|
|
||||||
{ regex: uWordBoundaryGroup("(Austria|AT|AUT|AUSTRIA|VIENNA)"), iso3: "AUT", flag: "🇦🇹", priority: 120 }, // Austria
|
|
||||||
{ regex: uWordBoundaryGroup("(Беларусь|Белоруссия|BELARUS)"), iso3: "BLR", flag: "🇧🇾", priority: 130 }, // Belarus
|
|
||||||
{ regex: uWordBoundaryGroup("(Brazil|BR|BRA|BRAZIL|SAO\s*PAULO)"), iso3: "BRA", flag: "🇧🇷", priority: 140 }, // Brazil
|
|
||||||
{ regex: uWordBoundaryGroup("(Bulgaria|BG|BGR|BULGARIA|SOFIA)"), iso3: "BGR", flag: "🇧🇬", priority: 150 }, // Bulgaria
|
|
||||||
{ regex: uWordBoundaryGroup("(Canada|CA|CAN|CANADA|TORONTO)"), iso3: "CAN", flag: "🇨🇦", priority: 160 }, // Canada
|
|
||||||
{ regex: uWordBoundaryGroup("(КИТАЙ|China)"), iso3: "CHN", flag: "🇨🇳", priority: 170 }, // China
|
|
||||||
{ regex: uWordBoundaryGroup("(Czech\s*Republic|CZ|CZE|CZECH|PRAGUE)"), iso3: "CZE", flag: "🇨🇿", priority: 180 }, // Czech Republic
|
|
||||||
{ regex: uWordBoundaryGroup("(Denmark|DK|DNK|DENMARK|COPENHAGEN)"), iso3: "DNK", flag: "🇩🇰", priority: 190 }, // Denmark
|
|
||||||
{ regex: uWordBoundaryGroup("(Egypt|EG|EGY|EGYPT|CAIRO)"), iso3: "EGY", flag: "🇪🇬", priority: 200 }, // Egypt
|
|
||||||
{ regex: uWordBoundaryGroup("(Эстония|EE|EST|ESTONIA|TALLINN)"), iso3: "EST", flag: "🇪🇪", priority: 210 }, // Estonia
|
|
||||||
{ regex: uWordBoundaryGroup("(Финляндия|FI|FIN|FINLAND|HELSINKI)"), iso3: "FIN", flag: "🇫🇮", priority: 220 }, // Finland
|
|
||||||
{ regex: uWordBoundaryGroup("(Франция|FR|FRA|FRANCE|PARIS|MARSEILLE)"), iso3: "FRA", flag: "🇫🇷", priority: 230 }, // France
|
|
||||||
{ regex: uWordBoundaryGroup("(Georgia|GE|GEO|GEORGIA|TBILISI)"), iso3: "GEO", flag: "🇬🇪", priority: 240 }, // Georgia
|
|
||||||
{ regex: uWordBoundaryGroup("(Германия|DE|DEU|GER(MANY)?|FRANKFURT|BERLIN|MUNICH)"), iso3: "DEU", flag: "🇩🇪", priority: 250 }, // Germany
|
|
||||||
{ regex: uWordBoundaryGroup("(Гонконг|HK|HKG|HONG\s*KONG)"), iso3: "HKG", flag: "🇭🇰", priority: 260 }, // Hong Kong
|
|
||||||
{ regex: uWordBoundaryGroup("(India|IN|IND|INDIA|MUMBAI)"), iso3: "IND", flag: "🇮🇳", priority: 270 }, // India
|
|
||||||
{ regex: uWordBoundaryGroup("(Ireland|IE|IRL|IRELAND|DUBLIN)"), iso3: "IRL", flag: "🇮🇪", priority: 280 }, // Ireland
|
|
||||||
{ regex: uWordBoundaryGroup("(Israel|IL|ISR|ISRAEL|TEL\s*AVIV)"), iso3: "ISR", flag: "🇮🇱", priority: 290 }, // Israel
|
|
||||||
{ regex: uWordBoundaryGroup("(Italy|IT|ITA|ITALY|ROME)"), iso3: "ITA", flag: "🇮🇹", priority: 300 }, // Italy
|
|
||||||
{ regex: uWordBoundaryGroup("(Япония|JP|JPN|JAPAN|TOKYO|OSAKA)"), iso3: "JPN", flag: "🇯🇵", priority: 310 }, // Japan
|
|
||||||
{ regex: uWordBoundaryGroup("(Kazakhstan|KZ|KAZ|KAZAKHSTAN|ALMATY)"), iso3: "KAZ", flag: "🇰🇿", priority: 320 }, // Kazakhstan
|
|
||||||
{ regex: uWordBoundaryGroup("(Латвия|LV|LVA|LATVIA|RIGA)"), iso3: "LVA", flag: "🇱🇻", priority: 330 }, // Latvia
|
|
||||||
{ regex: uWordBoundaryGroup("(Литва|LT|LTU|LITHUANIA|VILNIUS)"), iso3: "LTU", flag: "🇱🇹", priority: 340 }, // Lithuania
|
|
||||||
{ regex: uWordBoundaryGroup("(Malaysia|MY|MYS|MALAYSIA|KUALA\s*LUMPUR)"), iso3: "MYS", flag: "🇲🇾", priority: 350 }, // Malaysia
|
|
||||||
{ regex: uWordBoundaryGroup("(Moldova|MD|MDA|MOLDOVA|CHISINAU)"), iso3: "MDA", flag: "🇲🇩", priority: 360 }, // Moldova
|
|
||||||
{ regex: uWordBoundaryGroup("(Нидерланды|NL|NLD|NETHERLANDS|HOLLAND|AMSTERDAM)"), iso3: "NLD", flag: "🇳🇱", priority: 370 }, // Netherlands
|
|
||||||
{ regex: uWordBoundaryGroup("(Nigeria|NG|NGA|NIGERIA|LAGOS)"), iso3: "NGA", flag: "🇳🇬", priority: 380 }, // Nigeria
|
|
||||||
{ regex: uWordBoundaryGroup("(Норвегия|NO|NOR|NORWAY|OSLO)"), iso3: "NOR", flag: "🇳🇴", priority: 390 }, // Norway
|
|
||||||
{ regex: uWordBoundaryGroup("(Philippines|PH|PHL|PHILIPPINES|MANILA)"), iso3: "PHL", flag: "🇵🇭", priority: 400 }, // Philippines
|
|
||||||
{ regex: uWordBoundaryGroup("(Польша|PL|POL|POLAND|WARSAW|WARSZAWA)"), iso3: "POL", flag: "🇵🇱", priority: 410 }, // Poland
|
|
||||||
{ regex: uWordBoundaryGroup("(Portugal|PT|PRT|PORTUGAL|LISBON)"), iso3: "PRT", flag: "🇵🇹", priority: 420 }, // Portugal
|
|
||||||
{ regex: uWordBoundaryGroup("(Romania|RO|ROU|ROMANIA|BUCHAREST)"), iso3: "ROU", flag: "🇷🇴", priority: 430 }, // Romania
|
|
||||||
{ regex: uWordBoundaryGroup("(Russia|RU|RUS|RUSSIA|MOSCOW)"), iso3: "RUS", flag: "🇷🇺", priority: 440 }, // Russia
|
|
||||||
{ regex: uWordBoundaryGroup("(Сингапур|SG|SGP|SINGAPORE)"), iso3: "SGP", flag: "🇸🇬", priority: 200 }, // Singapore
|
|
||||||
{ regex: uWordBoundaryGroup("(South Korea|Корея|KR|KOR|KOREA|SEOUL)"), iso3: "KOR", flag: "🇰🇷", priority: 450 }, // South Korea
|
|
||||||
{ regex: uWordBoundaryGroup("(Spain|ES|ESP|SPAIN|MADRID)"), iso3: "ESP", flag: "🇪🇸", priority: 460 }, // Spain
|
|
||||||
{ regex: uWordBoundaryGroup("(Швеция|SE|SWE|SWEDEN|STOCKHOLM)"), iso3: "SWE", flag: "🇸🇪", priority: 470 }, // Sweden
|
|
||||||
{ regex: uWordBoundaryGroup("(Швейцария|CH|CHE|SWITZERLAND|Switzerl)"), iso3: "CHE", flag: "🇨🇭", priority: 480 }, // Switzerland
|
|
||||||
{ regex: uWordBoundaryGroup("(Taiwan|TW|TWN|TAIWAN|TAIPEI)"), iso3: "TWN", flag: "🇹🇼", priority: 490 }, // Taiwan
|
|
||||||
{ regex: uWordBoundaryGroup("(Thailand|TH|THA|THAILAND|BANGKOK)"), iso3: "THA", flag: "🇹🇭", priority: 500 }, // Thailand
|
|
||||||
{ regex: uWordBoundaryGroup("(Турция|TR|TUR|TURKEY|ISTANBUL)"), iso3: "TUR", flag: "🇹🇷", priority: 510 }, // Turkey
|
|
||||||
{ regex: uWordBoundaryGroup("(UAE|United\s*Arab\s*Emirates|AE|ARE|DUBAI)"), iso3: "ARE", flag: "🇦🇪", priority: 520 }, // UAE
|
|
||||||
{ regex: uWordBoundaryGroup("(Великобритания|Англия|England|UK|GB|GBR|UNITED\s*KINGDOM)"), iso3: "GBR", flag: "🇬🇧", priority: 530 }, // UK
|
|
||||||
{ regex: uWordBoundaryGroup("(США|USA|US|UNITED\s*STATES|AMERICA|NEW\s*YORK|NYC)"), iso3: "USA", flag: "🇺🇸", priority: 540 }, // USA
|
|
||||||
{ regex: uWordBoundaryGroup("(Vietnam|VN|VNM|VIETNAM|HANOI)"), iso3: "VNM", flag: "🇻🇳", priority: 500 } // Vietnam
|
|
||||||
];
|
|
||||||
|
|
||||||
// 5) GeoIP mapping (ISO2 -> ISO3 + flag) used only if utils.geoip.lookup(ip) returns ISO2
|
|
||||||
const ISO2_TO_ISO3 = {
|
|
||||||
US: { iso3: "USA", flag: "🇺🇸" },
|
|
||||||
DE: { iso3: "DEU", flag: "🇩🇪" },
|
|
||||||
NL: { iso3: "NLD", flag: "🇳🇱" },
|
|
||||||
GB: { iso3: "GBR", flag: "🇬🇧" },
|
|
||||||
FR: { iso3: "FRA", flag: "🇫🇷" },
|
|
||||||
PL: { iso3: "POL", flag: "🇵🇱" },
|
|
||||||
FI: { iso3: "FIN", flag: "🇫🇮" },
|
|
||||||
SE: { iso3: "SWE", flag: "🇸🇪" },
|
|
||||||
NO: { iso3: "NOR", flag: "🇳🇴" },
|
|
||||||
CH: { iso3: "CHE", flag: "🇨🇭" },
|
|
||||||
EE: { iso3: "EST", flag: "🇪🇪" },
|
|
||||||
LV: { iso3: "LVA", flag: "🇱🇻" },
|
|
||||||
LT: { iso3: "LTU", flag: "🇱🇹" },
|
|
||||||
TR: { iso3: "TUR", flag: "🇹🇷" },
|
|
||||||
SG: { iso3: "SGP", flag: "🇸🇬" },
|
|
||||||
JP: { iso3: "JPN", flag: "🇯🇵" },
|
|
||||||
KR: { iso3: "KOR", flag: "🇰🇷" },
|
|
||||||
HK: { iso3: "HKG", flag: "🇭🇰" },
|
|
||||||
};
|
|
||||||
|
|
||||||
// 6) Protocol icons (based on proxy.type)
|
|
||||||
const PROTOCOL_ICONS = {
|
|
||||||
ss: "",
|
|
||||||
ssr: "",
|
|
||||||
vmess: "",
|
|
||||||
vless: "",
|
|
||||||
trojan: "",
|
|
||||||
http: "",
|
|
||||||
socks5: "",
|
|
||||||
snell: "",
|
|
||||||
wireguard: "",
|
|
||||||
hysteria: "",
|
|
||||||
hysteria2: "",
|
|
||||||
tuic: ""
|
|
||||||
};
|
|
||||||
|
|
||||||
const STANDARD_PORTS_BY_TYPE = {
|
|
||||||
wireguard: new Set(["51820"]),
|
|
||||||
vless: new Set(["443"]),
|
|
||||||
trojan: new Set(["443"]),
|
|
||||||
ss: new Set(["443"]),
|
|
||||||
};
|
|
||||||
|
|
||||||
const PROTOCOL_ICON_DEFAULT = ""; // fallback icon if type is unknown
|
|
||||||
|
|
||||||
|
|
||||||
const METATAG_RULES = {
|
|
||||||
// Keys are "network/type" OR "/type" (network-agnostic) OR "network/" (type-agnostic)
|
|
||||||
// Matching priority: exact "network/type" -> "/type" -> "network/" -> default
|
|
||||||
// 🅶🆃 🆃🆂 🆃🆅 🆆🆅 🆇🆅 🆆🅶 🅽🅸
|
|
||||||
pairMap: {
|
|
||||||
"grpc/trojan": "🅶🆃",
|
|
||||||
"tcp/trojan": "🆃🆃",
|
|
||||||
"tcp/ss": "🆃🆂",
|
|
||||||
"grpc/vless": "🅶🆅",
|
|
||||||
"tcp/vless": "🆃🆅",
|
|
||||||
"ws/vless": "🆆🆅",
|
|
||||||
"xhttp/vless": "🆇🆅",
|
|
||||||
|
|
||||||
"/wireguard": "🆆🅶",
|
|
||||||
"/naive": "🅽🅸",
|
|
||||||
},
|
|
||||||
|
|
||||||
defaultPair: "▫️", // fallback if nothing matches
|
|
||||||
includeFallbackText: false, // if true, append "(net/type)" when defaultPair is used
|
|
||||||
};
|
|
||||||
|
|
||||||
// Port formatting: superscript digits with left padding to 4 chars
|
|
||||||
// 𝟎𝟏𝟐𝟑𝟒𝟓𝟔𝟕𝟖𝟗
|
|
||||||
const PORT_FORMAT = {
|
|
||||||
padLeftTo: 3,
|
|
||||||
padChar: "0",
|
|
||||||
fancy: {
|
|
||||||
"0": "𝟎", "1": "𝟏", "2": "𝟐", "3": "𝟑", "4": "𝟒", "5": "𝟓", "6": "𝟔", "7": "𝟕", "8": "𝟖", "9": "𝟗",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
///////////////////////
|
|
||||||
// HELPERS
|
|
||||||
///////////////////////
|
|
||||||
|
|
||||||
function normalizeToken(s) {
|
|
||||||
return String(s || "").trim().toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
function uWordBoundaryGroup(inner) {
|
|
||||||
// Match if surrounded by non-letter/non-digit (Unicode-aware)
|
|
||||||
// We don't use lookbehind for max compatibility.
|
|
||||||
return new RegExp(`(?:^|[^\\p{L}\\p{N}])(?:${inner})(?=$|[^\\p{L}\\p{N}])`, "iu");
|
|
||||||
}
|
|
||||||
|
|
||||||
function portToFancy(port, type) {
|
|
||||||
let p = String(port ?? "").trim();
|
|
||||||
p = p.replace(/[^\d]/g, "");
|
|
||||||
if (!p) return "";
|
|
||||||
|
|
||||||
if (STANDARD_PORTS_BY_TYPE[type]?.has(p)) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// left pad to fixed width
|
|
||||||
if (PORT_FORMAT.padLeftTo && p.length < PORT_FORMAT.padLeftTo) {
|
|
||||||
p = p.padStart(PORT_FORMAT.padLeftTo, PORT_FORMAT.padChar);
|
|
||||||
}
|
|
||||||
|
|
||||||
// map digits
|
|
||||||
let out = "";
|
|
||||||
for (const ch of p) out += PORT_FORMAT.fancy[ch] ?? ch;
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildMetaTag(proxy) {
|
|
||||||
const net = safeStr(proxy && proxy.network) || "";
|
|
||||||
const typ = safeStr(proxy && proxy.type) || "";
|
|
||||||
const port = safeStr(proxy && proxy.port);
|
|
||||||
|
|
||||||
const { icon, matched } = metaPairIcon(net, typ);
|
|
||||||
const portSup = portToFancy(port, typ);
|
|
||||||
|
|
||||||
if (icon === METATAG_RULES.defaultPair && METATAG_RULES.includeFallbackText) {
|
|
||||||
return `${icon}${portSup}(${normalizeToken(net)}/${normalizeToken(typ)})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${icon}${portSup}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function metaPairIcon(network, type) {
|
|
||||||
const net = normalizeToken(network);
|
|
||||||
const typ = normalizeToken(type);
|
|
||||||
|
|
||||||
const exact = `${net}/${typ}`;
|
|
||||||
const typeOnly = `/${typ}`;
|
|
||||||
const netOnly = `${net}/`;
|
|
||||||
|
|
||||||
const m = METATAG_RULES.pairMap;
|
|
||||||
|
|
||||||
if (m[exact]) return { icon: m[exact], matched: exact };
|
|
||||||
if (m[typeOnly]) return { icon: m[typeOnly], matched: typeOnly };
|
|
||||||
if (m[netOnly]) return { icon: m[netOnly], matched: netOnly };
|
|
||||||
|
|
||||||
return { icon: METATAG_RULES.defaultPair, matched: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
function isIPv4(str) {
|
|
||||||
if (typeof str !== "string") return false;
|
|
||||||
const m = str.match(/^(\d{1,3})(\.\d{1,3}){3}$/);
|
|
||||||
if (!m) return false;
|
|
||||||
return str.split(".").every(oct => {
|
|
||||||
const n = Number(oct);
|
|
||||||
return n >= 0 && n <= 255 && String(n) === oct.replace(/^0+(\d)/, "$1"); // avoids "001" weirdness
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function uniq(arr) {
|
|
||||||
return [...new Set(arr.filter(Boolean))];
|
|
||||||
}
|
|
||||||
|
|
||||||
function sanitizeBaseName(name) {
|
|
||||||
let s = String(name || "");
|
|
||||||
|
|
||||||
// Remove noise patterns
|
|
||||||
for (const re of NOISE_PATTERNS) s = s.replace(re, " ");
|
|
||||||
|
|
||||||
// Collapse spaces
|
|
||||||
s = s.replace(/\s+/g, " ").trim();
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractIconTagsAndStrip(name) {
|
|
||||||
let s = String(name || "");
|
|
||||||
const tags = [];
|
|
||||||
|
|
||||||
for (const r of ICON_RULES) {
|
|
||||||
if (r.regex.test(s)) {
|
|
||||||
tags.push(r.icon);
|
|
||||||
s = s.replace(r.regex, " ");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const t of NAME_NETWORK_TAGS) {
|
|
||||||
if (t.regex.test(s)) {
|
|
||||||
tags.push(t.tag);
|
|
||||||
s = s.replace(t.regex, " ");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { stripped: s.replace(/\s+/g, " ").trim(), tags: uniq(tags) };
|
|
||||||
}
|
|
||||||
|
|
||||||
function detectCountryByName(name) {
|
|
||||||
const n = String(name || "");
|
|
||||||
// Order by priority, then first match wins
|
|
||||||
|
|
||||||
// Fast path: flag emoji
|
|
||||||
if (n.includes("🇦🇪")) return { iso3: "ARE", flag: "🇦🇪", priority: 1, source: "flag" };
|
|
||||||
if (n.includes("🇦🇱")) return { iso3: "ALB", flag: "🇦🇱", priority: 11, source: "flag" };
|
|
||||||
if (n.includes("🇦🇷")) return { iso3: "ARG", flag: "🇦🇷", priority: 2, source: "flag" };
|
|
||||||
if (n.includes("🇦🇹")) return { iso3: "AUT", flag: "🇦🇹", priority: 3, source: "flag" };
|
|
||||||
if (n.includes("🇦🇶")) return { iso3: "ATA", flag: "🇦🇶", priority: 11, source: "flag" };
|
|
||||||
if (n.includes("🇦🇺")) return { iso3: "AUS", flag: "🇦🇺", priority: 4, source: "flag" };
|
|
||||||
if (n.includes("🇧🇪")) return { iso3: "BEL", flag: "🇧🇪", priority: 11, source: "flag" };
|
|
||||||
if (n.includes("🇧🇬")) return { iso3: "BGR", flag: "🇧🇬", priority: 5, source: "flag" };
|
|
||||||
if (n.includes("🇧🇾")) return { iso3: "BLR", flag: "🇧🇾", priority: 6, source: "flag" };
|
|
||||||
if (n.includes("🇧🇷")) return { iso3: "BRA", flag: "🇧🇷", priority: 7, source: "flag" };
|
|
||||||
if (n.includes("🇨🇦")) return { iso3: "CAN", flag: "🇨🇦", priority: 8, source: "flag" };
|
|
||||||
if (n.includes("🇨🇭")) return { iso3: "CHE", flag: "🇨🇭", priority: 9, source: "flag" };
|
|
||||||
if (n.includes("🇨🇳")) return { iso3: "CHN", flag: "🇨🇳", priority: 10, source: "flag" };
|
|
||||||
if (n.includes("🇨🇳")) return { iso3: "CHN", flag: "🇨🇳", priority: 10, source: "flag" };
|
|
||||||
if (n.includes("🇨🇾")) return { iso3: "CYP", flag: "🇨🇾", priority: 11, source: "flag" };
|
|
||||||
if (n.includes("🇩🇪")) return { iso3: "DEU", flag: "🇩🇪", priority: 12, source: "flag" };
|
|
||||||
if (n.includes("🇩🇰")) return { iso3: "DNK", flag: "🇩🇰", priority: 13, source: "flag" };
|
|
||||||
if (n.includes("🇪🇪")) return { iso3: "EST", flag: "🇪🇪", priority: 14, source: "flag" };
|
|
||||||
if (n.includes("🇪🇬")) return { iso3: "EGY", flag: "🇪🇬", priority: 15, source: "flag" };
|
|
||||||
if (n.includes("🇪🇸")) return { iso3: "ESP", flag: "🇪🇸", priority: 16, source: "flag" };
|
|
||||||
if (n.includes("🇫🇮")) return { iso3: "FIN", flag: "🇫🇮", priority: 17, source: "flag" };
|
|
||||||
if (n.includes("🇫🇷")) return { iso3: "FRA", flag: "🇫🇷", priority: 18, source: "flag" };
|
|
||||||
if (n.includes("🇬🇧")) return { iso3: "GBR", flag: "🇬🇧", priority: 19, source: "flag" };
|
|
||||||
if (n.includes("🇬🇪")) return { iso3: "GEO", flag: "🇬🇪", priority: 20, source: "flag" };
|
|
||||||
if (n.includes("🇬🇷")) return { iso3: "GRC", flag: "🇬🇷", priority: 11, source: "flag" };
|
|
||||||
if (n.includes("🇭🇰")) return { iso3: "HKG", flag: "🇭🇰", priority: 21, source: "flag" };
|
|
||||||
if (n.includes("🇭🇷")) return { iso3: "HRV", flag: "🇭🇷", priority: 11, source: "flag" };
|
|
||||||
if (n.includes("🇮🇪")) return { iso3: "IRL", flag: "🇮🇪", priority: 22, source: "flag" };
|
|
||||||
if (n.includes("🇮🇱")) return { iso3: "ISR", flag: "🇮🇱", priority: 23, source: "flag" };
|
|
||||||
if (n.includes("🇮🇳")) return { iso3: "IND", flag: "🇮🇳", priority: 24, source: "flag" };
|
|
||||||
if (n.includes("🇮🇹")) return { iso3: "ITA", flag: "🇮🇹", priority: 25, source: "flag" };
|
|
||||||
if (n.includes("🇮🇸")) return { iso3: "ISL", flag: "🇮🇸", priority: 25, source: "flag" };
|
|
||||||
if (n.includes("🇯🇵")) return { iso3: "JPN", flag: "🇯🇵", priority: 26, source: "flag" };
|
|
||||||
if (n.includes("🇰🇷")) return { iso3: "KOR", flag: "🇰🇷", priority: 27, source: "flag" };
|
|
||||||
if (n.includes("🇰🇿")) return { iso3: "KAZ", flag: "🇰🇿", priority: 28, source: "flag" };
|
|
||||||
if (n.includes("🇱🇹")) return { iso3: "LTU", flag: "🇱🇹", priority: 29, source: "flag" };
|
|
||||||
if (n.includes("🇱🇻")) return { iso3: "LVA", flag: "🇱🇻", priority: 30, source: "flag" };
|
|
||||||
if (n.includes("🇲🇩")) return { iso3: "MDA", flag: "🇲🇩", priority: 31, source: "flag" };
|
|
||||||
if (n.includes("🇲🇾")) return { iso3: "MYS", flag: "🇲🇾", priority: 32, source: "flag" };
|
|
||||||
if (n.includes("🇳🇬")) return { iso3: "NGA", flag: "🇳🇬", priority: 33, source: "flag" };
|
|
||||||
if (n.includes("🇳🇱")) return { iso3: "NLD", flag: "🇳🇱", priority: 34, source: "flag" };
|
|
||||||
if (n.includes("🇳🇴")) return { iso3: "NOR", flag: "🇳🇴", priority: 35, source: "flag" };
|
|
||||||
if (n.includes("🇵🇭")) return { iso3: "PHL", flag: "🇵🇭", priority: 36, source: "flag" };
|
|
||||||
if (n.includes("🇵🇰")) return { iso3: "PAK", flag: "🇵🇰", priority: 11, source: "flag" };
|
|
||||||
if (n.includes("🇵🇱")) return { iso3: "POL", flag: "🇵🇱", priority: 37, source: "flag" };
|
|
||||||
if (n.includes("🇵🇹")) return { iso3: "PRT", flag: "🇵🇹", priority: 38, source: "flag" };
|
|
||||||
if (n.includes("🇷🇴")) return { iso3: "ROU", flag: "🇷🇴", priority: 39, source: "flag" };
|
|
||||||
if (n.includes("🇷🇺")) return { iso3: "RUS", flag: "🇷🇺", priority: 40, source: "flag" };
|
|
||||||
if (n.includes("🇸🇪")) return { iso3: "SWE", flag: "🇸🇪", priority: 41, source: "flag" };
|
|
||||||
if (n.includes("🇸🇬")) return { iso3: "SGP", flag: "🇸🇬", priority: 42, source: "flag" };
|
|
||||||
if (n.includes("🇸🇾")) return { iso3: "SYR", flag: "🇸🇾", priority: 11, source: "flag" };
|
|
||||||
if (n.includes("🇹🇭")) return { iso3: "THA", flag: "🇹🇭", priority: 43, source: "flag" };
|
|
||||||
if (n.includes("🇹🇷")) return { iso3: "TUR", flag: "🇹🇷", priority: 44, source: "flag" };
|
|
||||||
if (n.includes("🇹🇼")) return { iso3: "TWN", flag: "🇹🇼", priority: 45, source: "flag" };
|
|
||||||
if (n.includes("🇺🇦")) return { iso3: "UKR", flag: "🇺🇦", priority: 11, source: "flag" };
|
|
||||||
if (n.includes("🇺🇸")) return { iso3: "USA", flag: "🇺🇸", priority: 46, source: "flag" };
|
|
||||||
if (n.includes("🇻🇳")) return { iso3: "VNM", flag: "🇻🇳", priority: 47, source: "flag" };
|
|
||||||
|
|
||||||
const sorted = COUNTRY_RULES.slice().sort((a, b) => a.priority - b.priority);
|
|
||||||
for (const c of sorted) {
|
|
||||||
if (c.regex.test(n)) return { iso3: c.iso3, flag: c.flag, priority: c.priority, source: "name" };
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function detectCountryByGeoIP(server, utils) {
|
|
||||||
if (!isIPv4(server)) return null;
|
|
||||||
if (!utils || !utils.geoip || typeof utils.geoip.lookup !== "function") return null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const geo = utils.geoip.lookup(server);
|
|
||||||
const iso2 = geo && (geo.country || geo.country_code || geo.iso_code);
|
|
||||||
if (!iso2 || typeof iso2 !== "string") return null;
|
|
||||||
|
|
||||||
const key = iso2.toUpperCase();
|
|
||||||
const mapped = ISO2_TO_ISO3[key];
|
|
||||||
if (mapped) return { iso3: mapped.iso3, flag: mapped.flag, priority: 900, source: "geoip" };
|
|
||||||
|
|
||||||
// Unknown ISO2: keep something sane
|
|
||||||
return { iso3: key, flag: "🏳️", priority: 950, source: "geoip" };
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function pad2(n) {
|
|
||||||
const x = Number(n);
|
|
||||||
return x < 10 ? `0${x}` : String(x);
|
|
||||||
}
|
|
||||||
|
|
||||||
function safeStr(v) {
|
|
||||||
return (v === undefined || v === null) ? "" : String(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////
|
|
||||||
// OPERATOR
|
|
||||||
///////////////////////
|
|
||||||
|
|
||||||
function operator(proxies, targetPlatform, utils) {
|
|
||||||
// Sub-Store sometimes passes utils as global $utils; sometimes as 3rd arg; sometimes not at all.
|
|
||||||
// We'll accept any of them without whining.
|
|
||||||
const U = utils || (typeof $utils !== "undefined" ? $utils : null);
|
|
||||||
|
|
||||||
const buckets = Object.create(null);
|
|
||||||
|
|
||||||
for (const proxy of proxies) {
|
|
||||||
const originalName = safeStr(proxy && proxy.name);
|
|
||||||
|
|
||||||
// 1) Extract tags (icons) from ORIGINAL name, then strip those keywords out
|
|
||||||
const iconStage = extractIconTagsAndStrip(originalName);
|
|
||||||
|
|
||||||
// 2) Sanitize remaining base name (remove marketing trash, brackets, etc.)
|
|
||||||
const cleanBase = sanitizeBaseName(iconStage.stripped);
|
|
||||||
|
|
||||||
// 3) Detect country (name first, then GeoIP)
|
|
||||||
const byName = detectCountryByName(originalName);
|
|
||||||
const byGeo = detectCountryByGeoIP(proxy && proxy.server, U);
|
|
||||||
const country = byName || byGeo || { iso3: "UNK", flag: "🏴☠️", priority: 9999, source: "fallback" };
|
|
||||||
|
|
||||||
// 4) Protocol icon (based on type)
|
|
||||||
const proto = PROTOCOL_ICONS[(proxy && proxy.type) || ""] || PROTOCOL_ICON_DEFAULT;
|
|
||||||
|
|
||||||
// 5) Network/type/port tag (from proxy fields)
|
|
||||||
const metaTag = buildMetaTag(proxy);
|
|
||||||
|
|
||||||
// 6) Prepare bucket key
|
|
||||||
const key = country.iso3;
|
|
||||||
|
|
||||||
if (!buckets[key]) {
|
|
||||||
buckets[key] = {
|
|
||||||
country,
|
|
||||||
list: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep meta used for sorting and final formatting
|
|
||||||
buckets[key].list.push({
|
|
||||||
proxy,
|
|
||||||
_meta: {
|
|
||||||
originalName,
|
|
||||||
cleanBase,
|
|
||||||
iconTags: iconStage.tags,
|
|
||||||
proto,
|
|
||||||
metaTag
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 7) Sort buckets by priority
|
|
||||||
const bucketKeys = Object.keys(buckets).sort((a, b) => {
|
|
||||||
return (buckets[a].country.priority || 9999) - (buckets[b].country.priority || 9999);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 8) Sort inside each country bucket and rename with per-country numbering
|
|
||||||
const result = [];
|
|
||||||
|
|
||||||
for (const key of bucketKeys) {
|
|
||||||
const group = buckets[key];
|
|
||||||
|
|
||||||
group.list.sort((A, B) => {
|
|
||||||
// Tags do not affect numbering: sort only by sanitized base + server:port as tie-breaker
|
|
||||||
const an = A._meta.cleanBase.toLowerCase();
|
|
||||||
const bn = B._meta.cleanBase.toLowerCase();
|
|
||||||
if (an !== bn) return an.localeCompare(bn);
|
|
||||||
|
|
||||||
const as = `${safeStr(A.proxy.server)}:${safeStr(A.proxy.port)}`;
|
|
||||||
const bs = `${safeStr(B.proxy.server)}:${safeStr(B.proxy.port)}`;
|
|
||||||
return as.localeCompare(bs);
|
|
||||||
});
|
|
||||||
|
|
||||||
for (let i = 0; i < group.list.length; i++) {
|
|
||||||
const item = group.list[i];
|
|
||||||
const p = item.proxy;
|
|
||||||
const num = pad2(i + 1);
|
|
||||||
|
|
||||||
const debugSuffix = DEBUG_APPEND_ORIGINAL_NAME
|
|
||||||
? ` ⟦${item._meta.originalName}⟧`
|
|
||||||
: "";
|
|
||||||
|
|
||||||
const tagStr = item._meta.iconTags.length ? ` ${item._meta.iconTags.join(" ")}` : "";
|
|
||||||
|
|
||||||
p.name = `${group.country.flag}${item._meta.metaTag} ${group.country.iso3}-${num} ${item._meta.proto}${tagStr} ${debugSuffix}`
|
|
||||||
.replace(/\s+/g, " ")
|
|
||||||
.trim();
|
|
||||||
|
|
||||||
result.push(p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
function safeStringify(obj) {
|
|
||||||
const seen = new WeakSet();
|
|
||||||
return JSON.stringify(
|
|
||||||
obj,
|
|
||||||
(k, v) => {
|
|
||||||
if (typeof v === "object" && v !== null) {
|
|
||||||
if (seen.has(v)) return "[Circular]";
|
|
||||||
seen.add(v);
|
|
||||||
}
|
|
||||||
if (typeof v === "function") return `[Function: ${v.name || "anonymous"}]`;
|
|
||||||
if (typeof v === "bigint") return v.toString();
|
|
||||||
return v;
|
|
||||||
},
|
|
||||||
2
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function pickEnvSample() {
|
|
||||||
try {
|
|
||||||
const env = (typeof process !== "undefined" && process && process.env) ? process.env : null;
|
|
||||||
if (!env) return null;
|
|
||||||
|
|
||||||
// only show safe-ish keys, no full dump
|
|
||||||
const keys = Object.keys(env).sort();
|
|
||||||
const filtered = keys.filter(k =>
|
|
||||||
k.toLowerCase().includes("sub") ||
|
|
||||||
k.toLowerCase().includes("store") ||
|
|
||||||
k.toLowerCase().includes("script") ||
|
|
||||||
k.toLowerCase().includes("url") ||
|
|
||||||
k.toLowerCase().includes("option") ||
|
|
||||||
k.toLowerCase().includes("param")
|
|
||||||
);
|
|
||||||
|
|
||||||
const sample = {};
|
|
||||||
for (const k of filtered.slice(0, 50)) sample[k] = env[k];
|
|
||||||
return { keysCount: keys.length, filteredKeys: filtered.slice(0, 100), sample };
|
|
||||||
} catch (e) {
|
|
||||||
return { error: String(e) };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getGlobalDollarKeys() {
|
|
||||||
try {
|
|
||||||
return Object.getOwnPropertyNames(globalThis).filter(k => k.startsWith("$")).sort();
|
|
||||||
} catch {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Safe "typeof" probes: never throws even if variable doesn't exist
|
|
||||||
const probes = {
|
|
||||||
$content: typeof $content,
|
|
||||||
$files: typeof $files,
|
|
||||||
$options: typeof $options,
|
|
||||||
|
|
||||||
$params: typeof $params,
|
|
||||||
$args: typeof $args,
|
|
||||||
$arguments: typeof $arguments,
|
|
||||||
$argument: typeof $argument,
|
|
||||||
$argv: typeof $argv,
|
|
||||||
|
|
||||||
$ctx: typeof $ctx,
|
|
||||||
$context: typeof $context,
|
|
||||||
$request: typeof $request,
|
|
||||||
$req: typeof $req,
|
|
||||||
$url: typeof $url,
|
|
||||||
$scriptUrl: typeof $scriptUrl,
|
|
||||||
$script_url: typeof $script_url,
|
|
||||||
|
|
||||||
ProxyUtils: typeof ProxyUtils,
|
|
||||||
produceArtifact: typeof produceArtifact,
|
|
||||||
|
|
||||||
process: typeof process,
|
|
||||||
};
|
|
||||||
|
|
||||||
const values = {};
|
|
||||||
function maybeSet(name, getter) {
|
|
||||||
try {
|
|
||||||
const v = getter();
|
|
||||||
// Avoid huge outputs
|
|
||||||
if (typeof v === "string") values[name] = v.length > 800 ? v.slice(0, 800) + "…(truncated)" : v;
|
|
||||||
else values[name] = v;
|
|
||||||
} catch (e) {
|
|
||||||
values[name] = { error: String(e) };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
maybeSet("$options", () => (typeof $options !== "undefined" ? $options : null));
|
|
||||||
maybeSet("$params", () => (typeof $params !== "undefined" ? $params : null));
|
|
||||||
maybeSet("$args", () => (typeof $args !== "undefined" ? $args : null));
|
|
||||||
maybeSet("$arguments", () => (typeof $arguments !== "undefined" ? $arguments : null));
|
|
||||||
maybeSet("$argument", () => (typeof $argument !== "undefined" ? $argument : null));
|
|
||||||
maybeSet("$ctx", () => (typeof $ctx !== "undefined" ? $ctx : null));
|
|
||||||
maybeSet("$request", () => (typeof $request !== "undefined" ? $request : null));
|
|
||||||
maybeSet("$url", () => (typeof $url !== "undefined" ? $url : null));
|
|
||||||
maybeSet("$scriptUrl", () => (typeof $scriptUrl !== "undefined" ? $scriptUrl : null));
|
|
||||||
maybeSet("$script_url", () => (typeof $script_url !== "undefined" ? $script_url : null));
|
|
||||||
|
|
||||||
maybeSet("$contentPreview", () => (typeof $content === "string" ? $content.slice(0, 300) : $content));
|
|
||||||
maybeSet("$contentLength", () => (typeof $content === "string" ? $content.length : null));
|
|
||||||
maybeSet("$files", () => (typeof $files !== "undefined" ? $files : null));
|
|
||||||
|
|
||||||
const report = {
|
|
||||||
probes,
|
|
||||||
values,
|
|
||||||
globalDollarKeys: getGlobalDollarKeys(),
|
|
||||||
envSample: pickEnvSample(),
|
|
||||||
};
|
|
||||||
|
|
||||||
$content = safeStringify(report);
|
|
||||||
@@ -29,12 +29,12 @@ NETBIRD_SETUP_KEY="7369BE4D-C485-4339-A7CA-C245FD95E857"
|
|||||||
NETBIRD_MANAGEMENT_URL="https://webway.shamanlanding.org:443"
|
NETBIRD_MANAGEMENT_URL="https://webway.shamanlanding.org:443"
|
||||||
|
|
||||||
# Mihomo Version (Alpha)
|
# Mihomo Version (Alpha)
|
||||||
MIHOMO_URL="https://github.com/vernesong/mihomo/releases/download/Prerelease-Alpha/mihomo-linux-amd64-alpha-smart-ec7f445.gz"
|
MIHOMO_URL="https://github.com/vernesong/mihomo/releases/download/Prerelease-Alpha/mihomo-linux-amd64-alpha-smart-26a9e08.gz"
|
||||||
|
|
||||||
# Remote Resources
|
# Remote Resources
|
||||||
REPO_BASE="https://gitea.shamanlanding.org/DaTekShaman/clash-rules/raw/branch/main"
|
REPO_BASE="https://gitea.shamanlanding.org/DaTekShaman/clash-rules/raw/branch/main"
|
||||||
URL_CONFIG_MIHOMO="${REPO_BASE}/config-clash/cadian/cadian.current.yaml"
|
URL_CONFIG_MIHOMO="${REPO_BASE}/config-clash/solar/solar.yaml"
|
||||||
URL_SCRIPT_IPTABLES="${REPO_BASE}/scripts/iptables-mihomo-setup.sh"
|
URL_SCRIPT_IPTABLES="${REPO_BASE}/scripts/warpgates/iptables-mihomo-setup-alpine-mark2.sh"
|
||||||
URL_INIT_MIHOMO="${REPO_BASE}/init-scripts/openrc/mihomo"
|
URL_INIT_MIHOMO="${REPO_BASE}/init-scripts/openrc/mihomo"
|
||||||
URL_INIT_IPTABLES="${REPO_BASE}/init-scripts/openrc/mihomo-iptables"
|
URL_INIT_IPTABLES="${REPO_BASE}/init-scripts/openrc/mihomo-iptables"
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ UI_DIR="/etc/mihomo/ui"
|
|||||||
# ==========================================
|
# ==========================================
|
||||||
echo ">>> [1/8] Updating system and installing dependencies..."
|
echo ">>> [1/8] Updating system and installing dependencies..."
|
||||||
# Включаем community репозитории (обычно там лежит gcompat и прочее)
|
# Включаем community репозитории (обычно там лежит gcompat и прочее)
|
||||||
sed -i 's/^#//g' /etc/apk/repositories
|
sed -i '/v[0-9]\.[0-9]*\/community/s/^#//' /etc/apk/repositories
|
||||||
apk update
|
apk update
|
||||||
apk add bash curl wget ca-certificates tar iptables ip6tables jq coreutils libcap bind-tools nano openrc openssh sudo shadow
|
apk add bash curl wget ca-certificates tar iptables ip6tables jq coreutils libcap bind-tools nano openrc openssh sudo shadow
|
||||||
|
|
||||||
@@ -67,6 +67,7 @@ net.ipv4.conf.default.rp_filter=0
|
|||||||
net.ipv4.conf.wt0.rp_filter=0
|
net.ipv4.conf.wt0.rp_filter=0
|
||||||
EOF
|
EOF
|
||||||
sysctl -p /etc/sysctl.d/99-warpgate.conf
|
sysctl -p /etc/sysctl.d/99-warpgate.conf
|
||||||
|
rc-update add sysctl boot
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# 3. NETBIRD INSTALLATION
|
# 3. NETBIRD INSTALLATION
|
||||||
133
scripts/warpgates/iptables-mihomo-setup-alpine-mark2.sh
Normal file
133
scripts/warpgates/iptables-mihomo-setup-alpine-mark2.sh
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Config
|
||||||
|
# ----------------------------
|
||||||
|
MIHOMO_UID="mihomo"
|
||||||
|
REDIR_PORT="7892" # TCP Redirect
|
||||||
|
TPROXY_PORT="7893" # UDP/TCP TProxy
|
||||||
|
FW_MARK="0x1"
|
||||||
|
ROUTE_TABLE="100"
|
||||||
|
|
||||||
|
EXCLUDE_IFACES=("tun0")
|
||||||
|
INCLUDE_IFACES=("wt0" "eth1" "eth2")
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Helpers
|
||||||
|
# ----------------------------
|
||||||
|
ipt() { iptables "$@"; }
|
||||||
|
|
||||||
|
del_loop() {
|
||||||
|
local table=$1
|
||||||
|
local chain=$2
|
||||||
|
shift 2
|
||||||
|
local rule_args="$@"
|
||||||
|
|
||||||
|
while iptables -t "$table" -C "$chain" $rule_args 2>/dev/null; do
|
||||||
|
echo "Deleting from $table/$chain: $rule_args"
|
||||||
|
iptables -t "$table" -D "$chain" $rule_args
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_ip_rule() {
|
||||||
|
while ip rule list | grep -q "fwmark ${FW_MARK} lookup ${ROUTE_TABLE}"; do
|
||||||
|
ip rule del fwmark ${FW_MARK} lookup ${ROUTE_TABLE} || true
|
||||||
|
done
|
||||||
|
ip rule add fwmark ${FW_MARK} lookup ${ROUTE_TABLE}
|
||||||
|
ip route replace local 0.0.0.0/0 dev lo table ${ROUTE_TABLE}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# CLEANUP PHASE
|
||||||
|
# ----------------------------
|
||||||
|
echo "--- Cleaning up old rules (Robust Mode) ---"
|
||||||
|
|
||||||
|
del_loop nat OUTPUT -p tcp -m comment --comment "MIHOMO-JUMP" -j MIHOMO_REDIR
|
||||||
|
del_loop nat PREROUTING -i wt0 -p tcp -m comment --comment "MIHOMO-REDIRECT" -j REDIRECT --to-port "${REDIR_PORT}"
|
||||||
|
del_loop mangle PREROUTING -i wt0 -m comment --comment "MIHOMO-JUMP" -j MIHOMO_TPROXY
|
||||||
|
|
||||||
|
del_loop mangle OUTPUT -p tcp -m comment --comment "MIHOMO-MARK" -j MARK --set-mark "${FW_MARK}"
|
||||||
|
del_loop mangle OUTPUT -p udp -m comment --comment "MIHOMO-MARK" -j MARK --set-mark "${FW_MARK}"
|
||||||
|
|
||||||
|
for IFACE in "${EXCLUDE_IFACES[@]}"; do
|
||||||
|
del_loop mangle OUTPUT -o "${IFACE}" -m comment --comment "MIHOMO-EXCLUDE" -j RETURN
|
||||||
|
del_loop mangle PREROUTING -i "${IFACE}" -m comment --comment "MIHOMO-EXCLUDE" -j RETURN
|
||||||
|
del_loop nat OUTPUT -o "${IFACE}" -m comment --comment "MIHOMO-EXCLUDE" -j RETURN
|
||||||
|
done
|
||||||
|
|
||||||
|
del_loop mangle OUTPUT -m owner --uid-owner "${MIHOMO_UID}" -m comment --comment "MIHOMO-EXCLUDE" -j RETURN
|
||||||
|
del_loop nat OUTPUT -m owner --uid-owner "${MIHOMO_UID}" -m comment --comment "MIHOMO-EXCLUDE" -j RETURN
|
||||||
|
|
||||||
|
ipt -t mangle -F MIHOMO_TPROXY 2>/dev/null || true
|
||||||
|
ipt -t mangle -X MIHOMO_TPROXY 2>/dev/null || true
|
||||||
|
|
||||||
|
ipt -t nat -F MIHOMO_REDIR 2>/dev/null || true
|
||||||
|
ipt -t nat -X MIHOMO_REDIR 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "--- Cleanup finished. Applying new rules ---"
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# NAT (REDIRECT) - TCP
|
||||||
|
# ----------------------------
|
||||||
|
ipt -t nat -N MIHOMO_REDIR
|
||||||
|
|
||||||
|
# Exclusions for gateway's own traffic
|
||||||
|
ipt -t nat -A MIHOMO_REDIR -d 192.168.0.0/16 -j RETURN
|
||||||
|
ipt -t nat -A MIHOMO_REDIR -d 10.0.0.0/8 -j RETURN
|
||||||
|
ipt -t nat -A MIHOMO_REDIR -d 172.16.0.0/12 -j RETURN
|
||||||
|
ipt -t nat -A MIHOMO_REDIR -d 127.0.0.0/8 -j RETURN
|
||||||
|
ipt -t nat -A MIHOMO_REDIR -p tcp -j REDIRECT --to-ports "${REDIR_PORT}"
|
||||||
|
|
||||||
|
# Apply to OUTPUT (Local gateway traffic)
|
||||||
|
for IFACE in "${EXCLUDE_IFACES[@]}"; do
|
||||||
|
ipt -t nat -A OUTPUT -o "${IFACE}" -m comment --comment "MIHOMO-EXCLUDE" -j RETURN
|
||||||
|
done
|
||||||
|
ipt -t nat -A OUTPUT -m owner --uid-owner "${MIHOMO_UID}" -m comment --comment "MIHOMO-EXCLUDE" -j RETURN
|
||||||
|
ipt -t nat -A OUTPUT -p tcp -m comment --comment "MIHOMO-JUMP" -j MIHOMO_REDIR
|
||||||
|
|
||||||
|
# Apply to PREROUTING (wt0 Ingress) - Force Redir for NetBird (skips exclusions by design)
|
||||||
|
for IFACE in "${INCLUDE_IFACES[@]}"; do
|
||||||
|
if [ "$IFACE" = "wt0" ]; then
|
||||||
|
# wt0 (Netbird) пропускает исключения локальных подсетей по твоему дизайну
|
||||||
|
ipt -t nat -A PREROUTING -i "$IFACE" -p tcp -m comment --comment "MIHOMO-REDIRECT" -j REDIRECT --to-port "${REDIR_PORT}"
|
||||||
|
else
|
||||||
|
# LAN-трафик (eth1, eth2) должен прыгать в цепочку MIHOMO_REDIR для проверки исключений (192.168.x.x и т.д.)
|
||||||
|
ipt -t nat -A PREROUTING -i "$IFACE" -p tcp -m comment --comment "MIHOMO-JUMP-$IFACE" -j MIHOMO_REDIR
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# MANGLE (TPROXY) - UDP
|
||||||
|
# ----------------------------
|
||||||
|
ensure_ip_rule
|
||||||
|
ipt -t mangle -N MIHOMO_TPROXY
|
||||||
|
|
||||||
|
# Local exclusions: apply ONLY if traffic is NOT coming from NetBird (wt0)
|
||||||
|
ipt -t mangle -A MIHOMO_TPROXY ! -i wt0 -d 192.168.0.0/16 -j RETURN
|
||||||
|
ipt -t mangle -A MIHOMO_TPROXY ! -i wt0 -d 10.0.0.0/8 -j RETURN
|
||||||
|
ipt -t mangle -A MIHOMO_TPROXY ! -i wt0 -d 172.16.0.0/12 -j RETURN
|
||||||
|
ipt -t mangle -A MIHOMO_TPROXY ! -i wt0 -d 127.0.0.0/8 -j RETURN
|
||||||
|
|
||||||
|
# TProxy Targets (UDP only, TCP is handled by REDIRECT)
|
||||||
|
ipt -t mangle -A MIHOMO_TPROXY -p udp -j TPROXY --on-port "${TPROXY_PORT}" --tproxy-mark "${FW_MARK}/${FW_MARK}"
|
||||||
|
|
||||||
|
# Apply to OUTPUT (Local gateway traffic)
|
||||||
|
for IFACE in "${EXCLUDE_IFACES[@]}"; do
|
||||||
|
ipt -t mangle -A OUTPUT -o "${IFACE}" -m comment --comment "MIHOMO-EXCLUDE" -j RETURN
|
||||||
|
done
|
||||||
|
ipt -t mangle -A OUTPUT -m owner --uid-owner "${MIHOMO_UID}" -m comment --comment "MIHOMO-EXCLUDE" -j RETURN
|
||||||
|
|
||||||
|
# Mark local UDP packets
|
||||||
|
ipt -t mangle -A OUTPUT -p udp -m comment --comment "MIHOMO-MARK" -j MARK --set-mark "${FW_MARK}"
|
||||||
|
|
||||||
|
# Apply to PREROUTING (wt0 Ingress)
|
||||||
|
for IFACE in "${EXCLUDE_IFACES[@]}"; do
|
||||||
|
ipt -t mangle -A PREROUTING -i "${IFACE}" -m comment --comment "MIHOMO-EXCLUDE" -j RETURN
|
||||||
|
done
|
||||||
|
|
||||||
|
for IFACE in "${INCLUDE_IFACES[@]}"; do
|
||||||
|
ipt -t mangle -A PREROUTING -i "$IFACE" -m comment --comment "MIHOMO-JUMP-$IFACE" -j MIHOMO_TPROXY
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Done. Suboptimal hypervisor constraints bypassed successfully."
|
||||||
89
scripts/warpgates/iptables-mihomo-setup-alpine.sh
Normal file
89
scripts/warpgates/iptables-mihomo-setup-alpine.sh
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -u
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Config
|
||||||
|
# ----------------------------
|
||||||
|
MIHOMO_UID="mihomo"
|
||||||
|
TPROXY_PORT="7893"
|
||||||
|
FW_MARK="0x1"
|
||||||
|
ROUTE_TABLE="100"
|
||||||
|
|
||||||
|
# Интерфейсы клиентов (откуда прилетают запросы)
|
||||||
|
LAN_IFACES=("wt0" "eth1" "eth2")
|
||||||
|
|
||||||
|
# Порты самого сервера, которые НЕ надо проксировать (Web UI, SSH)
|
||||||
|
LOCAL_PORTS="9090,22"
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Helpers
|
||||||
|
# ----------------------------
|
||||||
|
ipt() { iptables "$@"; }
|
||||||
|
|
||||||
|
cleanup_references() {
|
||||||
|
local chain=$1
|
||||||
|
iptables-save | grep "\-j $chain" | sed "s/^-A/-D/" | while read rule; do
|
||||||
|
iptables -t mangle $rule 2>/dev/null || true
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_ip_rule() {
|
||||||
|
# 1. Перехват трафика от клиентов в TProxy (то, что мы уже починили)
|
||||||
|
if ! ip rule list | grep -q "fwmark ${FW_MARK} lookup ${ROUTE_TABLE}"; then
|
||||||
|
ip rule add fwmark ${FW_MARK} lookup ${ROUTE_TABLE} pref 90
|
||||||
|
fi
|
||||||
|
if ! ip route show table ${ROUTE_TABLE} | grep -q "local default"; then
|
||||||
|
ip route add local 0.0.0.0/0 dev lo table ${ROUTE_TABLE}
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. НОВОЕ: Выпуск трафика Mihomo в интернет в обход Netbird
|
||||||
|
if ! ip rule list | grep -q "fwmark 1337 lookup main"; then
|
||||||
|
ip rule add fwmark 1337 lookup main pref 80
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# 1. CLEANUP
|
||||||
|
# ----------------------------
|
||||||
|
echo "--- Cleaning up rules ---"
|
||||||
|
cleanup_references "MIHOMO_TPROXY"
|
||||||
|
ipt -t mangle -F MIHOMO_TPROXY 2>/dev/null || true
|
||||||
|
ipt -t mangle -X MIHOMO_TPROXY 2>/dev/null || true
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# 2. SETUP
|
||||||
|
# ----------------------------
|
||||||
|
ensure_ip_rule
|
||||||
|
|
||||||
|
# --- CHAIN: PREROUTING (Для клиентов) ---
|
||||||
|
ipt -t mangle -N MIHOMO_TPROXY
|
||||||
|
|
||||||
|
# === 1. Исключения по Портам (CRITICAL FIX) ===
|
||||||
|
# Если стучатся в веб-морду или SSH - пропускаем мимо TProxy
|
||||||
|
ipt -t mangle -A MIHOMO_TPROXY -p tcp -m multiport --dports "${LOCAL_PORTS}" -j RETURN
|
||||||
|
|
||||||
|
# === 2. Исключения по IP (Bypass) ===
|
||||||
|
# RFC1918 Private Networks
|
||||||
|
ipt -t mangle -A MIHOMO_TPROXY -d 0.0.0.0/8 -j RETURN
|
||||||
|
ipt -t mangle -A MIHOMO_TPROXY -d 10.0.0.0/8 -j RETURN
|
||||||
|
ipt -t mangle -A MIHOMO_TPROXY -d 127.0.0.0/8 -j RETURN
|
||||||
|
ipt -t mangle -A MIHOMO_TPROXY -d 169.254.0.0/16 -j RETURN
|
||||||
|
ipt -t mangle -A MIHOMO_TPROXY -d 172.16.0.0/12 -j RETURN
|
||||||
|
ipt -t mangle -A MIHOMO_TPROXY -d 192.168.0.0/16 -j RETURN
|
||||||
|
# Multicast
|
||||||
|
ipt -t mangle -A MIHOMO_TPROXY -d 224.0.0.0/4 -j RETURN
|
||||||
|
ipt -t mangle -A MIHOMO_TPROXY -d 240.0.0.0/4 -j RETURN
|
||||||
|
# !!! NETBIRD / CGNAT (Fix for VPN access) !!!
|
||||||
|
ipt -t mangle -A MIHOMO_TPROXY -d 100.64.0.0/10 -j RETURN
|
||||||
|
|
||||||
|
# === 3. Заворачиваем в TProxy ===
|
||||||
|
ipt -t mangle -A MIHOMO_TPROXY -p tcp -j TPROXY --on-port "${TPROXY_PORT}" --tproxy-mark "${FW_MARK}"
|
||||||
|
ipt -t mangle -A MIHOMO_TPROXY -p udp -j TPROXY --on-port "${TPROXY_PORT}" --tproxy-mark "${FW_MARK}"
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# 3. APPLY
|
||||||
|
# ----------------------------
|
||||||
|
for IFACE in "${LAN_IFACES[@]}"; do
|
||||||
|
echo "Adding TProxy rules for interface: $IFACE"
|
||||||
|
ipt -t mangle -A PREROUTING -i "$IFACE" -j MIHOMO_TPROXY
|
||||||
|
done
|
||||||
81
scripts/warpgates/update-core-and-dash.sh
Normal file
81
scripts/warpgates/update-core-and-dash.sh
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
UI_URL="https://github.com/Zephyruso/zashboard/releases/latest/download/dist-cdn-fonts.zip"
|
||||||
|
BIN_DIR="/usr/local/bin"
|
||||||
|
UI_DIR="/etc/mihomo/ui/zashboard"
|
||||||
|
|
||||||
|
echo "[*] Resolving latest Alpha URL from vernesong/mihomo..."
|
||||||
|
CORE_URL=$(curl -sL "https://api.github.com/repos/vernesong/mihomo/releases/tags/Prerelease-Alpha" | grep -o 'https://[^"]*mihomo-linux-amd64-alpha-smart-[^"]*\.gz' | head -n 1)
|
||||||
|
|
||||||
|
if [ -z "$CORE_URL" ]; then
|
||||||
|
echo "[-] ERROR: Failed to resolve download URL."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[+] Target URL: $CORE_URL"
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# ФАЗА 1: СЕТЕВЫЕ ОПЕРАЦИИ (пока жив DNS)
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
echo "[*] Downloading Mihomo Core..."
|
||||||
|
curl -SLf -o /tmp/mihomo.gz "$CORE_URL"
|
||||||
|
|
||||||
|
if [ ! -s /tmp/mihomo.gz ]; then
|
||||||
|
echo "[-] ERROR: Downloaded core file is empty or missing!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[*] Downloading Zashboard UI..."
|
||||||
|
curl -SLf -o /tmp/zashboard.zip "$UI_URL"
|
||||||
|
|
||||||
|
if [ ! -s /tmp/zashboard.zip ]; then
|
||||||
|
echo "[-] ERROR: Downloaded UI file is empty or missing!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# ФАЗА 2: ЛОКАЛЬНЫЕ ОПЕРАЦИИ (остановка сервиса)
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
echo "[*] Stopping mihomo service..."
|
||||||
|
rc-service mihomo stop
|
||||||
|
|
||||||
|
echo "[*] Unpacking and installing Mihomo Core..."
|
||||||
|
gzip -d -f /tmp/mihomo.gz
|
||||||
|
mv /tmp/mihomo "$BIN_DIR/mihomo"
|
||||||
|
chmod 755 "$BIN_DIR/mihomo"
|
||||||
|
chown root:root "$BIN_DIR/mihomo"
|
||||||
|
setcap 'cap_net_admin,cap_net_bind_service=+ep' "$BIN_DIR/mihomo"
|
||||||
|
|
||||||
|
echo "[*] Unpacking and installing Zashboard UI..."
|
||||||
|
# Создаем изолированную директорию для распаковки
|
||||||
|
mkdir -p /tmp/zash_temp
|
||||||
|
unzip -q -o /tmp/zashboard.zip -d /tmp/zash_temp/
|
||||||
|
|
||||||
|
# Динамически ищем, как GitHub назвал корневую папку внутри архива
|
||||||
|
EXTRACTED_DIR=$(find /tmp/zash_temp -mindepth 1 -maxdepth 1 -type d | head -n 1)
|
||||||
|
|
||||||
|
if [ -z "$EXTRACTED_DIR" ]; then
|
||||||
|
echo "[-] ERROR: Could not find extracted UI directory in the zip archive."
|
||||||
|
rc-service mihomo start
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -rf "$UI_DIR"/*
|
||||||
|
# Копируем содержимое найденной папки
|
||||||
|
cp -r "$EXTRACTED_DIR"/* "$UI_DIR"/
|
||||||
|
|
||||||
|
chown -R root:root "$UI_DIR"
|
||||||
|
find "$UI_DIR" -type d -exec chmod 755 {} \;
|
||||||
|
find "$UI_DIR" -type f -exec chmod 644 {} \;
|
||||||
|
|
||||||
|
# Зачищаем следы
|
||||||
|
rm -rf /tmp/zashboard.zip /tmp/zash_temp
|
||||||
|
|
||||||
|
echo "[*] Starting mihomo service..."
|
||||||
|
rc-service mihomo start
|
||||||
|
|
||||||
|
echo "[+] Update completed successfully."
|
||||||
Reference in New Issue
Block a user