-- The domains the router's name server (PowerDNS, gpgsql backend) is the primary of (vpnrouter_dns), in the -- vpn-router-dns database, whose domains, records and domainmetadata tables are PowerDNS's own schema. -- The whole file runs as one transaction, serialized by an advisory lock, so that two processes can load it at once. SELECT pg_advisory_xact_lock(hashtext('vpn-router dns schema')); DROP TABLE IF EXISTS dns_manual_records; -- A domain the router serves as its primary name server; sites says whether the host names of the enabled sites in -- it get records, ttl is the TTL of its records, null for dns.ttl. CREATE TABLE IF NOT EXISTS dns_domains ( name text PRIMARY KEY, sites boolean NOT NULL, ttl integer ); -- A secondary name server of a domain: its NS name and the address it transfers the domain from. CREATE TABLE IF NOT EXISTS dns_secondaries ( domain text NOT NULL REFERENCES dns_domains (name) ON DELETE CASCADE, nameserver text NOT NULL, address inet NOT NULL, PRIMARY KEY (domain, nameserver) ); -- A record an administrator added to a domain, besides those of the domain itself and of the sites. CREATE TABLE IF NOT EXISTS dns_records ( domain text NOT NULL REFERENCES dns_domains (name) ON DELETE CASCADE, name text NOT NULL, type text NOT NULL, content text NOT NULL, ttl integer NOT NULL, PRIMARY KEY (domain, name, type, content) ); -- The digest of the records last written to a zone and the serial of its SOA. CREATE TABLE IF NOT EXISTS dns_state ( zone text PRIMARY KEY, digest text NOT NULL, serial bigint NOT NULL ); CREATE OR REPLACE FUNCTION dns_domains_list() RETURNS TABLE (name text, sites boolean, ttl integer) LANGUAGE sql STABLE AS $$ SELECT name, sites, ttl FROM dns_domains ORDER BY name; $$; -- Add the domain, or set its sites and TTL. CREATE OR REPLACE PROCEDURE dns_domain_put(p_name text, p_sites boolean, p_ttl integer) LANGUAGE sql AS $$ INSERT INTO dns_domains VALUES (p_name, p_sites, p_ttl) ON CONFLICT (name) DO UPDATE SET sites = p_sites, ttl = p_ttl; $$; -- Remove the domain with its secondaries and records. CREATE OR REPLACE PROCEDURE dns_domain_remove(p_name text) LANGUAGE sql AS $$ DELETE FROM dns_domains WHERE name = p_name; $$; CREATE OR REPLACE FUNCTION dns_secondaries_list() RETURNS TABLE (domain text, nameserver text, address text) LANGUAGE sql STABLE AS $$ SELECT domain, nameserver, host(address) FROM dns_secondaries ORDER BY domain, nameserver; $$; -- Add the secondary of the domain, or set its address. CREATE OR REPLACE PROCEDURE dns_secondary_put(p_domain text, p_nameserver text, p_address inet) LANGUAGE sql AS $$ INSERT INTO dns_secondaries VALUES (p_domain, p_nameserver, p_address) ON CONFLICT (domain, nameserver) DO UPDATE SET address = p_address; $$; CREATE OR REPLACE PROCEDURE dns_secondary_remove(p_domain text, p_nameserver text) LANGUAGE sql AS $$ DELETE FROM dns_secondaries WHERE domain = p_domain AND nameserver = p_nameserver; $$; CREATE OR REPLACE FUNCTION dns_records_list() RETURNS TABLE (domain text, name text, type text, content text, ttl integer) LANGUAGE sql STABLE AS $$ SELECT domain, name, type, content, ttl FROM dns_records ORDER BY domain, name, type, content; $$; -- Add the record to the domain, or set its TTL. CREATE OR REPLACE PROCEDURE dns_record_put(p_domain text, p_name text, p_type text, p_content text, p_ttl integer) LANGUAGE sql AS $$ INSERT INTO dns_records VALUES (p_domain, p_name, p_type, p_content, p_ttl) ON CONFLICT (domain, name, type, content) DO UPDATE SET ttl = p_ttl; $$; -- Remove the records of the name and type in the domain, all of them without content. CREATE OR REPLACE PROCEDURE dns_record_remove(p_domain text, p_name text, p_type text, p_content text) LANGUAGE sql AS $$ DELETE FROM dns_records WHERE domain = p_domain AND name = p_name AND type = p_type AND (p_content IS NULL OR content = p_content); $$; -- Serve the zone as its primary and give it the records, its SOA first, whose content has {serial} for the serial, -- and the addresses of its secondaries, which are notified of its changes and may transfer it. The serial grows, to -- at least the current Unix time, only when the records differ from those last written. CREATE OR REPLACE PROCEDURE dns_apply(p_zone text, p_soa text, p_ttl integer, p_names text[], p_types text[], p_contents text[], p_ttls integer[], p_secondaries text[]) LANGUAGE plpgsql AS $$ DECLARE v_domain integer; v_serial bigint; v_digest text := md5(concat_ws('|', p_soa, p_ttl, array_to_string(p_names, ','), array_to_string(p_types, ','), array_to_string(p_contents, ','), array_to_string(p_ttls, ','))); BEGIN SELECT id INTO v_domain FROM domains WHERE name = p_zone; IF v_domain IS NULL THEN INSERT INTO domains (name, type) VALUES (p_zone, 'MASTER') RETURNING id INTO v_domain; ELSE UPDATE domains SET type = 'MASTER' WHERE id = v_domain AND type <> 'MASTER'; END IF; DELETE FROM domainmetadata WHERE domain_id = v_domain AND kind IN ('ALSO-NOTIFY', 'ALLOW-AXFR-FROM'); INSERT INTO domainmetadata (domain_id, kind, content) SELECT v_domain, kind, address FROM unnest(p_secondaries) AS address, unnest(ARRAY['ALSO-NOTIFY', 'ALLOW-AXFR-FROM']) AS kind; IF EXISTS (SELECT 1 FROM dns_state WHERE zone = p_zone AND digest = v_digest) THEN RETURN; END IF; SELECT greatest(coalesce(max(serial) + 1, 1), extract(epoch FROM now())::bigint) INTO v_serial FROM dns_state WHERE zone = p_zone; DELETE FROM records WHERE domain_id = v_domain; INSERT INTO records (domain_id, name, type, content, ttl, prio, disabled, auth) VALUES (v_domain, p_zone, 'SOA', replace(p_soa, '{serial}', v_serial::text), p_ttl, 0, false, true); INSERT INTO records (domain_id, name, type, content, ttl, prio, disabled, auth) SELECT v_domain, r.name, r.type, r.content, r.ttl, 0, false, true FROM unnest(p_names, p_types, p_contents, p_ttls) AS r(name, type, content, ttl); INSERT INTO dns_state VALUES (p_zone, v_digest, v_serial) ON CONFLICT (zone) DO UPDATE SET digest = v_digest, serial = v_serial; END; $$; -- Serve only the zones of the list, none for an empty one. CREATE OR REPLACE PROCEDURE dns_retain(p_zones text[]) LANGUAGE sql AS $$ DELETE FROM domains WHERE name <> ALL (p_zones); DELETE FROM dns_state WHERE zone <> ALL (p_zones); $$; -- The records of the zone as they are served. CREATE OR REPLACE FUNCTION dns_served(p_zone text) RETURNS TABLE (name text, type text, content text, ttl integer) LANGUAGE sql STABLE AS $$ SELECT r.name, r.type, r.content, r.ttl FROM records r JOIN domains d ON d.id = r.domain_id WHERE d.name = p_zone ORDER BY r.type <> 'SOA', r.name, r.type, r.content; $$;