Bridging Wireless & Ethernet networks with a Raspberry Pi

We recently acquired a second hand Dell laser printer, which was timely: our inkjet printer is a little long in the tooth, and is starting to require more frequent head cleaning and nozzle alignment, both of which use that liquid gold we call “ink”.

Unfortunately, the laser printer only supported ethernet out of the box, and while a wireless adapter was available, it was retailing for over $150. Fortunately, however, I had a Raspberry Pi lying around, that I was able to turn into a working solution.

There are plenty of posts out there talking about bridging networks with Raspberry Pi and Linux, but most of them seemed to either assume I care about systemd (I don’t), want to deal with static IP addresses (I don’t), or want to bridge two ethernet networks (I don’t). So here’s what worked for me.

There were two inflection points in getting this to work: the first was understanding that I don’t really want a Bridge: I want an [ARP​*​] Proxy. What’s the difference? Simply put: a Proxy can inspect and modify traffic going through it, while a Bridge does not. Why do we care about that? Because wireless access points are fickle creatures, and will often discard traffic originating from a host they don’t know about. Additionally, there are restrictions built into the bridge utilities that prevent you from doing a direct bridge from ethernet to wireless. By running a proxy, we’re able to make the bridged traffic (from our printer) appear as if it’s originating from the Raspberry Pi, which the access point definitely knows about.

I knew that DHCP would be involved if I didn’t want to deal with static IP addresses. The second inflection point was realizing that in order to make that work, and to make it transparent-ish from either side of the bridge, I needed to make sure the ethernet (eth0) and wireless (wlan0) ports had the same IP address. More on how in a moment.

Here are the steps I followed, largely based on the Debian wiki:

  1. Configure your Raspberry Pi with Raspbian and connect it to your wireless network. Getting this out of the way first will configure WPA supplicant, which is responsible for managing the wireless authentication and pre-shared keys. If you’re interested, /etc/wpa_supplicant/wpa_supplicant.conf will hold your configuration after you’re connected.
  2. Install the proxy daemon and helpers we’re going to use: parprouted and dhcp-helper
$ sudo apt-get install parprouted dhcp-helper
  1. Configure dhcp-helper to broadcast on wlan0

Add the following contents to /etc/default/dhcp-helper.

# relay dhcp requests as broadcast to wlan0
  1. Configure mDNS: when your printer magically appears in the Add Printer list, that’s mDNS at work. And we definitely want it to just show up, otherwise we’ll be back in static IP land. To do this, we need to reflect messages from one network to the other. Luckily, Avahi, the Linux mDNS implementation installed with Raspbian, makes this a one-line change.

Enable the Reflector in /etc/avahi/avahi-daemon.conf.

  1. Configure your network interfaces

While we’re most interested in our ethernet (eth0) and wireless (wlan0) interfaces, there are actually three interfaces for us to configure: loopback, ethernet, and wireless. I created a file for each in the /etc/network/interfaces.d/ directory.


auto lo
iface lo inet loopback


auto eth0
allow-hotplug eth0
iface eth0 inet manual


This is where the magic actually happens. I mentioned before that we need to bind the same IP address to both network interfaces. We also need to restart the DHCP Helper and proxy. We wire this up with post-up and post-down clauses in our wlan0 configuration: every post-up line is executed after the network interface comes up, and every post-down line is executed after it goes, ahem, down.

auto wlan0
allow-hotplug wlan0
iface wlan0 inet dhcp
wpa-conf /etc/wpa_supplicant/wpa_supplicant.conf
  post-up /sbin/ip addr add $(/sbin/ip addr show wlan0 | perl -wne 'm|^\s+inet (.*)/| && print $1')/32 dev eth0
  post-up /sbin/ifup eth0
  post-up /usr/sbin/parprouted wlan0 eth0
  post-up /etc/init.d/dhcp-helper start
  post-down /usr/bin/killall /usr/sbin/parprouted
  post-down /etc/init.d/dhcp-helper stop
  post-down /sbin/ifdown eth0
  1. Restart.

At this point your printer should be able to obtain an IP address using DHCP from your wireless network, and the printer should show up to your wireless clients.

  1. ​*​
    Address Resolution Protocol, which is used to map IP addresses to MAC addresses. Yeah, I forget it exists, too.

As someone who is easily distracted by naming rather than, uh, building, I’m a little terrified to have discovered Onelook.

Extending Create React App with Babel Macros

I have a co-dependent relationship with Create React App. When we’re good, we’re really good. When I have an unmet need — when I want to customize some part of the build process — I do my best to make CRA work. And when it doesn’t, for whatever reason, I eject and fall back to Webpack, Babel, & friends, resentful that CRA has let me down. Recently, though, I discovered that you can customize Babel configuration in a Create React App projects without ejecting.

