Improve Local Network Security for Cloudflare Tunnels

Minimize potential attack vectors and limit the scope of any compromise of the cloudflared tunnel daemon.

You may already be aware that a Cloudflare Tunnel (formerly known as Argo Tunnel) is a service that securely exposes local web servers to the internet without directly exposing them to the public. They are particularly useful when one connects to the Internet without a publicly routable IP address, most often behind the CGNAT of a mobile or other broadband provider. A Cloudflare Tunnel creates a secure, encrypted link between your server and Cloudflare’s network, eliminating the need to open ports on your firewall, and offering a solution when port forwarding isn’t available. This allows you to protect your infrastructure and easily manage access while Cloudflare handles security, traffic optimization, and DDoS protection.

This is not a tutorial on how to install and configure a Cloudflare Tunnel. There are myriad tutorials in text and video form, that detail that process fairly adequately. Rather, this article presents a method of enhancing the local network security aspects of running the cloudflared ‘lightweight’ connector daemon on your infrastructure, and addresses the terrifying potential for corporate snooping and absolute-breach security events.

A network namespace is a feature in Linux that can improve the security of running the cloudflared daemon, through isolating the tunnel’s network traffic from the rest of the system. By running cloudflared inside a separate network namespace, you create a distinct environment with its own network interfaces, IP addresses, firewall rules and routing tables. Were the cloudflared process to become compromised, an attacker would only have access to resources specifically made accessible from within that namespace, not the broader system.

Virtual Ethernet (veth) interfaces are pairs of network interfaces used to connect different network namespaces in Linux. Each veth pair consists of two interfaces: one in each namespace. These interfaces act like a “bridge” between the namespaces, allowing network traffic to pass between them. When one namespace sends data through its veth interface, the other namespace can receive it through the corresponding veth interface. This enables communication between isolated network namespaces while keeping their network environments separate and secure.

The presented systemd unit file strengthens the security of cloudflared by isolating it within a network namespace and tightly controlling its network access. Here’s how:

  • A network namespace (isolated_ns) is created, ensuring cloudflared runs in a completely isolated network environment.
  • A veth pair connects the isolated namespace to the host, enabling controlled communication while keeping it secure.
  • iptables rules are applied to restrict traffic within the namespace, allowing only specific outbound connections (e.g., DNS, web server) and blocking all other local network access.
  • On the host, IP forwarding and NAT are configured to route traffic from the isolated namespace to the internet while hiding the namespace’s IP, further securing the system.
  • When stopped, the systemd unit file ensures all network resources and rules are cleaned up, maintaining the system’s security.

This approach minimizes potential attack vectors and limits the scope of any compromise of the isolated namespace.

For the example presented, we have cloudflared running on the same machine as a webserver, and tunnel it’s ports 80 and 443 to Cloudflare’s infrastructure for wider exposure. The systemd unit is configured to deny all local LAN access from cloudflared, save for the ports on the webserver, a DNS server, and the Gateway to the Internet.

To configure the systemd unit for a similar webserver-only cloudflared setup, one need only adjust the requisite parameters. This is how we have these filled-out for the example network:

Environment=LAN_IP_RANGE=192.168.0.0/24    # The IP address range of example LAN.
Environment=WEBSERVER_IP=192.168.0.101     # The IP address of example webserver (may be external to cloudflared host).
Environment=GATEWAY_IP=192.168.0.1         # The IP address of example router's gateway.
Environment=DNSSERVER_IP=192.168.0.1       # The IP address of example router's DNS server.
Environment=TUNNEL_TOKEN=eyJhIjoiZDI0MMzdYzAtMTJhMi00MjQ3LWJhNTUtNjNmOGY5OGMzYjRhIOFlqZsakxUazBZV2QiLCJ0IjoiODI2ZGN0TVRJM1lXVXhaVEV3TldObSJ9WMxYzYzMWI4ZWQ3M2VliwicyI6Ik1qUTBzBZUzAwTlRkNWZjMDI0OWZmMjY3ak00WlRBd  # Example (munged) tunnel token.

To protect a cloudflared instance that is configured to tunnel other services (SSH, RDP, SMB, etc.), you need only adjust the setup and discharge of iptables rules that govern the server’s associated ports. In the following, where access to the webserver’s ports 80 and 443 is permitted, you would substitute your own services’ port numbers and attendant protocols (tcp/udp):

...
# Initialization:
...
# Allow outgoing traffic to the local host's Web server on port 80 and 443.
ExecStartPre=ip netns exec isolated_ns iptables -A OUTPUT -d $WEBSERVER_IP -p tcp --dport 80 -j ACCEPT
ExecStartPre=ip netns exec isolated_ns iptables -A OUTPUT -d $WEBSERVER_IP -p tcp --dport 443 -j ACCEPT
...
# De-initialization
...
# Remove the rule allowing outgoing traffic to the local host's Web server on port 80 and 443.
ExecStop=ip netns exec isolated_ns iptables -D OUTPUT -d $WEBSERVER_IP -p tcp --dport 80 -j ACCEPT
ExecStop=ip netns exec isolated_ns iptables -D OUTPUT -d $WEBSERVER_IP -p tcp --dport 443 -j ACCEPT
...

