Merge pull request #185 from ReturnFI/beta

OBFS & WARP management, UI improvements, and better normalsub config
This commit is contained in:
Whispering Wind
2025-06-03 00:05:58 +03:30
committed by GitHub
11 changed files with 721 additions and 354 deletions

View File

@ -1,18 +1,13 @@
# [1.11.0] - 2025-05-28 # [1.12.0] - 2025-06-03
#### ✨ Features & Enhancements
#### ⚠️ Attention Required * 🎨 **UI/UX:** Some **enhancements to the Settings page** with cleaner design and improved interactivity
* 🌐 **New Tabs:** Added **WARP** and **OBFS** management tabs to the settings panel for easier control
* 🔍 **OBFS:**
🚨 **Important:** Due to changes in the `normalsub` system (now using passwords in links and routing through Caddy), * Added **OBFS status API endpoint**
you must **re-activate NormalSUB** from the settings panel after upgrading to **v1.11.0**. * Introduced `--check` flag to the `manage_obfs` CLI tool
* Enabled **status checking** functionality to monitor OBFS state
Failure to do so may result in broken subscription links. * 🔧 **normalsub:** Improved `edit_subpath` with **better error handling** and **efficient Caddy reload**
* 📦 **Singbox:** Enhanced **Singbox template configuration** for better maintainability and compatibility
---
#### ✨ Features & Improvements
* 🔐 **normalsub:** Use **user password** instead of username in subscription path for improved privacy (harder to enumerate users)
* 🔁 **normalsub:** Now uses **Caddy** as a reverse proxy for better performance and flexibility
* 🧩 **WARP API:** Adapted backend to support **JSON-based status output**
* 🛠️ **Script Update:** WARP status script now outputs clean **JSON**, allowing easier parsing and integration

View File

@ -250,19 +250,25 @@ def server_info():
@cli.command('manage_obfs') @cli.command('manage_obfs')
@click.option('--remove', '-r', is_flag=True, help="Remove 'obfs' from config.json.") @click.option('--remove', '-r', is_flag=True, help="Remove 'obfs' from config.json.")
@click.option('--generate', '-g', is_flag=True, help="Generate new 'obfs' in config.json.") @click.option('--generate', '-g', is_flag=True, help="Generate new 'obfs' in config.json.")
def manage_obfs(remove: bool, generate: bool): @click.option('--check', '-c', is_flag=True, help="Check 'obfs' status in config.json.")
def manage_obfs(remove: bool, generate: bool, check: bool):
try: try:
if not remove and not generate: options_selected = sum([remove, generate, check])
raise click.UsageError('Error: You must use either --remove or --generate') if options_selected == 0:
if remove and generate: raise click.UsageError('Error: You must use either --remove, --generate, or --check.')
raise click.UsageError('Error: You cannot use both --remove and --generate at the same time') if options_selected > 1:
raise click.UsageError('Error: You can only use one of --remove, --generate, or --check at a time.')
if generate: if generate:
cli_api.enable_hysteria2_obfs() cli_api.enable_hysteria2_obfs()
click.echo('Obfs enabled successfully.') click.echo('OBFS enabled successfully.')
elif remove: elif remove:
cli_api.disable_hysteria2_obfs() cli_api.disable_hysteria2_obfs()
click.echo('Obfs disabled successfully.') click.echo('OBFS disabled successfully.')
elif check:
status_output = cli_api.check_hysteria2_obfs()
click.echo(status_output)
except Exception as e: except Exception as e:
click.echo(f'{e}', err=True) click.echo(f'{e}', err=True)
@ -361,16 +367,29 @@ def uninstall_warp():
@cli.command('configure-warp') @cli.command('configure-warp')
@click.option('--all', '-a', is_flag=True, help='Use WARP for all connections') @click.option('--set-all', 'set_all_traffic', type=click.Choice(['on', 'off']), help='Set WARP for all connections (on/off)', required=False)
@click.option('--popular-sites', '-p', is_flag=True, help='Use WARP for popular sites like Google, OpenAI, etc') @click.option('--set-popular-sites', type=click.Choice(['on', 'off']), help='Set WARP for popular sites (on/off)', required=False)
@click.option('--domestic-sites', '-d', is_flag=True, help='Use WARP for Iran domestic sites') @click.option('--set-domestic-sites', type=click.Choice(['on', 'off']), help='Set behavior for domestic sites (on=WARP, off=REJECT)', required=False)
@click.option('--block-adult-sites', '-x', is_flag=True, help='Block adult content (porn)') @click.option('--set-block-adult-sites', type=click.Choice(['on', 'off']), help='Set block adult content (on/off)', required=False)
def configure_warp(all: bool, popular_sites: bool, domestic_sites: bool, block_adult_sites: bool): def configure_warp_cmd(set_all_traffic: str | None,
set_popular_sites: str | None,
set_domestic_sites: str | None,
set_block_adult_sites: str | None):
if not any([set_all_traffic, set_popular_sites, set_domestic_sites, set_block_adult_sites]):
click.echo("Error: At least one configuration option must be provided to configure-warp.", err=True)
click.echo("Use --help for more information.")
return
try: try:
cli_api.configure_warp(all, popular_sites, domestic_sites, block_adult_sites) cli_api.configure_warp(
click.echo('WARP configured successfully.') all_state=set_all_traffic,
popular_sites_state=set_popular_sites,
domestic_sites_state=set_domestic_sites,
block_adult_sites_state=set_block_adult_sites
)
click.echo('WARP configuration update process initiated.')
except Exception as e: except Exception as e:
click.echo(f'{e}', err=True) click.echo(f'Error configuring WARP: {e}', err=True)
@cli.command('warp-status') @cli.command('warp-status')
def warp_status(): def warp_status():

View File

@ -218,6 +218,10 @@ def disable_hysteria2_obfs():
'''Removes 'obfs' from Hysteria2 configuration.''' '''Removes 'obfs' from Hysteria2 configuration.'''
run_cmd(['python3', Command.MANAGE_OBFS.value, '--remove']) run_cmd(['python3', Command.MANAGE_OBFS.value, '--remove'])
def check_hysteria2_obfs():
'''Removes 'obfs' from Hysteria2 configuration.'''
result = subprocess.run(["python3", Command.MANAGE_OBFS.value, "--check"], check=True, capture_output=True, text=True)
return result.stdout.strip()
def enable_hysteria2_masquerade(domain: str): def enable_hysteria2_masquerade(domain: str):
'''Enables masquerade for Hysteria2.''' '''Enables masquerade for Hysteria2.'''
@ -458,21 +462,28 @@ def uninstall_warp():
run_cmd(['python3', Command.UNINSTALL_WARP.value]) run_cmd(['python3', Command.UNINSTALL_WARP.value])
def configure_warp(all: bool, popular_sites: bool, domestic_sites: bool, block_adult_sites: bool): def configure_warp(all_state: str | None = None,
popular_sites_state: str | None = None,
domestic_sites_state: str | None = None,
block_adult_sites_state: str | None = None):
''' '''
Configures WARP with various options. Configures WARP with various options. States are 'on' or 'off'.
''' '''
cmd_args = [ cmd_args = [
'python3', Command.CONFIGURE_WARP.value 'python3', Command.CONFIGURE_WARP.value
] ]
if all: if all_state:
cmd_args.append('--all') cmd_args.extend(['--set-all', all_state])
if popular_sites: if popular_sites_state:
cmd_args.append('--popular-sites') cmd_args.extend(['--set-popular-sites', popular_sites_state])
if domestic_sites: if domestic_sites_state:
cmd_args.append('--domestic-sites') cmd_args.extend(['--set-domestic-sites', domestic_sites_state])
if block_adult_sites: if block_adult_sites_state:
cmd_args.append('--block-adult') cmd_args.extend(['--set-block-adult', block_adult_sites_state])
if len(cmd_args) == 2:
print("No WARP configuration options provided to cli_api.configure_warp.")
return
run_cmd(cmd_args) run_cmd(cmd_args)

