3
\$\begingroup\$

From code review: OO simple network time server with changes from a previous code inspection and added Windows support

I made many changes as per the review comments. The code has been significantly revamped and hopefully the code is clearer to read. A new feature is support for IPv4 or IPv6 in the udp_server class allowed due to changes made to the address class.

In addition, I added a CMakeLists.txt file so should be easier to build now. The CMakeList.txt is fairly basic as just getting started with cmake. debug and release options will probably be my next step.

The code lacks unit tests and another next move is to use cmake ctest and use that to call gtest unit tests. Is that the usual way to go?

How can this code be further improved?

address.hpp (socket address):

#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

//! C++ wrapper for C socket API sockaddr
class address {
public:
  /*! Construct with sockaddr
  \param[in] sock_address */
  explicit address(const sockaddr& sock_address);
  /*! construct from IP address/host/DNS name string and port, use any_ipv4 or any_ipv6 for any interface
  \param[in] name is string IP address or hostname or DNS name or use "any_ipv4" or "any_ipv6" for any IPv4/6 socket interface
  \param[in] port is port */
  address(const char* name, const unsigned short port);
  /*! retrieve socket API socket address object
  \return the raw sockaddr pointer  */ 
  const sockaddr* sockaddr_ptr() const;
  /*! Get size of socket address object, convenience function for interacting with C socket API
  \return length of either IPv4 sockaddr_in or IPv6 sockaddr_in6 */
  int socklen() const;
  /*! get IP address as string
  \return IP address string representation of socket object */
  std::string to_string() const;

  //! debug print address using C++ iostream functions
  friend std::ostream& operator<<(std::ostream& os, const address& address);

private:
  sockaddr_storage socket_address_; //!< raw socket address
};

#endif // ADDRESS_HPP_

address.cpp:

#include "address.hpp"

#include <iostream>
#include <iomanip>
#include <cstring>
#include <stdexcept>

#ifdef __linux__
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#endif

std::ostream& operator<<(std::ostream& os, const address& address)
{
  switch (address.socket_address_.ss_family) {
    case AF_INET: 
    {
        os << "Address family : AF_INET (Internetwork IPv4)\n";
        const sockaddr_in* ipv4_socket = reinterpret_cast<const sockaddr_in*>(address.sockaddr_ptr());
        os << "Port           : " << ntohs(ipv4_socket ? ipv4_socket->sin_port : 0) << '\n';
        break;
    }
    case AF_INET6: 
    {
        os << "Address family : AF_INET6 (Internetwork IPv6)\n";
        const sockaddr_in6* ipv6_socket = reinterpret_cast<const sockaddr_in6*>(address.sockaddr_ptr());
        os << "Port           : " << ntohs(ipv6_socket ? ipv6_socket->sin6_port : 0) << '\n';
        break;
    }

    default:
        os << "Address family : Not IpV4 or IPv6\n"; break;
  }
  os << "IP address     : " << address.to_string() << '\n';
  return os;
}

address::address(const sockaddr& sock_address) {
    if (sock_address.sa_family == AF_INET) {
        memcpy(&socket_address_, &sock_address, sizeof(sockaddr_in));
    }
    else if (sock_address.sa_family == AF_INET6) {
        memcpy(&socket_address_, &sock_address, sizeof(sockaddr_in6));
    }
    else {
        throw std::invalid_argument("Cannot convert sock address argument provided to AF_INET or AF_INET6 family socket");
    }
}

