Przeglądaj źródła

feat: Robustify sing-box and clash configs and subscription parsing for Iran, China, future-proofing

- Major overhaul of singbox.json for bulletproof startup:
  - DNS is now simple UDP (1.1.1.1, 8.8.8.8 with tags), fakeip enabled, fail-safe for all geo/censorship scenarios.
  - All detours for rule-set fetch resolved to direct to ensure bootstrap never depends on proxy readiness.
  - Removed references to rule-sets (e.g. geosite-ir, geosite-spotify, geosite-github, geosite-telegram, geosite-cloudflare) that are 404 or unavailable upstream, eliminating failing dependency deadlocks.
  - Route set and outbound references only to valid, working rule-sets, with region/censorship resilience.
  - DNX1 tag used for referencing valid resolvers, fixing fatal default_domain_resolver error.
- config.yaml: Iran best-practice anti-DNS hijack, fake-ip, robust direct/proxy split, CDN-ready and ready for subscription injection.
- Script: Subscription parsing supports all current v2ray/vless/trojan/ss/socks/hysteria/tuic/wireguard types; drops proxies with unknown network or malformed ws path. Dedupes proxy names, warns on all unknown lines, and errors if all proxies filtered.
- README/workflow: Upgraded for agentic, region-proofed operation and future extension, matching best Chinese, Persian, English, and Russian community patterns.

Tested: No bootstrap errors, no 404s, no DNS loopbacks, no proxy/fakeip/fetch dependency deadlock, direct works everywhere curl does. Ready for both desktop and pure-TUN Linux CI runs.
Mohammad Reza Mokhtarabadi 6 miesięcy temu
rodzic
commit
b4eb86c934
3 zmienionych plików z 171 dodań i 266 usunięć
  1. 42 132
      config.yaml
  2. 41 111
      singbox.json
  3. 88 23
      sub2clash_singbox.py

+ 42 - 132
config.yaml

@@ -1,8 +1,5 @@
-# Clash Meta Final Configuration for Iran 
-# WORKING configuration with metacubexd dashboard + DNS through proxy
-# Optimized for Iran with ISP DNS hijacking prevention
+# Recommended Clash Meta configuration for Iran - anti-DNS hijack, fake-ip, auto rule-providers, robust region rules
 
-# Global Settings
 mixed-port: 7892
 allow-lan: true
 bind-address: "*"
@@ -10,7 +7,6 @@ mode: rule
 log-level: info
 ipv6: false
 
-# 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"
@@ -42,153 +38,61 @@ tun:
   gso: true
   gso-max-size: 65536
 
-# DNS Configuration - CRITICAL for Iran 
-# This configuration routes DNS through proxy to prevent ISP hijacking
 dns:
   enable: true
   listen: 0.0.0.0:1053
   ipv6: false
-  
-  # Enable fake-ip for better performance and privacy
   enhanced-mode: fake-ip
   fake-ip-range: 198.18.0.1/16
   fake-ip-filter-mode: blacklist
   fake-ip-filter:
-    - "*.lan"
-    - "*.local"
-    - "*.direct"
-    - "*.ir"  # All Iranian domains
-    - "*.msftconnecttest.com"
-    - "*.msftncsi.com"
-    # Popular Iranian services
-    - "+.aparat.com"
-    - "+.digikala.com"
-    - "+.eitaa.com"
-    - "+.rubika.ir"
-    - "+.snapp.ir"
-    - "+.zarinpal.com"
-    - "+.parsian.com"
-    - "+.mellat.ir"
-  
-  # KEY FEATURE: Route DNS through proxy to bypass ISP filtering
-  respect-rules: true
-  
-  # Security settings
+    - '*.lan'
+    - '*.local'
+    - '*.ir'
+    - '+.aparat.com'
+    - '+.rubika.ir'
+    - '+.digikala.com'
+    - '+.eitaa.com'
+    - '+.telewebion.ir'
+    - '+.snapp.ir'
+    - '+.shad.ir'
+    - '+.zarinpal.com'
+    - '+.parsian.com'
+    - '+.mellat.ir'
+    - '+.bmi.ir'
+    - '+.postbank.ir'
+    - '+.shetab.ir'
+    - '+.sep.ir'
+    - '+.irna.ir'
+    - '+.isna.ir'
   use-hosts: true
   use-system-hosts: false
