2

The goal

I'm attempting to connect two Raspberry Pi's together using IPv6 over SLIP but it's not quite working. Here's what I've done:

The attempt

Both Pis are Model B running Jessie and the same process was applied to both. First, I edited the /boot/config.txt to enable uart0 on both machines. Next, I used raspi-config to turn off the redirection of tty to the uart.

The connection was then made between the two by connecting gnd (pin 6), tx0 (pin 8), and rx0, (pin 10). Naturally, gnd to gnd, and tx0 to rx0. Then the following commands:

sudo slattach -p slip -s 115200 /dev/ttyAMA0 &
sudo ifconfig sl0 mtu 1280
sudo ifconfig sl0 add 2016:0bd8:0:f101::101/64
ping6 -c 4 -I sl0 2016:bd8:0:f101::102

One of the devices was given ::101 and the other ::102.

On the 102 box, I run sudo tcpdump -i sl0 and on the 101 box, I run ping -c4 2016:bd8:0:f101::102.

The results

I see that the 102 box is actually receiving the ICMP6 echo requests, but never answers:

tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on sl0, link-type RAW (Raw IP), capture size 262144 bytes
20:47:17.096858 IP6 2016:bd8:0:f101::101 > 2016:bd8:0:f101::102: ICMP6, echo request, seq 1, length 64
20:47:18.104029 IP6 2016:bd8:0:f101::101 > 2016:bd8:0:f101::102: ICMP6, echo request, seq 2, length 64
20:47:19.103880 IP6 2016:bd8:0:f101::101 > 2016:bd8:0:f101::102: ICMP6, echo request, seq 3, length 64
20:47:20.103889 IP6 2016:bd8:0:f101::101 > 2016:bd8:0:f101::102: ICMP6, echo request, seq 4, length 64

Also, the ipv6 firewalls are clear. That is, I ran ip6tables -F on both devices.

Both devices also have Ethernet ports which obtain IPv6 addresses via a local DHCP server that hands out both IPv4 and IPv6 addresses. Over eth0 I can ping one from the other using IPv4 or IPv6 with no problem.

I should also mention that I can try from either end and I get the same result, so clearly it's not a wiring or UART problem.

Additional notes

I can issue a ping6 without specifying the interface and the routing is handled correctly. That is, if I type ping6 -c4 2016:bd8:0:f101::102, I can see the echo requests coming in on the serial interface on ::102.

Just in case something funny was happening, I also did a tcpdump on the eth0 port of ::2 and did not see my missing ICMP echoreply messages heading out that way. When I do a ping6 on the eth0 port, I see both echo and echoreply messages just fine.

I've also used ping6 -c4 ff02::1%sl0 to find all neighbors, but each node only reports itself.

The question

What can I do to figure out why the Pi is not responding? If I substitute IPv4 addresses, everything works just fine. Is there some strange interaction between SLIP and IPv6?

  • Seems right to me, but there's probably some triviality that we're both not seeing. I'd suggest that routing might be the problem, since you are specifying a specific interface in the ping, so no routing in that direction. But the reply obviously goes through the routing table, which seems to be the only difference between the direction that works and the one that doesn't. You could also try a tcpdump on the ethernet interface instead of (or in addition to) the slip and see if the replies are being sent that way. – MAP Jul 15 '16 at 3:54
  • I've added some detail under "Additional notes" that might help. – Edward Jul 15 '16 at 11:11
  • SLIP is an ancient, ancient "nonstandard" protocol with many shortcomings. One of which is that it only supports a single encapsulated protocol, and predates IPv6 by decades. There is little reason to use it today. – Michael Hampton Jul 21 '16 at 22:25
  • @MichaelHampton: Except when interfacing with small embedded systems which is my actual end goal. – Edward Jul 22 '16 at 19:59
  • OK, that's the last remaining valid reason to use SLIP. :) But it really should be the protocol of last resort, even on a small embedded system. Use PPP if you can. – Michael Hampton Jul 22 '16 at 20:01
4

The short answer is that it can't work using slattach and standard SLIP, but there is a way to do what you want.

Why it doesn't work

