Why block your own camera?

With home automation it’s now easy and fun to create your own home security setup. To that end you’ll want to have a number of sensors of various kinds, but in all likelihood, you’ll complement that with an IP camera to record pictures of the bad guys if they ever break in.

Now, there’s a few models of cheap IP cameras out there that can do the job. The one I’m using is TP-Link’s Tapo C110 one, which is very cheap on Amazon.

  Side note: I don’t necessarily recommend that camera. I initially intended to use it with micro-SD cards to record feeds in an additional place, apart from my server’s hard drive and a remote personal cloud. But it keeps spitting out errors about the card after hours or days when I set this up, with both cameras I have and 2 different SD cards. But… it’s cheap.

However, we’ve all read how IP camera makers suck at security, and people get their home feeds hacked without even knowing it, because the camera opens a tunnel to the maker’s infrastructure to allow for remote access. Remote access from an app is convenient, but if security is not taken seriously, the cost can be high.

Now, in my case, the camera’s motion detection and stream are set up and consumed by my own server, inside my LAN. If the alarm goes off, recording takes place locally, and then the server pushes copies of the videos to a remote place. For all of that to happen, the camera does not need to access the outside world. And because it does it anyway by default without a way to block it, it’s putting my private data at risk.

So let’s use some cool Linux stuff and prevent it from doing so, by turning a cheap server into a WiFi access point that controls who talks to what.

Overview of the setup

I’m addressing the problem with the following:

  • a Raspberry Pi (in this case a model 4B but something smaller would work)
    • connected to the main LAN via ethernet
    • creating a WiFi access point specifically for the cameras (and any other devices in need of similar “taming”)
  • Raspberry Pi OS bullseye (11)
  • the main protagonists will be dnsmasq and hostapd, and a custom script setting things up with iptables. With those guys we’ll turn the Raspberry into a router.

Note that nowadays I set up all my server stuff with Ansible. So I’ll be explaining things here both as if running the steps manually, and provide the ansible configuration that corresponds at the end (but I won’t show a standalone Ansible playbook, that’s beyond the scope of this article).

We’ll go about the setup in the following order:

  • installing stuff
  • setting up the WiFi hot spot
  • setting up DHCP
  • setting up routing

Preparing the server

We’re going to install the following packages:

sudo apt update && sudo apt install dnsmasq hostapd iptables

And we need to ensure WiFi is not being suppressed on the Raspberry:

sudo rfkill list
# 0: phy0: Wireless LAN
# 	Soft blocked: no
# 	Hard blocked: no
# 1: hci0: Bluetooth
#	Soft blocked: yes
#	Hard blocked: no

If the Wifi has a yes in soft or hard blocked, we need to unblock it with

sudo rfkill unblock 0

This setting is both applied immediately and saved for future reboots, so running the command once is enough.

Setting up the WiFi access point

Let’s set up hostapd. We’ll need to set the contents of /etc/hostapd/hostapd.conf to this:

interface=wlan0
driver=nl80211
ssid=my_hotspot
# 2.4GHz
hw_mode=g
channel=6
# set to 1 to enable QOS
wmm_enabled=0 
macaddr_acl=0
# WPA, not WEP
auth_algs=1
ignore_broadcast_ssid=0
# WPA-2
wpa=2
wpa_passphrase=somesecurepassword
wpa_key_mgmt=WPA-PSK
wpa_pairwise=TKIP
rsn_pairwise=CCMP

To be honest, I don’t know what some of these lines are for. I found this somewhere and it works for me. Of particular relevance will be:

  • ssid and wpa_passphrase need to be adjusted to what you want to set up as hotspot name and password
  • channel is set to 6, that works okay for me since my main WiFi access point is sitting next to the Raspberry and is on channel 1.
  • WEP is disabled via auth_algs=1 and wpa version is 2, which should be okay

Now we can enable the service. By default after installation it is masked (since it has no valid configuration before we set up the file above). So we’ll do:

sudo systemctl unmask hostapd
sudo systemctl enable --now hostapd