After installing the cloudflared connector daemon, and configuring the systemd unit file appropriately, backup the files /etc/systemd/system/cloudflared.service and /etc/systemd/system/cloudflared-update.service, and replace these with the new versions. The timer unit file /etc/systemd/system/cloudflared-update.timer requires no changes.

The security enhanced systemd unit file /etc/systemd/system/cloudflared.service reads as follows:

[Unit]
Description=cloudflared
After=network-online.target
Wants=network-online.target
# Copyright © 2025, Murray R. Van Luyn.

[Service]
TimeoutStartSec=0
Type=notify

Environment=LAN_IP_RANGE=192.168.0.0/24    # Insert the IP address range of your LAN.
Environment=WEBSERVER_IP=192.168.0.101     # Insert the IP address of your webserver (may be external to cloudflared host).
Environment=GATEWAY_IP=192.168.0.1         # Insert the IP address of your gateway.
Environment=DNSSERVER_IP=192.168.0.1       # Insert the IP address of your DNS server (may be external).
Environment=TUNNEL_TOKEN=eyJhIjoiZDI0MMzdYzAtMTJhMi00MjQ3LWJhNTUtNjNmOGY5OGMzYjRhIOFlqZsakxUazBZV2QiLCJ0IjoiODI2ZGN0TVRJM1lXVXhaVEV3TldObSJ9WMxYzYzMWI4ZWQ3M2VliwicyI6Ik1qUTBzBZUzAwTlRkNWZjMDI0OWZmMjY3ak00WlRBd  # Insert your tunnel token (munged).

# Initialization:

# Create network isolated_ns.
ExecStartPre=ip netns add isolated_ns

# Create the veth pair (veth0 and veth1) in the root network isolated_ns.
ExecStartPre=ip link add veth_host type veth peer name veth_ns

# Move veth1 into the network namespace isolated_ns.
ExecStartPre=ip link set veth_ns netns isolated_ns

# Assign IPs to and bring up the virtual ethernet interfaces.
ExecStartPre=ip addr add 192.168.100.1/24 dev veth_host
ExecStartPre=ip link set veth_host up

ExecStartPre=ip netns exec isolated_ns ip addr add 192.168.100.2/24 dev veth_ns
ExecStartPre=ip netns exec isolated_ns ip link set veth_ns up

# Set the default route and DNS server within the isolated_ns.
ExecStartPre=ip netns exec isolated_ns ip route add default via 192.168.100.1
ExecStartPre=ip netns exec isolated_ns sh -c "echo 'nameserver $DNSSERVER_IP' > /etc/resolv.conf"

# Allow localhost access inside the isolated_ns.
ExecStartPre=ip netns exec isolated_ns ip link set lo up
ExecStartPre=ip netns exec isolated_ns ip route add 127.0.0.0/8 dev lo


# 1. Inside the isolated network namespace (isolated_ns).

# Allow outgoing traffic to the DNS server on port 53.
ExecStartPre=ip netns exec isolated_ns iptables -A OUTPUT -d $DNSSERVER_IP -p tcp --dport 53 -j ACCEPT
ExecStartPre=ip netns exec isolated_ns iptables -A OUTPUT -d $DNSSERVER_IP -p udp --dport 53 -j ACCEPT

# Allow outgoing traffic to the local host's Web server on port 80 and 443.
ExecStartPre=ip netns exec isolated_ns iptables -A OUTPUT -d $WEBSERVER_IP -p tcp --dport 80 -j ACCEPT
ExecStartPre=ip netns exec isolated_ns iptables -A OUTPUT -d $WEBSERVER_IP -p tcp --dport 443 -j ACCEPT

# Allow outgoing traffic to localhost (127.0.0.1).
ExecStartPre=ip netns exec isolated_ns iptables -A OUTPUT -d 127.0.0.1 -j ACCEPT

# Allow incoming traffic from localhost (127.0.0.1).
ExecStartPre=ip netns exec isolated_ns iptables -A INPUT -s 127.0.0.1 -j ACCEPT

# Allow return traffic for related and established connections (response to queries).
ExecStartPre=ip netns exec isolated_ns iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT

# Block outgoing traffic to the local LAN (192.168.0.0/24) except for the Gateway, Webserver and DNS server.
ExecStartPre=ip netns exec isolated_ns iptables -A OUTPUT -d $LAN_IP_RANGE -j REJECT

# Block incoming traffic from the local LAN (192.168.0.0/24) except for the Gateway, Webserver and DNS server.
ExecStartPre=ip netns exec isolated_ns iptables -A INPUT -s $LAN_IP_RANGE -j REJECT


# 2. On the Host (Outside the isolated_ns).

