diff --git a/README.md b/README.md index 7bd658b..2a861f9 100644 --- a/README.md +++ b/README.md @@ -75,30 +75,22 @@ gunzip clash.gz chmod +x clash ``` -## Step 8: Edit Configuration File -Edit the `config.yaml` example file. +## Step 8: Enable Clash +Enable the Clash service. ```bash -vi /opt/clash/config.yaml -``` - -## Step 9: Set Up an SFTP Server (Optional) -For easier editing of files, set up an SFTP server. - -```bash -opkg install openssh-sftp-server +/etc/init.d/clash enable ``` -## Step 10: Enable and Start Clash -Enable and start the Clash service. +## Step 9: Managing Clash from LUCI interface +I've written a simple interface for managing Clash from LUCI interface `luci-app-ssclash`. Edit Clash config and Apply. -```bash -/etc/init.d/clash enable -/etc/init.d/clash start -``` +

+ +

-## Step 11: Access Web UI -You can access the Clash Web UI at: +## Step 10: You can access to Dashboard from LUCI interface or manual +You can access the Dashboard at: ``` http://ROUTER_IP:9090/ui/ @@ -113,8 +105,11 @@ To remove Clash, stop the service, delete the related files and kernel module `k ```bash /etc/init.d/clash stop -rm -rf /etc/init.d/clash +rm -f /etc/init.d/clash rm -rf /opt/clash +rm -f /usr/share/luci/menu.d/luci-app-ssclash.json +rm -f /usr/share/rpcd/acl.d/luci-app-ssclash.json +rm -rf /www/luci-static/resources/view/ssclash ``` --- diff --git a/rootfs/etc/init.d/clash b/rootfs/etc/init.d/clash index 1588a77..0025c04 100755 --- a/rootfs/etc/init.d/clash +++ b/rootfs/etc/init.d/clash @@ -5,41 +5,71 @@ STOP=89 USE_PROCD=1 +FLAG_FILE="/tmp/dnsmasq_once" + +msg() { + logger -p daemon.info -st "clash[$$]" "$*" +} + start_service() { - # The ruleset folder is used from tmpfs - if [ ! -d /tmp/clash/ruleset ]; then - mkdir -p /tmp/clash/ruleset - fi - if [ ! -L /opt/clash/ruleset ] || [ "$(readlink /opt/clash/ruleset)" != "/tmp/clash/ruleset" ]; then - rm -rf /opt/clash/ruleset - ln -s /tmp/clash/ruleset /opt/clash/ruleset - fi - - procd_open_instance - procd_set_param command /opt/clash/bin/clash -d /opt/clash - procd_set_param respawn - procd_close_instance - - uci add_list dhcp.@dnsmasq[0].server='127.0.0.1#7874' - uci set dhcp.@dnsmasq[0].cachesize='0' - uci set dhcp.@dnsmasq[0].noresolv='1' - uci commit - - /opt/clash/bin/clash-rules start - /etc/init.d/dnsmasq restart + # The ruleset folder is used from tmpfs + if [ ! -d /tmp/clash/ruleset ]; then + mkdir -p /tmp/clash/ruleset + msg "The folder '/tmp/clash/ruleset' was created" + fi + if [ ! -L /opt/clash/ruleset ] || [ "$(readlink /opt/clash/ruleset)" != "/tmp/clash/ruleset" ]; then + rm -rf /opt/clash/ruleset + ln -s /tmp/clash/ruleset /opt/clash/ruleset + msg "Created a symlink from '/tmp/clash/ruleset' to '/opt/clash/ruleset'" + fi + + procd_open_instance + procd_set_param command /opt/clash/bin/clash -d /opt/clash + procd_set_param stdout 1 + procd_set_param stderr 1 + procd_set_param respawn + procd_close_instance + msg "Clash instance is started" + + uci add_list dhcp.@dnsmasq[0].server='127.0.0.1#7874' + uci set dhcp.@dnsmasq[0].cachesize='0' + uci set dhcp.@dnsmasq[0].noresolv='1' + uci commit dhcp + msg "DNS settings have been changed" + + /opt/clash/bin/clash-rules start + msg "Firewall rules applied" + + /etc/init.d/dnsmasq restart > /dev/null 2>&1 + msg "dnsmasq restarted" } stop_service() { - uci del dhcp.@dnsmasq[0].server - uci del dhcp.@dnsmasq[0].cachesize - uci del dhcp.@dnsmasq[0].noresolv - uci commit + msg "Clash instance has been stopped" + uci del dhcp.@dnsmasq[0].server + uci del dhcp.@dnsmasq[0].cachesize + uci del dhcp.@dnsmasq[0].noresolv + uci commit dhcp + msg "DNS settings have been restored" + + /opt/clash/bin/clash-rules stop + msg "Firewall rules have been restored" + + if [ ! -f "$FLAG_FILE" ]; then + /etc/init.d/dnsmasq restart > /dev/null 2>&1 + msg "dnsmasq restarted" + fi - /opt/clash/bin/clash-rules stop - /etc/init.d/dnsmasq restart + rm -f "$FLAG_FILE" +} + +reload_service() { + touch "$FLAG_FILE" + stop + start } boot() { - sleep 10 - start -} \ No newline at end of file + sleep 10 + start +} diff --git a/rootfs/usr/share/luci/menu.d/luci-app-ssclash.json b/rootfs/usr/share/luci/menu.d/luci-app-ssclash.json new file mode 100644 index 0000000..ef62b67 --- /dev/null +++ b/rootfs/usr/share/luci/menu.d/luci-app-ssclash.json @@ -0,0 +1,28 @@ +{ + "admin/services/ssclash": { + "title": "ssclash", + "action": { + "type": "alias", + "path": "admin/services/ssclash/config" + }, + "depends": { + "acl": [ "luci-app-ssclash" ] + } + }, + "admin/services/ssclash/config": { + "title": "Config", + "order": 10, + "action": { + "type": "view", + "path": "ssclash/config" + } + }, + "admin/services/ssclash/log": { + "title": "Log", + "order": 20, + "action": { + "type": "view", + "path": "ssclash/log" + } + } +} diff --git a/rootfs/usr/share/rpcd/acl.d/luci-app-ssclash.json b/rootfs/usr/share/rpcd/acl.d/luci-app-ssclash.json new file mode 100644 index 0000000..a69e30a --- /dev/null +++ b/rootfs/usr/share/rpcd/acl.d/luci-app-ssclash.json @@ -0,0 +1,26 @@ +{ + "luci-app-ssclash": { + "description": "Grant access to Clash procedures", + "read": { + "file": { + "/opt/clash/config.yaml": [ "read" ], + "/sbin/logread": [ "exec" ] + }, + "ubus": { + "file": [ "read" ], + "service": [ "list" ] + } + }, + "write": { + "file": { + "/opt/clash/config.yaml": [ "write" ], + "/etc/init.d/clash start": [ "exec" ], + "/etc/init.d/clash stop": [ "exec" ], + "/etc/init.d/clash reload": [ "exec" ] + }, + "ubus": { + "file": [ "write" ] + } + } + } +} diff --git a/rootfs/www/luci-static/resources/view/ssclash/config.js b/rootfs/www/luci-static/resources/view/ssclash/config.js new file mode 100644 index 0000000..01aa035 --- /dev/null +++ b/rootfs/www/luci-static/resources/view/ssclash/config.js @@ -0,0 +1,112 @@ +'use strict'; +'require view'; +'require fs'; +'require ui'; +'require rpc'; + +var isReadonlyView = !L.hasViewPermission() || null; +let startStopButton = null; + +const callServiceList = rpc.declare({ + object: 'service', + method: 'list', + params: ['name'], + expect: { '': {} } +}); + +async function getServiceStatus() { + try { + return Object.values((await callServiceList('clash'))['clash']['instances'])[0]?.running; + } catch (ignored) { + return false; + } +} + +async function startService() { + if (startStopButton) startStopButton.disabled = true; + return fs.exec('/etc/init.d/clash', ['start']) + .catch(function(e) { + ui.addNotification(null, E('p', _('Unable to start service: %s').format(e.message)), 'error'); + }) + .finally(() => { + if (startStopButton) startStopButton.disabled = false; + }); +} + +async function stopService() { + if (startStopButton) startStopButton.disabled = true; + return fs.exec('/etc/init.d/clash', ['stop']) + .catch(function(e) { + ui.addNotification(null, E('p', _('Unable to stop service: %s').format(e.message)), 'error'); + }) + .finally(() => { + if (startStopButton) startStopButton.disabled = false; + }); +} + +async function toggleService() { + const running = await getServiceStatus(); + if (running) { + await stopService(); + } else { + await startService(); + } + window.location.reload(); +} + +async function openDashboard() { + let newWindow = window.open('', '_blank'); + const running = await getServiceStatus(); + if (running) { + let url = `http://${window.location.hostname}:9090/ui/?hostname=${window.location.hostname}&port=9090`; + newWindow.location.href = url; + } else { + newWindow.close(); + alert(_('Service is not running.')); + } +} + +return view.extend({ + load: function() { + return L.resolveDefault(fs.read('/opt/clash/config.yaml'), ''); + }, + handleSaveApply: function(ev) { + var value = (document.querySelector('textarea').value || '').trim().replace(/\r\n/g, '\n') + '\n'; + return fs.write('/opt/clash/config.yaml', value).then(function(rc) { + document.querySelector('textarea').value = value; + ui.addNotification(null, E('p', _('Contents have been saved.')), 'info'); + return fs.exec('/etc/init.d/clash', ['reload']); + }).then(function() { + window.location.reload(); + }).catch(function(e) { + ui.addNotification(null, E('p', _('Unable to save contents: %s').format(e.message)), 'error'); + }); + }, + render: async function(config) { + const running = await getServiceStatus(); + + return E([ + E('button', { + 'class': 'btn', + 'click': openDashboard + }, _('Open Dashboard')), + (startStopButton = E('button', { + 'class': 'btn', + 'click': toggleService, + 'style': 'margin-left: 10px;' + }, running ? _('Stop service') : _('Start service'))), + E('span', { + 'style': running ? 'color: green; margin-left: 10px;' : 'color: red; margin-left: 10px;' + }, running ? _('Clash is running') : _('Clash stopped')), + E('h2', _('Clash config')), + E('p', { 'class': 'cbi-section-descr' }, _('Your current Clash config. When applied, the changes will be saved and the service will be restarted.')), + E('textarea', { + 'style': 'width: 100% !important; padding: 5px; font-family: monospace', + 'rows': 35, + 'disabled': isReadonlyView + }, [config != null ? config : '']) + ]); + }, + handleSave: null, + handleReset: null +}); diff --git a/rootfs/www/luci-static/resources/view/ssclash/log.js b/rootfs/www/luci-static/resources/view/ssclash/log.js new file mode 100644 index 0000000..605bde1 --- /dev/null +++ b/rootfs/www/luci-static/resources/view/ssclash/log.js @@ -0,0 +1,63 @@ +'use strict'; +'require view'; +'require poll'; +'require fs'; + +return view.extend({ + load: function () { + return fs.stat('/sbin/logread'); + }, + + render: function (stat) { + const loggerPath = stat && stat.path ? stat.path : null; + + poll.add(() => { + if (loggerPath) { + return fs.exec_direct(loggerPath, ['-e', 'clash']) + .then(res => { + const log = document.getElementById('logfile'); + // Without log processing + // log.value = res ? res.trim() : _(''); + // Without log processing + // With log processing + if (res) { + const processedLog = res.trim().split('\n').map(line => { + const msgMatch = line.match(/msg="(.*?)"/); + if (msgMatch) { + return line.split(']: ')[0] + ']: ' + msgMatch[1]; + } + return line; + }).join('\n'); + + log.value = processedLog; + } else { + log.value = _(''); + } + // With log processing + log.scrollTop = log.scrollHeight; + }) + .catch(err => { + console.error('Error executing logread:', err); + }); + } + }); + + return E( + 'div', + { class: 'cbi-map' }, + E('div', { class: 'cbi-section' }, [ + E('textarea', { + id: 'logfile', + style: 'width: 100% !important; padding: 5px; font-family: monospace', + readonly: 'readonly', + wrap: 'off', + rows: 35, + }), + ]) + ); + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null, +}); diff --git a/scr-00.png b/scr-00.png new file mode 100644 index 0000000..7652526 Binary files /dev/null and b/scr-00.png differ