1 // Copyright (c) 1999-2004 Brian Wellington (bwelling (at) xbill.org) 2 3 package org.xbill.DNS; 4 5 import java.io.*; 6 import java.util.*; 7 8 /** 9 * A cache of DNS records. The cache obeys TTLs, so items are purged after 10 * their validity period is complete. Negative answers are cached, to 11 * avoid repeated failed DNS queries. The credibility of each RRset is 12 * maintained, so that more credible records replace less credible records, 13 * and lookups can specify the minimum credibility of data they are requesting. 14 * @see RRset 15 * @see Credibility 16 * 17 * @author Brian Wellington 18 */ 19 20 public class Cache { 21 22 private interface Element { 23 public boolean expired(); 24 public int compareCredibility(int cred); 25 public int getType(); 26 } 27 28 private static int 29 limitExpire(long ttl, long maxttl) { 30 if (maxttl >= 0 && maxttl < ttl) 31 ttl = maxttl; 32 long expire = (System.currentTimeMillis() / 1000) + ttl; 33 if (expire < 0 || expire > Integer.MAX_VALUE) 34 return Integer.MAX_VALUE; 35 return (int)expire; 36 } 37 38 private static class CacheRRset extends RRset implements Element { 39 private static final long serialVersionUID = 5971755205903597024L; 40 41 int credibility; 42 int expire; 43 44 public 45 CacheRRset(Record rec, int cred, long maxttl) { 46 super(); 47 this.credibility = cred; 48 this.expire = limitExpire(rec.getTTL(), maxttl); 49 addRR(rec); 50 } 51 52 public 53 CacheRRset(RRset rrset, int cred, long maxttl) { 54 super(rrset); 55 this.credibility = cred; 56 this.expire = limitExpire(rrset.getTTL(), maxttl); 57 } 58 59 public final boolean 60 expired() { 61 int now = (int)(System.currentTimeMillis() / 1000); 62 return (now >= expire); 63 } 64 65 public final int 66 compareCredibility(int cred) { 67 return credibility - cred; 68 } 69 70 public String 71 toString() { 72 StringBuffer sb = new StringBuffer(); 73 sb.append(super.toString()); 74 sb.append(" cl = "); 75 sb.append(credibility); 76 return sb.toString(); 77 } 78 } 79 80 private static class NegativeElement implements Element { 81 int type; 82 Name name; 83 int credibility; 84 int expire; 85 86 public 87 NegativeElement(Name name, int type, SOARecord soa, int cred, 88 long maxttl) 89 { 90 this.name = name; 91 this.type = type; 92 long cttl = 0; 93 if (soa != null) 94 cttl = soa.getMinimum(); 95 this.credibility = cred; 96 this.expire = limitExpire(cttl, maxttl); 97 } 98 99 public int 100 getType() { 101 return type; 102 } 103 104 public final boolean 105 expired() { 106 int now = (int)(System.currentTimeMillis() / 1000); 107 return (now >= expire); 108 } 109 110 public final int 111 compareCredibility(int cred) { 112 return credibility - cred; 113 } 114 115 public String 116 toString() { 117 StringBuffer sb = new StringBuffer(); 118 if (type == 0) 119 sb.append("NXDOMAIN " + name); 120 else 121 sb.append("NXRRSET " + name + " " + Type.string(type)); 122 sb.append(" cl = "); 123 sb.append(credibility); 124 return sb.toString(); 125 } 126 } 127 128 private static class CacheMap extends LinkedHashMap { 129 private int maxsize = -1; 130 131 CacheMap(int maxsize) { 132 super(16, (float) 0.75, true); 133 this.maxsize = maxsize; 134 } 135 136 int 137 getMaxSize() { 138 return maxsize; 139 } 140 141 void 142 setMaxSize(int maxsize) { 143 /* 144 * Note that this doesn't shrink the size of the map if 145 * the maximum size is lowered, but it should shrink as 146 * entries expire. 147 */ 148 this.maxsize = maxsize; 149 } 150 151 protected boolean removeEldestEntry(Map.Entry eldest) { 152 return maxsize >= 0 && size() > maxsize; 153 } 154 } 155 156 private CacheMap data; 157 private int maxncache = -1; 158 private int maxcache = -1; 159 private int dclass; 160 161 private static final int defaultMaxEntries = 50000; 162 163 /** 164 * Creates an empty Cache 165 * 166 * @param dclass The DNS class of this cache 167 * @see DClass 168 */ 169 public 170 Cache(int dclass) { 171 this.dclass = dclass; 172 data = new CacheMap(defaultMaxEntries); 173 } 174 175 /** 176 * Creates an empty Cache for class IN. 177 * @see DClass 178 */ 179 public 180 Cache() { 181 this(DClass.IN); 182 } 183 184 /** 185 * Creates a Cache which initially contains all records in the specified file. 186 */ 187 public 188 Cache(String file) throws IOException { 189 data = new CacheMap(defaultMaxEntries); 190 Master m = new Master(file); 191 Record record; 192 while ((record = m.nextRecord()) != null) 193 addRecord(record, Credibility.HINT, m); 194 } 195 196 private synchronized Object 197 exactName(Name name) { 198 return data.get(name); 199 } 200 201 private synchronized void 202 removeName(Name name) { 203 data.remove(name); 204 } 205 206 private synchronized Element [] 207 allElements(Object types) { 208 if (types instanceof List) { 209 List typelist = (List) types; 210 int size = typelist.size(); 211 return (Element []) typelist.toArray(new Element[size]); 212 } else { 213 Element set = (Element) types; 214 return new Element[] {set}; 215 } 216 } 217 218 private synchronized Element 219 oneElement(Name name, Object types, int type, int minCred) { 220 Element found = null; 221 222 if (type == Type.ANY) 223 throw new IllegalArgumentException("oneElement(ANY)"); 224 if (types instanceof List) { 225 List list = (List) types; 226 for (int i = 0; i < list.size(); i++) { 227 Element set = (Element) list.get(i); 228 if (set.getType() == type) { 229 found = set; 230 break; 231 } 232 } 233 } else { 234 Element set = (Element) types; 235 if (set.getType() == type) 236 found = set; 237 } 238 if (found == null) 239 return null; 240 if (found.expired()) { 241 removeElement(name, type); 242 return null; 243 } 244 if (found.compareCredibility(minCred) < 0) 245 return null; 246 return found; 247 } 248 249 private synchronized Element 250 findElement(Name name, int type, int minCred) { 251 Object types = exactName(name); 252 if (types == null) 253 return null; 254 return oneElement(name, types, type, minCred); 255 } 256 257 private synchronized void 258 addElement(Name name, Element element) { 259 Object types = data.get(name); 260 if (types == null) { 261 data.put(name, element); 262 return; 263 } 264 int type = element.getType(); 265 if (types instanceof List) { 266 List list = (List) types; 267 for (int i = 0; i < list.size(); i++) { 268 Element elt = (Element) list.get(i); 269 if (elt.getType() == type) { 270 list.set(i, element); 271 return; 272 } 273 } 274 list.add(element); 275 } else { 276 Element elt = (Element) types; 277 if (elt.getType() == type) 278 data.put(name, element); 279 else { 280 LinkedList list = new LinkedList(); 281 list.add(elt); 282 list.add(element); 283 data.put(name, list); 284 } 285 } 286 } 287 288 private synchronized void 289 removeElement(Name name, int type) { 290 Object types = data.get(name); 291 if (types == null) { 292 return; 293 } 294 if (types instanceof List) { 295 List list = (List) types; 296 for (int i = 0; i < list.size(); i++) { 297 Element elt = (Element) list.get(i); 298 if (elt.getType() == type) { 299 list.remove(i); 300 if (list.size() == 0) 301 data.remove(name); 302 return; 303 } 304 } 305 } else { 306 Element elt = (Element) types; 307 if (elt.getType() != type) 308 return; 309 data.remove(name); 310 } 311 } 312 313 /** Empties the Cache. */ 314 public synchronized void 315 clearCache() { 316 data.clear(); 317 } 318 319 /** 320 * Adds a record to the Cache. 321 * @param r The record to be added 322 * @param cred The credibility of the record 323 * @param o The source of the record (this could be a Message, for example) 324 * @see Record 325 */ 326 public synchronized void 327 addRecord(Record r, int cred, Object o) { 328 Name name = r.getName(); 329 int type = r.getRRsetType(); 330 if (!Type.isRR(type)) 331 return; 332 Element element = findElement(name, type, cred); 333 if (element == null) { 334 CacheRRset crrset = new CacheRRset(r, cred, maxcache); 335 addRRset(crrset, cred); 336 } else if (element.compareCredibility(cred) == 0) { 337 if (element instanceof CacheRRset) { 338 CacheRRset crrset = (CacheRRset) element; 339 crrset.addRR(r); 340 } 341 } 342 } 343 344 /** 345 * Adds an RRset to the Cache. 346 * @param rrset The RRset to be added 347 * @param cred The credibility of these records 348 * @see RRset 349 */ 350 public synchronized void 351 addRRset(RRset rrset, int cred) { 352 long ttl = rrset.getTTL(); 353 Name name = rrset.getName(); 354 int type = rrset.getType(); 355 Element element = findElement(name, type, 0); 356 if (ttl == 0) { 357 if (element != null && element.compareCredibility(cred) <= 0) 358 removeElement(name, type); 359 } else { 360 if (element != null && element.compareCredibility(cred) <= 0) 361 element = null; 362 if (element == null) { 363 CacheRRset crrset; 364 if (rrset instanceof CacheRRset) 365 crrset = (CacheRRset) rrset; 366 else 367 crrset = new CacheRRset(rrset, cred, maxcache); 368 addElement(name, crrset); 369 } 370 } 371 } 372 373 /** 374 * Adds a negative entry to the Cache. 375 * @param name The name of the negative entry 376 * @param type The type of the negative entry 377 * @param soa The SOA record to add to the negative cache entry, or null. 378 * The negative cache ttl is derived from the SOA. 379 * @param cred The credibility of the negative entry 380 */ 381 public synchronized void 382 addNegative(Name name, int type, SOARecord soa, int cred) { 383 long ttl = 0; 384 if (soa != null) 385 ttl = soa.getTTL(); 386 Element element = findElement(name, type, 0); 387 if (ttl == 0) { 388 if (element != null && element.compareCredibility(cred) <= 0) 389 removeElement(name, type); 390 } else { 391 if (element != null && element.compareCredibility(cred) <= 0) 392 element = null; 393 if (element == null) 394 addElement(name, new NegativeElement(name, type, 395 soa, cred, 396 maxncache)); 397 } 398 } 399 400 /** 401 * Finds all matching sets or something that causes the lookup to stop. 402 */ 403 protected synchronized SetResponse 404 lookup(Name name, int type, int minCred) { 405 int labels; 406 int tlabels; 407 Element element; 408 Name tname; 409 Object types; 410 SetResponse sr; 411 412 labels = name.labels(); 413 414 for (tlabels = labels; tlabels >= 1; tlabels--) { 415 boolean isRoot = (tlabels == 1); 416 boolean isExact = (tlabels == labels); 417 418 if (isRoot) 419 tname = Name.root; 420 else if (isExact) 421 tname = name; 422 else 423 tname = new Name(name, labels - tlabels); 424 425 types = data.get(tname); 426 if (types == null) 427 continue; 428 429 /* 430 * If this is the name, look for the actual type or a CNAME 431 * (unless it's an ANY query, where we return everything). 432 * Otherwise, look for a DNAME. 433 */ 434 if (isExact && type == Type.ANY) { 435 sr = new SetResponse(SetResponse.SUCCESSFUL); 436 Element [] elements = allElements(types); 437 int added = 0; 438 for (int i = 0; i < elements.length; i++) { 439 element = elements[i]; 440 if (element.expired()) { 441 removeElement(tname, element.getType()); 442 continue; 443 } 444 if (!(element instanceof CacheRRset)) 445 continue; 446 if (element.compareCredibility(minCred) < 0) 447 continue; 448 sr.addRRset((CacheRRset)element); 449 added++; 450 } 451 /* There were positive entries */ 452 if (added > 0) 453 return sr; 454 } else if (isExact) { 455 element = oneElement(tname, types, type, minCred); 456 if (element != null && 457 element instanceof CacheRRset) 458 { 459 sr = new SetResponse(SetResponse.SUCCESSFUL); 460 sr.addRRset((CacheRRset) element); 461 return sr; 462 } else if (element != null) { 463 sr = new SetResponse(SetResponse.NXRRSET); 464 return sr; 465 } 466 467 element = oneElement(tname, types, Type.CNAME, minCred); 468 if (element != null && 469 element instanceof CacheRRset) 470 { 471 return new SetResponse(SetResponse.CNAME, 472 (CacheRRset) element); 473 } 474 } else { 475 element = oneElement(tname, types, Type.DNAME, minCred); 476 if (element != null && 477 element instanceof CacheRRset) 478 { 479 return new SetResponse(SetResponse.DNAME, 480 (CacheRRset) element); 481 } 482 } 483 484 /* Look for an NS */ 485 element = oneElement(tname, types, Type.NS, minCred); 486 if (element != null && element instanceof CacheRRset) 487 return new SetResponse(SetResponse.DELEGATION, 488 (CacheRRset) element); 489 490 /* Check for the special NXDOMAIN element. */ 491 if (isExact) { 492 element = oneElement(tname, types, 0, minCred); 493 if (element != null) 494 return SetResponse.ofType(SetResponse.NXDOMAIN); 495 } 496 497 } 498 return SetResponse.ofType(SetResponse.UNKNOWN); 499 } 500 501 /** 502 * Looks up Records in the Cache. This follows CNAMEs and handles negatively 503 * cached data. 504 * @param name The name to look up 505 * @param type The type to look up 506 * @param minCred The minimum acceptable credibility 507 * @return A SetResponse object 508 * @see SetResponse 509 * @see Credibility 510 */ 511 public SetResponse 512 lookupRecords(Name name, int type, int minCred) { 513 return lookup(name, type, minCred); 514 } 515 516 private RRset [] 517 findRecords(Name name, int type, int minCred) { 518 SetResponse cr = lookupRecords(name, type, minCred); 519 if (cr.isSuccessful()) 520 return cr.answers(); 521 else 522 return null; 523 } 524 525 /** 526 * Looks up credible Records in the Cache (a wrapper around lookupRecords). 527 * Unlike lookupRecords, this given no indication of why failure occurred. 528 * @param name The name to look up 529 * @param type The type to look up 530 * @return An array of RRsets, or null 531 * @see Credibility 532 */ 533 public RRset [] 534 findRecords(Name name, int type) { 535 return findRecords(name, type, Credibility.NORMAL); 536 } 537 538 /** 539 * Looks up Records in the Cache (a wrapper around lookupRecords). Unlike 540 * lookupRecords, this given no indication of why failure occurred. 541 * @param name The name to look up 542 * @param type The type to look up 543 * @return An array of RRsets, or null 544 * @see Credibility 545 */ 546 public RRset [] 547 findAnyRecords(Name name, int type) { 548 return findRecords(name, type, Credibility.GLUE); 549 } 550 551 private final int 552 getCred(int section, boolean isAuth) { 553 if (section == Section.ANSWER) { 554 if (isAuth) 555 return Credibility.AUTH_ANSWER; 556 else 557 return Credibility.NONAUTH_ANSWER; 558 } else if (section == Section.AUTHORITY) { 559 if (isAuth) 560 return Credibility.AUTH_AUTHORITY; 561 else 562 return Credibility.NONAUTH_AUTHORITY; 563 } else if (section == Section.ADDITIONAL) { 564 return Credibility.ADDITIONAL; 565 } else 566 throw new IllegalArgumentException("getCred: invalid section"); 567 } 568 569 private static void 570 markAdditional(RRset rrset, Set names) { 571 Record first = rrset.first(); 572 if (first.getAdditionalName() == null) 573 return; 574 575 Iterator it = rrset.rrs(); 576 while (it.hasNext()) { 577 Record r = (Record) it.next(); 578 Name name = r.getAdditionalName(); 579 if (name != null) 580 names.add(name); 581 } 582 } 583 584 /** 585 * Adds all data from a Message into the Cache. Each record is added with 586 * the appropriate credibility, and negative answers are cached as such. 587 * @param in The Message to be added 588 * @return A SetResponse that reflects what would be returned from a cache 589 * lookup, or null if nothing useful could be cached from the message. 590 * @see Message 591 */ 592 public SetResponse 593 addMessage(Message in) { 594 boolean isAuth = in.getHeader().getFlag(Flags.AA); 595 Record question = in.getQuestion(); 596 Name qname; 597 Name curname; 598 int qtype; 599 int qclass; 600 int cred; 601 int rcode = in.getHeader().getRcode(); 602 boolean completed = false; 603 RRset [] answers, auth, addl; 604 SetResponse response = null; 605 boolean verbose = Options.check("verbosecache"); 606 HashSet additionalNames; 607 608 if ((rcode != Rcode.NOERROR && rcode != Rcode.NXDOMAIN) || 609 question == null) 610 return null; 611 612 qname = question.getName(); 613 qtype = question.getType(); 614 qclass = question.getDClass(); 615 616 curname = qname; 617 618 additionalNames = new HashSet(); 619 620 answers = in.getSectionRRsets(Section.ANSWER); 621 for (int i = 0; i < answers.length; i++) { 622 if (answers[i].getDClass() != qclass) 623 continue; 624 int type = answers[i].getType(); 625 Name name = answers[i].getName(); 626 cred = getCred(Section.ANSWER, isAuth); 627 if ((type == qtype || qtype == Type.ANY) && 628 name.equals(curname)) 629 { 630 addRRset(answers[i], cred); 631 completed = true; 632 if (curname == qname) { 633 if (response == null) 634 response = new SetResponse( 635 SetResponse.SUCCESSFUL); 636 response.addRRset(answers[i]); 637 } 638 markAdditional(answers[i], additionalNames); 639 } else if (type == Type.CNAME && name.equals(curname)) { 640 CNAMERecord cname; 641 addRRset(answers[i], cred); 642 if (curname == qname) 643 response = new SetResponse(SetResponse.CNAME, 644 answers[i]); 645 cname = (CNAMERecord) answers[i].first(); 646 curname = cname.getTarget(); 647 } else if (type == Type.DNAME && curname.subdomain(name)) { 648 DNAMERecord dname; 649 addRRset(answers[i], cred); 650 if (curname == qname) 651 response = new SetResponse(SetResponse.DNAME, 652 answers[i]); 653 dname = (DNAMERecord) answers[i].first(); 654 try { 655 curname = curname.fromDNAME(dname); 656 } 657 catch (NameTooLongException e) { 658 break; 659 } 660 } 661 } 662 663 auth = in.getSectionRRsets(Section.AUTHORITY); 664 RRset soa = null, ns = null; 665 for (int i = 0; i < auth.length; i++) { 666 if (auth[i].getType() == Type.SOA && 667 curname.subdomain(auth[i].getName())) 668 soa = auth[i]; 669 else if (auth[i].getType() == Type.NS && 670 curname.subdomain(auth[i].getName())) 671 ns = auth[i]; 672 } 673 if (!completed) { 674 /* This is a negative response or a referral. */ 675 int cachetype = (rcode == Rcode.NXDOMAIN) ? 0 : qtype; 676 if (rcode == Rcode.NXDOMAIN || soa != null || ns == null) { 677 /* Negative response */ 678 cred = getCred(Section.AUTHORITY, isAuth); 679 SOARecord soarec = null; 680 if (soa != null) 681 soarec = (SOARecord) soa.first(); 682 addNegative(curname, cachetype, soarec, cred); 683 if (response == null) { 684 int responseType; 685 if (rcode == Rcode.NXDOMAIN) 686 responseType = SetResponse.NXDOMAIN; 687 else 688 responseType = SetResponse.NXRRSET; 689 response = SetResponse.ofType(responseType); 690 } 691 /* DNSSEC records are not cached. */ 692 } else { 693 /* Referral response */ 694 cred = getCred(Section.AUTHORITY, isAuth); 695 addRRset(ns, cred); 696 markAdditional(ns, additionalNames); 697 if (response == null) 698 response = new SetResponse( 699 SetResponse.DELEGATION, 700 ns); 701 } 702 } else if (rcode == Rcode.NOERROR && ns != null) { 703 /* Cache the NS set from a positive response. */ 704 cred = getCred(Section.AUTHORITY, isAuth); 705 addRRset(ns, cred); 706 markAdditional(ns, additionalNames); 707 } 708 709 addl = in.getSectionRRsets(Section.ADDITIONAL); 710 for (int i = 0; i < addl.length; i++) { 711 int type = addl[i].getType(); 712 if (type != Type.A && type != Type.AAAA && type != Type.A6) 713 continue; 714 Name name = addl[i].getName(); 715 if (!additionalNames.contains(name)) 716 continue; 717 cred = getCred(Section.ADDITIONAL, isAuth); 718 addRRset(addl[i], cred); 719 } 720 if (verbose) 721 System.out.println("addMessage: " + response); 722 return (response); 723 } 724 725 /** 726 * Flushes an RRset from the cache 727 * @param name The name of the records to be flushed 728 * @param type The type of the records to be flushed 729 * @see RRset 730 */ 731 public void 732 flushSet(Name name, int type) { 733 removeElement(name, type); 734 } 735 736 /** 737 * Flushes all RRsets with a given name from the cache 738 * @param name The name of the records to be flushed 739 * @see RRset 740 */ 741 public void 742 flushName(Name name) { 743 removeName(name); 744 } 745 746 /** 747 * Sets the maximum length of time that a negative response will be stored 748 * in this Cache. A negative value disables this feature (that is, sets 749 * no limit). 750 */ 751 public void 752 setMaxNCache(int seconds) { 753 maxncache = seconds; 754 } 755 756 /** 757 * Gets the maximum length of time that a negative response will be stored 758 * in this Cache. A negative value indicates no limit. 759 */ 760 public int 761 getMaxNCache() { 762 return maxncache; 763 } 764 765 /** 766 * Sets the maximum length of time that records will be stored in this 767 * Cache. A negative value disables this feature (that is, sets no limit). 768 */ 769 public void 770 setMaxCache(int seconds) { 771 maxcache = seconds; 772 } 773 774 /** 775 * Gets the maximum length of time that records will be stored 776 * in this Cache. A negative value indicates no limit. 777 */ 778 public int 779 getMaxCache() { 780 return maxcache; 781 } 782 783 /** 784 * Gets the current number of entries in the Cache, where an entry consists 785 * of all records with a specific Name. 786 */ 787 public int 788 getSize() { 789 return data.size(); 790 } 791 792 /** 793 * Gets the maximum number of entries in the Cache, where an entry consists 794 * of all records with a specific Name. A negative value is treated as an 795 * infinite limit. 796 */ 797 public int 798 getMaxEntries() { 799 return data.getMaxSize(); 800 } 801 802 /** 803 * Sets the maximum number of entries in the Cache, where an entry consists 804 * of all records with a specific Name. A negative value is treated as an 805 * infinite limit. 806 * 807 * Note that setting this to a value lower than the current number 808 * of entries will not cause the Cache to shrink immediately. 809 * 810 * The default maximum number of entries is 50000. 811 * 812 * @param entries The maximum number of entries in the Cache. 813 */ 814 public void 815 setMaxEntries(int entries) { 816 data.setMaxSize(entries); 817 } 818 819 /** 820 * Returns the DNS class of this cache. 821 */ 822 public int 823 getDClass() { 824 return dclass; 825 } 826 827 /** 828 * Returns the contents of the Cache as a string. 829 */ 830 public String 831 toString() { 832 StringBuffer sb = new StringBuffer(); 833 synchronized (this) { 834 Iterator it = data.values().iterator(); 835 while (it.hasNext()) { 836 Element [] elements = allElements(it.next()); 837 for (int i = 0; i < elements.length; i++) { 838 sb.append(elements[i]); 839 sb.append("\n"); 840 } 841 } 842 } 843 return sb.toString(); 844 } 845 846 } 847