Newer
Older
XinYang_IOS / Pods / OpenVPNAdapter / Sources / OpenVPN3 / openvpn / aws / awsroute.hpp
@zhangfeng zhangfeng on 7 Dec 2023 13 KB 1.8.0
//    OpenVPN -- An application to securely tunnel IP networks
//               over a single port, with support for SSL/TLS-based
//               session authentication and key exchange,
//               packet encryption, packet authentication, and
//               packet compression.
//
//    Copyright (C) 2012-2020 OpenVPN Inc.
//
//    This program is free software: you can redistribute it and/or modify
//    it under the terms of the GNU Affero General Public License Version 3
//    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 Affero General Public License for more details.
//
//    You should have received a copy of the GNU Affero General Public License
//    along with this program in the COPYING file.
//    If not, see <http://www.gnu.org/licenses/>.

// Query/Set VPC routes, requires this policy:
//
// {
//     "Version": "2012-10-17",
//     "Statement": [
//         {
//             "Sid": "Stmt1478633458000",
//             "Effect": "Allow",
//             "Action": [
//                 "ec2:CreateRoute",
//                 "ec2:DescribeNetworkInterfaceAttribute",
//                 "ec2:DescribeNetworkInterfaces",
//                 "ec2:DescribeRouteTables",
//                 "ec2:ModifyNetworkInterfaceAttribute",
//                 "ec2:ReplaceRoute",
//                 "ec2:DeleteRoute"
//             ],
//             "Resource": [
//                 "*"
//             ]
//         }
//     ]
// }

#pragma once

#include <openvpn/common/xmlhelper.hpp>
#include <openvpn/aws/awshttp.hpp>
#include <openvpn/aws/awspc.hpp>
#include <openvpn/aws/awsrest.hpp>

namespace openvpn {
  namespace AWS {
    class Route
    {
    public:
      OPENVPN_EXCEPTION(aws_route_error);

      enum class RouteTargetType
        {
	  INTERFACE_ID,
	  INSTANCE_ID
	};

      class Context
      {
      public:
	Context(PCQuery::Info instance_info_arg,
		Creds creds_arg,
		RandomAPI::Ptr rng,
		Stop* async_stop_arg,
		const int debug_level)
	  : instance_info(std::move(instance_info_arg)),
	    http_context(std::move(rng), debug_level),
	    ts(http_context.transaction_set(ec2_host(instance_info))),
	    creds(std::move(creds_arg)),
	    async_stop(async_stop_arg)
	{
	}

	void reset()
	{
	  if (ts)
	    ts->hsc.reset();
	}

	std::string instance_id() const
	{
	  return instance_info.instanceId;
	}

      private:
	friend class Route;
	PCQuery::Info instance_info;
	HTTPContext http_context;
	WS::ClientSet::TransactionSet::Ptr ts;
	Creds creds;
	Stop* async_stop;
      };

      // Query network_interface_id and route_table_id
      // from EC2 API.
      class Info
      {
      public:
	Info(std::string network_interface_id_arg,
	     std::string route_table_id_arg)
	  : network_interface_id(std::move(network_interface_id_arg)),
	    route_table_id(std::move(route_table_id_arg))
	{
	}

