2 * This file is part of PowerDNS or dnsdist.
3 * Copyright -- PowerDNS.COM B.V. and its contributors
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of version 2 of the GNU General Public License as
7 * published by the Free Software Foundation.
9 * In addition, for the avoidance of any doubt, permission is granted to
10 * link this program with OpenSSL and to (re)distribute the binaries
11 * produced as the result of such linking.
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, write to the Free Software
20 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
24 #include "dnsdist-dnsparser.hh"
25 #include "dnsdist-nghttp2-in.hh"
26 #include "dnsdist-proxy-protocol.hh"
27 #include "dnsparser.hh"
29 #if defined(HAVE_DNS_OVER_HTTPS) && defined(HAVE_NGHTTP2)
32 class IncomingDoHCrossProtocolContext
: public CrossProtocolContext
35 IncomingDoHCrossProtocolContext(IncomingHTTP2Connection::PendingQuery
&& query
, std::shared_ptr
<IncomingHTTP2Connection
> connection
, IncomingHTTP2Connection::StreamID streamID
): CrossProtocolContext(std::move(query
.d_buffer
)), d_connection(connection
), d_query(std::move(query
))
39 std::optional
<std::string
> getHTTPPath() const override
41 return d_query
.d_path
;
44 std::optional
<std::string
> getHTTPScheme() const override
46 return d_query
.d_scheme
;
49 std::optional
<std::string
> getHTTPHost() const override
51 return d_query
.d_host
;
54 std::optional
<std::string
> getHTTPQueryString() const override
56 return d_query
.d_queryString
;
59 std::optional
<HeadersMap
> getHTTPHeaders() const override
61 if (!d_query
.d_headers
) {
64 return *d_query
.d_headers
;
67 void handleResponse(PacketBuffer
&& response
, InternalQueryState
&& state
) override
69 auto conn
= d_connection
.lock();
71 /* the connection has been closed in the meantime */
76 void handleTimeout() override
78 auto conn
= d_connection
.lock();
80 /* the connection has been closed in the meantime */
85 ~IncomingDoHCrossProtocolContext() override
90 std::weak_ptr
<IncomingHTTP2Connection
> d_connection
;
91 IncomingHTTP2Connection::PendingQuery d_query
;
92 IncomingHTTP2Connection::StreamID d_streamID
{-1};
96 class IncomingDoHCrossProtocolContext
: public DOHUnitInterface
99 IncomingDoHCrossProtocolContext(IncomingHTTP2Connection::PendingQuery
&& query
, const std::shared_ptr
<IncomingHTTP2Connection
>& connection
, IncomingHTTP2Connection::StreamID streamID
) :
100 d_connection(connection
), d_query(std::move(query
)), d_streamID(streamID
)
103 IncomingDoHCrossProtocolContext(const IncomingDoHCrossProtocolContext
&) = delete;
104 IncomingDoHCrossProtocolContext(IncomingDoHCrossProtocolContext
&&) = delete;
105 IncomingDoHCrossProtocolContext
& operator=(const IncomingDoHCrossProtocolContext
&) = delete;
106 IncomingDoHCrossProtocolContext
& operator=(IncomingDoHCrossProtocolContext
&&) = delete;
108 ~IncomingDoHCrossProtocolContext() override
= default;
110 [[nodiscard
]] std::string
getHTTPPath() const override
112 return d_query
.d_path
;
115 [[nodiscard
]] const std::string
& getHTTPScheme() const override
117 return d_query
.d_scheme
;
120 [[nodiscard
]] const std::string
& getHTTPHost() const override
122 return d_query
.d_host
;
125 [[nodiscard
]] std::string
getHTTPQueryString() const override
127 return d_query
.d_queryString
;
130 [[nodiscard
]] const HeadersMap
& getHTTPHeaders() const override
132 if (!d_query
.d_headers
) {
133 static const HeadersMap empty
{};
136 return *d_query
.d_headers
;
139 void setHTTPResponse(uint16_t statusCode
, PacketBuffer
&& body
, const std::string
& contentType
= "") override
141 d_query
.d_statusCode
= statusCode
;
142 d_query
.d_response
= std::move(body
);
143 d_query
.d_contentTypeOut
= contentType
;
146 void handleUDPResponse(PacketBuffer
&& response
, InternalQueryState
&& state
, const std::shared_ptr
<DownstreamState
>& downstream_
) override
148 std::unique_ptr
<DOHUnitInterface
> unit(this);
149 auto conn
= d_connection
.lock();
151 /* the connection has been closed in the meantime */
155 state
.du
= std::move(unit
);
156 TCPResponse
resp(std::move(response
), std::move(state
), nullptr, nullptr);
157 resp
.d_ds
= downstream_
;
161 gettimeofday(&now
, nullptr);
162 conn
->handleResponse(now
, std::move(resp
));
165 void handleTimeout() override
167 std::unique_ptr
<DOHUnitInterface
> unit(this);
168 auto conn
= d_connection
.lock();
170 /* the connection has been closed in the meantime */
176 gettimeofday(&now
, nullptr);
178 resp
.d_idstate
.d_streamID
= d_streamID
;
179 conn
->notifyIOError(now
, std::move(resp
));
182 std::weak_ptr
<IncomingHTTP2Connection
> d_connection
;
183 IncomingHTTP2Connection::PendingQuery d_query
;
184 IncomingHTTP2Connection::StreamID d_streamID
{-1};
187 void IncomingHTTP2Connection::handleResponse(const struct timeval
& now
, TCPResponse
&& response
)
189 if (std::this_thread::get_id() != d_creatorThreadID
) {
190 handleCrossProtocolResponse(now
, std::move(response
));
194 auto& state
= response
.d_idstate
;
195 if (state
.forwardedOverUDP
) {
196 dnsheader_aligned
responseDH(response
.d_buffer
.data());
198 if (responseDH
.get()->tc
&& state
.d_packet
&& state
.d_packet
->size() > state
.d_proxyProtocolPayloadSize
&& state
.d_packet
->size() - state
.d_proxyProtocolPayloadSize
> sizeof(dnsheader
)) {
199 vinfolog("Response received from backend %s via UDP, for query %d received from %s via DoH, is truncated, retrying over TCP", response
.d_ds
->getNameWithAddr(), state
.d_streamID
, state
.origRemote
.toStringWithPort());
200 auto& query
= *state
.d_packet
;
201 dnsdist::PacketMangling::editDNSHeaderFromRawPacket(&query
.at(state
.d_proxyProtocolPayloadSize
), [origID
= state
.origID
](dnsheader
& header
) {
202 /* restoring the original ID */
207 state
.forwardedOverUDP
= false;
208 bool proxyProtocolPayloadAdded
= state
.d_proxyProtocolPayloadSize
> 0;
209 auto cpq
= getCrossProtocolQuery(std::move(query
), std::move(state
), response
.d_ds
);
210 cpq
->query
.d_proxyProtocolPayloadAdded
= proxyProtocolPayloadAdded
;
211 if (g_tcpclientthreads
&& g_tcpclientthreads
->passCrossProtocolQueryToThread(std::move(cpq
))) {
214 vinfolog("Unable to pass DoH query to a TCP worker thread after getting a TC response over UDP");
215 notifyIOError(now
, std::move(response
));
220 IncomingTCPConnectionState::handleResponse(now
, std::move(response
));
223 std::unique_ptr
<DOHUnitInterface
> IncomingHTTP2Connection::getDOHUnit(uint32_t streamID
)
225 // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-array-to-pointer-decay): clang-tidy is getting confused by assert()
226 assert(streamID
<= std::numeric_limits
<IncomingHTTP2Connection::StreamID
>::max());
227 // NOLINTNEXTLINE(*-narrowing-conversions): generic interface between DNS and DoH with different types
228 auto query
= std::move(d_currentStreams
.at(static_cast<IncomingHTTP2Connection::StreamID
>(streamID
)));
229 return std::make_unique
<IncomingDoHCrossProtocolContext
>(std::move(query
), std::dynamic_pointer_cast
<IncomingHTTP2Connection
>(shared_from_this()), streamID
);
232 void IncomingHTTP2Connection::restoreDOHUnit(std::unique_ptr
<DOHUnitInterface
>&& unit
)
234 auto context
= std::unique_ptr
<IncomingDoHCrossProtocolContext
>(dynamic_cast<IncomingDoHCrossProtocolContext
*>(unit
.release()));
236 d_currentStreams
.at(context
->d_streamID
) = std::move(context
->d_query
);
240 IncomingHTTP2Connection::IncomingHTTP2Connection(ConnectionInfo
&& connectionInfo
, TCPClientThreadData
& threadData
, const struct timeval
& now
) :
241 IncomingTCPConnectionState(std::move(connectionInfo
), threadData
, now
)
243 nghttp2_session_callbacks
* cbs
= nullptr;
244 if (nghttp2_session_callbacks_new(&cbs
) != 0) {
245 throw std::runtime_error("Unable to create a callback object for a new incoming HTTP/2 session");
247 std::unique_ptr
<nghttp2_session_callbacks
, void (*)(nghttp2_session_callbacks
*)> callbacks(cbs
, nghttp2_session_callbacks_del
);
250 nghttp2_session_callbacks_set_send_callback(callbacks
.get(), send_callback
);
251 nghttp2_session_callbacks_set_on_frame_recv_callback(callbacks
.get(), on_frame_recv_callback
);
252 nghttp2_session_callbacks_set_on_stream_close_callback(callbacks
.get(), on_stream_close_callback
);
253 nghttp2_session_callbacks_set_on_begin_headers_callback(callbacks
.get(), on_begin_headers_callback
);
254 nghttp2_session_callbacks_set_on_header_callback(callbacks
.get(), on_header_callback
);
255 nghttp2_session_callbacks_set_on_data_chunk_recv_callback(callbacks
.get(), on_data_chunk_recv_callback
);
256 nghttp2_session_callbacks_set_error_callback2(callbacks
.get(), on_error_callback
);
258 nghttp2_session
* sess
= nullptr;
259 if (nghttp2_session_server_new(&sess
, callbacks
.get(), this) != 0) {
260 throw std::runtime_error("Coult not allocate a new incoming HTTP/2 session");
263 d_session
= std::unique_ptr
<nghttp2_session
, decltype(&nghttp2_session_del
)>(sess
, nghttp2_session_del
);
267 bool IncomingHTTP2Connection::checkALPN()
269 constexpr std::array
<uint8_t, 2> h2ALPN
{'h', '2'};
270 const auto protocols
= d_handler
.getNextProtocol();
271 if (protocols
.size() == h2ALPN
.size() && memcmp(protocols
.data(), h2ALPN
.data(), h2ALPN
.size()) == 0) {
274 vinfolog("DoH connection from %s expected ALPN value 'h2', got '%s'", d_ci
.remote
.toStringWithPort(), std::string(protocols
.begin(), protocols
.end()));
278 void IncomingHTTP2Connection::handleConnectionReady()
280 constexpr std::array
<nghttp2_settings_entry
, 1> settings
{{{NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS
, 100U}}};
281 auto ret
= nghttp2_submit_settings(d_session
.get(), NGHTTP2_FLAG_NONE
, settings
.data(), settings
.size());
283 throw std::runtime_error("Fatal error: " + std::string(nghttp2_strerror(ret
)));
286 ret
= nghttp2_session_send(d_session
.get());
288 throw std::runtime_error("Fatal error: " + std::string(nghttp2_strerror(ret
)));
292 bool IncomingHTTP2Connection::hasPendingWrite() const
294 return d_pendingWrite
;
297 IOState
IncomingHTTP2Connection::handleHandshake(const struct timeval
& now
)
299 auto iostate
= d_handler
.tryHandshake();
300 if (iostate
== IOState::Done
) {
301 handleHandshakeDone(now
);
302 if (d_handler
.isTLS()) {
304 d_connectionDied
= true;
310 if (!isProxyPayloadOutsideTLS() && expectProxyProtocolFrom(d_ci
.remote
)) {
311 d_state
= State::readingProxyProtocolHeader
;
312 d_buffer
.resize(s_proxyProtocolMinimumHeaderSize
);
313 d_proxyProtocolNeed
= s_proxyProtocolMinimumHeaderSize
;
316 d_state
= State::waitingForQuery
;
317 handleConnectionReady();
323 void IncomingHTTP2Connection::handleIO()
325 IOState iostate
= IOState::Done
;
329 gettimeofday(&now
, nullptr);
332 if (maxConnectionDurationReached(g_maxTCPConnectionDuration
, now
)) {
333 vinfolog("Terminating DoH connection from %s because it reached the maximum TCP connection duration", d_ci
.remote
.toStringWithPort());
335 d_connectionClosing
= true;
339 if (d_state
== State::starting
) {
340 if (isProxyPayloadOutsideTLS() && expectProxyProtocolFrom(d_ci
.remote
)) {
341 d_state
= State::readingProxyProtocolHeader
;
342 d_buffer
.resize(s_proxyProtocolMinimumHeaderSize
);
343 d_proxyProtocolNeed
= s_proxyProtocolMinimumHeaderSize
;
346 d_state
= State::doingHandshake
;
350 if (d_state
== State::doingHandshake
) {
351 iostate
= handleHandshake(now
);
352 if (d_connectionDied
) {
357 if (d_state
== State::readingProxyProtocolHeader
) {
358 auto status
= handleProxyProtocolPayload();
359 if (status
== ProxyProtocolResult::Done
) {
360 if (isProxyPayloadOutsideTLS()) {
361 d_state
= State::doingHandshake
;
362 iostate
= handleHandshake(now
);
363 if (d_connectionDied
) {
369 d_proxyProtocolNeed
= 0;
371 d_state
= State::waitingForQuery
;
372 handleConnectionReady();
375 else if (status
== ProxyProtocolResult::Error
) {
376 d_connectionDied
= true;
382 if (active() && !d_connectionClosing
&& (d_state
== State::waitingForQuery
|| d_state
== State::idle
)) {
384 iostate
= readHTTPData();
385 } while (active() && !d_connectionClosing
&& iostate
== IOState::Done
);
394 - if we have a pending write, we need to wait until the socket becomes writable
395 and then call handleWritableCallback
396 - if we have NeedWrite but no pending write, we need to wait until the socket
397 becomes writable but for handleReadableIOCallback
398 - if we have NeedRead, or nghttp2_session_want_read, wait until the socket
399 becomes readable and call handleReadableIOCallback
401 if (hasPendingWrite()) {
402 updateIO(IOState::NeedWrite
, handleWritableIOCallback
);
404 else if (iostate
== IOState::NeedWrite
) {
405 updateIO(IOState::NeedWrite
, handleReadableIOCallback
);
407 else if (!d_connectionClosing
) {
408 if (nghttp2_session_want_read(d_session
.get()) != 0) {
409 updateIO(IOState::NeedRead
, handleReadableIOCallback
);
413 catch (const std::exception
& e
) {
414 vinfolog("Exception when processing IO for incoming DoH connection from %s: %s", d_ci
.remote
.toStringWithPort(), e
.what());
415 d_connectionDied
= true;
420 void IncomingHTTP2Connection::writeToSocket(bool socketReady
)
424 IOState newState
= d_handler
.tryWrite(d_out
, d_outPos
, d_out
.size());
426 if (newState
== IOState::Done
) {
427 d_pendingWrite
= false;
430 if (active() && !d_connectionClosing
) {
431 updateIO(IOState::NeedRead
, handleReadableIOCallback
);
438 updateIO(newState
, handleWritableIOCallback
);
439 d_pendingWrite
= true;
442 catch (const std::exception
& e
) {
443 vinfolog("Exception while trying to write (%s) to HTTP client connection to %s: %s", (socketReady
? "ready" : "send"), d_ci
.remote
.toStringWithPort(), e
.what());
448 ssize_t
IncomingHTTP2Connection::send_callback(nghttp2_session
* session
, const uint8_t* data
, size_t length
, int flags
, void* user_data
)
450 auto* conn
= static_cast<IncomingHTTP2Connection
*>(user_data
);
451 if (conn
->d_connectionDied
) {
452 return static_cast<ssize_t
>(length
);
454 // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic): nghttp2 API
455 conn
->d_out
.insert(conn
->d_out
.end(), data
, data
+ length
);
457 if (conn
->d_connectionClosing
|| conn
->d_needFlush
) {
458 conn
->writeToSocket(false);
461 return static_cast<ssize_t
>(length
);
464 static const std::array
<const std::string
, static_cast<size_t>(NGHTTP2Headers::HeaderConstantIndexes::COUNT
)> s_headerConstants
{
477 "application/dns-message",
480 "application/dns-message",
482 "nghttp2-" NGHTTP2_VERSION
"/dnsdist",
491 static const std::string
s_authorityHeaderName(":authority");
492 static const std::string
s_pathHeaderName(":path");
493 static const std::string
s_methodHeaderName(":method");
494 static const std::string
s_schemeHeaderName(":scheme");
495 static const std::string
s_xForwardedForHeaderName("x-forwarded-for");
497 void NGHTTP2Headers::addStaticHeader(std::vector
<nghttp2_nv
>& headers
, NGHTTP2Headers::HeaderConstantIndexes nameKey
, NGHTTP2Headers::HeaderConstantIndexes valueKey
)
499 const auto& name
= s_headerConstants
.at(static_cast<size_t>(nameKey
));
500 const auto& value
= s_headerConstants
.at(static_cast<size_t>(valueKey
));
502 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast,cppcoreguidelines-pro-type-reinterpret-cast): nghttp2 API
503 headers
.push_back({const_cast<uint8_t*>(reinterpret_cast<const uint8_t*>(name
.c_str())), const_cast<uint8_t*>(reinterpret_cast<const uint8_t*>(value
.c_str())), name
.size(), value
.size(), NGHTTP2_NV_FLAG_NO_COPY_NAME
| NGHTTP2_NV_FLAG_NO_COPY_VALUE
});
506 void NGHTTP2Headers::addCustomDynamicHeader(std::vector
<nghttp2_nv
>& headers
, const std::string
& name
, const std::string_view
& value
)
508 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast,cppcoreguidelines-pro-type-reinterpret-cast): nghttp2 API
509 headers
.push_back({const_cast<uint8_t*>(reinterpret_cast<const uint8_t*>(name
.data())), const_cast<uint8_t*>(reinterpret_cast<const uint8_t*>(value
.data())), name
.size(), value
.size(), NGHTTP2_NV_FLAG_NO_COPY_NAME
| NGHTTP2_NV_FLAG_NO_COPY_VALUE
});
512 void NGHTTP2Headers::addDynamicHeader(std::vector
<nghttp2_nv
>& headers
, NGHTTP2Headers::HeaderConstantIndexes nameKey
, const std::string_view
& value
)
514 const auto& name
= s_headerConstants
.at(static_cast<size_t>(nameKey
));
515 NGHTTP2Headers::addCustomDynamicHeader(headers
, name
, value
);
518 IOState
IncomingHTTP2Connection::sendResponse(const struct timeval
& now
, TCPResponse
&& response
)
520 // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-array-to-pointer-decay): clang-tidy is getting confused by assert()
521 assert(response
.d_idstate
.d_streamID
!= -1);
522 auto& context
= d_currentStreams
.at(response
.d_idstate
.d_streamID
);
524 uint32_t statusCode
= 200U;
525 std::string contentType
;
526 bool sendContentType
= true;
527 auto& responseBuffer
= context
.d_buffer
;
528 if (context
.d_statusCode
!= 0) {
529 responseBuffer
= std::move(context
.d_response
);
530 statusCode
= context
.d_statusCode
;
531 contentType
= std::move(context
.d_contentTypeOut
);
534 responseBuffer
= std::move(response
.d_buffer
);
537 sendResponse(response
.d_idstate
.d_streamID
, context
, statusCode
, d_ci
.cs
->dohFrontend
->d_customResponseHeaders
, contentType
, sendContentType
);
538 handleResponseSent(response
);
540 return hasPendingWrite() ? IOState::NeedWrite
: IOState::Done
;
543 void IncomingHTTP2Connection::notifyIOError(const struct timeval
& now
, TCPResponse
&& response
)
545 if (std::this_thread::get_id() != d_creatorThreadID
) {
546 /* empty buffer will signal an IO error */
547 response
.d_buffer
.clear();
548 handleCrossProtocolResponse(now
, std::move(response
));
552 // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-array-to-pointer-decay): clang-tidy is getting confused by assert()
553 assert(response
.d_idstate
.d_streamID
!= -1);
554 auto& context
= d_currentStreams
.at(response
.d_idstate
.d_streamID
);
555 context
.d_buffer
= std::move(response
.d_buffer
);
556 sendResponse(response
.d_idstate
.d_streamID
, context
, 502, d_ci
.cs
->dohFrontend
->d_customResponseHeaders
);
559 bool IncomingHTTP2Connection::sendResponse(IncomingHTTP2Connection::StreamID streamID
, IncomingHTTP2Connection::PendingQuery
& context
, uint16_t responseCode
, const HeadersMap
& customResponseHeaders
, const std::string
& contentType
, bool addContentType
)
561 /* if data_prd is not NULL, it provides data which will be sent in subsequent DATA frames. In this case, a method that allows request message bodies (https://tools.ietf.org/html/rfc7231#section-4) must be specified with :method key (e.g. POST). This function does not take ownership of the data_prd. The function copies the members of the data_prd. If data_prd is NULL, HEADERS have END_STREAM set.
563 nghttp2_data_provider data_provider
;
565 data_provider
.source
.ptr
= this;
566 data_provider
.read_callback
= [](nghttp2_session
*, IncomingHTTP2Connection::StreamID stream_id
, uint8_t* buf
, size_t length
, uint32_t* data_flags
, nghttp2_data_source
* source
, void* cb_data
) -> ssize_t
{
567 auto* connection
= static_cast<IncomingHTTP2Connection
*>(cb_data
);
568 auto& obj
= connection
->d_currentStreams
.at(stream_id
);
570 if (obj
.d_queryPos
< obj
.d_buffer
.size()) {
571 size_t remaining
= obj
.d_buffer
.size() - obj
.d_queryPos
;
572 toCopy
= length
> remaining
? remaining
: length
;
573 memcpy(buf
, &obj
.d_buffer
.at(obj
.d_queryPos
), toCopy
);
574 obj
.d_queryPos
+= toCopy
;
577 if (obj
.d_queryPos
>= obj
.d_buffer
.size()) {
578 *data_flags
|= NGHTTP2_DATA_FLAG_EOF
;
579 obj
.d_buffer
.clear();
580 connection
->d_needFlush
= true;
582 return static_cast<ssize_t
>(toCopy
);
585 const auto& dohFrontend
= d_ci
.cs
->dohFrontend
;
586 auto& responseBody
= context
.d_buffer
;
588 std::vector
<nghttp2_nv
> headers
;
589 std::string responseCodeStr
;
590 std::string cacheControlValue
;
591 std::string location
;
592 /* remember that dynamic header values should be kept alive
593 until we have called nghttp2_submit_response(), at least */
594 /* status, content-type, cache-control, content-length */
597 if (responseCode
== 200) {
598 NGHTTP2Headers::addStaticHeader(headers
, NGHTTP2Headers::HeaderConstantIndexes::STATUS_NAME
, NGHTTP2Headers::HeaderConstantIndexes::OK_200_VALUE
);
599 ++dohFrontend
->d_validresponses
;
600 ++dohFrontend
->d_http2Stats
.d_nb200Responses
;
602 if (addContentType
) {
603 if (contentType
.empty()) {
604 NGHTTP2Headers::addStaticHeader(headers
, NGHTTP2Headers::HeaderConstantIndexes::CONTENT_TYPE_NAME
, NGHTTP2Headers::HeaderConstantIndexes::CONTENT_TYPE_VALUE
);
607 NGHTTP2Headers::addDynamicHeader(headers
, NGHTTP2Headers::HeaderConstantIndexes::CONTENT_TYPE_NAME
, contentType
);
611 if (dohFrontend
->d_sendCacheControlHeaders
&& responseBody
.size() > sizeof(dnsheader
)) {
612 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): API
613 uint32_t minTTL
= getDNSPacketMinTTL(reinterpret_cast<const char*>(responseBody
.data()), responseBody
.size());
614 if (minTTL
!= std::numeric_limits
<uint32_t>::max()) {
615 cacheControlValue
= "max-age=" + std::to_string(minTTL
);
616 NGHTTP2Headers::addDynamicHeader(headers
, NGHTTP2Headers::HeaderConstantIndexes::CACHE_CONTROL_NAME
, cacheControlValue
);
621 responseCodeStr
= std::to_string(responseCode
);
622 NGHTTP2Headers::addDynamicHeader(headers
, NGHTTP2Headers::HeaderConstantIndexes::STATUS_NAME
, responseCodeStr
);
624 if (responseCode
>= 300 && responseCode
< 400) {
625 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
626 location
= std::string(reinterpret_cast<const char*>(responseBody
.data()), responseBody
.size());
627 NGHTTP2Headers::addDynamicHeader(headers
, NGHTTP2Headers::HeaderConstantIndexes::CONTENT_TYPE_NAME
, "text/html; charset=utf-8");
628 NGHTTP2Headers::addDynamicHeader(headers
, NGHTTP2Headers::HeaderConstantIndexes::LOCATION_NAME
, location
);
629 static const std::string s_redirectStart
{"<!DOCTYPE html><TITLE>Moved</TITLE><P>The document has moved <A HREF=\""};
630 static const std::string s_redirectEnd
{"\">here</A>"};
631 responseBody
.reserve(s_redirectStart
.size() + responseBody
.size() + s_redirectEnd
.size());
632 responseBody
.insert(responseBody
.begin(), s_redirectStart
.begin(), s_redirectStart
.end());
633 responseBody
.insert(responseBody
.end(), s_redirectEnd
.begin(), s_redirectEnd
.end());
634 ++dohFrontend
->d_redirectresponses
;
637 ++dohFrontend
->d_errorresponses
;
638 switch (responseCode
) {
640 ++dohFrontend
->d_http2Stats
.d_nb400Responses
;
643 ++dohFrontend
->d_http2Stats
.d_nb403Responses
;
646 ++dohFrontend
->d_http2Stats
.d_nb500Responses
;
649 ++dohFrontend
->d_http2Stats
.d_nb502Responses
;
652 ++dohFrontend
->d_http2Stats
.d_nbOtherResponses
;
656 if (!responseBody
.empty()) {
657 NGHTTP2Headers::addDynamicHeader(headers
, NGHTTP2Headers::HeaderConstantIndexes::CONTENT_TYPE_NAME
, "text/plain; charset=utf-8");
660 static const std::string invalid
{"invalid DNS query"};
661 static const std::string notAllowed
{"dns query not allowed"};
662 static const std::string noDownstream
{"no downstream server available"};
663 static const std::string internalServerError
{"Internal Server Error"};
665 switch (responseCode
) {
667 responseBody
.insert(responseBody
.begin(), invalid
.begin(), invalid
.end());
670 responseBody
.insert(responseBody
.begin(), notAllowed
.begin(), notAllowed
.end());
673 responseBody
.insert(responseBody
.begin(), noDownstream
.begin(), noDownstream
.end());
678 responseBody
.insert(responseBody
.begin(), internalServerError
.begin(), internalServerError
.end());
685 const std::string contentLength
= std::to_string(responseBody
.size());
686 NGHTTP2Headers::addDynamicHeader(headers
, NGHTTP2Headers::HeaderConstantIndexes::CONTENT_LENGTH_NAME
, contentLength
);
688 for (const auto& [key
, value
] : customResponseHeaders
) {
689 NGHTTP2Headers::addCustomDynamicHeader(headers
, key
, value
);
692 auto ret
= nghttp2_submit_response(d_session
.get(), streamID
, headers
.data(), headers
.size(), &data_provider
);
694 d_currentStreams
.erase(streamID
);
695 vinfolog("Error submitting HTTP response for stream %d: %s", streamID
, nghttp2_strerror(ret
));
699 ret
= nghttp2_session_send(d_session
.get());
701 d_currentStreams
.erase(streamID
);
702 vinfolog("Error flushing HTTP response for stream %d: %s", streamID
, nghttp2_strerror(ret
));
709 static void processForwardedForHeader(const std::unique_ptr
<HeadersMap
>& headers
, ComboAddress
& remote
)
715 auto headerIt
= headers
->find(s_xForwardedForHeaderName
);
716 if (headerIt
== headers
->end()) {
720 std::string_view value
= headerIt
->second
;
722 auto pos
= value
.rfind(',');
723 if (pos
!= std::string_view::npos
) {
725 for (; pos
< value
.size() && value
[pos
] == ' '; ++pos
) {
728 if (pos
< value
.size()) {
729 value
= value
.substr(pos
);
732 auto newRemote
= ComboAddress(std::string(value
));
735 catch (const std::exception
& e
) {
736 vinfolog("Invalid X-Forwarded-For header ('%s') received from %s : %s", std::string(value
), remote
.toStringWithPort(), e
.what());
738 catch (const PDNSException
& e
) {
739 vinfolog("Invalid X-Forwarded-For header ('%s') received from %s : %s", std::string(value
), remote
.toStringWithPort(), e
.reason
);
743 static std::optional
<PacketBuffer
> getPayloadFromPath(const std::string_view
& path
)
745 std::optional
<PacketBuffer
> result
{std::nullopt
};
747 if (path
.size() <= 5) {
751 auto pos
= path
.find("?dns=");
752 if (pos
== string::npos
) {
753 pos
= path
.find("&dns=");
756 if (pos
== string::npos
) {
760 // need to base64url decode this
762 const size_t payloadSize
= path
.size() - pos
- 5;
763 size_t neededPadding
= 0;
764 switch (payloadSize
% 4) {
772 sdns
.reserve(payloadSize
+ neededPadding
);
773 sdns
= path
.substr(pos
+ 5);
774 for (auto& entry
: sdns
) {
785 if (neededPadding
!= 0) {
786 // re-add padding that may have been missing
787 sdns
.append(neededPadding
, '=');
790 PacketBuffer decoded
;
791 /* rough estimate so we hopefully don't need a new allocation later */
792 /* We reserve at few additional bytes to be able to add EDNS later */
793 const size_t estimate
= ((sdns
.size() * 3) / 4);
794 decoded
.reserve(estimate
);
795 if (B64Decode(sdns
, decoded
) < 0) {
799 result
= std::move(decoded
);
803 void IncomingHTTP2Connection::handleIncomingQuery(IncomingHTTP2Connection::PendingQuery
&& query
, IncomingHTTP2Connection::StreamID streamID
)
805 const auto handleImmediateResponse
= [this, &query
, streamID
](uint16_t code
, const std::string
& reason
, PacketBuffer
&& response
= PacketBuffer()) {
806 if (response
.empty()) {
807 query
.d_buffer
.clear();
808 query
.d_buffer
.insert(query
.d_buffer
.begin(), reason
.begin(), reason
.end());
811 query
.d_buffer
= std::move(response
);
813 vinfolog("Sending an immediate %d response to incoming DoH query: %s", code
, reason
);
814 sendResponse(streamID
, query
, code
, d_ci
.cs
->dohFrontend
->d_customResponseHeaders
);
817 if (query
.d_method
== PendingQuery::Method::Unknown
|| query
.d_method
== PendingQuery::Method::Unsupported
) {
818 handleImmediateResponse(400, "DoH query not allowed because of unsupported HTTP method");
822 ++d_ci
.cs
->dohFrontend
->d_http2Stats
.d_nbQueries
;
824 if (d_ci
.cs
->dohFrontend
->d_trustForwardedForHeader
) {
825 processForwardedForHeader(query
.d_headers
, d_proxiedRemote
);
827 /* second ACL lookup based on the updated address */
828 auto& holders
= d_threadData
.holders
;
829 if (!holders
.acl
->match(d_proxiedRemote
)) {
830 ++dnsdist::metrics::g_stats
.aclDrops
;
831 vinfolog("Query from %s (%s) (DoH) dropped because of ACL", d_ci
.remote
.toStringWithPort(), d_proxiedRemote
.toStringWithPort());
832 handleImmediateResponse(403, "DoH query not allowed because of ACL");
836 if (!d_ci
.cs
->dohFrontend
->d_keepIncomingHeaders
) {
837 query
.d_headers
.reset();
841 if (d_ci
.cs
->dohFrontend
->d_exactPathMatching
) {
842 if (d_ci
.cs
->dohFrontend
->d_urls
.count(query
.d_path
) == 0) {
843 handleImmediateResponse(404, "there is no endpoint configured for this path");
849 for (const auto& path
: d_ci
.cs
->dohFrontend
->d_urls
) {
850 if (boost::starts_with(query
.d_path
, path
)) {
856 handleImmediateResponse(404, "there is no endpoint configured for this path");
861 /* the responses map can be updated at runtime, so we need to take a copy of
862 the shared pointer, increasing the reference counter */
863 auto responsesMap
= d_ci
.cs
->dohFrontend
->d_responsesMap
;
865 for (const auto& entry
: *responsesMap
) {
866 if (entry
->matches(query
.d_path
)) {
867 const auto& customHeaders
= entry
->getHeaders();
868 query
.d_buffer
= entry
->getContent();
869 if (entry
->getStatusCode() >= 400 && !query
.d_buffer
.empty()) {
870 // legacy trailing 0 from the h2o era
871 query
.d_buffer
.pop_back();
874 sendResponse(streamID
, query
, entry
->getStatusCode(), customHeaders
? *customHeaders
: d_ci
.cs
->dohFrontend
->d_customResponseHeaders
, std::string(), false);
880 if (query
.d_buffer
.empty() && query
.d_method
== PendingQuery::Method::Get
&& !query
.d_queryString
.empty()) {
881 auto payload
= getPayloadFromPath(query
.d_queryString
);
883 query
.d_buffer
= std::move(*payload
);
886 ++d_ci
.cs
->dohFrontend
->d_badrequests
;
887 handleImmediateResponse(400, "DoH unable to decode BASE64-URL");
892 if (query
.d_method
== PendingQuery::Method::Get
) {
893 ++d_ci
.cs
->dohFrontend
->d_getqueries
;
895 else if (query
.d_method
== PendingQuery::Method::Post
) {
896 ++d_ci
.cs
->dohFrontend
->d_postqueries
;
903 gettimeofday(&now
, nullptr);
904 auto processingResult
= handleQuery(std::move(query
.d_buffer
), now
, streamID
);
906 switch (processingResult
) {
907 case QueryProcessingResult::TooSmall
:
908 handleImmediateResponse(400, "DoH non-compliant query");
910 case QueryProcessingResult::InvalidHeaders
:
911 handleImmediateResponse(400, "DoH invalid headers");
913 case QueryProcessingResult::Dropped
:
914 handleImmediateResponse(403, "DoH dropped query");
916 case QueryProcessingResult::NoBackend
:
917 handleImmediateResponse(502, "DoH no backend available");
919 case QueryProcessingResult::Forwarded
:
920 case QueryProcessingResult::Asynchronous
:
921 case QueryProcessingResult::SelfAnswered
:
925 catch (const std::exception
& e
) {
926 vinfolog("Exception while processing DoH query: %s", e
.what());
927 handleImmediateResponse(400, "DoH non-compliant query");
932 int IncomingHTTP2Connection::on_frame_recv_callback(nghttp2_session
* session
, const nghttp2_frame
* frame
, void* user_data
)
934 auto* conn
= static_cast<IncomingHTTP2Connection
*>(user_data
);
935 /* is this the last frame for this stream? */
936 if ((frame
->hd
.type
== NGHTTP2_HEADERS
|| frame
->hd
.type
== NGHTTP2_DATA
) && (frame
->hd
.flags
& NGHTTP2_FLAG_END_STREAM
) != 0) {
937 auto streamID
= frame
->hd
.stream_id
;
938 auto stream
= conn
->d_currentStreams
.find(streamID
);
939 if (stream
!= conn
->d_currentStreams
.end()) {
940 conn
->handleIncomingQuery(std::move(stream
->second
), streamID
);
943 vinfolog("Stream %d NOT FOUND", streamID
);
944 return NGHTTP2_ERR_CALLBACK_FAILURE
;
951 int IncomingHTTP2Connection::on_stream_close_callback(nghttp2_session
* session
, IncomingHTTP2Connection::StreamID stream_id
, uint32_t error_code
, void* user_data
)
953 auto* conn
= static_cast<IncomingHTTP2Connection
*>(user_data
);
955 conn
->d_currentStreams
.erase(stream_id
);
959 int IncomingHTTP2Connection::on_begin_headers_callback(nghttp2_session
* session
, const nghttp2_frame
* frame
, void* user_data
)
961 if (frame
->hd
.type
!= NGHTTP2_HEADERS
|| frame
->headers
.cat
!= NGHTTP2_HCAT_REQUEST
) {
965 auto* conn
= static_cast<IncomingHTTP2Connection
*>(user_data
);
966 auto insertPair
= conn
->d_currentStreams
.emplace(frame
->hd
.stream_id
, PendingQuery());
967 if (!insertPair
.second
) {
968 /* there is a stream ID collision, something is very wrong! */
969 vinfolog("Stream ID collision (%d) on connection from %d", frame
->hd
.stream_id
, conn
->d_ci
.remote
.toStringWithPort());
970 conn
->d_connectionClosing
= true;
971 conn
->d_needFlush
= true;
972 nghttp2_session_terminate_session(conn
->d_session
.get(), NGHTTP2_NO_ERROR
);
973 auto ret
= nghttp2_session_send(conn
->d_session
.get());
975 vinfolog("Error flushing HTTP response for stream %d from %s: %s", frame
->hd
.stream_id
, conn
->d_ci
.remote
.toStringWithPort(), nghttp2_strerror(ret
));
976 return NGHTTP2_ERR_CALLBACK_FAILURE
;
985 static std::string::size_type
getLengthOfPathWithoutParameters(const std::string_view
& path
)
987 auto pos
= path
.find('?');
988 if (pos
== string::npos
) {
995 int IncomingHTTP2Connection::on_header_callback(nghttp2_session
* session
, const nghttp2_frame
* frame
, const uint8_t* name
, size_t nameLen
, const uint8_t* value
, size_t valuelen
, uint8_t flags
, void* user_data
)
997 auto* conn
= static_cast<IncomingHTTP2Connection
*>(user_data
);
999 if (frame
->hd
.type
== NGHTTP2_HEADERS
&& frame
->headers
.cat
== NGHTTP2_HCAT_REQUEST
) {
1000 if (nghttp2_check_header_name(name
, nameLen
) == 0) {
1001 vinfolog("Invalid header name");
1002 return NGHTTP2_ERR_CALLBACK_FAILURE
;
1005 #if HAVE_NGHTTP2_CHECK_HEADER_VALUE_RFC9113
1006 if (nghttp2_check_header_value_rfc9113(value
, valuelen
) == 0) {
1007 vinfolog("Invalid header value");
1008 return NGHTTP2_ERR_CALLBACK_FAILURE
;
1010 #endif /* HAVE_NGHTTP2_CHECK_HEADER_VALUE_RFC9113 */
1012 auto headerMatches
= [name
, nameLen
](const std::string
& expected
) -> bool {
1013 return nameLen
== expected
.size() && memcmp(name
, expected
.data(), expected
.size()) == 0;
1016 auto stream
= conn
->d_currentStreams
.find(frame
->hd
.stream_id
);
1017 if (stream
== conn
->d_currentStreams
.end()) {
1018 vinfolog("Unable to match the stream ID %d to a known one!", frame
->hd
.stream_id
);
1019 return NGHTTP2_ERR_CALLBACK_FAILURE
;
1021 auto& query
= stream
->second
;
1022 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): nghttp2 API
1023 auto valueView
= std::string_view(reinterpret_cast<const char*>(value
), valuelen
);
1024 if (headerMatches(s_pathHeaderName
)) {
1025 #if HAVE_NGHTTP2_CHECK_PATH
1026 if (nghttp2_check_path(value
, valuelen
) == 0) {
1027 vinfolog("Invalid path value");
1028 return NGHTTP2_ERR_CALLBACK_FAILURE
;
1030 #endif /* HAVE_NGHTTP2_CHECK_PATH */
1032 auto pathLen
= getLengthOfPathWithoutParameters(valueView
);
1033 query
.d_path
= valueView
.substr(0, pathLen
);
1034 if (pathLen
< valueView
.size()) {
1035 query
.d_queryString
= valueView
.substr(pathLen
);
1038 else if (headerMatches(s_authorityHeaderName
)) {
1039 query
.d_host
= valueView
;
1041 else if (headerMatches(s_schemeHeaderName
)) {
1042 query
.d_scheme
= valueView
;
1044 else if (headerMatches(s_methodHeaderName
)) {
1045 #if HAVE_NGHTTP2_CHECK_METHOD
1046 if (nghttp2_check_method(value
, valuelen
) == 0) {
1047 vinfolog("Invalid method value");
1048 return NGHTTP2_ERR_CALLBACK_FAILURE
;
1050 #endif /* HAVE_NGHTTP2_CHECK_METHOD */
1052 if (valueView
== "GET") {
1053 query
.d_method
= PendingQuery::Method::Get
;
1055 else if (valueView
== "POST") {
1056 query
.d_method
= PendingQuery::Method::Post
;
1059 query
.d_method
= PendingQuery::Method::Unsupported
;
1060 vinfolog("Unsupported method value");
1065 if (conn
->d_ci
.cs
->dohFrontend
->d_keepIncomingHeaders
|| (conn
->d_ci
.cs
->dohFrontend
->d_trustForwardedForHeader
&& headerMatches(s_xForwardedForHeaderName
))) {
1066 if (!query
.d_headers
) {
1067 query
.d_headers
= std::make_unique
<HeadersMap
>();
1069 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): nghttp2 API
1070 query
.d_headers
->insert({std::string(reinterpret_cast<const char*>(name
), nameLen
), std::string(valueView
)});
1076 int IncomingHTTP2Connection::on_data_chunk_recv_callback(nghttp2_session
* session
, uint8_t flags
, IncomingHTTP2Connection::StreamID stream_id
, const uint8_t* data
, size_t len
, void* user_data
)
1078 auto* conn
= static_cast<IncomingHTTP2Connection
*>(user_data
);
1079 auto stream
= conn
->d_currentStreams
.find(stream_id
);
1080 if (stream
== conn
->d_currentStreams
.end()) {
1081 vinfolog("Unable to match the stream ID %d to a known one!", stream_id
);
1082 return NGHTTP2_ERR_CALLBACK_FAILURE
;
1084 if (len
> std::numeric_limits
<uint16_t>::max() || (std::numeric_limits
<uint16_t>::max() - stream
->second
.d_buffer
.size()) < len
) {
1085 vinfolog("Data frame of size %d is too large for a DNS query (we already have %d)", len
, stream
->second
.d_buffer
.size());
1086 return NGHTTP2_ERR_CALLBACK_FAILURE
;
1089 // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic): nghttp2 API
1090 stream
->second
.d_buffer
.insert(stream
->second
.d_buffer
.end(), data
, data
+ len
);
1095 int IncomingHTTP2Connection::on_error_callback(nghttp2_session
* session
, int lib_error_code
, const char* msg
, size_t len
, void* user_data
)
1097 auto* conn
= static_cast<IncomingHTTP2Connection
*>(user_data
);
1099 vinfolog("Error in HTTP/2 connection from %d: %s", conn
->d_ci
.remote
.toStringWithPort(), std::string(msg
, len
));
1100 conn
->d_connectionClosing
= true;
1101 conn
->d_needFlush
= true;
1102 nghttp2_session_terminate_session(conn
->d_session
.get(), NGHTTP2_NO_ERROR
);
1103 auto ret
= nghttp2_session_send(conn
->d_session
.get());
1105 vinfolog("Error flushing HTTP response on connection from %s: %s", conn
->d_ci
.remote
.toStringWithPort(), nghttp2_strerror(ret
));
1106 return NGHTTP2_ERR_CALLBACK_FAILURE
;
1112 IOState
IncomingHTTP2Connection::readHTTPData()
1114 IOState newState
= IOState::Done
;
1116 if (d_in
.size() < s_initialReceiveBufferSize
) {
1117 d_in
.resize(std::max(s_initialReceiveBufferSize
, d_in
.capacity()));
1120 newState
= d_handler
.tryRead(d_in
, got
, d_in
.size(), true);
1124 /* we got something */
1125 auto readlen
= nghttp2_session_mem_recv(d_session
.get(), d_in
.data(), d_in
.size());
1126 /* as long as we don't require a pause by returning nghttp2_error.NGHTTP2_ERR_PAUSE from a CB,
1127 all data should be consumed before returning */
1128 if (readlen
< 0 || static_cast<size_t>(readlen
) < d_in
.size()) {
1129 throw std::runtime_error("Fatal error while passing received data to nghttp2: " + std::string(nghttp2_strerror((int)readlen
)));
1132 nghttp2_session_send(d_session
.get());
1135 catch (const std::exception
& e
) {
1136 vinfolog("Exception while trying to read from HTTP client connection to %s: %s", d_ci
.remote
.toStringWithPort(), e
.what());
1138 return IOState::Done
;
1143 void IncomingHTTP2Connection::handleReadableIOCallback([[maybe_unused
]] int descriptor
, FDMultiplexer::funcparam_t
& param
)
1145 auto conn
= boost::any_cast
<std::shared_ptr
<IncomingHTTP2Connection
>>(param
);
1149 void IncomingHTTP2Connection::handleWritableIOCallback([[maybe_unused
]] int descriptor
, FDMultiplexer::funcparam_t
& param
)
1151 auto conn
= boost::any_cast
<std::shared_ptr
<IncomingHTTP2Connection
>>(param
);
1152 conn
->writeToSocket(true);
1155 void IncomingHTTP2Connection::stopIO()
1160 uint32_t IncomingHTTP2Connection::getConcurrentStreamsCount() const
1162 return d_currentStreams
.size();
1165 boost::optional
<struct timeval
> IncomingHTTP2Connection::getIdleClientReadTTD(struct timeval now
) const
1167 auto idleTimeout
= d_ci
.cs
->dohFrontend
->d_idleTimeout
;
1168 if (g_maxTCPConnectionDuration
== 0 && idleTimeout
== 0) {
1172 if (g_maxTCPConnectionDuration
> 0) {
1173 auto elapsed
= now
.tv_sec
- d_connectionStartTime
.tv_sec
;
1174 if (elapsed
< 0 || (static_cast<size_t>(elapsed
) >= g_maxTCPConnectionDuration
)) {
1177 auto remaining
= g_maxTCPConnectionDuration
- elapsed
;
1178 if (idleTimeout
== 0 || remaining
<= static_cast<size_t>(idleTimeout
)) {
1179 now
.tv_sec
+= static_cast<time_t>(remaining
);
1184 now
.tv_sec
+= idleTimeout
;
1188 void IncomingHTTP2Connection::updateIO(IOState newState
, const FDMultiplexer::callbackfunc_t
& callback
)
1190 boost::optional
<struct timeval
> ttd
{boost::none
};
1192 auto shared
= std::dynamic_pointer_cast
<IncomingHTTP2Connection
>(shared_from_this());
1197 gettimeofday(&now
, nullptr);
1199 if (newState
== IOState::NeedRead
) {
1200 /* use the idle TTL if the handshake has been completed (and proxy protocol payload received, if any),
1201 and we have processed at least one query, otherwise we use the shorter read TTL */
1202 if ((d_state
== State::waitingForQuery
|| d_state
== State::idle
) && (d_queriesCount
> 0 || d_currentQueriesCount
> 0)) {
1203 ttd
= getIdleClientReadTTD(now
);
1206 ttd
= getClientReadTTD(now
);
1208 d_ioState
->update(newState
, callback
, shared
, ttd
);
1210 else if (newState
== IOState::NeedWrite
) {
1211 ttd
= getClientWriteTTD(now
);
1212 d_ioState
->update(newState
, callback
, shared
, ttd
);
1217 void IncomingHTTP2Connection::handleIOError()
1219 d_connectionDied
= true;
1222 nghttp2_session_terminate_session(d_session
.get(), NGHTTP2_PROTOCOL_ERROR
);
1223 d_currentStreams
.clear();
1227 bool IncomingHTTP2Connection::active() const
1229 return !d_connectionDied
&& d_ioState
!= nullptr;
1232 #endif /* HAVE_DNS_OVER_HTTPS && HAVE_NGHTTP2 */