Simplifying port forwarding with LXD

by Christian Lønaas, 2016-09-03

LXD containers are brilliant, but lacks an easy way to forward ports from the containers to the host. One can use iptables manually, of course, but I really missed something easy like Docker. To try and remedy this, I have conjured up a little bash script. With this script, you can add, delete and list port forwarding rules.

It's a bit rough around the edges, but maybe I'll tidy it up a bit some day.

Read on for the script and examples.

Adding a rule

Let's say you have an SMTP server running in a container named 'mailbox'. To forward port 25 on the container to port 25 on the host, just do this: $ lxd-forward add mailbox 25 Another example, what if you have a web server running in the container 'tomcat', on port 8080, and want it forwarded to port 80 on the host? $ lxd-forward add tomcat 8080 80

Listing rules

To list rules, simply type: $ lxd-forward list Sample output:

Rule #  LXD     IP              Host    Container
2       kolab   172.29.46.196   143     143

Deleting a rule

To delete a rule, get the rule number from the 'list' command, e.g. 2, and do: $ lxd-forward delete 2

The script

Here's the whole thing. Keep in mind that some things are hard coded, like the ethernet device, so some modification may be required for it to run in another environment.

#!/usr/bin/env bash
cmd=$1
container_name=$2
container_port=$3
host_port=$4

function usage() {
  echo "Usage: $(basename ${0}) [add|list|delete|help] [container] [port] [host port]"
  exit 1
}

function error() {
  echo "$1"
  exit 1
}

test -n "$cmd" || usage

function list() {
  printf "Rule #\tCont\tIP\t\tHost port\tContainer port\n"
  containers=$(lxc list | grep RUNNING | awk '{print $2, $6}')
  echo "${containers}" | while read line; do
    c_name=$(echo $line | awk '{print $1}')
    c_ip=$(echo $line | awk '{print $2}')
    forwarded_ports=$(iptables -t nat --line-numbers -L --numeric | grep $c_ip)
    echo $forwarded_ports | while read ports_line; do
      test -z "${rule_no}" || break
      rule_no=$(echo $ports_line | awk '{print $1}')
      host_port=$(echo $ports_line | awk '{print $8}')
      host_port=$(echo $host_port | awk -F\: '{print $2}')
      container=$(echo $ports_line | awk '{print $9}')
      container=$(echo $container | awk -F\: '{print $3}')
      if [ "${rule_no}" != "" ]; then
        printf "${rule_no}\t${c_name}\t${c_ip}\t${host_port}\t${container}\n"
      fi
    done
  done
}

function forward() {
  c_name=$1
  c_port=$2
  h_port=$3
  container_info=$(lxc list $c_name | grep RUNNING | awk '{print $2, $6}' | grep $c_name)
  ip=$(echo -n $container_info | awk '{printf("%s",$2)}')
  test -n "$ip" || error "Container not found or not assigned an IP address"
  test -n "$c_port" || error "Container port not specified"
  test -n "$h_port" || h_port=$c_port

  iptables -t nat -A PREROUTING -p tcp -i eth0 --dport "$h_port" -j DNAT --to-destination "${ip}:${c_port}"
}

function delete() {
  rule_no=$1
  test -n "$rule_no" || error "Must specify rule no."
  iptables -t nat -D PREROUTING "${rule_no}"
}

case "$cmd" in
  "list") list
  ;;
  "add") forward $container_name $container_port $host_port
  ;;
  "delete") delete $2 # Rule no.
  ;;
  *) usage
  ;;
esac