1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.connectivity; 18 19 import static android.system.OsConstants.*; 20 21 import android.net.LinkAddress; 22 import android.net.LinkProperties; 23 import android.net.Network; 24 import android.net.NetworkUtils; 25 import android.net.RouteInfo; 26 import android.net.TrafficStats; 27 import android.net.util.NetworkConstants; 28 import android.os.SystemClock; 29 import android.system.ErrnoException; 30 import android.system.Os; 31 import android.system.StructTimeval; 32 import android.text.TextUtils; 33 import android.util.Pair; 34 35 import com.android.internal.util.IndentingPrintWriter; 36 37 import java.io.Closeable; 38 import java.io.FileDescriptor; 39 import java.io.InterruptedIOException; 40 import java.io.IOException; 41 import java.net.Inet4Address; 42 import java.net.Inet6Address; 43 import java.net.InetAddress; 44 import java.net.InetSocketAddress; 45 import java.net.NetworkInterface; 46 import java.net.SocketAddress; 47 import java.net.SocketException; 48 import java.net.UnknownHostException; 49 import java.nio.ByteBuffer; 50 import java.nio.charset.StandardCharsets; 51 import java.util.concurrent.CountDownLatch; 52 import java.util.concurrent.TimeUnit; 53 import java.util.Arrays; 54 import java.util.ArrayList; 55 import java.util.HashMap; 56 import java.util.List; 57 import java.util.Map; 58 import java.util.Random; 59 60 import libcore.io.IoUtils; 61 62 63 /** 64 * NetworkDiagnostics 65 * 66 * A simple class to diagnose network connectivity fundamentals. Current 67 * checks performed are: 68 * - ICMPv4/v6 echo requests for all routers 69 * - ICMPv4/v6 echo requests for all DNS servers 70 * - DNS UDP queries to all DNS servers 71 * 72 * Currently unimplemented checks include: 73 * - report ARP/ND data about on-link neighbors 74 * - DNS TCP queries to all DNS servers 75 * - HTTP DIRECT and PROXY checks 76 * - port 443 blocking/TLS intercept checks 77 * - QUIC reachability checks 78 * - MTU checks 79 * 80 * The supplied timeout bounds the entire diagnostic process. Each specific 81 * check class must implement this upper bound on measurements in whichever 82 * manner is most appropriate and effective. 83 * 84 * @hide 85 */ 86 public class NetworkDiagnostics { 87 private static final String TAG = "NetworkDiagnostics"; 88 89 private static final InetAddress TEST_DNS4 = NetworkUtils.numericToInetAddress("8.8.8.8"); 90 private static final InetAddress TEST_DNS6 = NetworkUtils.numericToInetAddress( 91 "2001:4860:4860::8888"); 92 93 // For brevity elsewhere. now()94 private static final long now() { 95 return SystemClock.elapsedRealtime(); 96 } 97 98 // Values from RFC 1035 section 4.1.1, names from <arpa/nameser.h>. 99 // Should be a member of DnsUdpCheck, but "compiler says no". 100 public static enum DnsResponseCode { NOERROR, FORMERR, SERVFAIL, NXDOMAIN, NOTIMP, REFUSED }; 101 102 private final Network mNetwork; 103 private final LinkProperties mLinkProperties; 104 private final Integer mInterfaceIndex; 105 106 private final long mTimeoutMs; 107 private final long mStartTime; 108 private final long mDeadlineTime; 109 110 // A counter, initialized to the total number of measurements, 111 // so callers can wait for completion. 112 private final CountDownLatch mCountDownLatch; 113 114 public class Measurement { 115 private static final String SUCCEEDED = "SUCCEEDED"; 116 private static final String FAILED = "FAILED"; 117 118 private boolean succeeded; 119 120 // Package private. TODO: investigate better encapsulation. 121 String description = ""; 122 long startTime; 123 long finishTime; 124 String result = ""; 125 Thread thread; 126 checkSucceeded()127 public boolean checkSucceeded() { return succeeded; } 128 recordSuccess(String msg)129 void recordSuccess(String msg) { 130 maybeFixupTimes(); 131 succeeded = true; 132 result = SUCCEEDED + ": " + msg; 133 if (mCountDownLatch != null) { 134 mCountDownLatch.countDown(); 135 } 136 } 137 recordFailure(String msg)138 void recordFailure(String msg) { 139 maybeFixupTimes(); 140 succeeded = false; 141 result = FAILED + ": " + msg; 142 if (mCountDownLatch != null) { 143 mCountDownLatch.countDown(); 144 } 145 } 146 maybeFixupTimes()147 private void maybeFixupTimes() { 148 // Allows the caller to just set success/failure and not worry 149 // about also setting the correct finishing time. 150 if (finishTime == 0) { finishTime = now(); } 151 152 // In cases where, for example, a failure has occurred before the 153 // measurement even began, fixup the start time to reflect as much. 154 if (startTime == 0) { startTime = finishTime; } 155 } 156 157 @Override toString()158 public String toString() { 159 return description + ": " + result + " (" + (finishTime - startTime) + "ms)"; 160 } 161 } 162 163 private final Map<InetAddress, Measurement> mIcmpChecks = new HashMap<>(); 164 private final Map<Pair<InetAddress, InetAddress>, Measurement> mExplicitSourceIcmpChecks = 165 new HashMap<>(); 166 private final Map<InetAddress, Measurement> mDnsUdpChecks = new HashMap<>(); 167 private final String mDescription; 168 169 NetworkDiagnostics(Network network, LinkProperties lp, long timeoutMs)170 public NetworkDiagnostics(Network network, LinkProperties lp, long timeoutMs) { 171 mNetwork = network; 172 mLinkProperties = lp; 173 mInterfaceIndex = getInterfaceIndex(mLinkProperties.getInterfaceName()); 174 mTimeoutMs = timeoutMs; 175 mStartTime = now(); 176 mDeadlineTime = mStartTime + mTimeoutMs; 177 178 // Hardcode measurements to TEST_DNS4 and TEST_DNS6 in order to test off-link connectivity. 179 // We are free to modify mLinkProperties with impunity because ConnectivityService passes us 180 // a copy and not the original object. It's easier to do it this way because we don't need 181 // to check whether the LinkProperties already contains these DNS servers because 182 // LinkProperties#addDnsServer checks for duplicates. 183 if (mLinkProperties.isReachable(TEST_DNS4)) { 184 mLinkProperties.addDnsServer(TEST_DNS4); 185 } 186 // TODO: we could use mLinkProperties.isReachable(TEST_DNS6) here, because we won't set any 187 // DNS servers for which isReachable() is false, but since this is diagnostic code, be extra 188 // careful. 189 if (mLinkProperties.hasGlobalIPv6Address() || mLinkProperties.hasIPv6DefaultRoute()) { 190 mLinkProperties.addDnsServer(TEST_DNS6); 191 } 192 193 for (RouteInfo route : mLinkProperties.getRoutes()) { 194 if (route.hasGateway()) { 195 InetAddress gateway = route.getGateway(); 196 prepareIcmpMeasurement(gateway); 197 if (route.isIPv6Default()) { 198 prepareExplicitSourceIcmpMeasurements(gateway); 199 } 200 } 201 } 202 for (InetAddress nameserver : mLinkProperties.getDnsServers()) { 203 prepareIcmpMeasurement(nameserver); 204 prepareDnsMeasurement(nameserver); 205 } 206 207 mCountDownLatch = new CountDownLatch(totalMeasurementCount()); 208 209 startMeasurements(); 210 211 mDescription = "ifaces{" + TextUtils.join(",", mLinkProperties.getAllInterfaceNames()) + "}" 212 + " index{" + mInterfaceIndex + "}" 213 + " network{" + mNetwork + "}" 214 + " nethandle{" + mNetwork.getNetworkHandle() + "}"; 215 } 216 getInterfaceIndex(String ifname)217 private static Integer getInterfaceIndex(String ifname) { 218 try { 219 NetworkInterface ni = NetworkInterface.getByName(ifname); 220 return ni.getIndex(); 221 } catch (NullPointerException | SocketException e) { 222 return null; 223 } 224 } 225 prepareIcmpMeasurement(InetAddress target)226 private void prepareIcmpMeasurement(InetAddress target) { 227 if (!mIcmpChecks.containsKey(target)) { 228 Measurement measurement = new Measurement(); 229 measurement.thread = new Thread(new IcmpCheck(target, measurement)); 230 mIcmpChecks.put(target, measurement); 231 } 232 } 233 prepareExplicitSourceIcmpMeasurements(InetAddress target)234 private void prepareExplicitSourceIcmpMeasurements(InetAddress target) { 235 for (LinkAddress l : mLinkProperties.getLinkAddresses()) { 236 InetAddress source = l.getAddress(); 237 if (source instanceof Inet6Address && l.isGlobalPreferred()) { 238 Pair<InetAddress, InetAddress> srcTarget = new Pair<>(source, target); 239 if (!mExplicitSourceIcmpChecks.containsKey(srcTarget)) { 240 Measurement measurement = new Measurement(); 241 measurement.thread = new Thread(new IcmpCheck(source, target, measurement)); 242 mExplicitSourceIcmpChecks.put(srcTarget, measurement); 243 } 244 } 245 } 246 } 247 prepareDnsMeasurement(InetAddress target)248 private void prepareDnsMeasurement(InetAddress target) { 249 if (!mDnsUdpChecks.containsKey(target)) { 250 Measurement measurement = new Measurement(); 251 measurement.thread = new Thread(new DnsUdpCheck(target, measurement)); 252 mDnsUdpChecks.put(target, measurement); 253 } 254 } 255 totalMeasurementCount()256 private int totalMeasurementCount() { 257 return mIcmpChecks.size() + mExplicitSourceIcmpChecks.size() + mDnsUdpChecks.size(); 258 } 259 startMeasurements()260 private void startMeasurements() { 261 for (Measurement measurement : mIcmpChecks.values()) { 262 measurement.thread.start(); 263 } 264 for (Measurement measurement : mExplicitSourceIcmpChecks.values()) { 265 measurement.thread.start(); 266 } 267 for (Measurement measurement : mDnsUdpChecks.values()) { 268 measurement.thread.start(); 269 } 270 } 271 waitForMeasurements()272 public void waitForMeasurements() { 273 try { 274 mCountDownLatch.await(mDeadlineTime - now(), TimeUnit.MILLISECONDS); 275 } catch (InterruptedException ignored) {} 276 } 277 getMeasurements()278 public List<Measurement> getMeasurements() { 279 // TODO: Consider moving waitForMeasurements() in here to minimize the 280 // chance of caller errors. 281 282 ArrayList<Measurement> measurements = new ArrayList(totalMeasurementCount()); 283 284 // Sort measurements IPv4 first. 285 for (Map.Entry<InetAddress, Measurement> entry : mIcmpChecks.entrySet()) { 286 if (entry.getKey() instanceof Inet4Address) { 287 measurements.add(entry.getValue()); 288 } 289 } 290 for (Map.Entry<Pair<InetAddress, InetAddress>, Measurement> entry : 291 mExplicitSourceIcmpChecks.entrySet()) { 292 if (entry.getKey().first instanceof Inet4Address) { 293 measurements.add(entry.getValue()); 294 } 295 } 296 for (Map.Entry<InetAddress, Measurement> entry : mDnsUdpChecks.entrySet()) { 297 if (entry.getKey() instanceof Inet4Address) { 298 measurements.add(entry.getValue()); 299 } 300 } 301 302 // IPv6 measurements second. 303 for (Map.Entry<InetAddress, Measurement> entry : mIcmpChecks.entrySet()) { 304 if (entry.getKey() instanceof Inet6Address) { 305 measurements.add(entry.getValue()); 306 } 307 } 308 for (Map.Entry<Pair<InetAddress, InetAddress>, Measurement> entry : 309 mExplicitSourceIcmpChecks.entrySet()) { 310 if (entry.getKey().first instanceof Inet6Address) { 311 measurements.add(entry.getValue()); 312 } 313 } 314 for (Map.Entry<InetAddress, Measurement> entry : mDnsUdpChecks.entrySet()) { 315 if (entry.getKey() instanceof Inet6Address) { 316 measurements.add(entry.getValue()); 317 } 318 } 319 320 return measurements; 321 } 322 dump(IndentingPrintWriter pw)323 public void dump(IndentingPrintWriter pw) { 324 pw.println(TAG + ":" + mDescription); 325 final long unfinished = mCountDownLatch.getCount(); 326 if (unfinished > 0) { 327 // This can't happen unless a caller forgets to call waitForMeasurements() 328 // or a measurement isn't implemented to correctly honor the timeout. 329 pw.println("WARNING: countdown wait incomplete: " 330 + unfinished + " unfinished measurements"); 331 } 332 333 pw.increaseIndent(); 334 335 String prefix; 336 for (Measurement m : getMeasurements()) { 337 prefix = m.checkSucceeded() ? "." : "F"; 338 pw.println(prefix + " " + m.toString()); 339 } 340 341 pw.decreaseIndent(); 342 } 343 344 345 private class SimpleSocketCheck implements Closeable { 346 protected final InetAddress mSource; // Usually null. 347 protected final InetAddress mTarget; 348 protected final int mAddressFamily; 349 protected final Measurement mMeasurement; 350 protected FileDescriptor mFileDescriptor; 351 protected SocketAddress mSocketAddress; 352 SimpleSocketCheck( InetAddress source, InetAddress target, Measurement measurement)353 protected SimpleSocketCheck( 354 InetAddress source, InetAddress target, Measurement measurement) { 355 mMeasurement = measurement; 356 357 if (target instanceof Inet6Address) { 358 Inet6Address targetWithScopeId = null; 359 if (target.isLinkLocalAddress() && mInterfaceIndex != null) { 360 try { 361 targetWithScopeId = Inet6Address.getByAddress( 362 null, target.getAddress(), mInterfaceIndex); 363 } catch (UnknownHostException e) { 364 mMeasurement.recordFailure(e.toString()); 365 } 366 } 367 mTarget = (targetWithScopeId != null) ? targetWithScopeId : target; 368 mAddressFamily = AF_INET6; 369 } else { 370 mTarget = target; 371 mAddressFamily = AF_INET; 372 } 373 374 // We don't need to check the scope ID here because we currently only do explicit-source 375 // measurements from global IPv6 addresses. 376 mSource = source; 377 } 378 SimpleSocketCheck(InetAddress target, Measurement measurement)379 protected SimpleSocketCheck(InetAddress target, Measurement measurement) { 380 this(null, target, measurement); 381 } 382 setupSocket( int sockType, int protocol, long writeTimeout, long readTimeout, int dstPort)383 protected void setupSocket( 384 int sockType, int protocol, long writeTimeout, long readTimeout, int dstPort) 385 throws ErrnoException, IOException { 386 final int oldTag = TrafficStats.getAndSetThreadStatsTag(TrafficStats.TAG_SYSTEM_PROBE); 387 try { 388 mFileDescriptor = Os.socket(mAddressFamily, sockType, protocol); 389 } finally { 390 TrafficStats.setThreadStatsTag(oldTag); 391 } 392 // Setting SNDTIMEO is purely for defensive purposes. 393 Os.setsockoptTimeval(mFileDescriptor, 394 SOL_SOCKET, SO_SNDTIMEO, StructTimeval.fromMillis(writeTimeout)); 395 Os.setsockoptTimeval(mFileDescriptor, 396 SOL_SOCKET, SO_RCVTIMEO, StructTimeval.fromMillis(readTimeout)); 397 // TODO: Use IP_RECVERR/IPV6_RECVERR, pending OsContants availability. 398 mNetwork.bindSocket(mFileDescriptor); 399 if (mSource != null) { 400 Os.bind(mFileDescriptor, mSource, 0); 401 } 402 Os.connect(mFileDescriptor, mTarget, dstPort); 403 mSocketAddress = Os.getsockname(mFileDescriptor); 404 } 405 getSocketAddressString()406 protected String getSocketAddressString() { 407 // The default toString() implementation is not the prettiest. 408 InetSocketAddress inetSockAddr = (InetSocketAddress) mSocketAddress; 409 InetAddress localAddr = inetSockAddr.getAddress(); 410 return String.format( 411 (localAddr instanceof Inet6Address ? "[%s]:%d" : "%s:%d"), 412 localAddr.getHostAddress(), inetSockAddr.getPort()); 413 } 414 415 @Override close()416 public void close() { 417 IoUtils.closeQuietly(mFileDescriptor); 418 } 419 } 420 421 422 private class IcmpCheck extends SimpleSocketCheck implements Runnable { 423 private static final int TIMEOUT_SEND = 100; 424 private static final int TIMEOUT_RECV = 300; 425 private static final int PACKET_BUFSIZE = 512; 426 private final int mProtocol; 427 private final int mIcmpType; 428 IcmpCheck(InetAddress source, InetAddress target, Measurement measurement)429 public IcmpCheck(InetAddress source, InetAddress target, Measurement measurement) { 430 super(source, target, measurement); 431 432 if (mAddressFamily == AF_INET6) { 433 mProtocol = IPPROTO_ICMPV6; 434 mIcmpType = NetworkConstants.ICMPV6_ECHO_REQUEST_TYPE; 435 mMeasurement.description = "ICMPv6"; 436 } else { 437 mProtocol = IPPROTO_ICMP; 438 mIcmpType = NetworkConstants.ICMPV4_ECHO_REQUEST_TYPE; 439 mMeasurement.description = "ICMPv4"; 440 } 441 442 mMeasurement.description += " dst{" + mTarget.getHostAddress() + "}"; 443 } 444 IcmpCheck(InetAddress target, Measurement measurement)445 public IcmpCheck(InetAddress target, Measurement measurement) { 446 this(null, target, measurement); 447 } 448 449 @Override run()450 public void run() { 451 // Check if this measurement has already failed during setup. 452 if (mMeasurement.finishTime > 0) { 453 // If the measurement failed during construction it didn't 454 // decrement the countdown latch; do so here. 455 mCountDownLatch.countDown(); 456 return; 457 } 458 459 try { 460 setupSocket(SOCK_DGRAM, mProtocol, TIMEOUT_SEND, TIMEOUT_RECV, 0); 461 } catch (ErrnoException | IOException e) { 462 mMeasurement.recordFailure(e.toString()); 463 return; 464 } 465 mMeasurement.description += " src{" + getSocketAddressString() + "}"; 466 467 // Build a trivial ICMP packet. 468 final byte[] icmpPacket = { 469 (byte) mIcmpType, 0, 0, 0, 0, 0, 0, 0 // ICMP header 470 }; 471 472 int count = 0; 473 mMeasurement.startTime = now(); 474 while (now() < mDeadlineTime - (TIMEOUT_SEND + TIMEOUT_RECV)) { 475 count++; 476 icmpPacket[icmpPacket.length - 1] = (byte) count; 477 try { 478 Os.write(mFileDescriptor, icmpPacket, 0, icmpPacket.length); 479 } catch (ErrnoException | InterruptedIOException e) { 480 mMeasurement.recordFailure(e.toString()); 481 break; 482 } 483 484 try { 485 ByteBuffer reply = ByteBuffer.allocate(PACKET_BUFSIZE); 486 Os.read(mFileDescriptor, reply); 487 // TODO: send a few pings back to back to guesstimate packet loss. 488 mMeasurement.recordSuccess("1/" + count); 489 break; 490 } catch (ErrnoException | InterruptedIOException e) { 491 continue; 492 } 493 } 494 if (mMeasurement.finishTime == 0) { 495 mMeasurement.recordFailure("0/" + count); 496 } 497 498 close(); 499 } 500 } 501 502 503 private class DnsUdpCheck extends SimpleSocketCheck implements Runnable { 504 private static final int TIMEOUT_SEND = 100; 505 private static final int TIMEOUT_RECV = 500; 506 private static final int RR_TYPE_A = 1; 507 private static final int RR_TYPE_AAAA = 28; 508 private static final int PACKET_BUFSIZE = 512; 509 510 private final Random mRandom = new Random(); 511 512 // Should be static, but the compiler mocks our puny, human attempts at reason. responseCodeStr(int rcode)513 private String responseCodeStr(int rcode) { 514 try { 515 return DnsResponseCode.values()[rcode].toString(); 516 } catch (IndexOutOfBoundsException e) { 517 return String.valueOf(rcode); 518 } 519 } 520 521 private final int mQueryType; 522 DnsUdpCheck(InetAddress target, Measurement measurement)523 public DnsUdpCheck(InetAddress target, Measurement measurement) { 524 super(target, measurement); 525 526 // TODO: Ideally, query the target for both types regardless of address family. 527 if (mAddressFamily == AF_INET6) { 528 mQueryType = RR_TYPE_AAAA; 529 } else { 530 mQueryType = RR_TYPE_A; 531 } 532 533 mMeasurement.description = "DNS UDP dst{" + mTarget.getHostAddress() + "}"; 534 } 535 536 @Override run()537 public void run() { 538 // Check if this measurement has already failed during setup. 539 if (mMeasurement.finishTime > 0) { 540 // If the measurement failed during construction it didn't 541 // decrement the countdown latch; do so here. 542 mCountDownLatch.countDown(); 543 return; 544 } 545 546 try { 547 setupSocket(SOCK_DGRAM, IPPROTO_UDP, TIMEOUT_SEND, TIMEOUT_RECV, 548 NetworkConstants.DNS_SERVER_PORT); 549 } catch (ErrnoException | IOException e) { 550 mMeasurement.recordFailure(e.toString()); 551 return; 552 } 553 mMeasurement.description += " src{" + getSocketAddressString() + "}"; 554 555 // This needs to be fixed length so it can be dropped into the pre-canned packet. 556 final String sixRandomDigits = String.valueOf(mRandom.nextInt(900000) + 100000); 557 mMeasurement.description += " qtype{" + mQueryType + "}" 558 + " qname{" + sixRandomDigits + "-android-ds.metric.gstatic.com}"; 559 560 // Build a trivial DNS packet. 561 final byte[] dnsPacket = getDnsQueryPacket(sixRandomDigits); 562 563 int count = 0; 564 mMeasurement.startTime = now(); 565 while (now() < mDeadlineTime - (TIMEOUT_RECV + TIMEOUT_RECV)) { 566 count++; 567 try { 568 Os.write(mFileDescriptor, dnsPacket, 0, dnsPacket.length); 569 } catch (ErrnoException | InterruptedIOException e) { 570 mMeasurement.recordFailure(e.toString()); 571 break; 572 } 573 574 try { 575 ByteBuffer reply = ByteBuffer.allocate(PACKET_BUFSIZE); 576 Os.read(mFileDescriptor, reply); 577 // TODO: more correct and detailed evaluation of the response, 578 // possibly adding the returned IP address(es) to the output. 579 final String rcodeStr = (reply.limit() > 3) 580 ? " " + responseCodeStr((int) (reply.get(3)) & 0x0f) 581 : ""; 582 mMeasurement.recordSuccess("1/" + count + rcodeStr); 583 break; 584 } catch (ErrnoException | InterruptedIOException e) { 585 continue; 586 } 587 } 588 if (mMeasurement.finishTime == 0) { 589 mMeasurement.recordFailure("0/" + count); 590 } 591 592 close(); 593 } 594 getDnsQueryPacket(String sixRandomDigits)595 private byte[] getDnsQueryPacket(String sixRandomDigits) { 596 byte[] rnd = sixRandomDigits.getBytes(StandardCharsets.US_ASCII); 597 return new byte[] { 598 (byte) mRandom.nextInt(), (byte) mRandom.nextInt(), // [0-1] query ID 599 1, 0, // [2-3] flags; byte[2] = 1 for recursion desired (RD). 600 0, 1, // [4-5] QDCOUNT (number of queries) 601 0, 0, // [6-7] ANCOUNT (number of answers) 602 0, 0, // [8-9] NSCOUNT (number of name server records) 603 0, 0, // [10-11] ARCOUNT (number of additional records) 604 17, rnd[0], rnd[1], rnd[2], rnd[3], rnd[4], rnd[5], 605 '-', 'a', 'n', 'd', 'r', 'o', 'i', 'd', '-', 'd', 's', 606 6, 'm', 'e', 't', 'r', 'i', 'c', 607 7, 'g', 's', 't', 'a', 't', 'i', 'c', 608 3, 'c', 'o', 'm', 609 0, // null terminator of FQDN (root TLD) 610 0, (byte) mQueryType, // QTYPE 611 0, 1 // QCLASS, set to 1 = IN (Internet) 612 }; 613 } 614 } 615 } 616