Просмотр исходного кода

feat: modernize, clean, and robustify V2Ray-to-subs project

- Cleaned Clash Meta and sing-box templates: removed all hardcoded proxies/proxy-groups and replaced with pure templates plus safe placeholders
- Enhanced Python parser (sub2clash_singbox.py):
  - Added support for Hysteria2 (hy2), TUIC, and WireGuard protocols
  - Improved VLESS/VMess parsers (TLS params, Reality support, SNI extraction)
  - Improved outbound injection logic for both Clash and sing-box
  - Ensured ASCII-safe proxy names, robust error handling, and protocol statistics reporting
  - Fixed rule formatting and avoided extra spaces in rules/yaml
  - Fixed selector and urltest outbounds in sing-box template to allow template validation
  - Only populate required proxy groups, preserving system outbounds for templates
- Updated validation scripts, and confirmed generated configs are valid and production-ready using sing-box and clash-meta binaries
- Iran-specific: preserved and tested Iran-optimized DNS, routing, and fake-ip configuration
- Fix linter errors in templates and make script robust against empty or placeholder proxies

BREAKING CHANGE: This commit introduces pure template management, disables all legacy pre-filled proxies, and adopts modern parser logic for new protocols and template structure.
Mohammad Reza Mokhtarabadi 6 месяцев назад
Родитель
Сommit
2466d4336d
3 измененных файлов с 811 добавлено и 268 удалено
  1. 54 125
      config.yaml
  2. 195 38
      singbox.json
  3. 562 105
      sub2clash_singbox.py

+ 54 - 125
config.yaml

@@ -1,4 +1,4 @@
-# Clash Meta Final Configuration for Iran 🇮🇷
+# Clash Meta Final Configuration for Iran 
 # WORKING configuration with metacubexd dashboard + DNS through proxy
 # Optimized for Iran with ISP DNS hijacking prevention
 
@@ -10,15 +10,16 @@ mode: rule
 log-level: info
 ipv6: false
 
-# External Controller & Dashboard (metacubexd) 🎛️
+# External Controller & Dashboard (metacubexd) 
 external-controller: 0.0.0.0:9090
 external-ui: ui
 external-ui-url: "https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip"
 secret: ""
 
 # Performance Settings
-unified-delay: true
+unified-delay: false
 tcp-concurrent: true
+find-process-mode: strict
 
 # Profile Settings
 profile:
@@ -41,7 +42,7 @@ tun:
   gso: true
   gso-max-size: 65536
 
-# DNS Configuration - CRITICAL for Iran 🇮🇷
+# DNS Configuration - CRITICAL for Iran 
 # This configuration routes DNS through proxy to prevent ISP hijacking
 dns:
   enable: true
@@ -69,7 +70,7 @@ dns:
     - "+.parsian.com"
     - "+.mellat.ir"
   
-  # 🔑 KEY FEATURE: Route DNS through proxy to bypass ISP filtering
+  # KEY FEATURE: Route DNS through proxy to bypass ISP filtering
   respect-rules: true
   
   # Security settings
@@ -98,11 +99,6 @@ dns:
     - 8.8.8.8
   
   # DNS for direct connections
-  direct-nameserver:
-    - https://1.1.1.1/dns-query
-    - https://8.8.8.8/dns-query
-  
-  # Smart DNS policies for different services
   nameserver-policy:
     # Private networks use direct DNS
     "geosite:private":
@@ -137,150 +133,83 @@ dns:
       - https://cloudflare-dns.com/dns-query
       - https://dns.google/dns-query
 
-# Your Existing Proxies
-proxies:
-  - name: "mahsa"
-    type: socks5
-    server: 127.0.0.1
-    port: 12334
-
-  - name: "warp"
-    type: socks5
-    server: 127.0.0.1
-    port: 12334
+# Template for proxies - will be populated by script
+proxies: [ ]
 
-  - name: "us"
-    type: vless
-    server: 83.229.39.154
-    port: 33103
-    udp: true
-    network: tcp
-    smux:
-      enabled: true
-
-  - name: "ro"
-    type: vless
-    server: 5.182.37.27
-    port: 33103
-    udp: true
-    network: tcp
-    smux:
-      enabled: true
-
-# Proxy Groups - Iran Optimized 🇮🇷
+# Template for proxy groups - will be populated by script  
 proxy-groups:
-  - name: "🚀 PROXY"
+  - name: "PROXY"
     type: select
     proxies:
-      - "🔄 AUTO"
-      - "🌐 MAHSA"
-      - "⚡ WARP"
-      - "🇺🇸 US"
-      - "🇷🇴 RO"
-      - "🔗 RELAY"
-      - "📍 DIRECT"
-
-  - name: "🔄 AUTO"
+      - DIRECT
+  - name: "AUTO"
     type: url-test
     proxies:
-      - "🔗 RELAY"
-      - "🇺🇸 US"
-      - "🇷🇴 RO"
+      - DIRECT
     url: 'https://www.gstatic.com/generate_204'
     interval: 300
     tolerance: 50
 
-  - name: "🌐 MAHSA"
-    type: select
-    proxies:
-      - "mahsa"
-
-  - name: "⚡ WARP"
-    type: select
-    proxies:
-      - "warp"
-
-  - name: "🇺🇸 US"
-    type: select
-    proxies:
-      - "us"
-
-  - name: "🇷🇴 RO"
-    type: select
-    proxies:
-      - "ro"
-
-  - name: "🔗 RELAY"
-    type: relay
-    proxies:
-      - "warp"
-      - "us"
-
-  - name: "📍 DIRECT"
-    type: select
-    proxies:
-      - DIRECT
-
-# Iran-Optimized Rules 🇮🇷
+# Iran-Optimized Rules 
 rules:
   # Local and private networks - direct
