Delegation WIP.

This commit is contained in:
Philippe Pittoli 2025-07-29 00:35:23 +02:00
parent c5433bb95a
commit 38e3ce432e
3 changed files with 122 additions and 44 deletions

View file

@ -36,4 +36,11 @@ module DNSManager
class TokenNotFoundException < ::Exception class TokenNotFoundException < ::Exception
end end
# The domain doesn't have delegation parameters, but a generation of a delegation file is asked.
class NoDelegation < ::Exception
property domain : String
def initialize(@domain)
end
end
end end

View file

@ -6,6 +6,15 @@ require "./service.cr"
require "dodb" require "dodb"
def safe_write(filename : String, &block)
filename_wip = "#{filename}.wip"
File.open(filename_wip, "w") do |file|
yield file
end
# Rename WIP filename to final file name.
File.rename filename_wip, filename
end
class DNSManager::Storage class DNSManager::Storage
getter domains : DODB::Storage::Common(Domain) getter domains : DODB::Storage::Common(Domain)
getter domains_by_name : DODB::Trigger::IndexCached(Domain) getter domains_by_name : DODB::Trigger::IndexCached(Domain)
@ -23,6 +32,7 @@ class DNSManager::Storage
getter root : String getter root : String
getter zonefiledir : String getter zonefiledir : String
getter delegationdir : String
property dnsmanagerd : DNSManager::Service? = nil property dnsmanagerd : DNSManager::Service? = nil
@ -66,6 +76,9 @@ class DNSManager::Storage
@zonefiledir = "#{@root}/bind9-zones" @zonefiledir = "#{@root}/bind9-zones"
Dir.mkdir_p @zonefiledir Dir.mkdir_p @zonefiledir
@delegationdir = "#{@root}/bind9-delegations"
Dir.mkdir_p @delegationdir
Baguette::Log.info "storage initialized" Baguette::Log.info "storage initialized"
if reindex if reindex
@ -79,6 +92,23 @@ class DNSManager::Storage
end end
end end
# Delegation token file: file that is used off-application to know when the delegation file should be re-generated.
# Each time this file is touched, the (root) zone files are generated.
#
# RATIONALE: generating the final (root) zone file may become quite slow over time.
# Instead of generating the file for each change (synchronously),
# it will be regenerated periodically by a script outside the application.
def delegation_token_file
"#{@delegationdir}/regen-token-file"
end
# Updates the token file.
def update_delegation_token_file
File.open(delegation_token_file, "w") do |file|
file << "last change: #{Baguette::Log.now}"
end
end
# Generate Bind9 zone files. # Generate Bind9 zone files.
# The file is written in a temporary file then moved, enabling safe manipulation of the file # The file is written in a temporary file then moved, enabling safe manipulation of the file
# since its content always will be consistent even if not up-to-date. # since its content always will be consistent even if not up-to-date.
@ -90,17 +120,11 @@ class DNSManager::Storage
return return
end end
# Safe write. filename = "#{@zonefiledir}/#{zone.domain}"
filename_final = "#{@zonefiledir}/#{zone.domain}" Baguette::Log.info "writing zone file #{filename}"
filename_wip = "#{filename_final}.wip" safe_write filename do |file|
Baguette::Log.info "writing zone file #{filename_final}"
File.open(filename_wip, "w") do |file|
zone.to_bind9 file zone.to_bind9 file
end end
# Rename WIP filename to final file name.
File.rename filename_wip, filename_final
end end
# Request to generate a zone file. # Request to generate a zone file.
@ -374,6 +398,12 @@ class DNSManager::Storage
Response::RRDeleted.new rrid Response::RRDeleted.new rrid
end end
# Removes a delegation file, then update the token file.
def remove_delegation_file(domain : String) : Nil
Baguette::Log.info "Removing a delegation file."
File.delete "#{@delegationdir}/#{domain}"
end
# Removes a Bind9 zonefile. # Removes a Bind9 zonefile.
def remove_bind9_zonefile(domain : String) : Nil def remove_bind9_zonefile(domain : String) : Nil
Baguette::Log.info "Removing a Bind9 zone file." Baguette::Log.info "Removing a Bind9 zone file."
@ -382,31 +412,8 @@ class DNSManager::Storage
# Deletes a domain. # Deletes a domain.
def delete_domain(user_id : UserDataID, domain_name : String) : IPC::JSON def delete_domain(user_id : UserDataID, domain_name : String) : IPC::JSON
zone_must_exist! domain_name wipe_domain user_id, domain_name
user_should_own! user_id, domain_name wipe_zone user_id, domain_name
domain = @domains_by_name.get domain_name
# The user isn't the only owner, just remove him from the list of owners.
if domain.owners.size > 1
domain_cloned = domain.clone
domain_cloned.owners.select! { |o| o != user_id }
@domains_by_name.update domain_cloned
# Not really a domain deletion, but that'll do the trick.
return Response::DomainDeleted.new domain_name
end
# Remove the related zone and their registered tokens.
zones_by_domain.delete domain_name
# The domain may not have tokens.
tokens_by_domain.delete? domain_name
# Remove this domain_name from the list of user's domains.
domains_by_name.delete domain_name
remove_bind9_zonefile domain_name
Response::DomainDeleted.new domain_name Response::DomainDeleted.new domain_name
end end
@ -449,24 +456,33 @@ class DNSManager::Storage
# Deletes the zone and its tokens to wipe the zone data then delegates the domain. # Deletes the zone and its tokens to wipe the zone data then delegates the domain.
def delegate_domain(user_id : UserDataID, domain_name : String, def delegate_domain(user_id : UserDataID, domain_name : String,
nameserver1 : String, nameserver2 : String) : IPC::JSON nameserver1 : String, nameserver2 : String) : IPC::JSON
_ = zone_must_exist! domain_name
user_should_own! user_id, domain_name
# Name server domains verification. # Verifies the provided name server domains.
return Response::InvalidDomainName.new unless Zone.is_domain_valid? nameserver1 return Response::InvalidDomainName.new unless Zone.is_domain_valid? nameserver1
return Response::InvalidDomainName.new unless Zone.is_domain_valid? nameserver2 return Response::InvalidDomainName.new unless Zone.is_domain_valid? nameserver2
# Remove the related zone and their registered tokens. # Wipes the domain from dnsmanager (generated zone file, tokens).
zones_by_domain.delete domain_name wipe_domain user_id, domain_name
# The domain may not have tokens. remove_bind9_zonefile domain_name
tokens_by_domain.delete? domain_name
# TODO, FIXME: actually delegate the domain.
# Creates the new zone.
zone = Zone.new domain_name zone = Zone.new domain_name
zone.delegation = Zone::Delegation.new nameserver1, nameserver2 zone.delegation = Zone::Delegation.new nameserver1, nameserver2
zones_by_domain.update_or_create zone.domain, zone
filename = "#{@delegationdir}/#{domain_name}"
Baguette::Log.info "New delegation file: #{filename}"
safe_write filename do |file|
zone.to_delegation file
rescue e : NoDelegation
Baguette::Log.error "domain #{domain_name}: trying to delegate but doesn't have delegation parameters"
end
# Once the new delegation file has been written, the script generating the (root) zone file must
# be informed by touching a file (named "delegation token file" in the source code).
update_delegation_token_file
zones_by_domain.update_or_create zone
Response::DomainDelegated.new domain_name, nameserver1, nameserver2 Response::DomainDelegated.new domain_name, nameserver1, nameserver2
end end
@ -483,6 +499,7 @@ class DNSManager::Storage
@tokens_by_domain.delete? domain_cloned.name @tokens_by_domain.delete? domain_cloned.name
@zones_by_domain.delete domain_cloned.name @zones_by_domain.delete domain_cloned.name
@domains_by_name.delete domain_cloned.name @domains_by_name.delete domain_cloned.name
remove_bind9_zonefile domain_cloned.name
else else
@domains_by_name.update domain_cloned @domains_by_name.update domain_cloned
end end
@ -544,6 +561,51 @@ class DNSManager::Storage
zone zone
end end
# WARNING: this function removes a domain with all its related data (zone file, delegation file, indexes, etc.).
#
# RATIONALE: wipe_zone can be used to renew to remove zone-related content (the entry in the zone db, tokens
# and generated zone file) while preserving the entry in the domain db.
# wipe_zone is called when the domain is deleted or when it is delegated.
def wipe_zone(user_id : UserDataID, domain_name : String)
zone = zone_must_exist! domain_name
user_should_own! user_id, domain_name
Baguette::Log.info "Wiping zone content for #{domain_name}"
# Remove the related zone and their registered tokens.
zones_by_domain.delete domain_name
# The domain may not have tokens.
tokens_by_domain.delete? domain_name
# Remove the delegation file and regenerate the delegation files.
if delegation = zone.delegation
remove_delegation_file domain_name
end
# There is no need to keep a generated zone file.
remove_bind9_zonefile domain_name
end
# WARNING: this function removes a domain from the @domains db.
def wipe_domain(user_id : UserDataID, domain_name : String)
zone = zone_must_exist! domain_name
user_should_own! user_id, domain_name
Baguette::Log.info "Wiping domain entry for #{domain_name}"
domain = @domains_by_name.get domain_name
# The user isn't the only owner, just remove him from the list of owners.
if domain.owners.size > 1
domain_cloned = domain.clone
domain_cloned.owners.select! { |o| o != user_id }
@domains_by_name.update domain_cloned
return
end
# Remove this domain_name from the list of user's domains.
domains_by_name.delete domain_name
end
# Owning a domain means to be in the owners' list of the domain. # Owning a domain means to be in the owners' list of the domain.
def user_should_own!(user_id : UserDataID, domain : String) : Nil def user_should_own!(user_id : UserDataID, domain : String) : Nil
d = domains_by_name.get? domain d = domains_by_name.get? domain

View file

@ -55,6 +55,15 @@ class DNSManager::Storage::Zone
def_clone def_clone
end end
def to_delegation(io : IO)
if delegation = @delegation
io << "#{@domain}. 1800 IN NS #{delegation.nameserver1}\n"
io << "#{@domain}. 1800 IN NS #{delegation.nameserver2}\n"
else
raise NoDelegation.new @domain
end
end
alias Error = String alias Error = String
# Store a Resource Record: A, AAAA, TXT, PTR, CNAME… # Store a Resource Record: A, AAAA, TXT, PTR, CNAME…