# Enable IP forwarding on the host.
ExecStartPre=sysctl -w net.ipv4.ip_forward=1

# Set up NAT for traffic from the isolated network (192.168.100.0/24) through the host (eth0).
ExecStartPre=iptables -t nat -A POSTROUTING -s 192.168.100.0/24 -o eth0 -j MASQUERADE

# Allow forwarding from the isolated_ns through the Gateway on ports 7844, 443 and 53.
ExecStartPre=iptables -A FORWARD -i veth_host -o eth0 -p tcp --dport 7844 -j ACCEPT
ExecStartPre=iptables -A FORWARD -i veth_host -o eth0 -p udp --dport 7844 -j ACCEPT
ExecStartPre=iptables -A FORWARD -i veth_host -o eth0 -p tcp --dport 443 -j ACCEPT
ExecStartPre=iptables -A FORWARD -i veth_host -o eth0 -p tcp --dport 53 -j ACCEPT
ExecStartPre=iptables -A FORWARD -i veth_host -o eth0 -p udp --dport 53 -j ACCEPT

# Allow return traffic from the internet to the isolated_ns on ports 7844, 443 and 53 (return traffic).
ExecStartPre=iptables -A FORWARD -i eth0 -o veth_host -m state --state RELATED,ESTABLISHED -j ACCEPT

# Drop all other incoming traffic to the isolated_ns.
ExecStartPre=iptables -A FORWARD -i eth0 -o veth_host -j REJECT

# Drop all other outgoing traffic from the isolated_ns.
ExecStartPre=iptables -A FORWARD -i veth_host -o eth0 -j REJECT

# Isolate isolated_ns to prevent it using IPv6 traffic in, out or forward.
ExecStartPre=ip netns exec isolated_ns ip6tables -P INPUT DROP
ExecStartPre=ip netns exec isolated_ns ip6tables -P OUTPUT DROP
ExecStartPre=ip netns exec isolated_ns ip6tables -P FORWARD DROP


# Run the cloudflared tunnel service in network namespace isolation.
ExecStart=ip netns exec isolated_ns cloudflared tunnel run --token $TUNNEL_TOKEN


# De-initialization

# 1. Inside the isolated_ns (Isolated Network isolated_ns).

# Remove the rule allowing outgoing traffic to the DNS server on port 53.
ExecStop=ip netns exec isolated_ns iptables -D OUTPUT -d $DNSSERVER_IP -p tcp --dport 53 -j ACCEPT
ExecStop=ip netns exec isolated_ns iptables -D OUTPUT -d $DNSSERVER_IP -p udp --dport 53 -j ACCEPT

# Remove the rule allowing outgoing traffic to the local host's Web server on port 80 and 443.
ExecStop=ip netns exec isolated_ns iptables -D OUTPUT -d $WEBSERVER_IP -p tcp --dport 80 -j ACCEPT
ExecStop=ip netns exec isolated_ns iptables -D OUTPUT -d $WEBSERVER_IP -p tcp --dport 443 -j ACCEPT

# Remove the rule allowing outgoing traffic to localhost (127.0.0.1).
ExecStop=ip netns exec isolated_ns iptables -D OUTPUT -d 127.0.0.1 -j ACCEPT

# Remove the rule allowing incoming traffic from localhost (127.0.0.1).
ExecStop=ip netns exec isolated_ns iptables -D INPUT -s 127.0.0.1 -j ACCEPT

# Remove the rule allowing return traffic for related and established connections.
ExecStop=ip netns exec isolated_ns iptables -D INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT

# Remove the rule blocking outgoing traffic to the local LAN (192.168.0.0/24).
ExecStop=ip netns exec isolated_ns iptables -D OUTPUT -d $LAN_IP_RANGE -j REJECT

# Remove the rule blocking incoming traffic from the local LAN (192.168.0.0/24).
ExecStop=ip netns exec isolated_ns iptables -D INPUT -s $LAN_IP_RANGE -j REJECT


# 2. On the Host (Outside the isolated_ns).

# Disable IP forwarding on the host.
ExecStop=sysctl -w net.ipv4.ip_forward=0

# Remove the NAT rule for traffic from the isolated_ns through the host.
ExecStop=iptables -t nat -D POSTROUTING -s 192.168.100.0/24 -o eth0 -j MASQUERADE

# Remove forwarding from the isolated_ns to the Gateway on ports 7844, 443 and 53.
ExecStop=iptables -D FORWARD -i veth_host -o eth0 -p tcp --dport 7844 -j ACCEPT
ExecStop=iptables -D FORWARD -i veth_host -o eth0 -p udp --dport 7844 -j ACCEPT
ExecStop=iptables -D FORWARD -i veth_host -o eth0 -p tcp --dport 443 -j ACCEPT
ExecStop=iptables -D FORWARD -i veth_host -o eth0 -p tcp --dport 53 -j ACCEPT
ExecStop=iptables -D FORWARD -i veth_host -o eth0 -p udp --dport 53 -j ACCEPT