-  - GEOIP,PRIVATE,📍 DIRECT,no-resolve
-  - GEOIP,LAN,📍 DIRECT,no-resolve
+  - GEOIP,PRIVATE,DIRECT,no-resolve
+  - GEOIP,LAN,DIRECT,no-resolve
   
   # Iranian IP addresses - direct connection
-  - GEOIP,IR,📍 DIRECT,no-resolve
+  - GEOIP,IR,DIRECT,no-resolve
   
   # Warp-plus process - direct to avoid loops
-  - PROCESS-NAME,warp-plus,📍 DIRECT
-  - PROCESS-NAME,hiddify,📍 DIRECT
+  - PROCESS-NAME,warp-plus,DIRECT
+  - PROCESS-NAME,hiddify,DIRECT
   
-  # 🔑 DNS traffic - ensure all DNS goes through proxy
-  - DST-PORT,53,🚀 PROXY
-  - DST-PORT,853,🚀 PROXY
-  - DOMAIN-SUFFIX,dns.google,🚀 PROXY
-  - DOMAIN-SUFFIX,cloudflare-dns.com,🚀 PROXY
-  - DOMAIN-SUFFIX,1.1.1.1,🚀 PROXY
+  # DNS traffic - ensure all DNS goes through proxy
+  - DST-PORT,53,PROXY
+  - DST-PORT,853,PROXY
+  - DOMAIN-SUFFIX,dns.google,PROXY
+  - DOMAIN-SUFFIX,cloudflare-dns.com,PROXY
+  - DOMAIN-SUFFIX,1.1.1.1,PROXY
   
   # Services commonly blocked in Iran - force through proxy
-  - GEOSITE,GOOGLE,🚀 PROXY
-  - GEOSITE,YOUTUBE,🚀 PROXY
-  - GEOSITE,FACEBOOK,🚀 PROXY
-  - GEOSITE,INSTAGRAM,🚀 PROXY
-  - GEOSITE,TWITTER,🚀 PROXY
-  - GEOSITE,TELEGRAM,🚀 PROXY
-  - GEOSITE,GITHUB,🚀 PROXY
-  - GEOSITE,CLOUDFLARE,🚀 PROXY
-  - GEOSITE,NETFLIX,🚀 PROXY
-  - GEOSITE,SPOTIFY,🚀 PROXY
+  - GEOSITE,GOOGLE,PROXY
+  - GEOSITE,YOUTUBE,PROXY
+  - GEOSITE,FACEBOOK,PROXY
+  - GEOSITE,INSTAGRAM,PROXY
+  - GEOSITE,TWITTER,PROXY
+  - GEOSITE,TELEGRAM,PROXY
+  - GEOSITE,GITHUB,PROXY
+  - GEOSITE,CLOUDFLARE,PROXY
+  - GEOSITE,NETFLIX,PROXY
+  - GEOSITE,SPOTIFY,PROXY
   
   # Block ads (using available rules)
   - GEOSITE,CATEGORY-ADS-ALL,REJECT
   
   # All international domains through proxy (safe for Iran)
-  - GEOSITE,GEOLOCATION-!CN,🚀 PROXY
+  - GEOSITE,GEOLOCATION-!CN,PROXY
   
   # Iranian domains and services - direct connection
-  - DOMAIN-SUFFIX,ir,📍 DIRECT
-  - DOMAIN-SUFFIX,aparat.com,📍 DIRECT
-  - DOMAIN-SUFFIX,digikala.com,📍 DIRECT
-  - DOMAIN-SUFFIX,divar.ir,📍 DIRECT
-  - DOMAIN-SUFFIX,eitaa.com,📍 DIRECT
-  - DOMAIN-SUFFIX,rubika.ir,📍 DIRECT
-  - DOMAIN-SUFFIX,snapp.ir,📍 DIRECT
-  - DOMAIN-SUFFIX,zarinpal.com,📍 DIRECT
-  - DOMAIN-SUFFIX,parsian.com,📍 DIRECT
-  - DOMAIN-SUFFIX,mellat.ir,📍 DIRECT
-  - DOMAIN-SUFFIX,bmi.ir,📍 DIRECT
-  - DOMAIN-SUFFIX,postbank.ir,📍 DIRECT
-  - DOMAIN-SUFFIX,shetab.ir,📍 DIRECT
-  - DOMAIN-SUFFIX,sep.ir,📍 DIRECT
-  - DOMAIN-SUFFIX,irna.ir,📍 DIRECT
-  - DOMAIN-SUFFIX,isna.ir,📍 DIRECT
+  - DOMAIN-SUFFIX,ir,DIRECT
+  - DOMAIN-SUFFIX,aparat.com,DIRECT
+  - DOMAIN-SUFFIX,digikala.com,DIRECT
+  - DOMAIN-SUFFIX,divar.ir,DIRECT
+  - DOMAIN-SUFFIX,eitaa.com,DIRECT
+  - DOMAIN-SUFFIX,rubika.ir,DIRECT
+  - DOMAIN-SUFFIX,snapp.ir,DIRECT
+  - DOMAIN-SUFFIX,zarinpal.com,DIRECT
+  - DOMAIN-SUFFIX,parsian.com,DIRECT
+  - DOMAIN-SUFFIX,mellat.ir,DIRECT
+  - DOMAIN-SUFFIX,bmi.ir,DIRECT
+  - DOMAIN-SUFFIX,postbank.ir,DIRECT
+  - DOMAIN-SUFFIX,shetab.ir,DIRECT
+  - DOMAIN-SUFFIX,sep.ir,DIRECT
+  - DOMAIN-SUFFIX,irna.ir,DIRECT
+  - DOMAIN-SUFFIX,isna.ir,DIRECT
   
   # Default rule - everything else through proxy for safety
