package be.neutrinet.ispng.dns; import be.neutrinet.ispng.util.Zookeeper; import net.killa.kept.KeptList; import net.killa.kept.KeptMap; import org.apache.curator.framework.CuratorFramework; import org.apache.log4j.Logger; import org.apache.zookeeper.CreateMode; import org.apache.zookeeper.ZooDefs; import org.xbill.DNS.*; import java.net.InetAddress; import java.text.SimpleDateFormat; import java.util.*; /** * Created by wannes on 1/26/15. */ public class ZoneBuilder { private final static String PREFIX = "/ispng/dns/"; private final static SimpleDateFormat SERIAL_NUMBER_FORMAT = new SimpleDateFormat("YYYYMMDD"); public static Name SAME_AS_ROOT; static { try { SAME_AS_ROOT = Name.fromString("@"); } catch (Exception ex) { Logger.getLogger(ZoneBuilder.class).error("Something went terribly wrong", ex); } } private KeptMap zones; private Map<String, KeptList<DNSRecord>> records = new HashMap<>(); // TODO persist zone serial number suffix private Map<String, Integer> suffixes = new HashMap<>(); private int ttl; private List<String> nameServers = new ArrayList<>(); private Properties cfg; public void boot(Properties cfg) { this.cfg = cfg; this.ttl = Integer.parseInt(cfg.getProperty("zone.all.record.TTL")); for (String part : cfg.getProperty("zone.all.nameservers").split(";")) { this.nameServers.add(part.trim()); } boot(); } public void boot() { CuratorFramework cf = Zookeeper.get(); try { String dir = "/ispng/dns"; Zookeeper.ensurePathExists(dir); zones = new KeptMap(cf.getZookeeperClient().getZooKeeper(), PREFIX + "zoneMap", ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL); } catch (Exception ex) { Logger.getLogger(getClass()).error("Failed to boot DNS zones", ex); } } public void renderZonesToZK() { try { Map<String, List<DNSRecord>> zs = DNSRecords.buildZones(); zones.clear(); for (Map.Entry<String, List<DNSRecord>> entry : zs.entrySet()) { String label = entry.getKey(); zones.put(label, DNSRecords.labelToZone.get(entry.getKey())); Zookeeper.ensurePathExists(PREFIX + "zones"); KeptList<DNSRecord> kl = new KeptList<>(DNSRecord.class, Zookeeper.getZK(), PREFIX + "zones/" + label, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL); kl.clear(); kl.addAll(entry.getValue()); records.put(label, kl); } } catch (Exception ex) { Logger.getLogger(getClass()).error("Failed to rebuild zone info from DB", ex); } } public Map<Name, Zone> rebuildZones() { HashMap<Name, Zone> zones = new HashMap<>(); try { for (Map.Entry<String, String> entry : this.zones.entrySet()) { KeptList<DNSRecord> kl = new KeptList<>(DNSRecord.class, Zookeeper.getZK(), PREFIX + "zones/" + entry.getKey(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL); zones.put(Name.fromString(entry.getKey()), buildZone(entry.getValue(), kl)); } } catch (Exception ex) { Logger.getLogger(getClass()).error("Failed to rebuild zones", ex); } return zones; } public Zone buildZone(String zoneRoot, List<DNSRecord> DNSRecords) { try { ArrayList<Record> dnsRecords = new ArrayList<>(); Name root = Name.fromString(zoneRoot); int suffix = suffixes.getOrDefault(zoneRoot, 1); int serial = Integer.parseInt(SERIAL_NUMBER_FORMAT.format(new Date()) + String.format("%02d", suffix)); suffixes.put(zoneRoot, suffix + 1); // SOA record dnsRecords.add(buildSOARecord(Name.concatenate(SAME_AS_ROOT, root), serial)); // NS records for (String ns : nameServers) { dnsRecords.add(new NSRecord(Name.concatenate(SAME_AS_ROOT, root), DClass.IN, ttl, ensureFQDN(ns))); } // * records for (DNSRecord r : DNSRecords) dnsRecords.add(buildDNSRecord(root, r)); return new Zone(root, dnsRecords.toArray(new Record[]{})); } catch (Exception ex) { Logger.getLogger(getClass()).error("Failed to build zone " + zoneRoot, ex); } return null; } private SOARecord buildSOARecord(Name name, int serial) throws Exception { return new SOARecord( name, DClass.IN, ttl, ensureFQDN(cfg.getProperty("zone.all.soa.nameserver")), ensureFQDN(cfg.getProperty("zone.all.soa.admin")), serial, Integer.parseInt(cfg.getProperty("zone.all.soa.refresh")), Integer.parseInt(cfg.getProperty("zone.all.soa.retry")), Integer.parseInt(cfg.getProperty("zone.all.soa.expiry")), Integer.parseInt(cfg.getProperty("zone.all.soa.nxdomainTTL")) ); } private Record buildDNSRecord(Name root, DNSRecord record) throws Exception { switch (record.getType()) { case DNSRecord.PTR: return new PTRRecord(Name.fromString(record.getName(), root), DClass.IN, record.getTtl(), ensureFQDN(record.getTarget())); case DNSRecord.NS: return new NSRecord(Name.fromString(record.getName(), root), DClass.IN, record.getTtl(), ensureFQDN(record.getTarget())); case DNSRecord.AAAA: return new AAAARecord(Name.fromString(record.getName(), root), DClass.IN, record.getTtl(), InetAddress.getByName(record.getTarget())); case DNSRecord.A: return new ARecord(Name.fromString(record.getName(), root), DClass.IN, record.getTtl(), InetAddress.getByName(record.getTarget())); default: throw new IllegalArgumentException("Invalid DNS record type"); } } public void addOrUpdate(DNSRecord r, String zoneLabel) { if (r.validate() && DNSRecords.getZoneMapping().containsKey(zoneLabel.toLowerCase())) { DNSRecords.getDaoForZone(zoneLabel).ifPresent((dao) -> { try { dao.createOrUpdate(r); // todo : atm a full zone rebuild is triggered per modification, // obviously that is not so good for performance renderZonesToZK(); } catch (Exception ex) { Logger.getLogger(getClass()).error("Failed to modify dns record " + r.getName() + " in zone " + zoneLabel, ex); } }); } else { throw new IllegalArgumentException("Illegal DNSRecord and/or zone name"); } } public final Name ensureFQDN(String name) { if (!name.endsWith(".")) { name += "."; } try { return Name.fromString(name); } catch (Exception ex) { Logger.getLogger(getClass()).error("Failed to get FQDN from " + name, ex); } return null; } }