2017/05/25(Thu)EdgeRouter X(ER-X)で802.1q VLAN + Hairpin NATを実現する方法

2017/05/24 3:21 Hardware::NetworkSwitch
EdgeRouter X (ER-X)というUbiquiti Networkが販売するEdgeRouterシリーズのルータでの設定方法例についてまとめる。このエントリは、802.1q VLANとHairpin NATを組み合わせて動作させる方法について。

諸注意

config内に日本語で補足を入れているが、文字化けの原因になる可能性があるので注意。コメントは入れるなら英語で入れた方が良いかも。(自分はそうしている)
また下記のconfigは、実環境を参考に、説明用の設定値に変更しながら手打ちしたものなので、syntax error等があるかもしれない。その場合は適宜修正して、できれば本記事にコメントを頂けるとありがたい。

何故Hairpin NATが必要か

一つのISPを家庭内LANと自宅サーバで共有するネットワークを構築し、かつ、ルータとサーバが同一ではない場合、家庭内LANから自宅サーバに設定したドメインでアクセスすると、ルータの管理画面やsshにアクセスしてしまうという問題が起こる。
これは、LAN内のPCからの通信がルータを経由する際、宛先のグローバルアドレスがルータ自身のアドレスであるため、ルータが応答を返してしまうからである。このようなパケットも、インターネット側からアクセスされたものと想定して、NAT変換等を自動的に行う仕様のルータ(Hairpin NAT, NAT Loopback, NAT Reflection対応ルータなどと呼ばれる)もあるが、殆どの家庭用ルータは非対応で、EdgeRouterもデフォルトはオフとなっている。

簡易設定は簡単だが…

EdgeRouterは結構作り込まれているので、Web管理画面からチェックを1つ入れるだけでHairpin NATを有効にすることが出来るのだが、
er-x-hairpin-nat.png

実はこれには罠があり、ethポート(ルータの物理ポート)をuntaggedで利用している場合向けの機能となっているようだ。また、この機能はport-forwardというNAT設定を簡略的に行う設定項目となってしまうため、firewallが自動設定に纏められたりして、細かな制御や確認がし辛いという難点がある。
port-forward {
    auto-firewall enable
    hairpin-nat enable
    lan-interface switch0
    rule 1 {
        description myserver-http
        forward-to {
            address 192.168.11.10
            port 80
        }
        original-port 80
        protocol tcp
    }
    wan-interface eth1
}

natルールを使って記述する方法

nat設定方法で指定する場合、グローバルアドレスが固定IPでなければconfigが書けないという制約があるものの、EdgeRouter - NAT Hairpin (Nat Inside-to-Inside / Loopback / Reflection)にも紹介されている方法でよいはず(untagged環境では未検証)。以下のrule 20rule 6020を参照してほしい。

この設定で、以下のようなiptablesコマンドと同様の設定がされる。
hairpin-nat.png

nat {
    /* inbound-interfaceがpppoe0(on eth1)向けのNAT設定 */
    rule 10 {
        description myserver-http
        destination {
            port 80
        }
        inbound-interface pppoe0
        inside-address {
            address 192.168.11.10
            port 80
        }
        protocol tcp
        type destination
    }
    
    /* inbound-interfaceがeth0向けのNAT設定 */
    rule 20 {
        description myserver-http-hairpin
        destination {
            address 192.51.100.123
            port 80
        }
        inbound-interface eth0
        inside-address {
            address 192.168.11.10
            port 80
        }
        protocol tcp
        type destination
    }
    
    /* DNATはrule 1~4999、SNAT/Masqueradeはrule 5000~9999と決まっていることに注意 */
    
    /* Internetの通常NAT設定 */
    rule 6000 {
        description internet-share
        outbound-interface pppoe0
        protocol all
        source {
            address 192.168.11.0/24
        }
        type masquerade
    }
    
    /* Hairpin NAT設定 */
    rule 6020 {
        description myserver-hairpin-nat
        destination {
            address 192.168.11.10
        }
        outbound-interface eth0
        source {
            address 192.168.11.0/24
        }
        type masquerade
    }
}


802.1q VLAN上でHairpin NATを行う

