To deepen my understanding of Linux networking, I set up a small Proxmox cluster with VXLAN tunneling and an Envoy proxy in front of the nodes. It started as a simple learning project, but quickly turned into one of those rare experiences where the concepts finally click instead of just being memorized.
What made this project memorable wasn’t just getting everything to work. It was the feeling that things finally connected.
When I read the explanation about the Diffie-Hellman key exchange failing because the packet size exceeded the MTU, it immediately made sense. Not because I memorized anything, but because everything I’d learned on my journey so far suddenly aligned. And that felt incredibly rewarding.
I wasn’t just applying a fix , I really understood why it was the fix.
And once again, I noticed something I keep rediscovering: whenever I take the time to visualize something, it stops being complicated and becomes intuitive. I had this experience before, and this project confirmed it again.
Key takeaways from this project:
sshd -Dd for the first time, following the handshake step-by-step until I saw exactly where the MTU caused the DH exchange to fail./etc/ made me appreciate the clarity and control that come with configuration-by-file./etc/ burned its structure into my brain. I finally have an intuitive sense of what lives there.
Topology drawn by me
The SSH Connection from VM100 (Frontend) to vm101 (Backend) wouldn’t work. I tried tinkering with the sshd config.
To debug I had to run the sshd service manually on vm101 with -o you can enable specific options without changing the config file permanently.
VM101$ sudo /usr/sbin/sshd -Dd -p 2222 -o PasswordAuthentication=yes
VM100$ ssh -vv -p 2222 user@172.20.0.101

