Cantennas outperform every consumer-grade Wi-Fi antenna I’ve had the bad luck of purchasing. Cantenna is a mashup of ‘can’ and ‘antenna’ creating the nickname for a directional waveguide antenna built from re-purposed steel cans. For anyone who has yet to build one, it makes an excellent afternoon project. Here are some build instructions and technical details. I went beyond that, and ended up catching a rogue WiFi access point in the process.

When I needed to extend the range of some ESP8266-based sensors, cantennas were right at the top of my list of things to try. It was easy enough to build one, attach it to a Wemos Mini D1 Pro, and call the job done… leaving me with plenty of time to over-engineer it, and I ended up down a bit of a rabbit hole.

The first thing I did was stop using cans. Canned goods are not only expensive in my corner of the world, but more importantly don’t lend themselves that well to making a standardized antenna in volume. I can also only eat so many beans! The latter reason alone is enough to consider an alternative design like a modular dish reflector.

Building a Better Cantenna: Ditch the Cans

However, for about the same price as a large canned food item, I was able to purchase a sheet of 0.6mm thick copper. After measuring it out and cutting it, I wrapped it around the inside of an 80 mm PVC pipe, resulting in a tube about 79 mm wide. Later I found out it was easier to wrap the copper around the outside of the tube, and attach 90 mm diameter hose clamps to hold it in place. In both cases I soldered the result on to a copper base.

Then I measured and installed an IPEX antenna connector where the pickup wire should be before plugging a wire of the correct length into it. It seemed to work quite well, but it would be better if I could characterize it a little, or at least come up with a way to conveniently align a few cantennas. It turns out that it’s easy to get the RSSI (Received Signal Strength Indication) of the access point an ESP8266 running NodeMCU is connected to:

x = wifi.sta.getrssi() print (x)

Using RSSI for Antenna Alignment

While RSSI is a vague way of measuring signal strength, it seemed good enough to align cantennas. To test it out, I set one cantenna-connected mini D1 Pro to both act as an access point and connect to my router.

wifi.setphymode(wifi.PHYMODE_B) tries=0 ssid="Home Network Name" pswd="myrealpassword" --start by setting both station and AP modes wifi.setmode(wifi.STATIONAP) wifi.ap.setip({ip = "192.168.4.1", netmask = "255.255.255.0", gateway = "192.168.0.1"}); wifi.ap.config({ssid="Tenna",pwd = "foxhunting"}) tmr.alarm(0, 100, 1, function(Q) if wifi.ap.getip() == nil then tries=tries+1 --wait for AP to come up else tmr.stop(0) print("after "..tries.." AP IP=",wifi.ap.getip()) --now connect to LAN wifi.sta.config(ssid,pswd) wifi.sta.connect() tries=0 tmr.alarm(0, 100, 1, function(Q) if wifi.sta.getip() == nil then tries=tries+1 --wait for LAN to come up else tmr.stop(0) print("after "..tries.." STA IP=",wifi.sta.getip()) dofile("server.lua") end end) end end)

Once the access point was running and it was connected to my router, any UDP packets it received on a certain port were set to be uploaded to an IoT data logger. I used Thingsboard, but other services would work just fine. I saw a range of -2 to -91 dBi this way, using Wi-Fi 802.11b.

port=10001 ClientID = 'put anything here' token = 'put token here' m = mqtt.Client(ClientID, 120, token) srv=net.createServer(net.UDP) srv:on("receive", function(srv, pl) m:connect("mydomain.com", 1883, 0, function(client) print("connected") m:publish("v1/devices/me/telemetry", '{"RSSI":'..pl..'}', 2, 1) m:close() end) end) srv:listen(port)

A second cantenna-connected mini D1 Pro was programmed to measure the RSSI and send it to the first one as a UDP packet. I powered it with a small spare cellphone battery. This worked quite well and saved me a screen as I could just walk around with this system and leave my smartphone open on the IoT data logging backend. The range was quite good, passing through a number of concrete buildings. The UDP packet sending function crashed every few minutes due to what was reported as an ‘unknown error’, but rather than debug it, I called the offending function in protected mode (pcall) and since then it has worked flawlessly:

sda = 2 -- SDA Pin scl = 1 -- SCL Pin port=10001 sla = 0x3C i2c.setup(0, sda, scl, i2c.SLOW) disp = u8g.ssd1306_64x48_i2c(sla) disp:setFontRefHeightExtendedText() disp:setDefaultForegroundColor() disp:setFontPosTop() disp:firstPage() repeat pl = "Fox" deg = "Hunt" disp:setFont(u8g.font_profont17r) disp:drawStr(0, 20, pl) disp:setFont(u8g.font_profont17r) disp:drawStr(0, 35, deg) until disp:nextPage() == false enduser_setup.start( function() print("Connected to wifi as:" .. wifi.sta.getip()) end, function(err, str) print("enduser_setup: Err #" .. err .. ": " .. str) end, print -- Lua print function can serve as the debug callback ); function poll(level) pl = wifi.sta.getrssi() if pl == nil then print "NC" pl = "NC" sla = 0x3C i2c.setup(0, sda, scl, i2c.SLOW) disp = u8g.ssd1306_64x48_i2c(sla) disp:setFontRefHeightExtendedText() disp:setDefaultForegroundColor() disp:setFontPosTop() disp:firstPage() repeat cee = "dBi" disp:setFont(u8g.font_profont17r) disp:drawStr(5, 15, pl) disp:setFont(u8g.font_profont17r) disp:drawStr(5, 35, cee) until disp:nextPage() == false else sla = 0x3C i2c.setup(0, sda, scl, i2c.SLOW) disp = u8g.ssd1306_64x48_i2c(sla) disp:setFontRefHeightExtendedText() disp:setDefaultForegroundColor() disp:setFontPosTop() disp:firstPage() repeat cee = "dBi" disp:setFont(u8g.font_profont17r) disp:drawStr(5, 15, pl) disp:setFont(u8g.font_profont17r) disp:drawStr(5, 35, cee) until disp:nextPage() == false end pcall(send) end function send(level) pl = wifi.sta.getrssi() if pl == nil then print "NC" pl = "NC" else print (pl) udpSocket = net.createUDPSocket() udpSocket:send(port,"192.168.4.1",pl) end end tmr.alarm(1, 1000, tmr.ALARM_AUTO, function() poll(0) end)

One thing I noticed was that it was very easy to directly align the antennas using the RSSI reading, even without good line of sight. I started thinking it might be a good tool for locating rogue access points… so I added a small screen to display the RSSI of a chosen network, and used the ‘end user config’ function (included above) so my smartphone can instruct it to scan for and connect to arbitrary networks.

Field Testing as an AP Locator

I brought it to a coffee shop I knew I could try it at without irritating the staff. I was able to find the router in under a minute, which I probably could have done visually in about the same amount of time because it was just an off-the-shelf router left on a table in plain view – but the point was that it worked well. Then a few memories clicked into place and something occurred to me…

I had seen a restaurant months ago that had one more Wi-Fi network than made sense. There was a network for each floor, one that was clearly for the POS system, and then one more that had slightly different capitalization in the SSID. I didn’t think anything about it at the time, but it was time to go for a visit.

It was immediately clear that it was a rogue access point, because it used the same password as the staff gave me, but was redirecting me to download a file called ‘facebook.scr’ (what a blast from the past – why are .SCR files still executable?). The Internet still worked because the rogue access point was acting as a bridge to the proper Wi-Fi network.

So I downloaded the file, opened a terminal, and ran ‘strings facebook.scr > output.txt’, looked through it, and saw that it was run of the mill Bitcoin mining malware. There was enough information within the file to see the mining volume in the mining pool too. In any case, I had an access point to find!

It was on another floor, but didn’t take long to find. The legitimate access points were just normal residential routers taped to the wall and were easy to spot, but the rogue access point was another story. It was clearly professionally installed at or before the time the business moved in: the wiring went through the same conduits as the electrical sockets installed in the walls. It was also painted over in exactly the same color of ceiling paint as everything else. In other words, it wasn’t just left around. It was very carefully installed, and probably consists of a commercial ceiling-mount Wi-Fi router with improved firmware.

More interesting still, was that the Bitcoin mining power of the pool was not huge, but much greater than a single such device would explain if it represents their primary vector. Given the careful installation, it’s possible that it is. I’m no masked vigilante though — I’ve passed my findings off to the relevant authorities, and they can deal with it as they see fit from here on.