/*
  chronyd/chronyc - Programs for keeping computer clocks accurate.

 **********************************************************************
 * Copyright (C) Richard P. Curnow  1997-2003
 * Copyright (C) Timo Teras  2009
 * Copyright (C) Miroslav Lichvar  2009, 2013-2016, 2018-2021
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of version 2 of the GNU General Public License as
 * published by the Free Software Foundation.
 * 
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 * 
 **********************************************************************

  =======================================================================

  This file deals with the IO aspects of reading and writing NTP packets
  */

#include "config.h"

#include "sysincl.h"

#include "memory.h"
#include "ntp_io.h"
#include "ntp_core.h"
#include "ntp_sources.h"
#include "ptp.h"
#include "sched.h"
#include "socket.h"
#include "local.h"
#include "logging.h"
#include "conf.h"
#include "privops.h"
#include "util.h"

#ifdef HAVE_LINUX_TIMESTAMPING
#include "ntp_io_linux.h"
#endif

#define INVALID_SOCK_FD -1

/* The server/peer and client sockets for IPv4 and IPv6 */
static int server_sock_fd4;
static int server_sock_fd6;
static int client_sock_fd4;
static int client_sock_fd6;

/* Reference counters for server sockets to keep them open only when needed */
static int server_sock_ref4;
static int server_sock_ref6;

/* Flag indicating we create a new connected client socket for each
   server instead of sharing client_sock_fd4 and client_sock_fd6 */
static int separate_client_sockets;

/* Flag indicating the server sockets are not created dynamically when needed,
   either to have a socket for client requests when separate client sockets
   are disabled and client port is equal to server port, or the server port is
   disabled */
static int permanent_server_sockets;

/* Flag indicating the server IPv4 socket is bound to an address */
static int bound_server_sock_fd4;

/* PTP event port, or 0 if disabled */
static int ptp_port;

/* Shared server/client sockets for NTP-over-PTP */
static int ptp_sock_fd4;
static int ptp_sock_fd6;

/* Buffer for transmitted NTP-over-PTP messages */
static PTP_NtpMessage *ptp_message;

/* Flag indicating that we have been initialised */
static int initialised=0;

/* ================================================== */

/* Forward prototypes */
static void read_from_socket(int sock_fd, int event, void *anything);

/* ================================================== */

static int
open_socket(int family, int local_port, int client_only, IPSockAddr *remote_addr)
{
  int sock_fd, sock_flags, dscp, events = SCH_FILE_INPUT;
  IPSockAddr local_addr;
  char *iface;

  if (!SCK_IsIpFamilyEnabled(family))
    return INVALID_SOCK_FD;

  if (!client_only) {
    CNF_GetBindAddress(family, &local_addr.ip_addr);
    iface = CNF_GetBindNtpInterface();
  } else {
    CNF_GetBindAcquisitionAddress(family, &local_addr.ip_addr);
    iface = CNF_GetBindAcquisitionInterface();
  }

  local_addr.port = local_port;

  sock_flags = SCK_FLAG_RX_DEST_ADDR | SCK_FLAG_PRIV_BIND;
  if (!client_only)
    sock_flags |= SCK_FLAG_BROADCAST;

  sock_fd = SCK_OpenUdpSocket(remote_addr, &local_addr, iface, sock_flags);
  if (sock_fd < 0) {
    if (!client_only)
      LOG(LOGS_ERR, "Could not open NTP socket on %s", UTI_IPSockAddrToString(&local_addr));
    return INVALID_SOCK_FD;
  }

  dscp = CNF_GetNtpDscp();
  if (dscp > 0 && dscp < 64) {
#ifdef IP_TOS
    if (!SCK_SetIntOption(sock_fd, IPPROTO_IP, IP_TOS, dscp << 2))
      ;
#endif
  }

  if (!client_only && family == IPADDR_INET4 && local_addr.port > 0)
    bound_server_sock_fd4 = local_addr.ip_addr.addr.in4 != INADDR_ANY;

  /* Enable kernel/HW timestamping of packets */
#ifdef HAVE_LINUX_TIMESTAMPING
  if (!NIO_Linux_SetTimestampSocketOptions(sock_fd, client_only, &events))
#endif
    if (!SCK_EnableKernelRxTimestamping(sock_fd))
      ;

  /* Register handler for read and possibly exception events on the socket */
  SCH_AddFileHandler(sock_fd, events, read_from_socket, NULL);

  return sock_fd;
}

