Thursday 1 October 2020

Streaming over 4G with a VPN connection

Installed: 2020-08-20-raspios-buster-armhf-lite.img

After installation if you can mount /boot before inserting it in the Pi then touch ssh so you can start the Pi without having to connect a monitor and keyboard.
$ sudo touch {sdmountpoint}/boot/ssh

- Insert the card into your Pi then login as the user 'pi' with the default password 'raspberry'
$ sudo raspi-config
1. Change User Password
4. Localistation Options
    I2 Change Time Zone
8. Update
5. Interfacing Options
    P1 Camera (<Yes> we want to enable it)
    P2 SSH (<Yes> we want to enable it if not already done so)
7. Advanced Options
    A3 Memory Split (change to 256 or 512)
<Finish>
You can reboot now or wait until after the OS update.

- $ sudo apt-get update
- $ sudo apt-get upgrade
- $ sudo reboot

Install the VPN package
$ sudo apt-get install openvpn

OpenVPN is an excellent and very configurable solution. The below setup gives a very basic guide on getting a simple connection working.

In this example the Pi (10.8.0.2) will be the VPN client and our 'monitoring' machine (10.8.0.1) will be the VPN server.

[ On your monitoring machine (the assumption is you also have a user called 'pi' here)]
$ cd
$ openvpn --genkey --secret ~/static.key
$ vi server.conf
dev tun
ifconfig 10.8.0.1 10.8.0.2
secret /home/pi/static.key

- Start the VPN server
$ sudo /usr/sbin/openvpn /home/pi/server.conf

[ On your Pi ]
$ cd
Copy the static.key file you created earlier to /home/pi and change permissions to 600
$ chmod 600 /home/pi/static.key
$ vi client.conf
remote mypublicip.ordomain
dev tun
ifconfig 10.8.0.2 10.8.0.1
secret /home/pi/static.key

I've tested using a Huawei E3372 USB dongle and a Sierra AirCard 320U . The Huawei configures an interface with an IP address automatically. If this is the case for you then skip to 'Start the VPN client'. The Sierra does not so we need to create a PPP interface and 'dial up' using wvdial for this card.

Type 'ip a' to see what interfaces you have after connecting your 3G/4G card.

Configuring the 'wvdial' package (if required)
$ sudo apt-get install wvdial

To fill in the configuration file you will need the APN information from your SIM provider. This easiest way to get this is to examine the network connection information with the SIM in a mobile phone. Alternatively you can try https://www.apnsettings.org/{countryname}

- Edit /etc/wvdial.conf
The most important fields to update here are 'Init5' and 'Modem', you may need less or more settings than below. For my KPN connection I had to add the 'Remote Name' variable, for T-mobile it was not needed.

My /etc/wvdial.conf file (with KPN) looks like this;

[Dialer Defaults]
Init1 = ATZ
Init2 = ATQ0 V1 E1 S0=0 &C1 &D2 +FCLASS=0
Init3 = ATS0=0
Init4 = AT+COPS?
Init5 = AT+CGDCONT=1,"IP","internet"
Remote Name = "KPN Mobiel Internet"
Modem Type = USB Modem
Stupid Mode = 1
New PPPD = yes
Modem = /dev/ttyUSB3
ISDN = 0
Phone = *99#
Password = { }
Username = { }
Dial Command = ATD

To test, run wvdial. You should see an output similar to this...
$ sudo wvdial
--> WvDial: Internet dialer version 1.61
--> Cannot get information for serial port.
--> Initializing modem.
--> Sending: ATZ
OK
--> Sending: ATQ0 V1 E1 S0=0 &C1 &D2 +FCLASS=0
OK
--> Sending: ATS0=0
OK
--> Sending: AT+COPS?
+COPS: 0,0,"NL KPN",0
OK
--> Sending: AT+CGDCONT=1,"IP","internet"
OK
--> Modem initialized.
--> Sending: ATD*99#
--> Waiting for carrier.
CONNECT 42000000
--> Carrier detected.  Starting PPP immediately.
--> Starting pppd at Wed Sep 30 22:15:00 2020
--> Pid of pppd: 2667
--> Using interface ppp0
--> local  IP address 200.121.12.16
--> remote IP address 100.164.204.204
--> primary   DNS address ???.???.???.??
--> secondary DNS address ???.???.???.??

If I check my interface table now I have a ppp0 interface;
$ ifconfig ppp0
ppp0: flags=4305<UP,POINTOPOINT,RUNNING,NOARP,MULTICAST>  mtu 1500
        inet 200.121.12.16  netmask 255.255.255.255  destination 100.164.204.204
[...]

