Skip to content

Commit 0b425e7

Browse files
Merge pull request #28 from CopernicaMarketingSoftware/buffer-sizes
Prevent one buffer copy
2 parents dfe8e97 + a393de5 commit 0b425e7

File tree

9 files changed

+129
-110
lines changed

9 files changed

+129
-110
lines changed

include/dnscpp/message.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ class Message
168168
* Is the message truncated? In that case it is better to use tcp
169169
* @return bool
170170
*/
171-
bool truncated() const { return flag(ns_f_tc); }
171+
bool truncated() const { /*if (rand() % 2 == 0) return true; else*/ return flag(ns_f_tc); }
172172

173173
/**
174174
* Is recursion desired (false for responses)

include/dnscpp/query.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@
2424
*/
2525
namespace DNS {
2626

27+
// we advertise that we support 1200 bytes for our response buffer size,
28+
// this is the same buffer size as libresolv seems to use. Their ratio
29+
// is that this limits the risk that dgram message get fragmented,
30+
// which makes the system vulnerable for injection
31+
constexpr size_t EDNSPacketSize = 1200;
32+
2733
/**
2834
* Forward declarations
2935
*/

include/dnscpp/socket.h

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ class Socket : public Inbound, public Watchable
5656
* All the buffered responses that came in
5757
* @var std::list
5858
*/
59-
std::list<std::pair<Ip,std::basic_string<unsigned char>>> _responses;
59+
std::list<std::pair<Ip,std::vector<unsigned char>>> _responses;
6060

6161
protected:
6262
/**
@@ -74,10 +74,9 @@ class Socket : public Inbound, public Watchable
7474
* Add a message for delayed processing
7575
* @param addr the address from which the message was received
7676
* @param buffer the response buffer
77-
* @param size size of the buffer
7877
*/
79-
void add(const sockaddr *addr, const unsigned char *buffer, size_t size);
80-
void add(const Ip &addr, const unsigned char *buffer, size_t size);
78+
void add(const sockaddr *addr, std::vector<unsigned char> &&buffer);
79+
void add(const Ip &addr, std::vector<unsigned char> &&buffer);
8180

8281
public:
8382
/**

include/dnscpp/tcp.h

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -71,19 +71,25 @@ class Tcp : public Socket, private Monitor
7171
* @var int
7272
*/
7373
int _fd;
74-
74+
75+
/**
76+
* Size of the buffer to fill
77+
* @var uint16_t
78+
*/
79+
uint16_t _size;
80+
7581
/**
76-
* The buffer that is being filled right now (first two bytes contain the size)
77-
* @var unsigned char *
82+
* The response that is being filled right now
83+
* @var vector
7884
*/
7985
std::vector<unsigned char> _buffer;
8086

8187
/**
82-
* How far is the _buffer now filled?
88+
* How many bytes of the frame were transferred?
8389
* @var size_t
8490
*/
85-
size_t _filled = 0;
86-
91+
size_t _transferred = 0;
92+
8793
/**
8894
* Identifier user for monitoring the filedescriptor in the event loop
8995
* @var void *
@@ -140,23 +146,18 @@ class Tcp : public Socket, private Monitor
140146
*/
141147
virtual void notify() override;
142148

143-
/**
144-
* Reallocate the buffer if it turns out that our buffer is too small for the expected response
145-
* @return bool
146-
*/
147-
bool reallocate();
148-
149-
/**
150-
* Size of the response -- this method only works if we have already received the frist two bytes
151-
* @return uint16_t
152-
*/
153-
uint16_t responsesize() const;
154-
155149
/**
156150
* Number of bytes that we expect in the next read operation
157151
* @return size_t
158152
*/
159153
size_t expected() const;
154+
155+
/**
156+
* Check return value of a recv syscall
157+
* @param bytes The bytes transferred
158+
* @return true if we should leap out (an error occurred or we'd block), false if not
159+
*/
160+
bool updatetransferred(ssize_t bytes);
160161

161162
/**
162163
* Method that is called when there are no more subscribers, and that

src/query.cpp

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -194,12 +194,9 @@ bool Query::edns(bool dnssec)
194194

195195
// the type of the pseudo-record is "opt"
196196
put16(TYPE_OPT);
197-
198-
// we advertise that we support 1200 bytes for our response buffer size,
199-
// this is the same buffer size as libresolv seems to use. Their ratio
200-
// is that this limits the risk that dgram message get fragmented,
201-
// which makes the system vulnerable for injection
202-
put16(1200);
197+
198+
// tell the server that we support larger UDP packets
199+
put16(EDNSPacketSize);
203200

204201
// extended rcode (0 because the normal rcode is good enough) and the
205202
// edns version (also 0 because that is the mose up-to-date version)

src/socket.cpp

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,16 @@ namespace DNS {
2424
* Add a message for delayed processing
2525
* @param addr the address from which the message was received
2626
* @param buffer the response buffer
27-
* @param size size of the buffer
2827
*/
29-
void Socket::add(const sockaddr *addr, const unsigned char *buffer, size_t size)
28+
void Socket::add(const sockaddr *addr, std::vector<unsigned char> &&buffer)
3029
{
3130
// avoid exceptions (in case the ip cannot be parsed)
3231
try
3332
{
3433
// @todo inbound messages that do not come from port 53 can be ignored
3534

3635
// remember the response for now
37-
// @todo make this more efficient (without all the string-copying)
38-
_responses.emplace_back(addr, std::basic_string<unsigned char>(buffer, size));
36+
_responses.emplace_back(addr, move(buffer));
3937
}
4038
catch (...)
4139
{
@@ -50,12 +48,11 @@ void Socket::add(const sockaddr *addr, const unsigned char *buffer, size_t size)
5048
* Add a message for delayed processing
5149
* @param addr the address from which the message was received
5250
* @param buffer the response buffer
53-
* @param size size of the buffer
5451
*/
55-
void Socket::add(const Ip &addr, const unsigned char *buffer, size_t size)
52+
void Socket::add(const Ip &addr, std::vector<unsigned char> &&buffer)
5653
{
5754
// add to the list
58-
_responses.emplace_back(addr, std::basic_string<unsigned char>(buffer, size));
55+
_responses.emplace_back(addr, move(buffer));
5956

6057
// reschedule the processing of messages
6158
_handler->onBuffered(this);

src/tcp.cpp

Lines changed: 60 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -174,52 +174,20 @@ Inbound *Tcp::send(const Query &query)
174174
return result2 >= (ssize_t)query.size() ? this : nullptr;
175175
}
176176

177-
/**
178-
* Size of the response -- this method only works if we have already received the frist two bytes
179-
* @return uint16_t
180-
*/
181-
uint16_t Tcp::responsesize() const
182-
{
183-
// result variable
184-
uint16_t result;
185-
186-
// get the first two bytes from the buffer
187-
memcpy(&result, _buffer.data(), 2);
188-
189-
// put the bytes in the right order
190-
return ntohs(result);
191-
}
192-
193177
/**
194178
* Number of bytes that we expect in the next read operation
195179
* @return size_t
196180
*/
197181
size_t Tcp::expected() const
198182
{
199183
// if we have not yet received the first two bytes, we expect those first
200-
switch (_filled) {
201-
case 0: return 2;
202-
case 1: return 1;
203-
default: return responsesize() - (_filled - 2);
184+
switch (_transferred) {
185+
case 0: return sizeof(uint16_t);
186+
case 1: return sizeof(uint16_t) - 1;
187+
default: return _size - (_transferred - sizeof(uint16_t));
204188
}
205189
}
206190

207-
/**
208-
* Reallocate the buffer if it turns out that our buffer is too small for the expected response
209-
* @return bool
210-
*/
211-
bool Tcp::reallocate()
212-
{
213-
// preferred buffer size
214-
size_t preferred = responsesize() + 2;
215-
216-
// reallocate the buffer (but do not shrink)
217-
_buffer.resize(std::max(_buffer.size(), preferred));
218-
219-
// report result
220-
return true;
221-
}
222-
223191
/**
224192
* Upgrate the socket from a _connecting_ socket to a _connected_ socket
225193
*/
@@ -230,10 +198,7 @@ void Tcp::upgrade()
230198

231199
// if the connection failed
232200
if (!_connected) return fail();
233-
234-
// already allocate enough data for the first two bytes (holding the size of a response)
235-
_buffer.resize(2);
236-
201+
237202
// we no longer monitor for writability, but for readability instead
238203
_loop->update(_identifier, _fd, 1, this);
239204

@@ -268,6 +233,26 @@ void Tcp::fail()
268233
_handler->onBuffered(this);
269234
}
270235

236+
/**
237+
* Check return value of a recv syscall
238+
* @param bytes The bytes transferred
239+
* @return true if we should leap out (an error occurred), false if not
240+
*/
241+
bool Tcp::updatetransferred(ssize_t result)
242+
{
243+
// the operation would block, but don't leap out
244+
if (result < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) return false;
245+
246+
// if there is a failure we leap out as well
247+
if (result <= 0) return fail(), true;
248+
249+
// update the number of transferred bytes
250+
_transferred += result;
251+
252+
// don't leap out
253+
return false;
254+
}
255+
271256
/**
272257
* Method that is called when the socket becomes active (readable in our case)
273258
*/
@@ -279,30 +264,45 @@ void Tcp::notify()
279264

280265
// if the socket is not yet connected, it might be connected right now
281266
if (!_connected) return upgrade();
282-
283-
// receive data from the socket
284-
auto result = ::recv(_fd, _buffer.data() + _filled, expected(), MSG_DONTWAIT);
285-
286-
// do nothing if the operation is blocking
287-
if (result < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) return;
288-
289-
// if there is a failure we leap out
290-
if (result <= 0) return fail();
291-
292-
// update the number of bytes received
293-
_filled += result;
294-
295-
// after we've received the first two bytes, we can reallocate the buffer so that it is of sufficient size
296-
if (_filled == 2 && !reallocate()) return fail();
297-
267+
268+
// We can be in two receive states: the first state is that we're waiting for the
269+
// size of the buffer. The second state is that we are waiting for the response content itself.
270+
// To determine in what state we're in, we can check how many bytes have been transferred.
271+
// If that's less than 2 then we're still waiting for the response size.
272+
if (_transferred < sizeof(uint16_t))
273+
{
274+
const auto result = ::recv(_fd, (uint8_t*)&_size + _transferred, sizeof(uint16_t) - _transferred, MSG_DONTWAIT);
275+
276+
// if there is a failure we leap out
277+
if (updatetransferred(result)) return;
278+
279+
// if we still haven't received the two bytes we should leap out here
280+
else if (_transferred < sizeof(uint16_t)) return;
281+
282+
// OK: the size of the rest of the frame was received, we know how much to allocate
283+
// update the size
284+
_size = ntohs(_size);
285+
286+
// size the buffer accordingly
287+
_buffer.resize(_size);
288+
}
289+
290+
// calculate offset into the buffer
291+
size_t offset = _transferred - sizeof(uint16_t);
292+
293+
// This is the second state of the Tcp state machine. At this point we know we have
294+
// received at least two bytes of the frame, and so we know we have resized the
295+
// buffer accordingly. All that's left to do is to await the full response content
296+
if (updatetransferred(::recv(_fd, _buffer.data() + offset, _buffer.size() - offset, MSG_DONTWAIT))) return;
297+
298298
// continue waiting if we have not yet received everything there is
299299
if (expected() > 0) return;
300-
301-
// all data has been received, buffer the response for now
302-
add(_ip, _buffer.data() + 2, _filled - 2);
300+
301+
// all data has been received, we can move the response content into a deferred list to be processed later
302+
add(_ip, move(_buffer));
303303

304304
// for the next response we empty the buffer again
305-
_filled = 0;
305+
_transferred = 0;
306306
}
307307

308308
/**

src/udp.cpp

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include "../include/dnscpp/processor.h"
66
#include "../include/dnscpp/query.h"
77
#include <unistd.h>
8+
#include <cassert>
89

910
/**
1011
* Begin namespace
@@ -168,25 +169,29 @@ void Udp::notify()
168169
// do nothing if there is no socket (how is that possible!?)
169170
if (!valid()) return;
170171

171-
// the buffer to receive the response in
172-
// @todo use a macro
173-
unsigned char buffer[65536];
174-
175172
// structure will hold the source address (we use an ipv6 struct because that is also big enough for ipv4)
176173
struct sockaddr_in6 from; socklen_t fromlen = sizeof(from);
177174

178-
// we want to get as much messages at onces as possible, but not run forever
179-
// @todo use scatter-gather io to optimize this further
180-
for (size_t messages = 0; messages < 1024; ++messages)
175+
// the buffer to receive the response in
176+
std::vector<unsigned char> buffer;
177+
178+
// read all messages until depleted
179+
while (true)
181180
{
182-
// reveive the message (the DONTWAIT option is needed because this is a blocking socket, but we dont want to block now)
183-
auto bytes = recvfrom(_fd, buffer, sizeof(buffer), MSG_DONTWAIT, (struct sockaddr *)&from, &fromlen);
181+
// make the buffer large enough
182+
buffer.resize(EDNSPacketSize);
183+
184+
// receive the message (the DONTWAIT option is needed because this is a blocking socket, but we dont want to block now)
185+
auto bytes = recvfrom(_fd, buffer.data(), buffer.size(), MSG_DONTWAIT, (struct sockaddr *)&from, &fromlen);
184186

185187
// if there were no bytes, leap out
186188
if (bytes <= 0) break;
187-
189+
190+
// shrink to fit
191+
buffer.resize(bytes);
192+
188193
// add to the buffer
189-
add((const sockaddr *)&from, buffer, bytes);
194+
add((const sockaddr *)&from, move(buffer));
190195
}
191196
}
192197

0 commit comments

Comments
 (0)