# Remove the rule allowing return traffic from the internet to the isolated_ns.
ExecStop=iptables -D FORWARD -i eth0 -o veth_host -m state --state RELATED,ESTABLISHED -j ACCEPT

# Remove the rule rejecting incoming traffic to the isolated_ns.
ExecStop=iptables -D FORWARD -i eth0 -o veth_host -j REJECT

# Remove the rule rejecting outgoing traffic from the isolated_ns.
ExecStop=iptables -D FORWARD -i veth_host -o eth0 -j REJECT

# Remove the default route and DNS configuration from the isolated_ns.
ExecStop=ip netns exec isolated_ns ip route del default via 192.168.100.1
ExecStop=ip netns exec isolated_ns sh -c "echo '' > /etc/resolv.conf"

# Delete the loopback interface route (optional cleanup).
ExecStop=ip netns exec isolated_ns ip route del 127.0.0.0/8 dev lo

# Bring down the veth interfaces.
ExecStop=ip link set veth_host down
ExecStop=ip netns exec isolated_ns ip link set veth_ns down

# Delete the veth pair.
ExecStop=ip link delete veth_host

# Delete the network namespace isolated_ns.
ExecStop=ip netns del isolated_ns


Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target

The security enhanced systemd update unit file /etc/systemd/system/cloudflared-update.service reads as follows:

[Unit]
Description=Update cloudflared
After=network-online.target
Wants=network-online.target

[Service]
ExecStart=/bin/bash -c '/usr/bin/ip netns exec isolated_ns /usr/bin/cloudflared update; code=$?; if [ $code -eq 11 ]; then systemctl restart cloudflared; exit 0; fi; exit $code'

The system unit presented is far from robust, and will leave artefacts from any previous, failed invocation. These will cause subsequent executions to fail repeatedly. To clean-up all remnants, and start the service reliably and cleanly, use the following sequence of commands:

sudo systemctl disable cloudflared.service

sudo systemctl stop cloudflared.service

sudo ip netns del isolated_ns    # Just to be sure.

sudo systemctl enable cloudflared.service

sudo reboot

Here is a list of commands you might find useful, to assist in configuring, running and debugging the systemd unit:

sudo nano /etc/systemd/system/cloudflared.service

sudo nano /etc/systemd/system/cloudflared-update.service

sudo systemctl daemon-reload

sudo ip netns add isolated_ns

sudo ip netns del isolated_ns

sudo systemctl enable cloudflared.service

sudo systemctl disable cloudflared.service

sudo systemctl start cloudflared.service

sudo systemctl stop cloudflared.service

sudo systemctl status cloudflared.service

journalctl -u cloudflared.service -b | grep -i error

Warnings in the cloudflared.service log relating to ICMP can safely be ignored. I could find no way to provide the isolated cloudflared executable with raw socket creation capability (needed to ping), and the service works fine without it. If you can find some means to accomplish this, then I’m all ears.

Arduino I2C Slave Peripheral

With a recognition of the existing gap, I present this contribution aimed at enhancing the publicly accessible guidelines for laying out and coding Arduino I2C slave devices. This proposal introduces an organizational framework for coding I2C slave peripherals on Arduino, illustrated through a Platformio-based example.

Structure

Using an Atmel328PB microcontroller, we simulate a series of 16-bit peripheral registers, each with an 8-bit address. These registers are programmed akin to standard silicon I2C devices, allowing individual configuration for read-only, write-only, or read-write operations as perceived by the I2C master.

/*****************************************************************************
 *
 *                              Global Variables
 *
 *****************************************************************************/
// An array to store register values
int i2c_registers[I2C_NUM_REGS] = {0};

// I2C session persistent register address
uint8_t registerAddr = 0xFF;

In the development of an I2C slave application, peripheral functionality must be implemented through data source and data sink operations, skillfully integrated with the aforementioned emulated device registers.

In scenarios demanding highly efficient application code execution, integration occurs within the I2C interrupt service context. These application operations are executed dynamically during active I2C communication. However, integrating time-consuming application operations here can potentially delay I2C timing to a degree that compromises standards compliance, resulting in communication failures.

To mitigate this risk, in cases where application code runs slower, linkage between data source/sink operations and emulated peripheral registers occurs outside the I2C service interrupt context, typically within or under the task loop scope. Despite this approach, simultaneous access to emulated registers can occur, due to I2C ISR service routine triggering, leading to data integrity issues.

To ensure robustness, a pair of interrupt-suspending memory access macros is provided. These macros guarantee atomicity during peripheral register data manipulation within the task loop, thereby preventing potential corruption

I2C Call-back Functions

During program setup, the I2C bus under configuration, is assigned a device address, and call-back routines for the service of wired I2C bus transactions. When an I2C-inbound write occurs, data is consumed and/ or is stored in a register, with code inside the receiveEvent() callback. When an I2C-inbound read occurs, data is application generated, or is retrieved from a register, inside the requestEvent() ISR.

  Wire.begin(I2C_ADDRESS);      // Initialize I2C communication as a slave
  Wire.onReceive(receiveEvent); // Register the receive event handler
  Wire.onRequest(requestEvent); // Register the request event handler

