KVM環境のNATでポートフォワード

はじめに

以前の記事で書いたlibvirtのフックを利用して、NATでポートフォワードする方法を示す。 古いブログなどではiptables-save/iptables-restoreを利用してポートフォワードの設定を永続化している例もあるが、フックを使う方式が正しいと思われる。

サンプルのポートフォワードのフック

まずは、libvirtのドキュメントにサンプル掲載されている、ポートフォワードのフックを示す。 サンプルなので仕方がないが、このままだとポートフォワードの設定を変更するたびにスクリプトの変更が必要になる。

#!/bin/bash

# IMPORTANT: Change the "VM NAME" string to match your actual VM Name.
# In order to create rules to other VMs, just duplicate the below block and configure
# it accordingly.
if [ "${1}" = "VM NAME" ]; then

   # Update the following variables to fit your setup
   GUEST_IP=
   GUEST_PORT=
   HOST_PORT=

   if [ "${2}" = "stopped" ] || [ "${2}" = "reconnect" ]; then
    /sbin/iptables -D FORWARD -o virbr0 -d  $GUEST_IP -j ACCEPT
    /sbin/iptables -t nat -D PREROUTING -p tcp --dport $HOST_PORT -j DNAT --to $GUEST_IP:$GUEST_PORT
   fi
   if [ "${2}" = "start" ] || [ "${2}" = "reconnect" ]; then
    /sbin/iptables -I FORWARD -o virbr0 -d  $GUEST_IP -j ACCEPT
    /sbin/iptables -t nat -I PREROUTING -p tcp --dport $HOST_PORT -j DNAT --to $GUEST_IP:$GUEST_PORT
   fi
fi

Pythonでリライトしたポートフォワードのフック

前出のサンプルの欠点を埋めるために、ポートフォワードの設定部分を以下のようなYAMLで外だしするようにした。

"web-server":
  - bridge: virbr0
    vm_ip: 192.168.8.4
    forwardings:
      - host_port: 10022
        vm_port: 22
      - host_port: 10080
        vm_port: 80

"other-vm":
  - bridge: virbr0
    vm_ip: 192.168.8.8
    forwardings:
      - host_port: 20022
        vm_port: 22

さすがにbashYAMLはキビシイので、サンプルスクリプトPythonでリライトした。 以下のスクリプト/etc/libvert/hooks/qemuという名前で作成し、実行権を与えておく。 そして、このスクリプトと同じディレクトリに、上記のようなYAMLport-forwarding.ymlというファイル名で配置しておくと、これを読み込んで適切にiptablesを実行する。

#!/usr/bin/python3

import os
import os.path
import sys
import subprocess
import yaml


def run_command(command):
    print(command, file=sys.stderr)
    subprocess.run(command)


def entries(entries):
    for entry in entries:
        yield (entry, entry["bridge"], entry["vm_ip"])


def forwardings(entry):
    vm_ip = entry["vm_ip"]
    for forwarding in entry["forwardings"]:
        yield (str(forwarding["host_port"]), vm_ip + ":" + str(forwarding["vm_port"]))


def on_libvirt_hook(config, vm_name, operation, sub_operation, extra_argument):
    if vm_name in config:
        for entry, bridge, vm_ip in entries(config[vm_name]):
            if operation in ["stopped", "reconnect"]:
                run_command(["/sbin/iptables", "-D", "FORWARD" ,"-o", bridge, "-d", vm_ip,  "-j", "ACCEPT"])
                for host_port, vm_ip_and_port in forwardings(entry):
                    run_command(["/sbin/iptables", "-t", "nat", "-D", "PREROUTING", "-p", "tcp", "--dport", host_port, "-j", "DNAT", "--to", vm_ip_and_port])

            if operation in ["start", "reconnect"]:
                run_command(["/sbin/iptables", "-I", "FORWARD" ,"-o", bridge, "-d", vm_ip,  "-j", "ACCEPT"])
                for host_port, vm_ip_and_port in forwardings(entry):
                    run_command(["/sbin/iptables", "-t", "nat", "-I", "PREROUTING", "-p", "tcp", "--dport", host_port, "-j", "DNAT", "--to", vm_ip_and_port])


if __name__ == '__main__':
    config_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), "port-forwarding.yml")
    with open(config_file) as f:
        config = yaml.load(f)

    on_libvirt_hook(config, sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4])

動作確認