Now you should be able to see the hotspot from WiFi devices like your phone. If you connect, however, you won’t have internet (or any kind of) access. We’ll address that in the next section.

If you leave this as it is, however, after rebooting the hotspot won’t be visible anymore. That’s because of a race condition between the DHCP client and hostapd, since the DHCP client, at least in my case, starts after hostapd. To prevent this we’ll edit /etc/dhcpcd.conf and add the following at the end:

interface wlan0
nohook wpa_supplicant

I’m not 100% sure of everything that’s happening here, but I believe this avoids dhcpcd triggering wpa_supplicant which in turns would override the WiFi module’s settings and changing it back from hot post to client.

Setting up a DHCP server with dnsmasq

Now on to setting up a DHCP server. Your cameras and other devices connecting to the WiFi hotspot will rely on DHCP to get their IP address. dnsmasq is a lightweight server that is capable of simple DNS services, but also of DHCP. We’ll set it up in such a way that it doesn’t do anything related to DNS but simply handles DHCP server tasks. Edit /etc/dnsmasq.conf as follows:

interface=wlan0
port=0
dhcp-range=10.0.0.50,10.0.0.150,12h
dhcp-option=6,1.1.1.1
dhcp-host=B4:B0:24:AA:BB:CC,10.0.0.84
dhcp-host=5C:A6:E6:AA:BB:CC,10.0.0.85

Here we’re telling dnsmasq to:

  • use the range of IP’s 10.0.0.50 to 10.0.0.150, with a lease time of 12 hours (the time after which devices have to ask for a renewal of the IP)
  • use 1.1.1.1 as DNS server – note: I’m actually using a LAN address in my case since I host my own DNS server. Consider that when we block internet access, access to an external DNS like 1.1.1.1 will be blocked too – but then, that’s probably not a problem anyway.
  • set up permanent addresses (within the range!) for a couple of MAC addresses corresponding to the cameras. This is not mandatory, but I do assign permanent IP addresses to my cameras so that home automation knows where to find them.

We also need to set up an IP address for the wlan0 interface. dnsmasq does require one before we can use it, and it should be set up with a subnet that covers the IP address range we’ve configured.

sudo ip addr add 10.0.0.1/24 dev wlan0
  This setting is not permanent and will be lost on reboot. We’ll address this with the script further down.

Now save and start dnsmasq:

sudo systemctl enable --now dnsmasq

Connected devices will now receive an IP address. Don’t get impatient though, we still don’t have full internet connectivity as we need to set up routing!

Setting up routing

Now we just need to tell our Raspberry to act as a router between its 2 interfaces, eth0 and wlan0, which are on different subnets.

First, we’ll let the kernel know about our intentions. By default packet forwarding is disabled and we enable it with:

sudo sysctl -w net.ipv4.ip_forward=1

This will take effect immediately and persist beyond reboot.

Let’s get another thing out of the way. Assuming the IP address of the Pi on eth0 is 192.168.1.20, we are going to turn it into a router that will forward traffic from the 192.168.1.0/24 subnet to the 10.0.0.0/24 one. That means that traffic directed at our IP cameras on 10.0.0.84 need to reach our Pi on 192.168.1.20. How will other devices know? Well, the easiest way for me was to tell my home router (the “internet box”) to route traffic meant for 10.0.0.0/24 via 192.168.1.20. Each home router is different but usually the advanced options allow you to set up a static route. The benefit is that for every device in the main network, 10.0.0.0/24 is nothing special, and to send traffic there, they need to direct layer-2 frames to the main router, like they would for any target outside the 192.168.1.0/24 subnet. The router will then send this to the Pi. Make sure you set up your router accordingly.

Now, on to adding the route on our Raspberry. It’s as easy as this:

sudo route add -net 10.0.0.0/24 dev wlan0

Essentially we’re saying: anything that goes to our new subnet needs to be routed out through the WiFi interface. By default, there’s already a route for everything else through the Ethernet interface, so traffic coming back from the IP cameras will get routed in the other direction without additional settings. Those 2 lines in the route output confirm this:

