Thursday, March 9, 2017

Connecting two Pis (or other boxes) directly via TCP/IP without a webserver

Hardware:  Two Raspberry Pi 3 Model B's


Software: 2017-01-11 Raspbian Jessie Lite (minimal) / Python


Sometimes you just need something simple.  

In fact, sometimes the layers of possibilities included with webservers and other high-level communications methods are not exactly a benefit.  In particular, when you're dealing with very basic inter-system communications that are more about keep-alive and simple status messages, larger libraries/methods sometimes come with more burdens than benefits.  This is where socket connections can be applied.


While of course, it is always best to secure things, sometimes the meaning behind the word "secure" is not as simple as it sounds.  In the example below, the information being transmitted is not secured (it is clear text), but the use of a simple TCP/IP protocol which is extremely limited in capabilities can very well be how "the system" is secured.  If we are just talking about "heartbeat" style communications on systems then in many cases the actual contents of the transmission are simply verified and then discarded.  


When we implement higher-level protocols with more features we inherently bring some risk into our own overall systems which requires a more vigilant approach for ongoing security.  When we are dealing with small systems (e.g. Raspberry Pi, oDroid, etc.), sometimes security updates can get "forgotten" or are beneath the notice of IT teams who can't imagine such a small thing could pose such a high risk.

H1N1 virus
H1N1: Come at me bro!

So for certain things, simple is still the way to go.

Is this basic example secure?  Well, we'd need to clean up a few other items per message length and enforce some restrictions there including what we do with the messages we send/receive (e.g. trimming them and discarding extraneous data).  Additionally, if you really wanted to start heading down the road toward secured data within this kind of transaction, then you might start reading up on a Python TLS/SSL wrapper for sockets at:
Regardless, to keep things reasonably simple as a starting point, this method of setting up two Raspberry Pi's (or other Linux-style systems) to communicate with each other via a simple TCP/IP protocol transaction is a good basic example of keeping things simple. 

Source Code

A little Googling led me to Doug Hellman's blog and gave me some basic Python source code on communicating between two boxes:

Server-side (tcpserver.py):

import socket
import sys

# Create a TCP/IP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Bind the socket to the address given on the command line
server_name = sys.argv[1]
server_address = (server_name, 10000)
print >>sys.stderr, 'starting up on %s port %s' % server_address
sock.bind(server_address)
sock.listen(1)

while True:
    print >>sys.stderr, 'waiting for a connection'
    connection, client_address = sock.accept()
    try:
        print >>sys.stderr, 'client connected:', client_address
        while True:
            data = connection.recv(16)
            print >>sys.stderr, 'received "%s"' % data
            if data:
                connection.sendall(data)
            else:
                break
    finally:
        connection.close()

Client-side (tcpclient.py):

import socket
import sys

# Create a TCP/IP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Connect the socket to the port on the server given by the caller
server_address = (sys.argv[1], 10000)
print >>sys.stderr, 'connecting to %s port %s' % server_address
sock.connect(server_address)

try:

    message = 'This is the message.  It will be repeated.'
    print >>sys.stderr, 'sending "%s"' % message
    sock.sendall(message)

    amount_received = 0
    amount_expected = len(message)
    while amount_received < amount_expected:
        data = sock.recv(16)
        amount_received += len(data)
        print >>sys.stderr, 'received "%s"' % data

finally:
    sock.close()

While I've left this "as-is" and haven't really changed this around my own use-case, it is still a pretty good basic example of a starting point.

Setup

To get this running as a basic test, you should log into each of your RPi(s), open an editor and cut/paste the code above into the client/server respectively.  For example, after logging in to the first Pi (server) type in the following:

sudo nano tcpserver.py

Then paste in the first code segment above.  Then hit CTRL-X, Y, <Enter>.  On the second Pi (client), log in and again type in the following:

sudo nano tcpclient.py

Then paste in the second code segment above. Then again hit CTRL-X, Y, <Enter>.  

Running the code now will allow the two boxes to communicate.  

NOTE:  The first time I tried this I couldn't get the boxes to talk and started checking for firewall settings and such on Linux.  This was an error.  Always start simple.  In my case, a reboot of the Pi's and my local switch solved the problem.

To test what you've done thus far you'll need the IP address that the server should be listening to and the client should be talking to.  So first we need the IP address of our "server".  

Type in the following command on the first pi (server):

ifconfig 

On a default Pi install, this will give you output similar to:

eth0      Link encap:Ethernet  HWaddr b8:27:eb:3d:50:5c
          inet addr:192.168.1.30  Bcast:192.168.1.255  Mask:255.255.255.0
          inet6 addr: fe80::fd36:15c8:e25b:afac/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:1601946 errors:0 dropped:15605 overruns:0 frame:0
          TX packets:20303 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:95590888 (91.1 MiB)  TX bytes:1256399 (1.1 MiB)

lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          inet6 addr: ::1/128 Scope:Host
          UP LOOPBACK RUNNING  MTU:65536  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:1
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

wlan0     Link encap:Ethernet  HWaddr b8:27:eb:68:05:09
          inet6 addr: fe80::675a:7904:8aac:dd22/64 Scope:Link
          UP BROADCAST MULTICAST  MTU:1500  Metric:1
          RX packets:1 errors:0 dropped:1 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:60 (60.0 B)  TX bytes:0 (0.0 B)

If you are connected via your wired Ethernet (as I was) then the first section on eth0 is for you.  The relevant part is the section:  inet addr:192.168.1.30.  This is the current IP Address of the unit.  

If you are using a wireless connection then the wlan0 section would be what you'd look at.

NOTE: I didn't test this w/ IPv6.  I have no idea if it will work properly using that vs. IPv4.

Once you have the IP Address you can get along to testing your setup.

NOTE: if you used the IP address in the lo section (127.0.0.1) then this is only for local loopback and cannot be reached by any other device on the network.  However, with some minor work, this means that you actually could run both the client and the server code on a single RPi.

Running the code

Start on the server and type in the following (replace the 192.168.1.30 IP Address I use here w/ the address of your Pi server):

sudo python tcpserver.py 192.168.1.30

This will fire up the listener and you'll get output similar to the following:

starting up on 192.168.1.30 port 10000
waiting for a connection

Assuming you have it running correctly, then your Pi is now listening on port 10000 at the IP address 192.168.1.30 (or whatever your IP Address is).

Now go to the RPi client and type in a line similar to the following (replace the 192.168.1.30 again with the IP Address of your RPi server):

sudo python tcpclient.py 192.168.1.30  

Assuming the two units are able to reach each other you'll get the following on the client:

sending "This is the message.  It will be repeated."
received "This is the mess"
received "age.  It will be"
received " repeated."

On the RPi server you should also see something similar to:

client connected: ('192.168.1.46', 57882)
received "This is the mess"
received "age.  It will be"
received " repeated."
received ""
waiting for a connection

This means that the client sent the message to the server, it received it and sent back what it received.  Which is essentially how you could do some very basic error-correction to ensure your communication went through properly.

Troubleshooting

If you get an error when trying to start the server code, you most likely left off the IP address from the line above.  For example:

Traceback (most recent call last):
  File "socket_server.py", line 8, in <module>
    server_name = sys.argv[1]
IndexError: list index out of range

The code is expecting an argument and since it didn't find one it is failing (and not gracefully).

As the code exists, you might get the following error when running the client side:

connecting to 192.168.1.30 port 10000
Traceback (most recent call last):
  File "socket_client.py", line 10, in <module>
    sock.connect(server_address)
  File "/usr/lib/python2.7/socket.py", line 224, in meth
    return getattr(self._sock,name)(*args)
socket.error: [Errno 111] Connection refused

This means that the two units can communicate with each other over the network (e.g. ping), but the server refused the connection.  While it is possible you could shut the port down via other means, in this example the most likely situation is just that the server side code is not running.  Start or restart the server side and worst case reboot the server and try again.

If you at first only get something similar to the following when running the client side:

connecting to 192.168.1.30 port 10000

Then eventually get:

Traceback (most recent call last):
  File "socket_client.py", line 10, in <module>
    sock.connect(server_address)
  File "/usr/lib/python2.7/socket.py", line 224, in meth
    return getattr(self._sock,name)(*args)
socket.error: [Errno 110] Connection timed out

Then the client is attempting to reach 192.168.1.30 (or your server's IP) but is unable to reach the server or get any response.  You should check your network connections, reboot any networking gear between the two units and/or reboot the RPi units as well.

Learning more

I would definitely go back to Doug's blog and read through his article in full.  He gives some additional details on what some of the specific bits of code do here.  It can be very edifying.  As well, the link above to a library for TLS/SSL would be the next level to build on top of this design.   There is also a good walk through of sockets as a whole at https://docs.python.org/2/howto/sockets.html.

Finally, always keep in mind that anything sent to you from the device you expect might also come from one you don't.  So you should definitely be thinking about the kind of havoc that might be created in your code if suddenly some "other device" on the network started trying to open sockets every 1ms to your server and attempts to send the entire contents of Wikipedia at it.  

Sometimes simple solutions like this are your friend, but that doesn't mean you can let your guard down completely.

No comments:

Post a Comment