/* ================================================== */

static int
open_separate_client_socket(IPSockAddr *remote_addr)
{
  return open_socket(remote_addr->ip_addr.family, 0, 1, remote_addr);
}

/* ================================================== */

static void
close_socket(int sock_fd)
{
  if (sock_fd == INVALID_SOCK_FD)
    return;

#ifdef HAVE_LINUX_TIMESTAMPING
  NIO_Linux_NotifySocketClosing(sock_fd);
#endif
  SCH_RemoveFileHandler(sock_fd);
  SCK_CloseSocket(sock_fd);
}

/* ================================================== */

void
NIO_Initialise(void)
{
  int server_port, client_port;

  assert(!initialised);
  initialised = 1;

#ifdef PRIVOPS_BINDSOCKET
  SCK_SetPrivBind(PRV_BindSocket);
#endif

#ifdef HAVE_LINUX_TIMESTAMPING
  NIO_Linux_Initialise();
#else
  if (1) {
    CNF_HwTsInterface *conf_iface;
    if (CNF_GetHwTsInterface(0, &conf_iface))
      LOG_FATAL("HW timestamping not supported");
  }
#endif

  server_port = CNF_GetNTPPort();
  client_port = CNF_GetAcquisitionPort();

  /* Use separate connected sockets if client port is negative */
  separate_client_sockets = client_port < 0;
  if (client_port < 0)
    client_port = 0;

  permanent_server_sockets = !server_port || (!separate_client_sockets &&
                                              client_port == server_port);

  server_sock_fd4 = INVALID_SOCK_FD;
  server_sock_fd6 = INVALID_SOCK_FD;
  client_sock_fd4 = INVALID_SOCK_FD;
  client_sock_fd6 = INVALID_SOCK_FD;
  server_sock_ref4 = 0;
  server_sock_ref6 = 0;

  if (permanent_server_sockets && server_port) {
    server_sock_fd4 = open_socket(IPADDR_INET4, server_port, 0, NULL);
    server_sock_fd6 = open_socket(IPADDR_INET6, server_port, 0, NULL);
  }

  if (!separate_client_sockets) {
    if (client_port != server_port || !server_port) {
      client_sock_fd4 = open_socket(IPADDR_INET4, client_port, 1, NULL);
      client_sock_fd6 = open_socket(IPADDR_INET6, client_port, 1, NULL);
    } else {
      client_sock_fd4 = server_sock_fd4;
      client_sock_fd6 = server_sock_fd6;
    }
  }

  if ((server_port && permanent_server_sockets &&
       server_sock_fd4 == INVALID_SOCK_FD && server_sock_fd6 == INVALID_SOCK_FD) ||
      (!separate_client_sockets &&
       client_sock_fd4 == INVALID_SOCK_FD && client_sock_fd6 == INVALID_SOCK_FD)) {
    LOG_FATAL("Could not open NTP sockets");
  }

  ptp_port = CNF_GetPtpPort();
  ptp_sock_fd4 = INVALID_SOCK_FD;
  ptp_sock_fd6 = INVALID_SOCK_FD;
  ptp_message = NULL;

  if (ptp_port > 0) {
    ptp_sock_fd4 = open_socket(IPADDR_INET4, ptp_port, 0, NULL);
    ptp_sock_fd6 = open_socket(IPADDR_INET6, ptp_port, 0, NULL);
    ptp_message = MallocNew(PTP_NtpMessage);
  }
}

/* ================================================== */

