本文主要研究某国产Linux操作系统无法使用nodejs程序重命名指定目录文件的问题。
起因
最近遇到一个业务程序迁移不同操作系统的问题:该业务程序使用nodejs语言编写的web应用,并且要设置为开机自启动。运行过程正常,网页正常打开,但当设置并保存网卡信息到/etc
目录指定文件时,就提示权限不足。
排查及解决
问题表现
该nodejs程序在开机自动启动后,使用浏览器访问相应端口,保存参数信息,错误信息如下:
Error: EACCES: permission denied, rename '/etc/network/interfaces.d/eth0.tmp' -> '/etc/network/interfaces.d/eth0' at Object.renameSync (node:fs:1026:3) at saveConfig
但是,在命令行终端下,停掉进程,再手动启动,保存过程十分顺利。
尝试解决
分析代码,保存参数的过程,是将配置参数写到一个.tmp
的临时文件,再将其重命名为原文件,如此完成保存的操作。此处不评价这种做法优劣,因为已经在生产环境(x86平台)中用着了。
从错误信息看,问题出在fs.renameSync
函数。从错误的结果反推,尝试了几个方法。既然无法使用API函数解决,则其它方式,比如使用child_process
模块,在nodejs中调用shell,即执行mv xx.tmp xx
命令,但失败。既然是/etc
目录的权限问题,那好,修改/etc/network/interfaces.d/
权限为777
,但失败。
进一步分析,如果真的是权限问题,为何能够创建xx.tmp
文件,且内容是正确的?如果不是权限问题,为何无法将其重命名为原文件?
检查过权限和用户组,是正常的。因为普通用户在命令行终端手动执行程序,是正常的,但开机自启动却失败。下面分别是手动启动和开机启动的进程状态:
latelee 35811 7.5 0.2 590640 43696 pts/1 Sl 10:18 0:00 node /server_test.js
latelee 17573 0.0 0.2 624000 45728 ? Sl 12:28 0:00 node /server_test.js
从结果看,手动启动的那一项多了pts/1
,这是没毛病的,因为就是在终端里启动的。
解决方法
后来因其它任务中断,此事不再继续。经过一段较长时间后,本着问题是自己的,始终要解决的精神,重拾起来。
回想想先前在安装该操作系统时,遇到一些权限问题,如用ssh远程连接后,执行脚本会卡住跑不起来(经查,是因为安全机制阻止了非法应用程序执行);明明监听了某端口,但外部机器无法访问(经查,是因为防火墙阻止了端口的访问)。
于是往这方向想,终于搜索到一个方法,在命令终端执行:
$ sudo setstatus softmode -p
设置后,再进行测试,参数保存成功。
原来问题出现操作系统的安全保存措施上。
现场重演
下面模拟实际工程关于保存配置文件的关键代码,并做测试。
模拟代码
var express = require('express');
var path = require('path');
const fs = require('fs');
const moment = require("moment");
var g_port = 5000
var testnetfile="/etc/network/interfaces.d/eth0_test"
var g_netstr = ""
function readMyFile(filename, ret) {
var tmpstr = fs.readFileSync(filename, "utf8");
g_netstr = tmpstr;
writeToLog("read net file " + filename + " ret: \r\n" + g_netstr)
return ret;
}
function saveToFile(filename, str) {
var tempfile = filename + ".tmp";
fs.writeFileSync(tempfile, str);
fs.renameSync(tempfile, filename);
}
function testRename(filename) {
var ret = {};
ret = readMyFile(filename, ret)
interfaces = ret
str = g_netstr + "\n"
str += moment().format("YYYY-MM-DDTHH:mm:ss") + " change by me" + "\n"
saveToFile(filename, str)
}
///
var app = express();
app.get('/', function (req, res) {
res.render("index.html");
})
app.get('/test', function (req, res) {
writeToLog("================start test rename=================")
if (req.query.test_num == 1) {
testRename(testnetfile)
}
g_netstr = "";
g_netstr = fs.readFileSync(testnetfile, "utf8")
writeToLog("after rename \r\n" + g_netstr)
var response = {
"result": g_netstr,
};
// console.log(response);
res.end(JSON.stringify(response));
})
app.set('views', path.join(__dirname, '.'));
app.engine('html', require('ejs').renderFile);
app.set('view engine', 'html');
var server = app.listen(g_port, function () {
var port = server.address().port
console.log("listening on %s", port)
})
function writeToLog(log) {
const datestr = moment().format("YYYY-MM-DDTHH:mm:ss");
logstr = "[" + datestr+ "] " + log;
console.log(logstr);
fs.appendFile('node_log.txt', logstr + '\n', (err) => {
if (err) {
// console.error('写入日志失败', err);
} else {
// console.log('日志写入成功');
}
});
}
启动脚本
编写执行脚本/home/latelee/mystart.sh
:
#!/bin/bash
LOGFILE=/tmp/mystart_log.txt
cd /home/latelee/plat_test/node_server_test
date >> $LOGFILE
echo "begin start nodejs test" >> $LOGFILE
#node ./server.js &
node /home/latelee/plat_test/node_server_test/server_test.js &
脚本要添加可执行属性:
chmod +x /home/latelee/mystart.sh
否则启动时会提示:
mystart.service: Failed at step EXEC spawning /home/latelee/mystart.sh: Permission denied
开机自启动
编写systemd
启动所需要的配置文件/etc/systemd/system/mystart.service
:
[Unit]
Description=Start my demo after tty1 is ready , in case of interrupting plymouth, that cause cannot enter into tty1
[Service]
Type=forking
User=latelee
ExecStart=/home/latelee/mystart.sh &
#ExecStartPre=/home/latelee/mystart_pre.sh &
#ExecStartPost=/home/latelee/mystart_after.sh &
[Install]
WantedBy=multi-user.target
使能之:
$ sudo -s systemctl enable mystart
Created symlink /etc/systemd/system/multi-user.target.wants/mystart.service → /etc/systemd/system/mystart.service.
注意,配置文件里可以指定多个执行脚本,按先后顺序有ExecStartPre
,ExecStart
、ExecStartPost
。一般用ExecStart
即可,如果的确有多个依赖且有先后顺序的脚本,则配置文件里写的启动脚本一般要存在,否则不能正常启动,提示如下:
mystart.service: Failed to execute command: No such file or directory
mystart.service: Failed at step EXEC spawning /home/latelee/mystart_pre.sh: No such file or directory
重启机器后,查看启动结果:
latelee@latelee-pc:~$ systemctl status mystart
● mystart.service - Start my demo after tty1 is ready , in case of interrupting plymouth, that cause cannot enter into tty1
Loaded: loaded (/etc/systemd/system/mystart.service; enabled; vendor preset: enabled)
Active: active (running) since Thu 2024-11-21 12:01:33 CST; 1min 45s ago
Main PID: 939 (node)
Tasks: 7 (limit: 19233)
Memory: 72.3M
CGroup: /system.slice/mystart.service
└─939 node /home/latelee/plat_test/node_server_test/server_test.js
11月 21 12:01:33 latelee-pc systemd[1]: Starting Start my demo after tty1 is ready , in case of interrupting plymouth, that cause cannot enter into tty1...
11月 21 12:01:33 latelee-pc systemd[1]: Started Start my demo after tty1 is ready , in case of interrupting plymouth, that cause cannot enter into tty1.
11月 21 12:01:34 latelee-pc mystart.sh[939]: listening on 5000
测试
在网页上保存参数后,网页提示:
Error: EACCES: permission denied, rename '/etc/network/interfaces.d/eth0_test.tmp' -> '/etc/network/interfaces.d/eth0_test' at Object.renameSync (node:fs:1026:3) at saveToFile (/home/latelee/plat_test/node_server_test/server_test.js:23:6) at testRename (/home/latelee/plat_test/node_server_test/server_test.js:33:5) at Object.handle (/home/latelee/plat_test/node_server_test/server_test.js:48:9) at next_layer (/home/latelee/plat_test/node_server_test/node_modules/express/lib/router/route.js:103:13) at Route.dispatch (/home/latelee/plat_test/node_server_test/node_modules/express/lib/router/route.js:107:5) at /home/latelee/plat_test/node_server_test/node_modules/express/lib/router/index.js:195:24 at Function.proto.process_params (/home/latelee/plat_test/node_server_test/node_modules/express/lib/router/index.js:251:12) at next (/home/latelee/plat_test/node_server_test/node_modules/express/lib/router/index.js:189:19) at next (/home/latelee/plat_test/node_server_test/node_modules/express/lib/router/index.js:166:38)
可以重现上述问题。
麒麟桌面操作系统安全机制略记
本文使用的操作系统是麒麟桌面版,该系统有kysec
安全机制,默认启动,此时,不能执行自定义脚本、自定义程序,外部不能访问自定义端口。可以通过图形界面修改,或用setstatus
设置。
$ setstatus
usage: setstatus < disable | enable | softmode >
setstatus < disable | enable | softmode > -p (if kysec status is not disabled)
setstatus -f < exectl | netctl > < off | enforcing | warning > [-p]
setstatus -f < fpro | kmod | ppro | pblk | devctl | ipt> < off | on > [-p]
setstatus -f < eperm > < off | on > [-p]
setstatus -f < kid > < off | partition | disable_privacy > [-p]
其中,-p
即permanent
,表示永久生效。第一行并列的disable | enable | softmode
我有点想不通,disable
和enable
已经是一事两面,符合结构化思维的分类原则了,但又多了个softmode
,不过的确可以用softmode
解决问题。
比如无法使用hwclock
命令写硬件时间,可以如下设置:
$ sudo setstatus -f kid off -p
比如解决本文遇到的无法重命名的问题,可以如下设置:
$ sudo setstatus softmode -p
顺着这个思路,最终找到/etc/default/grub
文件,可以将
GRUB_CMDLINE_LINUX_SECURITY="security=kysec"
改为
GRUB_CMDLINE_LINUX_SECURITY="security= "
即,把kysec
这个字符,改为空格,注意是空格。然后执行sudo update-grub
更新initrd
镜像。不过动到内核的时候,还是小心为上。要长期测试系统是否稳定。
国内主流桌面版系统都有权限管理、安全措施,对于日常使用其实是足够的。由于笔者所涉及的是稍底层,且要动到操作系统的,因为不合适。至于为何不用嵌入式版,可能是因为当时从官站找镜像时,无意忽略掉,就用桌面版了。
小结
由特定语言编写的特定场景的问题引发出操作系统安全机制,搞了十几年Linux了,似乎是第一次遇到。主要是因为问题表现较奇特,如果是一刀切那样就好办,像这次,能够写入新文件,在终端执行正常,这种情况就是我的知识荒原了。虽然花费的时间较长,但加深了国产系统的一些认知,耗时有所值。