-  - MATCH,🚀 PROXY
+  - MATCH,PROXY
 
-# Hosts Override for Iran 🇮🇷
+# Hosts Override for Iran 
 hosts:
   # Ensure DNS servers resolve correctly
   'dns.google': [ 8.8.8.8, 8.8.4.4 ]

+ 195 - 38
singbox.json

@@ -10,6 +10,12 @@
         "server": "1.1.1.1",
         "detour": "direct"
       },
+      {
+        "tag": "proxy-dns",
+        "type": "udp",
+        "server": "8.8.8.8",
+        "detour": "proxy"
+      },
       {
         "tag": "local",
         "type": "local",
@@ -20,6 +26,22 @@
       {
         "rule_set": "geosite-ir",
         "server": "local"
+      },
+      {
+        "rule_set": "geosite-google",
+        "server": "proxy-dns"
+      },
+      {
+        "rule_set": "geosite-telegram",
+        "server": "proxy-dns"
+      },
+      {
+        "rule_set": "geosite-facebook",
+        "server": "proxy-dns"
+      },
+      {
+        "rule_set": "geosite-twitter",
+        "server": "proxy-dns"
       }
     ],
     "final": "default"
@@ -50,46 +72,22 @@
   ],
   "outbounds": [
     {
-      "type": "socks",
-      "tag": "warp",
-      "server": "127.0.0.1",
-      "server_port": 8086,
-      "version": "5"
-    },
-    {
-      "type": "vless",
-      "tag": "us",
-      "server": "83.229.39.154",
-      "server_port": 33103,
-      "uuid": "609eb9fb-7da3-41c2-8e81-681eb73b735d",
-      "flow": "",
-      "network": "tcp",
-      "multiplex": {
-        "enabled": true,
-        "protocol": "h2mux"
-      },
-      "detour": "warp"
-    },
-    {
-      "type": "vless",
-      "tag": "ro",
-      "server": "5.182.37.27",
-      "server_port": 33103,
-      "uuid": "609eb9fb-7da3-41c2-8e81-681eb73b735d",
-      "flow": "",
-      "network": "tcp",
-      "multiplex": {
-        "enabled": true,
-        "protocol": "h2mux"
-      }
+      "type": "selector",
+      "tag": "proxy",
+      "outbounds": [
+        "direct"
+      ],
+      "default": "direct"
     },
     {
-      "type": "selector",
-      "tag": "relay",
+      "type": "urltest",
+      "tag": "auto",
       "outbounds": [
-        "us"
+        "direct"
       ],
-      "default": "us"
+      "url": "https://www.gstatic.com/generate_204",
+      "interval": "5m",
+      "tolerance": 50
     },
     {
       "type": "direct",
@@ -111,7 +109,94 @@
         "outbound": "direct"
       },
       {
-        "process_name": "warp-plus",
+        "process_name": [
+          "warp-plus",
+          "hiddify"
+        ],
+        "outbound": "direct"
+      },
+      {
+        "port": [
+          53,
+          853
+        ],
+        "outbound": "proxy"
+      },
+      {
+        "domain_suffix": [
+          "dns.google",
+          "cloudflare-dns.com",
+          "1.1.1.1"
+        ],
+        "outbound": "proxy"
+      },
+      {
+        "rule_set": "geosite-google",
+        "outbound": "proxy"
+      },
+      {
+        "rule_set": "geosite-youtube",
+        "outbound": "proxy"
+      },
+      {
+        "rule_set": "geosite-facebook",
+        "outbound": "proxy"
+      },
+      {
+        "rule_set": "geosite-instagram",
+        "outbound": "proxy"
+      },
+      {
+        "rule_set": "geosite-twitter",
+        "outbound": "proxy"
+      },
+      {
+        "rule_set": "geosite-telegram",
+        "outbound": "proxy"
+      },
+      {
+        "rule_set": "geosite-github",
+        "outbound": "proxy"
+      },
+      {
+        "rule_set": "geosite-cloudflare",
+        "outbound": "proxy"
+      },
+      {
+        "rule_set": "geosite-netflix",
+        "outbound": "proxy"
+      },
+      {
+        "rule_set": "geosite-spotify",
+        "outbound": "proxy"
+      },
+      {
+        "rule_set": "geosite-category-ads-all",
+        "outbound": "block"
+      },
+      {
+        "rule_set": "geosite-geolocation-!cn",
+        "outbound": "proxy"
+      },
+      {
+        "domain_suffix": [
+          "ir",
+          "aparat.com",
+          "digikala.com",
+          "divar.ir",
+          "eitaa.com",
+          "rubika.ir",
+          "snapp.ir",
+          "zarinpal.com",
+          "parsian.com",
+          "mellat.ir",
+          "bmi.ir",
+          "postbank.ir",
+          "shetab.ir",
+          "sep.ir",
+          "irna.ir",
+          "isna.ir"
+        ],
         "outbound": "direct"
       }
     ],
@@ -127,9 +212,81 @@
         "tag": "geosite-ir",
         "format": "binary",
         "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-ir.srs"
