[입 개발] Java.net.InetAddress 의 getLocalHost() 버그…

모 오픈소스를 실행시키다가 이상한 일이 생겨서, 버그인가 하고 보다가… 재미난 현상을 발견했습니다. Java.net.InetAddress 가 뭔가 이상한 결과를 넘겨주는 것입니다. 먼저… 간단한 소스를 보시죠. 테스트 프로그램은 다음과 같습니다.(결론부터 말하자면… 자바의 버그라고 할 수는 없습니다. ㅋㅋㅋ, DNS 변경으로 일단 원하는 결과가 나오는 ㅎㅎㅎ)

* 결론적으로는 U+등이 디지털 네임스랑 계약을 맺고, 분석이 되지 않는 도메인을 디지털네임스로 질의하고, 이를 디지털 네임스에서 키워드로 등록되었거나, 등록되지 않은 주소를 자신의 ip등으로 돌려줘서 발생하는 이슈로 추측되고 있습니다.)

import java.io.*;
import java.util.*;
import java.net.InetAddress;
import java.net.UnknownHostException;

public class Test {
  public static void main(String [] args) {
    try {
      System.out.println(InetAddress.getLocalHost());
    } catch(UnknownHostException var1) {
      System.out.println("Exception : " + var1);
    }
  }
}

그런데 그 결과가 다음과 같습니다. -_-

charsyam ~/works/test $ java Test
charsyam.local/218.38.137.28
charsyam ~/works/test $ java Test
charsyam.local/218.38.137.28
charsyam ~/works/test $ java Test
charsyam.local/192.168.1.7
charsyam ~/works/test $ java Test
charsyam.local/192.168.1.7

네, getLocalHost()를 호출한 결과가 218.38.137.28 이거나 192.168.1.7이 나옵니다. 실제 저희 집의 네트웍은 공유기 밑에 접속이 되는 것이라, 192.168.1.7이 기대한 값입니다. 혹시나 외부 아이피인가 해서 확인해도 제 공유기가 가진 아이피도, 위의 218.38.137.28 값은 아니었습니다. 전혀 상관 없는 값이죠.

그런데 재미있는 것은 이 것은 단순히 자바의 문제는 아니라는 것입니다.
dig/nslookup 으로 해본 결과입니다.

nslookup 결과

charsyam ~ $ nslookup Macintosh-7.local
Server:		192.168.1.1
Address:	192.168.1.1#53

Name:	Macintosh-7.local
Address: 218.38.137.28

dig 결과

charsyam ~ $ dig Macintosh-7.local

; <<>> DiG 9.8.3-P1 <<>> Macintosh-7.local
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 40380
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;Macintosh-7.local.		IN	A

;; ANSWER SECTION:
Macintosh-7.local.	3600	IN	A	218.38.137.28

;; Query time: 5 msec
;; SERVER: 192.168.1.1#53(192.168.1.1)
;; WHEN: Sun Dec 28 23:44:09 2014
;; MSG SIZE  rcvd: 51

네 DNS 결과가 이상합니다. 흑흑흑, 일단 네임서버가 뭔가 이상한 결과를 던져주는 건 일단 확실한데… 제가 보기엔 여기에 설정된 네임서버가 이상하고, 여기서 질의도 이상한데, 거기에 대해서 이상한 결과를 주는 듯 합니다.

그럼 왜 위의 getLocalHost()의 결과가 저럴까요? 이름만 보면 localhost를 줘야 할 것 같은데… 그게 아닙니다.
이제 Java 소스를 까보도록 하겠습니다. 저부분만…