	Info(Context& ctx)
	{
	  // AWS IDs local to this constructor
	  std::string vpc_id;
	  std::string subnet_id;

	  // first request -- get the AWS network interface
	  {
	    // create API query
	    {
	      REST::Query q;
	      q.emplace_back("Action", "DescribeNetworkInterfaces");
	      q.emplace_back("Filter.1.Name", "attachment.instance-id");
	      q.emplace_back("Filter.1.Value.1", ctx.instance_info.instanceId);
	      q.emplace_back("Filter.2.Name", "addresses.private-ip-address");
	      q.emplace_back("Filter.2.Value.1", ctx.instance_info.privateIp);
	      add_transaction(ctx, std::move(q));
	    }

	    // do transaction
	    execute_transaction(ctx);

	    // process reply
	    {
	      // get the transaction
	      WS::ClientSet::Transaction& t = ctx.ts->first_transaction();

	      // get reply
	      const std::string reply = t.content_in_string();

	      // check the reply status
	      if (!t.http_status_success())
		OPENVPN_THROW(aws_route_error, "DescribeNetworkInterfaces: " << t.format_status(*ctx.ts));

	      // parse XML reply
	      const Xml::Document doc(reply, "DescribeNetworkInterfaces");
	      const tinyxml2::XMLElement* item = Xml::find(&doc,
							   "DescribeNetworkInterfacesResponse",
							   "networkInterfaceSet",
							   "item");
	      if (!item)
		OPENVPN_THROW(aws_route_error, "DescribeNetworkInterfaces: cannot locate <item> tag in returned XML:\n" << reply);
	      network_interface_id = Xml::find_text(item, "networkInterfaceId");
	      vpc_id = Xml::find_text(item, "vpcId");
	      subnet_id = Xml::find_text(item, "subnetId");
	      if (network_interface_id.empty() || vpc_id.empty() || subnet_id.empty())
		OPENVPN_THROW(aws_route_error, "DescribeNetworkInterfaces: cannot locate one of networkInterfaceId, vpcId, or subnetId in returned XML:\n" << reply);
	    }
	  }

	  // second request -- get the VPC routing table
	  {
	    // create API query
	    {
	      REST::Query q;
	      q.emplace_back("Action", "DescribeRouteTables");
	      q.emplace_back("Filter.1.Name", "vpc-id");
	      q.emplace_back("Filter.1.Value.1", vpc_id);
	      q.emplace_back("Filter.2.Name", "association.subnet-id");
	      q.emplace_back("Filter.2.Value.1", subnet_id);
	      add_transaction(ctx, std::move(q));
	    }

	    // do transaction
	    execute_transaction(ctx);

	    // process reply
	    {
	      // get the transaction
	      WS::ClientSet::Transaction& t = ctx.ts->first_transaction();

	      // get reply
	      const std::string reply = t.content_in_string();

	      // check the reply status
	      if (!t.http_status_success())
		OPENVPN_THROW(aws_route_error, "DescribeRouteTables: " << t.format_status(*ctx.ts) << '\n' << reply);

	      // parse XML reply
	      const Xml::Document doc(reply, "DescribeRouteTables");
	      route_table_id = Xml::find_text(&doc,
					      "DescribeRouteTablesResponse",
					      "routeTableSet",
					      "item",
					      "routeTableId");
	      if (route_table_id.empty())
		OPENVPN_THROW(aws_route_error, "DescribeRouteTables: cannot locate routeTableId in returned XML:\n" << reply);
	    }
	  }
	}

	std::string to_string() const
	{
	  return '[' + network_interface_id + '/' + route_table_id + ']';
	}

	std::string network_interface_id;
	std::string route_table_id;
      };