Last month I was working on some shared React components and ran into this again: I wanted to use Tailwind.css — fine — but I also wanted to include it in the resulting Javascript files as CSS-in-JS​*​. I initially despaired, thinking I’d have to eject the components in order to customize the Babel configuration.

And then I discovered Babel Macros, which — lo and behold — are supported by CRA since 2018!

Babel Macros are exactly what they sound like if you’re familiar with Lisp-y languages: they’re code that generates different code. In other words, they give you a functional interface to Babel’s transpiling capabilities. This allows you to write “normal” Javascript (or Typescript) code that CRA can process, but when that code is executed, it hooks into Babel’s runtime.

For my Tailwind CSS-in-JS it looks like this.

First, I tell Babel (and by extension CRA) that I want to enable macros, by adding macros to the list of plugins in my .babelrc:

   "presets": ["@babel/env", "@babel/react", "@babel/typescript"],
   "plugins": [

Then, when I want to use Tailwind-in-JS, I import the macro and use it to tag a string.

import tw from "@tailwindcssinjs/macro";


// in my react component
return (
      style={tw`font-normal text-sm text-gray-700`}

Note that I’m setting what looks like the Tailwind class list as the style property of my element: that’s because tw is actually a function that utilizes Babel’s macro functionality to map the classes to their underlying CSS properties.

With this small bit of configuration in place, running the CRA build script results in pure Javascript I can use in my downstream projects, including the necessary CSS.

There are other advantages, too: someone reading this code can now “follow their nose” to figure out what’s going on. One of the most persistent problems I’ve encountered when approaching a large codebase is understanding how the source is built: where does a dependency come from? how is the code compiled? where — why!? — does a transformation happen? This component now answers those questions for me: the use of Babel (and the macro) is explicit.

  1. ​*​
    There’s probably another post here: getting shared components to work with external CSS has been a real pain for me.

The Habit

A mentor, Naomi, once told me “it’s more fun to write programs that help you write programs than it is to write programs”. While funny — and true, at least for me — I think what she was getting at is something a little more general: meta-work is a way of distracting ourselves from the real work. Or, more nefariously, meta-work is a way of feeling like we’re accomplishing something when we’re standing still (with respect to our goals).

This has been coming up for me recently around blogging and blogging software. I had a really good blogging habit for a while, and it served me well. Arguably I have my career because of it. Today when I read something interesting and want to comment, amplify, or rebut part of it, my thoughts still go to publishing. But I’ve lost the habit, the muscle, so instead I dither. I focus on how much I pay for WordPress hosting​*​. I view-source and look for tell-tale signs of what tool another author is using. I familiarize myself with headless CMSes, flat files, and “IndieWeb” standards. What I do not do is write.

Almost inevitably two things happen. First, I run out of time: I have to get back to work, I have to make dinner, etc. And second, when I finally return to the open browser tabs — now 90% meta, 10% what I wanted to reflect on — I say to myself, “why the fuck not wordpress? it’s not like it’s done anything to you in the past; yeah, it’s not new, and it’s not written in a language you’re enamoured with today, but it’s not like you have time to hack on it anyway. And if you did, you could do a lot worse than plugging into an ecosystem that big. Suck it up.” And then I close the tabs because I’m annoyed with how much time I’ve plowed into not-writing.

Later me is right: writing my own blogging software is in no way a good use of my time right now; I am not not-writing because of the software; using Jekyll Hugo Pelican Plume will not suddenly cause me to blog; making POSSE work is not a way to rebuild the blogging muscle; my theme is not my problem; post formats do not matter; Guttenberg didn’t reduce my likelihood to post; the list goes on.

Sometimes meta work is an interesting, high leverage way to approach things: there have been times I wrote a program that helped me write a program, and the result was doing something in a few days that we thought would take a few weeks​†​. The difference is, in those cases, I was already actively using the mental muscles I needed, I was already in “the habit”. When it comes to writing, making art, or sewing, the habit is really all that matters.

  1. ​*​
    I pay because a few years ago I was trying to get myself out of a similar rut, this time around what VPS to use and how to secure it. That time I told myself, “it doesn’t fucking matter, just pay to have someone else deal with this, and find a managed service.” And I’ve resented them ever since, despite the great service they provide. Probably because I’m not using the service I pay for.
  2. ​†​
    For example, the time I wrote a codemod using jscodeshift that added sane security defaults to Lob’s entire API codebase.

If you want to succeed as an artist, make as much work as possible. That is the secret sauce. Regardless of whether you are after commercial success or simply want to improve in your craft, the answer is the same, make more work.

How To Become A Successful Artist

I spent most of last week trying to deal with state in a React app; I wound up using my “secret weapon”: Contexts. Recoil looks like an interesting alternative.