This article continues the series begun last month by getting two Raspberry Pis to communicate over a 6LoWPAN network. It looks at how to make them talk to other IPv6 hosts on different network segments, necessary to get IoT data off the sensors and onto the internet.
In the first article in this series (in the November 2016 issue), I described how to configure two Raspberry Pis to talk using the low power wireless protocol 6LoWPAN over IEEE 802.15.4 with the OpenLabs wireless module. As an example, I showed Python code running a server on one RPi to deliver CPU temperature data to a client on another.
But, that isn't the real point of using 6LoWPAN. You could have done the same thing using Zigbee, Bluetooth Low Energy, Z-Wave or some other low power networking system. The point of using 6LoWPAN is that it creates and sends IPv6 packets. This potentially brings it into the wider internet world where IP packets can be routed across multiple hosts without having to decode and re-code the packets.
The 6LoWPAN network generates IPv6 packets. The internet is very slowly moving across to IPv6, but much of it is still IPv4. If you have to deal with an IPv4 network, these are your choices:
Decode the packets on the RPi and use the data in them to talk to IPv4 hosts thereafter. That's what you could have done after following along with the first article in this series—you had decoded a packet and then could manipulate it or send it on.
Tunnel across the IPv4 network to another IPv6 network and carry on from there.
Use NAT'ing techniques to convert IPv6 packets into IPv4 packets automatically. This isn't easy and is beyond the scope of this article. (If you are interested, look up NAT64.)
I'm going to assume you can continue to use IPv6, so the RPi is in an IPv6 6LoWPAN network on one side and an IPv6 Ethernet/Wi-Fi network on the other.
This article looks at how to get IPv6 packets from a 6LoWPAN network and route them into an IPv6 network (and of course, back the other way). As in the first article, if you're following along, you will use Raspberry Pis with the same OpenLabs modules. And just like in the first article, there are a number of problems to be resolved along the way.
The goal is to get suitable IPv6 addresses generated on all the 6LoWPAN devices and for one of these devices to act as a router (an “edge router”) between the 6LoWPAN network and some other IPv6 network. This article actually ends up really being about routing between IPv6 link local networks, joining them into a global IPv6 network.
IPv6 has several different types of addresses, just like IPv4 does. Link local addresses are visible only on a single link, and you can't route them. They are like link local IPv4 addresses, and they're in the address range fe80::/10. There also are site local addresses in the range fec0::/10, but these are deprecated. Multicast addresses are in the ff00::/8 range. The loopback address is ::1/128. Every other address is a global address.
Whenever you reboot the OpenLabs module, it gives itself a new MAC address. This is used to generate the link local address, and later you will see this used as part of the process to generate a global address. For the gateway, you will need a fixed global address, or external clients won't know how to find it. So for the gateway, you should fix the MAC address to ensure that you get a “known” global address.
General IPv6 addresses also are difficult to read and remember—16 hexadecimal numbers. There are special simplification rules for addresses with zeros in them, so I will exploit those here so you get simple addresses (for this article only, of course). You will set simple addresses on the gateway (needed) and on the sensor (convenient).
The MAC address 02:0:0:0:0:0:0:1 generates the IPv6 host address ::1, which is about as simple as you can get. Set that on the gateway with:
ip link set dev wpan0 address 02:0:0:0:0:0:0:1
to give IPv6 host part ::1, and for convenience on the sensor with:
ip link set dev wpan0 address 02:0:0:0:0:0:0:2
When any host starts its networking, it is assigned an IPv6 link local address automatically, based on its MAC address, which you have just set on the gateway and sensor RPis. Routing tables also are set up on the local link, so hosts on the same link can talk to each other directly.
It's easy to tell which addresses are link local addresses; they start with the prefix fe80:. The ifconfig program on any Linux/UNIX box will show something like this:
inet6 addr: fe80::84f1:df50:eb27:97ff/64 Scope:Link
Because you've fixed the MAC address on the gateway, it's just:
inet6 addr: fe80::1/64 Scope:Link
Link local packets aren't routable—that is, you can't send packets from one link to another link. To route packets from one link to another, they must have a unique global or unique local address.
Unique global addresses will be given to you by your internet provider, or you can buy them from an organization like ARIN. Australia's internet providers are way behind and very few of them support IPv6. I don't want to buy one when I can't use it yet. But, unique local addresses are good enough. I can route them across my private network for free, across all my network segments. I probably won't ever want to broadcast my temperature data across the whole internet anyway—at most I would process it on my own network or send it to a particular cloud service.
The unique-local-ipv6.com website generates random unique local /48 prefixes, such as:
fd28:e5e1:869::/48
That leaves you 80 bits (128–48) for any subnets you want to create and unique addresses within those subnets. So you can specify any 80 bits you want, or easier, any 20 hex digits. I'm going to cheat a bit and simplify this to prefix fd28::/64 as a 64-bit prefix. Use this on the gateway explicitly by setting:
ip addr add fd28::1/64 dev lowpan0
The RPi you are going to use as the gateway must have two NICs. Well, this one does: the 6LoWPAN device and the Ethernet device. But just like any router in any UNIX system, it has to be configured for packet forwarding between the NICs. This is really easy: edit the /etc/sysctl.conf file and uncomment the line:
net.ipv6.conf.all.forwarding=1
Then reboot, and it's an IPv6 router.
You now have one fixed routable address that will be used for external clients to talk to the gateway/router. You also have a fixed link local address for hosts on this local link to talk to the gateway. At present, you have only one other RPi in the network, but your 6LoWPAN network might consist of hundreds or even thousands of nodes, and they need to be configured too, ending up with routable addresses so that external clients can get and set information on the sensors/actuators. But, you don't want to be assigning addresses manually to every one of them!
The answer is stateless address autoconfiguration using router solicitation and router advertisements. You have to set up and configure router advertising, but then it becomes a no-brainer. This is the IPv6 equivalent of DHCP.
A new IPv6 node attempting to join a network will send out a router solicitation message using IPv6 multicast on its link local network. A router then will generate a router advertisement, which it will send back using unicast, which will contain enough information for the new node to configure itself.
The information supplied in the router adverts has basically two components:
The link local address of the router, so that the node can send it messages.
A prefix to be used as the network component of a routable address, to be used by the node to generate a unique local routable address for the node.
That's why you need a fixed link local address for the router to be used in router adverts. This is in addition to the fixed routable address, so that external clients can talk to the 6LoWPAN side of the gateway.
The Linux dæmon to act as a router advertisement dæmon for UNIX-like systems is radvd. The version in the RPi repositories is unfortunately out of date, so you need to get a current version from GitHub and build it:
git clone https://github.com/linux-wpan/radvd.git -b 6LoWPAN cd radvd ./autogen.sh ./configure --prefix=/usr/local --sysconfdir=/etc ↪--mandir=/usr/share/man make sudo make install
You may need to install bison from the repositories if it can't find flex.
Once built and installed, radvd uses the configuration file /etc/radvd.conf with the following contents:
interface lowpan0 { AdvSendAdvert on; # UnicastOnly on; AdvCurHopLimit 255; AdvSourceLLAddress on; prefix fd28::/64 { # AdvOnLink off; AdvOnLink on; AdvAutonomous on; AdvRouterAddr on; }; abro fe80::1 { AdvVersionLow 10; AdvVersionHigh 2; AdvValidLifeTime 2; }; };
This is adapted from Sebastian Meiling's page “Setup native 6LoWPAN router using Raspbian and RADVD” (https://github.com/RIOT-Makers/wpan-raspbian/wiki/Setup-native-6LoWPAN-router-using-Raspbian-and-RADVD). The prefix is the random prefix I used earlier, fd28::/64. The abro (“Authoritative Border Router Option”) is the link local address of the router. You will need to set your own addresses—at a minimum, the routable prefix.
I've made a couple changes to Sebastian's configuration: I've set AdvOnLink to On; whereas he has it as Off. Setting the advert to On means:
The router itself won't get an address. That's okay; you want it to have a fixed address not assigned by radvd.
For each node that has its IPv6 address set by radvd, entries will be made in the routing table to route fd28::/64 addresses through the 6LoWPAN device. Addresses with this prefix are “on this link”.
Most important, general addresses (::/0) will route using the lowpan0 NIC through the link local gateway address fe80::1 to the external world.
I've also removed the UnicastOnly on setting. The reasons are:
Router adverts contain a timeout, defaulting to 30 minutes.
Unless updated, hosts will remove the routing entry on expiration of the timeout.
Hosts don't usually request new router adverts, only once on startup; they expect the router to multicast new adverts every few minutes.
The UnicastOnly on setting stops radvd from sending out these adverts, so you need to remove it to allow the routing tables on hosts to be renewed.
All the work on the 6LoWPAN side is now done. On the Ethernet side, I also want to have an IPv6 network, and as I am using unique local addresses, this network will be my private network, probably with many link segments. To change it to be internet-global, I would just need to change the unique local addresses to unique global addresses.
Initially I had problems routing IPv6 packets on my private network. My home router (a Linksys EA6900) didn't seem to want to route packets from my “external” host through to the gateway. I fixed that by using a cross-over cable directly from my “external” host to the gateway, and then after all, the home router decided to cooperate. Then with radvd also delivering adverts on the Ethernet side to my “external” host, I could ping from the 6LoWPAN network to the Ethernet network and vice versa.
In summary, the steps to go through on the 6LoWPAN side are:
Configure /etc/radvd.conf.
Bring up the 6LoWPAN device.
Set link local and routable addresses on the 6LoWPAN device.
Start up radvd.
The radvd configuration file is described above. The startup script for the rest should be run as root and is:
#!/bin/bash # set the MAC address ip link set dev wpan0 address 02:0:0:0:0:0:0:1 iwpan dev wpan0 set pan_id 0xbeef ip link add link wpan0 name lowpan0 type lowpan ifconfig wpan0 up ifconfig lowpan0 up # set the gateway address on the 6LoWPAN side ip addr add fd28::1/64 dev lowpan0 # start the router advert daemon radvd -m stderr
On the Ethernet side, I also had configured /etc/radvd.conf to deliver adverts with the fd44:: prefix, but I didn't get around to simplifying the Ethernet MAC addresses.
The resulting IPv6 addresses on the gateway are:
eth0 Link encap: ... inet6 addr: fd44:::4adf:10a9:5c79:7954/64 Scope:Global inet6 addr: fe80::4adf:10a9:5c79:7954/64 Scope:Link lowpan0 Link encap: ... inet6 addr: fd28::1/64 Scope:Global inet6 addr: fe80::1/64 Scope:Link
The RPi acting as sensor doesn't have to do much; radvd does most of it. The startup script is just:
#!/bin/bash ip link set dev wpan0 address 02:0:0:0:0:0:0:2 iwpan dev wpan0 set pan_id 0xbeef ip link add link wpan0 name lowpan0 type lowpan ifconfig wpan0 up ifconfig lowpan0 up
But, courtesy of radvd, the device now has an IPv6-routable address: fd28::2, as shown by ifconfig:
lowpan0 Link encap: ... inet6 addr: fd28::2/64 Scope:Global inet6 addr: fe80::2/64 Scope:Link
The routing table on the sensor RPi looks like:
$ route -A inet6 Kernel IPv6 routing table Destination Next Hop Flag Met Ref Use If fd28::/64 :: UAe 256 0 0 lowpan0 fe80::/64 :: U 256 0 0 lowpan0 ::/0 fe80::1 UGDAe 1024 0 0 lowpan0 ff00::/8 :: U 256 1 18 lowpan0
As you can see, addresses with the prefix fd28::/64 are on the link through the lowpan0 device. The address ::/0 is the default route address, so all other packets are routed through the lowpan0 NIC via the Next Hop address fe80::1.
You can test this from each RPi by pinging the other RPi. That just tests local routing though. To test this properly, you need to be able to talk through the Ethernet/Wi-Fi NIC on the RPi router to another IPv6 device.
I've got the RPi router talking to an “external” host through a crossover cable for simplicity, with radvd delivering router adverts to it.
So then from the desktop, I can ping my RPi sensor:
$ping6 fd28::2 PING fd28::2(fd28::2) 56 data bytes 64 bytes from fd28::2: icmp_seq=1 ttl=254 time=14.0 ms 64 bytes from fd28::2: icmp_seq=2 ttl=254 time=16.4 ms 64 bytes from fd28::2: icmp_seq=3 ttl=254 time=17.9 ms
If you get successful pings, you know it works.
With that in place, the server code from my previous article (in the November 2016 issue) can be modified to use routable addresses rather than link local addresses. This basically means you don't have to specify the “scope id” (the NIC) anymore:
#!/usr/bin/python3 import socket from subprocess import PIPE, Popen HOST = '' # Symbolic name meaning all available interfaces PORT = 2016 # Arbitrary non-privileged port def get_cpu_temperature(): process = Popen(['vcgencmd', 'measure_temp'], stdout=PIPE) output, _error = process.communicate() return output def main(): s6 = socket.socket(socket.AF_INET6, socket.SOCK_STREAM, 0) s6.bind((HOST, PORT, 0, 0)) s6.listen(1) while True: conn, addr = s6.accept() conn.send(get_cpu_temperature()) conn.close() if __name__ == '__main__': main()
The client gets modified similarly, omitting the scope id:
#!/usr/bin/python3 import socket import time ADDR = 'fd28::2' PORT = 2016 def main(): while True: s6 = socket.socket(socket.AF_INET6, socket.SOCK_STREAM, 0) s6.connect((ADDR, PORT, 0, 0)) data = s6.recv(1024) print(data.decode('utf-8'), end='') # get it again after 10 seconds time.sleep(10) if __name__ == '__main__': main()
The output from that on the client is:
temp=38.5'C temp=38.5'C temp=39.0'C ...
This article has shown that 6LoWPAN devices can communicate to other IPv6 systems on the routable internet. It has been mainly a journey about configuring IPv6 systems and setting up the IPv6 equivalent of DHCP. However, the story for low power wireless isn't over yet. The IoT at the application layer is standardizing on the CoAP and MQTT protocols, and in my next article, I'll take a look at the CoAP application protocol.