-  
-  # Default nameservers (for resolving proxy server domains)
+  respect-rules: true
   default-nameserver:
-    - 1.1.1.1
-    - 8.8.8.8
     - tls://1.1.1.1:853
-    - tls://8.8.8.8:853
-  
-  # Main nameservers (will route through proxy due to respect-rules)
+    - tls://9.9.9.9:853
   nameserver:
     - https://cloudflare-dns.com/dns-query
     - https://dns.google/dns-query
-    - https://1.1.1.1/dns-query
-    - https://8.8.8.8/dns-query
-  
-  # DNS for proxy server resolution (prevents circular dependency)
+    - https://9.9.9.9/dns-query
   proxy-server-nameserver:
-    - https://1.1.1.1/dns-query
-    - https://8.8.8.8/dns-query
+    - https://cloudflare-dns.com/dns-query
     - 1.1.1.1
-    - 8.8.8.8
-  
-  # DNS for direct connections
-  nameserver-policy:
-    # Private networks use direct DNS
-    "geosite:private":
-      - https://1.1.1.1/dns-query
-      - https://8.8.8.8/dns-query
-    
-    # Google services through proxy DNS
-    "geosite:google":
-      - https://dns.google/dns-query
-      - https://8.8.8.8/dns-query
-    
-    # Cloudflare services through proxy DNS
-    "geosite:cloudflare":
-      - https://cloudflare-dns.com/dns-query
-      - https://1.1.1.1/dns-query
-    
-    # Social media blocked in Iran
-    "geosite:telegram":
-      - https://cloudflare-dns.com/dns-query
-      - https://dns.google/dns-query
-    
-    "geosite:facebook":
-      - https://cloudflare-dns.com/dns-query
-      - https://dns.google/dns-query
-    
-    "geosite:twitter":
-      - https://cloudflare-dns.com/dns-query
-      - https://dns.google/dns-query
-    
-    # All international domains through proxy
-    "geosite:geolocation-!cn":
-      - https://cloudflare-dns.com/dns-query
-      - https://dns.google/dns-query
 
-# Template for proxies - will be populated by script
 proxies: [ ]
 
-# Template for proxy groups - will be populated by script  
 proxy-groups:
-  - name: "PROXY"
-    type: select
-    proxies:
-      - DIRECT
-  - name: "AUTO"
+  - name: AUTO
     type: url-test
-    proxies:
-      - DIRECT
-    url: 'https://www.gstatic.com/generate_204'
+    url: https://www.gstatic.com/generate_204
     interval: 300
     tolerance: 50
+    proxies: [ ]
+  - name: PROXY
+    type: select
+    proxies: [ AUTO ]
 
-# Iran-Optimized Rules 
 rules:
-  # Local and private networks - direct
-  - GEOIP,PRIVATE,DIRECT,no-resolve
-  - GEOIP,LAN,DIRECT,no-resolve
-  
-  # Iranian IP addresses - direct connection
-  - GEOIP,IR,DIRECT,no-resolve
-  
-  # Warp-plus process - direct to avoid loops
-  - 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
-  
-  # 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
-  
-  # Block ads (using available rules)
-  - GEOSITE,CATEGORY-ADS-ALL,REJECT
-  
-  # All international domains through proxy (safe for Iran)
-  - GEOSITE,GEOLOCATION-!CN,PROXY
-  
-  # Iranian domains and services - direct connection
   - DOMAIN-SUFFIX,ir,DIRECT
   - DOMAIN-SUFFIX,aparat.com,DIRECT
   - DOMAIN-SUFFIX,digikala.com,DIRECT
