Making Sophos SSLVPN work on Linux
My employer hands out "SSLVPN" (it's actually not, that's purely branding) access as a .scx file for the Sophos Connect client, which only exists for Windows and macOS. Turns out that the .scx file is actually all you need.
Inside you'll find everything needed for a standard IKEv1 remote-access tunnel:
{
"gateway" : "vpn.example.com",
"proposals" : [ "aes256-sha2_256-modp2048" ],
"child" : {
"remote_ts" : [ "192.168.200.0/24", "10.61.50.0/24" ]
},
"remote_auth" : {
"psk" : { "secret" : "<the pre-shared key, in plaintext!>" }
},
"local_auth" : { "xauth" : { "can_save" : true } }
}
So: IKEv1, PSK plus XAuth (your username/password), AES-256/SHA-256/DH14, split tunneling for two subnets, and a virtual IP assigned by the firewall via mode config. Side note: the PSK sits in that file in plaintext, so treat .scx files as secrets (thanks, Sophos!).
Translating to swanctl
The whole thing becomes one /etc/swanctl/conf.d/ file with a shape similar to this:
connections {
work {
version = 1 # IKEv1
remote_addrs = vpn.example.com
vips = 0.0.0.0 # request a virtual IP
proposals = aes256-sha256-modp2048
local { auth = psk }
local-xauth { auth = xauth
xauth_id = [email protected] }
remote { auth = psk
id = <gateway IP> }
children {
work { remote_ts = 192.168.200.0/24
esp_proposals = aes256-sha256-modp2048 }
work2 { remote_ts = 10.61.50.0/24
esp_proposals = aes256-sha256-modp2048 }
}
}
}
secrets {
ike-work { id = <gateway IP> secret = "<PSK from the .scx>" }
xauth-work { id = [email protected] secret = "<your password>" }
}
Then do swanctl --load-all and swanctl --initiate --child work. It just works. Well, almost.
Gotcha 1: IKEv1 does one subnet per child SA
My first config naively listed both subnets in one child. The tunnel came up and actually covered only the first subnet. IKEv1 Quick Mode negotiates exactly one traffic selector pair per CHILD_SA; there's no way around it. The Windows client hides this by negotiating one SA per subnet behind the scenes. The fix is to do the same: one child block per subnet (the work / work2 split above). Both use the same IKE_SA, so there's still only one login.
Gotcha 2: Docker will conflict with your tunnel
With both SAs established, actual traffic still didn't go anywhere. swanctl --list-sas showed 0 bytes, 0 packets on every SA. Packets weren't entering the tunnel properly.
The culprit was in the NAT table:
-A POSTROUTING -s 172.22.0.0/16 ! -o br-014a9ffb9136 -j MASQUERADE
Docker had created a bridge network on 172.22.0.0/16, and the firewall happened to assign my VPN virtual IP from inside that range. Docker's MASQUERADE rule matched every VPN-bound packet and rewrote its source address before IPsec encapsulation, after which the packet no longer matched the tunnel policy and was silently sent in the clear to die.
The fix is to simply exempt IPsec-bound traffic from NAT, ahead of Docker's rules:
iptables -t nat -I POSTROUTING 1 -m policy --pol ipsec --dir out -j ACCEPT
Gotcha 3: don't bother with NetworkManager
The NM strongSwan plugin is IKEv2-only; the vpnc plugin does speak IKEv1 XAuth but its crypto stops at SHA-1 and DH group 5, which absolutely no modern firewall accepts. There is, AFAIK, no GUI that handles this rather odd setup.
A tiny systemd unit is a decent fix:
[Unit]
Description=Work VPN
Requires=strongswan.service
After=strongswan.service network-online.target
Wants=network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStartPre=/usr/bin/swanctl --load-all --noprompt
ExecStartPre=/usr/bin/bash -c 'iptables -t nat -C POSTROUTING -m policy --pol ipsec --dir out -j ACCEPT 2>/dev/null || iptables -t nat -I POSTROUTING 1 -m policy --pol ipsec --dir out -j ACCEPT'
ExecStart=/usr/bin/swanctl --initiate --child work --timeout 30
ExecStart=/usr/bin/swanctl --initiate --child work2 --timeout 30
ExecStop=/usr/bin/swanctl --terminate --ike work --timeout 15
ExecStopPost=-/usr/bin/iptables -t nat -D POSTROUTING -m policy --pol ipsec --dir out -j ACCEPT
[Install]
WantedBy=multi-user.target
systemctl start work-vpn to connect, systemctl stop work-vpn to disconnect. The iptables rule gets created and dropped as needed.