sudo route |grep " eth0" # grep just to filter out the 2 lines I want to highlight
# default         192.168.1.1     0.0.0.0         UG    202    0        0 eth0
# 192.168.1.0     0.0.0.0         255.255.255.0   U     202    0        0 eth0
  Note that the route we added will be lost on reboot. The script and service in the next section will address this.

Finally we’re ready for the plat de rรฉsistance: setting up iptables rules! Here’s the policy we want:

  • drop any forward packet by default
  • but accept the forwarding from eth0 to wlan0
  • and accept the forwarding from wlan0 to eth0… only if it’s meant for the local LAN!

So concretely, we want this sequence of commands:

  # general rule
  iptables -P FORWARD DROP
  # allow that way
  iptables -A FORWARD -i eth0 -o wlan0 -j ACCEPT 
  # allow the other way but on the one condition
  iptables -A FORWARD -o eth0 -i wlan0 -d 192.168.1.0/24 -j ACCEPT 

If we wanted to fully open communication, which is going to be useful for the initial setup of the cameras, for example, we could change that last line for:

  # allow the other way unconditionally
  iptables -A FORWARD -o eth0 -i wlan0 -j ACCEPT 

And that’s it. If you’ve followed all the steps, you should now be able to “jail” your cameras, while retaining full bidirectional access from your LAN. Test it by connecting a phone to the hotspot!

  As with previous sections, keep in mind that these iptables commands are lost upon reboot. Now we finally tackle this.

Writing a script to turn internet on/off for the camera

We’ve configured and enabled a few services that will keep running after a reboot. But we’ve encountered a few commands that don’t make changes permanent:

  • adding the IP address
  • adding the route from eth0 to wlan0
  • setting iptables rules

To fix that, we’re going to write a short script that will take care of setting things up as needed. And we’ll then create a service that will start after each boot to run said script.

Here goes the full script. Store it where you want; I’ll assume /root/wifirouter.sh later on.

#!/usr/bin/bash

undosetup () {
  iptables -D FORWARD -o eth0 -i wlan0 -j ACCEPT 2>/dev/null
  iptables -D FORWARD -i eth0 -o wlan0 -j ACCEPT 2>/dev/null
  iptables -D FORWARD -o eth0 -i wlan0 -d 192.168.1.0/24 -j ACCEPT 2>/dev/null
  route del -net 10.0.0.0/24
}

setupbasic () {
  ip addr add 10.0.0.1/24 dev wlan0
  iptables -A FORWARD -i eth0 -o wlan0 -j ACCEPT 2>/dev/null
  iptables -A FORWARD -o eth0 -i wlan0 -d 192.168.1.0/24 -j ACCEPT 2>/dev/null
  iptables -P FORWARD DROP 2>/dev/null
  route add -net 10.0.0.0/24 dev wlan0
}

setupfull () {
  ip addr add 10.0.0.1/24 dev wlan0
  iptables -A FORWARD -o eth0 -i wlan0 -j ACCEPT 2>/dev/null
  iptables -A FORWARD -i eth0 -o wlan0 -j ACCEPT 2>/dev/null
  iptables -P FORWARD DROP 2>/dev/null
  route add -net 10.0.0.0/24 dev wlan0
}

if [ "$1" = "undo" ]; then
  echo Unsetting packet forwarding for wifi point
  undosetup
  exit 0
fi

if [ "$1" = "block" ]; then
  echo Setting up iptable for blocking access to internet
  undosetup
  setupbasic
  exit 0
fi

if [ "$1" = "allow" ]; then
  echo Setting up iptable for allowing access to internet
  undosetup
  setupfull
  exit 0
fi

echo Use one of these options:
echo "  undo"
echo "  block"
echo "  allow"

exit 1

If I’m being honest, this can certainly be written more elegantly. But it works for me. I’m defining 3 functions which I call based on the arguments the script is run with (block which will be our startup setting, allow which will enable cameras to access internet, and undo which cancels the whole settings). I’m deleting the rules and adding new ones back when setting up, so that we can call the script several times without ending up with conflicting rules.

