Skip to content

Commit

Permalink
Wrote a simple interface to control Clash from the LUCI interface
Browse files Browse the repository at this point in the history
luci-app-ssclash
  • Loading branch information
zerolabnet committed Aug 15, 2024
1 parent 1c20c1e commit 35847de
Show file tree
Hide file tree
Showing 7 changed files with 303 additions and 49 deletions.
33 changes: 14 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
<p align="center">
<img src="scr-00.png" width="100%">
</p>

## 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/
Expand All @@ -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
```

---
Expand Down
90 changes: 60 additions & 30 deletions rootfs/etc/init.d/clash
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
sleep 10
start
}
28 changes: 28 additions & 0 deletions rootfs/usr/share/luci/menu.d/luci-app-ssclash.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
26 changes: 26 additions & 0 deletions rootfs/usr/share/rpcd/acl.d/luci-app-ssclash.json
Original file line number Diff line number Diff line change
@@ -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" ]
}
}
}
}
112 changes: 112 additions & 0 deletions rootfs/www/luci-static/resources/view/ssclash/config.js
Original file line number Diff line number Diff line change
@@ -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
});
63 changes: 63 additions & 0 deletions rootfs/www/luci-static/resources/view/ssclash/log.js
Original file line number Diff line number Diff line change
@@ -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,
});
Binary file added scr-00.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 35847de

Please sign in to comment.