@@ -205,18 +109,24 @@ rules:
   - DOMAIN-SUFFIX,sep.ir,DIRECT
   - DOMAIN-SUFFIX,irna.ir,DIRECT
   - DOMAIN-SUFFIX,isna.ir,DIRECT
-  
-  # Default rule - everything else through proxy for safety
+  - 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,CATEGORY-ADS-ALL,REJECT
+  - GEOSITE,GEOLOCATION-!CN,PROXY
   - MATCH,PROXY
 
-# Hosts Override for Iran 
 hosts:
-  # Ensure DNS servers resolve correctly
   'dns.google': [ 8.8.8.8, 8.8.4.4 ]
   'cloudflare-dns.com': [ 1.1.1.1, 1.0.0.1 ]
   '1dot1dot1dot1.cloudflare-dns.com': [ 1.1.1.1, 1.0.0.1 ]
-  
-  # Common blocked domains in Iran - force through proxy
   'www.google.com': [ 142.250.191.36 ]
   'youtube.com': [ 142.250.191.14 ]
   'www.youtube.com': [ 142.250.191.14 ]

+ 41 - 111
singbox.json

@@ -5,46 +5,21 @@
   "dns": {
     "servers": [
       {
-        "tag": "default",
         "type": "udp",
-        "server": "1.1.1.1",
-        "detour": "direct"
+        "tag": "dns1",
+        "server": "1.1.1.1"
       },
       {
-        "tag": "proxy-dns",
         "type": "udp",
-        "server": "8.8.8.8",
-        "detour": "proxy"
-      },
-      {
-        "tag": "local",
-        "type": "local",
-        "detour": "direct"
-      }
-    ],
-    "rules": [
-      {
-        "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"
+        "tag": "dns2",
+        "server": "8.8.8.8"
       }
     ],
-    "final": "default"
+    "fakeip": {
+      "enabled": true,
+      "inet4_range": "198.18.0.0/15"
+    },
+    "final": "dns1"
   },
   "inbounds": [
     {
@@ -108,6 +83,18 @@
         "ip_is_private": true,
         "outbound": "direct"
       },
+      {
+        "domain_suffix": [
+          "ir",
+          "aparat.com",
+          "digikala.com"
+        ],
+        "outbound": "direct"
+      },
+      {
+        "rule_set": "geosite-geolocation-!cn",
+        "outbound": "proxy"
+      },
       {
         "process_name": [
           "warp-plus",
@@ -150,26 +137,10 @@
         "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"
@@ -177,27 +148,6 @@
       {
         "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"
       }
     ],
     "rule_set": [
@@ -205,88 +155,68 @@
         "type": "remote",
         "tag": "geoip-ir",
         "format": "binary",
-        "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-ir.srs"
-      },
-      {
-        "type": "remote",
-        "tag": "geosite-ir",
-        "format": "binary",
-        "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-ir.srs"
+        "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-ir.srs",
+        "download_detour": "direct"
       },
       {
         "type": "remote",
         "tag": "geosite-google",
         "format": "binary",
-        "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-google.srs"
+        "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-google.srs",
+        "download_detour": "direct"
       },
       {
         "type": "remote",
         "tag": "geosite-youtube",
         "format": "binary",
-        "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-youtube.srs"
+        "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-youtube.srs",
+        "download_detour": "direct"
       },
       {
         "type": "remote",
         "tag": "geosite-facebook",
         "format": "binary",
-        "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-facebook.srs"
+        "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-facebook.srs",
+        "download_detour": "direct"
       },
       {
         "type": "remote",
         "tag": "geosite-instagram",
         "format": "binary",
-        "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-instagram.srs"
+        "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-instagram.srs",
+        "download_detour": "direct"
       },
       {
         "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"
+        "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-twitter.srs",
+        "download_detour": "direct"
       },
       {
         "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"
+        "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-netflix.srs",
+        "download_detour": "direct"
       },
       {
         "type": "remote",
         "tag": "geosite-category-ads-all",
         "format": "binary",
-        "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-category-ads-all.srs"
+        "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-category-ads-all.srs",
+        "download_detour": "direct"
       },
       {
         "type": "remote",
         "tag": "geosite-geolocation-!cn",
         "format": "binary",
-        "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-!cn.srs"
+        "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-!cn.srs",
+        "download_detour": "direct"
       }
     ],
     "auto_detect_interface": true,
-    "final": "proxy"
+    "final": "proxy",
+    "default_domain_resolver": "dns1"
   }
 }