スクリプト/etc/libvert/hooks/qemuコマンドラインから直接実行して、iptablesの変化を見てみる。 まずは、スクリプト実行前のiptablesの状態を以下に示す。

$ sudo iptables -L
Chain INPUT (policy ACCEPT)
target     prot opt source               destination         
ACCEPT     udp  --  anywhere             anywhere             udp dpt:domain
ACCEPT     tcp  --  anywhere             anywhere             tcp dpt:domain
ACCEPT     udp  --  anywhere             anywhere             udp dpt:bootps
ACCEPT     tcp  --  anywhere             anywhere             tcp dpt:bootps

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination         
ACCEPT     all  --  anywhere             192.168.8.0/24       ctstate RELATED,ESTABLISHED
ACCEPT     all  --  192.168.8.0/24       anywhere            
ACCEPT     all  --  anywhere             anywhere            
REJECT     all  --  anywhere             anywhere             reject-with icmp-port-unreachable
REJECT     all  --  anywhere             anywhere             reject-with icmp-port-unreachable

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination         
ACCEPT     udp  --  anywhere             anywhere             udp dpt:bootpc

$ sudo iptables -L -t nat
Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination         

Chain INPUT (policy ACCEPT)
target     prot opt source               destination         

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination         

Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination         
RETURN     all  --  192.168.8.0/24       base-address.mcast.net/24 
RETURN     all  --  192.168.8.0/24       255.255.255.255     
MASQUERADE  tcp  --  192.168.8.0/24      !192.168.8.0/24       masq ports: 1024-65535
MASQUERADE  udp  --  192.168.8.0/24      !192.168.8.0/24       masq ports: 1024-65535
MASQUERADE  all  --  192.168.8.0/24      !192.168.8.0/24      

そして、以下のようにweb-serverというVMを起動した想定で、スクリプトを実行する。

$ sudo /etc/libvirt/hooks/qemu web-server start - -
['/sbin/iptables', '-I', 'FORWARD', '-o', 'virbr0', '-d', '192.168.8.4', '-j', 'ACCEPT']
['/sbin/iptables', '-t', 'nat', '-I', 'PREROUTING', '-p', 'tcp', '--dport', '10022', '-j', 'DNAT', '--to', '192.168.8.4:22']
['/sbin/iptables', '-t', 'nat', '-I', 'PREROUTING', '-p', 'tcp', '--dport', '10080', '-j', 'DNAT', '--to', '192.168.8.4:80']

スクリプト実行後のiptablesの状態は以下のようになり、適切にエントリが追加されていることが分かる。

$ sudo iptables -L
Chain INPUT (policy ACCEPT)
target     prot opt source               destination         
ACCEPT     udp  --  anywhere             anywhere             udp dpt:domain
ACCEPT     tcp  --  anywhere             anywhere             tcp dpt:domain
ACCEPT     udp  --  anywhere             anywhere             udp dpt:bootps
ACCEPT     tcp  --  anywhere             anywhere             tcp dpt:bootps

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination         
ACCEPT     all  --  anywhere             192.168.8.4  ←●追加された
ACCEPT     all  --  anywhere             192.168.8.0/24       ctstate RELATED,ESTABLISHED
ACCEPT     all  --  192.168.8.0/24       anywhere            
ACCEPT     all  --  anywhere             anywhere            
REJECT     all  --  anywhere             anywhere             reject-with icmp-port-unreachable
REJECT     all  --  anywhere             anywhere             reject-with icmp-port-unreachable

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination         
ACCEPT     udp  --  anywhere             anywhere             udp dpt:bootpc

$ sudo iptables -L -t nat
Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination         
DNAT       tcp  --  anywhere             anywhere             tcp dpt:amanda to:192.168.8.4:80  ←●追加された
DNAT       tcp  --  anywhere             anywhere             tcp dpt:10022 to:192.168.8.4:22  ←●追加された

Chain INPUT (policy ACCEPT)
target     prot opt source               destination         

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination         

Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination         
RETURN     all  --  192.168.8.0/24       base-address.mcast.net/24 
RETURN     all  --  192.168.8.0/24       255.255.255.255     
MASQUERADE  tcp  --  192.168.8.0/24      !192.168.8.0/24       masq ports: 1024-65535
MASQUERADE  udp  --  192.168.8.0/24      !192.168.8.0/24       masq ports: 1024-65535
MASQUERADE  all  --  192.168.8.0/24      !192.168.8.0/24      

参考