#navi_header|Java| JavaでのSocketおよびInetAddressを使ったプログラミングで、DNSの名前解決はどのように行われているのか、ソースなどを追いかけてみた。 #outline|| ---- - サンプルコードの実行環境 : Win7 64bit + Oracle JDK 1.8.0.31 - 確認に使ったソースコード -- Oracle JDK 1.8.0.31 および 1.7.0.25 の src.zip -- http://jdk7src.sourceforge.net/ -- https://github.com/openjdk-mirror/jdk7u-jdk ---- * Socket(String host, int port)での名前解決の仕組み 三行で結論: + java.net.Socketのコンストラクタ "Socket(String host, int port)" + →内部的には "java.net.InetAddress#getByName(String host)" を呼んでる。 + →最終的にはnative(=JNI)に辿り着き、今回はそこでギブアップ。 まず "Socket(String host, int port)" コンストラクタでの名前解決の使い方を見てみる。 DnsTest1.java: #code|java|> import java.net.InetAddress; import java.net.Socket; public class DnsTest1 { public static void main(String[] args) { try (Socket socket = new Socket("www.google.com", 80)) { InetAddress ia = socket.getInetAddress(); System.out.println(ia.getHostAddress()); } catch (Exception e) { e.printStackTrace(); } } } ||< 実行結果: 216.58.220.228 名前解決がどうなっているのか?JDKのソースから、このコンストラクタのソースを確認してみる。 #code|java|> public Socket(String host, int port) throws UnknownHostException, IOException { this(host != null ? new InetSocketAddress(host, port) : new InetSocketAddress(InetAddress.getByName(null), port), (SocketAddress) null, true); } ||< "host"がnullでなければ、"new InetSocketAddress(host, port)" を呼び出している。 #code|java|> public InetSocketAddress(String hostname, int port) { checkHost(hostname); InetAddress addr = null; String host = null; try { addr = InetAddress.getByName(hostname); } catch(UnknownHostException e) { host = hostname; } holder = new InetSocketAddressHolder(host, addr, checkPort(port)); } ||< アドレスを"InetAddress.getByName(hostname)"でInetAddressを取得している。 InetAddress.getByName(String host): #code|java|> public static InetAddress getByName(String host) throws UnknownHostException { return InetAddress.getAllByName(host)[0]; } ||< 試しに、InetAddress.getAllByName() を使ってみる。 DnsTest2.java: #code|java|> import java.net.InetAddress; public class DnsTest2 { public static void main(String[] args) throws Exception { InetAddress[] ias = InetAddress.getAllByName("www.google.com"); for (InetAddress ia : ias) { System.out.println(ia.getHostAddress()); } } } ||< 実行結果: #pre||> 173.194.38.211 173.194.38.212 173.194.38.209 173.194.38.210 173.194.38.208 ||< "InetAddress.getAllByName(host)"からの流れだが、途中はスキップし、"getAddressesFromNameService(String host, InetAddress reqAddr)"の呼び出しまで進めてみる。 #code|java|> private static InetAddress[] getAddressesFromNameService(String host, InetAddress reqAddr) throws UnknownHostException { /* ... */ try { for (NameService nameService : nameServices) { try { addresses = nameService.lookupAllHostAddr(host); success = true; break; } catch (UnknownHostException uhe) { /* ... */ ||< "nameService" は "sun.net.spi.nameservice.NameService" のListになっており、NameServiceはinterfaceとなっている。 InetAddress.java中の、"nameService"の初期化処理周りのコードを見てみる: #code|java|> /* Used to store the name service provider */ private static List nameServices = null; static { // create the impl impl = InetAddressImplFactory.create(); // get name service if provided and requested String provider = null;; String propPrefix = "sun.net.spi.nameservice.provider."; int n = 1; nameServices = new ArrayList(); provider = AccessController.doPrivileged( new GetPropertyAction(propPrefix + n)); while (provider != null) { NameService ns = createNSProvider(provider); if (ns != null) nameServices.add(ns); n++; provider = AccessController.doPrivileged( new GetPropertyAction(propPrefix + n)); } // if not designate any name services provider, // create a default one if (nameServices.size() == 0) { NameService ns = createNSProvider("default"); nameServices.add(ns); } } ||< 簡単にまとめると、以下のような処理になっている。 + "impl" staticメンバに、InetAddressImplのインスタンスを "InetAddressImplFactory.create()" メソッドで生成してセット。 + Javaのプロパティで "sun.net.spi.nameservice.provider.1", "sun.net.spi.nameservice.provider.2", ... の順で探していき、指定されているプロバイダ名があれば、InetAddress.createNSProvider()でインスタンスを生成して、nameServicesに追加する。 + "createNSProvider("default")" で取得したデフォルトのNameServiceインスタンス(provider)をnameServicesに追加する。 JDK付属のJRE内を "sun.net.spi.nameservice.provider" でgrepしてみたが、特に設定は見つからなかった。 よって、デフォルトの環境では "createNSProvider("default")" で取得したデフォルトのNameServiceインスタンスを使っていると考えられる。 そこで、InetAddress.createNSProvider()の実装を見てみて、"default"を引数に渡した時の挙動を確認する: #code|java|> private static NameService createNSProvider(String provider) { if (provider == null) return null; NameService nameService = null; if (provider.equals("default")) { // initialize the default name service nameService = new NameService() { public InetAddress[] lookupAllHostAddr(String host) throws UnknownHostException { return impl.lookupAllHostAddr(host); } public String getHostByAddr(byte[] addr) throws UnknownHostException { return impl.getHostByAddr(addr); } }; } else { /* ... "default" 以外のNameServiceプロバイダの生成 ... */ } return nameService; } ||< "default" providerの場合、InetAddressの "impl" staticメンバに呼び出しを委譲した NameService インスタンスを生成している。 "impl" は InetAddressImpl の実装であり、 "lookupAllHostAddr()" と "getHostByAddr()" に委譲している。これらの実装を追うため、まず、"InetAddressImplFactory#create()" のソースを確認する。 InetAddressImplFactoryはInetAddress.java内で定義されていた: #code|java|> /* * Simple factory to create the impl */ class InetAddressImplFactory { static InetAddressImpl create() { return InetAddress.loadImpl(isIPv6Supported() ? "Inet6AddressImpl" : "Inet4AddressImpl"); } static native boolean isIPv6Supported(); } ||< native(=JNI)の isIPv6Supported() に応じて、Inet4AddressImplかInet6AddressImplかに、クラスの実体を切り替えている。 今回はInet4AddressImplのソースを確認してみる。 java/net/Inet4AddressImpl.java: #code|java|> class Inet4AddressImpl implements InetAddressImpl { public native String getLocalHostName() throws UnknownHostException; public native InetAddress[] lookupAllHostAddr(String hostname) throws UnknownHostException; public native String getHostByAddr(byte[] addr) throws UnknownHostException; private native boolean isReachable0(byte[] addr, int timeout, byte[] ifaddr, int ttl) throws IOException; /* ... */ ||< 肝心の "lookupAllHostAddr()" などが軒並み native(=JNI) になっている。今回はJNIのソースまでは追い切れないため、Socket -> InetAddress 経由での名前解決処理についてはここで打ち切る。 ** InetAddressでの名前解決処理をカスタマイズするには InetAddressでの名前解決処理をカスタマイズするには、これまで見てきた内容を整理すると以下のようになる。 + sun.net.spi.nameservice.NameService を実装する。 ++ https://github.com/openjdk-mirror/jdk7u-jdk/blob/master/src/share/classes/sun/net/spi/nameservice/NameService.java ++ また、以下のクラスも実装しないと駄目っぽい。 ++ https://github.com/openjdk-mirror/jdk7u-jdk/blob/master/src/share/classes/sun/net/spi/nameservice/NameServiceDescriptor.java + "sun.net.spi.nameservice.provider.N" のJavaプロパティに、実装したprovider名を指定する。 ++ これについても、正確にどういう名前を指定すればよいのかについてはInetAddressのソースなど要確認。SPIに沿ってるぽいので、そのへんも要確認。 また実装サンプルと思わしきコードがあるので、これも参考になる。 - https://github.com/openjdk-mirror/jdk7u-jdk/blob/master/src/share/classes/sun/net/spi/nameservice/dns/DNSNameService.java - https://github.com/openjdk-mirror/jdk7u-jdk/blob/master/src/share/classes/sun/net/spi/nameservice/dns/DNSNameServiceDescriptor.java * "networkaddress.cache.ttl" はどこで使われているのか? JavaのDNSキャッシュとして "%JAVA_HOME%\lib\security\java.security" に設定する"networkaddress.cache.ttl"という値がある。InetAddressのJavaDocに説明がある。 - https://docs.oracle.com/javase/jp/8/api/java/net/InetAddress.html -- デフォルトは「実装に固有の期間キャッシュ」する。 -- -1 だと「ずっとキャッシュする」(キャッシュポイゾニング対策になる) -- 1以上ならキャッシュ秒数 -- 0ならキャッシュ期間0秒なので毎回問い合わせになりそう。 これまで見てきたように、Javaにおける名前解決は以下の2種類の実装が提供されている。 + (Socketコンストラクタ ->) "InetAddress#getByName()" -> native(JNI) + JNDI DNS プロバイダ では、"networkaddress.cache.ttl" はどちらで使われているか? 結論から言うと、そもそもこのプロパティ値、InetAddressのJavaDocに記載されているのだから、InetAddressでの名前解決で使われる。 JNDI DNS プロバイダについては、sunパッケージ/com.sunパッケージのjavaソースも含めてgrepした範囲では使われている様子は確認できなかった。 "networkaddress.cache.ttl" についてsunパッケージ/com.sunパッケージも含めてJavaソースをgrepしてみると、以下のJavaファイルで参照していた。 sun/net/InetAddressCachePolicy.java: (openjdk7 : https://github.com/openjdk-mirror/jdk7u-jdk/blob/master/src/share/classes/sun/net/InetAddressCachePolicy.java ) #code|java|> package sun.net; import java.security.PrivilegedAction; import java.security.Security; public final class InetAddressCachePolicy { // Controls the cache policy for successful lookups only private static final String cachePolicyProp = "networkaddress.cache.ttl"; private static final String cachePolicyPropFallback = "sun.net.inetaddr.ttl"; // Controls the cache policy for negative lookups only private static final String negativeCachePolicyProp = "networkaddress.cache.negative.ttl"; private static final String negativeCachePolicyPropFallback = "sun.net.inetaddr.negative.ttl"; public static final int FOREVER = -1; public static final int NEVER = 0; /* default value for positive lookups */ public static final int DEFAULT_POSITIVE = 30; /* ... */ static { Integer tmp = null; try { tmp = new Integer( java.security.AccessController.doPrivileged ( new PrivilegedAction() { public String run() { return Security.getProperty(cachePolicyProp); } })); } catch (NumberFormatException e) { // ignore } if (tmp != null) { cachePolicy = tmp.intValue(); if (cachePolicy < 0) { cachePolicy = FOREVER; } propertySet = true; } else { /* ... */ } else { /* No properties defined for positive caching. If there is no * security manager then use the default positive cache value. */ if (System.getSecurityManager() == null) { cachePolicy = DEFAULT_POSITIVE; } } /* ... */ ||< ポイントとしては、このソースコードだと "networkaddress.cache.ttl" or "sun.net.inetaddr.ttl" が指定されていればその値を使うが、どこにも指定が無ければ、DEFAULT_POSITIVE, つまり30秒が名前解決のキャッシュTTLとなる。 現在の実行環境では実際にどの値なのか確認するため、以下のようなGroovyスクリプトを作成し、OracleJDK8のJRE環境で実行してみた。 https://gist.github.com/msakamoto-sf/733aa7a2d0b461766b77 実行結果: #pre||> groovy t_networkaddress_cache_ttl.groovy 30 # positive cache は30秒 10 # negative cache は10秒 false # positive cache 設定は未設定だった。 true # negative cache は設定されてた。 ||< Javaでは以下のようになる。設定の未設定・設定有りについては省略。 DnsTest3.java: #code|java|> import sun.net.InetAddressCachePolicy; public class DnsTest3 { public static void main(String[] args) throws Exception { System.out.println(InetAddressCachePolicy.get()); System.out.println(InetAddressCachePolicy.getNegative()); } } ||< ※Eclipse上で "Access restriction on class due to restriction on required library rt.jar" というエラーが発生した場合は、 Windows -> Preferences -> Java -> Compiler -> Errors/Warnings で "Forbidden reference (access rules)" を Warning または Ignore に設定する。 実際に実行環境の jre/lib/security/java.security を見てみると、以下のように "networkaddress.cache.ttl" はコメントアウトされていて未設定、 "networkaddress.cache.negative.ttl" は10秒に設定されていた。 #pre||> ... # # The Java-level namelookup cache policy for successful lookups: # # any negative value: caching forever # any positive value: the number of seconds to cache an address for # zero: do not cache # # default value is forever (FOREVER). For security reasons, this # caching is made forever when a security manager is set. When a security # manager is not set, the default behavior in this implementation # is to cache for 30 seconds. # # NOTE: setting this to anything other than the default value can have # serious security implications. Do not set it unless # you are sure you are not exposed to DNS spoofing attack. # #networkaddress.cache.ttl=-1 # The Java-level namelookup cache policy for failed lookups: # # any negative value: cache forever # any positive value: the number of seconds to cache negative lookup results # zero: do not cache # # In some Microsoft Windows networking environments that employ # the WINS name service in addition to DNS, name service lookups # that fail may take a noticeably long time to return (approx. 5 seconds). # For this reason the default caching policy is to maintain these # results for 10 seconds. # # networkaddress.cache.negative.ttl=10 ... ||< InetAddressCachePolicy クラス名で sun/com.sun パッケージ含めたJDKのJavaソースをgrepしたところ、 "java/net/InetAddress.java" でのみ使われていた。 InetAddressの中に Cache クラスというのがネストされて定義されており、その中で参照されている。 #code|java|> /** * A cache that manages entries based on a policy specified * at creation time. */ static final class Cache { private LinkedHashMap cache; private Type type; enum Type {Positive, Negative}; /** * Create cache */ public Cache(Type type) { this.type = type; cache = new LinkedHashMap(); } private int getPolicy() { if (type == Type.Positive) { return InetAddressCachePolicy.get(); } else { return InetAddressCachePolicy.getNegative(); } } /* ... */ ||< ** "networkaddress.cache.ttl" のデフォルト値は? - OpenJDK7 : 30秒(ソースコード上で確認 https://github.com/openjdk-mirror/jdk7u-jdk/blob/master/src/share/classes/sun/net/InetAddressCachePolicy.java ) - Windows 64bit版 Oracle JDK 8 (1.8.0.31) : 30秒(実測値) - 他のバージョン/他のJVM製品 : 不明。ドキュメントやソースを確認、可能ならばサポートに問い合わせ。 ** "networkaddress.cache.ttl" はDNSサーバが返すTTLとどう連携するか? これはあくまでも「Javaライブラリのレイヤーで名前解決したのをキャッシュする期間」である。そして、InetAddress経由の名前解決は、デフォルトではnative(=JNI)の実装になってしまい、その内部までは調べきれなかった。 そのため、以下のようなケースではどうなるか、確認していない。 - DNSのNSレコードのTTLは数秒になっているのに、SecurityManager付き + "networkaddress.cache.ttl=-1" でJavaを起動した場合。 -- → 最初に名前解決したのがずっとキャッシュされてしまい、DNS側のTTLの設定値とは無関係な動きになってしまうのか? この点を確認するのであれば、自前で sun.net.spi.nameservice.NameService 及び NameServiceDescriptor を実装して名前解決の要求があったらlogするようにして、それを "sun.net.spi.nameservice.provider.N" あたりに設定すれば良いだろう。 個人的な想像だが、 NameService の lookupAllHostAddr() や getHostByAddr() はDNSサーバからのTTL情報をやりとりできるメソッドシグネチャになってないため、DNSのTTLは InetAddress のキャッシュ処理とは連携出来ないのではないか、と予想される。つまりまず "networkaddress.cache.ttl" によるキャッシュ期間がチェックされ、それがexpireされて始めてDNS問い合わせが発生する。この時DNSサーバからのTTL情報をどう扱うかは、NameServiceの実装に依存することが予想される。 * 参考資料 networkaddress.cache.ttl 関連: - JavaのDNSキャッシュ - 人類みんなごくつぶし -- http://d.hatena.ne.jp/muimy/20110105/1294225912 - java.net.URLの闇 - blog.scheakur.com -- http://blog.scheakur.com/post/74938746551/java-net-url ネットワーク関連のJavaプロパティ - JDK6 : https://docs.oracle.com/javase/jp/6/technotes/guides/net/properties.html - JDK7 : https://docs.oracle.com/javase/jp/7/technotes/guides/net/properties.html - JDK8 : https://docs.oracle.com/javase/jp/8/technotes/guides/net/properties.html Eclipse上でプログラミングしている時に、sun.net や com.sun 以下のパッケージやクラスを参照しようとして "Access restriction on class due to restriction on required library rt.jar" みたいなエラーが発生した時 - java - Access restriction on class due to restriction on required library rt.jar? - Stack Overflow -- http://stackoverflow.com/questions/860187/access-restriction-on-class-due-to-restriction-on-required-library-rt-jar - java ee - Access restriction on jdk1.7/jre/lib/rt.jar - Stack Overflow -- http://stackoverflow.com/questions/10428984/access-restriction-on-jdk1-7-jre-lib-rt-jar - [Eclipse] Access restriction: Class is not accessible due to restriction on required library @Digizol -- http://www.digizol.com/2008/09/eclipse-access-restriction-on-library.html - Javaで「Access restriction」と出たときの対処 - Fight the Future -- http://jyukutyo.hatenablog.com/entry/20101211/1292161476 sun/com.sunなどのパッケージを含んだJavaソース - JDK付属の「src.zip」の代わりに「jdk7src」を使ってEclipseの「Source not found」を回避する方法 -- http://did2memo.net/2012/12/17/eclipse-jdk-src-zip-jdk7src-source-not-found/ - http://jdk7src.sourceforge.net/ OpenJDK7ミラーソース - https://github.com/openjdk-mirror/jdk7u-jdk #navi_footer|Java|