1.The Infrastructure Problem: Reaching the Basement
Every home lab eventually hits a physical wall — or in my case, a physical floor. I needed to deploy a bare-metal Proxmox hypervisor in my basement. The location had stable AC power, but no Ethernet, and running new CAT6 wasn’t an option. The fallback was Powerline Communication (PLC) using the HomePlug AV2 standard.
However, bringing a hypervisor online isn’t just about getting a ping response; it requires strict network segmentation. My infrastructure architecture relies on 802.1Q VLANs to separate traffic:
- VLAN 100: Management Network
- VLAN 10: Main Network
- VLAN 20: Development Network
- VLAN 30: Guest Network
Here is the intended logical topology:
flowchart LR
subgraph MainNet [Upstream Network]
Core["Main Router"]
PLC1["TL-PA8010P"]
Core -- " 802.1Q Trunk<br>(VLAN 100, 10, 20, 30) " --> PLC1
end
Grid(("Home Electrical Wiring<br>AES-128 Encrypted NMK"))
subgraph BasementLab [Basement Proxmox Lab]
WPA["TL-WPA8630P<br>OpenWrt Mgmt: 192.168.100.6/24"]
subgraph Proxmox [Proxmox Server]
BMC["IPMI Dedicated Port<br>(VLAN 100)"]
PVE["Proxmox Host & VMs<br>(802.1Q Trunk)"]
end
NAS["Standalone NAS<br>(VLAN 10)"]
WiFi(("Wi-Fi AP<br>VLAN 10 / 20 / 30"))
WPA -- " LAN 1 / Port 3<br>(Untagged PVID 100) " --> BMC
WPA -- " LAN 0 / Port 4<br>(Trunk: 10, 20, 30) " --> PVE
WPA -- " LAN 2 / Port 2<br>(Untagged PVID 10) " --> NAS
WPA -. " Internal Bridge " .-> WiFi
end
PLC1 === Grid
Grid === WPA
The Roadblock: Standard consumer PLC adapters—like the TP-Link TL-WPA8630P—are limited in capabilities. 802.1Q VLANs for network segmentation are not supported. The stock OEM firmware bridges all traffic into a single domain. It offers no way to assign Port VLAN IDs (PVIDs) and does not allow VLAN trunking.
To enable 802.1Q VLANs, I needed to get OpenWrt onto the device. Doing so for this specific device first required solving a hardware bottleneck.
2.Embedded Hacking: Taking Control of the Silicon
CAUTION
Powerline adapters operate directly on mains voltage (110V/230V), which may be lethal. Even after being unplugged, the internal capacitors remain charged for a while and can deliver a dangerous shock.
Do not power the disassembled board from the mains while working on it. As suggested by the OpenWrt Wiki for this device, if you need to power the board for live testing, inject power safely using the 3.3V serial header.
The TP-Link TL-WPA8630P v2 is powered by a highly capable Qualcomm QCA9563 SoC that has plenty of muscle to run OpenWrt, but it ships with a crippling hardware limitation: a tiny 8MB SPI flash chip.
A modern OpenWrt build equipped with necessary routing packages simply will not fit in 8MB. Because of this, official OpenWrt support for this device was dropped around OpenWrt version 21. To bring this device up to modern standards, I had two main tasks:
- Upgrade the physical memory chip to something bigger.
- Build custom OpenWrt firmware tailored to the new hardware constraints.
2.1. The Silicon Transplant and the Flash Dump
A core tenet of infrastructure engineering is always having a rollback plan. Because I didn’t want to blindly trust my manual SMD soldering abilities, I needed to capture the original flash contents before applying any heat to the board. This factory dump contains vital, device-specific data: the U-Boot bootloader (which holds compiled-in settings to properly initialize the RAM), as well as Wi-Fi calibration data required for the hardware to function.
I connected a CH341A USB SPI Programmer with a SOIC8 test clip directly to the factory chip while it was still on
the motherboard. To rule out any data corruption caused by background SoC activity, I used flashrom on my Linux
workstation to pull five separate dumps:
# Read the factory 8MB SPI flash chip (repeated 5 times)
flashrom -p ch341a_spi -r tplink_factory_8mb_on_board_take_1.bin
(...)
Next, I ran some basic sanity checks to ensure data integrity:
# Compare hashes between files to ensure the reads were identical
sha256sum tplink_factory_8mb_take*.bin
# Inspect the binary headers and extract human-readable text
binwalk tplink_factory_8mb_on_board_take_1.bin
strings tplink_factory_8mb_on_board_take_1.bin
Once I was confident in the backup, I desoldered the chip. Just to be certain, I put the desoldered chip directly into the programmer, took one final dump, and compared the hashes — they matched. Finally, I soldered a new 16MB Winbond 25Q128JVSQ chip that I had lying around onto the board.
2.2. Building a Custom Firmware
To utilize the newly expanded space, I needed firmware with a custom partition layout. Initially, I tried replacing the bootloader with a generic open-source alternative, but the board failed to boot—likely due to incorrect RAM initialization parameters. Rather than spending days decompiling the original bootloader to extract the exact RAM init parameters, I decided to build around it.
This meant keeping TP-Link’s proprietary U-Boot. The issue is TP-Link utilizes a “SafeLoader” mechanism that hardcodes
partition boundaries and expects the OS kernel to be located at a very specific address. My workaround was to engineer a
“split-flash” map: I would keep the first 8MB logically identical to satisfy U-Boot, but map the
extra 8MB on the new chip as a secondary rootfs filesystem partition.
First, I pulled down the OpenWrt source and updated the feeds:
git clone https://git.openwrt.org/openwrt/openwrt.git
cd openwrt
./scripts/feeds update -a && ./scripts/feeds install -a
Even though official support was dropped, the device definition files for older hardware still exist in the repository.
I modified the Device Tree (DTS) file for the 8MB chip to recognize my 16MB layout. I added the following block to the end
of the partitions definition to map target/linux/ath79/dts/qca9563_tplink_tl-wpa8630p-v2.0-eu.dts the upper 8MB of the
Winbond chip:
partition@800000 {
label = "rootfs";
reg = <0x800000 0x800000>;
};
Then, it was time to compile the OS:
# Select the target (ath79), subtarget (tiny), and WPA8630P profile
make menuconfig
# Compile the custom firmware using all CPU cores
make -j$(nproc) V=s
The build process throws a size error at the very end, complaining that the firmware is too large for the
target. This is expected. To bypass the build script’s limitations, I manually stitched the firmware parts together
using dd to create the final 16MB payload:
# 1. Create a blank 16MB file padded with standard flash erasure bytes (0xFF)
tr '\000' '\377' < /dev/zero | dd of=custom_16mb.bin bs=1024 count=16384 iflag=fullblock
# 2. Drop the original 8MB factory dump (containing the bootloader & calibration) into the beginning
dd if=original_8mb.bin of=custom_16mb.bin bs=1024 count=8192 conv=notrunc
# 3. Inject the newly compiled OpenWrt kernel at the 256KB offset (where SafeLoader expects it)
dd if=build_dir/target-mips_24kc_musl/linux-ath79_tiny/tplink_tl-wpa8630p-v2.0-eu-kernel.bin of=custom_16mb.bin bs=1024 seek=256 conv=notrunc
# 4. Inject the compiled SquashFS root filesystem into the upper 8MB boundary
dd if=build_dir/target-mips_24kc_musl/linux-ath79_tiny/root.squashfs of=custom_16mb.bin bs=1024 seek=8192 conv=notrunc
# 5. Flash the fully assembled 16MB binary directly to the chip
flashrom -p ch341a_spi -w custom_16mb.bin
After flashing the chip and reassembling the unit, it booted correctly.
2.3.Resurrecting the Radios
Wi-Fi radios require calibration data to comply with FCC/CE transmission limits and to maintain signal
stability. This data is stored in the ART (Atheros Radio Test) partition, officially
labeled radio on this board.
While my custom 16MB image retained this partition, the 5GHz PCIe radio remained dead. The calibration data
was physically there, but the OpenWrt ath10k driver was failing to read it correctly from the MTD block as a quick
look
in dmesg revealed.
To fix this, I had to manually extract the specific calibration block from the raw MTD partition and hot-patch it directly into the Linux firmware directory so the driver could load it during initialization:
# Extract the calibration block for the QCA9888 radio and put in the location the driver expects
dd if=/dev/mtd6 of=/lib/firmware/ath10k/QCA9888/hw2.0/board.bin bs=1 skip=20480 count=12064
# Reload the kernel modules to initialize the radio with the newly applied calibration
rmmod ath10k_pci
rmmod ath10k_core
modprobe ath10k_pci
3.Investigating Security of PLC
TL;DR: PLC security is sufficient for a home environment if you change the “password” aka the Network Membership Key (NMK).
Out of the box, most AV2 adapters ship with the same public key (HomePlugAV), making your network trivial to join or
sniff from a neighboring apartment. I generated a custom, cryptographically secure AES-128 NMK and injected it directly
into the chip’s NVRAM via the OpenWrt CLI, following
the OpenWrt Wiki.
3.1.The Weak Links: Vendor Firmware & Silicon Blobs
The primary vulnerability in PLC setups isn’t the protocol, but the software:
-
Vendor Firmware: TP-Link has a poor track record for patching security flaws. Replacing the stock OS with OpenWrt effectively eradicates this attack surface.
-
The QCA7550 SoC: The Powerline communication is handled by an independent Qualcomm SoC. My adapters shipped with an ancient firmware version: v2.2.3 (year 2016), which is vulnerable to CVE-2022-33265 (CVSS 9.8 — Critical).
While TP-Link has neglected these updates, the firmware is fortunately universal across QCA7550 devices and released by Qualcomm. I sourced the v2.13.0053 (year 2023) firmware (addressing the critical CVSS flaw) and manually backported it.
3.2.Implementation: Backporting the Baseband
To update the firmware without losing configuration, you must flash the new firmware executable (.nvm) with
device’s unique Parameter Information Block (.pib). It is done using the plctool utility from the OpenWrt host.
# 1. Install management tools
apk update && apk add open-plc-utils-plctool open-plc-utils-plcrate open-plc-utils-chknvm
# 2. Baseline check (identifying the firmware)
plctool -a -i br-lan local
# 3. Backup unique hardware calibration (PIB)
plctool -i br-lan -p WPA8630P_factory.pib local
# 4. Flash the modern, patched firmware
# Note: Use the 'stripped' NVM for WPA8630P to fit limited flash boundaries
plctool -i br-lan -P /root/WPA8630P_factory.pib -N /root/QCA75XX-2.13.0053_stripped.nvm local
# 5. Update the other end (PA8010P) remotely
# CAUTION: Do not mix up PIBs between units. This will probably cause a brick.
#
# Make sure device is paired.
# Then repeat the procedure using remote device's MAC instead of 'local'.
After flashing, a hard power cycle is required to initialize the new DSP logic.
4.Architecting the Network: VLAN Trunking in Silicon
With the PLC link secured and the firmware patched, the final step was to configure 802.1Q VLAN Trunking on the hardware switch. This allows the basement lab to carry isolated traffic over the same physical copper wire.
4.1.Internal Switch Mapping
The TL-WPA8630P uses an internal QCA8337N switch. To configure it correctly, you have to map the internal ports to the physical layout:
- Port 0 (CPU): The internal interface to OpenWrt host.
- Port 5 (PLC): The QCA7550 PLC interface.
- Port 4 (LAN0): Configured as a Trunk for Proxmox.
- Port 3 (LAN1): Access Port for IPMI (VLAN 100).
- Port 2 (LAN2): Access Port for NAS (VLAN 10).
4.2.Configuration Script
I used the following uci commands to wipe the default switching state and define the new VLAN boundaries. This setup
creates “Access Ports” at the hardware level by using PVIDs to force untagged traffic from specific devices into their
respective VLANs.
# 1. Reset switch state
while uci -q delete network.@switch_vlan[0]; do :; done
while uci -q delete network.@switch_port[0]; do :; done
# 2. Define VLANs
# VLAN 10: Traffic (Trunk on LAN0 & PLC, Access on LAN2)
uci add network switch_vlan
uci set network.@switch_vlan[-1].device="switch0"
uci set network.@switch_vlan[-1].vlan="10"
uci set network.@switch_vlan[-1].ports="0t 2 4t 5t"
# VLAN 100: Management (Trunk on PLC, Access on LAN1)
uci add network switch_vlan
uci set network.@switch_vlan[-1].device="switch0"
uci set network.@switch_vlan[-1].vlan="100"
uci set network.@switch_vlan[-1].ports="0t 3 5t"
# 3. Port-Based VLAN IDs (PVID)
# LAN1 -> Management (VLAN 100)
uci add network switch_port
uci set network.@switch_port[-1].device='switch0'
uci set network.@switch_port[-1].port='3'
uci set network.@switch_port[-1].pvid='100'
# LAN2 -> Traffic (VLAN 10)
uci add network switch_port
uci set network.@switch_port[-1].device='switch0'
uci set network.@switch_port[-1].port='2'
uci set network.@switch_port[-1].pvid='10'
# 4. Host Management Interface & Firewall
uci set network.mgmt=interface
uci set network.mgmt.ifname='eth0.100'
uci set network.mgmt.proto='static'
uci set network.mgmt.ipaddr='192.168.100.6'
uci set network.mgmt.netmask='255.255.255.0'
uci set network.mgmt.gateway='192.168.100.1'
uci add_list network.mgmt.dns='192.168.100.1'
uci add firewall zone
uci set firewall.@zone[-1].name='mgmt'
uci set firewall.@zone[-1].network='mgmt'
uci set firewall.@zone[-1].input='ACCEPT'
uci commit network && uci commit firewall
/etc/init.d/network restart
5.Performance Tests and Conclusion
To quantify the impact of the v2.13.0053 backport, I conducted a series of 3-minutes iperf3 tests, comparing the
factory firmware against the modern Qualcomm blob. Tests were performed in two scenarios: a “Synthetic Maximum” (both
PLC endpoints connected to the same
extension cord) and the “Real World” (basement to the second floor).
5.1.Scenario 1: Synthetic Maximum (Same Extension Cord)
This baseline eliminates house-wiring interference to test the raw throughput potential of the DSP logic.
| Firmware | Proto | UL [Mbps] | DL [Mbps] |
|---|---|---|---|
| v2.2.3 | TCP | 411 | 247 |
| v2.13 | TCP | 432 | 402 |
| v2.2.3 | UDP | 457 | 300 |
| v2.13 | UDP | 497 | 483 |
5.2.Scenario 2: Real World (Basement Deployment)
The adapters were moved to their target locations. Note that these tests were conducted on different days with varying
electromagnetic noise profiles, v2.2.3 and v2.13 t2 - on weekday, v2.13 t1 - on weekend.
| Firmware | Proto | UL [Mbps] | DL [Mbps] |
|---|---|---|---|
| v2.2.3 | TCP | 153 | 185 |
| v2.13 t1 | TCP | 146 | 148 |
| v2.13 t2 | TCP | 162 | 207 |
| v2.2.3 | UDP | 175 | 213 |
| v2.13 t1 | UDP | 168 | 194 |
| v2.13 t2 | UDP | 182 | 244 |
Note: Throughput on firmware v2.13 was noticeably more stable during the 180-second test window.