// use any_ipv4 or any_ipv6 for any interface
address::address(const char* name, const unsigned short port) : socket_address_({}) {

  if (!name || std::strlen(name) == 0) {
    throw std::invalid_argument("Invalid address string");
  }

  if (std::strcmp(name, "any_ipv4") == 0) {
      sockaddr_in ipv4_socket = {};
      ipv4_socket.sin_addr.s_addr = INADDR_ANY;  // bind to all interfaces
      ipv4_socket.sin_port = ntohs(port);
      ipv4_socket.sin_family = AF_INET;
      memcpy(&socket_address_, &ipv4_socket, sizeof(sockaddr_in));
  }
  else if (std::strcmp(name, "any_ipv6") == 0) {
      sockaddr_in6 ipv6_socket = {};
      ipv6_socket.sin6_addr = in6addr_any; // bind to all interfaces
      ipv6_socket.sin6_port = ntohs(port);
      ipv6_socket.sin6_family = AF_INET6;
      memcpy(&socket_address_, &ipv6_socket, sizeof(sockaddr_in6));
  } else {

      struct addrinfo hints {};
      struct addrinfo* result, * rp;
      hints.ai_family = AF_UNSPEC;    /* Allow IPv4 or IPv6 */
      hints.ai_flags = AI_PASSIVE;    /* For wildcard IP address */

      int s = getaddrinfo(name, NULL, &hints, &result);
      if (s != 0) {
          fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(s));
          exit(EXIT_FAILURE);
      }

      for (rp = result; rp != NULL; rp = rp->ai_next) {
          if (rp->ai_family == AF_INET6) {
              sockaddr_in6* ipv6_socket = reinterpret_cast<sockaddr_in6*>(rp->ai_addr);
              ipv6_socket->sin6_port = ntohs(port);
              memcpy(&socket_address_, ipv6_socket, sizeof(sockaddr_in6));
          }

          if (rp->ai_family == AF_INET) {             
              sockaddr_in* ipv4_socket = reinterpret_cast<sockaddr_in*>(rp->ai_addr);
              ipv4_socket->sin_port = ntohs(port);
              memcpy(&socket_address_, ipv4_socket, sizeof(sockaddr_in));
          }
          break;
      }

      freeaddrinfo(result);
  }
}

const sockaddr* address::sockaddr_ptr() const {
  return reinterpret_cast<const sockaddr*>(&socket_address_);
}

int address::socklen() const {
    switch (socket_address_.ss_family) {
    case AF_INET:
        return sizeof(sockaddr_in);
    case AF_INET6:
        return sizeof(sockaddr_in6);
    default:
        return 0;
    }
}

std::string address::to_string() const {
  char hostname[NI_MAXHOST + 1] = {};
  getnameinfo(sockaddr_ptr(), socklen(), hostname, NI_MAXHOST, nullptr, 0, NI_NUMERICHOST);
  return hostname;
}

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&)>;

//! UDP Server implementation using select
class udp_server {
public:
  enum class socket_type { IPv4, IPv6 };

  /*! Construct with port, callback for custom data handler and receive buffer size
  \param[in] port
  \param[in] socket_type - IPv4 or IPv6
  \param[in] request_callback - callback function to process client request data
  \param[in] buffer_size - size of buffer to be used for receiving client data, defaults to 4096 bytes
  */
  udp_server(uint16_t port, socket_type sockettype, client_request_callback request_callback, size_t buffer_size = 4096);

  //! destructor to clean up socket resources when server closes
  ~udp_server();

  //! call run to start server
  void run();
  
  /*! send data to address - typically used to respond to a client request
  \param[in] data - data to be sent
  \param[in] length - number of bytes to be sent
  \param[in] address - client address to send data
  \return number of bytes successfully sent */
  ssize_t send(const char* data, const size_t length, const address& address);

private:
  uint16_t port_;
  socket_type socket_type_;
  client_request_callback request_callback_;
  std::vector<char> buffer_;
  int socket_fd_;
};

#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);
  }

  int make_socket(enum udp_server::socket_type sockettype) {
    int socket_fd = socket(sockettype == udp_server::socket_type::IPv4 ?
               PF_INET : PF_INET6, SOCK_DGRAM, 0);
    if (socket_fd == -1) {
      bailout("socket error");
    }
    return socket_fd;
  }

  void bind_server_socket(int socket_fd, enum udp_server::socket_type sockettype, uint16_t port) {
    // bind the server address to the socket
    if (sockettype == udp_server::socket_type::IPv4) {
      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 result = bind(socket_fd, reinterpret_cast<sockaddr*>(&server_addr), len_inet);
      if (result == -1) {
        bailout("bind error 1");
      }
    } else {
      sockaddr_in6 server_addr = {}; // AF_INET6
      server_addr.sin6_family = AF_INET6;
      server_addr.sin6_port = htons(port);
      server_addr.sin6_addr = in6addr_any;     
      socklen_t len_inet = sizeof server_addr;
      int result = bind(socket_fd, reinterpret_cast<sockaddr*>(&server_addr), len_inet);
      if (result == -1) {
        bailout("bind error 2");
      }
    }
  }
}