View File

@ -9,16 +9,14 @@ from init_paths import *
from paths import * from paths import *
def restart_hysteria(): def restart_hysteria():
"""Restart the Hysteria2 service using the CLI script."""
try: try:
subprocess.run(["python3", CLI_PATH, "restart-hysteria2"], subprocess.run(["python3", CLI_PATH, "restart-hysteria2"],
stdout=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL) stderr=subprocess.DEVNULL)
except Exception as e: except Exception as e:
print(f"⚠️ Failed to restart Hysteria2: {e}") print(f"Failed to restart Hysteria2: {e}")
def remove_obfs(): def remove_obfs():
"""Remove the 'obfs' section from the config."""
try: try:
with open(CONFIG_FILE, 'r') as f: with open(CONFIG_FILE, 'r') as f:
config = json.load(f) config = json.load(f)
@ -27,9 +25,9 @@ def remove_obfs():
del config['obfs'] del config['obfs']
with open(CONFIG_FILE, 'w') as f: with open(CONFIG_FILE, 'w') as f:
json.dump(config, f, indent=2) json.dump(config, f, indent=2)
print("Successfully removed 'obfs' from config.json.") print("Successfully removed 'obfs' from config.json.")
else: else:
print(" 'obfs' section not found in config.json.") print("'obfs' section not found in config.json.")
restart_hysteria() restart_hysteria()
@ -39,13 +37,12 @@ def remove_obfs():
print(f"❌ Error removing 'obfs': {e}") print(f"❌ Error removing 'obfs': {e}")
def generate_obfs(): def generate_obfs():
"""Generate and add an 'obfs' section with a random password."""
try: try:
with open(CONFIG_FILE, 'r') as f: with open(CONFIG_FILE, 'r') as f:
config = json.load(f) config = json.load(f)
if 'obfs' in config: if 'obfs' in config:
print(" 'obfs' section already exists. Replacing it.") print("'obfs' section already exists. Replacing it.")
del config['obfs'] del config['obfs']
password = ''.join(secrets.choice(string.ascii_letters + string.digits) for _ in range(32)) password = ''.join(secrets.choice(string.ascii_letters + string.digits) for _ in range(32))
@ -60,29 +57,47 @@ def generate_obfs():
with open(CONFIG_FILE, 'w') as f: with open(CONFIG_FILE, 'w') as f:
json.dump(config, f, indent=2) json.dump(config, f, indent=2)
print(f"Successfully added 'obfs' to config.json with password: {password}") print(f"Successfully added 'obfs' to config.json with password: {password}")
restart_hysteria() restart_hysteria()
except FileNotFoundError: except FileNotFoundError:
print(f"Config file not found: {CONFIG_FILE}") print(f"Config file not found: {CONFIG_FILE}")
except Exception as e: except Exception as e:
print(f"Error generating 'obfs': {e}") print(f"Error generating 'obfs': {e}")
def check_obfs():
try:
with open(CONFIG_FILE, 'r') as f:
config = json.load(f)
if 'obfs' in config:
print("OBFS is active.")
else:
print("OBFS is not active.")
except FileNotFoundError:
print(f"Config file not found: {CONFIG_FILE}")
except Exception as e:
print(f"Error checking 'obfs' status: {e}")
def main(): def main():
if len(sys.argv) != 2: if len(sys.argv) != 2:
print("Usage: python3 obfs_manager.py --remove|-r | --generate|-g") print("Usage: python3 obfs_manager.py --remove|-r | --generate|-g | --check|-c")
sys.exit(1) sys.exit(1)
option = sys.argv[1] option = sys.argv[1]
if option in ("--remove", "-r"): if option in ("--remove", "-r"):
print("Removing 'obfs' from config.json...") # print("Removing 'obfs' from config.json...")
remove_obfs() remove_obfs()
elif option in ("--generate", "-g"): elif option in ("--generate", "-g"):
print("Generating 'obfs' in config.json...") # print("Generating 'obfs' in config.json...")
generate_obfs() generate_obfs()
elif option in ("--check", "-c"):
# print("Checking 'obfs' status in config.json...")
check_obfs()
else: else:
print("Invalid option. Use --remove|-r or --generate|-g") print("Invalid option. Use --remove|-r, --generate|-g, or --check|-c")
sys.exit(1) sys.exit(1)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -1,171 +1,159 @@
{ {
"log": {
"level": "info",
"timestamp": true
},
"dns": { "dns": {
"servers": [ "final": "local-dns",
{
"tag": "proxyDns",
"address": "tls://8.8.8.8",
"detour": "Proxy"
},
{
"tag": "localDns",
"address": "https://223.5.5.5/dns-query",
"detour": "direct"
}
],
"rules": [ "rules": [
{ {
"outbound": "any", "action": "route",
"server": "localDns" "clash_mode": "Global",
"server": "proxy-dns",
"source_ip_cidr": [
"172.19.0.0/30",
"fdfe:dcba:9876::1/126"
]
}, },
{ {
"rule_set": "geosite-ir", "action": "route",
"server": "proxyDns" "server": "proxy-dns",
}, "source_ip_cidr": [
{ "172.19.0.0/30",
"clash_mode": "direct", "fdfe:dcba:9876::1/126"
"server": "localDns" ]
},
{
"clash_mode": "global",
"server": "proxyDns"
} }
], ],
"final": "localDns", "servers": [
"strategy": "ipv4_only" {
"type": "https",
"server": "1.1.1.1",
"address_resolver": "local-dns",
"detour": "proxy",
"tag": "proxy-dns"
},
{
"address": "local",
"detour": "direct",
"tag": "local-dns"
}
],
"strategy": "prefer_ipv4"
}, },
"inbounds": [ "inbounds": [
{ {
"tag": "tun-in",
"type": "tun",
"address": [ "address": [
"172.19.0.0/30" "172.19.0.1/30",
"fdfe:dcba:9876::1/126"
], ],
"mtu": 9000,
"auto_route": true, "auto_route": true,
"strict_route": true, "endpoint_independent_nat": false,
"stack": "system", "mtu": 9000,
"platform": { "platform": {
"http_proxy": { "http_proxy": {
"enabled": true, "enabled": true,
"server": "127.0.0.1", "server": "127.0.0.1",
"server_port": 2080 "server_port": 2080
} }
} },
"stack": "system",
"strict_route": false,
"type": "tun"
}, },
{ {
"tag": "mixed-in",
"type": "mixed",
"listen": "127.0.0.1", "listen": "127.0.0.1",
"listen_port": 2080 "listen_port": 2080,
"type": "mixed",
"users": []
} }
], ],
"log": {
"level": "warn",
"timestamp": true
},
"outbounds": [ "outbounds": [
{ {
"tag": "Proxy",
"type": "selector",
"outbounds": [ "outbounds": [
"auto", "auto",
"direct" "direct"
]
},
{
"tag": "Global",
"type": "selector",
"outbounds": [
"direct"
]
},
{
"tag": "auto",
"type": "urltest",
"outbounds": [
"Proxy"
], ],
"url": "http://www.gstatic.com/generate_204", "tag": "proxy",
"type": "selector"
},
{
"interval": "10m", "interval": "10m",
"tolerance": 50 "outbounds": [],
"tag": "auto",
"tolerance": 50,
"type": "urltest",
"url": "http://www.gstatic.com/generate_204"
}, },
{ {
"type": "direct", "tag": "direct",
"tag": "direct" "type": "direct"
},
{
"type": "direct",
"tag": "local"
} }
], ],
"route": { "route": {
"auto_detect_interface": true, "auto_detect_interface": true,
"final": "Proxy", "final": "proxy",
"rule_set": [
{
"download_detour": "direct",
"format": "binary",
"tag": "geosite-ads",
"type": "remote",
"url": "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geosite-category-ads-all.srs"
},
{
"download_detour": "direct",
"format": "binary",
"tag": "geoip-private",
"type": "remote",
"url": "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geoip-private.srs"
},
{
"download_detour": "direct",
"format": "binary",
"tag": "geosite-ir",
"type": "remote",
"url": "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geosite-ir.srs"
},
{
"download_detour": "direct",
"format": "binary",
"tag": "geoip-ir",
"type": "remote",
"url": "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geoip-ir.srs"
}
],
"rules": [ "rules": [
{ {
"inbound": [
"tun-in",
"mixed-in"
],
"action": "sniff" "action": "sniff"
}, },
{ {
"type": "logical", "action": "route",
"mode": "or", "clash_mode": "Direct",
"rules": [ "outbound": "direct"
{
"port": 53
}, },
{ {
"action": "route",
"clash_mode": "Global",
"outbound": "proxy"
},
{
"action": "hijack-dns",
"protocol": "dns" "protocol": "dns"
}
],
"action": "hijack-dns"
},
{
"rule_set": "geosite-category-ads-all",
"action": "reject"
},
{
"rule_set": "geosite-category-ads-all",
"outbound": "Proxy"
},
{
"ip_is_private": true,
"outbound": "direct"
}, },
{ {
"action": "route", "action": "route",
"rule_set": "geosite-ir", "outbound": "direct",
"outbound": "direct"
},
{
"action": "route",
"rule_set": "geoip-ir",
"outbound": "direct"
}
],
"rule_set": [ "rule_set": [
{ "geosite-ir",
"tag": "geosite-category-ads-all", "geoip-ir",
"type": "remote", "geosite-private"
"format": "binary", ]
"url": "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geosite-category-ads-all.srs",
"download_detour": "direct"
}, },
{ {
"type": "remote", "action": "reject",
"tag": "geoip-ir", "rule_set": [
"format": "binary", "geoip-ads"
"url": "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geoip-ir.srs", ]
"update_interval": "120h0m0s"
},
{
"type": "remote",
"tag": "geosite-ir",
"format": "binary",
"url": "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geosite-ir.srs",
"update_interval": "120h0m0s"
} }
] ]
} }

View File

@ -4,6 +4,7 @@ import json
import sys import sys
import subprocess import subprocess
from pathlib import Path from pathlib import Path
import argparse
core_scripts_dir = Path(__file__).resolve().parents[1] core_scripts_dir = Path(__file__).resolve().parents[1]
if str(core_scripts_dir) not in sys.path: if str(core_scripts_dir) not in sys.path:
@ -11,149 +12,184 @@ if str(core_scripts_dir) not in sys.path:
from paths import * from paths import *
def warp_configure_handler(all_traffic=False, popular_sites=False, domestic_sites=False, block_adult_sites=False): def warp_configure_handler(
""" set_all_traffic_state: str | None = None,
Configure WARP routing rules based on provided parameters set_popular_sites_state: str | None = None,
set_domestic_sites_state: str | None = None,
Args: set_block_adult_sites_state: str | None = None
all_traffic (bool): Toggle WARP for all traffic ):
popular_sites (bool): Toggle WARP for popular sites (Google, Netflix, etc.) try:
domestic_sites (bool): Toggle between WARP and Reject for domestic sites
block_adult_sites (bool): Toggle blocking of adult content
"""
with open(CONFIG_FILE, 'r') as f: with open(CONFIG_FILE, 'r') as f:
config = json.load(f) config = json.load(f)
except FileNotFoundError:
print(f"Error: Configuration file {CONFIG_FILE} not found.")
sys.exit(1)
except json.JSONDecodeError:
print(f"Error: Could not decode JSON from {CONFIG_FILE}.")
sys.exit(1)
modified = False modified = False
if all_traffic: if 'acl' not in config:
warp_all_active = any(rule == "warps(all)" for rule in config.get('acl', {}).get('inline', [])) config['acl'] = {}
if 'inline' not in config['acl']:
config['acl']['inline'] = []
if set_all_traffic_state is not None:
warp_all_rule = "warps(all)"
warp_all_active = warp_all_rule in config['acl']['inline']
if set_all_traffic_state == "on":
if not warp_all_active:
config['acl']['inline'].append(warp_all_rule)
print("All traffic rule: Enabled.")
modified = True
else:
print("All traffic rule: Already enabled.")
elif set_all_traffic_state == "off":
if warp_all_active: if warp_all_active:
config['acl']['inline'] = [rule for rule in config['acl']['inline'] if rule != "warps(all)"] config['acl']['inline'] = [rule for rule in config['acl']['inline'] if rule != warp_all_rule]
print("Traffic configuration changed to Direct.") print("All traffic rule: Disabled.")
modified = True modified = True
else: else:
if 'acl' not in config: print("All traffic rule: Already disabled.")
config['acl'] = {}
if 'inline' not in config['acl']:
config['acl']['inline'] = []
config['acl']['inline'].append("warps(all)")
print("Traffic configuration changed to WARP.")
modified = True
if popular_sites: if set_popular_sites_state is not None:
popular_rules = [ popular_rules = [
"warps(geoip:google)", "warps(geoip:google)", "warps(geosite:google)", "warps(geosite:netflix)",
"warps(geosite:google)", "warps(geosite:spotify)", "warps(geosite:openai)", "warps(geoip:openai)"
"warps(geosite:netflix)",
"warps(geosite:spotify)",
"warps(geosite:openai)",
"warps(geoip:openai)"
] ]
if set_popular_sites_state == "on":
rule_exists = any(rule in config.get('acl', {}).get('inline', []) for rule in popular_rules) added_any = False
for rule in popular_rules:
if rule_exists: if rule not in config['acl']['inline']:
config['acl']['inline'] = [rule for rule in config['acl']['inline'] config['acl']['inline'].append(rule)
if rule not in popular_rules] added_any = True
print("WARP configuration for Google, OpenAI, etc. removed.") if added_any:
print("Popular sites rule: Enabled/Updated.")
modified = True modified = True
else: else:
if 'acl' not in config:
config['acl'] = {} all_present = all(rule in config['acl']['inline'] for rule in popular_rules)
if 'inline' not in config['acl']: if all_present:
config['acl']['inline'] = [] print("Popular sites rule: Already enabled.")
config['acl']['inline'].extend(popular_rules) else:
print("WARP configured for Google, OpenAI, etc.") print("Popular sites rule: Enabled/Updated.")
modified = True modified = True
elif set_popular_sites_state == "off":
if domestic_sites: removed_any = False
ir_site_rule = "warps(geosite:ir)" initial_len = len(config['acl']['inline'])
ir_ip_rule = "warps(geoip:ir)" config['acl']['inline'] = [rule for rule in config['acl']['inline'] if rule not in popular_rules]
reject_site_rule = "reject(geosite:ir)" if len(config['acl']['inline']) < initial_len:
reject_ip_rule = "reject(geoip:ir)" removed_any = True
if removed_any:
using_warp = (ir_site_rule in config.get('acl', {}).get('inline', []) and print("Popular sites rule: Disabled.")
ir_ip_rule in config.get('acl', {}).get('inline', []))
using_reject = (reject_site_rule in config.get('acl', {}).get('inline', []) and
reject_ip_rule in config.get('acl', {}).get('inline', []))
if 'acl' not in config:
config['acl'] = {}
if 'inline' not in config['acl']:
config['acl']['inline'] = []
if using_warp:
config['acl']['inline'] = [reject_site_rule if rule == ir_site_rule else
reject_ip_rule if rule == ir_ip_rule else
rule for rule in config['acl']['inline']]
print("Configuration changed to Reject for geosite:ir and geoip:ir.")
modified = True
elif using_reject:
config['acl']['inline'] = [ir_site_rule if rule == reject_site_rule else
ir_ip_rule if rule == reject_ip_rule else
rule for rule in config['acl']['inline']]
print("Configuration changed to Use WARP for geosite:ir and geoip:ir.")
modified = True modified = True
else: else:
config['acl']['inline'].extend([reject_site_rule, reject_ip_rule]) print("Popular sites rule: Already disabled.")
print("Added Reject configuration for geosite:ir and geoip:ir.")
modified = True
if block_adult_sites: if set_domestic_sites_state is not None:
ir_site_warp_rule = "warps(geosite:ir)"
ir_ip_warp_rule = "warps(geoip:ir)"
ir_site_reject_rule = "reject(geosite:ir)"
ir_ip_reject_rule = "reject(geoip:ir)"
if set_domestic_sites_state == "on":
changed_to_warp = False
if ir_site_reject_rule in config['acl']['inline'] or ir_ip_reject_rule in config['acl']['inline']:
config['acl']['inline'] = [r for r in config['acl']['inline'] if r not in [ir_site_reject_rule, ir_ip_reject_rule]]
changed_to_warp = True
if ir_site_warp_rule not in config['acl']['inline']:
config['acl']['inline'].append(ir_site_warp_rule)
changed_to_warp = True
if ir_ip_warp_rule not in config['acl']['inline']:
config['acl']['inline'].append(ir_ip_warp_rule)
changed_to_warp = True
if changed_to_warp:
print("Domestic sites: Configured to use WARP.")
modified = True
else:
print("Domestic sites: Already configured to use WARP.")
elif set_domestic_sites_state == "off":
changed_to_reject = False
if ir_site_warp_rule in config['acl']['inline'] or ir_ip_warp_rule in config['acl']['inline']:
config['acl']['inline'] = [r for r in config['acl']['inline'] if r not in [ir_site_warp_rule, ir_ip_warp_rule]]
changed_to_reject = True
if ir_site_reject_rule not in config['acl']['inline']:
config['acl']['inline'].append(ir_site_reject_rule)
changed_to_reject = True
if ir_ip_reject_rule not in config['acl']['inline']:
config['acl']['inline'].append(ir_ip_reject_rule)
changed_to_reject = True
if changed_to_reject:
print("Domestic sites: Configured to REJECT.")
modified = True
else:
print("Domestic sites: Already configured to REJECT.")
if set_block_adult_sites_state is not None:
nsfw_rule = "reject(geosite:nsfw)" nsfw_rule = "reject(geosite:nsfw)"
is_blocking_nsfw = nsfw_rule in config['acl']['inline']
blocked = nsfw_rule in config.get('acl', {}).get('inline', []) if 'resolver' not in config: config['resolver'] = {}
if 'tls' not in config['resolver']: config['resolver']['tls'] = {}
if blocked: desired_resolver = ""
config['acl']['inline'] = [rule for rule in config['acl']['inline'] if set_block_adult_sites_state == "on":
if rule != nsfw_rule] desired_resolver = "1.1.1.3:853"
if 'resolver' not in config: if not is_blocking_nsfw:
config['resolver'] = {} config['acl']['inline'].append(nsfw_rule)
if 'tls' not in config['resolver']: print("Adult content blocking: Enabled.")
config['resolver']['tls'] = {}
config['resolver']['tls']['addr'] = "1.1.1.1:853"
print("Adult content blocking removed and resolver updated.")
modified = True modified = True
else: else:
if 'acl' not in config: print("Adult content blocking: Already enabled.")
config['acl'] = {} elif set_block_adult_sites_state == "off":
if 'inline' not in config['acl']: desired_resolver = "1.1.1.1:853"
config['acl']['inline'] = [] if is_blocking_nsfw:
config['acl']['inline'].append(nsfw_rule) config['acl']['inline'] = [rule for rule in config['acl']['inline'] if rule != nsfw_rule]
if 'resolver' not in config: print("Adult content blocking: Disabled.")
config['resolver'] = {}
if 'tls' not in config['resolver']:
config['resolver']['tls'] = {}
config['resolver']['tls']['addr'] = "1.1.1.3:853"
print("Adult content blocked and resolver updated.")
modified = True modified = True
else:
print("Adult content blocking: Already disabled.")
if config['resolver']['tls'].get('addr') != desired_resolver:
config['resolver']['tls']['addr'] = desired_resolver
print(f"Resolver: Updated to {desired_resolver}.")
modified = True
if 'acl' in config and 'inline' in config['acl']:
config['acl']['inline'] = [rule for rule in config['acl']['inline'] if rule]
if modified: if modified:
with open(CONFIG_FILE, 'w') as f: with open(CONFIG_FILE, 'w') as f:
json.dump(config, f, indent=2) json.dump(config, f, indent=2)
print("Configuration updated. Attempting to restart hysteria2 service...")
try: try:
subprocess.run(["python3", CLI_PATH, "restart-hysteria2"], subprocess.run(["python3", CLI_PATH, "restart-hysteria2"],
stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True) stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True, timeout=10)
except subprocess.CalledProcessError: print("Hysteria2 service restarted successfully.")
print("Warning: Failed to restart hysteria2") except subprocess.CalledProcessError as e:
print(f"Warning: Failed to restart hysteria2. STDERR: {e.stderr.decode().strip()}")
except subprocess.TimeoutExpired:
print("Warning: Timeout expired while trying to restart hysteria2 service.")
if __name__ == "__main__": if __name__ == "__main__":
import argparse parser = argparse.ArgumentParser(description="Configure WARP settings. At least one option must be provided.")
parser.add_argument("--set-all", choices=['on', 'off'], help="Set WARP for all traffic (on/off)")
parser = argparse.ArgumentParser(description="Configure WARP settings") parser.add_argument("--set-popular-sites", choices=['on', 'off'], help="Set WARP for popular sites (on/off)")
parser.add_argument("--all", action="store_true", help="Toggle WARP for all traffic") parser.add_argument("--set-domestic-sites", choices=['on', 'off'], help="Set behavior for domestic sites (on=WARP, off=REJECT)")
parser.add_argument("--popular-sites", action="store_true", help="Toggle WARP for popular sites") parser.add_argument("--set-block-adult", choices=['on', 'off'], help="Set blocking of adult content (on/off)")
parser.add_argument("--domestic-sites", action="store_true", help="Toggle between WARP and Reject for domestic sites")
parser.add_argument("--block-adult", action="store_true", help="Toggle blocking of adult content")
args = parser.parse_args() args = parser.parse_args()
if not any(vars(args).values()):
parser.print_help()
sys.exit(1)
warp_configure_handler( warp_configure_handler(
all_traffic=args.all, set_all_traffic_state=args.set_all,
popular_sites=args.popular_sites, set_popular_sites_state=args.set_popular_sites,
domestic_sites=args.domestic_sites, set_domestic_sites_state=args.set_domestic_sites,
block_adult_sites=args.block_adult set_block_adult_sites_state=args.set_block_adult
) )

View File

@ -1,5 +1,5 @@
from fastapi import APIRouter, BackgroundTasks, HTTPException, UploadFile, File from fastapi import APIRouter, BackgroundTasks, HTTPException, UploadFile, File
from ..schema.config.hysteria import ConfigFile, GetPortResponse, GetSniResponse from ..schema.config.hysteria import ConfigFile, GetPortResponse, GetSniResponse, GetObfsResponse
from ..schema.response import DetailResponse, IPLimitConfig, SetupDecoyRequest, DecoyStatusResponse, IPLimitConfigResponse from ..schema.response import DetailResponse, IPLimitConfig, SetupDecoyRequest, DecoyStatusResponse, IPLimitConfigResponse
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
import shutil import shutil
@ -223,6 +223,23 @@ async def disable_obfs():
raise HTTPException(status_code=400, detail=f'Error: {str(e)}') raise HTTPException(status_code=400, detail=f'Error: {str(e)}')
@router.get('/check-obfs', response_model=GetObfsResponse, summary='Check Hysteria2 OBFS Status')
async def check_obfs():
"""
Checks the current status of Hysteria2 OBFS.
Returns:
A GetObfsResponse containing the Hysteria2 OBFS status message (e.g., 'OBFS is active.').
Raises:
HTTPException: if an error occurs while checking the Hysteria2 OBFS status.
"""
try:
obfs_status_message = cli_api.check_hysteria2_obfs()
return GetObfsResponse(obfs=obfs_status_message)
except Exception as e:
raise HTTPException(status_code=400, detail=f'Error checking OBFS status: {str(e)}')
@router.get('/enable-masquerade/{domain}', response_model=DetailResponse, summary='Enable Hysteria2 masquerade') @router.get('/enable-masquerade/{domain}', response_model=DetailResponse, summary='Enable Hysteria2 masquerade')
async def enable_masquerade(domain: str): async def enable_masquerade(domain: str):
""" """

View File

@ -8,7 +8,7 @@ import cli_api
router = APIRouter() router = APIRouter()
@router.post('/install', response_model=DetailResponse, summary='Install WARP') @router.post('/install', response_model=DetailResponse, summary='Install WARP', name="install_warp")
async def install(): async def install():
""" """
Installs WARP. Installs WARP.
@ -27,7 +27,7 @@ async def install():
raise HTTPException(status_code=400, detail=f'Error: {str(e)}') raise HTTPException(status_code=400, detail=f'Error: {str(e)}')
@router.delete('/uninstall', response_model=DetailResponse, summary='Uninstall WARP') @router.delete('/uninstall', response_model=DetailResponse, summary='Uninstall WARP', name="uninstall_warp")
async def uninstall(): async def uninstall():
""" """
Uninstalls WARP. Uninstalls WARP.
@ -45,7 +45,7 @@ async def uninstall():
raise HTTPException(status_code=400, detail=f'Error: {str(e)}') raise HTTPException(status_code=400, detail=f'Error: {str(e)}')
@router.post('/configure', response_model=DetailResponse, summary='Configure WARP') @router.post('/configure', response_model=DetailResponse, summary='Configure WARP', name="configure_warp")
async def configure(body: ConfigureInputBody): async def configure(body: ConfigureInputBody):
""" """
Configures WARP with the given options. Configures WARP with the given options.
@ -60,14 +60,23 @@ async def configure(body: ConfigureInputBody):
HTTPException: If an error occurs during configuration, an HTTP 400 error is raised with the error details. HTTPException: If an error occurs during configuration, an HTTP 400 error is raised with the error details.
""" """
try: try:
cli_api.configure_warp(body.all, body.popular_sites, body.domestic_sites, all_st = 'on' if body.all else 'off'
body.block_adult_sites) pop_sites_st = 'on' if body.popular_sites else 'off'
dom_sites_st = 'on' if body.domestic_sites else 'off'
block_adult_st = 'on' if body.block_adult_sites else 'off'
cli_api.configure_warp(
all_state=all_st,
popular_sites_state=pop_sites_st,
domestic_sites_state=dom_sites_st,
block_adult_sites_state=block_adult_st
)
return DetailResponse(detail='WARP configured successfully.') return DetailResponse(detail='WARP configured successfully.')
except Exception as e: except Exception as e:
raise HTTPException(status_code=400, detail=f'Error: {str(e)}') raise HTTPException(status_code=404, detail=f'Error configuring WARP: {str(e)}')
@router.get('/status', response_model=StatusResponse, summary='Get WARP Status') @router.get('/status', response_model=StatusResponse, summary='Get WARP Status', name="status_warp")
async def status(): async def status():
try: try:
status_json_str = cli_api.warp_status() status_json_str = cli_api.warp_status()

View File

@ -18,3 +18,6 @@ class GetPortResponse(BaseModel):
class GetSniResponse(BaseModel): class GetSniResponse(BaseModel):
sni: str sni: str
class GetObfsResponse(BaseModel):
obfs: str

View File

@ -40,6 +40,11 @@
<a class='nav-link' id='sni-tab' data-toggle='pill' href='#sni' role='tab' <a class='nav-link' id='sni-tab' data-toggle='pill' href='#sni' role='tab'
aria-controls='sni' aria-selected='false'><i class="fas fa-shield-alt"></i> Change aria-controls='sni' aria-selected='false'><i class="fas fa-shield-alt"></i> Change
SNI</a> SNI</a>
</li>
<li class='nav-item'>
<a class='nav-link' id='obfs-tab' data-toggle='pill' href='#obfs' role='tab'
aria-controls='obfs' aria-selected='false'><i class="fas fa-user-secret"></i>
OBFS</a>
</li> </li>
<li class='nav-item'> <li class='nav-item'>
<a class='nav-link' id='ip-tab' data-toggle='pill' href='#change_ip' role='tab' <a class='nav-link' id='ip-tab' data-toggle='pill' href='#change_ip' role='tab'
@ -61,6 +66,10 @@
aria-controls='decoy' aria-selected='false'><i class="fas fa-mask"></i> aria-controls='decoy' aria-selected='false'><i class="fas fa-mask"></i>
Decoy Site</a> Decoy Site</a>
</li> </li>
<li class='nav-item'>
<a class='nav-link' id='warp-tab-link' data-toggle='pill' href='#warp-content' role='tab'
aria-controls='warp-content' aria-selected='false'><i class="fas fa-cloud"></i> WARP</a>
</li>
</ul> </ul>
</div> </div>
<div class='card-body' style="margin-left: 25px;"> <div class='card-body' style="margin-left: 25px;">
@ -71,22 +80,21 @@
<ul class='nav nav-tabs' id='subs-tabs' role='tablist'> <ul class='nav nav-tabs' id='subs-tabs' role='tablist'>
<li class='nav-item'> <li class='nav-item'>
<a class='nav-link active' id='normal-tab' data-toggle='tab' href='#normal' role='tab' <a class='nav-link active' id='normal-tab' data-toggle='tab' href='#normal' role='tab'
aria-controls='normal' aria-selected='true'><strong>Normal</strong></a> aria-controls='normal' aria-selected='true'><strong>Service Control</strong></a>
</li> </li>
<li class='nav-item normal-sub-config-tab-li' style="display: none;"> <!-- Initially hidden --> <li class='nav-item normal-sub-config-tab-li' style="display: none;">
<a class='nav-link' id='normal-sub-config-link-tab' data-toggle='tab' href='#normal-sub-config-content' role='tab' <a class='nav-link' id='normal-sub-config-link-tab' data-toggle='tab' href='#normal-sub-config-content' role='tab'
aria-controls='normal-sub-config-content' aria-selected='false'><strong>Configure</strong></a> aria-controls='normal-sub-config-content' aria-selected='false'><strong>Configure Link</strong></a>
</li> </li>
</ul> </ul>
<div class='tab-content' id='subs-tabs-content'> <div class='tab-content' id='subs-tabs-content'>
<br> <br>
<!-- Normal Sub Service Control Tab -->
<div class='tab-pane fade show active' id='normal' role='tabpanel' aria-labelledby='normal-tab'> <div class='tab-pane fade show active' id='normal' role='tabpanel' aria-labelledby='normal-tab'>
<form id="normal_sub_service_form"> <form id="normal_sub_service_form">
<div class='form-group'> <div class='form-group'>
<label for='normal_domain'>Domain:</label> <label for='normal_domain'>Domain:</label>
<input type='text' class='form-control' id='normal_domain' <input type='text' class='form-control' id='normal_domain'
placeholder='Enter Domain'> placeholder='sub.example.com'>
<div class="invalid-feedback"> <div class="invalid-feedback">
Please enter a valid domain (without http:// or https://). Please enter a valid domain (without http:// or https://).
</div> </div>
@ -94,26 +102,25 @@
<div class='form-group'> <div class='form-group'>
<label for='normal_port'>Port:</label> <label for='normal_port'>Port:</label>
<input type='text' class='form-control' id='normal_port' <input type='text' class='form-control' id='normal_port'
placeholder='Enter Port'> placeholder='e.g., 8080'>
<div class="invalid-feedback"> <div class="invalid-feedback">
Please enter a valid port number. Please enter a valid port number.
</div> </div>
</div> </div>
<button id="normal_start" type='button' class='btn btn-success'> <button id="normal_start" type='button' class='btn btn-success'>
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="display: none;"></span> <span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="display: none;"></span>
Start Start Service
</button> </button>
<button id="normal_stop" type='button' class='btn btn-danger' <button id="normal_stop" type='button' class='btn btn-danger'
style="display: none;">Stop</button> style="display: none;">Stop Service</button>
</form> </form>
</div> </div>
<!-- Normal Sub Configuration Tab -->
<div class='tab-pane fade' id='normal-sub-config-content' role='tabpanel' aria-labelledby='normal-sub-config-link-tab'> <div class='tab-pane fade' id='normal-sub-config-content' role='tabpanel' aria-labelledby='normal-sub-config-link-tab'>
<form id="normal_sub_config_form"> <form id="normal_sub_config_form">
<div class='form-group'> <div class='form-group'>
<label for='normal_subpath_input'>Subpath:</label> <label for='normal_subpath_input'>Subscription Path Segment:</label>
<input type='text' class='form-control' id='normal_subpath_input' <input type='text' class='form-control' id='normal_subpath_input'
placeholder='Enter subpath (e.g., mysub)'> placeholder='e.g., mysub (becomes /mysub/...)'>
<div class="invalid-feedback"> <div class="invalid-feedback">
Please enter a valid subpath (alphanumeric characters only, e.g., mysub). Please enter a valid subpath (alphanumeric characters only, e.g., mysub).
</div> </div>
@ -183,6 +190,24 @@
</form> </form>
</div> </div>
<!-- OBFS Tab -->
<div class='tab-pane fade' id='obfs' role='tabpanel' aria-labelledby='obfs-tab'>
<div class="mb-3">
<h5>OBFS Status</h5>
<div id="obfs_status_container" class="p-3 border rounded">
<span id="obfs_status_message">Loading OBFS status...</span>
</div>
</div>
<button id="obfs_enable_btn" type='button' class='btn btn-success' style="display: none;">
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="display: none;"></span>
Enable OBFS
</button>
<button id="obfs_disable_btn" type='button' class='btn btn-danger' style="display: none;">
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="display: none;"></span>
Disable OBFS
</button>
</div>
<!-- Change IP Tab --> <!-- Change IP Tab -->
<div class='tab-pane fade' id='change_ip' role='tabpanel' aria-labelledby='ip-tab'> <div class='tab-pane fade' id='change_ip' role='tabpanel' aria-labelledby='ip-tab'>
<form id="change_ip_form"> <form id="change_ip_form">
@ -208,18 +233,41 @@
<!-- Backup Tab --> <!-- Backup Tab -->
<div class='tab-pane fade' id='backup' role='tabpanel' aria-labelledby='backup-tab'> <div class='tab-pane fade' id='backup' role='tabpanel' aria-labelledby='backup-tab'>
<div class="row">
<div class="col-md-6">
<div class="card card-outline card-success h-100">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-upload"></i> Restore from Backup</h3>
</div>
<div class="card-body">
<p>Upload a previously downloaded .zip backup file to restore your panel settings and Hysteria configuration.</p>
<div class="form-group"> <div class="form-group">
<label for="backup_file">Upload Backup:</label> <label for="backup_file">Select Backup File (.zip):</label>
<input type="file" class="form-control-file" id="backup_file" accept=".zip"> <input type="file" class="form-control-file" id="backup_file" accept=".zip">
</div> </div>
<button id="upload_backup" type='button' class='btn btn-success'>Upload</button> <button id="upload_backup" type='button' class='btn btn-success btn-block'>
<button id="download_backup" type='button' class='btn btn-primary'>Download Backup</button> <span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="display: none;"></span>
Upload and Restore
</button>
<div class="progress mt-3" style="display: none;"> <div class="progress mt-3" style="display: none;">
<div id="backup_progress_bar" class="progress-bar" role="progressbar" style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div> <div id="backup_progress_bar" class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">0%</div>
</div>
<div id="backup_status" class="mt-2 small"></div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card card-outline card-primary h-100">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-download"></i> Create New Backup</h3>
</div>
<div class="card-body d-flex flex-column justify-content-center">
<p>Download a .zip file containing a full backup of your panel settings and Hysteria configuration.</p>
<button id="download_backup" type='button' class='btn btn-primary btn-block mt-auto'>Download Backup</button>
</div>
</div>
</div>
</div> </div>
<div id="backup_status" class="mt-2"></div>
</div> </div>
<!-- IP Limit Tab --> <!-- IP Limit Tab -->
@ -307,6 +355,63 @@
</div> </div>
</div> </div>
<!-- WARP Tab -->
<div class='tab-pane fade' id='warp-content' role='tabpanel' aria-labelledby='warp-tab-link'>
<div id="warp_initial_controls">
<div class='alert alert-info'>WARP service is not active.</div>
<button id="warp_start_btn" type='button' class='btn btn-success mt-3'>
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="display: none;"></span>
Install & Start WARP
</button>
</div>
<div id="warp_active_controls" style="display: none;">
<div class='alert alert-success mb-3'>WARP service is active.</div>
<div class="card card-outline card-secondary">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-cogs"></i> WARP Routing Configuration</h3>
</div>
<div class="card-body">
<form id="warp_config_form">
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="warp_all_traffic">
<label class="custom-control-label" for="warp_all_traffic">Route All Traffic through WARP</label>
</div>
</div>
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="warp_popular_sites">
<label class="custom-control-label" for="warp_popular_sites">Route Popular Sites through WARP</label>
</div>
</div>
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="warp_domestic_sites">
<label class="custom-control-label" for="warp_domestic_sites">Route Domestic Sites through WARP</label>
</div>
</div>
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="warp_block_adult_sites">
<label class="custom-control-label" for="warp_block_adult_sites">Block Adult Sites (WARP Family DNS)</label>
</div>
</div>
<button id="warp_save_config_btn" type='button' class='btn btn-primary'>
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="display: none;"></span>
Save Configuration
</button>
</form>
</div>
</div>
<button id="warp_stop_btn" type='button' class='btn btn-danger mt-3'>
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="display: none;"></span>
Stop & Uninstall WARP
</button>
</div>
</div>
</div> </div>
</div> </div>
<!-- /.card --> <!-- /.card -->
@ -330,6 +435,7 @@
initUI(); initUI();
fetchDecoyStatus(); fetchDecoyStatus();
fetchObfsStatus();
function isValidPath(path) { function isValidPath(path) {
if (!path) return false; if (!path) return false;
@ -519,7 +625,8 @@
const servicesMap = { const servicesMap = {
"hysteria_telegram_bot": "#telegram_form", "hysteria_telegram_bot": "#telegram_form",
"hysteria_normal_sub": "#normal_sub_service_form", "hysteria_normal_sub": "#normal_sub_service_form",
"hysteria_iplimit": "#ip-limit-service" "hysteria_iplimit": "#ip-limit-service",
"hysteria_warp": "warp_service"
}; };
Object.keys(servicesMap).forEach(service => { Object.keys(servicesMap).forEach(service => {
@ -575,6 +682,17 @@
$("#max_ips").val(""); $("#max_ips").val("");
$("#block_duration, #max_ips").removeClass('is-invalid'); $("#block_duration, #max_ips").removeClass('is-invalid');
} }
} else if (service === "hysteria_warp") {
const isWarpServiceRunning = data[service];
if (isWarpServiceRunning) {
$("#warp_initial_controls").hide();
$("#warp_active_controls").show();
fetchWarpFullStatusAndConfig();
} else {
$("#warp_initial_controls").show();
$("#warp_active_controls").hide();
$("#warp_config_form")[0].reset();
}
} else { } else {
const $formSelector = $(targetSelector); const $formSelector = $(targetSelector);
if (isRunning) { if (isRunning) {
@ -722,6 +840,68 @@
} }
function fetchObfsStatus() {
$.ajax({
url: "{{ url_for('check_obfs') }}",
type: "GET",
success: function (data) {
updateObfsUI(data.obfs);
},
error: function (xhr, status, error) {
$("#obfs_status_message").html('<span class="text-danger">Failed to fetch OBFS status.</span>');
console.error("Failed to fetch OBFS status:", error, xhr.responseText);
$("#obfs_enable_btn").hide();
$("#obfs_disable_btn").hide();
}
});
}
function updateObfsUI(statusMessage) {
$("#obfs_status_message").text(statusMessage);
if (statusMessage === "OBFS is active.") {
$("#obfs_enable_btn").hide();
$("#obfs_disable_btn").show();
$("#obfs_status_container").removeClass("border-danger border-warning alert-danger alert-warning").addClass("border-success alert-success");
} else if (statusMessage === "OBFS is not active.") {
$("#obfs_enable_btn").show();
$("#obfs_disable_btn").hide();
$("#obfs_status_container").removeClass("border-success border-danger alert-success alert-danger").addClass("border-warning alert-warning");
} else {
$("#obfs_enable_btn").hide();
$("#obfs_disable_btn").hide();
$("#obfs_status_container").removeClass("border-success border-warning alert-success alert-warning").addClass("border-danger alert-danger");
}
}
function enableObfs() {
confirmAction("enable OBFS", function () {
sendRequest(
"{{ url_for('enable_obfs') }}",
"GET",
null,
"OBFS enabled successfully!",
"#obfs_enable_btn",
false,
fetchObfsStatus
);
});
}
function disableObfs() {
confirmAction("disable OBFS", function () {
sendRequest(
"{{ url_for('disable_obfs') }}",
"GET",
null,
"OBFS disabled successfully!",
"#obfs_disable_btn",
false,
fetchObfsStatus
);
});
}
function startTelegram() { function startTelegram() {
if (!validateForm('telegram_form')) return; if (!validateForm('telegram_form')) return;
const apiToken = $("#telegram_api_token").val(); const apiToken = $("#telegram_api_token").val();
@ -927,6 +1107,84 @@
}); });
} }
function fetchWarpFullStatusAndConfig() {
$.ajax({
url: "{{ url_for('status_warp') }}",
type: "GET",
success: function (data) {
$("#warp_all_traffic").prop('checked', data.all_traffic_via_warp || false);
$("#warp_popular_sites").prop('checked', data.popular_sites_via_warp || false);
$("#warp_domestic_sites").prop('checked', data.domestic_sites_via_warp || false);
$("#warp_block_adult_sites").prop('checked', data.block_adult_content || false);
$("#warp_initial_controls").hide();
$("#warp_active_controls").show();
},
error: function (xhr, status, error) {
let errorMsg = "Failed to fetch WARP configuration.";
if (xhr.responseJSON && xhr.responseJSON.detail) {
errorMsg = xhr.responseJSON.detail;
}
console.error("Error fetching WARP config:", errorMsg, xhr.responseText);
if (xhr.status === 404) {
$("#warp_initial_controls").show();
$("#warp_active_controls").hide();
$("#warp_config_form")[0].reset();
Swal.fire("Info", "WARP service might not be fully configured. Please try reinstalling if issues persist.", "info");
} else {
$("#warp_config_form")[0].reset();
Swal.fire("Warning", "Could not load current WARP configuration values. Please check manually or re-save.", "warning");
}
}
});
}
$("#warp_start_btn").on("click", function() {
confirmAction("install and start WARP", function () {
sendRequest(
"{{ url_for('install_warp') }}",
"POST",
null,
"WARP installation request sent. The page will reload.",
"#warp_start_btn",
true
);
});
});
$("#warp_stop_btn").on("click", function() {
confirmAction("stop and uninstall WARP", function () {
sendRequest(
"{{ url_for('uninstall_warp') }}",
"DELETE",
null,
"WARP uninstallation request sent. The page will reload.",
"#warp_stop_btn",
true
);
});
});
$("#warp_save_config_btn").on("click", function() {
const configData = {
all: $("#warp_all_traffic").is(":checked"),
popular_sites: $("#warp_popular_sites").is(":checked"),
domestic_sites: $("#warp_domestic_sites").is(":checked"),
block_adult_sites: $("#warp_block_adult_sites").is(":checked")
};
confirmAction("save WARP configuration", function () {
sendRequest(
"{{ url_for('configure_warp') }}",
"POST",
configData,
"WARP configuration saved successfully!",
"#warp_save_config_btn",
false,
fetchWarpFullStatusAndConfig
);
});
});
$("#telegram_start").on("click", startTelegram); $("#telegram_start").on("click", startTelegram);
@ -944,6 +1202,8 @@
$("#ip_limit_change_config").on("click", configIPLimit); $("#ip_limit_change_config").on("click", configIPLimit);
$("#decoy_setup").on("click", setupDecoy); $("#decoy_setup").on("click", setupDecoy);
$("#decoy_stop").on("click", stopDecoy); $("#decoy_stop").on("click", stopDecoy);
$("#obfs_enable_btn").on("click", enableObfs);
$("#obfs_disable_btn").on("click", disableObfs);
$('#normal_domain, #sni_domain, #decoy_domain').on('input', function () { $('#normal_domain, #sni_domain, #decoy_domain').on('input', function () {

60
menu.sh
View File

@ -384,18 +384,17 @@ warp_configure_handler() {
if systemctl is-active --quiet "$service_name"; then if systemctl is-active --quiet "$service_name"; then
echo -e "${cyan}=== WARP Status ===${NC}" echo -e "${cyan}=== WARP Status ===${NC}"
status_json=$(python3 $CLI_PATH warp-status) status_json=$(python3 $CLI_PATH warp-status)
all_traffic=$(echo "$status_json" | grep -o '"all_traffic_via_warp": *[^,}]*' | cut -d':' -f2 | tr -d ' "') all_traffic=$(echo "$status_json" | grep -o '"all_traffic_via_warp": *[^,}]*' | cut -d':' -f2 | tr -d ' "')
popular_sites=$(echo "$status_json" | grep -o '"popular_sites_via_warp": *[^,}]*' | cut -d':' -f2 | tr -d ' "') popular_sites=$(echo "$status_json" | grep -o '"popular_sites_via_warp": *[^,}]*' | cut -d':' -f2 | tr -d ' "')
domestic_sites=$(echo "$status_json" | grep -o '"domestic_sites_via_warp": *[^,}]*' | cut -d':' -f2 | tr -d ' "') domestic_sites_via_warp=$(echo "$status_json" | grep -o '"domestic_sites_via_warp": *[^,}]*' | cut -d':' -f2 | tr -d ' "')
block_adult=$(echo "$status_json" | grep -o '"block_adult_content": *[^,}]*' | cut -d':' -f2 | tr -d ' "') block_adult=$(echo "$status_json" | grep -o '"block_adult_content": *[^,}]*' | cut -d':' -f2 | tr -d ' "')
display_status() { display_status() {
local label="$1" local label="$1"
local status="$2" local status_val="$2"
if [ "$status" = "true" ]; then if [ "$status_val" = "true" ]; then
echo -e " ${green}${NC} $label: ${green}Enabled${NC}" echo -e " ${green}${NC} $label: ${green}Enabled${NC}"
else else
echo -e " ${red}${NC} $label: ${red}Disabled${NC}" echo -e " ${red}${NC} $label: ${red}Disabled${NC}"
@ -404,38 +403,49 @@ warp_configure_handler() {
display_status "All Traffic via WARP" "$all_traffic" display_status "All Traffic via WARP" "$all_traffic"
display_status "Popular Sites via WARP" "$popular_sites" display_status "Popular Sites via WARP" "$popular_sites"
display_status "Domestic Sites via WARP" "$domestic_sites" display_status "Domestic Sites via WARP" "$domestic_sites_via_warp"
display_status "Block Adult Content" "$block_adult" display_status "Block Adult Content" "$block_adult"
echo -e "${cyan}==================${NC}" echo -e "${cyan}==================${NC}"
echo echo
echo "Configure WARP Options:" echo "Configure WARP Options (Toggle):"
echo "1. Use WARP for all traffic" echo "1. All traffic via WARP"
echo "2. Use WARP for popular sites" echo "2. Popular sites via WARP"
echo "3. Use WARP for domestic sites" echo "3. Domestic sites (WARP/Reject)"
echo "4. Block adult content" echo "4. Block adult content"
echo "5. WARP Status Profile" echo "5. WARP Status Profile (IP etc.)"
echo "6. Change IP address" echo "6. Change WARP IP address"
echo "0. Cancel" echo "0. Cancel"
read -p "Select an option: " option read -p "Select an option to toggle: " option
case $option in case $option in
1) python3 $CLI_PATH configure-warp --all ;; 1)
2) python3 $CLI_PATH configure-warp --popular-sites ;; target_state=$([ "$all_traffic" = "true" ] && echo "off" || echo "on")
3) python3 $CLI_PATH configure-warp --domestic-sites ;; python3 $CLI_PATH configure-warp --set-all "$target_state" ;;
4) python3 $CLI_PATH configure-warp --block-adult-sites ;; 2)
target_state=$([ "$popular_sites" = "true" ] && echo "off" || echo "on")
python3 $CLI_PATH configure-warp --set-popular-sites "$target_state" ;;
3)
target_state=$([ "$domestic_sites_via_warp" = "true" ] && echo "off" || echo "on")
python3 $CLI_PATH configure-warp --set-domestic-sites "$target_state" ;;
4)
target_state=$([ "$block_adult" = "true" ] && echo "off" || echo "on")
python3 $CLI_PATH configure-warp --set-block-adult-sites "$target_state" ;;
5) 5)
ip=$(curl -s --interface wgcf --connect-timeout 0.5 http://v4.ident.me) current_ip=$(python3 $CLI_PATH warp-status | grep -o '"ip": *"[^"]*"' | cut -d':' -f2- | tr -d '" ')
if [ -z "$current_ip" ]; then
current_ip=$(curl -s --interface wgcf --connect-timeout 1 http://v4.ident.me || echo "N/A")
fi
cd /etc/warp/ && wgcf status cd /etc/warp/ && wgcf status
echo echo
echo -e "${yellow}Warp IP:${NC} ${cyan}$ip${NC}" echo -e "${yellow}Warp IP:${NC} ${cyan}${current_ip}${NC}"
;; ;;
6) 6)
old_ip=$(curl -s --interface wgcf --connect-timeout 0.5 http://v4.ident.me) old_ip=$(curl -s --interface wgcf --connect-timeout 1 http://v4.ident.me || echo "N/A")
echo -e "${yellow}Current IP:${NC} ${cyan}$old_ip${NC}" echo -e "${yellow}Current IP:${NC} ${cyan}$old_ip${NC}"
echo "Restarting $service_name..." echo "Restarting $service_name to attempt IP change..."
systemctl restart "$service_name" systemctl restart "$service_name"
echo -n "Waiting for service to restart" echo -n "Waiting for service to restart"
@ -445,18 +455,22 @@ warp_configure_handler() {
done done
echo echo
new_ip=$(curl -s --interface wgcf --connect-timeout 0.5 http://v4.ident.me) new_ip=$(curl -s --interface wgcf --connect-timeout 1 http://v4.ident.me || echo "N/A")
echo -e "${yellow}New IP:${NC} ${green}$new_ip${NC}" echo -e "${yellow}New IP:${NC} ${green}$new_ip${NC}"
if [ "$old_ip" != "$new_ip" ]; then if [ "$old_ip" != "N/A" ] && [ "$new_ip" != "N/A" ] && [ "$old_ip" != "$new_ip" ]; then
echo -e "${green}✓ IP address changed successfully${NC}" echo -e "${green}✓ IP address changed successfully${NC}"
else elif [ "$old_ip" = "$new_ip" ] && [ "$old_ip" != "N/A" ]; then
echo -e "${yellow}⚠ IP address remained the same${NC}" echo -e "${yellow}⚠ IP address remained the same${NC}"
else
echo -e "${red}✗ Could not verify IP change.${NC}"
fi fi
;; ;;
0) echo "WARP configuration canceled." ;; 0) echo "WARP configuration canceled." ;;
*) echo -e "${red}Invalid option. Please try again.${NC}" ;; *) echo -e "${red}Invalid option. Please try again.${NC}" ;;
esac esac
# echo "Command sent. Check status again to see changes."
else else
echo -e "${red}$service_name is not active. Please start the service before configuring WARP.${NC}" echo -e "${red}$service_name is not active. Please start the service before configuring WARP.${NC}"
fi fi