      // Set sourceDestCheck flag on AWS network interface.
      static void set_source_dest_check(Context& ctx,
					const std::string& network_interface_id,
					const bool source_dest_check)
      {
	const std::string sdc = source_dest_check ? "true" : "false";

	// first get the attribute and see if it is already set
	// the way we want it
	{
	  REST::Query q;
	  q.emplace_back("Action", "DescribeNetworkInterfaceAttribute");
	  q.emplace_back("NetworkInterfaceId", network_interface_id);
	  q.emplace_back("Attribute", "sourceDestCheck");
	  add_transaction(ctx, std::move(q));
	}

	// do transaction
	execute_transaction(ctx);

	// process reply
	{
	  // get the transaction
	  WS::ClientSet::Transaction& t = ctx.ts->first_transaction();

	  // get reply
	  const std::string reply = t.content_in_string();

	  // check the reply status
	  if (!t.http_status_success())
	    OPENVPN_THROW(aws_route_error, "DescribeNetworkInterfaceAttribute: " << t.format_status(*ctx.ts) << '\n' << reply);

	  // parse XML reply
	  const Xml::Document doc(reply, "DescribeNetworkInterfaceAttribute");
	  const std::string retval = Xml::find_text(&doc,
						    "DescribeNetworkInterfaceAttributeResponse",
						    "sourceDestCheck",
						    "value");
	  // already set to desired value?
	  if (retval == sdc)
	    return;
	}

	// create API query
	{
	  REST::Query q;
	  q.emplace_back("Action", "ModifyNetworkInterfaceAttribute");
	  q.emplace_back("NetworkInterfaceId", network_interface_id);
	  q.emplace_back("SourceDestCheck.Value", sdc);
	  add_transaction(ctx, std::move(q));
	}

	// do transaction
	execute_transaction(ctx);

	// process reply
	{
	  // get the transaction
	  WS::ClientSet::Transaction& t = ctx.ts->first_transaction();

	  // get reply
	  const std::string reply = t.content_in_string();

	  // check the reply status
	  if (!t.http_status_success())
	    OPENVPN_THROW(aws_route_error, "ModifyNetworkInterfaceAttribute: " << t.format_status(*ctx.ts) << '\n' << reply);

	  // parse XML reply
	  const Xml::Document doc(reply, "ModifyNetworkInterfaceAttribute");
	  const std::string retval = Xml::find_text(&doc,
						    "ModifyNetworkInterfaceAttributeResponse",
						    "return");
	  if (retval != "true")
	    OPENVPN_THROW(aws_route_error, "ModifyNetworkInterfaceAttribute: returned failure status: " << '\n' << reply);

	  OPENVPN_LOG("AWS EC2 ModifyNetworkInterfaceAttribute " << network_interface_id << " SourceDestCheck.Value=" << sdc);
	}
      }

      static void delete_route(Context& ctx,
      			       const std::string& route_table_id,
			       const std::string& cidr,
			       bool ipv6)
      {
	{
	  REST::Query q;
	  q.emplace_back("Action", "DeleteRoute");
	  q.emplace_back(ipv6 ? "DestinationIpv6CidrBlock" : "DestinationCidrBlock", cidr);
	  q.emplace_back("RouteTableId", route_table_id);
	  add_transaction(ctx, std::move(q));
	}

	// do transaction
	execute_transaction(ctx);

	// process reply
	{
	  // get the transaction
	  WS::ClientSet::Transaction& t = ctx.ts->first_transaction();

	  // get reply
	  const std::string reply = t.content_in_string();

	  // check the reply status
	  if (!t.http_status_success())
	    OPENVPN_THROW(aws_route_error, "DeleteRoute: " << t.format_status(*ctx.ts) << '\n' << reply);

	  // parse XML reply
	  const Xml::Document doc(reply, "DeleteRoute");
	  const std::string retval = Xml::find_text(&doc,
						    "DeleteRouteResponse",
						    "return");
	  if (retval != "true")
	    OPENVPN_THROW(aws_route_error, "DeleteRoute: returned failure status: " << '\n' << reply);

	  OPENVPN_LOG("AWS EC2 DeleteRoute " << cidr << " -> table " << route_table_id);
	}
      }