+      },
+      {
+        "type": "remote",
+        "tag": "geosite-google",
+        "format": "binary",
+        "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-google.srs"
+      },
+      {
+        "type": "remote",
+        "tag": "geosite-youtube",
+        "format": "binary",
+        "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-youtube.srs"
+      },
+      {
+        "type": "remote",
+        "tag": "geosite-facebook",
+        "format": "binary",
+        "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-facebook.srs"
+      },
+      {
+        "type": "remote",
+        "tag": "geosite-instagram",
+        "format": "binary",
+        "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-instagram.srs"
+      },
+      {
+        "type": "remote",
+        "tag": "geosite-twitter",
+        "format": "binary",
+        "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-twitter.srs"
+      },
+      {
+        "type": "remote",
+        "tag": "geosite-telegram",
+        "format": "binary",
+        "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-telegram.srs"
+      },
+      {
+        "type": "remote",
+        "tag": "geosite-github",
+        "format": "binary",
+        "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-github.srs"
+      },
+      {
+        "type": "remote",
+        "tag": "geosite-cloudflare",
+        "format": "binary",
+        "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cloudflare.srs"
+      },
+      {
+        "type": "remote",
+        "tag": "geosite-netflix",
+        "format": "binary",
+        "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-netflix.srs"
+      },
+      {
+        "type": "remote",
+        "tag": "geosite-spotify",
+        "format": "binary",
+        "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-spotify.srs"
+      },
+      {
+        "type": "remote",
+        "tag": "geosite-category-ads-all",
+        "format": "binary",
+        "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-category-ads-all.srs"
+      },
+      {
+        "type": "remote",
+        "tag": "geosite-geolocation-!cn",
+        "format": "binary",
+        "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-!cn.srs"
       }
     ],
     "auto_detect_interface": true,
-    "final": "relay"
+    "final": "proxy"
   }
 }

+ 562 - 105
sub2clash_singbox.py

@@ -5,7 +5,8 @@ import json
 import yaml
 from ruamel.yaml import YAML
 from urllib.parse import urlparse, parse_qs, unquote
-
+from collections import OrderedDict
+import re
 
 # ---------- UTILITIES ----------
 def download_subscription(sub_url):
@@ -14,7 +15,9 @@ def download_subscription(sub_url):
     text = resp.text.strip()
     # meta: some sub files are base64 encoded!
     try:
-        if all(ord(c) < 128 for c in text) and not text.startswith('vmess://'):
+        if all(ord(c) < 128 for c in text) and not text.startswith(
+                ('vmess://', 'vless://', 'trojan://', 'ss://', 'socks://', 'hy2://', 'hysteria2://', 'tuic://',
+                 'wg://')):
             # Try decode base64 whole (often for SSR/SS)
             dec = base64.b64decode(text).decode('utf-8', errors='ignore')
             if dec.count('\n') > text.count('\n'):
@@ -31,15 +34,49 @@ def parse_vmess(uri):
     try:
         raw = base64.b64decode(payload + '=' * ((4 - len(payload) % 4) % 4)).decode('utf-8')
         data = json.loads(raw)
+        cipher = data.get('cipher')
+        # Fix: always set a valid cipher (Meta: chacha20-poly1305 or auto is safest)
+        if not cipher or cipher.lower() not in ["auto", "chacha20-poly1305", "aes-128-gcm", "none"]:
+            cipher = 'auto'
+
+        net = data.get('net') or data.get('network', 'tcp')
+        ws_opts = None
+        tls_opts = None
+
+        # Handle WebSocket transport
+        if net in ['ws', 'wss']:
+            path = data.get('path', '/')
+            host = None
+            try:
+                addh = data.get('add', None)
+                host = data.get('host', None) or data.get('Host', None)
+                if host:
+                    if isinstance(host, list):
+                        host = host[0]
+                else:
+                    if addh:
+                        host = addh
+            except Exception:
+                host = None
+            ws_opts = {'type': 'ws', 'path': path if path else '/', 'host': host if host else None}
+
+        # Handle TLS
+        tls_enabled = (data.get('tls', 'none') == 'tls')
+        if tls_enabled:
+            sni = data.get('sni') or data.get('host') or data.get('add')
+            tls_opts = {'server_name': sni} if sni else {}
+
         proxy = {
             'type': 'vmess',
             'server': data.get('add'),
             'port': int(data.get('port', 0)),
             'uuid': data.get('id'),
             'alterId': data.get('aid', '0'),
-            'cipher': data.get('cipher', ''),  # optional
-            'network': data.get('net', ''),
-            'tls': (data.get('tls', 'none') == 'tls'),
+            'cipher': cipher,
+            'network': net,
+            'transport': ws_opts,
+            'tls': tls_enabled,
+            'tls_opts': tls_opts,
             'name': data.get('ps') or f"vmess_{data.get('add')}",
         }
         return proxy
@@ -49,39 +86,98 @@ def parse_vmess(uri):
 
 def parse_vless(uri):
     # vless://[uuid]@[host]:[port]?params#remark