It might seem as though Linux would classify IPv4 or IPv6 packets according to the IP number, which is the first byte of the IP datagram in both. However, the way it's actually done is by examining the contents of the datalink layer packet header, as with the EtherType for an Ethernet packet.

Because SLIP has no encapsulation that tells what it's wrapping, there is no way to indicate the type of datagram using standard SLIP. Hence, the SLIP implementation for Linux (and probably every other implementation of SLIP) simply assumes IPv4 datagrams.

This Linux IPv6 HOWTO alludes to this in section 4.1.4:

A major issue is that because of the network layer structure of kernel implementation an IPv6 packet isn't really recognized by it's IP header number (6 instead of 4). It's recognized by the protocol number of the Layer 2 transport protocol. Therefore any transport protocol which doesn't use such protocol number cannot dispatch IPv6 packets. Note: the packet is still transported over the link, but on receivers side, the dispatching won't work (you can see this e.g. using tcpdump).

How to use something like SLIP for IPv6

While the better way to do this, as mentioned in the comments, is via PPP, for the meager processors of some embedded systems, the complexity of PPP is unappealing.

For that reason, it's appealing to use something simple like SLIP when transmitting via serial port. It's possible to do a SLIP-like encapsulation of IPv6 packets, but with a tweak to include the EtherType.

One way to do this is to use the tuntap driver. Quoting from the tuntap FAQ,

The TUN is Virtual Point-to-Point network device. TUN driver was designed as low level kernel support for IP tunneling. It provides to userland application two interfaces:

  • /dev/tunX - character device;
  • tunX - virtual Point-to-Point interface.

Userland application can write IP frame to /dev/tunX and kernel will receive this frame from tunX interface. In the same time every frame that kernel writes to tunX interface can be read by userland application from /dev/tunX device.

Using this for the purposes of sending and receiving IPv6 packets via serial port consists of these steps:

  1. install the tuntap drivers
  2. create the tun device
  3. write userland software to fetch packets from the tun device and write them to the serial device (and vice versa)
  4. run that software

In this answer, I'll show only a rudimentary version of the software that doesn't actually do any SLIP encoding. However, that can be added if required.

On my version of Raspbian (Jessie, running kernel version 4.4.13), the tuntap module has already been enabled, compiled and installed, so we can essentially skip the first step. This might not always be the case if you've built your own version of the kernel, though, so I included the step above for completeness.

Creating the tun device can be done like this:

maketun

#! /bin/bash
# first create the device
ip tuntap add mode tun
# give an address to tun0
ip -6 addr add 2016:bd8:0:f101::102/64 dev tun0 nodad
# bring up the link
ip -6 link set dev tun0 up

This creates a /dev/tun0 device with the IPv6 address shown. This requires root privileges, so I run it using sudo ./maketun. After this is done, running ifconfig tun0 gives the following report:

tun0      Link encap:UNSPEC  HWaddr 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00  
          inet6 addr: 2016:bd8:0:f101::102/64 Scope:Global
          UP POINTOPOINT NOARP MULTICAST  MTU:1500  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:500 
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

Note that by default, the MTU is 1500 bytes, but we could alter this if desired. Whatever value is actually used, however, is quite important to the software that reconstructs the data which is shown in the next section.

serialipv6.c

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
#include <fcntl.h>
#include <termios.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <linux/if.h>
#include <linux/if_tun.h>

#define max(a,b) ((a)>(b) ? (a):(b))

int tun_open(char *devname)
{
    struct ifreq ifr;
    int fd, err;

    if ((fd = open("/dev/net/tun", O_RDWR)) == -1) {
        perror("open /dev/net/tun");
        exit(1);
    }
    memset(&ifr, 0, sizeof(ifr));
    ifr.ifr_flags = IFF_TUN;
    strncpy(ifr.ifr_name, devname, IFNAMSIZ);

    if ((err = ioctl(fd, TUNSETIFF, (void *)&ifr)) == -1) {
        perror("ioctl TUNSETIFF");
        close(fd);
        exit(1);
    }
    return fd;
}

