I put up my SNTP Server code for review here: SNTPv4 server based on rfc 4330 in C++
a fortnight ago and since then I have made changes as per code review comments and also added support for Windows, mostly just by adding the winsock startup and shutdown functions and I also made the Makefile work for Windows in addition to Linux.
For the Makefile to work on Windows you need to install GNU make for Windows and assuming you already have Visual Studio installed, run the Native Tools Command Prompt for Visual Studio to setup the required env variables. I only tested with 64 bit building on Windows.
Is creating a Makefile to work under Windows a fools errand?
Is the code clear?
How could it be improved?
Very interested to hear any feedback on the code.
Address.hpp:
/*
C++ wrapper for C socket API sockaddr_in
*/
#ifndef ADDRESS_HPP_
#define ADDRESS_HPP_
#include <string>
#include <cstdint>
#include <iosfwd>
#ifdef _WIN32
#include <winsock2.h> // Windows sockets v2
#include <ws2tcpip.h> // WinSock2 Extension, eg inet_pton, inet_ntop, sockaddr_in6
#elif __linux__ || __unix__
#include <arpa/inet.h>
#else
#error Unsupported platform
#endif
class Address {
public:
//! construct from sockaddr_in
explicit Address(const sockaddr_in& sock_address);
//! construct from 32 bit unsigned integer and a port
Address(const std::uint32_t ipv4_address, const unsigned short port);
//! construct from IP address string and port, use * for INADDR_ANY
Address(const char* dotted_decimal, const unsigned short port);
//! retrieve socket API sockaddr_in
const sockaddr_in* get() const;
//! get size of sockaddr_in
std::size_t size() const;
//! get IP address as string
std::string get_ip() const;
friend std::ostream& operator<<(std::ostream& os, const Address& address);
private:
sockaddr_in sock_addr_;
};
//! debug print address
std::ostream& operator<<(std::ostream& os, const Address& address);
#endif // ADDRESS_HPP_
Address.cpp:
#include "address.hpp"
#include <iostream>
#include <iomanip>
#include <cstring>
#include <stdexcept>
std::ostream& operator<<(std::ostream& os, const Address& address)
{
os << "Address family : AF_INET (Internetwork IPv4)\n";
os << "Port : " << ntohs(address.sock_addr_.sin_port) << '\n';
os << "IP address : " << address.get_ip() << '\n';
return os;
}
Address::Address(const sockaddr_in& sock_address) : sock_addr_{ sock_address } {}
// use * for INADDR_ANY
Address::Address(const char* dotted_decimal, const unsigned short port) : sock_addr_({}) {
if (!dotted_decimal || std::strlen(dotted_decimal) == 0 || std::strcmp(dotted_decimal, "*") == 0) {
sock_addr_.sin_addr.s_addr = INADDR_ANY; // bind to all interfaces
}
else {
if (inet_pton(AF_INET, dotted_decimal, &sock_addr_.sin_addr) != 1) {
throw std::invalid_argument("invalid IPv4 address");
}
}
sock_addr_.sin_family = PF_INET;
sock_addr_.sin_port = htons(port);
}
Address::Address(const uint32_t ipv4_address, const unsigned short port) : sock_addr_{} {
sock_addr_.sin_family = PF_INET;
sock_addr_.sin_addr.s_addr = ipv4_address;
sock_addr_.sin_port = htons(port);
}
const sockaddr_in* Address::get() const {
return &sock_addr_;
}
size_t Address::size() const {
return sizeof sock_addr_;
}
std::string Address::get_ip() const {
char buffer[64];
const char* ipv4 = inet_ntop(PF_INET, &sock_addr_.sin_addr, buffer, 64);
return ipv4 ? ipv4 : "";
}
udp_server.hpp:
/*
Basic implementation of UDP socket server using select
User provides callback function to handle client requests
*/
#ifndef UDP_SERVER__
#define UDP_SERVER__
#ifdef _WIN32
#ifdef _WIN64
typedef __int64 ssize_t;
#else
typedef int ssize_t;
#endif
#endif
#include <functional>
#include <vector>
#include <cstdint>
#include "address.hpp"
//! callback signature for user provided function for handling request data from clients
using client_request_callback = std::function<void(const char*, const size_t, const Address&)>;
class udp_server {
public:
//! construct with port, callback for custom data handler and receive buffer size
udp_server(uint16_t port, client_request_callback request_callback, size_t buffer_size = 4096);
//! destructor
virtual ~udp_server();
//! call run to start server
void run();
//! send returns number of bytes successfully sent
ssize_t send(const char* data, const size_t length, const Address& address);
private:
uint16_t port_;
client_request_callback rq_callback_;
//const size_t buffer_size_;
std::vector<char> buffer_;
int s_;
};
#endif // UDP_SERVER__
udp_server.cpp:
#ifdef _WIN32
#include <winsock2.h> // Windows sockets v2
#include <ws2tcpip.h> // WinSock2 Extension, eg inet_pton, inet_ntop, sockaddr_in6
namespace {
int close(int fd) {
return closesocket(fd);
}
}
#ifdef _WIN64
typedef __int64 ssize_t;
#else
typedef int ssize_t;
#endif
#elif __linux__ || __unix__
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <arpa/inet.h>
#else
#error Unsupported platform
#endif
#include <iostream>
#include <cerrno>
#include <cstring>
#include <cstdint>
#include <cstdlib> // std::exit()
#include "udp-server.hpp"
namespace {
void bailout(const char* msg) {
std::perror(msg);
std::exit(1);
}
}
udp_server::udp_server(uint16_t port, client_request_callback request_callback, size_t buffer_size) : port_(port), rq_callback_(request_callback ), buffer_(buffer_size), s_(-1) {
std::clog << "udp_server will bind to port: " << port_ << std::endl;
}
udp_server::~udp_server() {
if (s_ != -1) {
close(s_);
}
#ifdef _WIN32
WSACleanup();
#endif
}
//! start server
void udp_server::run() {
#ifdef _WIN32
WSADATA w = { 0 };
int error = WSAStartup(0x0202, &w);
if (error || w.wVersion != 0x0202)
{ // there was an error
throw "Could not initialise Winsock2\n";
}
#endif
// create server socket
s_ = socket(PF_INET, SOCK_DGRAM, 0);
if (s_ == -1) {
bailout("socket error");
}
// bind the server address to the socket
sockaddr_in server_addr = {}; // AF_INET
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(port_);
server_addr.sin_addr.s_addr = INADDR_ANY;
socklen_t len_inet = sizeof server_addr;
int r = bind(s_, reinterpret_cast<sockaddr*>(&server_addr), len_inet);
if ( r == -1) {
bailout("bind error");
}
// express interest in socket s for read events
fd_set rx_set; // read set
FD_ZERO(&rx_set); // init
int maxfds = s_ + 1;
// start the server loop
for (;;) {
FD_SET(s_, &rx_set);
// block until socket activity
int n = select(maxfds, &rx_set, NULL, NULL, NULL);
if (n == -1) {
bailout("select error");
} else if ( !n ) {
// select timeout
continue;
}
// if udp socket is readable receive the message.
if (FD_ISSET(s_, &rx_set)) {
sockaddr_in sock_address {};
socklen_t len_client = sizeof sock_address;
// retrieve data received
ssize_t recbytes = recvfrom(s_, buffer_.data(), buffer_.size(), 0,
reinterpret_cast<sockaddr*>(&sock_address), &len_client);
if (recbytes <= 0) {
std::cerr << "recvfrom returned: " << recbytes << std::endl;
continue;
}
// Create an address from client_address and pass to callback function with data
Address client_address(sock_address);
rq_callback_(buffer_.data(), recbytes, client_address);
FD_CLR(s_, &rx_set);
}
} // for loop
}
//! returns number of bytes successfully sent
ssize_t udp_server::send(const char* data, const size_t length, const Address& address) {
return sendto(s_, data, length, 0, (const sockaddr*)address.get(), static_cast<int>(address.size()));
}
ntp-server.hpp:
/*
Basic implementation of v4 SNTP server as per: https://www.rfc-editor.org/rfc/rfc4330
Uses udp_server for low level UDP socket communication
Separation of socket and ntp handling via passing a callback function to udp server
*/
#ifndef NTP_SERVER_HPP_
#define NTP_SERVER_HPP_
#include "udp-server.hpp"
#include <cstdint>
class ntp_server {
public:
//! initialise ntp_server with server port, defaults to well known NTP port 123
explicit ntp_server(std::uint16_t port = 123);
//! start NTP server
void run();
//! callback to handle data received from NTP client
void read_callback(const char* data, const std::size_t length, const Address& address);
private:
udp_server udp_server_;
};
#endif // NTP_SERVER_HPP_
ntp-server.cpp:
#include "ntp-server.hpp"
#include <cstdio>
#include <cstring> // memcpy
#include <iostream>
#include <iomanip>
#ifdef _WIN32
int gettimeofday(struct timeval* tp, struct timezone* tzp)
{
// Note: some broken versions only have 8 trailing zero's, the correct epoch has 9 trailing zero's
// This magic number is the number of 100 nanosecond intervals since January 1, 1601 (UTC)
// until 00:00:00 January 1, 1970
static const std::uint64_t EPOCH = ((std::uint64_t)116444736000000000ULL);
SYSTEMTIME system_time;
FILETIME file_time;
std::uint64_t time;
GetSystemTime(&system_time);
SystemTimeToFileTime(&system_time, &file_time);
time = ((std::uint64_t)file_time.dwLowDateTime);
time += ((std::uint64_t)file_time.dwHighDateTime) << 32;
tp->tv_sec = (long)((time - EPOCH) / 10000000L);
tp->tv_usec = (long)(system_time.wMilliseconds * 1000);
return 0;
}
#else
#include <sys/time.h> // gettimeofday
#endif
namespace {
/* unix epoch is 1970-01-01 00:00:00 +0000 (UTC) but start of ntp time
is 1900-01-01 00:00:00 UTC, so adjust with difference */
const std::uint32_t NTP_UTIME_DIFF = 2208988800U; /* 1970 - 1900 */
const std::uint64_t NTP_SCALE_FRAC = 4294967296;
const std::size_t ntp_msg_size{48};
// utility function to print an array in hex
template<typename T>
void printhex (T* buf, std::size_t len)
{
auto previous_flags = std::clog.flags();
for (std::size_t i = 0; i < len; i++) {
std::clog << std::hex << std::setfill('0') << std::setw(2) << (buf[i] & 0xFF) << ' ';
}
std::clog.setf(previous_flags);
}
/* get timestamp for NTP in LOCAL ENDIAN, in/out arg is a uint32_t[2] array with
most significant
32 bit part no. seconds since 1900-01-01 00:00:00 and least significant 32
bit part fractional seconds */
void gettime64(std::uint32_t ts[])
{
struct timeval tv;
gettimeofday(&tv, NULL);
if (tv.tv_sec > 0xFFFFFFFF) {
std::clog << "timeval time_t seconds value > 0xFFFFFFFF, (" << tv.tv_sec << ") and will overflow\n";
}
ts[0] = static_cast<uint32_t>(tv.tv_sec) + NTP_UTIME_DIFF; // ntp seconds
if ((NTP_SCALE_FRAC * tv.tv_usec) / 1000000UL > 0xFFFFFFFF) {
std::clog << "timeval fractional seconds value > 0xFFFFFFFF, (" << tv.tv_sec << ") and will overflow\n";
}
ts[1] = static_cast<uint32_t>((NTP_SCALE_FRAC * tv.tv_usec) / 1000000UL); //ntp usecs
}
/* get timestamp for NTP in LOCAL ENDIAN, returns uint64_t with most significant
32 bit part no. seconds since 1900-01-01 00:00:00 and least significant 32
bit part fractional seconds */
uint64_t gettime64()
{
uint32_t ts[2];
gettime64(ts);
uint64_t tmp = ts[0];
return (tmp << 32) + ts[1];
}
// helper to populate an array at start_index with uint32_t
void populate_32bit_value(unsigned char* buffer, int start_index, std::uint32_t value) {
std::uint32_t* p32 = reinterpret_cast<std::uint32_t*>(&buffer[start_index]);
*p32 = value;
}
/* create the NTP response message to be sent to client
Args:
recv_buf - array received from NTP client (should be 48 bytes in length)
recv_time - array containing time NTP request received
send_buf - byte array to be sent to client
*/
void make_reply(const unsigned char recv_buf[], std::uint32_t recv_time[], unsigned char* send_buf) {
/* LI VN Mode
Leap Indicator = 0
Version Number = 4 (SNTPv4)
Mode = 4 = server
0x24 == LI=0, version=4 (SNTPv4), mode=4 (server) 00 100 100 */
send_buf[0] = 0x24;
/* Stratum = 1 (primary reference). A stratum 1 level NTP server is
synchronised by a reference clock, eg in UK the Anthorn Radio Station
in Cumbria. (Not true - next project work out how to sync up with radio
signal. Typically in a real world scenario, subsidiary ntp servers at
lower levels of stratum would sync with a stratum ntp server. */
send_buf[1] = 0x1;
// Poll Interval - - we set to max allowable poll interval
send_buf[2] = 0x11; // 17 == 2^17 (exponent)
// Precision
send_buf[3] = 0xFA; // 0xFA == -6 - 2^(-6) == mains clock frequency
// *** below are 32 bit values
/* Root Delay - total roundtrip delay to primary ref source in seconds
set to zero - simplification */
populate_32bit_value(send_buf, 4, htonl(0));
/* Root Dispersion - max error due to clock freq tolerance in secs, svr sets
set to zero (simplification) */
populate_32bit_value(send_buf, 8, htonl(0));
/* Reference Identifier - reference source, LOCL means uncalibrated local clock
We must send in network byte order (we assume we built svr little endian) */
std::uint32_t refid ( 0x4c4f434c ); // LOCL in ASCII
populate_32bit_value(send_buf, 12, htonl(refid));
// *** below are 64 bit values
/* Reference Timestamp - time system clock was last set or corrected
investigate - if we assume client is requesting every poll interval 2^17 - just simulate what time was back then
2^17 = 131072 */
std::uint64_t ntp_now = gettime64();
std::uint32_t p32_seconds_before = htonl(static_cast<uint32_t>(ntp_now >> 32) - 131072);
std::uint32_t p32_frac_seconds_before = htonl(ntp_now & 0xFFFFFFFF);
populate_32bit_value(send_buf, 16, p32_seconds_before);
populate_32bit_value(send_buf, 20, p32_frac_seconds_before);
/* Originate Timestamp: This is the time at which the request departed
the client for the server, in 64-bit timestamp format. We can copy
value from client request */
std::memcpy(&send_buf[24], &recv_buf[40], 8);
// Receive Timestamp - get from time rq received by server
std::uint32_t* p32 = reinterpret_cast<uint32_t*>(&send_buf[32]);
*p32++ = htonl(recv_time[0]); // seconds part
*p32++ = htonl(recv_time[1]); // fraction of seconds part
// Transmit Timestamp - re-use ntp_now time obtained above
populate_32bit_value(send_buf, 40, htonl(static_cast<uint32_t>(ntp_now >> 32)));
populate_32bit_value(send_buf, 44, htonl(ntp_now & 0xFFFFFFFF));
}
} // unnamed namespace
// is there any way to do this and not have to use bind. It is a little ugly
ntp_server::ntp_server(std::uint16_t port)
: udp_server_(port,
std::bind(&ntp_server::read_callback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3),
ntp_msg_size) { }
void ntp_server::run() {
udp_server_.run();
}
void ntp_server::read_callback(const char* data, const std::size_t length, const Address& address) {
std::uint32_t recv_time[2];
gettime64(recv_time);
std::clog << "new data in\n";
printhex(data, length);
std::clog << '\n';
std::clog << "Client address:\n" << address << std::endl;
unsigned char send_buf[ntp_msg_size] {};
make_reply(reinterpret_cast<const unsigned char*>(data), recv_time, send_buf);
ssize_t ret;
if ( (ret = udp_server_.send(reinterpret_cast<const char*>(send_buf), ntp_msg_size, address)) != ntp_msg_size) {
std::cerr << "Error sending response to client: " << ret;
perror("sendto");
}
std::clog << "data sent:\n";
printhex(send_buf, ntp_msg_size);
std::clog << std::endl;
}
main.cpp:
#include <iostream>
#include "ntp-server.hpp"
int main() {
std::clog << "Starting SNTPv4 Server\n";
ntp_server server(123);
server.run();
}
Makefile:
ifeq ($(OS),Windows_NT)
UNAME := Windows
CXX=cl.exe
CXXFLAGS=-EHs -nologo -std:c++17
RM=rd /s /q
MKDIR_P=mkdir
LINKER=link.exe
LIBS=ws2_32.lib
LFLAGS=-MACHINE:X64 -nologo -SUBSYSTEM:CONSOLE
OBJSFX=.obj
OUTCC=-Fo$@
OUTLINK=-out:$@
DBGCFLAGS = -DDEBUG -W4 -MTd -TP -Od
RELCFLAGS = -DNDEBUG -W4 -MT -TP -O2
EXE = ntpserver.exe
else
UNAME := $(shell uname -s)
CXX=g++
CXXFLAGS=-Wall -std=c++17 -Wextra -Wconversion
MKDIR_P=mkdir -p
LINKER=g++
RM=rm -rf
OBJSFX=.o
OUTCC=-o $@
OUTLINK=-o $@
DBGCFLAGS = -DDEBUG -g
RELCFLAGS = -DNDEBUG
EXE = ntpserver
endif
SOURCES = main.cpp udp-server.cpp ntp-server.cpp address.cpp
OBJS = $(SOURCES:.cpp=$(OBJSFX))
# Debug build settings
DBGDIR = debug
DBGEXE = $(DBGDIR)/$(EXE)
DBGOBJS = $(addprefix $(DBGDIR)/, $(OBJS))
# Release build settings
RELDIR = release
RELEXE = $(RELDIR)/$(EXE)
RELOBJS = $(addprefix $(RELDIR)/, $(OBJS))
.PHONY: all clean debug prep release remake
# Default build
all: prep release
# Debug rules
debug: $(DBGEXE)
$(DBGEXE): $(DBGOBJS)
$(LINKER) $(LFLAGS) $^ $(LIBS) $(OUTLINK)
$(DBGDIR)/%$(OBJSFX): %.cpp
$(CXX) -c $(CXXFLAGS) $(DBGCFLAGS) $< $(OUTCC)
# Release rules
release: $(RELEXE)
$(RELEXE): $(RELOBJS)
$(LINKER) $(LFLAGS) $^ $(LIBS) $(OUTLINK)
$(RELDIR)/%$(OBJSFX): %.cpp
$(CXX) -c $(CXXFLAGS) $(RELCFLAGS) $< $(OUTCC)
# Other rules
prep:
@$(MKDIR_P) $(DBGDIR) $(RELDIR)
remake: clean all
clean:
$(RM) debug release
WIN_TICKS_FROM_EPOCH
. You should make this explicit. \$\endgroup\$