home ホーム search 検索 -  login ログイン  | reload edit datainfo version cmd icon diff delete  | help ヘルプ

Java/Socket, InetAddressにおけるDNS名前解決の仕組みと networkaddress.cache.ttl (v1)

Java/Socket, InetAddressにおけるDNS名前解決の仕組みと networkaddress.cache.ttl (v1)

Java / Socket, InetAddressにおけるDNS名前解決の仕組みと networkaddress.cache.ttl (v1)
id: 1346 所有者: msakamoto-sf    作成日: 2015-02-08 00:12:49
カテゴリ: DNS Java ネットワーク 

JavaでのSocketおよびInetAddressを使ったプログラミングで、DNSの名前解決はどのように行われているのか、ソースなどを追いかけてみた。



Socket(String host, int port)での名前解決の仕組み

三行で結論:

  1. java.net.Socketのコンストラクタ "Socket(String host, int port)"
  2. →内部的には "java.net.InetAddress#getByName(String host)" を呼んでる。
  3. →最終的にはnative(=JNI)に辿り着き、今回はそこでギブアップ。

まず "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);
        }
    }

簡単にまとめると、以下のような処理になっている。

  1. "impl" staticメンバに、InetAddressImplのインスタンスを "InetAddressImplFactory.create()" メソッドで生成してセット。
  2. Javaのプロパティで "sun.net.spi.nameservice.provider.1", "sun.net.spi.nameservice.provider.2", ... の順で探していき、指定されているプロバイダ名があれば、InetAddress.createNSProvider()でインスタンスを生成して、nameServicesに追加する。
  3. "createNSProvider("default")" で取得したデフォルトのNameServiceインスタンス(provider)をnameServicesに追加する。

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での名前解決処理をカスタマイズするには

InetAddressでの名前解決処理をカスタマイズするには、これまで見てきた内容を整理すると以下のようになる。

  1. sun.net.spi.nameservice.NameService を実装する。
    1. https://github.com/openjdk-mirror/jdk7u-jdk/blob/master/src/share/classes/sun/net/spi/nameservice/NameService.java
    2. また、以下のクラスも実装しないと駄目っぽい。
    3. https://github.com/openjdk-mirror/jdk7u-jdk/blob/master/src/share/classes/sun/net/spi/nameservice/NameServiceDescriptor.java
  2. "sun.net.spi.nameservice.provider.N" のJavaプロパティに、実装したprovider名を指定する。
    1. これについても、正確にどういう名前を指定すればよいのかについてはInetAddressのソースなど要確認。SPIに沿ってるぽいので、そのへんも要確認。

また実装サンプルと思わしきコードがあるので、これも参考になる。

"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種類の実装が提供されている。

  1. (Socketコンストラクタ ->) "InetAddress#getByName()" -> native(JNI)
  2. 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 )

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();
            }
        }
        /* ... */

"networkaddress.cache.ttl" のデフォルト値は?

"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プロパティ

Eclipse上でプログラミングしている時に、sun.net や com.sun 以下のパッケージやクラスを参照しようとして "Access restriction on class due to restriction on required library rt.jar" みたいなエラーが発生した時

sun/com.sunなどのパッケージを含んだJavaソース

OpenJDK7ミラーソース



プレーンテキスト形式でダウンロード
現在のバージョン : 1
更新者: msakamoto-sf
更新日: 2015-02-08 00:16:40
md5:b80edda6584addecc35afea019d3ae53
sha1:8f1312a6fc6dd77605fafc337827211621d29b05
コメント
コメントを投稿するにはログインして下さい。