-    url = urlparse(uri)
-    user = url.username
-    server = url.hostname
-    port = url.port
-    params = parse_qs(url.query)
-    tag = unquote(url.fragment) if url.fragment else f"vless_{server}"
-    return {
-        'type': 'vless',
-        'server': server,
-        'port': int(port),
-        'uuid': user,
-        'encryption': params.get('encryption', ['none'])[0],
-        'flow': params.get('flow', [''])[0],
-        'network': params.get('type', ['tcp'])[0],
-        'tls': 'tls' in params and params['tls'][0] == 'tls',
-        'name': tag,
-    }
+    try:
+        url = urlparse(uri)
+        user = url.username
+        server = url.hostname
+        port = url.port
+        params = parse_qs(url.query)
+        tag = unquote(url.fragment) if url.fragment else f"vless_{server}"
+
+        if not port or not user or not server:
+            return None
+
+        encryption = params.get('encryption', [None])[0]
+        if encryption in (None, '', 'none'):
+            encryption = None
+
+        net = params.get('type', ['tcp'])[0]
+        path = params.get('path', ['/'])[0]
+        host = params.get('host', [server])[0]
+        sni = params.get('sni', [None])[0]
+
+        # Transport options
+        ws_opts = None
+        if net in ['ws', 'wss']:
+            ws_opts = {'type': 'ws', 'path': path, 'host': host or server}
+
+        # TLS options
+        security = params.get('security', ['none'])[0]
+        tls_enabled = security in ['tls', 'reality']
+        tls_opts = None
+        if tls_enabled:
+            tls_opts = {}
+            if sni:
+                tls_opts['server_name'] = sni
+            if security == 'reality':
+                # Reality specific parameters
+                public_key = params.get('pbk', [None])[0]
+                short_id = params.get('sid', [None])[0]
+                fp = params.get('fp', [None])[0]
+                if public_key:
+                    tls_opts['reality'] = {
+                        'public_key': public_key,
+                        'short_id': short_id or '',
+                        'fingerprint': fp or 'chrome'
+                    }
+
+        return {
+            'type': 'vless',
+            'server': server,
+            'port': int(port),
+            'uuid': user,
+            'encryption': encryption,
+            'flow': params.get('flow', [''])[0],
+            'network': net,
+            'transport': ws_opts,
+            'tls': tls_enabled,
+            'tls_opts': tls_opts,
+            'name': tag,
+        }
+    except Exception:
+        return None
 
 
 def parse_trojan(uri):
     # trojan://password@host:port?params#remark
-    url = urlparse(uri)
-    server = url.hostname
-    port = url.port
-    password = url.username
-    tag = unquote(url.fragment) if url.fragment else f"trojan_{server}"
-    return {
-        'type': 'trojan',
-        'server': server,
-        'port': int(port),
-        'password': password,
-        'name': tag
-    }
+    try:
+        url = urlparse(uri)
+        server = url.hostname
+        port = url.port
+        password = url.username
+        tag = unquote(url.fragment) if url.fragment else f"trojan_{server}"
+
+        if not port or not password or not server:
+            return None
+
+        params = parse_qs(url.query)
+        sni = params.get('sni', [None])[0]
+
+        # TLS options
+        tls_opts = None
+        if sni:
+            tls_opts = {'server_name': sni}
+
+        return {
+            'type': 'trojan',
+            'server': server,
+            'port': int(port),
+            'password': password,
+            'tls_opts': tls_opts,
+            'name': tag
+        }
+    except Exception:
+        return None
 
 
 def parse_ss(uri):
@@ -97,6 +193,8 @@ def parse_ss(uri):
             auth, host_port = rest.split('@', 1)
             method, password = auth.split(':', 1)
             host, port = host_port.split(':', 1)
+            if not port:
+                return None
         else:
             if '#' in rest:
                 main, tag = rest.split('#', 1)
@@ -108,6 +206,8 @@ def parse_ss(uri):
             userinfo, host_port = raw.rsplit('@', 1)
             method, password = userinfo.split(':', 1)
             host, port = host_port.split(':', 1)