void
NIO_Finalise(void)
{
  if (server_sock_fd4 != client_sock_fd4)
    close_socket(client_sock_fd4);
  close_socket(server_sock_fd4);
  server_sock_fd4 = client_sock_fd4 = INVALID_SOCK_FD;

  if (server_sock_fd6 != client_sock_fd6)
    close_socket(client_sock_fd6);
  close_socket(server_sock_fd6);
  server_sock_fd6 = client_sock_fd6 = INVALID_SOCK_FD;

  close_socket(ptp_sock_fd4);
  close_socket(ptp_sock_fd6);
  ptp_sock_fd4 = ptp_sock_fd6 = INVALID_SOCK_FD;
  Free(ptp_message);

#ifdef HAVE_LINUX_TIMESTAMPING
  NIO_Linux_Finalise();
#endif

  initialised = 0;
}

/* ================================================== */

int
NIO_OpenClientSocket(NTP_Remote_Address *remote_addr)
{
  switch (remote_addr->ip_addr.family) {
    case IPADDR_INET4:
      if (ptp_port > 0 && remote_addr->port == ptp_port)
        return ptp_sock_fd4;
      if (separate_client_sockets)
        return open_separate_client_socket(remote_addr);
      return client_sock_fd4;
    case IPADDR_INET6:
      if (ptp_port > 0 && remote_addr->port == ptp_port)
        return ptp_sock_fd6;
      if (separate_client_sockets)
        return open_separate_client_socket(remote_addr);
      return client_sock_fd6;
    default:
      return INVALID_SOCK_FD;
  }
}

/* ================================================== */

int
NIO_OpenServerSocket(NTP_Remote_Address *remote_addr)
{
  switch (remote_addr->ip_addr.family) {
    case IPADDR_INET4:
      if (ptp_port > 0 && remote_addr->port == ptp_port)
        return ptp_sock_fd4;
      if (permanent_server_sockets)
        return server_sock_fd4;
      if (server_sock_fd4 == INVALID_SOCK_FD)
        server_sock_fd4 = open_socket(IPADDR_INET4, CNF_GetNTPPort(), 0, NULL);
      if (server_sock_fd4 != INVALID_SOCK_FD)
        server_sock_ref4++;
      return server_sock_fd4;
    case IPADDR_INET6:
      if (ptp_port > 0 && remote_addr->port == ptp_port)
        return ptp_sock_fd6;
      if (permanent_server_sockets)
        return server_sock_fd6;
      if (server_sock_fd6 == INVALID_SOCK_FD)
        server_sock_fd6 = open_socket(IPADDR_INET6, CNF_GetNTPPort(), 0, NULL);
      if (server_sock_fd6 != INVALID_SOCK_FD)
        server_sock_ref6++;
      return server_sock_fd6;
    default:
      return INVALID_SOCK_FD;
  }
}

/* ================================================== */

static int
is_ptp_socket(int sock_fd)
{
  return ptp_port > 0 && sock_fd != INVALID_SOCK_FD &&
    (sock_fd == ptp_sock_fd4 || sock_fd == ptp_sock_fd6);
}

/* ================================================== */

void
NIO_CloseClientSocket(int sock_fd)
{
  if (is_ptp_socket(sock_fd))
    return;

  if (separate_client_sockets)
    close_socket(sock_fd);
}

/* ================================================== */

void
NIO_CloseServerSocket(int sock_fd)
{
  if (permanent_server_sockets || sock_fd == INVALID_SOCK_FD || is_ptp_socket(sock_fd))
    return;

  if (sock_fd == server_sock_fd4) {
    if (--server_sock_ref4 <= 0) {
      close_socket(server_sock_fd4);
      server_sock_fd4 = INVALID_SOCK_FD;
    }
  } else if (sock_fd == server_sock_fd6) {
    if (--server_sock_ref6 <= 0) {
      close_socket(server_sock_fd6);
      server_sock_fd6 = INVALID_SOCK_FD;
    }
  } else {
    assert(0);
  }
}

/* ================================================== */