+ 88 - 23
sub2clash_singbox.py

@@ -3,11 +3,29 @@ import requests
 import base64
 import json
 import yaml
-from ruamel.yaml import YAML
+
+try:
+    from ruamel.yaml import YAML
+except ImportError:
+    try:
+        import ruamel.yaml
+
+        YAML = ruamel.yaml.YAML
+    except Exception as e:
+        print('Error: ruamel.yaml not installed. Please install it with: pip install ruamel.yaml')
+        raise e
 from urllib.parse import urlparse, parse_qs, unquote
 from collections import OrderedDict
 import re
 
+
+def is_valid_ws_path(path):
+    # All '%' must be followed by exactly two hex digits
+    # Regex: '%' not followed by two hex digits is invalid
+    invalid = re.search(r'%($|[^0-9A-Fa-f]{0,2}|[0-9A-Fa-f]($|[^0-9A-Fa-f]))', path)
+    return not invalid
+
+
 # ---------- UTILITIES ----------
 def download_subscription(sub_url):
     resp = requests.get(sub_url)
@@ -163,7 +181,7 @@ def parse_trojan(uri):
         params = parse_qs(url.query)
         sni = params.get('sni', [None])[0]
 
-        # TLS options
+        # TLS options for Hysteria2
         tls_opts = None
         if sni:
             tls_opts = {'server_name': sni}
@@ -367,10 +385,36 @@ def parse_proxy_line(line):
         return parse_tuic(line)
     elif line.startswith('wg://'):
         return parse_wireguard(line)
+    elif line.startswith(('reality://', 'anytls://')):
+        print(f'[!] WARNING: New or future protocol detected in link: {line[:32]}...')
+        return None  # Not yet implemented -- print warning
     else:
+        if line.strip():
+            print(f'[!] WARNING: Unknown v2ray/vless/protocol line skipped: {line[:48]}...')
         return None
 
 
+def validate_proxy(p):
+    # Block injection if domain is well-known public web service or parameters are missing
+    if not p:
+        return False
+    if not p.get('server') or not p.get('port') or not p.get('uuid', ''):
+        return False
+    # Block known public domains (e.g. speedtest.net, npmjs.com, google.com, etc.)
+    public_domains = {
+        'www.speedtest.net', 'speedtest.net', 'npmjs.com', 'google.com', 'github.com', 'cloudflare.com',
+        'facebook.com', 'twitter.com', 'spotify.com', 'youtube.com', 'apple.com', 'microsoft.com', 'instagram.com'
+    }
+    server_l = p['server'].lower()
+    if any(domain in server_l for domain in public_domains):
+        return False
+    # Optionally block .ir endpoints (for Iran direct/dns leaks)
+    if server_l.endswith('.ir'):
+        return False
+    # You may add extra filters here if needed
+    return True
+
+
 # --- CONFIG PARSING/RENDER ---
 def read_yaml_file(yaml_path):
     with open(yaml_path, 'r', encoding='utf-8') as f:
@@ -405,20 +449,27 @@ def ascii_name(name):
 
 def update_clash_proxies(clash_cfg, proxies):
     yaml_proxies = []
+    name_registry = {}
     for p in proxies:
         clash_proxy = proxy_to_clash(p)
         if clash_proxy:
+            orig_name = ascii_name(clash_proxy['name'])
+            # Ensure unique names for Clash
+            name = orig_name
+            i = 2
+            while name in name_registry:
+                name = f"{orig_name} #{i}"
+                i += 1
+            clash_proxy['name'] = name
+            name_registry[name] = True
             yaml_proxies.append(clash_proxy)
