Chienomi

localhostのポート競合を気にせずサーバーを起動する

Live With Linux::technique

TCPはそもそもUnixにおいて内部的に使われてきたものである。 何の話かというと、システムでは良く知られているサーバー以外に、普通にPCを使っている上でまぁまぁサーバーが起動したりしているということだ。

私の手元の環境だと

❯ ss -tuna | grep LISTEN
tcp   LISTEN    0      128          0.0.0.0:22            0.0.0.0:*          
tcp   LISTEN    0      4096       127.0.0.1:631           0.0.0.0:*          
tcp   LISTEN    0      50         127.0.0.1:6342          0.0.0.0:*          
tcp   LISTEN    0      50         127.0.0.1:6341          0.0.0.0:*          
tcp   LISTEN    0      4096           [::1]:631              [::]:* 

意外とそうでもなかったが、22, 631, 6341, 6342が使われていることがわかる。

22は言わずと知れたsshdであり、631はcupsd。 63416342はmegasyncである。1

最近はローカルでwebサーバーを起動する機会も多い。 webサーバーを起動するアプリケーションにすれば、簡単にGUIを用意できるのは大きなメリットだ。 だが、webサーバーはだいたい1080とか8000とか8080とか8888を使いたがるので、これらのポートは奪い合いになりやすい。 自分が書いているプログラム単独だと良くても、なんだかんだそのようなポート競合を気にしないといけない状況というのも出てくる。

この問題を解決する方法として、Linuxの伝家の宝刀とも言えるnamespaceを使う方法もあるが、そんなことをしなくても実は簡単に解決できる。

What is localhost?

Linuxとネットワークの基礎知識に関する話をしよう。

localhostは直接解決可能なホスト名だ。/etc/hostsを見ると

# Standard host addresses
127.0.0.1  localhost
::1        localhost ip6-localhost ip6-loopback

のように書かれている。 つまり、localhostはIPv4では127.0.0.1に、IPv6では::1に解決される。

IPアドレスはネットワークとホストの2セクションからなっている。 これはルーティングに絡んでいる。

例えば、192.168.1.0/24とした場合、上位の24ビットがネットワークのアドレス、下位の8ビットがホストのアドレスであるという意味になる。 おっと、そもそもIPv4は32ビットの値を8ビットずつに区切って10進数で表し、.でつないでいるという知識も必要だ。

192.168.1.0/24/24はネットマスク255.255.255.0に等しい。 ネットマスクはビットが1である部分がネットワーク、0である部分がホストを示す。

ネットマスクは途中で01が交じるような場合はネットマスク表記が必要になるが、普通は上位に1が並ぶ構造なので、/Nの形で書ける。

IPアドレスはホストではなく、NICに対して与えられる。 NICのアドレスが192.168.1.10/24であるとしよう。 この場合、宛先が192.168.1.5だったとすると、宛先はそのNICが属しているネットワークに同じく属しているということになる。 そのため、192.168.1.10のNICからパケットを出せば届いてくれるはずだ、ということで、192.168.1.5宛てのパケットは192.168.1.10から出ていく。

自分のNICがどれも所属していないネットワークが宛先になっている場合に配送を依頼するために使われる宛先がデフォルトゲートウェイだ。 なお、ルーティングテーブルを手動で定義すれば、それとは別に「この宛先の場合このNICから出す」というのを指定できる。

さて、ではlocalhostの話に戻ろう。 127.0.0.1::1はループバックアドレスといい、そのコンピュータ自身と通信するために予約されているアドレスになる。 コンピュータにはループバックを使うためのループバックインターフェイスという仮想NICが用意されている。 Linuxではloだ。

❯ ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host noprefixroute 
       valid_lft forever preferred_lft forever
2: eno1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether aa:aa:bb:cc:dd:ee brd ff:ff:ff:ff:ff:ff
    altname enp8s0
    inet 192.168.1.60/24 brd 192.168.1.255 scope global noprefixroute eno1
       valid_lft forever preferred_lft forever
    inet6 fe80::aaaa:bbbb:cccc:dddd/64 scope link noprefixroute 
       valid_lft forever preferred_lft forever

loというNICにはinet(IPv4)として127.0.0.1/8が、inet6(IPv6)として::1/128が割り当てられていることがわかる。