int
NIO_IsServerSocket(int sock_fd)
{
  return sock_fd != INVALID_SOCK_FD &&
    (sock_fd == server_sock_fd4 || sock_fd == server_sock_fd6 || is_ptp_socket(sock_fd));
}

/* ================================================== */

int
NIO_IsServerSocketOpen(void)
{
  return server_sock_fd4 != INVALID_SOCK_FD || server_sock_fd6 != INVALID_SOCK_FD ||
    ptp_sock_fd4 != INVALID_SOCK_FD || ptp_sock_fd6 != INVALID_SOCK_FD;
}

/* ================================================== */

int
NIO_IsServerConnectable(NTP_Remote_Address *remote_addr)
{
  int sock_fd;

  sock_fd = open_separate_client_socket(remote_addr);
  if (sock_fd == INVALID_SOCK_FD)
    return 0;

  close_socket(sock_fd);

  return 1;
}

/* ================================================== */

static void
process_message(SCK_Message *message, int sock_fd, int event)
{
  NTP_Local_Address local_addr;
  NTP_Local_Timestamp local_ts;
  struct timespec sched_ts;

  SCH_GetLastEventTime(&local_ts.ts, &local_ts.err, NULL);
  local_ts.source = NTP_TS_DAEMON;
  sched_ts = local_ts.ts;

  if (message->addr_type != SCK_ADDR_IP) {
    DEBUG_LOG("Unexpected address type");
    return;
  }

  local_addr.ip_addr = message->local_addr.ip;
  local_addr.if_index = message->if_index;;
  local_addr.sock_fd = sock_fd;

#ifdef HAVE_LINUX_TIMESTAMPING
  if (NIO_Linux_ProcessMessage(message, &local_addr, &local_ts, event))
    return;
#else
  if (!UTI_IsZeroTimespec(&message->timestamp.kernel)) {
    LCL_CookTime(&message->timestamp.kernel, &local_ts.ts, &local_ts.err);
    local_ts.source = NTP_TS_KERNEL;
  }
#endif

  if (local_ts.source != NTP_TS_DAEMON)
    DEBUG_LOG("Updated RX timestamp delay=%.9f tss=%u",
              UTI_DiffTimespecsToDouble(&sched_ts, &local_ts.ts), local_ts.source);

  if (!NIO_UnwrapMessage(message, sock_fd))
    return;

  /* Just ignore the packet if it's not of a recognized length */
  if (message->length < NTP_HEADER_LENGTH || message->length > sizeof (NTP_Packet)) {
    DEBUG_LOG("Unexpected length");
    return;
  }

  NSR_ProcessRx(&message->remote_addr.ip, &local_addr, &local_ts, message->data, message->length);
}

/* ================================================== */

static void
read_from_socket(int sock_fd, int event, void *anything)
{
  SCK_Message *messages;
  int i, received, flags = 0;

#ifdef HAVE_LINUX_TIMESTAMPING
  if (NIO_Linux_ProcessEvent(sock_fd, event))
    return;
#endif

  if (event == SCH_FILE_EXCEPTION) {
#ifdef HAVE_LINUX_TIMESTAMPING
    flags |= SCK_FLAG_MSG_ERRQUEUE;
#else
    assert(0);
#endif
  }

  messages = SCK_ReceiveMessages(sock_fd, flags, &received);
  if (!messages)
    return;

  for (i = 0; i < received; i++)
    process_message(&messages[i], sock_fd, event);
}

/* ================================================== */

int
NIO_UnwrapMessage(SCK_Message *message, int sock_fd)
{
  PTP_NtpMessage *msg;

  if (!is_ptp_socket(sock_fd))
    return 1;

  if (message->length <= PTP_NTP_PREFIX_LENGTH) {
    DEBUG_LOG("Unexpected length");
    return 0;
  }

  msg = message->data;

  if (msg->header.type != PTP_TYPE_DELAY_REQ || msg->header.version != PTP_VERSION ||
      ntohs(msg->header.length) != message->length ||
      msg->header.domain != PTP_DOMAIN_NTP ||
      ntohs(msg->header.flags) != PTP_FLAG_UNICAST ||
      ntohs(msg->tlv_header.type) != PTP_TLV_NTP ||
      ntohs(msg->tlv_header.length) != message->length - PTP_NTP_PREFIX_LENGTH) {
    DEBUG_LOG("Unexpected PTP message");
    return 0;
  }

  message->data = (char *)message->data + PTP_NTP_PREFIX_LENGTH;
  message->length -= PTP_NTP_PREFIX_LENGTH;

  DEBUG_LOG("Unwrapped PTP->NTP len=%d", message->length);

  return 1;
}

