I had (have?) a detailed discussion of how to use
pppd (Point-To-Point Protocol Daemon)
in the pipeline. However, Twitter friend Gravis
posted a very nice article on how PPP
the protocol works, and I felt compelled to convert my original detailed post
into a more concise form!
The Point-To-Point Protocol (PPP) is a Layer 2 protocol that is meant to connect computers in a network, like Ethernet or Wifi. However, unlike Ethernet and Wifi, PPP doesn't specify how voltages should be sent from one computer to another. The Fundamentals section in Gravis' article provides great detail as to the rationale. An important consequence of PPP not specifying how voltages are sent is that lots of computing hardware can be used to send and receive PPP packets, such as RS-232 ports.
This article will focus on sending/receiving PPP packets over Universal Asynchronous Receiver Transmitter (UART). Due to their simplicitly, UARTs are extremely common hardware for communications and debugging on Systems-on-a-Chip (SoCs), FPGAs, Single-Board Computers (SBCs), and just many computers in general. Considering their proliferation, a UART becomes a powerful communications interface when combined with PPP and TCP/IP; you now have a means to connect computers without any other I/O interfaces to the rest of the TCP/IP-speaking Internet if you so choose!
One common PPP implementation is the Point-To-Point Protocol Daemon
for Unix systems, or
pppd provides both a daemon (service) and kernel
component to create a network interface that can send and receive TCP/IP packets
(encapsulated by PPP) to and from a UART.
You can use
pppd to set up two computers running Unix or a Unix-like OS to
communicate to each other over their own local TCP/IP network. In fact that's a
major characteristic of a Point To Point network! In such a setup, both
computers can contact each other via applications like
ssh, but are
blissfully unaware of any other computers that exist in the world.
In the absence of any dedicated networking hardware, you can still use
connect Unix systems to the Internet. This article will describe how to
pppd to connect to the Internet proper, using my current setup at home as
an example. My setup should be applicable to most use cases, as long as one
of the computers in your PPP links has another network interface besides a
UART, such as an Ethernet port or Wifi adapter.
I have two Single-Board Computers (SBCs) deployed at present- an ASUS TinkerBoard running a Debian variant, and a Raspberry Pi 1 Model A+ running NetBSD. The TinkerBoard SoC has 5 UARTs, an Ethernet port, and a Wifi adapter/antenna. 4 UARTs are exposed on GPIO headers. I use one of the additional UARTs exclusively for PPP. The Raspberry Pi has only a single UART. I will describe how to use a single UART for both a terminal and a PPP session later.
UARTs are often exposed as three or more pin headers on your computer's PCB- receive (RX), transmit (TX), and ground (GND) 1 . For PPP communication to work, at a minimum, you will need to connect GND to GND, RX to TX, and TX to RX between your two computers. The minimum setup works just fine and is what I use.
Your SBC vendor will typically list UART pins as part of their GPIO pinout on
their website (NanoPI NEO,
for example). Many SBCs, including TinkerBoard use
a Raspberry Pi-compatible GPIO header, which will expose
at least one UART. If the pinout is not publicly available, you may need to reverse
pinout! Or, if a USB port is available, you can use USB to Serial Cable
to create a new UART and pins- I can confirm
pppd works fine with USB adapters
on Linux and NetBSD.
If you don't have a TinkerBoard and Raspberry Pi, the important part of my setup is that the TinkerBoard has multiple pieces of hardware for network interfaces- Wifi, Ethernet, and UART; my Raspberry Pi has only one- a UART. By routing UART traffic between two computers via PPP to/from other existing networking hardware, it is possible to connect the UART-only computer to the Internet.
In the next sections, I will talk about how to set up
pppd for both my
TinkerBoard and Raspberry Pi. If you don't have those particular pieces of
hardware, the config options I provide should be good defaults.
As far as I understand,
pppd nominally treats both peers in a connection as
interchangable. Unlike some communications protocols like SPI or I2C, there is
no concept of controller or peripheral.
Therefore, it normally doesn't make much of a difference which peer you set up
first to bring up a PPP link3
However, there are options you can pass to
pppd to share settings (at least
IP address- there might be more?) from one peer to another. Seeing as the
TinkerBoard peer in my setup needs to route traffic to/from the Internet anyway,
which already requires different set of config options from the Raspberry Pi,
I use extra config options to treat the TinkerBoard as a controller for a
Raspberry Pi peripheral.
Besides the command line,
pppd initializes itself using a configuration file
containing options under the
/etc/ppp directory. Options file names take
the form of
$DEVICE represents the device name of
your serial port. You will need to know the names that your Unix system
assigned your serial ports before setting up
pppd. In my case, I have
UART2 on the TinkerBoard GPIO header specifically for the PPP link.
This is called
ttyS2 by Linux 2
I have posted my
/etc/ppp/options.ttyS2 config file in full below, and I will
briefly explain what each options entails afterwards. If you want more
information, I encourage you to consult the
which details all possible options that
pppd understands on the command line
or in a config file:
noauth local nodetach nocrtscts xonxoff 192.168.1.164:192.168.1.165 persist proxyarp
As explained in Gravis' article, the PPP Protocol contains a number of
authentication modes. Because I physically have access to both SBCs a few
inches away, I don't bother with authentication to bring up the
interfaces using the
noauth option. Readers should feel free to experiment,
but I have no experience with PPP links that require authentication.
pppd from relying on the Carrier Detect (CD) and Data
Terminal Ready (DTR) signals
common on RS-232 serial ports. We only connect RX, TX, and GND between the
Raspberry Pi to the TinkerBoard, and I'm not even sure if the remaining
common UART signals are available on from the Raspberry Pi SoC.
pppd from forking to become a background process. This
is required so
pppd plays nice
Simple service types in
From debugging when I initially set the PPP link up in early 2018, I needed
nocrtscts to disable hardware flow control
on the TinkerBoard side. As noted above, we only connect RX, TX, and GND
between the two SBCs. I found out via logic analyzer traces that without this
pppd on the TinkerBoard will wait forever for the flow control to
indicate that the Raspberry Pi is ready :).
I don't know or remember why this option isn't required on the Raspberry Pi side. The man page states that hardware flow control settings are left unchanged if no option to enable or disable it was given. It's possible NetBSD on the Raspberry Pi doesn't enable hardware flow control on the UART (if the signals even exist), but Debian on the TinkerBoard does?
If you are connecting two Unix computers whose serial ports have hardware flow
control signals, feel free to experiment by removing both
local options and see what happens- once you have a working setup :)!
xonxoff enables software flow control on the serial line. I don't believe
it interacts with hardware flow control options.
I've found this option actually increases network speeds at higher baud
rates. I used to have a table comparing speeds with
xonxoff and without
xonxoff, but I seem to have lost it aside from two old entries without
units (probably bytes/s):
# Baud XONXOFF No Flow Ctrl 500000 47107 48450 750000 65203 20411
Also, I don't believe
750000 baud works anymore after a
192.168.1.164:192.168.1.165- Format is
<local_IP_address>:<remote_IP_address>, this option tells
IP addresses the peers should use. Because of the
I have for
pppd on the Raspberry Pi side, the Raspberry Pi will accept whatever IP address
is given by the TinkerBoard. As written out, the above option will set the
TinkerBoard's IP address to
192.168.1.164 and the Raspberry Pi's IP address to
I don't know how netmasks work with
pppd at this time. However, my
experience is that
pppd "does the right thing" so it thinks the
interface and Wifi and Ethernet interfaces are part of the same network
proxyarp and IP forwarding is enabled.
pppd to reconnect if the connection is lost, instead of
terminating. I'm not sure how essential this option is when paired with
an init system (which can also tell a daemon to restart).
proxyarp is a complex option, and discussing how Address Resolution Protocol
(ARP) works is beyond the scope of this article. However, I'll attempt to
overly summarize anyway.
ARP is a link-layer protocol used by some other link-layer protocols like Ethernet and Wifi to figure out which IP addresses belong to what machine on a network. PPP, being a point-to-point link, does not have a concept of ARP. In order for computers using Ethernet or Wifi to see both nodes of a PPP link, both ends of the link need to participate in ARP requests/replies anyway. Proxy ARP a technique that allows a device (the TinkerBoard) to answer ARP requests on behalf of another machine (the Raspberry Pi).
If you don't include this option, the TinkerBoard will be accessible from
other computers via the
IP address bound to the PPP interface (
192.168.1.164), but the Raspberry
192.168.1.165) will be inaccessible. I show this by deliberately
commenting out the
proxyarp option, restarting the PPP link, and using
arping to see which parts of the link are accessible from a Wifi-connected
Proxy ARP disabled:
william@xubuntu-dtrain:~$ arping -c 4 -I wlp3s0 192.168.1.164 ARPING 192.168.1.164 from 192.168.1.168 wlp3s0 Unicast reply from 192.168.1.164 [F0:03:8C:90:B2:A1] 19.411ms Unicast reply from 192.168.1.164 [F0:03:8C:90:B2:A1] 10.464ms Unicast reply from 192.168.1.164 [F0:03:8C:90:B2:A1] 6.619ms Sent 4 probes (2 broadcast(s)) Received 3 response(s) william@xubuntu-dtrain:~$ arping -c 4 -I wlp3s0 192.168.1.165 ARPING 192.168.1.165 from 192.168.1.168 wlp3s0 Sent 4 probes (4 broadcast(s)) Received 0 response(s) william@xubuntu-dtrain:~$
Proxy ARP enabled (IP forwarding enabled):
william@xubuntu-dtrain:~$ arping -c 4 -I wlp3s0 192.168.1.164 ARPING 192.168.1.164 from 192.168.1.168 wlp3s0 Unicast reply from 192.168.1.164 [F0:03:8C:90:B2:A1] 103.548ms Unicast reply from 192.168.1.164 [F0:03:8C:90:B2:A1] 6.851ms Unicast reply from 192.168.1.164 [F0:03:8C:90:B2:A1] 8.460ms Unicast reply from 192.168.1.164 [F0:03:8C:90:B2:A1] 4.718ms Sent 4 probes (1 broadcast(s)) Received 4 response(s) william@xubuntu-dtrain:~$ arping -c 4 -I wlp3s0 192.168.1.165 ARPING 192.168.1.165 from 192.168.1.168 wlp3s0 Unicast reply from 192.168.1.165 [F0:03:8C:90:B2:A1] 704.047ms Unicast reply from 192.168.1.165 [F0:03:8C:90:B2:A1] 14.626ms Unicast reply from 192.168.1.165 [F0:03:8C:90:B2:A1] 16.761ms Unicast reply from 192.168.1.165 [F0:03:8C:90:B2:A1] 8.088ms Sent 4 probes (1 broadcast(s)) Received 4 response(s)
Aside from setting up the above config file, you will probably want to enable IP forwarding, described in the next section. IP forwarding is not required to set up a Point-To-Point link by itself, but it is required so that both ends of the link, especially the UART-only side, can access the Internet.
Any traffic that's meant for the Raspberry Pi from one of the other interfaces
needs to somehow reach the UART interface with the PPP link. With the above
pppd itself will:
pppdthat does this).
As you can see,
pppd delegates some responsibilites to the OS.
pppd has a kernel component too, but these syscalls seem to not be
As you may have guessed, it is also kernel's responsibility to make sure that traffic from the TinkerBoard's Ethernet and Wifi interfaces somehow get sent over the UART wires. That somehow is called IP forwarding.
for you, and (as far as I understand) will not check other interfaces for packets meant for one of the two computers reachable on the UART interface. Instead, you must ask the OS to forward packets for you from one interface to the computers on another interface, effectively turning one of your computers with a PPP link into a router.4
Normally, Linux will not forward packets from one interface to another by default. You have a few options:
sysctl -w net.ipv4.ip_forward=1at least once after each reboot when you want to connect the UART-only computer to the Internet.
net.ipv4.ip_forward = 1in
/etc/sysctl.conffor IP forwarding to persist between reboots.
pppdalso can enable IP forwarding using the
[Unit] Description=PPP Wants=network-online.target After=network-online.target [Service] Type=idle ExecStart=/usr/sbin/pppd -d /dev/ttyS2 576000 Restart=always TimeoutStartSec=120 [Install] WantedBy=multi-user.target Alias=ppp.service
Since my TinkerBoard is running a Debian variant, it also comes with
So we create a unit file for our
At this point, one half of our PPP link is fully set up. If running Linux,
pppd on the other half of the link is similar to the above
pppdconfig file with the correct options (described below).
pppdto easily setup/tear down the PPP link (at boot, preferably- mention getty autodetects?). The command line options
-dand baud rate should be identical to the first computer, and the device name should reflect the UART identified in step 1.
Since we don't need to forward traffic over the other computer's UART to anywhere else, the IP forwarding step is unnecessary on the other half of the link.
However, my Raspberry Pi runs NetBSD instead of Linux for various reasons, and
thus I will describe setting up the PPP link on the Raspberry Pi side using
NetBSD. The setup is a bit more involved, but it has one neat advantage: a
single UART can be used to connect to the Raspberry Pi using either a PPP
session or via the traditional
login over serial port. Multiplexing a UART
like this requires special logic in the
program that monitors serial ports before login. The NetBSD
getty has PPP
detection logic, while I'm not aware of a Linux
getty package that does.
No matter which *nix variant the other half of the link is running, you should still use a config file to the one I present below on the other computer.
Because my Raspberry Pi is an original Model A+, it doesn't have an Ethernet port. This means that short of using a USB Wifi adapter (which I find flaky), using the UART is one of the only ways to connect to Internet. However, there's only one UART on the Raspberry Pi5 ), and I need that UART occassionally for a serial console for debugging.
As it turns out, there's nothing preventing me from using the same UART for
a serial console and a PPP connection a long as I don't try to use
pppd and a
serial console at the same time over the same UART.
NetBSD comes with
pppd as part of the
base set tarball, so there is no
bootstrapping problem with needing to download
pppd before using
pppd wasn't part of the base distribution, I would use a separate computer to
pppd package for your distro and then transfer it via flash
drive ("sneakernet") or use Kermit
over the UART via a USB-to-serial cable connection.
getty implementation provided with NetBSD is capable of multiplexing
login via a serial console with a PPP session. A Linux
such capability may be coming soon.
# PPP network link login # # these entries can be used by ISPs or others who want to be able # to offer both a "shell" and a PPP login on the same port. Setting # the "pp" attribute allows getty(8) to recognize a PPP link start # negotiation, and invoke the program listed, in addition to normal # login(1). # # N.B.: if PPP is recognized, this bypasses normal login/password # exchange; the expectation is that you'll configure pppd (or whatever) # to require a PAP or CHAP handshake for authentication after PPP is # started up. # # It is also recommended that you use hardware (CTS/RTS) flow control # on the port, and run the port as fast as possible, to allow modems # extra time to do data compression, if enabled. # ppp:np:ce:ck:pp=/usr/sbin/pppd: # ppp.19200|PPP-19200:sp#19200:tc=ppp: ppp.38400|PPP-38400:sp#38400:tc=ppp: ppp.57600|PPP-57600:sp#57600:tc=ppp: ppp.115200|PPP-115200:sp#115200:tc=ppp: ppp.230400|PPP-230400:sp#230400:tc=ppp: ppp.460800|PPP-460800:sp#460800:tc=ppp: ppp.500000|PPP-500000:sp#500000:tc=ppp: ppp.576000|PPP-576000:sp#576000:tc=ppp:
# $NetBSD: ttys,v 1.8 2019/09/25 23:09:21 abs Exp $ # # from: @(#)ttys 5.1 (Berkeley) 4/17/89 # # name getty type status comments # console "/usr/libexec/getty ppp.576000" vt100 on secure constty "/usr/libexec/getty default" vt100 off secure ttyE0 "/usr/libexec/getty Pc" wsvt25 off secure ttyE1 "/usr/libexec/getty Pc" wsvt25 off secure ttyE2 "/usr/libexec/getty Pc" wsvt25 off secure ttyE3 "/usr/libexec/getty Pc" wsvt25 off secure tty00 "/usr/libexec/getty default" unknown off secure tty01 "/usr/libexec/getty default" unknown off secure tty02 "/usr/libexec/getty default" unknown off secure tty03 "/usr/libexec/getty default" unknown off secure tty04 "/usr/libexec/getty default" unknown off secure tty05 "/usr/libexec/getty default" unknown off secure tty06 "/usr/libexec/getty default" unknown off secure tty07 "/usr/libexec/getty default" unknown off secure
Now, we need to set up a config file on the Raspberry Pi side of things. It is perfectly
fine to use
console (as in
/dev/console) as the serial device name.
NetBSD will assign
/dev/console to the UART exposed on the GPIO header,
which is what we want.
ipcp-accept-local noauth local xonxoff defaultroute
Here are short explanations on new options that
pppd needs for this scenario
where the Raspberry Pi spawns
pppd instances when it detects PPP activity from the
pppdto accept an IP address from the other peer and use it as our own. There is an analogous
ipcp-accept-remoteoption that I don't use. I don't remember why I only use
ipcp-accept-local, but it's possible it's not actually required. I will need to re-test at some point!
defaultroutesays "if the current peer can't figure out where a packet should go, send it to the peer at the other end of the PPP link and see if they can figure it out." Since the TinkerBoard is acting as a router, it should be able to route packets to the outside world on its Wifi or Ethernet interface.
# Generated by resolvconf domain $DOMAIN nameserver $NAMESERVER
After you've verified
dd if=/dev/urandom bs=512 count=1024 | dbclient firstname.lastname@example.org 'cat > /dev/null'for speed test.
|Baud Rate||Data Copied||Speed|
|230400||524288 bytes||22.1 kB/s|
|460800||524288 bytes||41.2 kB/s|
|500000||524288 bytes||40.9 kB/s|
|576000||524288 bytes||44.3 kB/s|
|921600||524288 bytes||44.1 kB/s|
|1000000||524288 bytes||29.9 kB/s|
UARTs are not designed for high speeds, emitting voltages over a single-ended interface suspectible to noise. However, the speeds you can get (tens of kBps) are acceptable for a number of background tasks and downloading software.
For better or worse, connecting a computer to the outside world is a prerequisite for making the computer useful over long periods of time. While there is a lot of work to get data from source to destination across the world, we can leverage the existing design to connect machines to the outside world that would normally be overlooked. We used pppd to connect an overlooked machine like the Raspberry Pi Model A to the outside world. By leveraging existing relatively simple hardware like a UART, an Internet connection will hopefully give the Model A and similar computers many more years of useful life.
Since you've made it this far, I imagine you want to actually see the PPP link working. Well, ask and you shall receive!
The following section briefly discusses problems I have personally run into when getting a PPP link between my TinkerBoard and Raspberry Pi to work.
I have to take some time to diagnose this; in my current setup, the TinkerBoard will refuse to boot if the Raspberry Pi has powered on and drives any pins on the TinkerBoard's I/O connector. To fix this, power off the Raspberry Pi until the TinkerBoard starts booting. I will need to experiment to find which GPIOs are the culprit.
Make sure your
options file contains
local on both the TinkerBoard
and Raspberry Pi. It is possible that without this option,
pppd on the TinkerBoard
will wait for hardware flow control signals that will never arrive.
sshinto the Raspberry Pi from a remote computer besides the TinkerBoard.
Make sure IP forwarding is enabled on the TinkerBoard, and make sure that
pppd on the TinkerBoard is configured with the
proxyarp option. See
footnote #4, as there may be other failure modes due to not including
those options that I've since forgotten.
Make sure your nameserver in
resolv.conf is set up properly. If using
DHCP, this is taken care of for you. However,
pppd setup falls under
"static" IP, and while it handles most static IP configuration steps via its
config file, setting up your nameserver is not one of those steps.
I think this is a NetBSD issue; NetBSD uses the break condition (abnormally
long low voltage) to start the kernel debugger. Any noise on the line during
TinkerBoard and/or NetBSD boot could create that condition, causing the Raspberry Pi to
stop responding. And my own experience is that
pppd doesn't tolerate the boot
messages sent from the Raspberry Pi well, seeing those messages as garbage. Therefore I
recommend not starting the
pppd link at boot via
Rather, wait until you need to access the other end of the UART after boot
has completed on both ends of the link, and then run
systemctl start rpippp.service.
Thanks to the multiplexed
pppd on the NetBSD side, one good
debugging technique on the Pi side is to stop the PPP link and access the Pi
via serial console using a program such as
minicom. If you press "Enter" and
db>, the kernel debugger has been invoked. Type
then "Enter" to exit the debugger. If you see
login: or similar without a
login banner, the NetBSD
getty has stopped listening for PPP packets. You
will need to log in, and then log back out to restart the
PPP detection FSM.
After a recent power outage, I had to manually restart both the TinkerBoard
(because of the GPIO problem above) as well as the Raspberry Pi. Even after waiting
until after both the TinkerBoard and Raspberry Pi finished boot, and debugging using
the above procedure, the Raspberry Pi wasn't responding to any activity over the
serial console, even in
minicom; it seemed to have crashed completely. The
On a hunch, when I rebooted the Pi, I commented out the entirity of my
/etc/ppp/ip-up script. The PPP link came up successfully. At this point,
I stopped the link, used
minicom to restore the
and I was able to restart the link successfully too. I wonder if there's
a bug with
ntpdate that makes it refuse to run until after the link has been
brought up once since the last reboot? I will need to investigate.
1 The power pin (Vcc) may also be exposed be exposed as a pin. Some USB UART adapters, such as FTDI cables, also provide a Vcc output pin to supply power to target boards with a UART. In our case, I assume the board is _not_ being powered by a USB adapter, and so Vcc pins can be ignored.
2 The RockChip that powers the TinkerBoard has 5 UARTs. From reading the manual
and comparing pinouts,
UART0 is not exposed on the GPIO header.
UART1 is exposed on the GPIO header at the same pins as on a
Raspberry Pi, so I leave that alone for compatibility. This leaves
UART2 as the first free available UART- and thankfully, it has
a device node!
3 When doing my first PPP experiments back in 2017, I debugged various link bringup scenarios using USB UARTs; I was able to get a working link no matter which order I brought up both peers- including Internet.
4 If you enable Proxy ARP, chances are you want to enable IP forwarding as well.
I seem to vaguely recall during my early PPP debugging sessions that enabling
Proxy ARP but not IP forwarding, or vice-versa, would result in
different errors. In 2020 however, I can't seem to duplicate different failure
modes. If I don't have both proxy ARP and IP forwarding, the Raspberry Pi is
inaccessible from all other computers, but the TinkerBoard's
interface is accessible just fine.
Last Updated: 2020-07-02