このため、localhostというホスト名を指定すると127.0.0.1に解決され、127.0.0.1loを経由して自分自身と通信する。

だが良く見てほしい。loのネットワークは/8なのだ。 つまり、ループバックアドレスとしては127.0.0.1だけでなく、127.0.0.1から127.255.255.254までの16777214個のアドレスが存在している。

普通のルールに則って考えれば127.0.0.1でも127.255.255.254でも、loを経由して自分自身と通信するため、そんなにたくさんのアドレスが存在する意味は全くない、ように見える。 実際、ループバックアドレスとして127.0.0.1以外が使われることは全くないため、IPv6では::1/128と1個だけになった。

ところが少なくともLinuxではそうではない。 ループバックアドレスは、loにバインドされているアドレスには関係なく、すべてのループバックアドレスが固有のループバックインターフェイスに結びついている。 (127.0.0.1::1は同じNICである。)

そして、ポートはNICに対してバインドされる。0.0.0.0を指定してすべてのNICに対してバインドすることはできるが、そもそもNICごとにポートが存在しているのだ。

もう一度ssの結果を見てみよう。

tcp   LISTEN    0      128          0.0.0.0:22            0.0.0.0:*
tcp   LISTEN    0      4096       127.0.0.1:631           0.0.0.0:*

22ポートは0.0.0.0に対してバインドされているから、すべてのNICからアクセスできる。 しかし、631のほうは127.0.0.1に対してバインドされているため、127.0.0.1から入ってきた場合にしかつながらない。

そろそろ気づいただろうか。 そう、127.0.0.1:8000127.0.0.2:8000は別物としてアクセスできるのだ。

実際に使う

次のスクリプトは、カレントディレクトリをルートとしてwebサーバーを起動するRubyスクリプトだ。

#!/usr/bin/ruby
require 'webrick'

srv = WEBrick::HTTPServer.new({ DocumentRoot: Dir.pwd,
                                BindAddress: 'localhost',
                                Port: 8000 })
trap("INT"){ srv.shutdown }
srv.start

この場合、webサーバーはlocalhost:8000に対してバインドされるわけだが、言い換えれば127.0.0.1:8000に対してバインドされる。そして、http://127.0.0.1:8000でアクセス可能だ。

ではBindAddressを変更してみよう。

#!/usr/bin/ruby
require 'webrick'

srv = WEBrick::HTTPServer.new({ DocumentRoot: Dir.pwd,
                                BindAddress: '127.0.0.2',
                                Port: 8000 })
trap("INT"){ srv.shutdown }
srv.start

今度は127.0.0.2:8000に対してバインドされるようになった。 こうなると、http://127.0.0.1:8000http://localhost:8000でアクセスすることはできず、http://127.0.0.2:8000でのみアクセスできる。

これでも衝突する可能性はゼロではないが、127.0.0.1以外のループバックアドレスを使う例はほとんどないこと、そしてポート番号と比べ膨大な数が存在することから、衝突を避けるのはずっと容易だ。

localhost127.0.0.1あるいは::1に解決されるため、127.0.0.2を表すことはできない。 アドレスを変えたためにアドレスで表記することになるのは嫌、と考えるかもしれない。 その場合は/etc/hostsに書いておくといい。

127.0.0.2  myapp

これでhttp://myapp:8000というアドレスでアクセスできるようになった。 ループバックアドレスは外部ホストからは完全に分離されているため、そのマシン固有のこととして考えて良いため、そのホストの/etc/hostsに書けばよい。

ただ、配布するスクリプトにbind addressとしてmyappと書くことはできない。 他のホストはその名前でループバックインターフェイスが解決できないからだ。

だが、そもそもbind addressに書くアドレスで名前解決させるのは、そもそもあまりよろしくない。

注意点

127.0.0.1127.0.0.2が別のインターフェイスになっているのは、あくまでLinuxの挙動なので、他のOSだとそうではないかもしれない。

なお、仮想NICを増やして同じようなことをすることもできるが、Linux的にはループバックアドレスを使い分けるのが事前設定も不要で圧倒的に簡単だ。

おまけ

PureBuilder Simplyは現状、テストサーバーを127.0.0.1に固定でバインドする仕様だが、手元のブランチではtestserver_addressによって指定できるようになっている。

127.0.0.2を指定できるだけでなく、0.0.0.0を指定して他のホストからアクセスすることもできるようになった。