The unpopulated receiveEvent() call-back

An unpopulated receiveEvent() ISR services wired I2C register storage instructions. Not yet linked to any application functionality, it merely provides write access to peripheral register memory.

/*****************************************************************************
 *
 *                               receiveEvent()
 *
 *****************************************************************************/
// Event handler for receiving data from the master, to write to a register
void receiveEvent(int numBytes) 
{ 
  // Read the requested register address
  registerAddr = Wire.read(); 
  
  if (numBytes == 3) 
  {
    // Read the data to write
    uint16_t value = Wire.read() ;        // value LSB
    value |= Wire.read() << 8;            // value MSB

    switch (registerAddr)
    {
      // Comment-out any read-only registers to prevent master write
      case I2C_REG_0:
      case I2C_REG_1: 
      case I2C_REG_2:
      case I2C_REG_3:
      case I2C_REG_4:
      case I2C_REG_5:
      case I2C_REG_6:
      case I2C_REG_7:
      {
        i2c_registers[registerAddr] = value;
        break;
      }
      default: return;
    }

    I2C_WRITE_DEBUG(registerAddr, value)
  }
}

The unpopulated requestEvent() call-back

An unpopulated requestEvent() ISR services wired I2C register retrieval instructions. Not yet linked to any application functionality, it merely provides read access to peripheral register memory.

/*****************************************************************************
 *
 *                               requestEvent()
 *
 *****************************************************************************/
// Event handler for responding to requests for register contents, from the master
void requestEvent() 
{
  uint16_t value = 0;

  switch (registerAddr)
  {
    // Comment out any write-only registers to prevent master read
    case I2C_REG_0:
    case I2C_REG_1:
    case I2C_REG_2:
    case I2C_REG_3:
    case I2C_REG_4:
    case I2C_REG_5:
    case I2C_REG_6:
    case I2C_REG_7:
    {
      value = i2c_registers[registerAddr];
      break;
    }
    default:
      return;
  }

  // Send the data read from
  Wire.write(value & 0xFF); // send LSB
  Wire.write(value >> 8);   // send MSB

  I2C_READ_DEBUG(registerAddr, value);

  return;
}

Read-ability and write-ability

So, we may see from the above, in the unpopulated ISR pair, an I2C master has the ability to write a value to one of a number of memory-only registers, and then read it back from there verbatim. We’ll see later, when we come to adding applications, such simultaneous read-ability and write-ability may not be desirable.

To make a peripheral register read-only, we remove the receiveEvent() ISRs ability to service peripheral register write requests. We do this by commenting-out service routine switch cases that involve the register.

