Bug #7100

WEBrick::HTTPServer.new で BindAddress を指定しない場合に必ず警告が記録される

Added by Sho Hashimoto over 1 year ago. Updated about 1 year ago.

[ruby-dev:46189]
Status:Closed
Priority:Low
Assignee:Akira Tanaka
Category:-
Target version:next minor
ruby -v:ruby 1.9.3p194 (2012-04-20 revision 35410) [x86_64-linux] Backport:

Description

=begin
以下のようにすると必ず警告が記録されるようです。

$ ruby -v -r webrick -e 'WEBrick::HTTPServer.new(Port: 3000)'
ruby 1.9.3p194 (2012-04-20 revision 35410) [x86_64-linux]
[2012-09-04 19:20:48] INFO WEBrick 1.3.1
[2012-09-04 19:20:48] INFO ruby 1.9.3 (2012-04-20) [x86_64-linux]
[2012-09-04 19:20:48] WARN TCPServer Error: Address already in use - bind(2)

1.8 では記録されませんでした。1.9.1 以降は記録されました。

lib/webrick/utils.rb の WEBrick::Utils#create_listeners が以下のようになっており、

res = Socket::getaddrinfo(address, port,
Socket::AFUNSPEC, # address family
Socket::SOCK
STREAM, # socket type
0, # protocol
Socket::AIPASSIVE) # flag
last
error = nil
sockets = []
res.each{|ai|
begin
logger.debug("TCPServer.new(#{ai[3]}, #{port})") if logger
sock = TCPServer.new(ai[3], port)
...

Socket.getaddrinfo が 1.9 から複数値を返すからのようです。1.8.7 だと 0.0.0.0 の方だけでした。

$ ruby -v -r pp -r socket -e 'pp Socket::getaddrinfo(nil, 3000, Socket::AFUNSPEC, Socket::SOCKSTREAM, 0, Socket::AIPASSIVE)'
ruby 1.9.3p194 (2012-04-20 revision 35410) [x8664-linux]
[["AF
INET", 3000, "0.0.0.0", "0.0.0.0", 2, 1, 6],
["AF
INET6", 3000, "::", "::", 10, 1, 6]]

WEBrick::Utils#create_listeners のコメントとマッチしなくなるデメリットがあるのですが、config[:BindAddress] のデフォルト値を 0.0.0.0 か :: のどちらかにしてしまうのはいかがでしょう。
=end

webrick-dont-use-ipv4-mapped-ipv6-address.patch Magnifier (1.32 KB) Akira Tanaka, 11/07/2012 07:19 PM

Associated revisions

Revision 39551
Added by Akira Tanaka about 1 year ago

History

#1 Updated by Sho Morita over 1 year ago

=begin
私のところでも同様の症状が出ています。そういった警告が出る場合、WEBrick サーバーに IPv6 でアクセスできなくなってしまいます。また、環境によっては http://localhost:3000/ のように localhost を指定してもアクセス不能になってしまいます。(:BindAddress を明示的に指定すれば問題ないのですけれども…)

WEBrick のドキュメントには :BindAddress に関して

デフォルトの nil や "0.0.0.0", "::" などを指定した場合は使用可能なすべてのネットワークインターフェースに対して listen を開始します。

と書いてあるものの、実際には

  • nil を指定すると使用可能なすべての IPv4 および IPv6 ネットワークインターフェースに対して listen する。
  • "0.0.0.0" を指定すると使用可能なすべての IPv4 ネットワークインターフェースに対してのみ listen する。
  • "::" を指定すると使用可能なすべての IPv6 ネットワークインターフェースに対してのみ listen する。(一部システムでは nil と指定したのと同様に、すべての IPv4 と IPv6 ネットワークインターフェースに対して listen する)

となります。

一部のシステム(Linuxなど)では、IPv6 ワイルドカードアドレスである :: を bind すると、IPv6 ネットワークインターフェースだけではなく、IPv4 ネットワークインターフェースも bind されます。0.0.0.0 と :: を両方 bind しようとすると、後から bind した方が Address already in use(EADDRINUSE) エラーとなり失敗するため、IPv6 でのアクセスが行えなくなってしまいます。

などによると、Ruby 1.9.2 で Socket にたくさん機能が追加され、IPv6 問題がだいぶ改善されたようです。そこで追加された Socket#ipv6only! を呼び出すと IPV6_V6ONLY ソケットオプションが有効になり、0.0.0.0 と :: の両方を bind する事ができるようになります。

しかし、現在 WEBrick が使用している TCPServer は従来からある API で、IPV6V6ONLY ソケットオプションをセットしません。新しい API である Socket.tcpserver_sockets などは、必要に応じて Socket#ipv6only! を呼び出してくれます。

ですので、TCPServer.new ではなく Socket.tcpserversockets を使うようにするのが良いかと思ったのですが、TCPServer.new のオブジェクトと Socket.tcpserversockets のオブジェクトは一部互換性の無い部分があり、置き換えてしまうと WEBrick を利用する既存のソフトウェアで互換性問題が起こるのではないか心配です。

そこで、互換性の問題が起こらないように TCPServer を使うのを維持しつつ問題を修正しようとすると、

  • :BindAddress == nil である場合に、:: を先に bind し、0.0.0.0 を後から bind (その際 EADDRINUSE が起きても無視)する。

というようにすれば良いのではないかと思います。一応パッチを作ってみました。

Index: lib/webrick/utils.rb
===================================================================
--- lib/webrick/utils.rb (revision 37168)
+++ lib/webrick/utils.rb (working copy)
@@ -79,6 +79,15 @@
Socket::AIPASSIVE) # flag
last
error = nil
sockets = []
+ # If address == nil, Socket.getaddrinfo returns 2 entries, the
+ # IPv4 wildcard address "0.0.0.0" and the IPv6 wildcard address "::".
+ # On some systems, if you try to bind for both "0.0.0.0" and "::",
+ # the later one will fails.
+ # To workaround such behaviour, try bind for "::" first, then
+ # bind "0.0.0.0" and ignore EADDRINUSE error.
+ if address.nil?
+ res = res.sortby{|i| i[4]}.reverse
+ end
res.each{|ai|
begin
logger.debug("TCPServer.new(#{ai[3]}, #{port})") if logger
@@ -87,7 +96,9 @@
Utils::set
closeonexec(sock)
sockets << sock
rescue => ex
- logger.warn("TCPServer Error: #{ex}") if logger
+ logger.warn("TCPServer Error: #{ex}") if logger and !(address.nil? and
+ ai[4] == Socket::AFINET and
+ Errno::EADDRINUSE === ex)
last
error = ex
end
}

ちなみに、根本的な解決にはなりませんが、

# sysctl net.ipv6.bindv6only=1

とすると、IPV6_V6ONLY ソケットオプションがデフォルトで有効になり、:BindAddress が nil でも警告は出なくなります。

=end

#2 Updated by Yusuke Endoh over 1 year ago

  • Status changed from Open to Assigned
  • Assignee set to Akira Tanaka
  • Target version set to 2.0.0

akr さん、どう思われますか?

Yusuke Endoh mame@tsg.ne.jp

#3 Updated by Akira Tanaka over 1 year ago

2012/11/5 mame (Yusuke Endoh) mame@tsg.ne.jp:

akr さん、どう思われますか?

IPv4-mapped IPv6 address はプラットフォームによって利用できたりできなかったりするので、
どのプラットフォームでも利用しないで動作するようにするのが動作が一貫してよいと思います。

私は、長期的には、TCPServer とかを使うのはやめて、
Socket に移行して欲しいと思っていますが、
互換性の問題を無視することもできませんよね。

というふたつの点を考えると、Socket.tcpserversockets でソケットを作って、
そこから TCPServer.for_fd で TCPServer のインスタンスを作るあたりかなぁ、と
思います。
--
[田中 哲][たなか あきら][Tanaka Akira]

#4 Updated by Akira Tanaka over 1 year ago

webrick-dont-use-ipv4-mapped-ipv6-address.patch みたいなかんじかなぁ。

#5 Updated by Yusuke Endoh about 1 year ago

  • Target version changed from 2.0.0 to next minor

ちょっと今からだと怖すぎるのと、BindAddress を指定するという workaround があるようなので、
すみませんが一旦 next minor に。
2.0.0-pXXX で直すかどうかは nagachika さんにお任せします。

Yusuke Endoh mame@tsg.ne.jp

#6 Updated by Akira Tanaka about 1 year ago

  • Status changed from Assigned to Closed
  • % Done changed from 0 to 100

This issue was solved with changeset r39551.
Sho, thank you for reporting this issue.
Your contribution to Ruby is greatly appreciated.
May Ruby be with you.


Also available in: Atom PDF