2014-04-18 21:19:28 +00:00
|
|
|
|
module Propellor.Property.Dns (
|
|
|
|
|
module Propellor.Types.Dns,
|
2014-04-19 01:58:23 +00:00
|
|
|
|
primary,
|
2015-01-03 23:10:28 +00:00
|
|
|
|
signedPrimary,
|
2014-04-18 21:19:28 +00:00
|
|
|
|
secondary,
|
2014-04-19 05:55:32 +00:00
|
|
|
|
secondaryFor,
|
2014-04-18 21:19:28 +00:00
|
|
|
|
mkSOA,
|
|
|
|
|
writeZoneFile,
|
2014-04-18 23:06:55 +00:00
|
|
|
|
nextSerialNumber,
|
|
|
|
|
adjustSerialNumber,
|
|
|
|
|
serialNumberOffset,
|
2014-04-19 07:14:26 +00:00
|
|
|
|
WarningMessage,
|
2014-04-19 01:10:44 +00:00
|
|
|
|
genZone,
|
2014-04-18 21:19:28 +00:00
|
|
|
|
) where
|
2014-04-10 05:46:33 +00:00
|
|
|
|
|
|
|
|
|
import Propellor
|
2014-04-18 21:19:28 +00:00
|
|
|
|
import Propellor.Types.Dns
|
2014-04-10 05:46:33 +00:00
|
|
|
|
import Propellor.Property.File
|
|
|
|
|
import qualified Propellor.Property.Apt as Apt
|
2015-01-04 23:24:18 +00:00
|
|
|
|
import qualified Propellor.Property.Ssh as Ssh
|
2014-04-10 05:46:33 +00:00
|
|
|
|
import qualified Propellor.Property.Service as Service
|
2015-01-03 23:10:28 +00:00
|
|
|
|
import Propellor.Property.Scheduled
|
|
|
|
|
import Propellor.Property.DnsSec
|
2014-04-18 18:29:25 +00:00
|
|
|
|
import Utility.Applicative
|
|
|
|
|
|
2014-04-19 01:10:44 +00:00
|
|
|
|
import qualified Data.Map as M
|
|
|
|
|
import qualified Data.Set as S
|
2014-04-18 18:29:25 +00:00
|
|
|
|
import Data.List
|
2014-04-10 05:46:33 +00:00
|
|
|
|
|
2014-04-19 01:58:23 +00:00
|
|
|
|
-- | Primary dns server for a domain.
|
|
|
|
|
--
|
2014-04-19 03:20:07 +00:00
|
|
|
|
-- Most of the content of the zone file is configured by setting properties
|
|
|
|
|
-- of hosts. For example,
|
|
|
|
|
--
|
|
|
|
|
-- > host "foo.example.com"
|
|
|
|
|
-- > & ipv4 "192.168.1.1"
|
2014-04-19 05:28:46 +00:00
|
|
|
|
-- > & alias "mail.exmaple.com"
|
2014-04-19 03:20:07 +00:00
|
|
|
|
--
|
2014-04-19 05:26:38 +00:00
|
|
|
|
-- Will cause that hostmame and its alias to appear in the zone file,
|
|
|
|
|
-- with the configured IP address.
|
2014-04-19 03:20:07 +00:00
|
|
|
|
--
|
2014-04-19 07:11:34 +00:00
|
|
|
|
-- The [(BindDomain, Record)] list can be used for additional records
|
2014-04-19 15:42:31 +00:00
|
|
|
|
-- that cannot be configured elsewhere. This often includes NS records,
|
|
|
|
|
-- TXT records and perhaps CNAMEs pointing at hosts that propellor does
|
|
|
|
|
-- not control.
|
2014-04-23 19:04:35 +00:00
|
|
|
|
--
|
|
|
|
|
-- The primary server is configured to only allow zone transfers to
|
|
|
|
|
-- secondary dns servers. These are determined in two ways:
|
|
|
|
|
--
|
|
|
|
|
-- 1. By looking at the properties of other hosts, to find hosts that
|
|
|
|
|
-- are configured as the secondary dns server.
|
|
|
|
|
--
|
|
|
|
|
-- 2. By looking for NS Records in the passed list of records.
|
|
|
|
|
--
|
2014-04-23 20:03:17 +00:00
|
|
|
|
-- In either case, the secondary dns server Host should have an ipv4 and/or
|
2014-04-23 20:21:38 +00:00
|
|
|
|
-- ipv6 property defined.
|
2014-04-19 15:00:48 +00:00
|
|
|
|
primary :: [Host] -> Domain -> SOA -> [(BindDomain, Record)] -> RevertableProperty
|
|
|
|
|
primary hosts domain soa rs = RevertableProperty setup cleanup
|
2014-04-19 01:58:23 +00:00
|
|
|
|
where
|
2015-01-04 17:42:01 +00:00
|
|
|
|
setup = setupPrimary zonefile id hosts domain soa rs
|
2015-01-04 16:44:05 +00:00
|
|
|
|
`onChange` Service.reloaded "bind9"
|
2015-01-04 17:22:23 +00:00
|
|
|
|
cleanup = cleanupPrimary zonefile domain
|
2014-04-19 15:00:48 +00:00
|
|
|
|
`onChange` Service.reloaded "bind9"
|
|
|
|
|
|
2015-01-04 17:22:23 +00:00
|
|
|
|
zonefile = "/etc/bind/propellor/db." ++ domain
|
|
|
|
|
|
2015-01-04 17:42:01 +00:00
|
|
|
|
setupPrimary :: FilePath -> (FilePath -> FilePath) -> [Host] -> Domain -> SOA -> [(BindDomain, Record)] -> Property
|
|
|
|
|
setupPrimary zonefile mknamedconffile hosts domain soa rs =
|
2015-01-04 23:24:18 +00:00
|
|
|
|
withwarnings baseprop
|
2015-01-04 16:44:05 +00:00
|
|
|
|
`requires` servingZones
|
|
|
|
|
where
|
2015-01-04 23:24:18 +00:00
|
|
|
|
hostmap = hostMap hosts
|
|
|
|
|
-- Known hosts with hostname located in the domain.
|
|
|
|
|
indomain = M.elems $ M.filterWithKey (\hn _ -> inDomain domain $ AbsDomain $ hn) hostmap
|
|
|
|
|
|
|
|
|
|
(partialzone, zonewarnings) = genZone indomain hostmap domain soa
|
|
|
|
|
baseprop = Property ("dns primary for " ++ domain) satisfy
|
2014-04-19 05:26:38 +00:00
|
|
|
|
(addNamedConf conf)
|
2015-01-04 23:24:18 +00:00
|
|
|
|
satisfy = do
|
|
|
|
|
sshfps <- zip (repeat (AbsDomain domain)) . concat
|
|
|
|
|
<$> mapM genSSHFP indomain
|
|
|
|
|
let zone = partialzone
|
|
|
|
|
{ zHosts = zHosts partialzone ++ rs ++ sshfps }
|
|
|
|
|
ifM (liftIO $ needupdate zone)
|
|
|
|
|
( makeChange $ writeZoneFile zone zonefile
|
|
|
|
|
, noChange
|
|
|
|
|
)
|
|
|
|
|
withwarnings p = adjustProperty p $ \a -> do
|
2014-04-23 20:03:17 +00:00
|
|
|
|
mapM_ warningMessage $ zonewarnings ++ secondarywarnings
|
2015-01-04 23:24:18 +00:00
|
|
|
|
a
|
2014-04-19 05:26:38 +00:00
|
|
|
|
conf = NamedConf
|
|
|
|
|
{ confDomain = domain
|
2014-04-23 19:04:35 +00:00
|
|
|
|
, confDnsServerType = Master
|
2015-01-04 17:42:01 +00:00
|
|
|
|
, confFile = mknamedconffile zonefile
|
2014-04-19 05:26:38 +00:00
|
|
|
|
, confMasters = []
|
2014-04-23 19:04:35 +00:00
|
|
|
|
, confAllowTransfer = nub $
|
2014-04-23 20:21:38 +00:00
|
|
|
|
concatMap (\h -> hostAddresses h hosts) $
|
|
|
|
|
secondaries ++ nssecondaries
|
2014-04-19 05:26:38 +00:00
|
|
|
|
, confLines = []
|
|
|
|
|
}
|
2014-04-23 20:21:38 +00:00
|
|
|
|
secondaries = otherServers Secondary hosts domain
|
2014-04-23 20:03:17 +00:00
|
|
|
|
secondarywarnings = map (\h -> "No IP address defined for DNS seconary " ++ h) $
|
|
|
|
|
filter (\h -> null (hostAddresses h hosts)) secondaries
|
2014-04-23 20:21:38 +00:00
|
|
|
|
nssecondaries = mapMaybe (domainHostName <=< getNS) rootRecords
|
2014-04-23 19:04:35 +00:00
|
|
|
|
rootRecords = map snd $
|
|
|
|
|
filter (\(d, _r) -> d == RootDomain || d == AbsDomain domain) rs
|
2015-01-04 23:24:18 +00:00
|
|
|
|
needupdate zone = do
|
2014-04-19 05:26:38 +00:00
|
|
|
|
v <- readZonePropellorFile zonefile
|
|
|
|
|
return $ case v of
|
|
|
|
|
Nothing -> True
|
|
|
|
|
Just oldzone ->
|
|
|
|
|
-- compare everything except serial
|
|
|
|
|
let oldserial = sSerial (zSOA oldzone)
|
|
|
|
|
z = zone { zSOA = (zSOA zone) { sSerial = oldserial } }
|
|
|
|
|
in z /= oldzone || oldserial < sSerial (zSOA zone)
|
2014-04-19 01:58:23 +00:00
|
|
|
|
|
2015-01-04 16:44:05 +00:00
|
|
|
|
|
2015-01-04 17:22:23 +00:00
|
|
|
|
cleanupPrimary :: FilePath -> Domain -> Property
|
|
|
|
|
cleanupPrimary zonefile domain = check (doesFileExist zonefile) $
|
2015-01-04 16:44:05 +00:00
|
|
|
|
property ("removed dns primary for " ++ domain)
|
|
|
|
|
(makeChange $ removeZoneFile zonefile)
|
|
|
|
|
`requires` namedConfWritten
|
|
|
|
|
|
2015-01-03 23:10:28 +00:00
|
|
|
|
-- | Primary dns server for a domain, secured with DNSSEC.
|
|
|
|
|
--
|
|
|
|
|
-- This is like `primary`, except the resulting zone
|
|
|
|
|
-- file is signed.
|
|
|
|
|
-- The Zone Signing Key (ZSK) and Key Signing Key (KSK)
|
|
|
|
|
-- used in signing it are taken from the PrivData.
|
|
|
|
|
--
|
|
|
|
|
-- As a side effect of signing the zone, a
|
|
|
|
|
-- </var/cache/bind/dsset-domain.>
|
|
|
|
|
-- file will be created. This file contains the DS records
|
|
|
|
|
-- which need to be communicated to your domain registrar
|
|
|
|
|
-- to make DNSSEC be used for your domain. Doing so is outside
|
|
|
|
|
-- the scope of propellor (currently). See for example the tutorial
|
|
|
|
|
-- <https://www.digitalocean.com/community/tutorials/how-to-setup-dnssec-on-an-authoritative-bind-dns-server--2>
|
|
|
|
|
--
|
|
|
|
|
-- The 'Recurrance' controls how frequently the signature
|
|
|
|
|
-- should be regenerated, using a new random salt, to prevent
|
2015-01-04 19:00:40 +00:00
|
|
|
|
-- zone walking attacks. `Weekly Nothing` is a reasonable choice.
|
2015-01-04 19:22:22 +00:00
|
|
|
|
--
|
|
|
|
|
-- To transition from 'primary' to 'signedPrimary', you can revert
|
|
|
|
|
-- the 'primary' property, and add this property.
|
|
|
|
|
--
|
|
|
|
|
-- Note that DNSSEC zone files use a serial number based on the unix epoch.
|
|
|
|
|
-- This is different from the serial number used by 'primary', so if you
|
|
|
|
|
-- want to later disable DNSSEC you will need to adjust the serial number
|
|
|
|
|
-- passed to mkSOA to ensure it is larger.
|
2015-01-03 23:10:28 +00:00
|
|
|
|
signedPrimary :: Recurrance -> [Host] -> Domain -> SOA -> [(BindDomain, Record)] -> RevertableProperty
|
|
|
|
|
signedPrimary recurrance hosts domain soa rs = RevertableProperty setup cleanup
|
|
|
|
|
where
|
2015-01-04 17:52:59 +00:00
|
|
|
|
setup = combineProperties ("dns primary for " ++ domain ++ " (signed)")
|
2015-01-04 18:20:22 +00:00
|
|
|
|
[ setupPrimary zonefile signedZoneFile hosts domain soa rs'
|
2015-01-04 17:52:59 +00:00
|
|
|
|
, toProp (zoneSigned domain zonefile)
|
2015-01-04 19:00:40 +00:00
|
|
|
|
, forceZoneSigned domain zonefile `period` recurrance
|
2015-01-04 17:52:59 +00:00
|
|
|
|
]
|
2015-01-04 16:44:05 +00:00
|
|
|
|
`onChange` Service.reloaded "bind9"
|
|
|
|
|
|
2015-01-04 17:22:23 +00:00
|
|
|
|
cleanup = cleanupPrimary zonefile domain
|
|
|
|
|
`onChange` toProp (revert (zoneSigned domain zonefile))
|
2015-01-04 16:44:05 +00:00
|
|
|
|
`onChange` Service.reloaded "bind9"
|
|
|
|
|
|
2015-01-04 18:20:22 +00:00
|
|
|
|
-- Include the public keys into the zone file.
|
|
|
|
|
rs' = include PubKSK : include PubZSK : rs
|
|
|
|
|
include k = (RootDomain, INCLUDE (keyFn domain k))
|
|
|
|
|
|
2015-01-04 17:22:23 +00:00
|
|
|
|
-- Put DNSSEC zone files in a different directory than is used for
|
|
|
|
|
-- the regular ones. This allows 'primary' to be reverted and
|
|
|
|
|
-- 'signedPrimary' enabled, without the reverted property stomping
|
|
|
|
|
-- on the new one's settings.
|
|
|
|
|
zonefile = "/etc/bind/propellor/dnssec/db." ++ domain
|
|
|
|
|
|
2014-04-19 05:26:38 +00:00
|
|
|
|
-- | Secondary dns server for a domain.
|
2014-04-19 05:42:19 +00:00
|
|
|
|
--
|
2014-04-19 05:55:32 +00:00
|
|
|
|
-- The primary server is determined by looking at the properties of other
|
|
|
|
|
-- hosts to find which one is configured as the primary.
|
|
|
|
|
--
|
2014-04-19 05:42:19 +00:00
|
|
|
|
-- Note that if a host is declared to be a primary and a secondary dns
|
|
|
|
|
-- server for the same domain, the primary server config always wins.
|
2014-04-19 15:00:48 +00:00
|
|
|
|
secondary :: [Host] -> Domain -> RevertableProperty
|
2014-04-23 19:04:35 +00:00
|
|
|
|
secondary hosts domain = secondaryFor (otherServers Master hosts domain) hosts domain
|
2014-04-19 05:55:32 +00:00
|
|
|
|
|
|
|
|
|
-- | This variant is useful if the primary server does not have its DNS
|
|
|
|
|
-- configured via propellor.
|
2014-04-19 15:00:48 +00:00
|
|
|
|
secondaryFor :: [HostName] -> [Host] -> Domain -> RevertableProperty
|
|
|
|
|
secondaryFor masters hosts domain = RevertableProperty setup cleanup
|
2014-04-19 05:26:38 +00:00
|
|
|
|
where
|
2014-06-09 05:45:58 +00:00
|
|
|
|
setup = pureInfoProperty desc (addNamedConf conf)
|
2014-04-19 15:00:48 +00:00
|
|
|
|
`requires` servingZones
|
|
|
|
|
cleanup = namedConfWritten
|
|
|
|
|
|
2014-10-08 17:14:21 +00:00
|
|
|
|
desc = "dns secondary for " ++ domain
|
2014-04-19 05:26:38 +00:00
|
|
|
|
conf = NamedConf
|
|
|
|
|
{ confDomain = domain
|
2014-04-23 19:04:35 +00:00
|
|
|
|
, confDnsServerType = Secondary
|
2014-04-19 05:26:38 +00:00
|
|
|
|
, confFile = "db." ++ domain
|
2014-04-19 05:55:32 +00:00
|
|
|
|
, confMasters = concatMap (\m -> hostAddresses m hosts) masters
|
2014-04-23 19:04:35 +00:00
|
|
|
|
, confAllowTransfer = []
|
|
|
|
|
, confLines = []
|
2014-04-19 05:26:38 +00:00
|
|
|
|
}
|
2014-04-10 05:46:33 +00:00
|
|
|
|
|
2014-04-23 19:04:35 +00:00
|
|
|
|
otherServers :: DnsServerType -> [Host] -> Domain -> [HostName]
|
|
|
|
|
otherServers wantedtype hosts domain =
|
2014-05-31 22:02:56 +00:00
|
|
|
|
M.keys $ M.filter wanted $ hostMap hosts
|
2014-04-23 19:04:35 +00:00
|
|
|
|
where
|
2014-06-09 05:45:58 +00:00
|
|
|
|
wanted h = case M.lookup domain (fromNamedConfMap $ _namedconf $ hostInfo h) of
|
2014-04-23 19:04:35 +00:00
|
|
|
|
Nothing -> False
|
|
|
|
|
Just conf -> confDnsServerType conf == wantedtype
|
|
|
|
|
&& confDomain conf == domain
|
|
|
|
|
|
2014-04-19 05:26:38 +00:00
|
|
|
|
-- | Rewrites the whole named.conf.local file to serve the zones
|
|
|
|
|
-- configured by `primary` and `secondary`, and ensures that bind9 is
|
|
|
|
|
-- running.
|
|
|
|
|
servingZones :: Property
|
2014-04-19 15:00:48 +00:00
|
|
|
|
servingZones = namedConfWritten
|
2014-04-19 05:26:38 +00:00
|
|
|
|
`onChange` Service.reloaded "bind9"
|
2014-04-21 02:54:31 +00:00
|
|
|
|
`requires` Apt.serviceInstalledRunning "bind9"
|
2014-04-19 15:00:48 +00:00
|
|
|
|
|
|
|
|
|
namedConfWritten :: Property
|
|
|
|
|
namedConfWritten = property "named.conf configured" $ do
|
|
|
|
|
zs <- getNamedConf
|
|
|
|
|
ensureProperty $
|
|
|
|
|
hasContent namedConfFile $
|
|
|
|
|
concatMap confStanza $ M.elems zs
|
2014-04-10 05:46:33 +00:00
|
|
|
|
|
2014-04-18 21:19:28 +00:00
|
|
|
|
confStanza :: NamedConf -> [Line]
|
|
|
|
|
confStanza c =
|
2014-04-10 05:46:33 +00:00
|
|
|
|
[ "// automatically generated by propellor"
|
2014-04-18 21:19:28 +00:00
|
|
|
|
, "zone \"" ++ confDomain c ++ "\" {"
|
2014-04-23 19:04:35 +00:00
|
|
|
|
, cfgline "type" (if confDnsServerType c == Master then "master" else "slave")
|
2014-04-18 21:19:28 +00:00
|
|
|
|
, cfgline "file" ("\"" ++ confFile c ++ "\"")
|
2014-04-10 05:46:33 +00:00
|
|
|
|
] ++
|
2014-04-23 19:04:35 +00:00
|
|
|
|
mastersblock ++
|
|
|
|
|
allowtransferblock ++
|
2014-04-18 21:19:28 +00:00
|
|
|
|
(map (\l -> "\t" ++ l ++ ";") (confLines c)) ++
|
2014-04-10 05:46:33 +00:00
|
|
|
|
[ "};"
|
|
|
|
|
, ""
|
|
|
|
|
]
|
|
|
|
|
where
|
|
|
|
|
cfgline f v = "\t" ++ f ++ " " ++ v ++ ";"
|
2014-04-23 19:04:35 +00:00
|
|
|
|
ipblock name l =
|
|
|
|
|
[ "\t" ++ name ++ " {" ] ++
|
|
|
|
|
(map (\ip -> "\t\t" ++ fromIPAddr ip ++ ";") l) ++
|
2014-04-10 05:46:33 +00:00
|
|
|
|
[ "\t};" ]
|
2014-04-23 19:04:35 +00:00
|
|
|
|
mastersblock
|
|
|
|
|
| null (confMasters c) = []
|
|
|
|
|
| otherwise = ipblock "masters" (confMasters c)
|
|
|
|
|
-- an empty block prohibits any transfers
|
|
|
|
|
allowtransferblock = ipblock "allow-transfer" (confAllowTransfer c)
|
2014-04-10 05:46:33 +00:00
|
|
|
|
|
2014-04-19 05:26:38 +00:00
|
|
|
|
namedConfFile :: FilePath
|
|
|
|
|
namedConfFile = "/etc/bind/named.conf.local"
|
2014-04-18 18:29:25 +00:00
|
|
|
|
|
2014-04-19 01:10:44 +00:00
|
|
|
|
-- | Generates a SOA with some fairly sane numbers in it.
|
2014-04-18 23:06:55 +00:00
|
|
|
|
--
|
2014-04-19 01:58:23 +00:00
|
|
|
|
-- The Domain is the domain to use in the SOA record. Typically
|
2014-04-19 02:57:51 +00:00
|
|
|
|
-- something like ns1.example.com. So, not the domain that this is the SOA
|
2014-04-19 01:58:23 +00:00
|
|
|
|
-- record for.
|
|
|
|
|
--
|
2014-04-18 23:06:55 +00:00
|
|
|
|
-- The SerialNumber can be whatever serial number was used by the domain
|
|
|
|
|
-- before propellor started managing it. Or 0 if the domain has only ever
|
|
|
|
|
-- been managed by propellor.
|
|
|
|
|
--
|
|
|
|
|
-- You do not need to increment the SerialNumber when making changes!
|
|
|
|
|
-- Propellor will automatically add the number of commits in the git
|
|
|
|
|
-- repository to the SerialNumber.
|
2014-04-19 14:47:38 +00:00
|
|
|
|
mkSOA :: Domain -> SerialNumber -> SOA
|
|
|
|
|
mkSOA d sn = SOA
|
2014-04-18 20:49:36 +00:00
|
|
|
|
{ sDomain = AbsDomain d
|
2014-04-18 23:06:55 +00:00
|
|
|
|
, sSerial = sn
|
2014-04-18 20:49:36 +00:00
|
|
|
|
, sRefresh = hours 4
|
|
|
|
|
, sRetry = hours 1
|
|
|
|
|
, sExpire = 2419200 -- 4 weeks
|
2014-04-19 02:57:51 +00:00
|
|
|
|
, sNegativeCacheTTL = hours 8
|
2014-04-18 20:49:36 +00:00
|
|
|
|
}
|
|
|
|
|
where
|
|
|
|
|
hours n = n * 60 * 60
|
2014-04-18 18:29:25 +00:00
|
|
|
|
|
|
|
|
|
dValue :: BindDomain -> String
|
|
|
|
|
dValue (RelDomain d) = d
|
|
|
|
|
dValue (AbsDomain d) = d ++ "."
|
2014-04-19 14:47:38 +00:00
|
|
|
|
dValue (RootDomain) = "@"
|
2014-04-18 18:29:25 +00:00
|
|
|
|
|
|
|
|
|
rField :: Record -> String
|
2014-04-18 21:19:28 +00:00
|
|
|
|
rField (Address (IPv4 _)) = "A"
|
|
|
|
|
rField (Address (IPv6 _)) = "AAAA"
|
2014-04-18 18:29:25 +00:00
|
|
|
|
rField (CNAME _) = "CNAME"
|
|
|
|
|
rField (MX _ _) = "MX"
|
|
|
|
|
rField (NS _) = "NS"
|
|
|
|
|
rField (TXT _) = "TXT"
|
2014-04-19 03:29:01 +00:00
|
|
|
|
rField (SRV _ _ _ _) = "SRV"
|
2015-01-04 23:24:18 +00:00
|
|
|
|
rField (SSHFP _ _ _) = "SSHFP"
|
2015-01-04 18:20:22 +00:00
|
|
|
|
rField (INCLUDE _) = "$INCLUDE"
|
2014-04-18 18:29:25 +00:00
|
|
|
|
|
|
|
|
|
rValue :: Record -> String
|
2014-04-18 21:19:28 +00:00
|
|
|
|
rValue (Address (IPv4 addr)) = addr
|
|
|
|
|
rValue (Address (IPv6 addr)) = addr
|
2014-04-18 18:29:25 +00:00
|
|
|
|
rValue (CNAME d) = dValue d
|
|
|
|
|
rValue (MX pri d) = show pri ++ " " ++ dValue d
|
|
|
|
|
rValue (NS d) = dValue d
|
2014-04-19 03:29:01 +00:00
|
|
|
|
rValue (SRV priority weight port target) = unwords
|
|
|
|
|
[ show priority
|
|
|
|
|
, show weight
|
|
|
|
|
, show port
|
|
|
|
|
, dValue target
|
|
|
|
|
]
|
2015-01-04 23:24:18 +00:00
|
|
|
|
rValue (SSHFP x y s) = unwords
|
|
|
|
|
[ show x
|
|
|
|
|
, show y
|
|
|
|
|
, s
|
|
|
|
|
]
|
2015-01-04 18:20:22 +00:00
|
|
|
|
rValue (INCLUDE f) = f
|
2014-04-18 18:29:25 +00:00
|
|
|
|
rValue (TXT s) = [q] ++ filter (/= q) s ++ [q]
|
|
|
|
|
where
|
2014-04-18 20:33:06 +00:00
|
|
|
|
q = '"'
|
2014-04-18 18:29:25 +00:00
|
|
|
|
|
2014-04-19 07:13:42 +00:00
|
|
|
|
-- | Adjusts the serial number of the zone to always be larger
|
|
|
|
|
-- than the serial number in the Zone record,
|
|
|
|
|
-- and always be larger than the passed SerialNumber.
|
2014-04-18 18:44:46 +00:00
|
|
|
|
nextSerialNumber :: Zone -> SerialNumber -> Zone
|
2014-04-18 23:06:55 +00:00
|
|
|
|
nextSerialNumber z serial = adjustSerialNumber z $ \sn -> succ $ max sn serial
|
2014-04-18 18:29:25 +00:00
|
|
|
|
|
2014-04-18 23:06:55 +00:00
|
|
|
|
adjustSerialNumber :: Zone -> (SerialNumber -> SerialNumber) -> Zone
|
2014-04-19 01:10:44 +00:00
|
|
|
|
adjustSerialNumber (Zone d soa l) f = Zone d soa' l
|
2014-04-18 20:33:06 +00:00
|
|
|
|
where
|
2014-04-18 23:06:55 +00:00
|
|
|
|
soa' = soa { sSerial = f (sSerial soa) }
|
2014-04-18 20:33:06 +00:00
|
|
|
|
|
2014-04-18 23:06:55 +00:00
|
|
|
|
-- | Count the number of git commits made to the current branch.
|
|
|
|
|
serialNumberOffset :: IO SerialNumber
|
|
|
|
|
serialNumberOffset = fromIntegral . length . lines
|
|
|
|
|
<$> readProcess "git" ["log", "--pretty=%H"]
|
2014-04-18 20:33:06 +00:00
|
|
|
|
|
2014-04-18 18:29:25 +00:00
|
|
|
|
-- | Write a Zone out to a to a file.
|
|
|
|
|
--
|
2014-04-18 23:06:55 +00:00
|
|
|
|
-- The serial number in the Zone automatically has the serialNumberOffset
|
|
|
|
|
-- added to it. Also, just in case, the old serial number used in the zone
|
|
|
|
|
-- file is checked, and if it is somehow larger, its succ is used.
|
2014-04-18 18:29:25 +00:00
|
|
|
|
writeZoneFile :: Zone -> FilePath -> IO ()
|
|
|
|
|
writeZoneFile z f = do
|
2014-04-18 23:06:55 +00:00
|
|
|
|
oldserial <- oldZoneFileSerialNumber f
|
|
|
|
|
offset <- serialNumberOffset
|
|
|
|
|
let z' = nextSerialNumber
|
|
|
|
|
(adjustSerialNumber z (+ offset))
|
2014-04-19 01:58:23 +00:00
|
|
|
|
oldserial
|
|
|
|
|
createDirectoryIfMissing True (takeDirectory f)
|
2014-04-18 18:29:25 +00:00
|
|
|
|
writeFile f (genZoneFile z')
|
2014-04-18 20:33:06 +00:00
|
|
|
|
writeZonePropellorFile f z'
|
2014-04-18 18:29:25 +00:00
|
|
|
|
|
2014-04-19 15:00:48 +00:00
|
|
|
|
removeZoneFile :: FilePath -> IO ()
|
|
|
|
|
removeZoneFile f = do
|
|
|
|
|
nukeFile f
|
|
|
|
|
nukeFile (zonePropellorFile f)
|
|
|
|
|
|
2014-04-18 20:33:06 +00:00
|
|
|
|
-- | Next to the zone file, is a ".propellor" file, which contains
|
|
|
|
|
-- the serialized Zone. This saves the bother of parsing
|
|
|
|
|
-- the horrible bind zone file format.
|
|
|
|
|
zonePropellorFile :: FilePath -> FilePath
|
2014-04-19 01:10:44 +00:00
|
|
|
|
zonePropellorFile f = f ++ ".propellor"
|
2014-04-18 18:29:25 +00:00
|
|
|
|
|
2014-04-18 23:06:55 +00:00
|
|
|
|
oldZoneFileSerialNumber :: FilePath -> IO SerialNumber
|
|
|
|
|
oldZoneFileSerialNumber = maybe 0 (sSerial . zSOA) <$$> readZonePropellorFile
|
2014-04-18 20:33:06 +00:00
|
|
|
|
|
|
|
|
|
writeZonePropellorFile :: FilePath -> Zone -> IO ()
|
|
|
|
|
writeZonePropellorFile f z = writeFile (zonePropellorFile f) (show z)
|
2014-04-18 18:29:25 +00:00
|
|
|
|
|
2014-04-18 20:33:06 +00:00
|
|
|
|
readZonePropellorFile :: FilePath -> IO (Maybe Zone)
|
|
|
|
|
readZonePropellorFile f = catchDefaultIO Nothing $
|
2014-04-19 06:08:38 +00:00
|
|
|
|
readish <$> readFileStrict (zonePropellorFile f)
|
2014-04-18 18:29:25 +00:00
|
|
|
|
|
|
|
|
|
-- | Generating a zone file.
|
|
|
|
|
genZoneFile :: Zone -> String
|
2014-04-19 01:10:44 +00:00
|
|
|
|
genZoneFile (Zone zdomain soa rs) = unlines $
|
2014-04-19 14:47:38 +00:00
|
|
|
|
header : genSOA soa ++ map (genRecord zdomain) rs
|
2014-04-18 18:29:25 +00:00
|
|
|
|
where
|
2014-04-19 01:10:44 +00:00
|
|
|
|
header = com $ "BIND zone file for " ++ zdomain ++ ". Generated by propellor, do not edit."
|
2014-04-18 18:29:25 +00:00
|
|
|
|
|
2014-04-19 14:47:38 +00:00
|
|
|
|
genRecord :: Domain -> (BindDomain, Record) -> String
|
2015-01-04 18:20:22 +00:00
|
|
|
|
genRecord _ (_, record@(INCLUDE _)) = intercalate "\t"
|
|
|
|
|
[ rField record
|
|
|
|
|
, rValue record
|
|
|
|
|
]
|
2014-04-19 14:47:38 +00:00
|
|
|
|
genRecord zdomain (domain, record) = intercalate "\t"
|
2015-01-04 18:20:22 +00:00
|
|
|
|
[ domainHost zdomain domain
|
|
|
|
|
, "IN"
|
|
|
|
|
, rField record
|
|
|
|
|
, rValue record
|
|
|
|
|
]
|
2014-04-18 18:29:25 +00:00
|
|
|
|
|
2014-04-19 14:47:38 +00:00
|
|
|
|
genSOA :: SOA -> [String]
|
|
|
|
|
genSOA soa =
|
|
|
|
|
-- "@ IN SOA ns1.example.com. root ("
|
|
|
|
|
[ intercalate "\t"
|
|
|
|
|
[ dValue RootDomain
|
|
|
|
|
, "IN"
|
|
|
|
|
, "SOA"
|
|
|
|
|
, dValue (sDomain soa)
|
|
|
|
|
, "root"
|
|
|
|
|
, "("
|
2014-04-18 18:29:25 +00:00
|
|
|
|
]
|
2014-04-19 14:47:38 +00:00
|
|
|
|
, headerline sSerial "Serial"
|
|
|
|
|
, headerline sRefresh "Refresh"
|
|
|
|
|
, headerline sRetry "Retry"
|
|
|
|
|
, headerline sExpire "Expire"
|
|
|
|
|
, headerline sNegativeCacheTTL "Negative Cache TTL"
|
|
|
|
|
, inheader ")"
|
|
|
|
|
]
|
|
|
|
|
where
|
2014-04-18 18:29:25 +00:00
|
|
|
|
headerline r comment = inheader $ show (r soa) ++ "\t\t" ++ com comment
|
|
|
|
|
inheader l = "\t\t\t" ++ l
|
|
|
|
|
|
|
|
|
|
-- | Comment line in a zone file.
|
|
|
|
|
com :: String -> String
|
|
|
|
|
com s = "; " ++ s
|
|
|
|
|
|
2014-04-19 01:10:44 +00:00
|
|
|
|
type WarningMessage = String
|
|
|
|
|
|
2015-01-04 23:24:18 +00:00
|
|
|
|
-- | Generates SSHFP records for hosts that have configured
|
|
|
|
|
-- ssh public keys.
|
|
|
|
|
--
|
|
|
|
|
-- This is done using ssh-keygen, so sadly needs IO.
|
|
|
|
|
genSSHFP :: Host -> Propellor [Record]
|
|
|
|
|
genSSHFP h = concat <$> (gen =<< get)
|
|
|
|
|
where
|
|
|
|
|
get = fromHost [h] (hostName h) Ssh.getPubKey
|
|
|
|
|
gen = liftIO . mapM go . M.elems . fromMaybe M.empty
|
|
|
|
|
go pubkey = withTmpFile "sshfp" $ \tmp tmph -> do
|
|
|
|
|
hPutStrLn tmph pubkey
|
|
|
|
|
hClose tmph
|
|
|
|
|
s <- catchDefaultIO "" $
|
|
|
|
|
readProcess "ssh-keygen" ["-r", "dummy", "-f", tmp]
|
|
|
|
|
return $ mapMaybe (parse . words) $ lines s
|
|
|
|
|
parse ("dummy":"IN":"SSHFP":x:y:s:[]) = do
|
|
|
|
|
x' <- readish x
|
|
|
|
|
y' <- readish y
|
|
|
|
|
return $ SSHFP x' y' s
|
|
|
|
|
parse _ = Nothing
|
|
|
|
|
|
2014-04-18 23:06:55 +00:00
|
|
|
|
-- | Generates a Zone for a particular Domain from the DNS properies of all
|
|
|
|
|
-- hosts that propellor knows about that are in that Domain.
|
2015-01-04 23:24:18 +00:00
|
|
|
|
--
|
|
|
|
|
-- Does not include SSHFP records.
|
|
|
|
|
genZone :: [Host] -> M.Map HostName Host -> Domain -> SOA -> (Zone, [WarningMessage])
|
|
|
|
|
genZone inzdomain hostmap zdomain soa =
|
2014-04-19 01:10:44 +00:00
|
|
|
|
let (warnings, zhosts) = partitionEithers $ concat $ map concat
|
|
|
|
|
[ map hostips inzdomain
|
|
|
|
|
, map hostrecords inzdomain
|
2015-01-04 23:24:18 +00:00
|
|
|
|
, map addcnames (M.elems hostmap)
|
2014-04-19 01:10:44 +00:00
|
|
|
|
]
|
2014-06-01 11:32:16 +00:00
|
|
|
|
in (Zone zdomain soa (simplify zhosts), warnings)
|
2014-04-18 23:06:55 +00:00
|
|
|
|
where
|
2014-04-19 01:10:44 +00:00
|
|
|
|
-- Each host with a hostname located in the zdomain
|
2014-06-09 05:45:58 +00:00
|
|
|
|
-- should have 1 or more IPAddrs in its Info.
|
2014-04-19 01:10:44 +00:00
|
|
|
|
--
|
|
|
|
|
-- If a host lacks any IPAddr, it's probably a misconfiguration,
|
|
|
|
|
-- so warn.
|
2014-05-31 22:02:56 +00:00
|
|
|
|
hostips :: Host -> [Either WarningMessage (BindDomain, Record)]
|
|
|
|
|
hostips h
|
2014-06-01 00:48:23 +00:00
|
|
|
|
| null l = [Left $ "no IP address defined for host " ++ hostName h]
|
2014-04-19 01:10:44 +00:00
|
|
|
|
| otherwise = map Right l
|
|
|
|
|
where
|
2014-06-09 05:45:58 +00:00
|
|
|
|
info = hostInfo h
|
2014-06-01 00:48:23 +00:00
|
|
|
|
l = zip (repeat $ AbsDomain $ hostName h)
|
2014-06-09 05:45:58 +00:00
|
|
|
|
(map Address $ getAddresses info)
|
2014-04-19 01:10:44 +00:00
|
|
|
|
|
|
|
|
|
-- Any host, whether its hostname is in the zdomain or not,
|
2014-04-19 02:57:51 +00:00
|
|
|
|
-- may have cnames which are in the zdomain. The cname may even be
|
|
|
|
|
-- the same as the root of the zdomain, which is a nice way to
|
|
|
|
|
-- specify IP addresses for a SOA record.
|
2014-04-19 01:10:44 +00:00
|
|
|
|
--
|
|
|
|
|
-- Add Records for those.. But not actually, usually, cnames!
|
|
|
|
|
-- Why not? Well, using cnames doesn't allow doing some things,
|
2014-04-19 02:57:51 +00:00
|
|
|
|
-- including MX and round robin DNS, and certianly CNAMES
|
|
|
|
|
-- shouldn't be used in SOA records.
|
2014-04-19 01:10:44 +00:00
|
|
|
|
--
|
|
|
|
|
-- We typically know the host's IPAddrs anyway.
|
|
|
|
|
-- So we can just use the IPAddrs.
|
2014-05-31 22:02:56 +00:00
|
|
|
|
addcnames :: Host -> [Either WarningMessage (BindDomain, Record)]
|
|
|
|
|
addcnames h = concatMap gen $ filter (inDomain zdomain) $
|
2014-06-09 05:45:58 +00:00
|
|
|
|
mapMaybe getCNAME $ S.toList (_dns info)
|
2014-04-19 01:10:44 +00:00
|
|
|
|
where
|
2014-06-09 05:45:58 +00:00
|
|
|
|
info = hostInfo h
|
|
|
|
|
gen c = case getAddresses info of
|
2014-04-19 01:10:44 +00:00
|
|
|
|
[] -> [ret (CNAME c)]
|
|
|
|
|
l -> map (ret . Address) l
|
|
|
|
|
where
|
2014-10-08 17:17:11 +00:00
|
|
|
|
ret record = Right (c, record)
|
2014-04-19 01:10:44 +00:00
|
|
|
|
|
|
|
|
|
-- Adds any other DNS records for a host located in the zdomain.
|
2014-05-31 22:02:56 +00:00
|
|
|
|
hostrecords :: Host -> [Either WarningMessage (BindDomain, Record)]
|
|
|
|
|
hostrecords h = map Right l
|
2014-04-19 01:10:44 +00:00
|
|
|
|
where
|
2014-06-09 05:45:58 +00:00
|
|
|
|
info = hostInfo h
|
2014-06-01 00:48:23 +00:00
|
|
|
|
l = zip (repeat $ AbsDomain $ hostName h)
|
2014-06-09 05:45:58 +00:00
|
|
|
|
(S.toList $ S.filter (\r -> isNothing (getIPAddr r) && isNothing (getCNAME r)) (_dns info))
|
2014-04-19 01:10:44 +00:00
|
|
|
|
|
2014-06-01 11:32:16 +00:00
|
|
|
|
-- Simplifies the list of hosts. Remove duplicate entries.
|
|
|
|
|
-- Also, filter out any CHAMES where the same domain has an
|
|
|
|
|
-- IP address, since that's not legal.
|
|
|
|
|
simplify :: [(BindDomain, Record)] -> [(BindDomain, Record)]
|
|
|
|
|
simplify l = nub $ filter (not . dupcname ) l
|
|
|
|
|
where
|
|
|
|
|
dupcname (d, CNAME _) | any (matchingaddr d) l = True
|
|
|
|
|
dupcname _ = False
|
|
|
|
|
matchingaddr d (d', (Address _)) | d == d' = True
|
|
|
|
|
matchingaddr _ _ = False
|
|
|
|
|
|
2014-04-19 01:10:44 +00:00
|
|
|
|
inDomain :: Domain -> BindDomain -> Bool
|
|
|
|
|
inDomain domain (AbsDomain d) = domain == d || ('.':domain) `isSuffixOf` d
|
|
|
|
|
inDomain _ _ = False -- can't tell, so assume not
|
|
|
|
|
|
|
|
|
|
-- | Gets the hostname of the second domain, relative to the first domain,
|
|
|
|
|
-- suitable for using in a zone file.
|
|
|
|
|
domainHost :: Domain -> BindDomain -> String
|
|
|
|
|
domainHost _ (RelDomain d) = d
|
2014-04-19 14:47:38 +00:00
|
|
|
|
domainHost _ RootDomain = "@"
|
2014-04-19 01:10:44 +00:00
|
|
|
|
domainHost base (AbsDomain d)
|
|
|
|
|
| dotbase `isSuffixOf` d = take (length d - length dotbase) d
|
|
|
|
|
| base == d = "@"
|
|
|
|
|
| otherwise = d
|
|
|
|
|
where
|
|
|
|
|
dotbase = '.':base
|
|
|
|
|
|
2014-06-09 05:45:58 +00:00
|
|
|
|
addNamedConf :: NamedConf -> Info
|
2014-06-01 00:39:56 +00:00
|
|
|
|
addNamedConf conf = mempty { _namedconf = NamedConfMap (M.singleton domain conf) }
|
|
|
|
|
where
|
2014-10-08 17:14:21 +00:00
|
|
|
|
domain = confDomain conf
|
2014-06-01 00:39:56 +00:00
|
|
|
|
|
|
|
|
|
getNamedConf :: Propellor (M.Map Domain NamedConf)
|
2014-06-09 05:45:58 +00:00
|
|
|
|
getNamedConf = asks $ fromNamedConfMap . _namedconf . hostInfo
|