void receiveEvent(int numBytes) 
{

...

    switch (registerAddr)
    {
      // Comment out any read-only registers to prevent master write
      case I2C_REG_0:
      //case I2C_REG_1:  // Make register 1 read-only
      case I2C_REG_2:
      case I2C_REG_3:
      case I2C_REG_4:
      case I2C_REG_5:
      case I2C_REG_6:
      case I2C_REG_7:
      {
        i2c_registers[registerAddr] = value;
        break;
      }
      default: return;
    }

...

To make a peripheral register write-only, we remove the requestEvent() ISRs ability to service peripheral register read requests. Again, we do this by commenting-out service routine switch cases that involve the register.

void requestEvent() 
{

...

  switch (registerAddr)
  {
    // Comment out any write-only registers to prevent master read
    case I2C_REG_0:
    case I2C_REG_1:
    // case I2C_REG_2:  // Make register 2 write-only
    case I2C_REG_3:
    case I2C_REG_4:
    case I2C_REG_5:
    case I2C_REG_6:
    case I2C_REG_7:
    {
      value = i2c_registers[registerAddr];
      break;
    }
    default:
      return;
  }

...

Application linkage inside loop()

Now that we have our peripheral registers configured, variously as read and write, read-only, or write-only, we come to linking these to application code. This involves the data sink and data source routines, that you will develop for your peripheral.

Remember, all access to peripheral registers outside the I2C interrupt service context, must employ the atomic register access macros, I2C_ATOMIC_REG_RD() and I2C_ATOMIC_REG_WR(). Again, this is vital to prevent the data corruption that occurs, when I2C ISRs are invoked, during peripheral register access. The macros momentarily ‘lock-out’ the ISRs.

/************************************************************************
 *
 *                                loop()
 *
 ************************************************************************/
void loop() 
{
  {
    uint16_t value = 0;

    // Read adc, and atomically save val in read-only slave register 1
    value = myadc_read();
    // i2c_registers[I2C_REG_1] = value; <- not atomic, instead...
    I2C_ATOMIC_REG_WR(i2c_registers[I2C_REG_1], value);

    // Retrieve contents of slave register 0 atomically, and consume
    // value = i2c_registers[I2C_REG_0]; <- not atomic, instead...
    I2C_ATOMIC_REG_RD(value, i2c_registers[I2C_REG_0]);
    myservo_set_pos(value);
  }

  // Other application tasks.

}

In the above, we see that a logically read-only peripheral register, I2C_REG_1, is fed samples from an onboard ADC, using an application call to myadc_read(). A remotely wired, I2C master may read the stored register data, with calls such as wiringPi’s wiringPiI2CReadReg16().

As well, above, we have an either read and write, or write-only configured peripheral register, I2C_REG_0, feeding position data to an attached servo. It does this via an application call to myservo_set_pos(). A remotely wired, I2C master may write the servo position data, with calls such as wiringPi’s wiringPiI2CWriteReg16().

On-demand, vs polled register access

The above mentioned approach for linking application data sink and source functionality, to underlying peripheral register memory, from outside I2C ISR service context, can be wasteful. Significant cpu cycles are consumed, in the application either constantly polling peripheral registers for incoming data, or frequently updating peripheral registers for fresh outgoing data.

To increase application efficiency, an alternate user code linkage solution exists, for on-demand exchange of data with peripheral registers. This involves application data source and data sink function calls, from within I2C ISR service context.

As mentioned previously, application calls made inside either of the I2C ISR call-backs, must be very brief, so as not to disrupt wired I2C communication. Between the servo and ADC application calls dealt with previously, only the myservo_set_pos() call is suitable. The myadc_read() function involves lengthy over-sampling, and any attempt to link it ‘on-demand’, from within an ISR, will break associated I2C transactions.

Application linkage inside the I2C ISRs

Here we examine the alternative approach to linking an I2C slave peripheral’s servo position. Data is exchanged only when requested to be, by a wired I2C bus transaction. The I2C receiveEvent() ISR handles incoming data, and we merely write that ‘value’ data to the servo, using the necessarily fast myservo_set_pos() application call.

void receiveEvent(int numBytes) 
{

...

    switch (registerAddr)
    {
      // Comment out any read-only registers to prevent master write
      case I2C_REG_0:
      {
        myservo_set_pos(value);
        i2c_registers[registerAddr] = value;
        break;
      }
      //case I2C_REG_1:  // Read-only
      case I2C_REG_2:
      case I2C_REG_3:
      case I2C_REG_4:
      case I2C_REG_5:
      case I2C_REG_6:
      case I2C_REG_7:
      {
        i2c_registers[registerAddr] = value;
        break;
      }
      default: return;
    }

    I2C_WRITE_DEBUG(registerAddr, value)
  }
}

...

In this case, we have chosen to make the servo peripheral register both readable, and writeable. To accomplish this, we must also store the incoming data in the underlying peripheral register, as shown above.

In the case where we wanted to make the application’s servo peripheral register write only, we would not store the incoming position data in any underlying peripheral register, or, at the very least, make the underlying register write only. This is again accomplished, by commenting-out the appropriate switch statement case, inside the requestEvent() ISR.

void requestEvent() 
{

...

  switch (registerAddr)
  {
    // Comment out any write-only registers to prevent master read
    //case I2C_REG_0:  // Make servo register 0 write-only
    case I2C_REG_1:
    case I2C_REG_2:
    case I2C_REG_3:
    case I2C_REG_4:
    case I2C_REG_5:
    case I2C_REG_6:
    case I2C_REG_7:
    {
      value = i2c_registers[registerAddr];
      break;
    }
    default:
      return;
  }

...

For the final case, we examine how outgoing application data may be linked to peripheral register requests, on-demand, from within I2C ISR context. For this, we will read an attached switch, with an imaginary application call, myswitch_get_posn(). We know that the call will be fast enough not to break wired I2C communication, and that logically the operation must be read-only.

The I2C requestEvent() ISR handles outgoing data, and we merely service a request for the switch position, with data from the myswitch_get_posn() application call.

void requestEvent() 
{

...

  switch (registerAddr)
  {
    // Comment out any write-only registers to prevent master read
    case I2C_REG_2:
    {
      value = myswitch_get_posn();
      break;
    }
    case I2C_REG_0:
    case I2C_REG_1:
    case I2C_REG_3:
    case I2C_REG_4:
    case I2C_REG_5:
    case I2C_REG_6:
    case I2C_REG_7:
    {
      value = i2c_registers[registerAddr];
      break;
    }
    default:
      return;
  }

...

As seen above, the switch peripheral register is logically read only, so stored peripheral register memory is not involved. To disable any attempt to write a peripheral register, making it read only, we comment-out it’s associated entry in the receiveEvent() I2C ISR.

void requestEvent() 
{

...

  switch (registerAddr)
  {
    // Comment out any write-only registers to prevent master read
    case I2C_REG_0:
    case I2C_REG_1:
    // case I2C_REG_2:  // Make switch register 2 write-only
    case I2C_REG_3:
    case I2C_REG_4:
    case I2C_REG_5:
    case I2C_REG_6:
    case I2C_REG_7:
    {
      value = i2c_registers[registerAddr];
      break;
    }
    default:
      return;
  }

...

Improving polled access efficiency

/************************************************************************
 *
 *                                loop()
 *
 ************************************************************************/
void loop() 
{
  {
    uint16_t value = 0;

    if (myservo_changed())
    {
      // Retrieve contents of slave register 0 atomically, and consume
      I2C_ATOMIC_REG_RD(value, i2c_registers[I2C_REG_0]);
      myservo_set_changed(0);

      // From here on, receiveEvent() ISR can take new register data,
      // and the snapshot 'value' can be processed, even if exhaustively
      myservo_set_pos(value);
    }
  }

  // Other application tasks.

}
void receiveEvent(int numBytes) 
{

...

    switch (registerAddr)
    {
      // Comment out any read-only registers to prevent master write
      case I2C_REG_0:
      {
        i2c_registers[registerAddr] = value;
        myservo_set_changed(1);
        break;
      }
      case I2C_REG_1:
      case I2C_REG_2:
      case I2C_REG_3:
      case I2C_REG_4:
      case I2C_REG_5:
      case I2C_REG_6:
      case I2C_REG_7:
      {
        i2c_registers[registerAddr] = value;
        break;
      }
      default: return;
    }

    I2C_WRITE_DEBUG(registerAddr, value)
  }
}

...

Well, that’s how the I2C slave peripheral model works, how to constrain access to it’s underlying peripheral registers, and how to link-in application code. Application linkage methods were shown, both by polling inside loop(), and by on-demand calls inside ISR context.

Just remember to keep application calls short inside the ISRs, and to use atomic register access macros outside them.

Associated Files:

The attached Arduino I2C slave demonstration code for this example, runs on an AT328PB. It reads the position of an ADC-connected potentiometer, and stores this data in a 16-bit I2C register numbered 1. It also reads data in a 16-bit I2C register numbered 0, and sets the corresponding position of an attached servo.

Example C code for a wiringPi-installed, Raspberry Pi master is also provided, which remotely reads the potentiometer value, and writes a proportionate value back to the I2C slave peripheral’s servo control register.

Demonstration code, of the suggested organisational paradigm, for an Arduino I2C slave peripheral. I2C_slave_model.zip

Automated Incremental Backups

Tired of time consuming and fiddly backing-up of your Raspberry Pi’s SD card? This guide will provide you with care-free, automated incremental backups of your system’s images, to a permanently attached USB flash drive.

Raspberry Pi Board plus USB Flash Drive.


In case of irrecoverable events, the images stored on the USB flash drive, may be used to re-flash your SD card, using your favourite image writing software, on any Windows or Linux machine.

Additionally, there is described a simple method for mounting the USB flash drive stored backup images, to the Raspberry Pi’s existing file-system, so that individual files may be easily recovered and restored.

Step 1: Add a USB Flash Drive That Automatically Mounts Itself.

Because we will be dealing with files larger than the 4GB limit of the FAT32 file system, and because we will be restoring images on either a Windows or Linux machine, your USB flash drive must be formatted with the NTFS file system. NTFS permits us the storage of file permissions, and is a more fault tolerant journaling file system.

Your USB flash drive must also be large enough to accommodate at least one image the same size as your Raspberry Pi’s SD card, and you will benefit from having extra room to accommodate compressed backup image ‘snapshots’. For my 16GB system SD card, I chose a ludicrously inexpensive 32GB USB 3.0/2.0 flash drive.

Okay, to mount the NTFS drive, you’ll need to install driver support:

sudo apt-get update
sudo apt-get install ntfs-3g

If you insert the USB flash drive, then list block storage devices, you should see the device, along with it’s NTFS volume. The volume of interest in my case is /dev/sda1:

lsblk

Mount the USB flash drive’s NTFS volume under the /media directory:

sudo mount /dev/sda1 /media

Establish the unique universal id (uuid) of the flash drive: (Be aware the uuid will change any time you reformat the USB flash drive, requiring subsequent re-configuration.)

ls -l /dev/disk/by-uuid

Find out the group id (gid) of pi user:

id -g pi

Obtain the user id (uid) of pi user:

id -u pi

Now, to have the system automatically mount the USB flash drive any time you reboot, we will append a line to the /etc/fstab configuration file, filling in the required information ascertained in preceding steps:

sudo nano /etc/fstab
In nano, fill-out the following line, then append it to the end of the file:

UUID=enter_uuid_here /media ntfs-3g ofail,uid=enter_uid_here,gid=enter_gid_here,noatime 0 0

To check if the new configuration works, we must first un-mount the USB flash drive. The gotcha here is that if your current working directory is anywhere in or under /media, then you have to cd elsewhere, or you’ll get a device busy error:

sudo umount /dev/sda1

Now, to automatically mount all the system’s drives mentioned in /etc/fstab:

sudo mount -a

Check that the NTFS volume on the flash drive is mounted, and now has a mount point under /media with:

lsblk

Now reboot your system, and check again that the USB flash drive has been automatically mounted (per above):

sudo reboot

Step 2: Install the Backup Script, and Automate With a Cron Job.

We’ll be installing the shell script bkup_rpimage.sh v1.0, written by the masterful jinx, and made available by other Raspberry Pi forum users on Github.

Firstly, if you don’t already have a bin directory under your home folder, then here’s how to get one…for free! Your .profile configuration will automagically add this directory to your PATH the next time you login, but we won’t rely on that here.

mkdir /home/pi/bin

Next, download the backup script into your bin directory:

cd /home/pi/bin
wget https://raw.githubusercontent.com/lzkelley/bkup_rpimage/master/bkup_rpimage.sh

So it does not become accidentally overwritten, and to make the script executable:

chmod 554 bkup_rpimage.sh

Okay, so now we do our first full manual backup with the new script. Be patient, as this may take anywhere between 20 and 90+ minutes, depending on your file system size, and system’s capabilities:

sudo /home/pi/bin/bkup_rpimage.sh start -c /media/rpi_backup.img

Looking in your /media directory, you should now see an rpi_backup.img file, of about the same size as your SD card. This is the image you will re-flash to your SD card with Etcher, Win32_Disk_Imager, etc., if ever the need arises:

ls -l /media

Subsequently executing the backup command listed above, will do an incremental update to this backed-up image, taking very little time, and only making changes that reflect those made to the SD card since the last full or incremental backup.

Instead of manually invoking the backup script for the update however, we’re going to add a line to our cron configuration, and have the system automatically perform the update for us, each day at midnight:

sudo crontab -e
In the crontab editor, add the following line to the bottom of the file:

0 0 * * * sudo /home/pi/bin/bkup_rpimage.sh start -c /media/rpi_backup.img

See ‘man 5 crontab’ if you’d like to know how to adjust the time or frequency of system image backup updates.

The bkup_rpimage.sh v1.0 shell script provides options for creating compressed backup images of system files, but not in a format useful under Windows. We’ll be installing support for .zip file compression, so compressed image backups may be recovered with either Windows or Linux.

Install zip program for Windows compatible compressed backup images:

sudo apt-get update
sudo apt-get install zip

Compressing backup images places a considerable burden on system resources, especially if the operation is automated and undertaken very regularly. Instead of this, we’ll be compressing backup images manually, and only when the need for ‘snapshots’ arises.

Manually compress a date-stamped snapshot of the backup image, as a background process:

zip /media/rpi_backup.img.$(date +%Y-%m-%d).zip /media/rpi_backup.img &

Compressed ‘snapshots’ of system file backup images should now be available on your USB flash drive, along with the most recent, uncompressed backup image. To gain access to these, simply un-mount the USB drive, then move the USB drive to either a Windows or Linux machine, for SD card re-flashing, using Etcher, Win32_Disk_Imager, etc.:

sudo umount /dev/sda1

Step 4: Retrieving Individual Files.

Say you accidentally overwrite an important file that you know is on your last backup. You don’t want to lose the entire days work since your last backup, so restoring the whole file-system is not an option. How can you access just the lost file?

The backup script bkup_rpimage.sh has options that allow you to easily mount and dismount a backed-up image file, to and from your file-system. Once mounted, you can readily traverse the mounted backup’s tree, seeking any file contained therein:

To mount the backed-up image under /mnt:

sudo /home/pi/bin/bkup_rpimage.sh mount /media/rpi_backup.img /mnt/

Notice that files in the backup image are now attached, and accessible under /mnt:.

cat /mnt/home/pi/.bashrc

…and to un-mount the backup image on the USB drive from the file-system we issue:

sudo /home/pi/bin/bkup_rpimage.sh umount /media/rpi_backup.img /mnt/

Step 5: Some Scripts You May Find Useful.

Rather than trying to remember or lookup the backup system’s incantations, you might find it worthwhile adding these 4 simple scripts to your ~/bin directory:

nano /home/pi/bin/manual_backup.sh

#!/bin/bash
sudo /home/pi/bin/bkup_rpimage.sh start -c /media/rpi_backup.img
nano /home/pi/bin/compress_backup.sh

#!/bin/bash
zip /media/rpi_backup.img.$(date +%Y-%m-%d).zip /media/rpi_backup.img &
nano /home/pi/bin/mount_backup.sh

#!/bin/bash
sudo /home/pi/bin/bkup_rpimage.sh mount /media/rpi_backup.img /mnt/
nano /home/pi/bin/unmount_backup.sh

#!/bin/bash
sudo /home/pi/bin/bkup_rpimage.sh umount /media/rpi_backup.img /mnt/

Please don’t forget to make these scripts read only and executable:

chmod 554 manual_backup.sh compress_backup.sh mount_backup.sh unmount_backup.sh