Now, we’ll just create a systemd service file to run this at startup. Create /etc/systemd/system/wifirouter.service with the following content:

[Unit]
Description=Restore iptables rules
Wants=network-online.target
After=network-online.target hostapd.service 
Before=dnsmasq.service # this script will set up the IP that dnsmasq needs to start properly

[Service]
Type=oneshot
ExecStart=/root/wifirouter.sh block

[Install]
WantedBy=multi-user.target

This is a one-shot service: it will run once and be happy with itself until the next boot, which is what we want. Also, we want systemd to run this after the WiFi hotspot is active, but before the DHCP server is running. This is because our script sets up the IP address for the wlan0 interface, and we need that IP for dnsmasq to start properly.

With that file save we can enable it:

sudo systemctl daemon-reload
sudo systemctl enable wifirouter

Use enable --now if you’ve skipped the manual configuration of IP, route and iptables in previous steps.

We’re done. You can reboot if you want to check everything still works, and you should be able to connect your IP cameras after running sudo /root/wifirouter.sh allow, and then sudo /root/wifirouter.sh block once you’re done.

The Ansible version

The Ansible version of everything that precedes is as follows:

# generated by chatgpt! needs customisation

- name: Install required packages
  apt:
    name:
      - dnsmasq
      - hostapd
      - iptables
  tags: packages

- name: Create dnsmasq config
  template:
    src: templates/dnsmasq.conf.j2
    dest: /etc/dnsmasq.conf
  register: dnsmasq
  tags: config
    
- name: Create hostapd config
  template:
    src: templates/hostapd.conf.j2
    dest: /etc/hostapd/hostapd.conf
  register: hostapd
  tags: config

- name: unblock wifi
  command: rfkill unblock 0
  tags: config

- name: set up wlan ip address
  blockinfile:
    path: /etc/dhcpcd.conf
    block: |
      interface wlan0
      nohook wpa_supplicant      
  tags: config
    
- name: Enable dnsmasq and hostapd services
  service:
    name: "{{ item }}"
    state: "{% if hostapd.changed or dnsmasq.changed %}re{% endif %}started"
    masked: no
    enabled: yes
  with_items:
  - dnsmasq
  - hostapd
  tags: service
    
- name: Enable IP forwarding
  sysctl:
    name: net.ipv4.ip_forward
    value: 1
    state: present
  tags: fwding
    
- name: Set up iptables rules script
  template:
    src: "templates/wifirouter.sh.j2"
    dest: "/root/wifirouter.sh"
    mode: "0755"
  tags: iptables,script
    
- name: Create wifirouter service
  template:
    src: templates/wifirouter.service.j2
    dest: /etc/systemd/system/wifirouter.service
  register: serv
  tags: iptables

- name: Enable iptables restore service
  service:
    name: wifirouter
    enabled: yes
    state: started
    daemon_reload: "{% if serv.changed %}yes{% else %}no{% endif %}"
  tags: iptables
  Note that I’ve used template files here. In my actual setup I configure some paths with variables, that’s why I rely on templates almost all the time. You could use copy instead of template.

For the content of the files that are copied refer to the previous sections.

Conclusion

So, is it worth it? To be honest I started doing this without knowing for sure if my cameras would keep working locally if I put them in jail like this.

It’s only been a few days, but for now they do. I’ve noticed that if I open the app immediately after setting up the jail, it searches for the cameras and complains, but then stops immediately if I release the cameras from their jail. However after more time (hours), the app struggles to locate the cameras again even after opening up the gates, for a minute or 2, and only then finds them again. But so far I haven’t lost access via the app overall, and the cameras keep working.

Streaming and activation of motion detection from Home Assistant keeps working perfectly when the camera is in jail.

We’ll have to see within a few weeks if the camera ends up giving up at some point, but things are looking good so far.

Join the conversation on this article over there !