JavaでのSocketおよびInetAddressを使ったプログラミングで、DNSの名前解決はどのように行われているのか、ソースなどを追いかけてみた。
三行で結論:
まず "Socket(String host, int port)" コンストラクタでの名前解決の使い方を見てみる。
DnsTest1.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のソースから、このコンストラクタのソースを確認してみる。
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)" を呼び出している。
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):
public static InetAddress getByName(String host) throws UnknownHostException { return InetAddress.getAllByName(host)[0]; }
試しに、InetAddress.getAllByName() を使ってみる。
DnsTest2.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()); } } }
実行結果:
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)"の呼び出しまで進めてみる。
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"の初期化処理周りのコードを見てみる:
/* Used to store the name service provider */ private static List<NameService> 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<NameService>(); 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); } }
簡単にまとめると、以下のような処理になっている。
JDK付属のJRE内を "sun.net.spi.nameservice.provider" でgrepしてみたが、特に設定は見つからなかった。
よって、デフォルトの環境では "createNSProvider("default")" で取得したデフォルトのNameServiceインスタンスを使っていると考えられる。
そこで、InetAddress.createNSProvider()の実装を見てみて、"default"を引数に渡した時の挙動を確認する:
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内で定義されていた:
/* * 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:
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での名前解決処理をカスタマイズするには、これまで見てきた内容を整理すると以下のようになる。
また実装サンプルと思わしきコードがあるので、これも参考になる。
JavaのDNSキャッシュとして "%JAVA_HOME%\lib\security\java.security" に設定する"networkaddress.cache.ttl"という値がある。InetAddressのJavaDocに説明がある。
これまで見てきたように、Javaにおける名前解決は以下の2種類の実装が提供されている。
では、"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 )
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<String>() { 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
実行結果:
groovy t_networkaddress_cache_ttl.groovy 30 # positive cache は30秒 10 # negative cache は10秒 false # positive cache 設定は未設定だった。 true # negative cache は設定されてた。
Javaでは以下のようになる。設定の未設定・設定有りについては省略。
DnsTest3.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秒に設定されていた。
... # # 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 クラスというのがネストされて定義されており、その中で参照されている。
/** * A cache that manages entries based on a policy specified * at creation time. */ static final class Cache { private LinkedHashMap<String, CacheEntry> cache; private Type type; enum Type {Positive, Negative}; /** * Create cache */ public Cache(Type type) { this.type = type; cache = new LinkedHashMap<String, CacheEntry>(); } private int getPolicy() { if (type == Type.Positive) { return InetAddressCachePolicy.get(); } else { return InetAddressCachePolicy.getNegative(); } } /* ... */
これはあくまでも「Javaライブラリのレイヤーで名前解決したのをキャッシュする期間」である。そして、InetAddress経由の名前解決は、デフォルトではnative(=JNI)の実装になってしまい、その内部までは調べきれなかった。
そのため、以下のようなケースではどうなるか、確認していない。
この点を確認するのであれば、自前で 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プロパティ
Eclipse上でプログラミングしている時に、sun.net や com.sun 以下のパッケージやクラスを参照しようとして "Access restriction on class due to restriction on required library rt.jar" みたいなエラーが発生した時
sun/com.sunなどのパッケージを含んだJavaソース
OpenJDK7ミラーソース
コメント