0
votes

I am considering writing a BBS-like program in C and thinking about exactly how the I/O architecture would work with such a program. I'm familiar with sockets programming already, more specifically the master/remote model (not sure if there's a more official name for it) where a master process running as a daemon runs the vast majority of the application in a main process. When remote TTYs connect, they do so in a separate process that communicates with the main process via a Unix domain socket, and there's a thread on the main process for each remote TTY's I/O. All the modules and functionality are running in the main process.

This works well for things like CLIs for some kind of process, but I don't think it's as well suited for a significantly richer/more interactive program, where I think it'd make much more sense for all the TTYs to be managed in the same process rather than communicating over a socket. For example, you can't run ncurses over a socket, since the termios that we care about is in that remote process, not in our main process or usable over the socket. So taking the master/remote model further, you'd need to move a lot of logic from the main program to the remote processes.

The problem I'm a little stuck on is exactly how you can have the main process handling all the TTYs without itself handling all of the network socket traffic. For example, say we want to allow telnet and SSH connections. With the master/remote model, it might look like this:

Telnet:

  1. Inbound telnet connection
  2. Telnet server launches /usr/sbin/remote_process (custom login shell)
  3. remote_process (a C program, shell script, etc.) begins executing, communicating with main_process

SSH:

  1. Inbound SSH connection
  2. Authentication
  3. SSH server launches /usr/sbin/remote_process (custom login shell)
  4. remote_process (a C program, shell script, etc.) begins executing, communicating with main_process

Importantly, with the master/remote model we consider above, the telnet/SSH protocol is abstracted away from the program in question. It doesn't care if the incoming connection is from Telnet, SSH, a serial port, etc. We don't need to handle the details of these protocols ourselves.

Naively trying to apply this to the single-process model, handling all the TTYs directly, I would think the thing to do would be that step # 3/4 somehow needs to have the main process take over its terminal/PTY. main_process can't be called directly though, since it's already running, and I'm not sure if anything like that would be possible since somehow it would be moving the master/slave for the pty between processes, but the goal would be to have main_process doing everything remote_process was doing in the other model, directly handling the I/O from the Telnet server, SSH server, etc.

The standard way of doing this kind of thing seems to be having the main_process directly run its own listeners - that is, instead of listening for UNIX domain socket connections, directly accept Telnet/SSH traffic, etc. But then, the program is now responsible for handling the details of each individual protocol.

You can see an example of this with SyncrhonetBBS: https://github.com/SynchronetBBS/sbbs/tree/b35365c2e470bde58838cbb7445fe7e8c4bc1beb/src/syncterm

The BBS program itself has code to handle each supported protocol: SSH, TELNET, TELNETS, etc.

(I suppose there is a third model: have the main daemon process itself be quite minimal in what it does, and just have each individual TTY process contain the bulk of all the logic, and just use the daemon process for IPC between the TTYs... but then that gets tricky if you want to do stuff like dynamically loadable and unloadable modules that are really at a "system" level as opposed to per-TTY... so I'm not really considering this other extreme).

Is there any way to have the best of both worlds - be able to control all the different TTYs from a single process, but without having to directly implement protocol-specific handling? And if so, how does the TTY setup occur? I'm not looking for code examples here so much as a general high-level explanation/guidance of what this would likely look and how the different components - processes, sockets, TTYs - would interact.

remote_process could create the PTY pair (posix_openpt(), grantpt(), unlockpt()) and pass the name of the slave (ptsname()) and the terminal type (from the SSH or TELNET protocol) over the socket to main_process. main_process can then open the slave and set up ncurses for the correct terminal type. Note: grantpt() will set the owner of the slave to the UID of the remote_process process, so remote_process and main_process should run as the same user. Handling SIGWINCH may be tricky. - Ian Abbott
The ssh/telnet server would usually create the pty before launching remote_process -- so remote_process just needs to pass the pty slave it is connected to to main_process, which can then open it (probabliy with O_NOCTTY) and deal with it directly. - Chris Dodd
@IanAbbott So in that case, is remote_process free to exit after passing that along to the main process (and let's say also receiving a confirmation from main that it's taken over), or does it need to keep running afterwards? As in, if remote exits at that point, the SSH session will keep going fine, even though that thing in the "middle" just disappeared? I'm not sure what else it would be doing at that point, if it had to stay up - InterLinked
No, if remote_process exits, the sshd/telnetd server will shut down the connection (unless you modify them). It will also get any signals associated with the pty (such as SIGWINCH), which it should somehow tell main_process about. Could just forward them, but main_process can't then easily tell which pty the signal is associated with. - Chris Dodd
@ChrisDodd So there's no way to somehow have SSH talk directly to a thread on the main process, each SSH connection has to talk to its own relaying process? (As for telling apart, I guess if it had to be there, it could sent the PTY over the socket, in theory...) And is this kind of model actually used, or have I just made up a weird kind of scenario? - InterLinked