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:
- install the tuntap drivers
- create the
tun device
- write userland software to fetch packets from the
tun device and write them to the serial device (and vice versa)
- 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.