GoのHTTPサーバーを80番や443番ポートでListenする方法を調べた

1024以下の番号のポートでサーバーをListenするには、rootで実行する必要がある。それはもちろん嫌なので、GoのWebサーバーを80番ポートでサービスするためにどういう方法があるのか調べた。

root権限で起動してListenしてからSetuidで権限降格

最初はroot権限で起動して、80番ポートをListenしてからsetuidで権限降格するやりかた。 Node.jsでもそんな感じだったし、まあそういう感じだろうと当たりをつけて調べたら、どうもLinuxではうまくいかないらしい。

コメント欄でのid:methaneさんの指摘によると、Linuxのsetuidシステムコールは、実行したスレッドにしか効力が無い。Goは自動的に複数スレッドに分散されるので、権限降格されないまま実行されるGoroutineが出てくる事になり、望ましくない。さらにGo 1.4ではLinuxでのSetuid/Setgidが禁止されるらしい。Oh...

という訳で他の方法を探す。

rootで起動した起動スクリプトからポートのfdをもらってExec

Go で 1024 以下のポートを Listen するアプリを作る - methaneのブログ

id:methaneさんの実装。外部スクリプトで80番をListenして、そのポートのfdをコマンドラインオプションで与えてGoのサーバーをexecする。サーバの方は、ポートで起動する場合はgoprogram -port 8080、fd受け渡しの場合はgoprogram -fd 4みたいな感じで起動できるように実装を変更する必要がある。外部スクリプトの方はCircusのようなツールを利用することもできるようだ

トリッキーな起動オプションの実装が追加されるのは若干気持ち悪い気がするが、それさえ気にしなければ実装コストも少ないしパフォーマンスペナルティも多分ない。ソケット管理ツールを書くなり、既存のツールを学習するコストはそこそこ面倒くさそうで気になる。

iptablesでポートフォワーディングする

この方法であればサーバー側実装には全く手を入れる必要は無い。起動用のツールをrootで立ち上げる必要が無いのでクリーンな感じもする。ひとつのサーバーを動かすためにシステム設定に手を入れるのは気が引けるとか、運用コストが上がりそうとか思うが、Chefでiptablesの書き換えすればOKみたいな環境であれば手軽なのでは。

Deploying Go servers with Docker - The Go Blogとかを読んでも、Dockerを使うならDockerにポートフォワーディングを任せればいいみたいな感じがするので、環境側でなんとかする方向はありっぽい。

ローカルにHTTP層のReverse Proxyを使うという手もある。この場合プロキシを挟むのでパフォーマンスペナルティや、プロキシに使うサーバとの相性などが気になる。

実際、Nginxを挟む事によるパフォーマンスペナルティはちょっと無視できない。

あるいは今気づいたけど、もともとReverse Proxy用のサーバを別に立てるつもりなら、そもそも気にしなくてよい問題ではある。

GoバイナリにウェルノウンポートをListenするケーパビリティを設定する

ケーパビリティ で権限を少しだけ与える - いますぐ実践! Linuxシステム管理 / Vol.183

ケーパビリティとは、ファイルまたはスレッドに対して、root権限の一部を付与したり剥奪したりできるもの。実行ファイルに対してsetcapコマンドで任意の権限を与えられる。GoWebの人たちはこの方法を使っているらしい。この場合は以下のようになる。

setcap 'cap_net_bind_service=+ep' /path/to/program

Why not use Goweb? · Issue #49 · stretchr/goweb · GitHub

簡単にcapabilityを試してみた。通常のlsでは/root配下が覗けないが、読み込み時パーミッションチェックをスキップするCAP_DAC_READ_SEARCHケーパビリティをlsのコピーに付与したところ、sudoもなしに一般ユーザーで/root配下が参照できる。

[vagrant@localhost ~]$ ls /root
ls: ディレクトリ /root を開くことが出来ません: 許可がありません
[vagrant@localhost ~]$ cp /usr/bin/ls ./ls.copy
[vagrant@localhost ~]$ getcap ./ls.copy
[vagrant@localhost ~]$ sudo setcap 'CAP_DAC_READ_SEARCH=+ep' ./ls.copy
[vagrant@localhost ~]$ getcap ./ls.copy
./ls.copy = cap_dac_read_search+ep
[vagrant@localhost ~]$ ./ls.copy /root
anaconda-ks.cfg
[vagrant@localhost ~]$ 

ただ、このcapability、手元で検証した限りではcpなどで複製すると権限は失われてしまうようなので、デプロイ先の環境でsudo setcapしてやる必要があるようだ。

まとめ

今回候補に挙げた手法は以下の通り。

  • Listen後にsetuidで権限降格する
    • Linuxでは問題がある
  • 起動スクリプト(カスタムメイド or Circus)からfdをもらう
    • ソケット管理ソフトウェアを用意する必要がある
    • サーバー側にもfdによるListenに対応するコードを追加する必要がある
  • iptablesなど、OSやコンテナ側でポートフォワーディングする
    • システム側に手を加える必要がある
  • ローカルでProxyを立てる
    • パフォーマンスペナルティがある
  • ケーパビリティを設定する
    • デプロイ先ホストにバイナリを配置後、sudo setcapする必要がある。

この中では、ポートフォワーディングかケーパビリティが筋が良さそうな気がしている。せっかくシングルバイナリデプロイができるGoなのだし、できればバイナリで完結する手法でなんとかしたいが、そういう方法は今は無いようだ。