int serial_open(char *ttyname)
{
    int fd = open(ttyname, O_RDWR);
    if (fd == -1) {
        perror("open serial port");
        exit(1);
    }
    // set essential serial parameters
    fcntl(fd, F_SETFL, 0);
    struct termios newtio;
    memset(&newtio, 0, sizeof(newtio));
    newtio.c_cflag = B115200 | CS8 | CLOCAL | CREAD;
    newtio.c_iflag |= IGNPAR;
    newtio.c_iflag &= ~(IXON | IXOFF | IXANY);  // no flow control
    newtio.c_oflag &= ~OPOST;   // raw output mode
    newtio.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
    newtio.c_cc[VMIN] = 0;      // require at least this many chars
    newtio.c_cc[VTIME] = 0;     // interchar timeout
    tcsetattr(fd, TCSANOW, &newtio);
    return fd;
}

int main(int argc, char *argv[])
{
    const size_t buflen = 1600;
    const size_t mtu = 1500;
    char buf1[buflen];
    char buf2[buflen];
    char *curr = buf2;
    char *end = &buf2[buflen];
    int f1, f2, l, fm;
    fd_set fds;

    f1 = tun_open("tun0");
    f2 = serial_open("/dev/ttyAMA0");

    fm = max(f1, f2) + 1;

    ioctl(f1, TUNSETNOCSUM, 1);

    while (1) {
        FD_ZERO(&fds);
        FD_SET(f1, &fds);
        FD_SET(f2, &fds);

        select(fm, &fds, NULL, NULL, NULL);

        if (FD_ISSET(f1, &fds)) {
            l = read(f1, buf1, sizeof(buf1));
            write(f2, buf1, l);
        }
        if (FD_ISSET(f2, &fds)) {
            l = read(f2, curr, end - curr);
            curr += l;
            int ethertype = ntohs(*(uint16_t *) (&buf2[2]));
            int len = curr - buf2;
            int iplen = ntohs(*(uint16_t *) (&buf2[8]));
            if (ethertype == 0x86dd && (len - iplen) >= 44) {
                // only write a whole packet, since serial is much slower 
                write(f1, buf2, len > mtu ? mtu : len);
                memset(buf2, 0, 16);
                curr = buf2;
                if (len > mtu) {
                    len -= mtu;
                    memcpy(buf2, buf2 + mtu, len);
                    curr += len;
                }
            }
        }
    }
}

This is not brilliant software. In fact, it's rather hackish, but it compiles cleanly and runs correctly on my Raspberry Pi devices. When it's installed and running (using ./serialipv6 & to have it run in the background), we can ping the other Pi using IPv6 over the serial port.

>ping6 -c4 2016:bd8:0:f101::101
PING 2016:bd8:0:f101::101(2016:bd8:0:f101::101) 56 data bytes
64 bytes from 2016:bd8:0:f101::101: icmp_seq=1 ttl=64 time=20.0 ms
64 bytes from 2016:bd8:0:f101::101: icmp_seq=2 ttl=64 time=19.7 ms
64 bytes from 2016:bd8:0:f101::101: icmp_seq=3 ttl=64 time=19.7 ms
64 bytes from 2016:bd8:0:f101::101: icmp_seq=4 ttl=64 time=19.7 ms

--- 2016:bd8:0:f101::101 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3005ms
rtt min/avg/max/mdev = 19.724/19.821/20.082/0.151 ms

It's not well documented as far as I could tell, but when TUN makes the datagram available to a userland process, it actually prepends 4 bytes which are two bytes of zeroes followed by a 16-bit EtherType, so 0x86dd for IPv6 which is hard-coded in the program. What follows is a standard IPv6 datagram. Since the IPv6 payload length field only gives the payload length, we have to add 44 additional bytes (40 for base IPv6 header + 4 for TUN encapsulation) to find out how many bytes were actually sent. This is the source of the hard-coded 44 in the software.

Tearing everything down is as simple as stopping the serialipv6 program (I use killall serialipv6) and then, optionally, removing the tun0 device with ip -6 tuntap del mode tun dev tun0.

Conclusion

While standard SLIP should probably be called SLIPv4 and does not support IPv6 on Linux-based platforms, one can send IPv6 traffic via serial port using a technique like the one shown. It's not meant to be a definitive implementation, but I'm hoping that the next person who might ask this question will save a lot of time and benefit from this answer.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.