UDP tunnel that wraps WireGuard traffic inside BACnet/IP packets (port 47808).
This project is for educational purposes only. The code may contain bugs and security vulnerabilities — use at your own risk. The author takes no responsibility for any damage, data loss, or legal consequences arising from its use.
lift-proxy provides no encryption or authentication of its own. It is a plain, unauthenticated UDP transport. All security — confidentiality, integrity, authentication — is handled entirely by WireGuard. Do not use lift-proxy to tunnel anything other than WireGuard (or another encrypted protocol) without understanding the implications.
[WireGuard client] <-> [lift-proxy client] <--(BACnet/IP UDP)--> [lift-proxy server] <-> [WireGuard server]
WireGuard packets are encapsulated in BACnet/IP BVLC frames, which look like industrial automation traffic to a DPI firewall.
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j$(nproc)Binary: build/lift-proxy
On the VPS (runs alongside WireGuard):
./lift-proxy \
--mode=server \
--listen=0.0.0.0:47808 \
--forward=127.0.0.1:51820--listen— port that receives BACnet packets from clients--forward— local WireGuard UDP port
On the client machine:
./lift-proxy \
--mode=client \
--listen=127.0.0.1:51820 \
--forward=SERVER_IP:47808 \
--queue_size=16--listen— local port WireGuard is configured to use as endpoint--forward— server IP and BACnet port--queue_size— send queue depth (see tuning section)
[Interface]
PrivateKey = <server private key>
Address = 10.10.0.1/24
ListenPort = 51820
[Peer]
PublicKey = <client public key>
AllowedIPs = 10.10.0.2/32[Interface]
PrivateKey = <client private key>
Address = 10.10.0.2/24
DNS = 1.1.1.1
MTU = 1414
[Peer]
PublicKey = <server public key>
Endpoint = 127.0.0.1:51820
AllowedIPs = <0.0.0.0/0 split excluding your server IP>
PersistentKeepalive = 25The Endpoint points to localhost because lift-proxy client listens there and forwards to the real server.
AllowedIPs must route all traffic through the tunnel except the server's own IP (otherwise tunnel packets would loop back into the tunnel). WireGuard doesn't support exclusions natively, so you need to split 0.0.0.0/0 into CIDR blocks that cover everything except your server IP. Use any online "exclude IP from CIDR" calculator (e.g. search "wireguard exclude ip allowed ips calculator").
The correct MTU for the WireGuard interface is:
MTU = physical_iface_MTU - 20 (outer IP) - 8 (outer UDP) - 10 (BACnet header) - 48 (WireGuard overhead)
For a standard 1500 MTU network: 1414.
If MTU is too high, large packets (TLS handshakes, HTTP responses) get fragmented and may be dropped by the network, causing:
- TLS handshake timeouts in browsers
- Sites loading via curl but not in browser
- GitHub and other HTTPS-heavy sites timing out
The --queue_size flag controls how many packets can queue up before older ones are dropped.
Default is 4096, which causes severe bufferbloat on slow upload links — ping spikes to 500-700ms during uploads because the queue holds several seconds of backlog.
Recommended values:
16— low latency, some packet loss under heavy load (TCP self-throttles quickly)64— balanced4096— default, only useful on very fast symmetric links
For typical consumer connections (10-50 Mbit/s upload), use --queue_size=16.
Without PersistentKeepalive, WireGuard lets the session go idle and must re-handshake on the next packet. This causes the first ping after a long idle period to take 1+ seconds.
Set PersistentKeepalive = 25 in the client's [Peer] section.