Dial-Up Networking with PPP
Remote network access always seems to be popular for users. We have nearly 500 users in my department and a significant number of them use or would like to use the Internet outside of business hours. When I joined this department, I inherited a perfect example of what a PPP server should not look like. After careful design and testing, I transformed the old monster into a worthy system.
In this article, I will briefly cover the benefits of PPP versus SLIP before I examine the magic inside the box. SLIP is the Serial-Line-IP-Protocol and it will only transport IP traffic. While this is probably sufficient for most dial-up users, PPP allows additional protocols (like AppleTalk and IPX) to be transported simultaneously. PPP clients capable of auto-negotiating protocol parameters (such as MTU/MRU, IP addresses, name servers, and routing information) are far more widespread than equivalent SLIP clients due to the popularity of the Windows family of operating systems.
PPP is a host-to-host protocol, rather than a user-to-host protocol (like a login screen). When used to link networks, the gateways authenticate to each other without regard for the users on either side. In a dial-up situation, we can consider the client (the machine that intiated the connection) as a network with one host and one user. Because of this, it becomes acceptable for the client to borrow the identity of the user requesting the connection and present it as credentials with which to authenticate.
PPP is not strictly limited to dial-up networking although I will confine my discussion to it. PPP is a peer-to-peer protocol capable of linking two networks together and thereby linking all of the networks to which they connect. PPP also offers a number of choices for authentication methods. I was aiming for the lowest common denominator (a few old Macintosh systems running early System 7 with MacPPP), so I chose to use PAP authentication over logins and CHAP. I was not particularily comfortable with my users being able to log in and get a shell on a machine that looks like a modem with an Ethernet card, and I knew that there were quite a number of users without support for CHAP. (PAP is the Password Authentication Protocol, and CHAP is the Challenge Handshake Authentication Protocol.) A properly implemented CHAP system is more secure than a PAP system, because CHAP authenticates with a cryptographic hash of the identity, timestamp, and a random number. PAP, on the other hand, only checks a password on initial negotiation. For my purposes, I decided that the benefits of implementing CHAP were marginal compared with the cost. I consider it non-trivial to tap a telephone line and hijack a connection in progress. At higher speeds, modems are rather sensitive to noise, and an attempt at interception would probably result in a disconnect -- TEMPEST attacks notwithstanding.
Building the Server
The modem pool is based on OpenBSD 2.6 with a Cyclades 16-port ISA serial controller. Because I inherited the modem pool, I was limited to using hardware not too far removed from the kit already in service. PC hardware is inexpensive to purchase and maintain. If something is likely to break, we've found it reasonable to keep spare parts on hand and simply toss the defective piece. The modem pool has moved from a 66-MHz 486sx to a 100-MHz 486dx4 with 32 M memory. Our modems are mostly 28k8 with a few 14k4 and 33k6 units mixed in. Again, they are inexpensive and easy to replace.
I chose to move away from Linux and re-implement the modem pool on OpenBSD, which has an enviable reputation for security and for being one of the best network stacks available. While I did not do proper benchmarks, the modem pool has received rave reviews on significantly better performance, even though I'm now mangling every packet with IPNAT.
Although the GENERIC kernel that ships with OpenBSD is sufficient to build a modem pool on, I reconfigured the kernel to save space and to get the Cyclades card to work with the plug-n-pray network card. I simply booted the system and removed support for all the devices that the kernel did not autodetect on boot (save for a few of the NICs I keep for spare parts). I changed the serial card's IRQ and IO range to something the machine wasn't already using and noted that change in the new kernel configuration.
Although the GENERIC kernel that ships with OpenBSD is sufficient to build a modem pool on, I reconfigured the kernel to save space and to get the Cyclades card to work with the network card. I simply booted the system and removed support for all the devices that the kernel did not autodetect on boot (save for a few of the NICs I keep for spare parts). To create my custom kernel, I copied the GENERIC config file to one called TERMSERV. Kernel config files live in /sys/arch/i386/conf. Then I changed the serial card IRQ and IO range to something the machine wasn't using and noted that change in the new kernel configuration. Other options I added into TERMSERV were the following:
option GATEWAY, PPP_DEFLATE, PPP_BSDCOMP, PPP_FILTER, IPFILTER, \
pseudo-device bpfilter 8
pseudo-device ppp 16
These options cause the kernel to include IP forwarding code, the in-kernel PPP driver with packet filtering, PPP compression, IPF for firewalling and NAT, BPF support for diagnostics, and drivers and buffer space for up to 16 simultaneous PPP connections.
Once the kernel knew about the PPP and IP forwarding, I installed mgetty as the software to answer incoming calls. I chose mgetty because it can autodetect PPP Link Control Protocol packets and spawn a copy of pppd rather than log in. This means I can use /etc/nologin to prevent users from actually getting a shell --we have far better places for them to do that. mgetty comes with amazingly complete documentation but note that it is necessary to add -DAUTO_PPP into the Makefile. That will compile in the support for PPP autodetection. The sample configuration files included with mgetty give an example of what to do when an incoming PPP call is detected (see Listing 1). I deleted the options that mgetty would normally try to pass off to pppd, preferring to keep my pppd options with the rest of the PPP configuration, in /etc/ppp.
You can feed pppd a number of options, so I decided to make the server push all the configuration upon the client. (See Listing 2 for the options I used to make this happen.) I chose to use server-specified options because this allows me to maintain only one configuration and simplifies configuration and troubleshooting. I can now provide users with ten simple click-here, type-this instructions. Assuming that these instructions have been followed and Windows is operating correctly, any problems must be on the server. To further simplify matters, I assigned dynamic addresses. Each port on the Cyclades card (ttycX) has a file called /etc/ppp/options.ttycX, which contains a line similar to the one below. The server has an IP alias of 192.168.0.254:
192.168.0.254:192.168.0.Y #Y = X+1
Early in the server's lifetime, I encountered some sticky problems, including modems dropping connections frequently and not being able to see the network beyond the server. Including Berkeley Packet Filter (BPF) support was quite useful in tracing these problems (using tcpdump) to misconfigured clients or bad IPNAT rules. While testing the server, I found it useful to keep tcpdump running on the server while dialing in from a machine with a configuration similar to those my users have at home.
Modem lines are a relatively scarce resource (there are only 16 lines for our 500 users), so a method for dropping idle connections may be in order. PPP_FILTER allows the PPP daemon to ignore certain types of traffic (POP3, IMAP, ping) when determining idle time. A user who has set his mail program to check for email and then goes away all day should be dropped because the connection isn't in serious use. Interactive login methods like ssh and telnet keep the connection alive. There are ways to abuse this, of course, but simply setting Netscape to check mail every minute won't work any more.
When I was rebuilding the modem server, IP addresses were in somewhat short supply. The modem pool was tying up 16 addresses that had less than five percent utilization. This was obviously unacceptable. My solution was to use IPNAT. I assigned each modem line an unroutable IP address and let the modem controller sort out the translation. The clients can't tell that they're behind a NAT box. They have an IP and a router, and everything works normally. The only problem is that NAT breaks active ftp unless a proxy (like the one integrated into IPNAT) is used (see Listing 3). Several pleasant side effects include the inability to run a server on your PC (intentional or otherwise), and the increase in privacy and security. Termserv looks like one very busy computer, and there's no way to see what's on the other side. By logging into the modem controller, I can still establish a relation between IP, tty, and user, thereby preventing egregious abuse of a relatively anonymous modem pool.
To further strengthen the security and reliability of the system, I use IPFilter and PPPFilter. IPFilter is a popular packet filtering package. I've configured it to block communication with the worst banner servers, so that we waste as little bandwidth as possible. PPPFilter can be set to filter out certain classes of wasteful or harmful packets, and to only count certain types of traffic for resetting the idle timer. ftp, HTTP, ssh, telnet and SMTP are good candidates for logging, while IMAP, POP3, and NNTP connections should probably not keep the connection alive. This decision depends heavily on your site's security policy and resource limitations.
I attempted to use a failsafe authentication system where I had active control over which users were permitted to access the pool. I chose to use YP to get passwords from the main login server, and then use a control file to build /etc/ppp/pap-secrets. For a user to be granted access, the modem pool must find a username in both passwd and pap-secrets. If the user is in one file, but not the other, then authentication fails. This means that a user can't try to kill the YP server remotely and then break into the modem pool. It also prevents users from discovering the modem pool number and trying to use it without first consulting me for the specifics of the server and the terms of service. The system will not provide a shell to anyone other than root, enforcing my policy that termserv is merely an Ethernet-to-modem converter.
Once the technology is proven stable, it must be backed up with administrative controls. Our central networking department has written a wonderful set of terms of service. I now print the instructions for modem pool use on the back of a copy of said TOS, because if they're using the modem, they must have read and agreed to the TOS.
Finally, system documentation is absolutely critical. OpenBSD has excellent manpages and FAQs discussing the software I'm using. Once a modem pool is deployed at a site, a manual detailing everything from end-user setup to complete server rebuilds should be included (that means things like the TOS and printouts of any scripts written to support the server). Alternatively, a CD would be a good choice. I keep a bootable CD backup of the modem server inside the case, and another on my bookshelf. If the modem pool breaks, I have a verified backup in a known, logical location. A collection of pointers to the latest versions of the software used is possibly a sufficient backup after enough of these boxes have been built.
After adding the third user and grepping his password out of yppasswd, I decided it was tool time. Included are a collection of shell and Perl scripts that made my life easier. Since a PPP session isn't a true login, PPP users don't show up with who. To get around that, I wrote ppp-who, which counts the active number of PPP interfaces and prints the users with active PPP connections. This works because because mgetty writes a utmp entry when pppd is successfully started.
To discover peak usage hours, we created pppcount and ifgraph. pppcount is generally run every 5 or 10 minutes from cron, and it counts the number of active PPP interfaces, adds a timestamp, and makes an entry into a logfile. ifgraph then reads the logfile and prints a bargraph of connections. ifgraph can run in absolute mode, where it prints historical totals of connections, or in average mode, where it prints average use for each interval.
Since I have a control file of all authorized users, I needed an automatic utility like mk_pap_secrets to read it and generate /etc/ppp/pap-secrets. It's a shell script that reads /etc/ppp/pppusers.allow (one username per line), slurps up yppasswd, and reformats it for use as pap-secrets. It's not pretty, but it should be free of race conditions and other nasty passwd-related security holes.
Finally, pppacct generates up-to-the-minute reports of connect time, in 1 minute increments. It includes logfile generation, the ability to generate stats for months other than the current one, and descriptive statistics like average connect time and standard deviation. It writes a report to stdout, and will optionally spew a timestamped logfile if invoked with the -log flag. Use -m and -wtmp to generate reports for months other than current using something other than /var/log/wtmp as the data source.
All the accounting programs read and write to /root/ppp-accounting.
About the Author
Chris works for the University of Alberta, in Mathematical Sciences. His primary computing interests are in security, networks, and hardware. When not computing, Chris can be found engaging in a variety of risky, high-adrenaline sports. I don't need the red pill -- I've got UN*X and coffee.