      // Create/replace a VPC route
      static void replace_create_route(Context& ctx,
				       const std::string& route_table_id,
				       const std::string& route,
				       RouteTargetType target_type,
				       const std::string& target_value,
				       bool ipv6)
      {
	std::string target_type_str;

	switch (target_type)
	{
	case RouteTargetType::INSTANCE_ID:
	  target_type_str = "InstanceId";
	  break;

	case RouteTargetType::INTERFACE_ID:
	  target_type_str = "NetworkInterfaceId";
	  break;

	default:
	  OPENVPN_THROW(aws_route_error,
	  		"replace_create_route: unknown RouteTargetType " << (int)target_type);
	}

	const std::string dest_cidr_block_name = ipv6 ?
		"DestinationCidrIpv6Block" : "DestinationCidrBlock";

	// create API query
	{
	  REST::Query q;
	  q.emplace_back("Action", "ReplaceRoute");
	  q.emplace_back(dest_cidr_block_name, route);
	  q.emplace_back(target_type_str, target_value);
	  q.emplace_back("RouteTableId", route_table_id);
	  add_transaction(ctx, std::move(q));
	}

	// do transaction
	execute_transaction(ctx);

	// process reply
	{
	  // get the transaction
	  WS::ClientSet::Transaction& t = ctx.ts->first_transaction();

	  // get reply
	  const std::string reply = t.content_in_string();

	  // Check the reply status.  We only throw on communication failure,
	  // since ReplaceRoute will legitimately fail if the route doesn't
	  // exist yet.
	  if (!t.comm_status_success())
	    OPENVPN_THROW(aws_route_error, "ReplaceRoute: " << t.format_status(*ctx.ts) << '\n' << reply);

	  // ReplaceRoute succeeded?
	  if (t.request_status_success())
	    {
	      // parse XML reply
	      const Xml::Document doc(reply, "ReplaceRoute");
	      const std::string retval = Xml::find_text(&doc,
							"ReplaceRouteResponse",
							"return");
	      if (retval == "true")
		{
		  OPENVPN_LOG("AWS EC2 ReplaceRoute " << route << " -> table " << route_table_id);
		  return;
		}
	    }
	}

	// Now try CreateRoute
	{
	  REST::Query q;
	  q.emplace_back("Action", "CreateRoute");
	  q.emplace_back(dest_cidr_block_name, route);
	  q.emplace_back(target_type_str, target_value);
	  q.emplace_back("RouteTableId", route_table_id);
	  add_transaction(ctx, std::move(q));
	}

	// do transaction
	execute_transaction(ctx);

	// process reply
	{
	  // get the transaction
	  WS::ClientSet::Transaction& t = ctx.ts->first_transaction();

	  // get reply
	  const std::string reply = t.content_in_string();

	  // check the reply status
	  if (!t.http_status_success())
	    OPENVPN_THROW(aws_route_error, "CreateRoute: " << t.format_status(*ctx.ts) << '\n' << reply);

	  // parse XML reply
	  const Xml::Document doc(reply, "CreateRoute");
	  const std::string retval = Xml::find_text(&doc,
						    "CreateRouteResponse",
						    "return");
	  if (retval != "true")
	    OPENVPN_THROW(aws_route_error, "CreateRoute: returned failure status: " << '\n' << reply);

	  OPENVPN_LOG("AWS EC2 CreateRoute " << route << " -> table " << route_table_id);
	}
      }

    private:
      static void execute_transaction(Context& ctx)
      {
	WS::ClientSet::new_request_synchronous(ctx.ts, ctx.async_stop, ctx.http_context.rng(), true);
      }

      static void add_transaction(const Context& ctx, REST::Query&& q)
      {
	std::unique_ptr<WS::ClientSet::Transaction> t(new WS::ClientSet::Transaction);
	t->req.uri = ec2_uri(ctx, std::move(q));
	t->req.method = "GET";
	t->ci.keepalive = true;
	ctx.ts->transactions.clear();
	ctx.ts->transactions.push_back(std::move(t));
      }

      static std::string ec2_uri(const Context& ctx, REST::Query&& q)
      {
	REST::QueryBuilder qb;
	qb.date = REST::amz_date();
	qb.expires = 300;
	qb.region = ctx.instance_info.region;
	qb.service = "ec2";
	qb.method = "GET";
	qb.host = ec2_host(ctx.instance_info);
	qb.uri = "/";
	qb.parms = std::move(q);
	qb.parms.emplace_back("Version", "2015-10-01");
	qb.add_amz_parms(ctx.creds);
	qb.sort_parms();
	qb.add_amz_signature(ctx.http_context.digest_factory(), ctx.creds);
	return qb.uri_query();
      }

      static std::string ec2_host(const PCQuery::Info& instance_info)
      {
	return "ec2." + instance_info.region + ".amazonaws.com";
      }
    };
  }
}