com.aoindustries.aoserv.master.dns.DnsService Maven / Gradle / Ivy
Show all versions of aoserv-master Show documentation
/*
* aoserv-master - Master server for the AOServ Platform.
* Copyright (C) 2001-2013, 2015, 2017, 2018, 2019, 2020 AO Industries, Inc.
* [email protected]
* 7262 Bull Pen Cir
* Mobile, AL 36695
*
* This file is part of aoserv-master.
*
* aoserv-master is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* aoserv-master is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with aoserv-master. If not, see .
*/
package com.aoindustries.aoserv.master.dns;
import com.aoindustries.aoserv.client.account.Account;
import com.aoindustries.aoserv.client.dns.Record;
import com.aoindustries.aoserv.client.dns.RecordType;
import com.aoindustries.aoserv.client.dns.Zone;
import com.aoindustries.aoserv.client.dns.ZoneTable;
import com.aoindustries.aoserv.client.master.User;
import com.aoindustries.aoserv.client.net.AppProtocol;
import com.aoindustries.aoserv.client.schema.Table;
import com.aoindustries.aoserv.master.InvalidateList;
import com.aoindustries.aoserv.master.MasterServer;
import com.aoindustries.aoserv.master.MasterService;
import com.aoindustries.aoserv.master.ObjectFactories;
import com.aoindustries.aoserv.master.PackageHandler;
import com.aoindustries.aoserv.master.RequestSource;
import com.aoindustries.collections.IntList;
import com.aoindustries.collections.SortedArrayList;
import com.aoindustries.dbc.DatabaseAccess.Null;
import com.aoindustries.dbc.DatabaseConnection;
import com.aoindustries.net.DomainName;
import com.aoindustries.net.InetAddress;
import com.aoindustries.tlds.TopLevelDomain;
import java.io.IOException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang3.NotImplementedException;
/**
* Handles all the accesses to the DNS tables.
*
* @author AO Industries, Inc.
*/
// TODO: Move Zone-specific stuff into ZoneService
final public class DnsService implements MasterService {
/**
* Creates a new Record
.
*/
public int addRecord(
DatabaseConnection conn,
RequestSource source,
InvalidateList invalidateList,
String zone,
String domain,
String type,
int priority,
int weight,
int port,
String destination,
int ttl
) throws IOException, SQLException {
// Must be allowed to access this zone
checkAccessDNSZone(conn, source, "addRecord", zone);
// Must have appropriate priority
if(conn.executeBooleanQuery("select has_priority from dns.\"RecordType\" where type=?", type)) {
if(priority == Record.NO_PRIORITY) throw new IllegalArgumentException("priority required for type=" + type);
else if(priority<=0) throw new SQLException("Invalid priority: " + priority);
} else {
if(priority != Record.NO_PRIORITY) throw new SQLException("No priority allowed for type="+type);
}
// Must have appropriate weight
if(conn.executeBooleanQuery("select has_weight from dns.\"RecordType\" where type=?", type)) {
if(weight == Record.NO_WEIGHT) throw new IllegalArgumentException("weight required for type=" + type);
else if(weight<=0) throw new SQLException("Invalid weight: " + weight);
} else {
if(weight != Record.NO_WEIGHT) throw new SQLException("No weight allowed for type="+type);
}
// Must have appropriate port
if(conn.executeBooleanQuery("select has_port from dns.\"RecordType\" where type=?", type)) {
if(port == Record.NO_PORT) throw new IllegalArgumentException("port required for type=" + type);
else if(port < 1 || port > 65535) throw new SQLException("Invalid port: " + port);
} else {
if(port != Record.NO_PORT) throw new SQLException("No port allowed for type="+type);
}
// Must have a valid destination type unless is a TXT entry
if(!RecordType.TXT.equals(type)) {
try {
RecordType.checkDestination(
type,
destination
);
} catch(IllegalArgumentException err) {
throw new SQLException("Invalid destination: "+err.getMessage());
}
}
// Add the entry
int record = conn.executeIntUpdate(
"INSERT INTO dns.\"Record\" (\n"
+ " \"zone\",\n"
+ " \"domain\",\n"
+ " \"type\",\n"
+ " priority,\n"
+ " weight,\n"
+ " port,\n"
+ " destination,\n"
+ " ttl\n"
+ ") VALUES (?,?,?,?,?,?,?,?) RETURNING id",
zone,
domain,
type,
(priority == Record.NO_PRIORITY) ? Null.INTEGER : priority,
(weight == Record.NO_WEIGHT) ? Null.INTEGER : weight,
(port == Record.NO_PORT) ? Null.INTEGER : port,
destination,
(ttl == -1) ? Null.INTEGER : ttl
);
invalidateList.addTable(conn, Table.TableID.DNS_RECORDS, InvalidateList.allAccounts, InvalidateList.allHosts, false);
// Update the serial of the zone
updateDNSZoneSerial(conn, invalidateList, zone);
// Notify all clients of the update
return record;
}
/**
* Creates a new Zone
.
*/
@SuppressWarnings("deprecation")
public void addDNSZone(
DatabaseConnection conn,
RequestSource source,
InvalidateList invalidateList,
Account.Name packageName,
String zone,
InetAddress ip,
int ttl
) throws IOException, SQLException {
// Must be allowed to access this package
PackageHandler.checkAccessPackage(conn, source, "addDNSZone", packageName);
if(PackageHandler.isPackageDisabled(conn, packageName)) throw new SQLException("Not allowed to add Zone to disabled Package: "+packageName);
MasterServer.checkAccessHostname(conn, source, "addDNSZone", zone);
// Check the zone format
List tlds=getDNSTLDs(conn);
if(!ZoneTable.checkDNSZone(zone, tlds)) throw new SQLException("Invalid zone: "+zone);
// Must not be allocated in any way to another account
MasterServer.checkAccessHostname(conn, source, "addDNSZone", zone);
// Add the dns_zone entry
conn.executeUpdate(
"insert into dns.\"Zone\" values(?,?,?,?,?,?)",
zone,
zone,
packageName,
Zone.DEFAULT_HOSTMASTER,
Zone.getCurrentSerial(),
ttl
);
// Add the MX entry
conn.executeUpdate(
"insert into dns.\"Record\"(\"zone\", \"domain\", \"type\", priority, destination) values(?,?,?,?,?)",
zone,
"@",
RecordType.MX,
Zone.DEFAULT_MX_PRIORITY,
"mail"
);
final String INSERT_RECORD = "insert into dns.\"Record\"(\"zone\", \"domain\", \"type\", destination) values(?,?,?,?)";
// TODO: Take a "mail exchanger" parameter to properly setup the default MX records.
// If in this domain, sets up SPF like below. If outside this domain (ends in .),
// sets up MX to the mail exchanger, and CNAME "mail" to the mail exchanger.
// TODO: Take nameservers from reseller.Brand
String aType;
switch(ip.getAddressFamily()) {
case INET :
aType = RecordType.A;
break;
case INET6 :
aType = RecordType.AAAA;
break;
default :
throw new AssertionError();
}
conn.executeUpdate(INSERT_RECORD, zone, "@", RecordType.NS, "ns1.aoindustries.com.");
conn.executeUpdate(INSERT_RECORD, zone, "@", RecordType.NS, "ns2.aoindustries.com.");
conn.executeUpdate(INSERT_RECORD, zone, "@", RecordType.NS, "ns3.aoindustries.com.");
conn.executeUpdate(INSERT_RECORD, zone, "@", RecordType.NS, "ns4.aoindustries.com.");
conn.executeUpdate(INSERT_RECORD, zone, "@", RecordType.TXT, "v=spf1 a mx -all");
conn.executeUpdate(INSERT_RECORD, zone, "@", aType, ip.toString());
/*
conn.executeUpdate(INSERT_RECORD, zone, "ftp", aType, ip.toString());
conn.executeUpdate(INSERT_RECORD, zone, "ftp", RecordType.TXT, "v=spf1 -all");
*/
conn.executeUpdate(INSERT_RECORD, zone, "mail", aType, ip.toString());
// See http://www.openspf.org/FAQ/Common_mistakes#helo "Publish SPF records for HELO names used by your mail servers"
conn.executeUpdate(INSERT_RECORD, zone, "mail", RecordType.TXT, "v=spf1 a -all");
conn.executeUpdate(INSERT_RECORD, zone, "www", aType, ip.toString());
// See http://www.openspf.org/FAQ/Common_mistakes#all-domains "Publish null SPF records for your domains that don't send mail"
conn.executeUpdate(INSERT_RECORD, zone, "www", RecordType.TXT, "v=spf1 -all");
// Notify all clients of the update
invalidateList.addTable(conn, Table.TableID.DNS_ZONES, InvalidateList.allAccounts, InvalidateList.allHosts, false);
invalidateList.addTable(conn, Table.TableID.DNS_RECORDS, InvalidateList.allAccounts, InvalidateList.allHosts, false);
}
/**
* Removes a Record
.
*/
public void removeRecord(
DatabaseConnection conn,
RequestSource source,
InvalidateList invalidateList,
int record
) throws IOException, SQLException {
// Must be allowed to access this zone record
checkAccessRecord(conn, source, "removeRecord", record);
// Get the zone associated with the id
String zone=getZoneForRecord(conn, record);
// Remove the dns.Record entry
conn.executeUpdate("delete from dns.\"Record\" where id=?", record);
invalidateList.addTable(conn, Table.TableID.DNS_RECORDS, InvalidateList.allAccounts, InvalidateList.allHosts, false);
// Update the serial of the zone
updateDNSZoneSerial(conn, invalidateList, zone);
}
/**
* Removes a Zone
.
*/
public void removeDNSZone(
DatabaseConnection conn,
RequestSource source,
InvalidateList invalidateList,
String zone
) throws IOException, SQLException {
// Must be allowed to access this zone
checkAccessDNSZone(conn, source, "removeDNSZone", zone);
removeDNSZone(conn, invalidateList, zone);
}
/**
* Removes a Zone
.
*/
public void removeDNSZone(
DatabaseConnection conn,
InvalidateList invalidateList,
String zone
) throws IOException, SQLException {
// Remove the dns.Record entries
conn.executeUpdate("delete from dns.\"Record\" where \"zone\"=?", zone);
// Remove the dns.Zone entry
conn.executeUpdate("delete from dns.\"Zone\" where \"zone\"=?", zone);
// Notify all clients of the update
invalidateList.addTable(conn, Table.TableID.DNS_RECORDS, InvalidateList.allAccounts, InvalidateList.allHosts, false);
invalidateList.addTable(conn, Table.TableID.DNS_ZONES, InvalidateList.allAccounts, InvalidateList.allHosts, false);
}
/**
* Gets the part of the DNS entry before the zone or "@" for the zone itself.
*/
private static String getPreTld(DomainName hostname, DomainName tld) {
String hostnameStr = hostname.toLowerCase();
String tldStr = tld.toLowerCase();
if(hostnameStr.equals(tldStr)) {
return "@";
}
if(!hostnameStr.endsWith("." + tldStr)) {
throw new IllegalArgumentException("hostname not in tld: " + hostname + ", " + tld);
}
String preTld = hostnameStr.substring(0, hostnameStr.length() - ".".length() - tldStr.length());
if(preTld.isEmpty()) throw new IllegalArgumentException("Empty preTld: " + preTld);
return preTld;
}
/* Unused 2018-12-02:
public boolean addRecord(
DatabaseConnection conn,
InvalidateList invalidateList,
DomainName hostname,
InetAddress ipAddress,
List tlds
) throws IOException, SQLException {
DomainName tld = ZoneTable.getHostTLD(hostname, tlds);
String zone = tld + ".";
boolean exists = conn.executeBooleanQuery(
"select (select zone from dns.\"Zone\" where zone=?) is not null",
zone
);
if (exists) {
String preTld = getPreTld(hostname, tld);
exists = conn.executeBooleanQuery(
"select (select id from dns.\"Record\" where \"zone\"=? and \"type\"='A' and \"domain\"=?) is not null",
zone,
preTld
);
if (!exists) {
String aType;
switch(ipAddress.getAddressFamily()) {
case INET :
aType = RecordType.A;
break;
case INET6 :
aType = RecordType.AAAA;
break;
default :
throw new AssertionError();
}
conn.executeUpdate(
"insert into dns.\"Record\" (\"zone\", \"domain\", \"type\", destination) values (?,?,?,?)",
zone,
preTld,
aType,
ipAddress
);
invalidateList.addTable(
conn,
Table.TableID.DNS_RECORDS,
getAccountForDNSZone(conn, zone),
getDnsLinuxServers(conn),
false
);
updateDNSZoneSerial(conn, invalidateList, zone);
return true;
}
}
return false;
}
*/
private static void checkAccessRecord(DatabaseConnection conn, RequestSource source, String action, int record) throws IOException, SQLException {
if(
!isDNSAdmin(conn, source)
&& !PackageHandler.canAccessPackage(conn, source, getPackageForRecord(conn, record))
) {
String message=
"currentAdministrator="
+source.getCurrentAdministrator()
+" is not allowed to access dns_record: action='"
+action
+", id="
+record
;
throw new SQLException(message);
}
}
public boolean canAccessDNSZone(DatabaseConnection conn, RequestSource source, String zone) throws IOException, SQLException {
return
isDNSAdmin(conn, source)
|| PackageHandler.canAccessPackage(conn, source, getPackageForDNSZone(conn, zone))
;
}
private void checkAccessDNSZone(DatabaseConnection conn, RequestSource source, String action, String zone) throws IOException, SQLException {
if(!canAccessDNSZone(conn, source, zone)) {
String message=
"currentAdministrator="
+source.getCurrentAdministrator()
+" is not allowed to access dns_zone: action='"
+action
+", zone='"
+zone
+'\''
;
throw new SQLException(message);
}
}
/**
* Admin access to named info is granted if either no server is restricted, or one of the
* granted servers is a named machine.
*/
private static boolean isDNSAdmin(
DatabaseConnection conn,
RequestSource source
) throws IOException, SQLException {
User mu=MasterServer.getUser(conn, source.getCurrentAdministrator());
return mu!=null && mu.isDNSAdmin();
}
/* Unused 2018-12-02:
public Account.Name getAccountForRecord(DatabaseConnection conn, int record) throws IOException, SQLException {
return conn.executeObjectQuery(
ObjectFactories.accountingCodeFactory,
"select pk.accounting from dns.\"Record\" nr, dns.\"Zone\" nz, billing.\"Package\" pk where nr.\"zone\"=nz.\"zone\" and nz.package=pk.\"name\" and nr.id=?",
record
);
}
*/
private static Account.Name getAccountForDnsZone(DatabaseConnection conn, String zone) throws IOException, SQLException {
return conn.executeObjectQuery(ObjectFactories.accountNameFactory,
"select pk.accounting from dns.\"Zone\" nz, billing.\"Package\" pk where nz.package=pk.name and nz.zone=?",
zone
);
}
private static IntList getDnsLinuxServers(DatabaseConnection conn) throws IOException, SQLException {
return conn.executeIntListQuery("select distinct server from net.\"Bind\" where app_protocol=? and server in (select server from linux.\"Server\")", AppProtocol.DNS);
}
private static final Object dnstldLock=new Object();
private static List dnstldCache;
// TODO: Move to a TopLevelDomainService
/**
* Gets the contents of the dns.TopLevelDomain
table. Please note
* that these are only our manually configured entries, and do not contain the
* full list from {@link TopLevelDomain}.
*
* Also, this is a list of effective top-level domains, for the purposes of
* domain allocation. This means it includes things like com.au
,
* whereas the {@link TopLevelDomain} only includes au
.
*
*
* TODO: Automatically maintain this list from the {@link TopLevelDomain} source, with
* an "auto" flag. Add/remove as-needed when auto.
*
*
* TODO: Have a flag "isRegistrable" that enables/disables a domain as being
* allowed for use by clients. Something marked isRegistrable and auto should never be removed?
* Instead of removing auto entries, have a "removed" timestamp showing when it no longer exists?
*
*
* TODO: Allow a comment on each entry, too.
*
*
* TODO: This could replace ForbiddenZones by adding more specific entries, and marking as isRegistrable=false?
*
*/
public List getDNSTLDs(DatabaseConnection conn) throws IOException, SQLException {
synchronized(dnstldLock) {
if(dnstldCache==null) {
dnstldCache=conn.executeObjectCollectionQuery(
new ArrayList<>(),
ObjectFactories.domainNameFactory,
"select domain from dns.\"TopLevelDomain\""
);
}
return dnstldCache;
}
}
private static String getZoneForRecord(DatabaseConnection conn, int record) throws IOException, SQLException {
return conn.executeStringQuery("select \"zone\" from dns.\"Record\" where id=?", record);
}
public boolean isDNSZoneAvailable(DatabaseConnection conn, String zone) throws IOException, SQLException {
return conn.executeBooleanQuery("select (select zone from dns.\"Zone\" where zone=?) is null", zone);
}
private static Account.Name getPackageForRecord(DatabaseConnection conn, int record) throws IOException, SQLException {
return conn.executeObjectQuery(ObjectFactories.accountNameFactory,
"select nz.package from dns.\"Record\" nr, dns.\"Zone\" nz where nr.id=? and nr.\"zone\"=nz.\"zone\"",
record
);
}
private static Account.Name getPackageForDNSZone(DatabaseConnection conn, String zone) throws IOException, SQLException {
return conn.executeObjectQuery(ObjectFactories.accountNameFactory,
"select package from dns.\"Zone\" where zone=?",
zone
);
}
public void invalidateTable(Table.TableID tableID) {
switch(tableID) {
case DNS_TLDS :
synchronized(dnstldLock) {
dnstldCache=null;
}
break;
}
}
// TODO: Manage SPF records here
public void removeUnusedDNSRecord(
DatabaseConnection conn,
InvalidateList invalidateList,
DomainName hostname,
List tlds
) throws IOException, SQLException {
if(conn.executeBooleanQuery("select (select id from web.\"VirtualHostName\" where hostname=? limit 1) is null", hostname)) {
DomainName tld = ZoneTable.getHostTLD(hostname, tlds);
String zone = tld + ".";
if(conn.executeBooleanQuery("select (select zone from dns.\"Zone\" where zone=?) is not null", zone)) {
String preTld = getPreTld(hostname, tld);
int deleteCount = conn.executeUpdate(
"delete from dns.\"Record\" where\n"
+ " \"zone\"=?\n"
+ " and \"type\" in (?,?)\n"
+ " and \"domain\"=?",
zone,
RecordType.A, RecordType.AAAA,
preTld
);
if(deleteCount > 0) {
invalidateList.addTable(
conn,
Table.TableID.DNS_RECORDS,
getAccountForDnsZone(conn, zone),
getDnsLinuxServers(conn),
false
);
updateDNSZoneSerial(conn, invalidateList, zone);
}
}
}
}
/**
* Sets the default TTL for a Zone
.
*/
public void setDNSZoneTTL(
DatabaseConnection conn,
RequestSource source,
InvalidateList invalidateList,
String zone,
int ttl
) throws IOException, SQLException {
// Must be allowed to access this zone
checkAccessDNSZone(conn, source, "setDNSZoneTTL", zone);
if (ttl <= 0 || ttl > 24*60*60) {
throw new SQLException("Illegal TTL value: "+ttl);
}
conn.executeUpdate("update dns.\"Zone\" set ttl=? where zone=?", ttl, zone);
invalidateList.addTable(
conn,
Table.TableID.DNS_ZONES,
getAccountForDnsZone(conn, zone),
getDnsLinuxServers(conn),
false
);
updateDNSZoneSerial(conn, invalidateList, zone);
}
public void updateDhcpDnsRecords(
DatabaseConnection conn,
InvalidateList invalidateList,
int dhcpAddress,
InetAddress destination
) throws IOException, SQLException {
// Find the ids of the entries that should be changed
IntList records = conn.executeIntListQuery("select id from dns.\"Record\" where \"dhcpAddress\"=?", dhcpAddress);
// Build a list of affected zones
List zones=new SortedArrayList<>();
for(int c=0;c=todaySerial) {
// If so, just increment by one
serial++;
} else {
// Otherwise, set it to today with daily of 01
serial=todaySerial;
}
// Place the serial back in the database
conn.executeUpdate("update dns.\"Zone\" set serial=? where zone=?", serial, zone);
invalidateList.addTable(conn,
Table.TableID.DNS_ZONES,
InvalidateList.allAccounts,
InvalidateList.allHosts,
false
);
}
@SuppressWarnings("deprecation")
public void updateReverseDnsIfExists(
DatabaseConnection conn,
InvalidateList invalidateList,
InetAddress ip,
DomainName hostname
) throws IOException, SQLException {
switch(ip.getAddressFamily()) {
case INET : {
final String netmask;
final String ipStr = ip.toString();
if(
ipStr.startsWith("66.160.183.")
|| ipStr.startsWith("64.62.174.")
) {
netmask = "255.255.255.0";
} else if(ipStr.startsWith("64.71.144.")) {
netmask = "255.255.255.128";
} else {
netmask = null;
}
if(netmask!=null) {
String arpaZone=Zone.getArpaZoneForIPAddress(ip, netmask);
if(
conn.executeBooleanQuery(
"select (select zone from dns.\"Zone\" where zone=?) is not null",
arpaZone
)
) {
int pos=ipStr.lastIndexOf('.');
String oct4=ipStr.substring(pos+1);
if(
conn.executeBooleanQuery(
"select (select id from dns.\"Record\" where \"zone\"=? and \"domain\"=? and \"type\"=? limit 1) is not null",
arpaZone,
oct4,
RecordType.PTR
)
) {
updateDNSZoneSerial(conn, invalidateList, arpaZone);
conn.executeUpdate(
"update dns.\"Record\" set destination=? where \"zone\"=? and \"domain\"=? and \"type\"=?",
hostname.toString()+'.',
arpaZone,
oct4,
RecordType.PTR
);
invalidateList.addTable(conn, Table.TableID.DNS_RECORDS, InvalidateList.allAccounts, InvalidateList.allHosts, false);
}
}
}
break;
}
case INET6 :
throw new NotImplementedException();
default :
throw new AssertionError();
}
}
}