151 lines
6.8 KiB
PL/PgSQL
151 lines
6.8 KiB
PL/PgSQL
-- 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;
|
|
$$;
|