In the last line we can cleary see that the error occurs during the key exchange. After some googling i found an answer to the problem. (https://unix.stackexchange.com/a/786463) . And it made perfect sense.
I configured the MTU on the VXLAN to 1450. Since an Eliptic-Curve Diffie-Hellman (with a maximum key size of about 512 bits) shouldn’t exceed that MTU, the SSH2_KEXINIT message was likely being fragmanted in a way that prevented the connection from being established.
After setting the MTU on vm100 to 1450 it worked fine.
sudo ip link set dev ens18 down
sudo ip link set dev ens18 mtu 1450
sudo ip link set dev ens18 up

Also this seems to be a bug in Proxmox. The normal behavior should be, that the virtual NIC inherits the MTU from the bridge. Which in our case is the VXLAN with MTU set to 1450. (https://forum.proxmox.com/threads/bug-vxlan-and-mtu.161412/ )
I also had some issues with DNS, so I set the DNS globally through /etc/systemd/resolved.conf
Configure network using netplan https://netplan.readthedocs.io/en/stable/examples/#how-to-configure-a-static-ip-address-on-an-interface
configure static ip (node01):
network:
version: 2
ethernets:
ens2:
addresses:
- 192.168.250.2/24
nameservers:
addresses: [ "1.1.1.1", "8.8.8.8" ]
routes:
- to: default
via: 192.168.250.1
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host noprefixroute
valid_lft forever preferred_lft forever
2: ens2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 52:54:00:97:29:b4 brd ff:ff:ff:ff:ff:ff
altname enp0s2
altname enx5254009729b4
inet 192.168.250.2/24 brd 192.168.250.255 scope global ens2
valid_lft forever preferred_lft forever
inet6 fe80::5054:ff:fe97:29b4/64 scope link proto kernel_ll
valid_lft forever preferred_lft forever
3: ens3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 52:54:00:80:cc:ec brd ff:ff:ff:ff:ff:ff
altname enp0s3
altname enx52540080ccec
inet6 fe80::5054:ff:fe80:ccec/64 scope link proto kernel_ll
valid_lft forever preferred_lft forever
VXLAN Source: https://forum.proxmox.com/threads/vxlan-multitenant.50686/#post-256806
apt install ifupdown2
ifreload -a
After rebooting the GW and node01 I had to restart the nftables service and I choose to enable it by default:
systemctl enable nftables
systemctl start nftables
qemu-img resize noble-server-cloudimg-amd64.img 16G
qm create 8001 --name "ubuntu-2404-cloudinit-template" --ostype l26 \
--memory 4096 \
--agent 1 \
--bios ovmf --machine q35 --efidisk0 local-lvm:0,pre-enrolled-keys=0 \
--cpu host --sockets 1 --cores 2 \
--vga serial0 --serial0 socket \
--net0 virtio,bridge=vmbr0
qm importdisk 8001 jammy-server-cloudimg-amd64.img local-lvm
qm set 8001 --scsihw virtio-scsi-pci --virtio0 local-lvm:vm-8001-disk-1,discard=on
qm set 8001 --boot order=virtio0
qm set 8001 --scsi1 local-lvm:cloudinit
mkdir /var/lib/vz/snippets
cat << EOF | tee /var/lib/vz/snippets/vendor.yaml
#cloud-config
runcmd:
- apt update
- apt install -y qemu-guest-agent
- systemctl start qemu-guest-agent
- reboot
# Taken from https://forum.proxmox.com/threads/combining-custom-cloud-init-with-auto-generated.59008/page-3#post-428772
EOF
qm set 8001 --cicustom "vendor=local:snippets/vendor.yaml"
qm set 8001 --tags ubuntu-template,24.04,cloudinit
qm set 8001 --ciuser ins
qm set 8001 --cipassword ins@lab
qm set 8001 --sshkeys ~/.ssh/authorized_keys
qm set 8001 --ipconfig0 ip=dhcp
sudo qm template 8001
Add port forwarding rule to nftables:
sudo sed -i 's/.*100:22.*/&\n tcp dport 1337 dnat to 192.168.250.100:1337/' \
/etc/nftables.conf
sudo service nftables restart
#!/bin/bash
# /usr/local/bin/frontend-listener.sh
# Frontend proxy using socat – forwards traffic to backend VM
BACKEND_IP="172.20.0.101"
BACKEND_PORT="1337"
FRONTEND_PORT="1337"
LOGFILE="/var/log/frontend-listener.log"
echo "$(date): Starting frontend listener, forwarding ${FRONTEND_PORT} -> ${BACKEND_IP}:${BACKEND_PORT}" >> "$LOGFILE"
# loop forever to keep it running
while true; do
socat TCP-LISTEN:${FRONTEND_PORT},fork TCP:${BACKEND_IP}:${BACKEND_PORT} >> "$LOGFILE" 2>&1
echo "$(date): socat exited, restarting..." >> "$LOGFILE"
sleep 2
done
[Unit]
Description=Frontend Listener (socat forwarder to backend)
After=network-online.target
Wants=network-online.target
[Service]
ExecStart=/usr/local/bin/frontend-listener.sh
Restart=always
RestartSec=3
User=root
StandardOutput=append:/var/log/frontend-listener.log
StandardError=append:/var/log/frontend-listener.log
[Install]
WantedBy=multi-user.target
#!/bin/bash
# /usr/local/bin/backend-listener.sh
# Simple backend listener using named pipe + nc
LOGFILE="/var/log/backend-listener.log"
FIFO="/tmp/f"
# cleanup previous fifo if needed
rm -f "$FIFO"
mkfifo "$FIFO"
# endless loop
while true; do
cat "$FIFO" | printf "backend @ $(date)\n\n(╯°□°)╯︵ ┻━┻\nCtrl-C to exit\n" \
| nc -l -p 1337 >> "$LOGFILE" 2>&1 > "$FIFO"
done
[Unit]
Description=Custom Backend Listener on port 1337
After=network-online.target
Wants=network-online.target
[Service]
ExecStart=/usr/local/bin/backend-listener.sh
Restart=always
RestartSec=3
User=root
StandardOutput=append:/var/log/backend-listener.log
StandardError=append:/var/log/backend-listener.log
[Install]
WantedBy=multi-user.target
Source: https://git.proxmox.com/?p=pve-docs.git;a=blob_plain;f=vxlan-and-evpn.adoc;hb=HEAD
auto lo
iface lo inet loopback
iface ens2 inet manual
auto ens3
iface ens3 inet static
address 192.168.100.2/24
auto vxlan20000
iface vxlan20000 inet manual
vxlan-id 20000
vxlan-local-tunnelip 192.168.100.2
vxlan-remoteip 192.168.200.2
mtu 1450
auto vmbr0
iface vmbr0 inet static
address 172.20.0.10/24
bridge_ports vxlan20000
bridge-stp off
bridge-fd 0
mtu 1450
auto vmbr1
iface vmbr1 inet static
address 192.168.250.2/24
gateway 192.168.250.1
# dns-nameservers 1.1.1.1 8.8.8.8 globally defined in /etc/systemd/resolved.conf
bridge-ports ens2
bridge-stp off
bridge-fd 0
auto lo
iface lo inet loopback
iface ens2 inet manual
auto ens3
iface ens3 inet static
address 192.168.200.2/24
auto vxlan20000
iface vxlan20000 inet manual
vxlan-id 20000
vxlan-local-tunnelip 192.168.200.2
vxlan-remoteip 192.168.100.2
mtu 1450
auto vmbr0
iface vmbr0 inet static
address 172.20.0.20/24
bridge_ports vxlan20000
bridge-stp off
bridge-fd 0
mtu 1450
auto vmbr1
iface vmbr1 inet static
address 192.168.250.3/24
gateway 192.168.250.1
# dns-nameservers 1.1.1.1 8.8.8.8 globally configured in /etc/systemd/resolved.conf
bridge-ports ens2
bridge-stp off
# This file is generated from information provided by the datasource. Changes
# to it will not persist across an instance reboot. To disable cloud-init's
# network configuration capabilities, write a file
# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:
# network: {config: disabled}
network:
bridges:
lan:
addresses:
- 192.168.250.1/24
interfaces:
- switchports
ethernets:
ens2:
accept-ra: true
dhcp4: true
optional: false
ens4:
addresses:
- 192.168.100.1/24
dhcp4: false
optional: true
ens6:
addresses:
- 192.168.200.1/24
dhcp4: false
optional: true
switchports:
accept-ra: false
dhcp4: false
dhcp6: false
link-local: []
match:
name: en*
optional: true
version: 2