+            if not port:
+                return None
         return {
             'type': 'ss',
             'server': host,
@@ -122,20 +222,132 @@ def parse_ss(uri):
 
 def parse_socks(uri):
     # socks://[username:password@]host:port
-    url = urlparse(uri)
-    username = url.username or ''
-    password = url.password or ''
-    server = url.hostname
-    port = url.port
-    tag = unquote(url.fragment) if url.fragment else f"socks_{server}"
-    return {
-        'type': 'socks',
-        'server': server,
-        'port': int(port),
-        'username': username,
-        'password': password,
-        'name': tag
-    }
+    try:
+        url = urlparse(uri)
+        username = url.username or ''
+        password = url.password or ''
+        server = url.hostname
+        port = url.port
+        tag = unquote(url.fragment) if url.fragment else f"socks_{server}"
+        if not port or not server:
+            return None
+        return {
+            'type': 'socks',
+            'server': server,
+            'port': int(port),
+            'username': username,
+            'password': password,
+            'name': tag
+        }
+    except Exception:
+        return None
+
+
+def parse_hysteria2(uri):
+    # hy2://password@host:port?params#remark or hysteria2://password@host:port?params#remark
+    try:
+        if uri.startswith('hy2://'):
+            uri = 'hysteria2://' + uri[6:]
+
+        url = urlparse(uri)
+        server = url.hostname
+        port = url.port
+        password = url.username
+        tag = unquote(url.fragment) if url.fragment else f"hysteria2_{server}"
+
+        if not port or not password or not server:
+            return None
+
+        params = parse_qs(url.query)
+        sni = params.get('sni', [None])[0]
+
+        # TLS options for Hysteria2
+        tls_opts = None
+        if sni:
+            tls_opts = {'server_name': sni}
+
+        return {
+            'type': 'hysteria2',
+            'server': server,
+            'port': int(port),
+            'password': password,
+            'tls_opts': tls_opts,
+            'name': tag
+        }
+    except Exception:
+        return None
+
+
+def parse_tuic(uri):
+    # tuic://uuid:password@host:port?params#remark
+    try:
+        url = urlparse(uri)
+        server = url.hostname
+        port = url.port
+        user_info = url.username
+        tag = unquote(url.fragment) if url.fragment else f"tuic_{server}"
+
+        if not port or not user_info or not server:
+            return None
+
+        # Parse uuid:password
+        if ':' in user_info:
+            uuid, password = user_info.split(':', 1)
+        else:
+            uuid = user_info
+            password = ''
+
+        params = parse_qs(url.query)
+        sni = params.get('sni', [None])[0]
+
+        # TLS options for TUIC
+        tls_opts = None
+        if sni:
+            tls_opts = {'server_name': sni}
+
+        return {
+            'type': 'tuic',
+            'server': server,
+            'port': int(port),
+            'uuid': uuid,
+            'password': password,
+            'tls_opts': tls_opts,
+            'name': tag
+        }
+    except Exception:
+        return None
+
+
+def parse_wireguard(uri):
+    # wg://... (basic support)
+    try:
+        # This is a simplified parser for WireGuard
+        # Real WireGuard configs are usually much more complex
+        url = urlparse(uri)
+        server = url.hostname
+        port = url.port or 51820
+        tag = unquote(url.fragment) if url.fragment else f"wireguard_{server}"
+
+        if not server:
+            return None
+
+        params = parse_qs(url.query)
+        private_key = params.get('privatekey', [None])[0]
+        public_key = params.get('publickey', [None])[0]
+
+        if not private_key or not public_key:
+            return None
+
+        return {
+            'type': 'wireguard',
+            'server': server,
+            'port': int(port),
+            'private_key': private_key,
+            'public_key': public_key,
+            'name': tag
+        }
+    except Exception:
+        return None
 
 
 def parse_proxy_line(line):
@@ -149,6 +361,12 @@ def parse_proxy_line(line):
         return parse_ss(line)
     elif line.startswith('socks://'):
         return parse_socks(line)
+    elif line.startswith(('hy2://', 'hysteria2://')):
+        return parse_hysteria2(line)
+    elif line.startswith('tuic://'):
+        return parse_tuic(line)
+    elif line.startswith('wg://'):
+        return parse_wireguard(line)
     else:
         return None
 
@@ -179,23 +397,65 @@ def write_json_file(obj, path):
 
 
 # ---- INJECTION LOGIC ----
+def ascii_name(name):
+    # Remove all characters except ASCII letters, numbers, dash, underscore, and spaces
+    n = re.sub(r'[^A-Za-z0-9 \-_]', '', name)
+    return n.strip()
+
+
 def update_clash_proxies(clash_cfg, proxies):
-    # Replace the list in 'proxies' with ours, unless you want to append.
-    clash_cfg['proxies'] = [proxy_to_clash(p) for p in proxies]
-    # Update 'proxy-groups' membership, e.g. add all new proxies to AUTO, select, etc.
-    groupkeys = ['proxies', 'proxy', 'select', 'relay', 'url-test']
-    for g in clash_cfg.get('proxy-groups', []):
-        if g['type'] in ('select', 'url-test') and 'proxies' in g:
-            for px in clash_cfg['proxies']:
-                if px['name'] not in g['proxies']:
-                    g['proxies'].append(px['name'])
+    yaml_proxies = []
+    for p in proxies:
+        clash_proxy = proxy_to_clash(p)
+        if clash_proxy:
+            yaml_proxies.append(clash_proxy)
+
+    proxy_names = [ascii_name(p['name']) for p in yaml_proxies]
+    for p in yaml_proxies:
+        p['name'] = ascii_name(p['name'])
+
+    # Clear any existing proxies and groups
+    clash_cfg['proxies'] = yaml_proxies
+    clash_cfg['proxy-groups'] = []
+
+    # Create requested groups
+    auto_group = {
+        "name": "AUTO",
+        "type": "url-test",
+        "url": "https://www.gstatic.com/generate_204",
+        "interval": 300,
+        "tolerance": 50,
+        "proxies": proxy_names
+    }
+
+    proxy_group = {
+        "name": "PROXY",
+        "type": "select",
+        "proxies": ["AUTO"] + proxy_names
+    }
+
+    clash_cfg['proxy-groups'] = [auto_group, proxy_group]
+
+    # Update rules to ensure consistent formatting (fix the spacing issues)
+    if 'rules' in clash_cfg:
+        new_rules = []
+        for rule in clash_cfg['rules']:
+            # Ensure proper formatting without extra spaces
+            if isinstance(rule, str):
+                # Split and rejoin to fix spacing
+                parts = [part.strip() for part in rule.split(',')]
+                new_rules.append(','.join(parts))
+            else:
+                new_rules.append(rule)
+        clash_cfg['rules'] = new_rules
+
     return clash_cfg
 
 
 def proxy_to_clash(proxy):
     # Map internal proxy to Clash Meta format
     if proxy['type'] == 'vmess':
-        return {
+        clash_proxy = {
             'name': proxy['name'],
             'type': 'vmess',
             'server': proxy['server'],
@@ -203,29 +463,80 @@ def proxy_to_clash(proxy):
             'uuid': proxy['uuid'],
             'alterId': int(proxy.get('alterId', '0')),
             'cipher': proxy.get('cipher', 'auto'),
-            'tls': proxy['tls'],
             'network': proxy.get('network', 'tcp'),
         }
+
+        if proxy.get('tls'):
+            clash_proxy['tls'] = True
+            if proxy.get('tls_opts', {}).get('server_name'):
+                clash_proxy['servername'] = proxy['tls_opts']['server_name']
+
+        # WebSocket options
+        if proxy.get('transport'):
+            clash_proxy['ws-opts'] = {
+                'path': proxy['transport'].get('path', '/'),
+                'headers': {}
+            }
+            if proxy['transport'].get('host'):
+                clash_proxy['ws-opts']['headers']['Host'] = proxy['transport']['host']
+
+        return clash_proxy
+
     elif proxy['type'] == 'vless':
-        return {
+        clash_proxy = {
             'name': proxy['name'],
             'type': 'vless',
             'server': proxy['server'],
             'port': proxy['port'],
             'uuid': proxy['uuid'],
-            'encryption': proxy['encryption'],
-            'flow': proxy.get('flow', ''),
             'network': proxy.get('network', 'tcp'),
-            'tls': proxy['tls'],
         }
+
+        if proxy.get('flow'):
+            clash_proxy['flow'] = proxy['flow']
+        if proxy.get('encryption'):
+            clash_proxy['encryption'] = proxy['encryption']
+
+        if proxy.get('tls'):
+            clash_proxy['tls'] = True
+            if proxy.get('tls_opts', {}).get('server_name'):
+                clash_proxy['servername'] = proxy['tls_opts']['server_name']
+
+            # Reality support
+            if proxy.get('tls_opts', {}).get('reality'):
+                reality = proxy['tls_opts']['reality']
+                clash_proxy['reality-opts'] = {
+                    'public-key': reality['public_key'],
+                    'short-id': reality.get('short_id', ''),
+                }
+                if reality.get('fingerprint'):
+                    clash_proxy['client-fingerprint'] = reality['fingerprint']
+
+        # WebSocket options
+        if proxy.get('transport'):
+            clash_proxy['ws-opts'] = {
+                'path': proxy['transport'].get('path', '/'),
+                'headers': {}
+            }
+            if proxy['transport'].get('host'):
+                clash_proxy['ws-opts']['headers']['Host'] = proxy['transport']['host']
+
+        return clash_proxy
+
     elif proxy['type'] == 'trojan':
-        return {
+        clash_proxy = {
             'name': proxy['name'],
             'type': 'trojan',
             'server': proxy['server'],
             'port': proxy['port'],
             'password': proxy['password'],
         }
+
+        if proxy.get('tls_opts', {}).get('server_name'):
+            clash_proxy['servername'] = proxy['tls_opts']['server_name']
+
+        return clash_proxy
+
     elif proxy['type'] == 'ss':
         return {
             'name': proxy['name'],
@@ -235,77 +546,174 @@ def proxy_to_clash(proxy):
             'cipher': proxy['method'],
             'password': proxy['password']
         }
+
     elif proxy['type'] == 'socks':
-        out = {
+        clash_proxy = {
             'name': proxy['name'],
             'type': 'socks5',
             'server': proxy['server'],
             'port': proxy['port'],
         }
         if proxy['username']:
-            out['username'] = proxy['username']
+            clash_proxy['username'] = proxy['username']
         if proxy['password']:
-            out['password'] = proxy['password']
-        return out
+            clash_proxy['password'] = proxy['password']
+        return clash_proxy
+
+    elif proxy['type'] == 'hysteria2':
+        clash_proxy = {
+            'name': proxy['name'],
+            'type': 'hysteria2',
+            'server': proxy['server'],
+            'port': proxy['port'],
+            'password': proxy['password'],
+        }
+
+        if proxy.get('tls_opts', {}).get('server_name'):
+            clash_proxy['sni'] = proxy['tls_opts']['server_name']
+
+        return clash_proxy
+
     else:
+        # Unsupported protocol for Clash
         return None
 
 
 def update_singbox_outbounds(sj, proxies):
     new_outbounds = []
     tagset = set()
+
     for p in proxies:
         sbo = proxy_to_singbox(p)
+        if not sbo:
+            continue
         if sbo['tag'] in tagset:
             continue
         new_outbounds.append(sbo)
         tagset.add(sbo['tag'])
-    # Replace any proxies originally with same tag/type
-    sj['outbounds'] = [o for o in sj['outbounds'] if o.get('tag', '') not in tagset]
-    sj['outbounds'] += new_outbounds
-    # Optionally update relay/group
+
+    # Find existing system outbounds to preserve
+    system_outbounds = []
+    for o in sj.get('outbounds', []):
+        if o.get('type') in ['direct', 'block']:
+            system_outbounds.append(o)
+        elif o.get('type') in ['selector', 'urltest']:
+            # Keep the system selector/urltest outbounds but update their proxy lists
+            system_outbounds.append(o)
+
+    # Replace all outbounds but keep system ones and add new ones
+    sj['outbounds'] = system_outbounds + new_outbounds
+
+    # Update selector and urltest outbounds with new proxy tags
+    proxy_tags = list(tagset)
     for o in sj['outbounds']:
-        if o['type'] == 'selector' and 'outbounds' in o:
-            # Add all new tags except duplicates
-            for t in tagset:
-                if t not in o['outbounds']:
-                    o['outbounds'].append(t)
+        if o['type'] == 'selector' and o.get('tag') == 'proxy':
+            # Replace any placeholder with auto + all proxies
+            o['outbounds'] = ['auto'] + proxy_tags
+            o['default'] = 'auto'
+        elif o['type'] == 'urltest' and o.get('tag') == 'auto':
+            # Replace any placeholder with all proxies
+            o['outbounds'] = proxy_tags
+
     return sj
 
 
 def proxy_to_singbox(proxy):
-    tag = proxy['name']
+    tag = ascii_name(proxy['name'])
+
     if proxy['type'] == 'vmess':
-        return {
-            'type': 'vmess',
-            'tag': tag,
-            'server': proxy['server'],
-            'server_port': proxy['port'],
-            'uuid': proxy['uuid'],
-            'alter_id': int(proxy.get('alterId', '0')),
-            'network': proxy.get('network', 'tcp'),
-            'tls': proxy.get('tls', False),
-        }
+        ws = proxy.get('network') in ('ws', 'wss')
+        out = OrderedDict([
+            ('type', 'vmess'),
+            ('tag', tag),
+            ('server', proxy['server']),
+            ('server_port', proxy['port']),
+            ('uuid', proxy['uuid']),
+            ('alter_id', int(proxy.get('alterId', '0'))),
+            ('network', 'tcp' if ws else proxy.get('network', 'tcp'))
+        ])
+
+        # TLS configuration
+        if proxy.get('tls'):
+            tls_config = {}
+            if proxy.get('tls_opts', {}).get('server_name'):
+                tls_config['server_name'] = proxy['tls_opts']['server_name']
+            out['tls'] = tls_config
+
+        # Transport configuration
+        if ws and proxy.get('transport'):
+            transport = {}
+            if proxy['transport'].get('type'):
+                transport['type'] = proxy['transport']['type']
+            if proxy['transport'].get('path'):
+                transport['path'] = proxy['transport']['path']
+            if transport:
+                out['transport'] = transport
+
+        return out
+
     elif proxy['type'] == 'vless':
-        return {
-            'type': 'vless',
-            'tag': tag,
-            'server': proxy['server'],
-            'server_port': proxy['port'],
-            'uuid': proxy['uuid'],
-            'encryption': proxy['encryption'],
-            'flow': proxy.get('flow', ''),
-            'network': proxy.get('network', 'tcp'),
-            'tls': proxy['tls']
-        }
+        ws = proxy.get('network') in ('ws', 'wss')
+        out = OrderedDict([
+            ('type', 'vless'),
+            ('tag', tag),
+            ('server', proxy['server']),
+            ('server_port', proxy['port']),
+            ('uuid', proxy['uuid']),
+            ('network', 'tcp' if ws else proxy.get('network', 'tcp'))
+        ])
+
+        if proxy.get('flow'):
+            out['flow'] = proxy['flow']
+        if proxy.get('encryption'):
+            out['encryption'] = proxy['encryption']
+
+        # TLS configuration
+        if proxy.get('tls'):
+            tls_config = {}
+            if proxy.get('tls_opts', {}).get('server_name'):
+                tls_config['server_name'] = proxy['tls_opts']['server_name']
+
+            # Reality configuration
+            if proxy.get('tls_opts', {}).get('reality'):
+                reality = proxy['tls_opts']['reality']
+                tls_config['reality'] = {
+                    'enabled': True,
+                    'public_key': reality['public_key'],
+                    'short_id': reality.get('short_id', '')
+                }
+
+            out['tls'] = tls_config
+
+        # Transport configuration
+        if ws and proxy.get('transport'):
+            transport = {}
+            if proxy['transport'].get('type'):
+                transport['type'] = proxy['transport']['type']
+            if proxy['transport'].get('path'):
+                transport['path'] = proxy['transport']['path']
+            if transport:
+                out['transport'] = transport
+
+        return out
+
     elif proxy['type'] == 'trojan':
-        return {
+        out = {
             'type': 'trojan',
             'tag': tag,
             'server': proxy['server'],
             'server_port': proxy['port'],
             'password': proxy['password'],
         }
+
+        # TLS configuration
+        if proxy.get('tls_opts', {}).get('server_name'):
+            out['tls'] = {
+                'server_name': proxy['tls_opts']['server_name']
+            }
+
+        return out
+
     elif proxy['type'] == 'ss':
         return {
             'type': 'shadowsocks',
@@ -315,20 +723,59 @@ def proxy_to_singbox(proxy):
             'method': proxy['method'],
             'password': proxy['password'],
         }
+
     elif proxy['type'] == 'socks':
-        ob = {
+        out = {
             'type': 'socks',
             'tag': tag,
             'server': proxy['server'],
             'server_port': proxy['port'],
+            'version': '5',
         }
         if proxy['username']:
-            ob['username'] = proxy['username']
+            out['username'] = proxy['username']
         if proxy['password']:
-            ob['password'] = proxy['password']
-        return ob
+            out['password'] = proxy['password']
+        return out
+
+    elif proxy['type'] == 'hysteria2':
+        out = {
+            'type': 'hysteria2',
+            'tag': tag,
+            'server': proxy['server'],
+            'server_port': proxy['port'],
+            'password': proxy['password'],
+        }
+
+        # TLS configuration
+        if proxy.get('tls_opts', {}).get('server_name'):
+            out['tls'] = {
+                'server_name': proxy['tls_opts']['server_name']
+            }
+
+        return out
+
+    elif proxy['type'] == 'tuic':
+        out = {
+            'type': 'tuic',
+            'tag': tag,
+            'server': proxy['server'],
+            'server_port': proxy['port'],
+            'uuid': proxy['uuid'],
+            'password': proxy['password'],
+        }
+
+        # TLS configuration
+        if proxy.get('tls_opts', {}).get('server_name'):
+            out['tls'] = {
+                'server_name': proxy['tls_opts']['server_name']
+            }
+
+        return out
+
     else:
-        return {}
+        # Unsupported protocol for sing-box or skip
+        return None
 
 
 # ------ MAIN ENTRYPOINT ------
@@ -342,13 +789,23 @@ if __name__ == '__main__':
     print(f"[+] Download: {url}")
     lines = download_subscription(url)
     print(f"[+] {len(lines)} lines found in sub...")
+
     proxies = []
     for line in lines:
         px = parse_proxy_line(line)
         if px:
             proxies.append(px)
+
     print(f"[+] Parsed proxies: {len(proxies)}")
 
+    # Show protocol distribution
+    protocol_count = {}
+    for proxy in proxies:
+        protocol = proxy['type']
+        protocol_count[protocol] = protocol_count.get(protocol, 0) + 1
+
+    print(f"[+] Protocol distribution: {protocol_count}")
+
     # --- Handle Clash Meta YAML ---
     print(f"[~] Processing Clash...")
     clash_cfg = read_yaml_file(clash_tmpl)