코드를 해석하면 처음에 LocalHostName을 가져옵니다. 저의 경우는 Macintosh-7.local 이겠죠.
그리고 위의 값이 localhost면 그냥 루프백 주소를 줍니다. 즉 이러면 127.0.0.1 이나 정상적인 값이 갈듯 합니다.
그리고 그 호스트네임을 이용해서 InetAddress.getAddressesFromNameService 을 이용해서 InetAddress를 가져오는데, 여기서 뭔가 해당 도메인 파싱이 잘못되고, 도메인 네임서버로 가서 이상한 결과가 오는게 아닌가 싶습니다.

    public static InetAddress getLocalHost() throws UnknownHostException {

        SecurityManager security = System.getSecurityManager();
        try {
            String local = impl.getLocalHostName();

            if (security != null) {
                security.checkConnect(local, -1);
            }

            if (local.equals("localhost")) {
                return impl.loopbackAddress();
            }

            InetAddress ret = null;
            synchronized (cacheLock) {
                long now = System.currentTimeMillis();
                if (cachedLocalHost != null) {
                    if ((now - cacheTime) < maxCacheTime) // Less than 5s old?
                        ret = cachedLocalHost;
                    else
                        cachedLocalHost = null;
                }

                // we are calling getAddressesFromNameService directly
                // to avoid getting localHost from cache
                if (ret == null) {
                    InetAddress[] localAddrs;
                    try {
                        localAddrs =
                            InetAddress.getAddressesFromNameService(local, null);
                    } catch (UnknownHostException uhe) {
                        // Rethrow with a more informative error message.
                        UnknownHostException uhe2 =
                            new UnknownHostException(local + ": " +
                                                     uhe.getMessage());
                        uhe2.initCause(uhe);
                        throw uhe2;
                    }
                    cachedLocalHost = localAddrs[0];
                    cacheTime = now;
                    ret = localAddrs[0];
                }
            }
            return ret;
        } catch (java.lang.SecurityException e) {
            return impl.loopbackAddress();
        }
    }

참고로 getAddressesFromNameService 는 다음과 같이 DNS 프로바인더를 이용해서 실제 DNS쿼리를 하게 됩니다.

    private static InetAddress[] getAddressesFromNameService(String host, InetAddress reqAddr)
        throws UnknownHostException
    {
        InetAddress[] addresses = null;
        boolean success = false;
        UnknownHostException ex = null;

        // Check whether the host is in the lookupTable.
        // 1) If the host isn't in the lookupTable when
        //    checkLookupTable() is called, checkLookupTable()
        //    would add the host in the lookupTable and
        //    return null. So we will do the lookup.
        // 2) If the host is in the lookupTable when
        //    checkLookupTable() is called, the current thread
        //    would be blocked until the host is removed
        //    from the lookupTable. Then this thread
        //    should try to look up the addressCache.
        //     i) if it found the addresses in the
        //        addressCache, checkLookupTable()  would
        //        return the addresses.
        //     ii) if it didn't find the addresses in the
        //         addressCache for any reason,
        //         it should add the host in the
        //         lookupTable and return null so the
        //         following code would do  a lookup itself.
        if ((addresses = checkLookupTable(host)) == null) {
            try {
                // This is the first thread which looks up the addresses
                // this host or the cache entry for this host has been
                // expired so this thread should do the lookup.
                for (NameService nameService : nameServices) {
                    try {
                        /*
                         * Do not put the call to lookup() inside the
                         * constructor.  if you do you will still be
                         * allocating space when the lookup fails.
                         */

                        addresses = nameService.lookupAllHostAddr(host);
                        success = true;
                        break;
                    } catch (UnknownHostException uhe) {
                        if (host.equalsIgnoreCase("localhost")) {
                            InetAddress[] local = new InetAddress[] { impl.loopbackAddress() };
                            addresses = local;
                            success = true;
                            break;
                        }
                        else {
                            addresses = unknown_array;
                            success = false;
                            ex = uhe;
                        }
                    }
                }

                // More to do?
                if (reqAddr != null && addresses.length > 1 && !addresses[0].equals(reqAddr)) {
                    // Find it?
                    int i = 1;
                    for (; i < addresses.length; i++) {
                        if (addresses[i].equals(reqAddr)) {
                            break;
                        }
                    }
                    // Rotate
                    if (i < addresses.length) {
                        InetAddress tmp, tmp2 = reqAddr;
                        for (int j = 0; j < i; j++) {
                            tmp = addresses[j];
                            addresses[j] = tmp2;
                            tmp2 = tmp;
                        }
                        addresses[i] = tmp2;
                    }
                }
                // Cache the address.
                cacheAddresses(host, addresses, success);

                if (!success && ex != null)
                    throw ex;

            } finally {
                // Delete host from the lookupTable and notify
                // all threads waiting on the lookupTable monitor.
                updateLookupTable(host);
            }
        }

        return addresses;
    }