|
| 1 | +// This Source Code Form is subject to the terms of the Mozilla Public |
| 2 | +// License, v. 2.0. If a copy of the MPL was not distributed with this |
| 3 | +// file, You can obtain one at https://mozilla.org/MPL/2.0/. |
| 4 | + |
| 5 | +//! Dropshot API for configuring DNS namespace. |
| 6 | +//! |
| 7 | +//! ## Shape of the API |
| 8 | +//! |
| 9 | +//! The DNS configuration API has just two endpoints: PUT and GET of the entire |
| 10 | +//! DNS configuration. This is pretty anti-REST. But it's important to think |
| 11 | +//! about how this server fits into the rest of the system. When changes are |
| 12 | +//! made to DNS data, they're grouped together and assigned a monotonically |
| 13 | +//! increasing generation number. The DNS data is first stored into CockroachDB |
| 14 | +//! and then propagated from a distributed fleet of Nexus instances to a |
| 15 | +//! distributed fleet of these DNS servers. If we accepted individual updates to |
| 16 | +//! DNS names, then propagating a particular change would be non-atomic, and |
| 17 | +//! Nexus would have to do a lot more work to ensure (1) that all changes were |
| 18 | +//! propagated (even if it crashes) and (2) that they were propagated in the |
| 19 | +//! correct order (even if two Nexus instances concurrently propagate separate |
| 20 | +//! changes). |
| 21 | +//! |
| 22 | +//! This DNS server supports hosting multiple zones. We could imagine supporting |
| 23 | +//! separate endpoints to update the DNS data for a particular zone. That feels |
| 24 | +//! nicer (although it's not clear what it would buy us). But as with updates to |
| 25 | +//! multiple names, Nexus's job is potentially much easier if the entire state |
| 26 | +//! for all zones is updated at once. (Otherwise, imagine how Nexus would |
| 27 | +//! implement _renaming_ one zone to another without loss of service. With |
| 28 | +//! a combined endpoint and generation number for all zones, all that's necessary |
| 29 | +//! is to configure a new zone with all the same names, and then remove the old |
| 30 | +//! zone later in another update. That can be managed by the same mechanism in |
| 31 | +//! Nexus that manages regular name updates. On the other hand, if there were |
| 32 | +//! separate endpoints with separate generation numbers, then Nexus has more to |
| 33 | +//! keep track of in order to do the rename safely.) |
| 34 | +//! |
| 35 | +//! See RFD 367 for more on DNS propagation. |
| 36 | +//! |
| 37 | +//! ## ETags and Conditional Requests |
| 38 | +//! |
| 39 | +//! It's idiomatic in HTTP use ETags and conditional requests to provide |
| 40 | +//! synchronization. We could define an ETag to be just the current generation |
| 41 | +//! number of the server and honor standard `if-match` headers to fail requests |
| 42 | +//! where the generation number doesn't match what the client expects. This |
| 43 | +//! would be fine, but it's rather annoying: |
| 44 | +//! |
| 45 | +//! 1. When the client wants to propagate generation X, the client would have |
| 46 | +//! make an extra request just to fetch the current ETag, just so it can put |
| 47 | +//! it into the conditional request. |
| 48 | +//! |
| 49 | +//! 2. If some other client changes the configuration in the meantime, the |
| 50 | +//! conditional request would fail and the client would have to take another |
| 51 | +//! lap (fetching the current config and potentially making another |
| 52 | +//! conditional PUT). |
| 53 | +//! |
| 54 | +//! 3. This approach would make synchronization opt-in. If a client (or just |
| 55 | +//! one errant code path) neglected to set the if-match header, we could do |
| 56 | +//! the wrong thing and cause the system to come to rest with the wrong DNS |
| 57 | +//! data. |
| 58 | +//! |
| 59 | +//! Since the semantics here are so simple (we only ever want to move the |
| 60 | +//! generation number forward), we don't bother with ETags or conditional |
| 61 | +//! requests. Instead we have the server implement the behavior we want, which |
| 62 | +//! is that when a request comes in to update DNS data to generation X, the |
| 63 | +//! server replies with one of: |
| 64 | +//! |
| 65 | +//! (1) the update has been applied and the server is now running generation X |
| 66 | +//! (client treats this as success) |
| 67 | +//! |
| 68 | +//! (2) the update was not applied because the server is already at generation X |
| 69 | +//! (client treats this as success) |
| 70 | +//! |
| 71 | +//! (3) the update was not applied because the server is already at a newer |
| 72 | +//! generation |
| 73 | +//! (client probably starts the whole propagation process over because its |
| 74 | +//! current view of the world is out of date) |
| 75 | +//! |
| 76 | +//! This way, the DNS data can never move backwards and the client only ever has |
| 77 | +//! to make one request. |
| 78 | +//! |
| 79 | +//! ## Concurrent updates |
| 80 | +//! |
| 81 | +//! Given that we've got just one API to update the all DNS zones, and given |
| 82 | +//! that might therefore take a minute for a large zone, and also that there may |
| 83 | +//! be multiple Nexus instances trying to do it at the same time, we need to |
| 84 | +//! think a bit about what should happen if two Nexus do try to do it at the same |
| 85 | +//! time. Spoiler: we immediately fail any request to update the DNS data if |
| 86 | +//! there's already an update in progress. |
| 87 | +//! |
| 88 | +//! What else could we do? We could queue the incoming request behind the |
| 89 | +//! in-progress one. How large do we allow that queue to grow? At some point |
| 90 | +//! we'll need to stop queueing them. So why bother at all? |
| 91 | +
|
| 92 | +use std::{ |
| 93 | + collections::HashMap, |
| 94 | + net::{Ipv4Addr, Ipv6Addr}, |
| 95 | +}; |
| 96 | + |
| 97 | +use dropshot::{HttpError, HttpResponseOk, RequestContext}; |
| 98 | +use schemars::JsonSchema; |
| 99 | +use serde::{Deserialize, Serialize}; |
| 100 | + |
| 101 | +#[dropshot::api_description] |
| 102 | +pub trait DnsServerApi { |
| 103 | + type Context; |
| 104 | + |
| 105 | + #[endpoint( |
| 106 | + method = GET, |
| 107 | + path = "/config", |
| 108 | + )] |
| 109 | + async fn dns_config_get( |
| 110 | + rqctx: RequestContext<Self::Context>, |
| 111 | + ) -> Result<HttpResponseOk<DnsConfig>, HttpError>; |
| 112 | + |
| 113 | + #[endpoint( |
| 114 | + method = PUT, |
| 115 | + path = "/config", |
| 116 | + )] |
| 117 | + async fn dns_config_put( |
| 118 | + rqctx: RequestContext<Self::Context>, |
| 119 | + rq: dropshot::TypedBody<DnsConfigParams>, |
| 120 | + ) -> Result<dropshot::HttpResponseUpdatedNoContent, dropshot::HttpError>; |
| 121 | +} |
| 122 | + |
| 123 | +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] |
| 124 | +pub struct DnsConfigParams { |
| 125 | + pub generation: u64, |
| 126 | + pub time_created: chrono::DateTime<chrono::Utc>, |
| 127 | + pub zones: Vec<DnsConfigZone>, |
| 128 | +} |
| 129 | + |
| 130 | +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] |
| 131 | +pub struct DnsConfig { |
| 132 | + pub generation: u64, |
| 133 | + pub time_created: chrono::DateTime<chrono::Utc>, |
| 134 | + pub time_applied: chrono::DateTime<chrono::Utc>, |
| 135 | + pub zones: Vec<DnsConfigZone>, |
| 136 | +} |
| 137 | + |
| 138 | +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] |
| 139 | +pub struct DnsConfigZone { |
| 140 | + pub zone_name: String, |
| 141 | + pub records: HashMap<String, Vec<DnsRecord>>, |
| 142 | +} |
| 143 | + |
| 144 | +#[allow(clippy::upper_case_acronyms)] |
| 145 | +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] |
| 146 | +#[serde(tag = "type", content = "data")] |
| 147 | +pub enum DnsRecord { |
| 148 | + A(Ipv4Addr), |
| 149 | + AAAA(Ipv6Addr), |
| 150 | + SRV(SRV), |
| 151 | +} |
| 152 | + |
| 153 | +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] |
| 154 | +#[serde(rename = "Srv")] |
| 155 | +pub struct SRV { |
| 156 | + pub prio: u16, |
| 157 | + pub weight: u16, |
| 158 | + pub port: u16, |
| 159 | + pub target: String, |
| 160 | +} |
0 commit comments