blob: b1ed478f6f0c0d06d838ae22ed1e44172d58464a [file] [log] [blame]
// Copyright 2020 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package vpn
import (
"context"
"fmt"
"io/ioutil"
"path/filepath"
"time"
"chromiumos/tast/common/crypto/certificate"
"chromiumos/tast/errors"
"chromiumos/tast/local/bundles/cros/network/chroot"
"chromiumos/tast/testing"
)
const logName = "vpnlogs.txt"
// Constants that used by the L2TP/IPsec and IKEv2 server.
const (
caCertFile = "etc/swanctl/x509ca/ca.cert"
charonCommand = "/usr/libexec/ipsec/charon"
charonLogFile = "var/log/charon.log"
charonPidFile = "run/ipsec/charon.pid"
chapUser = "chapuser"
chapSecret = "chapsecret"
ikeClientIdentity = "client-id"
ikeServerIdentity = "C=US, ST=California, L=Mountain View, CN=chromelab-wifi-testbed-server.mtv.google.com"
ikev2ClientIP = "192.168.1.128"
ikev2ServerIP = "192.168.1.99"
ikev2InterfaceID = "2"
ipsecPresharedKey = "preshared-key"
makeIPsecDir = "mkdir -p /run/ipsec"
pppdPidFile = "run/ppp0.pid"
swanctlCommand = "/usr/sbin/swanctl"
viciSocketFile = "run/ipsec/charon.vici"
xauthUser = "xauth_user"
xauthPassword = "xauth_password"
xl2tpdCommand = "/usr/sbin/xl2tpd"
xl2tpdConfigFile = "etc/xl2tpd/xl2tpd.conf"
xl2tpdPidFile = "run/xl2tpd.pid"
xl2tpdServerIPAddress = "192.168.1.99"
)
var (
strongSwanDirectories = []string{
"etc/swanctl",
"etc/swanctl/x509ca",
"etc/swanctl/x509",
"etc/swanctl/private",
}
l2tpDirectories = []string{
"etc/ppp",
"etc/xl2tpd",
}
strongSwanConfigs = map[string]string{
"etc/strongswan.conf": "charon {\n" +
" filelog {\n" +
" test_vpn {\n" +
" path = {{.charon_logfile}}\n" +
" default = 3\n" +
" time_format = %b %e %T\n" +
" dmn = 2\n" +
" mgr = 2\n" +
" ike = 2\n" +
" net = 2\n" +
" }\n" +
" }\n" +
" install_routes = no\n" +
" ignore_routing_tables = 0\n" +
" routing_table = 0\n" +
"}\n",
"etc/swanctl/swanctl.conf": "connections {\n" +
" ikev1-l2tp-test {\n" +
" version = 1\n" +
" {{if .preshared_key}}" +
" local-psk {\n" +
" auth = psk\n" +
" }\n" +
" remote-psk {\n" +
" auth = psk\n" +
" }\n" +
" {{end}}" +
" {{if .xauth_user}}" +
" remote-xauth {\n" +
" auth = xauth\n" +
" }\n" +
" {{end}}" +
" {{if .server_cert_id}}" +
" local-cert {\n" +
" auth = pubkey\n" +
" id = {{.server_cert_id}}\n" +
" }\n" +
" {{end}}" +
" {{if .ca_cert_file}}" +
" remote-cert {\n" +
" auth = pubkey\n" +
" cacerts = /{{.ca_cert_file}}\n" +
" }\n" +
" {{end}}" +
" children {\n" +
" ikev1-l2tp {\n" +
" local_ts = dynamic[/1701]\n" +
" mode = transport\n" +
" }\n" +
" }\n" +
" }\n" +
"" +
" ikev2-test {\n" +
" version = 2\n" +
" pools = ikev2-vip-pools\n" +
" {{if .preshared_key}}" +
" local-psk {\n" +
" auth = psk\n" +
" id={{.server_id}}\n" +
" }\n" +
" remote-psk {\n" +
" auth = psk\n" +
" id={{.client_id}}\n" +
" }\n" +
" {{end}}" +
" {{if .eap_user}}" +
" remote-eap {\n" +
" auth = eap-mschapv2\n" +
" eap_id = %any\n" +
" }\n" +
" {{end}}" +
" {{if .server_cert_id}}" +
" local-cert {\n" +
" auth = pubkey\n" +
" id = {{.server_cert_id}}\n" +
" }\n" +
" {{end}}" +
" {{if .ca_cert_file}}" +
" remote-cert {\n" +
" auth = pubkey\n" +
" cacerts = /{{.ca_cert_file}}\n" +
" }\n" +
" {{end}}" +
" children {\n" +
" ikev2 {\n" +
" local_ts = 0.0.0.0/0\n" +
" {{if .client_vip}}" +
" remote_ts = {{.client_vip}}/32\n" +
" {{end}}" +
" {{if .if_id}}" +
" if_id_in = {{.if_id}}\n" +
" if_id_out = {{.if_id}}\n" +
" {{end}}" +
" mode = tunnel\n" +
" }\n" +
" }\n" +
" }\n" +
"}\n" +
"" +
"secrets {\n" +
" {{if .preshared_key}}" +
" ike-1 {\n" +
" secret = \"{{.preshared_key}}\"\n" +
" }\n" +
" {{end}}" +
" {{if .xauth_user}}" +
" xauth-1 {\n" +
" id = {{.xauth_user}}\n" +
" secret = {{.xauth_password}}\n" +
" }\n" +
" {{end}}" +
" {{if .eap_user}}" +
" eap-1 {\n" +
" id = \"{{.eap_user}}\"\n" +
" secret = \"{{.eap_password}}\"\n" +
" }\n" +
" {{end}}" +
"}\n" +
"pools {\n" +
" {{if .client_vip}}" +
" ikev2-vip-pools {\n" +
" addrs = {{.client_vip}}/32\n" +
" }\n" +
" {{end}}" +
"}\n",
"etc/passwd": "root:x:0:0:root:/root:/bin/bash\n" +
"vpn:*:20174:20174::/dev/null:/bin/false\n",
"etc/group": "vpn:x:20174:\n",
caCertFile: certificate.TestCert1().CACred.Cert,
"etc/swanctl/x509/server.cert": certificate.TestCert1().ServerCred.Cert,
"etc/swanctl/private/server.key": certificate.TestCert1().ServerCred.PrivateKey,
}
l2tpConfigs = map[string]string{
xl2tpdConfigFile: "[global]\n" +
"\n" +
"[lns default]\n" +
" ip range = 192.168.1.128-192.168.1.254\n" +
" local ip = {{if .use_underlay_ip}}{{.netns_ip}}{{else}}{{.xl2tpd_server_ip_address}}{{end}}\n" +
" require chap = yes\n" +
" refuse pap = yes\n" +
" require authentication = yes\n" +
" name = LinuxVPNserver\n" +
" ppp debug = yes\n" +
" pppoptfile = /etc/ppp/options.xl2tpd\n" +
" length bit = yes\n",
"etc/xl2tpd/l2tp-secrets": "* them l2tp-secret",
"etc/ppp/chap-secrets": "{{.chap_user}} * {{.chap_secret}} *",
"etc/ppp/options.xl2tpd": "ipcp-accept-local\n" +
"ipcp-accept-remote\n" +
"noccp\n" +
"auth\n" +
"crtscts\n" +
"idle 1800\n" +
"mtu 1410\n" +
"mru 1410\n" +
"nodefaultroute\n" +
"debug\n" +
"lock\n" +
"proxyarp\n" +
"ms-dns 8.8.8.8\n" +
"ms-dns 8.8.4.4\n",
}
)
// Constants that used by OpenVPN server
const (
openvpnCommand = "/usr/sbin/openvpn"
openvpnConfigFile = "etc/openvpn/openvpn.conf"
openvpnCaCertFile = "etc/openvpn/ca.crt"
openvpnServerCertFile = "etc/openvpn/server.crt"
openvpnServerKeyFile = "etc/openvpn/server.key"
openvpnTLSAuthFile = "etc/openvpn/ta.key"
openvpnDiffieHellmanFile = "etc/openvpn/diffie-hellman.pem"
openvpnExpectedAuthFile = "etc/openvpn_expected_authentication.txt"
openvpnAuthScript = "etc/openvpn_authentication_script.sh"
openvpnLogFile = "var/log/openvpn.log"
openvpnPidFile = "run/openvpn.pid"
openvpnStatusFile = "tmp/openvpn.status"
openvpnUsername = "username"
openvpnPassword = "password"
openvpnServerIPAddress = "10.11.12.1"
)
// dh1024PemKey is the Diffie–Hellman parameter which will be used by OpenVPN
// server in the test. The value is borrowed from Autotest
// (client/common_lib/cros/site_eap_certs.py)
const dh1024PemKey = `-----BEGIN DH PARAMETERS-----
MIGHAoGBAL/YrUzFuA5cPGzhXVqTvDugmPi9CpbWZx2+TCTKxZSjNiVJxcICSnql
uZtkR3sOAiWn384E4ZQTBrYPUguOuFfbMTRooADhezaG9SXtrE9oeVy9avIO7xQK
emZydO0bAsRV+eL0XkjGhSyhKoOvSIXaCbJUn7duEsfkICPRLWCrAgEC
-----END DH PARAMETERS-----
`
// openvpnTLSAuthKey is the TLS auth key used by OpenVPN. This value is
// generated by `openvpn --genkey --secret ta.key`
const openvpnTLSAuthKey = `-----BEGIN OpenVPN Static key V1-----
f2c67579a2e2b621d935b095d53d71d1
5daf6933da213a80b89e2a29cd949823
a281e06a6ffe8fdad456f6378da0e5a0
5ed7cc168d106925c1b0050c971c6419
dd9c6f0e90064e88ca598987326bcb21
c3eb6f1b0b31b35e0793d4ee10363a4c
f635d584c2008915c45b470a11395037
e8483bba005944f9470fd85ec0b26fe1
0ccd55c6880885855d73dd05af643f36
bbd2b85685bee57aefb7fd569df32598
a7bebf0b0c0ab13a1767eab17184f5f8
d543ee0000ae8b8f5d387e51eaebe5e7
837b9546a008c2c5e53c36d7ed7f1aef
076b9fd4da2383bec4f3d082b8c6f77a
15972ca985c52209e4651105fd76d81e
515ed3702ac44d975e0cce5418960ed2
-----END OpenVPN Static key V1-----`
var (
openvpnRootDirectories = []string{"etc/openvpn"}
openvpnConfigs = map[string]string{
openvpnCaCertFile: certificate.TestCert1().CACred.Cert,
openvpnServerCertFile: certificate.TestCert1().ServerCred.Cert,
openvpnServerKeyFile: certificate.TestCert1().ServerCred.PrivateKey,
openvpnDiffieHellmanFile: dh1024PemKey,
openvpnTLSAuthFile: openvpnTLSAuthKey,
openvpnAuthScript: "#!/bin/bash\ndiff -q $1 {{.expected_authentication_file}}\n",
openvpnExpectedAuthFile: "{{.username}}\n{{.password}}\n",
openvpnConfigFile: "ca /{{.ca_cert}}\n" +
"cert /{{.server_cert}}\n" +
"dev tun\n" +
"dh /{{.diffie_hellman_params_file}}\n" +
"keepalive 10 120\n" +
"local {{.netns_ip}}\n" +
"log /{{.log_file}}\n" +
"ifconfig-pool-persist /tmp/ipp.txt\n" +
"key /{{.server_key}}\n" +
"persist-key\n" +
"persist-tun\n" +
"port 1194\n" +
"proto udp\n" +
"server 10.11.12.0 255.255.255.0\n" +
"status /{{.status_file}}\n" +
"tls-auth /{{.tls_auth_file}}\n" +
"verb 5\n" +
"writepid /{{.pid_file}}\n" +
"tmp-dir /tmp\n" +
"{{.optional_user_verification}}\n",
}
)
// Constants that used by WireGuard server. Keys are generated randomly using
// wireguard-tools, only for test usages.
const (
wgClientPrivateKey = "8Ez9VkVl2JL+OhrLZvV2FXsRJTqtBpykhErNef5dzns="
wgClientPublicKey = "dN8f5XplOXpNDP1m9b1V3/AVuOogbw+HckGisfEAphA="
wgClientOverlayIP = "10.12.14.2"
wgClientOverlayIPPrefix = "32"
wgServerPrivateKey = "kKhUZZYELpnWFXZmHKvze5kMJ4UfViHo0aacwx9VSXo="
wgServerPublicKey = "VL4pfwqKV4pWX1xJRmvceOZLTftNKi2PrFoBbJWNKXw="
wgServerOverlayIP = "10.12.14.1"
wgServerAllowedIPs = "10.12.0.0/16"
wgServerListenPort = "12345"
wgSecondServerPrivateKey = "MKLi0UPHP09PwZDH0EPVd2mMTeGi98NDR8dfkzPuQHs="
wgSecondServerPublicKey = "wJXMGS2jhLPy4x75yev7oh92OwjHFcSWio4U/pWLYzg="
wgSecondServerOverlayIP = "192.168.100.1"
wgSecondServerAllowedIPs = "192.168.100.0/24"
wgSecondServerListenPort = "54321"
wgPresharedKey = "LqgZ5/qyT8J8nr25n9IEcUi+vOBkd3sphGn1ClhkHw0="
wgConfigFile = "tmp/wg.conf"
)
var (
wgConfigs = map[string]string{
wgConfigFile: "[Interface]\n" +
"PrivateKey = {{.server_private_key}}\n" +
"ListenPort = {{.server_listen_port}}\n" +
"\n" +
"[Peer]\n" +
"PublicKey = {{.client_public_key}}\n" +
"AllowedIPs = {{.client_ip}}/{{.client_ip_prefix}}\n" +
"{{if .preshared_key}}PresharedKey = {{.preshared_key}}{{end}}",
}
)
// Server represents a VPN server that can be used in the test.
type Server struct {
OverlayIP string
UnderlayIP string
netChroot *chroot.NetworkChroot
stopCommands [][]string
pidFiles []string
logFiles []string
}
// StartL2TPIPsecServer starts a L2TP/IPsec server.
func StartL2TPIPsecServer(ctx context.Context, authType string, ipsecUseXauth, underlayIPIsOverlayIP bool) (*Server, error) {
chro := chroot.NewNetworkChroot()
server := &Server{
netChroot: chro,
stopCommands: [][]string{},
pidFiles: []string{charonPidFile, xl2tpdPidFile, pppdPidFile},
logFiles: []string{charonLogFile},
}
chro.AddRootDirectories(strongSwanDirectories)
chro.AddRootDirectories(l2tpDirectories)
chro.AddConfigTemplates(strongSwanConfigs)
chro.AddConfigTemplates(l2tpConfigs)
configValues := map[string]interface{}{
"chap_user": chapUser,
"chap_secret": chapSecret,
"charon_logfile": charonLogFile,
"xl2tpd_server_ip_address": xl2tpdServerIPAddress,
"use_underlay_ip": underlayIPIsOverlayIP,
}
switch authType {
case AuthTypePSK:
configValues["preshared_key"] = ipsecPresharedKey
case AuthTypeCert:
configValues["server_cert_id"] = ikeServerIdentity
configValues["ca_cert_file"] = caCertFile
default:
return nil, errors.Errorf("L2TP/IPsec type %s is not defined", authType)
}
if ipsecUseXauth {
configValues["xauth_user"] = xauthUser
configValues["xauth_password"] = xauthPassword
}
// For running strongSwan VPN with flag --with-piddir=/run/ipsec. We
// want to use /run/ipsec for strongSwan runtime data dir instead of
// /run, and the cmdline flag applies to both client and server
chro.AddStartupCommand(makeIPsecDir)
chro.AddConfigValues(configValues)
chro.AddStartupCommand(fmt.Sprintf("%s &", charonCommand))
xl2tpdCmdStr := fmt.Sprintf("%s -c /%s -C /tmp/l2tpd.control", xl2tpdCommand, xl2tpdConfigFile)
chro.AddStartupCommand(xl2tpdCmdStr)
underlayIP, err := chro.Startup(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to start L2TP/IPsec server")
}
// After starting charon, execute `swanctl --load-all` to load the
// connection config. The execution may fail until the charon process is
// ready, so we use a Poll here.
if err := testing.Poll(ctx, func(ctx context.Context) error {
return chro.RunChroot(ctx, []string{swanctlCommand, "--load-all"})
}, &testing.PollOptions{Timeout: 5 * time.Second}); err != nil {
return nil, errors.Wrap(err, "failed to load swanctl config")
}
server.UnderlayIP = underlayIP
if underlayIPIsOverlayIP {
server.OverlayIP = underlayIP
} else {
server.OverlayIP = xl2tpdServerIPAddress
}
return server, nil
}
// StartIKEv2Server starts an IKEv2 server.
func StartIKEv2Server(ctx context.Context, authType string) (*Server, error) {
chro := chroot.NewNetworkChroot()
server := &Server{
netChroot: chro,
stopCommands: [][]string{{"/bin/ip", "link", "del", "xfrm1"}},
pidFiles: []string{charonPidFile},
logFiles: []string{charonLogFile},
}
chro.AddRootDirectories(strongSwanDirectories)
chro.AddConfigTemplates(strongSwanConfigs)
configValues := map[string]interface{}{
"chap_user": chapUser,
"chap_secret": chapSecret,
"charon_logfile": charonLogFile,
"client_vip": ikev2ClientIP,
"if_id": ikev2InterfaceID,
}
switch authType {
case AuthTypePSK:
configValues["client_id"] = ikeClientIdentity
configValues["preshared_key"] = ipsecPresharedKey
configValues["server_id"] = ikeServerIdentity
case AuthTypeCert:
configValues["ca_cert_file"] = caCertFile
configValues["server_cert_id"] = ikeServerIdentity
case AuthTypeEAP:
configValues["eap_user"] = xauthUser
configValues["eap_password"] = xauthPassword
configValues["server_cert_id"] = ikeServerIdentity
default:
return nil, errors.Errorf("IKEv2 type %s is not defined", authType)
}
chro.AddConfigValues(configValues)
// For running strongSwan VPN with flag --with-piddir=/run/ipsec. We
// want to use /run/ipsec for strongSwan runtime data dir instead of
// /run, and the cmdline flag applies to both client and server
chro.AddStartupCommand(makeIPsecDir)
chro.AddStartupCommand(fmt.Sprintf("%s &", charonCommand))
chro.AddStartupCommand("ip link add xfrm1 type xfrm dev lo if_id " + ikev2InterfaceID)
chro.AddStartupCommand("ip addr add dev xfrm1 " + ikev2ServerIP + "/24")
chro.AddStartupCommand("ip link set dev xfrm1 up")
underlayIP, err := chro.Startup(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to start IKEv2 VPN server")
}
// After starting charon, execute `swanctl --load-all` to load the
// connection config. The execution may fail until the charon process is
// ready, so we use a Poll here.
if err := testing.Poll(ctx, func(ctx context.Context) error {
return chro.RunChroot(ctx, []string{swanctlCommand, "--load-all"})
}, &testing.PollOptions{Timeout: 5 * time.Second}); err != nil {
return nil, errors.Wrap(err, "failed to load swanctl config")
}
server.UnderlayIP = underlayIP
server.OverlayIP = ikev2ServerIP
return server, nil
}
// StartOpenVPNServer starts an OpenVPN server.
func StartOpenVPNServer(ctx context.Context, useUserPassword bool) (*Server, error) {
chro := chroot.NewNetworkChroot()
server := &Server{
netChroot: chro,
stopCommands: [][]string{},
pidFiles: []string{openvpnPidFile},
logFiles: []string{openvpnLogFile},
}
chro.AddRootDirectories(openvpnRootDirectories)
chro.AddConfigTemplates(openvpnConfigs)
configValues := map[string]interface{}{
"ca_cert": openvpnCaCertFile,
"diffie_hellman_params_file": openvpnDiffieHellmanFile,
"expected_authentication_file": openvpnExpectedAuthFile,
"optional_user_verification": "",
"password": openvpnPassword,
"pid_file": openvpnPidFile,
"server_cert": openvpnServerCertFile,
"server_key": openvpnServerKeyFile,
"status_file": openvpnStatusFile,
"tls_auth_file": openvpnTLSAuthFile,
"username": openvpnUsername,
"log_file": openvpnLogFile,
}
if useUserPassword {
configValues["optional_user_verification"] = fmt.Sprintf("auth-user-pass-verify /%s via-file\nscript-security 2", openvpnAuthScript)
}
chro.AddConfigValues(configValues)
chro.AddStartupCommand("chmod 755 " + openvpnAuthScript)
chro.AddStartupCommand(fmt.Sprintf("%s --config /%s &", openvpnCommand, openvpnConfigFile))
chro.NetEnv = []string{
"OPENSSL_CONF=/etc/ssl/openssl.cnf.compat",
"OPENSSL_CHROMIUM_SKIP_TRUSTED_PURPOSE_CHECK=1",
}
underlayIP, err := chro.Startup(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to start OpenVPN server")
}
server.UnderlayIP = underlayIP
server.OverlayIP = openvpnServerIPAddress
return server, nil
}
// StartWireGuardServer starts a WireGuard server.
func StartWireGuardServer(ctx context.Context, clientPublicKey string, usePSK, isSecondServer bool) (*Server, error) {
chro := chroot.NewNetworkChroot()
server := &Server{
netChroot: chro,
stopCommands: [][]string{{"/bin/ip", "link", "del", "wg1"}},
pidFiles: []string{},
logFiles: []string{}, // No log for WireGuard server.
}
configValues := map[string]interface{}{
"client_public_key": clientPublicKey,
"client_ip": wgClientOverlayIP,
"client_ip_prefix": wgClientOverlayIPPrefix,
}
if usePSK {
configValues["preshared_key"] = wgPresharedKey
}
if isSecondServer {
configValues["server_private_key"] = wgSecondServerPrivateKey
configValues["server_listen_port"] = wgSecondServerListenPort
server.OverlayIP = wgSecondServerOverlayIP
} else {
configValues["server_private_key"] = wgServerPrivateKey
configValues["server_listen_port"] = wgServerListenPort
server.OverlayIP = wgServerOverlayIP
}
chro.AddConfigTemplates(wgConfigs)
chro.AddConfigValues(configValues)
chro.AddStartupCommand("ip link add wg1 type wireguard")
chro.AddStartupCommand("wg setconf wg1 /" + wgConfigFile)
chro.AddStartupCommand("ip addr add dev wg1 " + server.OverlayIP)
chro.AddStartupCommand("ip link set dev wg1 up")
var err error
if server.UnderlayIP, err = chro.Startup(ctx); err != nil {
return nil, errors.Wrap(err, "failed to start WireGuard server")
}
return server, nil
}
// StopServer stop VPN server instance.
func (s *Server) StopServer(ctx context.Context) error {
chro := s.netChroot
for _, cmd := range s.stopCommands {
if err := chro.RunChroot(ctx, cmd); err != nil {
return errors.Wrapf(err, "failed to execute %v", cmd)
}
}
for _, pidFile := range s.pidFiles {
if err := chro.KillPidFile(ctx, pidFile, true); err != nil {
return errors.Wrapf(err, "failed to kill the PID file %v", pidFile)
}
}
return nil
}
func (s *Server) collectLogs(ctx context.Context) error {
var getLogErr error
content, err := s.netChroot.GetLogContents(ctx, s.logFiles)
if err != nil {
getLogErr = errors.Wrap(err, "failed to get vpn log contents")
}
// Write the vpn logs to the file logName.
dir, ok := testing.ContextOutDir(ctx)
if !ok {
return errors.New("failed to get OutDir")
}
if err := ioutil.WriteFile(filepath.Join(dir, logName),
[]byte(content), 0644); err != nil {
return errors.Wrap(err, "failed to write vpnlogs output")
}
return getLogErr
}
// Exit does a best effort to stop the server, log the contents, and shut down the chroot.
func (s *Server) Exit(ctx context.Context) error {
var lastErr error
// We should stop the server before call GetLogContents, since the charon
// process may not flush all the contents before exiting.
if err := s.StopServer(ctx); err != nil {
testing.ContextLog(ctx, "Failed to stop vpn server: ", err)
lastErr = err
}
if err := s.collectLogs(ctx); err != nil {
testing.ContextLog(ctx, "Failed to collect logs: ", err)
lastErr = err
}
if err := s.netChroot.Shutdown(ctx); err != nil {
testing.ContextLog(ctx, "Failed to shutdown the chroot: ", err)
lastErr = err
}
return lastErr
}
// SetupInternetAccess setup internet connectivity for VPN server.
func (s *Server) SetupInternetAccess(ctx context.Context) error {
if err := s.netChroot.RunChroot(ctx, []string{"/sbin/iptables", "-t", "nat", "-A", "POSTROUTING", "!", "-s", s.OverlayIP, "-j", "SNAT", "--to", s.UnderlayIP, "-w"}); err != nil {
return errors.Wrap(err, "failed to setup internet connectivity")
}
return nil
}