Node. js supports multi-user web Terminal Implementation and security solutions, and node. js supports multiple users
As a common feature of local IDE, terminal (command line) supports git operations and file operations of projects. For WebIDE, without a web pseudo-terminal, only the encapsulated command line interface is completely insufficient for developers to use. Therefore, for a better user experience, the development of web pseudo terminals is also on the agenda.
Research
Terminals are slightly the same as command line tools in our cognitive scope. In other words, shell processes can be executed. Each time you enter a command string in the command line and press enter, the terminal process fork a sub-process to execute the input command. The terminal process calls wait4 () through the system () listen to the sub-process to exit and output the sub-process execution information through the exposed stdout.
If you implement a terminal function similar to Localization on the web end, you may need to do more: network latency and reliability assurance, shell user experience as close as possible to localization, web terminal UI width and output information adaptation, security access control and permission management. Before implementing a web terminal, you need to evaluate the core features of these functions. It is clear that: the implementation, user experience, and security of shell functions (web terminals are a function provided by online servers, so security must be ensured ). Web pseudo terminals can be officially launched only when these two functions are ensured.
The following describes how to implement these two functions (nodejs is used for server-side technologies ):
The node native module provides the repl module, which can be used for interactive Input and Output execution. It also provides the tab complementing function and custom output style functions. However, it can only execute node-related commands, therefore, the node native module child_porcess cannot achieve the purpose of executing the system shell. It provides the spawn function that encapsulates the underlying libuv uv_spawn, and the underlying execution system calls fork and execvp to execute shell commands. However, it does not provide other features of the Pseudo Terminal, such as tab auto-completion and arrow key display commands.
Therefore, the server cannot implement a Pseudo Terminal using the native module of node. Therefore, we need to continue to explore the principles of the Pseudo Terminal and the implementation direction of the node.
Pseudo Terminal
A Pseudo Terminal is not a real terminal, but a "service" provided by the kernel ". Terminal Services generally include three layers:
The underlying hardware driver of the line procedure (line discipline) that is provided at the top layer of the input/output interface provided to character Devices
Among them, the top-level interface is often implemented through System Call functions, such as (read, write); while the underlying hardware driver is responsible for the communication between the master and slave devices of the Pseudo Terminal, which is provided by the kernel; the line procedure looks abstract, but in fact it is functionally responsible for the "processing" of input and output information, for example, handling the interrupted characters in the input process (ctrl + c) and some rollback characters (backspace and delete), while converting the output line break n to rn.
A pseudo-terminal is divided into two parts: the main device and the slave device, which are two-way pipe connections (hardware drivers) by implementing the default line rules at the underlying layer ). Any input of the pseudo-terminal master device is reflected on the slave device, and vice versa. The output information from the device is also sent to the main device through an MPS queue. In this way, shell can be executed from the device on the Pseudo Terminal to complete the terminal's functions.
The Pseudo Terminal can simulate the tab population of the terminal and other shell special commands from the device. Therefore, if the node native module cannot meet the requirements, we need to focus on the underlying layer to see what functions the OS provides. Currently, the glibc Library provides the posix_openpt interface, but the process is cumbersome:
Use posix_openpt to open a pseudo-terminal master device grantpt and set the permission to unlockpt from the device to unlock the corresponding master device to get the read/write from the device name (like/dev/pts/123) from the device, perform operations
Therefore, a better pty library is encapsulated. You can use only one forkpty function to implement all the above functions. Write a node c ++ extension module and use the pty library to implement a terminal for Executing command lines from the device on a Pseudo terminal.
We will discuss the security of pseudo terminals at the end of this article.
Implementation of pseudo Terminals
Based on the characteristics of the master and slave devices of the Pseudo Terminal, we manage the lifecycle and resources of the Pseudo Terminal in the parent process where the master device is located, and execute shell in the sub-process where the device is located, information and results during execution are transmitted to the master device through a two-way pipeline, and stdout is provided to the process where the master device is located.
The following describes how to implement pty. js:
pid_t pid = pty_forkpty(&master, name, NULL, &winp);
switch (pid) {
case -1:
return Nan::ThrowError("forkpty(3) failed.");
case 0:
if (strlen(cwd)) chdir(cwd);
if (uid != -1 && gid != -1) {
if (setgid(gid) == -1) {
perror("setgid(2) failed.");
_exit(1);
}
if (setuid(uid) == -1) {
perror("setuid(2) failed.");
_exit(1);
}
}
pty_execvpe(argv[0], argv, env);
perror("execvp(3) failed.");
_exit(1);
default:
if (pty_nonblock(master) == -1) {
return Nan::ThrowError("Could not set master fd to nonblocking.");
}
Local<Object> obj = Nan::New<Object>();
Nan::Set(obj,
Nan::New<String>("fd").ToLocalChecked(),
Nan::New<Number>(master));
Nan::Set(obj,
Nan::New<String>("pid").ToLocalChecked(),
Nan::New<Number>(pid));
Nan::Set(obj,
Nan::New<String>("pty").ToLocalChecked(),
Nan::New<String>(name).ToLocalChecked());
pty_baton *baton = new pty_baton();
baton->exit_code = 0;
baton->signal_code = 0;
baton->cb.Reset(Local<Function>::Cast(info[8]));
baton->pid = pid;
baton->async.data = baton;
uv_async_init(uv_default_loop(), &baton->async, pty_after_waitpid);
uv_thread_create(&baton->tid, pty_waitpid, static_cast<void*>(baton));
return info.GetReturnValue().Set(obj);
}
First, use pty_forkpty (posix Implementation of forkpty, compatible with sunOS and unix systems) to create a Master/Slave Device, and then set the permissions (setuid and setgid) in the sub-process ), the execution system calls pty_execvpe (the encapsulation of execvpe), and the input information of the master device will be executed here (the file executed by the sub-process is sh and stdin will be listened );
The parent process exposes related objects to the node layer, such as the fd of the master device. socket object for two-way data transmission) and register the libuv Message Queue & baton-> async. When the sub-process exits, the & baton-> async message is triggered and the pty_after_waitpid function is executed;
Finally, the parent process creates a sub-process by calling uv_thread_create to listen on the exit message of the previous sub-process (by calling wait4 by executing the system, blocking the process listening for a specific pid, exit information is stored in the third parameter). The pty_waitpid function encapsulates the wait4 function and executes uv_async_send (& baton-> async) trigger message at the end of the function.
After the pty model is implemented at the underlying layer, stdio operations are required at the node layer. The pseudo-terminal master device is created to execute system calls in the parent process, and the file descriptor of the master device is exposed to the node layer through fd, then, the input and output of the Pseudo Terminal are completed by creating the corresponding FILE types such as PIPE and FILE based on fd. In fact, at the OS level, the master device of the Pseudo Terminal is regarded as a PIPE for two-way communication. In the node layer. socket (fd) creates a Socket to implement bidirectional IO of data streams. The slave device of the Pseudo Terminal also has the same input as the master device, so as to execute the corresponding commands in the sub-process, the output of the sub-process is also reflected in the master device through PIPE, which triggers the data event of the node-layer Socket object.
The descriptions of the parent process, master device, sub-process, and input and output from the device are confusing. The relationship between the parent process and the master device is as follows: the parent process creates the master device (which can be viewed as a PIPE) through a system call and obtains the fd of the master device. The parent process creates the connect socket of the fd to implement input and output to the sub-process (slave device. After the sub-process is created through forkpty, it executes the login_tty operation and resets the stdin, stderr, and stderr of the sub-process, all of which are copied to the fd (the other end of PIPE) of the sub-process ). Therefore, the input and output of a sub-process are associated with the fd of the slave device. The output data of the sub-process is PIPE, and the command of the parent process is read from PIPE. For more information, see the document forkpty implementation.
In addition, the pty Library provides the size setting of the Pseudo Terminal, so we can adjust the layout information of the Pseudo Terminal output information through parameters, therefore, this function allows you to adjust the width of a command line on the web. You only need to set the Pseudo Terminal window size on the pty layer. The window is a character unit.
Web terminal security assurance
Based on the pty library provided by glibc to implement the Pseudo Terminal background, there is no security guarantee. We want to directly operate a directory on the server through a web terminal, but the root permission can be directly obtained through the Pseudo Terminal background, which is intolerable for the service, because it directly affects the security of the server, all need to implement one: multiple users can be online at the same time, access permissions can be configured for each user, access to a specific directory, bash commands can be configured, users are isolated from each other, users are unaware of the current environment, and the Environment is simple and easy to deploy "system ".
The most suitable technology selection is docker. As a kernel-level isolation, it can make full use of hardware resources and easily map the files related to the host machine. But docker is not omnipotent. If the program runs in a docker container, it will become much more complicated to assign a container to every user, and it will not be controlled by O & M personnel, this is the so-called DooD (docker out of docker)-use docker commands on the host machine through binary files such as volume "/usr/local/bin/docker, enable the sibling image to run the build service. Using the docker-in-docker mode, which is frequently discussed in the industry, has many disadvantages, especially at the file system level, which can be found in references. Therefore, docker technology is not suitable for services already running in containers to solve user access security issues.
Next, we need to consider the solution on a single machine. Currently, I only come up with two solutions:
Command ACL: Implements restricted bash chroot through the command whitelist, creates a system user for each user, and cuts the user access range.
First, the command whitelist method should be excluded. First, the bash of linux with different release cannot be guaranteed to be the same. Second, it cannot effectively list all commands; finally, the tab Command provided by the pseudo-terminal is fully functional and the existence of special characters, such as delete, cannot effectively match the current command. Therefore, too many White List vulnerabilities are allowed.
Restricted bash, triggered by/bin/bash-r, can restrict the explicit "cd directory" of users, but there are many disadvantages:
Not enough to allow the execution of completely untrusted software. When a command found to be a shell script is executed, rbash will close any restrictions generated in the shell to execute the script. When users run bash or dash from rbash, they get an unrestricted shell. There are many ways to break the restricted bash shell, which is not easy to predict.
Finally, there seems to be only one solution, that is, chroot. Chroot modifies the user's root directory and runs commands under the specified root directory. The directory cannot be jumped out of the specified root directory, so it cannot access all directories of the original system. At the same time, chroot creates a system directory structure isolated from the original system, therefore, the commands of the original system cannot be used in the "New System" because they are completely new and empty. Finally, they are isolated and transparent when used by multiple users, fully meet our needs.
Therefore, we finally chose chroot as the security solution for web terminals. However, the use of chroot requires a lot of additional processing, including not only the creation of new users, but also the initialization of commands. As mentioned above, the "New System" is empty and does not have any executable binary files, such as "ls, pmd". Therefore, it is necessary to initialize the "New System. However, many binary files not only link many libraries statically, but also depend on the dynamic link library (dll) at runtime. Therefore, you need to find many dll dependencies for each command, which is very tedious. To help users get rid of this boring process, jailkit came into being.
Jailkit, really easy to use
Jailkit, as its name implies, is used to block users. Jailkit uses chroot internally to create user root directories, and provides a series of commands to initialize and copy binary files and all their dll files. These functions can be operated through the configuration file. Therefore, in actual development, jailkit and initialization shell scripts are used to implement File System isolation.
The initialization shell here refers to the pre-processing script. Because chroot needs to set the root directory for each user, the shell creates the corresponding user for each user who opens the command line permission, use the jailkit configuration file to copy basic binary files and Their dll files, such as basic shell commands, git, vim, and ruby. Then, perform additional processing on some commands and reset permissions.
You still need some skills to process the file ing between the new system and the original system. I used to establish a ing between directories other than the user root directory set by chroot through soft links. However, when accessing soft links in jail Prison, an error still occurs and the file cannot be found, this is due to the chroot feature. You are not authorized to access the file system outside the root directory. If you create a ing through a hard link, you can modify the hard-link files in the user root directory set by chroot. However, operations such as deletion and creation cannot be correctly mapped to the original system directory, in addition, hard links cannot connect to directories, so hard links do not meet requirements. Finally, they are implemented through mount -- bind, for example, mount -- bind/home/ttt/abc/usr/local/abc blocks the mounted directory (/usr/local/abc ), maintain the ing between the mounted directory and the mounted directory in the memory, access to/usr/local/abc will query/home/ttt/abc blocks in the memory ing table, and then perform operations to implement directory ing.
Finally, after initializing the "New System", run the jail command through the Pseudo Terminal:
Sudo jk_chrootlaunch-j/usr/local/jailuser/$ {creater}-u $ {creater}-x/bin/bashr
After the bash program is enabled, PIPE can communicate with the web terminal input received by the master device (through websocket.
End
Overall Design (only list the processing diagrams of a single service process on a single machine, and ignore the front-end nodes on the server ):