/*
 * mss_clamp.c - TCP MSS clamping plug-in
 *
 * Copyright (c) 2018 Cisco and/or its affiliates
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at:
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <vnet/vnet.h>
#include <vnet/plugin/plugin.h>
#include <mss_clamp/mss_clamp.h>
#include <mss_clamp/mss_clamp.api_types.h>

mssc_main_t mssc_main;

/* Action function shared between message handler and debug CLI */

static void
mssc_enable_disable_feat (u32 sw_if_index, u8 dir4, u8 dir6, int enable)
{
  if (dir4 == MSS_CLAMP_DIR_NONE && dir6 == MSS_CLAMP_DIR_NONE)
    return;

  // ip4
  if ((dir4 & MSS_CLAMP_DIR_RX) != MSS_CLAMP_DIR_NONE)
    vnet_feature_enable_disable ("ip4-unicast", "tcp-mss-clamping-ip4-in",
				 sw_if_index, enable, 0, 0);
  if ((dir4 & MSS_CLAMP_DIR_TX) != MSS_CLAMP_DIR_NONE)
    vnet_feature_enable_disable ("ip4-output", "tcp-mss-clamping-ip4-out",
				 sw_if_index, enable, 0, 0);
  // ip6
  if ((dir6 & MSS_CLAMP_DIR_RX) != MSS_CLAMP_DIR_NONE)
    vnet_feature_enable_disable ("ip6-unicast", "tcp-mss-clamping-ip6-in",
				 sw_if_index, enable, 0, 0);
  if ((dir6 & MSS_CLAMP_DIR_TX) != MSS_CLAMP_DIR_NONE)
    vnet_feature_enable_disable ("ip6-output", "tcp-mss-clamping-ip6-out",
				 sw_if_index, enable, 0, 0);
}

int
mssc_enable_disable (u32 sw_if_index, u8 dir4, u8 dir6, u16 mss4, u16 mss6)
{
  mssc_main_t *cm = &mssc_main;
  u8 *dir_enabled4, *dir_enabled6;
  int rv = 0;

  if (dir4 == MSS_CLAMP_DIR_NONE)
    mss4 = MSS_CLAMP_UNSET;
  if (dir6 == MSS_CLAMP_DIR_NONE)
    mss6 = MSS_CLAMP_UNSET;

  vec_validate_init_empty (cm->dir_enabled4, sw_if_index, MSS_CLAMP_DIR_NONE);
  vec_validate_init_empty (cm->dir_enabled6, sw_if_index, MSS_CLAMP_DIR_NONE);
  vec_validate_init_empty (cm->max_mss4, sw_if_index, MSS_CLAMP_UNSET);
  vec_validate_init_empty (cm->max_mss6, sw_if_index, MSS_CLAMP_UNSET);

  cm->max_mss4[sw_if_index] = mss4;
  cm->max_mss6[sw_if_index] = mss6;
  dir_enabled4 = &cm->dir_enabled4[sw_if_index];
  dir_enabled6 = &cm->dir_enabled6[sw_if_index];

  // Disable the directions that are no longer needed
  mssc_enable_disable_feat (sw_if_index, (*dir_enabled4) & ~dir4,
			    (*dir_enabled6) & ~dir6, 0);
  // Enable the new directions
  mssc_enable_disable_feat (sw_if_index, ~(*dir_enabled4) & dir4,
			    ~(*dir_enabled6) & dir6, 1);

  *dir_enabled4 = dir4;
  *dir_enabled6 = dir6;

  return rv;
}

int
mssc_get_mss (u32 sw_if_index, u8 *dir4, u8 *dir6, u16 *mss4, u16 *mss6)
{
  mssc_main_t *cm = &mssc_main;
  int rv = VNET_API_ERROR_FEATURE_DISABLED;

  if (vec_len (cm->dir_enabled4) > sw_if_index &&
      MSS_CLAMP_DIR_NONE != cm->dir_enabled4[sw_if_index])
    {
      *mss4 = cm->max_mss4[sw_if_index];
      *dir4 = cm->dir_enabled4[sw_if_index];
      rv = 0;
    }
  else
    {
      *mss4 = MSS_CLAMP_DIR_NONE;
      *dir4 = 0;
    }

  if (vec_len (cm->dir_enabled6) > sw_if_index &&
      MSS_CLAMP_DIR_NONE != cm->dir_enabled6[sw_if_index])
    {
      *mss6 = cm->max_mss6[sw_if_index];
      *dir6 = cm->dir_enabled6[sw_if_index];
      rv = 0;
    }
  else
    {
      *mss6 = MSS_CLAMP_DIR_NONE;
      *dir6 = 0;
    }
  return rv;
}

static uword
unformat_mssc_dir (unformat_input_t *input, va_list *args)
{
  u8 *result = va_arg (*args, u8 *);
  u8 dir = MSS_CLAMP_DIR_RX | MSS_CLAMP_DIR_TX;

  if (unformat (input, "disable"))
    dir = MSS_CLAMP_DIR_NONE;
  else if (unformat (input, "enable"))
    dir = MSS_CLAMP_DIR_RX | MSS_CLAMP_DIR_TX;
  else if (unformat (input, "rx"))
    dir = MSS_CLAMP_DIR_RX;
  else if (unformat (input, "tx"))
    dir = MSS_CLAMP_DIR_TX;
  else
    return 0;

  *result = dir;
  return 1;
}