/* ================================================== */

static int
wrap_message(SCK_Message *message, int sock_fd)
{
  assert(PTP_NTP_PREFIX_LENGTH == 48);

  if (!is_ptp_socket(sock_fd))
    return 1;

  if (!ptp_message)
    return 0;

  if (message->length < NTP_HEADER_LENGTH ||
      message->length + PTP_NTP_PREFIX_LENGTH > sizeof (*ptp_message)) {
    DEBUG_LOG("Unexpected length");
    return 0;
  }

  memset(ptp_message, 0, PTP_NTP_PREFIX_LENGTH);
  ptp_message->header.type = PTP_TYPE_DELAY_REQ;
  ptp_message->header.version = PTP_VERSION;
  ptp_message->header.length = htons(PTP_NTP_PREFIX_LENGTH + message->length);
  ptp_message->header.domain = PTP_DOMAIN_NTP;
  ptp_message->header.flags = htons(PTP_FLAG_UNICAST);
  ptp_message->tlv_header.type = htons(PTP_TLV_NTP);
  ptp_message->tlv_header.length = htons(message->length);
  memcpy((char *)ptp_message + PTP_NTP_PREFIX_LENGTH, message->data, message->length);

  message->data = ptp_message;
  message->length += PTP_NTP_PREFIX_LENGTH;

  DEBUG_LOG("Wrapped NTP->PTP len=%d", message->length - PTP_NTP_PREFIX_LENGTH);

  return 1;
}

/* ================================================== */
/* Send a packet to remote address from local address */

int
NIO_SendPacket(NTP_Packet *packet, NTP_Remote_Address *remote_addr,
               NTP_Local_Address *local_addr, int length, int process_tx)
{
  SCK_Message message;

  assert(initialised);

  if (local_addr->sock_fd == INVALID_SOCK_FD) {
    DEBUG_LOG("No socket to send to %s", UTI_IPSockAddrToString(remote_addr));
    return 0;
  }

  SCK_InitMessage(&message, SCK_ADDR_IP);

  message.data = packet;
  message.length = length;

  if (!wrap_message(&message, local_addr->sock_fd))
    return 0;

  /* Specify remote address if the socket is not connected */
  if (NIO_IsServerSocket(local_addr->sock_fd) || !separate_client_sockets) {
    message.remote_addr.ip.ip_addr = remote_addr->ip_addr;
    message.remote_addr.ip.port = remote_addr->port;
  }

  message.local_addr.ip = local_addr->ip_addr;

  /* Don't require responses to non-link-local addresses to use the same
     interface */
  message.if_index = SCK_IsLinkLocalIPAddress(&message.remote_addr.ip.ip_addr) ?
                       local_addr->if_index : INVALID_IF_INDEX;

#if !defined(HAVE_IN_PKTINFO) && defined(IP_SENDSRCADDR)
  /* On FreeBSD a local IPv4 address cannot be specified on bound socket */
  if (message.local_addr.ip.family == IPADDR_INET4 &&
      (bound_server_sock_fd4 || !NIO_IsServerSocket(local_addr->sock_fd)))
    message.local_addr.ip.family = IPADDR_UNSPEC;
#endif

#ifdef HAVE_LINUX_TIMESTAMPING
  if (process_tx)
    NIO_Linux_RequestTxTimestamp(&message, local_addr->sock_fd);
#endif

  if (!SCK_SendMessage(local_addr->sock_fd, &message, 0))
    return 0;

  return 1;
}