802.1q VLANを使う設定をしている場合は、おそらくこんな感じでbridgeを定義しているはず。
(2021/07/25 追記) brdigeを使ってVLANを組むのは推奨されていないっぽいので、次項を参照して下さい。
interfaces {
    bridge {
        br10 {
            address 192.168.11.1/24
            description myhome-lan
        }
        br20 {
            description onu-zone
        }
        :
    }
    ethernet {
        /* eth0: br10をuntagged */
        eth0 {
            bridge-group bridge br10
        }
        /* eth1: br20をuntagged */
        eth1 {
            bridge-group bridge br20
        }
        /* eth2: 以下はbr10をVLAN ID=10で、br20をVLAN ID=20でtaggedする */
        eth2 {
            vif 10 {
                bridge-group br10
            }
            vif 20 {
                bridge-group br20
            }
        }
        :
    }
}
Hairpin NATを行う設定でinbound-interface eth0やoutbound-interface eth0の部分をbr10に直せばよいのかというと、それだけでは動かない。
しばらく頭を悩ませたが、パケットキャプチャなどをしているうちに*1、パケットがbridgeに行った後に破棄されている感じであることが分かった。

で、そんな折に見つけたのがコレ。bridgeをpromiscuous modeにすると動くという解決方法。
Port Forwarding working externally / Hairpin doesn't work when accessing via LAN (EdgeOS v1.4.0)

VLAN環境下では、eth側の処理とbridge側の処理をそれぞれ経由するが、bridge内のsoftware処理では「パケットの宛先が(bridgeの192.168.11.0/24でない)グローバルアドレスが付いているのでdrop」と動作しているようだ。なので、br10をpromiscuousにしてやれば、この動作を防ぐ事が出来、Hairpin NATを有効に出来る。

設定としてはこうなる。(上記で書いていたrule 10, 6000は省略)
natのinterface名をbr10に変更し、set bridge br10 promiscuous enableを追加してやればよい。
nat {
    :
    /* inbound-interfaceがeth0向けのNAT設定 */
    rule 20 {
        description myserver-http-hairpin
        destination {
            address 192.51.100.123
            port 80
        }
        inbound-interface br10
        inside-address {
            address 192.168.11.10
            port 80
        }
        protocol tcp
        type destination
    }

    :
    /* Hairpin NAT設定 */
    rule 6020 {
        description myserver-hairpin-nat
        destination {
            address 192.168.11.10
        }
        outbound-interface br10
        source {
            address 192.168.11.0/24
        }
        type masquerade
    }
}

:

interfaces {
    bridge {
        br10 {
            address 192.168.11.1/24
            description myhome-lan
            promiscuous enable
        }
        br20 {
            description onu-zone
        }
        :
    }
    ethernet {
        /* eth0: br10をuntagged */
        eth0 {
            bridge-group bridge br10
        }
        /* eth1: br20をuntagged */
        eth1 {
            bridge-group bridge br20
        }
        /* eth2: 以下はbr10をVLAN ID=10で、br20をVLAN ID=20でtaggedする */
        eth2 {
            vif 10 {
                bridge-group br10
            }
            vif 20 {
                bridge-group br20
            }
        }
        :
    }
}

*1 : 中身はLinux箱なので/sbin/tcpdumpが使える!超便利!!

802.1q VLAN上でHairpin NATを行う(改良版)

前項では、bridgeを使ってVLANを作っていたが、本来はswitch-portを使って定義するものらしい。
(公式のドキュメントをよく読んでいなかったので、switch-portはnon VLAN専用だと思い込んでいた)