static clib_error_t *
mssc_enable_command_fn (vlib_main_t *vm, unformat_input_t *input,
			vlib_cli_command_t *cmd)
{
  u32 sw_if_index = ~0;
  u8 dir4 = ~0, dir6 = ~0;
  u32 mss4 = ~0, mss6 = ~0;
  int rv;

  while (unformat_check_input (input) != UNFORMAT_END_OF_INPUT)
    {
      if (unformat (input, "ip4 %U", unformat_mssc_dir, &dir4))
	;
      else if (unformat (input, "ip6 %U", unformat_mssc_dir, &dir6))
	;
      else if (unformat (input, "ip4-mss %d", &mss4))
	;
      else if (unformat (input, "ip6-mss %d", &mss6))
	;
      else if (unformat (input, "%U", unformat_vnet_sw_interface,
			 vnet_get_main (), &sw_if_index))
	;
      else
	break;
    }

  if (sw_if_index == ~0)
    return clib_error_return (0, "Please specify an interface");

  if (dir4 == (u8) ~0 || dir6 == (u8) ~0)
    return clib_error_return (
      0, "Please specify the MSS clamping direction for ip4 and ip6");

  if (dir4 != MSS_CLAMP_DIR_NONE)
    {
      if (mss4 == ~0)
	return clib_error_return (
	  0, "Please specify the Max Segment Size for ip4");
      if (mss4 >= MSS_CLAMP_UNSET)
	return clib_error_return (0, "Invalid Max Segment Size");
    }
  if (dir6 != MSS_CLAMP_DIR_NONE)
    {
      if (mss6 == ~0)
	return clib_error_return (
	  0, "Please specify the Max Segment Size for ip6");
      if (mss6 >= MSS_CLAMP_UNSET)
	return clib_error_return (0, "Invalid Max Segment Size");
    }

  rv = mssc_enable_disable (sw_if_index, dir4, dir6, mss4, mss6);

  if (rv)
    return clib_error_return (0, "Failed: %d = %U", rv, format_vnet_api_errno,
			      rv);

  return (NULL);
}

VLIB_CLI_COMMAND (mssc_enable_disable_command, static) = {
  .path = "set interface tcp-mss-clamp",
  .short_help = "set interface tcp-mss-clamp <interface-name> "
		"ip4 [enable|disable|rx|tx] ip4-mss <size> "
		"ip6 [enable|disable|rx|tx] ip6-mss <size>",
  .function = mssc_enable_command_fn,
};

static u8 *
format_mssc_clamping (u8 *s, va_list *args)
{
  u8 dir = va_arg (*args, u32);
  u16 mss = va_arg (*args, u32);
#define DIR2S(d)                                                              \
  (((d) == (MSS_CLAMP_DIR_RX | MSS_CLAMP_DIR_TX)) ?                           \
     "" :                                                                     \
     (((d) == MSS_CLAMP_DIR_RX) ? " [RX]" : " [TX]"))

  if (MSS_CLAMP_DIR_NONE == dir)
    {
      return format (s, "disabled");
    }
  u32 mss_u32 = mss;
  return format (s, "%d%s", mss_u32, DIR2S (dir));
}

static clib_error_t *
mssc_show_command_fn (vlib_main_t *vm, unformat_input_t *input,
		      vlib_cli_command_t *cmd)
{
  mssc_main_t *cm = &mssc_main;
  u32 sw_if_index = ~0;
  u32 ii;

  while (unformat_check_input (input) != UNFORMAT_END_OF_INPUT)
    {
      if (unformat (input, "%U", unformat_vnet_sw_interface, vnet_get_main (),
		    &sw_if_index))
	;
      else
	break;
    }

  if (sw_if_index == ~0)
    {
      vec_foreach_index (ii, cm->dir_enabled4)
	{
	  u8 dir4 = cm->dir_enabled4[ii];
	  u8 dir6 = cm->dir_enabled6[ii];
	  if (MSS_CLAMP_DIR_NONE != dir4 || MSS_CLAMP_DIR_NONE != dir6)
	    {
	      u16 mss4 = cm->max_mss4[ii];
	      u16 mss6 = cm->max_mss6[ii];
	      vlib_cli_output (vm, "%U: ip4: %U ip6: %U",
			       format_vnet_sw_if_index_name, vnet_get_main (),
			       ii, format_mssc_clamping, dir4, mss4,
			       format_mssc_clamping, dir6, mss6);
	    }
	}
    }
  else
    {
      u16 mss4, mss6;
      u8 dir4, dir6;
      mssc_get_mss (sw_if_index, &dir4, &dir6, &mss4, &mss6);
      vlib_cli_output (vm, "%U: ip4: %U ip6: %U", format_vnet_sw_if_index_name,
		       vnet_get_main (), sw_if_index, format_mssc_clamping,
		       dir4, mss4, format_mssc_clamping, dir6, mss6);
    }

  return (NULL);
}

VLIB_CLI_COMMAND (mssc_show_command, static) = {
  .path = "show interface tcp-mss-clamp",
  .short_help = "show interface tcp-mss-clamp [interface-name]",
  .long_help = "show TCP MSS clamping configurations",
  .function = mssc_show_command_fn,
};

static clib_error_t *
mssc_init (vlib_main_t *vm)
{
  return NULL;
}

VLIB_INIT_FUNCTION (mssc_init);

/*
 * fd.io coding-style-patch-verification: ON
 *
 * Local Variables:
 * eval: (c-set-style "gnu")
 * End:
 */