- Start the VPN client on the Pi
$ sudo /usr/sbin/openvpn /home/pi/client.conf
Openvpn should create and enable an interface called 'tun0'.

Make sure the openvpn port (default 1194) is open on your router for UDP and is forwarding to the server machine. For example, requests to port 1194 on your routers public address are forwarded to your VPN servers normal internal IP address.

All being well you should be able to connect from the VPN server to the Pi (VPN client) via IP 10.8.0.2
$ ssh pi@10.8.0.2
The authenticity of host '10.8.0.2 (10.8.0.2)' can't be established.
ECDSA key fingerprint is SHA256:yDbY8+0cAMOvZzwf+Z/KDPruNiaebhcsM/acA719eZA.
Are you sure you want to continue connecting (yes/no/[fingerprint])? 

You will probably want openvpn to start on the Pi automatically. A simple way is to place it in /etc/rc.local
If you are also using wvdial include the following two lines above the openvpn line...
/usr/bin/wvdial &
sleep 4

For example...

[...]
# Print the IP address
_IP=$(hostname -I) || true
if [ "$_IP" ]; then
  printf "My IP address is %s\n" "$_IP"
fi

/usr/sbin/openvpn /home/pi/client.conf &

exit 0

- Reboot the Pi without any ethernet or WiFi connection and see if you can login to pi@10.8.0.2

To get a rough idea of your connection speed you can run speedtest. The results you get will depend on many factors.
$ sudo apt-get install speedtest-cli
$ speedtest --simple
Ping: 80.068 ms
Download: 9.84 Mbit/s
Upload: 9.55 Mbit/s

- Here is an example of streaming the Pi camera using a python script
$ sudo apt-get install python3-picamera
$ vi mjpeg_streaming_demo.py

#!/usr/bin/python3
import io
import picamera
import logging
import socketserver
from threading import Condition
from http import server

PAGE="""\
<html>
<head>
<title>picamera MJPEG streaming...</title>
</head>
<body>
<h1>PiCamera MJPEG Streaming Demo</h1>
<img src="stream.mjpg" width="640" height="480" />
</body>
</html>
"""

class StreamingOutput(object):
    def __init__(self):
        self.frame = None
        self.buffer = io.BytesIO()
        self.condition = Condition()

    def write(self, buf):
        if buf.startswith(b'\xff\xd8'):
            # New frame, copy the existing buffer's content and notify all
            # clients it's available
            self.buffer.truncate()
            with self.condition:
                self.frame = self.buffer.getvalue()
                self.condition.notify_all()
            self.buffer.seek(0)
        return self.buffer.write(buf)

class StreamingHandler(server.BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == '/':
            self.send_response(301)
            self.send_header('Location', '/index.html')
            self.end_headers()
        elif self.path == '/index.html':
            content = PAGE.encode('utf-8')
            self.send_response(200)
            self.send_header('Content-Type', 'text/html')
            self.send_header('Content-Length', len(content))
            self.end_headers()
            self.wfile.write(content)
        elif self.path == '/stream.mjpg':
            self.send_response(200)
            self.send_header('Age', 0)
            self.send_header('Cache-Control', 'no-cache, private')
            self.send_header('Pragma', 'no-cache')
            self.send_header('Content-Type', 'multipart/x-mixed-replace; boundary=FRAME')
            self.end_headers()
            try:
                while True:
                    with output.condition:
                        output.condition.wait()
                        frame = output.frame
                    self.wfile.write(b'--FRAME\r\n')
                    self.send_header('Content-Type', 'image/jpeg')
                    self.send_header('Content-Length', len(frame))
                    self.end_headers()
                    self.wfile.write(frame)
                    self.wfile.write(b'\r\n')
            except Exception as e:
                logging.warning(
                    'Removed streaming client %s: %s',
                    self.client_address, str(e))
        else:
            self.send_error(404)
            self.end_headers()

class StreamingServer(socketserver.ThreadingMixIn, server.HTTPServer):
    allow_reuse_address = True
    daemon_threads = True

with picamera.PiCamera(resolution='640x480', framerate=24) as camera:
    output = StreamingOutput()
    camera.vflip = True
    camera.start_recording(output, format='mjpeg')
    try:
        address = ('', 8000)
        server = StreamingServer(address, StreamingHandler)
        server.serve_forever()
    finally:
        camera.stop_recording()

- Change the file permissions, start it running on your Pi then connect on your VPN server
$ chmod 755 mjpeg_streaming_demo.py
$ ./mjpeg_streaming_demo.py
On your VPN monitoring server connect to http://10.8.0.2:8000 via a web browser

Nice job if you have everything working!

No comments:

Post a Comment