まず最初に、set interfaces switch switch0 switch-port vlan-aware enableしておけば、switch-portがVLANで分離可能になり、bridge同様、「物理ポート定義(interfaces ethernet ethX)と切り離してVLANを管理できる」
よって、最初はこんな感じで2つのVLANが切ってあるはず。(pvidはUntagged port, vidはTagged port)
interfaces {
    switch switch0 {
        switch-port {
            /* eth0: VLAN 10をuntagged */
            interface eth0 {
                vlan {
                    pvid 10
                }
            }
            /* eth1: VLAN 20をuntagged */
            interface eth1 {
                vlan {
                    pvid 20
                }
            }
            /* eth2: VLAN 10と20をtaggedで、untaggedは設定なし */
            interface eth2 {
                vlan {
                    vid 10
                    vid 20
                }
            }
            :
            vlan-aware enable
        }
        vif 10 {
            address 192.168.11.1/24
            description myhome-lan
        }
        vif 20 {
            description onu-zone
        }
        :
    }
    ethernet {
        /* こちら側にVLANの設定は一切無し */
        eth0 {
            duplex auto
            speed auto
        }
        eth1 {
            duplex auto
            speed auto
        }
        eth2 {
            duplex auto
            speed auto
        }
        :
    }
}
bridgeではHairpin NATの設定を行っても、プロミスキャスにしないと動作しない*2が、以下のように同様のNAT設定を入れるだけでswitchの場合はうまく動作した。(本来こちらが期待する動作)
また前項ではNATルールにポート番号も指定していたが、基本的にグローバルアドレス向けの通信は全部Hairpinに吸い込んで良いはずなので、指定を外した。(protocolもallで良いかもしれないが、とりあえずtcpだけで事足りる)
nat {
    :
    /* inbound-interfaceがVLAN 10(eth0)向けのNAT設定 */
    rule 20 {
        description myserver-http-hairpin
        destination {
            address 192.51.100.123
        }
        inbound-interface switch0.10
        inside-address {
            address 192.168.11.10
        }
        protocol tcp
        type destination
    }

    :
    /* Hairpin NAT設定 */
    rule 6020 {
        description myserver-hairpin-nat
        destination {
            address 192.168.11.10
        }
        outbound-interface switch0.10
        source {
            address 192.168.11.0/24
        }
        type masquerade
    }
}

:

interfaces {
    switch switch0 {
        switch-port {
            /* eth0: VLAN 10をuntagged */
            interface eth0 {
                vlan {
                    pvid 10
                }
            }
            /* eth1: VLAN 20をuntagged */
            interface eth1 {
                vlan {
                    pvid 20
                }
            }
            /* eth2: VLAN 10と20をtaggedで、untaggedは設定なし */
            interface eth2 {
                vlan {
                    vid 10
                    vid 20
                }
            }
            :
            vlan-aware enable
        }
        vif 10 {
            address 192.168.11.1/24
            description myhome-lan
        }
        vif 20 {
            description onu-zone
        }
        :
    }
    ethernet {
        /* こちら側にVLANの設定は一切無し */
        eth0 {
            duplex auto
            speed auto
        }
        eth1 {
            duplex auto
            speed auto
        }
        eth2 {
            duplex auto
            speed auto
        }
        :
    }
}
こうすることで、VLAN 10配下のPC(192.168.11.20/24)が198.51.100.123:80にアクセスすると、ルータが192.168.11.1をSrcIPに置き換えて、LAN内のサーバ192.168.11.10にアクセスし、サーバはルータに応答を返却(SrcIP: 192.168.11.0, DstIP: 192.168.11.1)、Masquerade設定により戻りパケットのSrcIPが198.51.100.123に、DstIPが192.168.11.20に変更され通信が完結する。

クライアント
SrcIP: 192.168.11.20 (自分自身)
DstIP: 198.51.100.123 (自宅内サーバのグローバルIPアドレス)

ルータ(以下のように置換)
SrcIP: 192.168.11.1 (ルータがサーバに対して直接リーチできるアドレスに変換)
DstIP: 192.168.11.10

サーバの応答
SrcIP: 192.168.11.10
DstIP: 192.168.11.1

ルータ(以下のように置換)
SrcIP: 198.51.100.123
DstIP: 192.168.11.20

というわけで、サーバから見たアクセス元が全てルータになる点は仕様。

ちなみに、VLANが複数ある場合、NATのrule 20とrule 6020のセットをVLANの数だけ増やせば対応可能。inbound-interfaceおよびoutband-interfaceを該当のVLAN用のswitch0.xに設定し、そのVLANで扱っているローカルアドレスにinside-address addressおよびsource addressを置き換えればうまくいく。

*2 : L2のbridging用だから、関与せず?