udp_server::udp_server(uint16_t port, socket_type sockettype, client_request_callback request_callback, size_t buffer_size) : port_(port), socket_type_(sockettype), request_callback_(request_callback ), buffer_(buffer_size), socket_fd_(-1) {

  std::clog << "udp_server will bind to port: " << port_ << std::endl;
}

udp_server::~udp_server() {
  if (socket_fd_ != -1) {
    close(socket_fd_);
  }
#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
  socket_fd_ = make_socket(socket_type_);

  // bind the server address to the socket
  bind_server_socket(socket_fd_, socket_type_, port_);

  // express interest in socket s for read events
  fd_set rx_set; // read set
  FD_ZERO(&rx_set); // init
  int maxfds = socket_fd_ + 1;

  // start the server loop
  for (;;) {
    FD_SET(socket_fd_, &rx_set);

    // block until socket activity
    int number_fds_set = select(maxfds, &rx_set, NULL, NULL, NULL);
    if (number_fds_set == -1) {
      bailout("select error");
    } else if ( !number_fds_set ) {
      // select timeout
      continue;
    }

    // if udp socket is readable receive the message.
    if (FD_ISSET(socket_fd_, &rx_set)) {
      sockaddr_storage sock_address{};
      socklen_t len_client = sizeof sock_address;

      // retrieve data received
      ssize_t received_bytes = recvfrom(socket_fd_, buffer_.data(), buffer_.size(), 0,
                                 reinterpret_cast<sockaddr*>(&sock_address), &len_client);

      if (received_bytes <= 0) {
    std::cerr << "recvfrom returned: " << received_bytes << std::endl;
        continue;
      }

      // Create an address from client_address and pass to callback function with data
      address client_address(reinterpret_cast<const sockaddr&>(sock_address));
      request_callback_(buffer_.data(), received_bytes, client_address);

      FD_CLR(socket_fd_, &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(socket_fd_, data, length, 0, address.sockaddr_ptr(), address.socklen());
}

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>

//! Basic SNTP Server implementation in C++ adhering as closely as possible to RFC4330
class ntp_server {
public:
  /*! Construct with port number, or default of 123 well known port number for NTP
  \param[in] port */
  explicit ntp_server(std::uint16_t port = 123);

  //! start NTP server
  void run();

  /*! Callback function for processing client NTP request
  \param[in] data data read from client
  \param[in] length of data read
  \param[in] address of 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>

#include <chrono>

namespace {

struct ntp_reply {
    std::uint8_t li_vn_mode;
    std::uint8_t stratum;
    std::uint8_t poll_interval;
    std::uint8_t precision;
    std::uint32_t root_delay;
    std::uint32_t root_dispersion;
    std::uint32_t reference_source;
    std::uint64_t reference_timestamp;
    std::uint64_t originate_timestamp;
    std::uint64_t receive_timestamp;
    std::uint64_t transmit_timestamp;
};

const std::size_t ntp_msg_size{sizeof(ntp_reply)}; // 48 bytes
  
uint64_t htonll(uint64_t value)
{
  /* store 0x12 (18) in memory as an int (4 or 8 bytes) and cast to a char* and check first/leftmost byte
     11 00 00 00 - here the value is stored in leftmost, most significant byte - little endian
     00 00 00 11 - here value is stored in rightmost, least significant byte - big endian
     big endian looks the right way when you look at the memory block
  */ 
  static const int num = 0x12;

  // Check the endianness
  if (*reinterpret_cast<const char*>(&num) == num) {
    const uint32_t high_part = htonl(static_cast<uint32_t>(value >> 32));
    const uint32_t low_part = htonl(static_cast<uint32_t>(value & 0xFFFFFFFFLL));
    return (static_cast<uint64_t>(low_part) << 32) | high_part;
  } else {
    return value;
  }
}
  
// 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, 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 get_ntptime()
{
  /* 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;

  auto now = std::chrono::system_clock::now();
  auto now_timet = std::chrono::system_clock::to_time_t(now);
  std::chrono::hh_mm_ss hms_time{ now.time_since_epoch() };
  auto subsecs = hms_time.subseconds();

  uint64_t upper = now_timet;
  uint64_t lower = subsecs.count();
  uint64_t ntptime = ((upper + NTP_UTIME_DIFF) << 32) + (lower * (NTP_SCALE_FRAC / 1000000UL) & 0xFFFFFFFF);
  return ntptime;
}


/* create the NTP response message to be sent to client 
   Args:
   recv_buf - array received from NTP client (should be 48 bytes in length)
   returns NTP response message, ntp_reply object
*/
ntp_reply make_reply(const char recv_buf[]) {

  std::uint64_t ntp_now = get_ntptime();
  
  ntp_reply reply;
  /* 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 */
  reply.li_vn_mode = 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. */
  reply.stratum = 0x1;


  // Poll Interval - - we set to max allowable poll interval
  reply.poll_interval = 0x11;  // 17 == 2^17 (exponent)

  // Precision
  reply.precision = 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 */
  reply.root_delay = htonl(0);

  /* Root Dispersion - max error due to clock freq tolerance in secs, svr sets
     set to zero (simplification) */
  reply.root_dispersion = 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
  reply.reference_source = 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 p32_seconds_before = ((ntp_now >> 32) - 131072) & 0xFFFFFFFF;
  std::uint64_t p32_frac_seconds_before = ntp_now & 0xFFFFFFFF;  
  reply.reference_timestamp = htonll((p32_seconds_before << 32) + 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(&reply.originate_timestamp, &recv_buf[40], 8);

  // Receive Timestamp - get from time rq received by server
  reply.receive_timestamp = htonll(ntp_now);

  // Transmit Timestamp - re-use ntp_now time obtained above
  reply.transmit_timestamp = htonll(ntp_now);

  return reply;
}

}  // unnamed namespace

ntp_server::ntp_server(std::uint16_t port)
  : udp_server_(port, udp_server::socket_type::IPv4,
        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& client_address) {

  ntp_reply reply = make_reply(data);

  const char* msg = reinterpret_cast<const char*>(&reply);

  ssize_t ret;
  if ( (ret = udp_server_.send(msg, ntp_msg_size, client_address)) != ntp_msg_size) {
    std::cerr << "Error sending response to client: " << ret;
    perror("sendto");
  }

  // logging
  std::clog << "new data in\n";
  printhex(data, length);
  std::clog << '\n';

  std::clog << "Client address:\n" << client_address << std::endl;
  
  std::clog << "data sent:\n";
  printhex(msg, 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();
}

CMakeLists.txt:

# This project uses std::chrono::hh_mm_ss which requires c++20
# or cmake -D CMAKE_CXX_COMPILER=g++-11 <sources>
set(CMAKE_CXX_COMPILER g++-11)

cmake_minimum_required(VERSION 3.26)

project(ntpserver VERSION 1.0)

set (CMAKE_CXX_STANDARD 20)

add_executable(ntpserver main.cpp udp-server.cpp ntp-server.cpp address.cpp)

target_compile_options(ntpserver PRIVATE -Wall -Wextra -Wconversion)

add_library(ntpserver_compiler_flags INTERFACE)
target_compile_features(ntpserver_compiler_flags INTERFACE cxx_std_20)

message( STATUS "CMAKE_CXX_COMPILER: " ${CMAKE_CXX_COMPILER} )

readme.md (used if run doxygen on project)

Simple Network Time Server                         {#mainpage}
==========================

Welcome to this Simple Network Time Server code based on RFC4333, https://www.rfc-editor.org/rfc/rfc4330
\$\endgroup\$
1
  • 2
    \$\begingroup\$ The precision / delay / dispersion "accuracy estimate" numbers were initially "roughed in", to get past the coredump stage. But at this point you're ready to stop lying to the client and give it an honest assessment of how good your local timebase is. Advertising as "stratum 1" is especially concerning, as you're essentially trying to muscle out a (likely quite accurate) stratum 2 server and telling the client "hey, I'm better, sync to me!" Time for truth in advertising, please. Consider studying how this code's behavior differs from a reference implementation, during iburst and for a day. \$\endgroup\$
    – J_H
    May 21 at 15:01

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Browse other questions tagged or ask your own question.