Skip to content

Commit

Permalink
Pythonify shunt setup methods and integrate into tests (#927)
Browse files Browse the repository at this point in the history
  • Loading branch information
anurag6 authored Sep 29, 2021
1 parent de55ccc commit 556b137
Show file tree
Hide file tree
Showing 9 changed files with 313 additions and 23 deletions.
8 changes: 4 additions & 4 deletions bin/shunt_functions
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ function build_vxlan_ssh_conn {
SSH_IN_PORT=$1
SSH_OUT_PORT=$2
LOCAL_VXLAN_IP=${3:-"192.168.1.2"}
VNI=${4:-0}
REMOTE_IP=${4:-"192.168.21.1"}
LOCAL_IP=${5:-"192.168.21.2"}

Expand All @@ -16,7 +17,7 @@ function build_vxlan_ssh_conn {
sudo ip link set ep0 up

# socat session to move packets from ssh tunnel to UDP port
socat -T15 tcp4-listen:30001,reuseaddr,fork UDP:localhost:4789 &
socat -T15 tcp4-listen:$SSH_IN_PORT,reuseaddr,fork UDP:localhost:4789 &

# iptables command to change source IP from localhost
sudo iptables -t nat -A POSTROUTING -p udp -d 127.0.0.1 --dport 4789 -j SNAT --to-source $REMOTE_IP:38000-45000
Expand All @@ -25,10 +26,10 @@ function build_vxlan_ssh_conn {
sudo iptables -t nat -A OUTPUT -o ep0 -p udp --dport 4789 -j REDIRECT --to-ports 20000

# socat session to move packets from UDP port to ssh session
socat -T15 udp4-recvfrom:20000,reuseaddr,fork tcp:localhost:30000 &
socat -T15 udp4-recvfrom:20000,reuseaddr,fork tcp:localhost:$SSH_OUT_PORT &

# create vxlan tunnel
sudo ip link add vxlan type vxlan id 0 remote $REMOTE_IP dstport 4789 srcport 4789 4789 nolearning
sudo ip link add vxlan type vxlan id $VNI remote $REMOTE_IP dstport 4789 srcport 4789 4789 nolearning
sudo ip addr add $LOCAL_VXLAN_IP/24 dev vxlan
sudo ip link set vxlan up
}
Expand Down Expand Up @@ -57,7 +58,6 @@ function build_ssh_tunnel {
SSH_OUT_PORT=$2
REMOTE_HOST=$3

sleep 5
ssh -o StrictHostKeyChecking=no -L $SSH_OUT_PORT:127.0.0.1:$SSH_IN_PORT -N -f $REMOTE_HOST
ssh -o StrictHostKeyChecking=no -R $SSH_OUT_PORT:127.0.0.1:$SSH_IN_PORT -N -f $REMOTE_HOST
}
3 changes: 3 additions & 0 deletions shunt/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Shunt module"""

pass
94 changes: 94 additions & 0 deletions shunt/shell_command_helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""Module to help run shell commands and retrieve output"""

from __future__ import absolute_import
import subprocess
import multiprocessing


class ShellCommandHelper():
"""Hosts methods to run shell commands and retrieve output"""

TEN_MIN_SEC = 10 * 60

def __init__(self):
pass

def _run_process_command(self, command, capture=False):
"""
Args:
command: Takes an str line or list[str] to be run in shell
capture: Setting to True captures stdout, stderr and return code
returns:
Popen object
"""
command_list = command.split() if isinstance(command, str) else command
pipeout = subprocess.PIPE if capture else None
return subprocess.Popen(command_list, stdout=pipeout, stderr=pipeout)

def _reap_process_command(self, process):
"""
Args:
process: Popen object to reap
returns:
return code of command, stdout, stderr
"""
process.wait(timeout=self.TEN_MIN_SEC)
stdout, stderr = process.communicate()
strout = str(stdout, 'utf-8') if stdout else None
strerr = str(stderr, 'utf-8') if stderr else None
return process.returncode, strout, strerr

# pylint: disable=too-many-arguments
def run_cmd(self, cmd, arglist=None, strict=True,
capture=False, docker_container=None, detach=False):
"""
Args:
cmd: command to be executed
arglist: Additional arguments to add to command
strict: Raises exception if command execution fails
capture: Prints out and captures stdout and stderr output of command
docker_container: Set if command needs to be run within a docker container
returns:
return code of command, stdout, stderr
"""
command = ("docker exec %s " % docker_container) if docker_container else ""
command = command.split() + ([cmd] + arglist) if arglist else command + cmd
if detach:
self._run_process_command(command, capture=capture)
return None, None, None
retcode, out, err = self._reap_process_command(
self._run_process_command(command, capture=capture))
if strict and retcode:
if capture:
print('stdout: \n%s' % out)
print('stderr: \n%s' % err)
raise Exception('Command execution failed: %s' % str(command))
return retcode, out, err

def parallelize(self, target, target_args, batch_size=200):
"""
Parallelizes multiple runs of a target method with multiprocessing.
Args:
target_args: List of tuples which serve as args for target.
List size determines number of jobs
target: Target method
batch_size: Thread pool size
"""
jobs = []
for arg_tuple in target_args:
process = multiprocessing.Process(target=target, args=arg_tuple)
jobs.append(process)

batch_start = 0
while batch_start <= len(jobs):
batch_end = batch_start + batch_size
batch_jobs = jobs[batch_start:batch_end]

for job in batch_jobs:
job.start()

for job in batch_jobs:
job.join()
job.close()

batch_start = batch_end
145 changes: 145 additions & 0 deletions shunt/vxlan_over_ssh.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
"""Module to build VxLAN tunnels over SSH tunnels using shell commands"""

from __future__ import absolute_import
from shunt.shell_command_helper import ShellCommandHelper


def build_ssh_tunnel(ssh_in_port, ssh_out_port, remote_host, reverse=False):
"""
Args:
ssh_in_port: Destination port for tunnel
ssh_out_port: Source port for tunnel
remote_host: Remote host IP
reverse: Boolean. Creates reverse forwarding tunnel if True.
Returns: None
"""
shellcmd = ShellCommandHelper()
direction = 'R' if reverse else 'L'
shellcmd.run_cmd("ssh -o StrictHostKeyChecking=no -%s %s:127.0.0.1:%s -N -f %s"
% (direction, ssh_out_port, ssh_in_port, remote_host))


def build_bidirectional_ssh_tunnel(ssh_in_port, ssh_out_port, remote_host):
"""
Args:
ssh_in_port: Incoming traffice port
ssh_out_port: Outgoing traffic port
remote_host: Remote host IP
Returns: None
"""
build_ssh_tunnel(ssh_in_port, ssh_out_port, remote_host, reverse=False)
build_ssh_tunnel(ssh_in_port, ssh_out_port, remote_host, reverse=True)


def create_virtual_if(if_name, if_ip):
"""
Args:
if_name: interface name
if_ip: interface IP
Returns: None
"""
shellcmd = ShellCommandHelper()
shellcmd.run_cmd("sudo ip link add %s type dummy" % (if_name))
shellcmd.run_cmd("sudo ip addr add %s/24 dev %s" % (if_ip, if_name))
shellcmd.run_cmd("sudo ip link set %s up" % (if_name))


def socat_tcp_to_udp(src_port, dst_port):
"""
Socat command to map TCP port to UDP port
Args:
src_port: Source TCP port
dst_port: Destination UDP port
Returns: None
"""
shellcmd = ShellCommandHelper()
shellcmd.run_cmd("socat -T15 tcp4-listen:%s,reuseaddr,fork udp:localhost:%s"
% (src_port, dst_port), detach=True)


def socat_udp_to_tcp(src_port, dst_port):
"""
Socat command to map UDP port to TCP port
Args:
src_port: Source UDP port
dst_port: Destincation TCP port
Returns: None
"""
shellcmd = ShellCommandHelper()
shellcmd.run_cmd("socat -T15 udp4-listen:%s,reuseaddr,fork tcp:localhost:%s"
% (src_port, dst_port), detach=True)


def iptables_udp_change_source(dst_ip, dst_port, src_ip_port):
"""
iptables command to change source IP/ports of UDP packets
Args:
dst_ip: Destination IP of packet to be changed
dst_port: Destination port of packet to be changed
src_ip_port: <source_ip_to_be_added>:<source port range> e.g. 192.168.1.2:38000-45000
Returns: None
"""
shellcmd = ShellCommandHelper()
shellcmd.run_cmd(
"sudo iptables -t nat -A POSTROUTING -p udp -d %s --dport %s -j SNAT --to-source %s"
% (dst_ip, dst_port, src_ip_port))


def iptables_udp_divert_iface_traffic(iface, dst_port, target_dst_port):
"""
iptables command to divert udp traffic egressing from an interface to a different port
Args:
iface: Interface packets are egressign from
dst_port: Destination port of packets to be diverted
target_dst_port: Destination port packets are diverted to
Returns: None
"""
shellcmd = ShellCommandHelper()
shellcmd.run_cmd(
"sudo iptables -t nat -A OUTPUT -o %s -p udp --dport %s -j REDIRECT --to-ports %s"
% (iface, dst_port, target_dst_port))


def create_vxlan_tunnel(vni, remote_ip, vxlan_port, vtep_ip):
"""
Method to create a VxLAN tunnel and assign an IP to the VTEP
Args:
vni: VNI of tunnel to be created
remote_ip: Remote IP address for underlay network
vxlan_port: Remote destination port for VxLAN tunnel
Returns: None
"""
shellcmd = ShellCommandHelper()
shellcmd.run_cmd(
"sudo ip link add vxlan type vxlan id %s remote %s dstport %s srcport %s %s nolearning"
% (vni, remote_ip, vxlan_port, vxlan_port, vxlan_port))
shellcmd.run_cmd("sudo ip addr add %s/24 dev vxlan" % (vtep_ip))
shellcmd.run_cmd("sudo ip link set vxlan up")


def build_vxlan_ssh_conn(conn_params):
"""
Args:
conn_params: JSON. Format:{ # TODO: Turn into a proto for easy config exchange.
virt_if_name: Interface name of virtual interface for underlay n/w
virt_if_ip: IP for virtual interface
ssh_in_port: Port for incoming traffic
ssh_out_port: Port for outgoing traffic
remote_ip: Remote IP of underlay n/w
vni: VxLAN identifier
vxlan_if_ip: VxLAN interface IP
}
Returns: None
"""
VXLAN_PORT = '4789'
LOCAL_HOST = '127.0.0.1'
VXLAN_SOURCE_PORT_RANGE = "38000-45000"
INTERMEDIATE_PORT = "20000"
create_virtual_if(conn_params['virt_if_name'], conn_params['virt_if_ip'])
socat_tcp_to_udp(conn_params['ssh_in_port'], VXLAN_PORT)
source_ip_port = conn_params['remote_ip'] + ":" + VXLAN_SOURCE_PORT_RANGE
iptables_udp_change_source(LOCAL_HOST, VXLAN_PORT, source_ip_port)
iptables_udp_divert_iface_traffic(conn_params['virt_if_name'], VXLAN_PORT, INTERMEDIATE_PORT)
socat_udp_to_tcp(INTERMEDIATE_PORT, conn_params['ssh_out_port'])
create_vxlan_tunnel(
conn_params['vni'], conn_params['remote_ip'], VXLAN_PORT, conn_params['vxlan_if_ip'])
9 changes: 8 additions & 1 deletion testing/shunt/Dockerfile.shunthost
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,20 @@

FROM daqf/aardvark

RUN $AG update && $AG install openssh-server socat sudo iptables
RUN $AG update && $AG install openssh-server socat sudo iptables python3-dev

COPY ./testing/shunt/id_rsa .ssh/
COPY ./testing/shunt/id_rsa.pub .ssh/
COPY ./testing/shunt/authorized_keys .ssh/
COPY ./testing/shunt/start_shunt_host ./

RUN mkdir -p shunt
RUN mkdir -p testing
RUN mkdir -p testing/shunt

COPY ./shunt shunt
COPY ./testing/shunt testing/shunt

RUN chmod -R 700 .ssh

RUN service ssh start
Expand Down
41 changes: 41 additions & 0 deletions testing/shunt/build_vxlan_ssh_conn.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""Script to build vxlan over ssh for testing across docker hosts"""


from __future__ import absolute_import
import sys
from shunt.vxlan_over_ssh import build_vxlan_ssh_conn, build_bidirectional_ssh_tunnel

CONN_PARAMS = {
'virt_if_name': 'ep0',
'ssh_in_port': '30001',
'ssh_out_port': '30000',
'vni': '0'
}

CONN_PARAMS_CLIENT = {
'virt_if_ip': '192.168.21.2',
'remote_ip': '192.168.21.1',
'vxlan_if_ip': '192.168.1.2'
}
CONN_PARAMS_CLIENT.update(CONN_PARAMS)

CONN_PARAMS_SERVER = {
'virt_if_ip': '192.168.21.1',
'remote_ip': '192.168.21.2',
'vxlan_if_ip': '192.168.1.1'
}
CONN_PARAMS_SERVER.update(CONN_PARAMS)


def main():
"""Build vxlan over ssh for client/server"""
if sys.argv[1] == 'client':
build_bidirectional_ssh_tunnel(
CONN_PARAMS['ssh_in_port'], CONN_PARAMS['ssh_out_port'], 'shunt_host_server_1')
build_vxlan_ssh_conn(CONN_PARAMS_CLIENT)
elif sys.argv[1] == 'server':
build_vxlan_ssh_conn(CONN_PARAMS_SERVER)


if __name__ == "__main__":
main()
12 changes: 8 additions & 4 deletions testing/shunt/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
version: '2'

services:
host1:
host_client:
build:
context: ../..
dockerfile: ./testing/shunt/Dockerfile.shunthost
command: bash -c "./start_shunt_host 1"
depends_on:
- "host_server"
command: bash -c "./start_shunt_host client"
#command: bash -c "tail -f"
cap_add:
- ALL
host2:
host_server:
build:
context: ../..
dockerfile: ./testing/shunt/Dockerfile.shunthost
command: bash -c "./start_shunt_host 2"
command: bash -c "./start_shunt_host server"
#command: bash -c "tail -f"
cap_add:
- ALL
12 changes: 4 additions & 8 deletions testing/shunt/start_shunt_host
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,9 @@

host=$1
service ssh start
source bin/shunt_functions

if [ $host -eq 1 ]; then
build_ssh_tunnel 30001 30000 shunt_host2_1
build_vxlan_ssh_conn 30001 30000 192.168.1.1
else
build_vxlan_ssh_conn 30001 30000 192.168.1.2
fi

export PYTHONPATH=$PYTHONPATH:/root/shunt

python3 testing/shunt/build_vxlan_ssh_conn.py $host

tail -f /dev/null
Loading

0 comments on commit 556b137

Please sign in to comment.