1 // Copyright (C) 2013-2021 Internet Systems Consortium, Inc. ("ISC")
2 //
3 // This Source Code Form is subject to the terms of the Mozilla Public
4 // License, v. 2.0. If a copy of the MPL was not distributed with this
5 // file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 
7 #include <config.h>
8 
9 #include <d2srv/d2_log.h>
10 #include <d2srv/dns_client.h>
11 #include <dns/messagerenderer.h>
12 #include <stats/stats_mgr.h>
13 #include <limits>
14 
15 namespace isc {
16 namespace d2 {
17 
18 namespace {
19 
20 // OutputBuffer objects are pre-allocated before data is written to them.
21 // This is a default number of bytes for the buffers we create within
22 // DNSClient class.
23 const size_t DEFAULT_BUFFER_SIZE = 128;
24 
25 }
26 
27 using namespace isc::util;
28 using namespace isc::asiolink;
29 using namespace isc::asiodns;
30 using namespace isc::dns;
31 using namespace isc::stats;
32 
33 // This class provides the implementation for the DNSClient. This allows for
34 // the separation of the DNSClient interface from the implementation details.
35 // Currently, implementation uses IOFetch object to handle asynchronous
36 // communication with the DNS. This design may be revisited in the future. If
37 // implementation is changed, the DNSClient API will remain unchanged thanks
38 // to this separation.
39 class DNSClientImpl : public asiodns::IOFetch::Callback {
40 public:
41     /// @brief A buffer holding response from a DNS.
42     util::OutputBufferPtr in_buf_;
43 
44     /// A caller-supplied object which will hold the parsed response from DNS.
45     /// The response object is (or descends from) isc::dns::Message and is
46     /// populated using Message::fromWire().  This method may only be called
47     /// once in the lifetime of a Message instance.  Therefore, response_ is a
48     /// pointer reference thus allowing this class to replace the object
49     /// pointed to with a new Message instance each time a message is
50     /// received. This allows a single DNSClientImpl instance to be used for
51     /// multiple, sequential IOFetch calls. (@todo Trac# 3286 has been opened
52     /// against dns::Message::fromWire.  Should the behavior of fromWire change
53     /// the behavior here with could be reexamined).
54     D2UpdateMessagePtr& response_;
55 
56     /// @brief A caller-supplied external callback which is invoked when DNS
57     /// message exchange is complete or interrupted.
58     DNSClient::Callback* callback_;
59 
60     /// @brief A Transport Layer protocol used to communicate with a DNS.
61     DNSClient::Protocol proto_;
62 
63     /// @brief TSIG context used to sign outbound and verify inbound messages.
64     dns::TSIGContextPtr tsig_context_;
65 
66     /// @brief TSIG key name for stats.
67     std::string tsig_key_name_;
68 
69     /// @brief Constructor.
70     ///
71     /// @param response_placeholder Message object pointer which will be updated
72     /// with dynamically allocated object holding the DNS server's response.
73     /// @param callback Pointer to an object implementing @c DNSClient::Callback
74     /// class. This object will be called when DNS message exchange completes or
75     /// if an error occurs. NULL value disables callback invocation.
76     /// @param proto caller's preference regarding Transport layer protocol to
77     /// be used by DNS Client to communicate with a server.
78     DNSClientImpl(D2UpdateMessagePtr& response_placeholder,
79                   DNSClient::Callback* callback,
80                   const DNSClient::Protocol proto);
81 
82     /// @brief Destructor.
83     virtual ~DNSClientImpl();
84 
85     /// @brief This internal callback is called when the DNS update message
86     /// exchange is complete. It further invokes the external callback provided
87     /// by a caller. Before external callback is invoked, an object of the
88     /// D2UpdateMessage type, representing a response from the server is set.
89     virtual void operator()(asiodns::IOFetch::Result result);
90 
91     /// @brief Starts asynchronous DNS Update using TSIG.
92     ///
93     /// @param io_service IO service to be used to run the message exchange.
94     /// @param ns_addr DNS server address.
95     /// @param ns_port DNS server port.
96     /// @param update A DNS Update message to be sent to the server.
97     /// @param wait A timeout (in milliseconds) for the response. If a response
98     /// is not received within the timeout, exchange is interrupted. This value
99     /// must not exceed maximal value for 'int' data type.
100     /// @param tsig_key A pointer to an @c D2TsigKeyPtr object that will
101     /// (if not null) be used to sign the DNS Update message and verify the
102     /// response.
103     void doUpdate(asiolink::IOService& io_service,
104                   const asiolink::IOAddress& ns_addr,
105                   const uint16_t ns_port,
106                   D2UpdateMessage& update,
107                   const unsigned int wait,
108                   const D2TsigKeyPtr& tsig_key);
109 
110     /// @brief This function maps the IO error to the DNSClient error.
111     ///
112     /// @param result The IOFetch result to be converted to DNSClient status.
113     /// @return The DNSClient status corresponding to the IOFetch result.
114     DNSClient::Status getStatus(const asiodns::IOFetch::Result result);
115 
116     /// @brief This function updates statistics.
117     ///
118     /// @param stat The statistic name to be incremented.
119     /// @param update_key The flag indicating if the key statistics should also
120     /// be updated.
121     void incrStats(const std::string& stat, bool update_key = true);
122 };
123 
DNSClientImpl(D2UpdateMessagePtr & response_placeholder,DNSClient::Callback * callback,const DNSClient::Protocol proto)124 DNSClientImpl::DNSClientImpl(D2UpdateMessagePtr& response_placeholder,
125                              DNSClient::Callback* callback,
126                              const DNSClient::Protocol proto)
127     : in_buf_(new OutputBuffer(DEFAULT_BUFFER_SIZE)),
128       response_(response_placeholder), callback_(callback), proto_(proto) {
129 
130     // Response should be an empty pointer. It gets populated by the
131     // operator() method.
132     if (response_) {
133         isc_throw(isc::BadValue, "Response buffer pointer should be null");
134     }
135 
136     // @todo Currently we only support UDP. The support for TCP is planned for
137     // the future release.
138     if (proto_ == DNSClient::TCP) {
139         isc_throw(isc::NotImplemented, "TCP is currently not supported as a"
140                   << " Transport protocol for DNS Updates; please use UDP");
141     }
142 
143     // Given that we already eliminated the possibility that TCP is used, it
144     // would be sufficient  to check that (proto != DNSClient::UDP). But, once
145     // support TCP is added the check above will disappear and the extra check
146     // will be needed here anyway.
147     // Note that cascaded check is used here instead of:
148     //   if (proto_ != DNSClient::TCP && proto_ != DNSClient::UDP)..
149     // because some versions of GCC compiler complain that check above would
150     // be always 'false' due to limited range of enumeration. In fact, it is
151     // possible to pass out of range integral value through enum and it should
152     // be caught here.
153     if (proto_ != DNSClient::TCP) {
154         if (proto_ != DNSClient::UDP) {
155             isc_throw(isc::NotImplemented, "invalid transport protocol type '"
156                       << proto_ << "' specified for DNS Updates");
157         }
158     }
159 }
160 
~DNSClientImpl()161 DNSClientImpl::~DNSClientImpl() {
162 }
163 
164 void
operator ()(asiodns::IOFetch::Result result)165 DNSClientImpl::operator()(asiodns::IOFetch::Result result) {
166     // Get the status from IO. If no success, we just call user's callback
167     // and pass the status code.
168     DNSClient::Status status = getStatus(result);
169     if (status == DNSClient::SUCCESS) {
170         // Allocate a new response message. (Note that Message::fromWire
171         // may only be run once per message, so we need to start fresh
172         // each time.)
173         response_.reset(new D2UpdateMessage(D2UpdateMessage::INBOUND));
174 
175         // Server's response may be corrupted. In such case, fromWire will
176         // throw an exception. We want to catch this exception to return
177         // appropriate status code to the caller and log this event.
178         try {
179             response_->fromWire(in_buf_->getData(), in_buf_->getLength(),
180                                 tsig_context_.get());
181             incrStats("update-success");
182         } catch (const isc::Exception& ex) {
183             status = DNSClient::INVALID_RESPONSE;
184             LOG_DEBUG(d2_to_dns_logger, isc::log::DBGLVL_TRACE_DETAIL,
185                       DHCP_DDNS_INVALID_RESPONSE).arg(ex.what());
186             incrStats("update-error");
187         }
188 
189         if (tsig_context_) {
190             // Context is a one-shot deal, get rid of it.
191             tsig_context_.reset();
192         }
193     } else if (status == DNSClient::TIMEOUT) {
194         incrStats("update-timeout");
195     } else {
196         incrStats("update-error");
197     }
198 
199     // Once we are done with internal business, let's call a callback supplied
200     // by a caller.
201     if (callback_ != NULL) {
202         (*callback_)(status);
203     }
204 }
205 
206 DNSClient::Status
getStatus(const asiodns::IOFetch::Result result)207 DNSClientImpl::getStatus(const asiodns::IOFetch::Result result) {
208     switch (result) {
209     case IOFetch::SUCCESS:
210         return (DNSClient::SUCCESS);
211 
212     case IOFetch::TIME_OUT:
213         return (DNSClient::TIMEOUT);
214 
215     case IOFetch::STOPPED:
216         return (DNSClient::IO_STOPPED);
217 
218     default:
219         ;
220     }
221     return (DNSClient::OTHER);
222 }
223 
224 void
doUpdate(asiolink::IOService & io_service,const IOAddress & ns_addr,const uint16_t ns_port,D2UpdateMessage & update,const unsigned int wait,const D2TsigKeyPtr & tsig_key)225 DNSClientImpl::doUpdate(asiolink::IOService& io_service,
226                         const IOAddress& ns_addr,
227                         const uint16_t ns_port,
228                         D2UpdateMessage& update,
229                         const unsigned int wait,
230                         const D2TsigKeyPtr& tsig_key) {
231     // The underlying implementation which we use to send DNS Updates uses
232     // signed integers for timeout. If we want to avoid overflows we need to
233     // respect this limitation here.
234     if (wait > DNSClient::getMaxTimeout()) {
235         isc_throw(isc::BadValue, "A timeout value for DNS Update request must"
236                   " not exceed " << DNSClient::getMaxTimeout()
237                   << ". Provided timeout value is '" << wait << "'");
238     }
239 
240     // Create a TSIG context if we have a key, otherwise clear the context
241     // pointer.  Message marshalling uses non-null context is the indicator
242     // that TSIG should be used.
243     if (tsig_key) {
244         tsig_context_ = tsig_key->createContext();
245         tsig_key_name_ = tsig_key->getKeyName().toText();
246     } else {
247         tsig_context_.reset();
248         tsig_key_name_.clear();
249     }
250 
251     // A renderer is used by the toWire function which creates the on-wire data
252     // from the DNS Update message. A renderer has its internal buffer where it
253     // renders data by default. However, this buffer can't be directly accessed.
254     // Fortunately, the renderer's API accepts user-supplied buffers. So, let's
255     // create our own buffer and pass it to the renderer so as the message is
256     // rendered to this buffer. Finally, we pass this buffer to IOFetch.
257     dns::MessageRenderer renderer;
258     OutputBufferPtr msg_buf(new OutputBuffer(DEFAULT_BUFFER_SIZE));
259     renderer.setBuffer(msg_buf.get());
260 
261     // Render DNS Update message. This may throw a bunch of exceptions if
262     // invalid message object is given.
263     update.toWire(renderer, tsig_context_.get());
264 
265     // IOFetch has all the mechanisms that we need to perform asynchronous
266     // communication with the DNS server. The last but one argument points to
267     // this object as a completion callback for the message exchange. As a
268     // result operator()(Status) will be called.
269 
270     // Timeout value is explicitly cast to the int type to avoid warnings about
271     // overflows when doing implicit cast. It should have been checked by the
272     // caller that the unsigned timeout value will fit into int.
273     IOFetch io_fetch(IOFetch::UDP, io_service, msg_buf, ns_addr, ns_port,
274                      in_buf_, this, static_cast<int>(wait));
275 
276     // Post the task to the task queue in the IO service. Caller will actually
277     // run these tasks by executing IOService::run.
278     io_service.post(io_fetch);
279 
280     // Update sent statistics.
281     incrStats("update-sent");
282     if (tsig_key) {
283         incrStats("update-signed", false);
284     } else {
285         incrStats("update-unsigned", false);
286     }
287 }
288 
289 void
incrStats(const std::string & stat,bool update_key)290 DNSClientImpl::incrStats(const std::string& stat, bool update_key) {
291     StatsMgr& mgr = StatsMgr::instance();
292     mgr.addValue(stat, static_cast<int64_t>(1));
293     if (update_key && !tsig_key_name_.empty()) {
294         mgr.addValue(StatsMgr::generateName("key", tsig_key_name_, stat),
295                      static_cast<int64_t>(1));
296     }
297 }
298 
DNSClient(D2UpdateMessagePtr & response_placeholder,Callback * callback,const DNSClient::Protocol proto)299 DNSClient::DNSClient(D2UpdateMessagePtr& response_placeholder,
300                      Callback* callback, const DNSClient::Protocol proto)
301     : impl_(new DNSClientImpl(response_placeholder, callback, proto)) {
302 }
303 
~DNSClient()304 DNSClient::~DNSClient() {
305 }
306 
307 unsigned int
getMaxTimeout()308 DNSClient::getMaxTimeout() {
309     static const unsigned int max_timeout = std::numeric_limits<int>::max();
310     return (max_timeout);
311 }
312 
313 void
doUpdate(asiolink::IOService & io_service,const IOAddress & ns_addr,const uint16_t ns_port,D2UpdateMessage & update,const unsigned int wait,const D2TsigKeyPtr & tsig_key)314 DNSClient::doUpdate(asiolink::IOService& io_service,
315                     const IOAddress& ns_addr,
316                     const uint16_t ns_port,
317                     D2UpdateMessage& update,
318                     const unsigned int wait,
319                     const D2TsigKeyPtr& tsig_key) {
320     impl_->doUpdate(io_service, ns_addr, ns_port, update, wait, tsig_key);
321 }
322 
323 } // namespace d2
324 } // namespace isc
325