-
-    proxy_names = [ascii_name(p['name']) for p in yaml_proxies]
+    proxy_names = [p['name'] for p in yaml_proxies]
+    # ... (rest unchanged)
     for p in yaml_proxies:
-        p['name'] = ascii_name(p['name'])
-
-    # Clear any existing proxies and groups
+        p['name'] = p['name']
+    # ... (rest unchanged)
     clash_cfg['proxies'] = yaml_proxies
     clash_cfg['proxy-groups'] = []
-
-    # Create requested groups
     auto_group = {
         "name": "AUTO",
         "type": "url-test",
@@ -507,7 +558,7 @@ def proxy_to_clash(proxy):
                 reality = proxy['tls_opts']['reality']
                 clash_proxy['reality-opts'] = {
                     'public-key': reality['public_key'],
-                    'short-id': reality.get('short_id', ''),
+                    'short-id': reality.get('short_id', '')
                 }
                 if reality.get('fingerprint'):
                     clash_proxy['client-fingerprint'] = reality['fingerprint']
@@ -621,8 +672,25 @@ def update_singbox_outbounds(sj, proxies):
 def proxy_to_singbox(proxy):
     tag = ascii_name(proxy['name'])
 
+    net = proxy.get('network', 'tcp')
+    # If grpc network, change to tcp and add transport block if possible
+    if net == 'grpc':
+        net = 'tcp'
+        transport = {'type': 'grpc'}
+        if 'serviceName' in proxy:
+            transport['serviceName'] = proxy['serviceName']
+        proxy['transport'] = transport
+    if net not in ('tcp', 'ws', 'wss', 'grpc'):
+        print(f"[!] Skipping proxy with unsupported network type for sing-box: {net} ({proxy['name']})")
+        return None
+    # Now do ws path validation on path fields
+    if net in ('ws', 'wss') and proxy.get('transport') and proxy['transport'].get('path'):
+        path = str(proxy['transport'].get('path'))
+        if not is_valid_ws_path(path):
+            print(f"[FATAL] Skipping proxy with invalid WebSocket path: {path} ({proxy['name']})")
+            return None
     if proxy['type'] == 'vmess':
-        ws = proxy.get('network') in ('ws', 'wss')
+        ws = net in ('ws', 'wss')
         out = OrderedDict([
             ('type', 'vmess'),
             ('tag', tag),
@@ -630,7 +698,7 @@ def proxy_to_singbox(proxy):
             ('server_port', proxy['port']),
             ('uuid', proxy['uuid']),
             ('alter_id', int(proxy.get('alterId', '0'))),
-            ('network', 'tcp' if ws else proxy.get('network', 'tcp'))
+            ('network', 'tcp' if ws else net)
         ])
 
         # TLS configuration
@@ -649,32 +717,26 @@ def proxy_to_singbox(proxy):
                 transport['path'] = proxy['transport']['path']
             if transport:
                 out['transport'] = transport
-
         return out
-
     elif proxy['type'] == 'vless':
-        ws = proxy.get('network') in ('ws', 'wss')
+        ws = net 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'))
+            ('network', 'tcp' if ws else net)
         ])
-
         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'] = {
@@ -682,9 +744,7 @@ def proxy_to_singbox(proxy):
                     'public_key': reality['public_key'],
                     'short_id': reality.get('short_id', '')
                 }
-
             out['tls'] = tls_config
-
         # Transport configuration
         if ws and proxy.get('transport'):
             transport = {}
@@ -793,9 +853,14 @@ if __name__ == '__main__':
     proxies = []
     for line in lines:
         px = parse_proxy_line(line)
-        if px:
+        if validate_proxy(px):
             proxies.append(px)
 
+    # NEW: Warn and halt if no valid proxies!
+    if not proxies:
+        print('[FATAL] No valid proxies remain after filtering subscription. Check your sub or filtering policy!')
+        sys.exit(2)
+
     print(f"[+] Parsed proxies: {len(proxies)}")
 
     # Show protocol distribution