From 5475bb41e8f1ee2b5cc49b6b1afaeb0cb53a4021 Mon Sep 17 00:00:00 2001 From: Guslington Date: Fri, 10 Dec 2021 11:17:44 +1100 Subject: [PATCH] support creating routes for multiple subnets --- docs/slack-notifications.md | 1 - lib/cfnvpn/actions/embedded.rb | 7 +- lib/cfnvpn/actions/init.rb | 3 +- lib/cfnvpn/actions/modify.rb | 3 +- lib/cfnvpn/actions/revoke.rb | 5 +- lib/cfnvpn/actions/routes.rb | 36 ++-- lib/cfnvpn/actions/sessions.rb | 9 +- lib/cfnvpn/actions/share.rb | 7 +- lib/cfnvpn/actions/subnets.rb | 8 +- lib/cfnvpn/clientvpn.rb | 60 ++++-- .../lambdas/auto_route_populator/app.py | 196 +++++++++--------- .../lambdas/auto_route_populator/states.py | 2 - lib/cfnvpn/templates/vpn.rb | 34 ++- 13 files changed, 198 insertions(+), 173 deletions(-) diff --git a/docs/slack-notifications.md b/docs/slack-notifications.md index bde3926..fe84757 100644 --- a/docs/slack-notifications.md +++ b/docs/slack-notifications.md @@ -24,7 +24,6 @@ cfn-vpn modify [name] --slack-webhook-url "https://hooks.slack.com/services/T000 - `ROUTE_LIMIT_EXCEEDED`: no new routes can be added to the route table due to AWS route table limit - `AUTH_RULE_LIMIT_EXCEEDED`: no new authorization rules can be added to the rule list due to AWS auth rule limit - `RESOLVE_FAILED`: failed to resolve the provided dns entry -- `RATE_LIMIT_EXCEEDED`: concurrent modifications of the route table is being rated limited - `SUBNET_NOT_ASSOCIATED`: no subnets are associated with the Client VPN - `QUOTA_INCREASE_REQUEST`: automatic quota increase made diff --git a/lib/cfnvpn/actions/embedded.rb b/lib/cfnvpn/actions/embedded.rb index 3be579e..ed0f773 100644 --- a/lib/cfnvpn/actions/embedded.rb +++ b/lib/cfnvpn/actions/embedded.rb @@ -50,11 +50,10 @@ def download_certificates def download_config vpn = CfnVpn::ClientVpn.new(@name,@options['region']) - @endpoint_id = vpn.get_endpoint_id() - CfnVpn::Log.logger.debug "downloading client config for #{@endpoint_id}" - @config = vpn.get_config(@endpoint_id) + CfnVpn::Log.logger.debug "downloading client config for #{vpn.endpoint_id}" + @config = vpn.get_config() string = (0...8).map { (65 + rand(26)).chr.downcase }.join - @config.sub!(@endpoint_id, "#{string}.#{@endpoint_id}") + @config.sub!(vpn.endpoint_id, "#{string}.#{vpn.endpoint_id}") end def add_routes diff --git a/lib/cfnvpn/actions/init.rb b/lib/cfnvpn/actions/init.rb index bcdc747..ea1917f 100644 --- a/lib/cfnvpn/actions/init.rb +++ b/lib/cfnvpn/actions/init.rb @@ -143,8 +143,7 @@ def deploy_vpn def finish vpn = CfnVpn::ClientVpn.new(@name,@options['region']) - @endpoint_id = vpn.get_endpoint_id() - CfnVpn::Log.logger.info "Client VPN #{@endpoint_id} created. Run `cfn-vpn config #{@name}` to setup the client config" + CfnVpn::Log.logger.info "Client VPN #{vpn.endpoint_id} created. Run `cfn-vpn config #{@name}` to setup the client config" end end diff --git a/lib/cfnvpn/actions/modify.rb b/lib/cfnvpn/actions/modify.rb index f596eff..a979636 100644 --- a/lib/cfnvpn/actions/modify.rb +++ b/lib/cfnvpn/actions/modify.rb @@ -164,8 +164,7 @@ def deploy_vpn def finish vpn = CfnVpn::ClientVpn.new(@name,@options['region']) - @endpoint_id = vpn.get_endpoint_id() - CfnVpn::Log.logger.info "Client VPN #{@endpoint_id} modified." + CfnVpn::Log.logger.info "Client VPN #{vpn.endpoint_id} modified." end end diff --git a/lib/cfnvpn/actions/revoke.rb b/lib/cfnvpn/actions/revoke.rb index d7f4752..8ab6d89 100644 --- a/lib/cfnvpn/actions/revoke.rb +++ b/lib/cfnvpn/actions/revoke.rb @@ -42,9 +42,8 @@ def revoke_certificate def apply_rekocation_list vpn = CfnVpn::ClientVpn.new(@name,@options['region']) - endpoint_id = vpn.get_endpoint_id() - vpn.put_revoke_list(endpoint_id,"#{@cert_dir}/crl.pem") - CfnVpn::Log.logger.info("revoked client #{@options['client_cn']} from #{endpoint_id}") + vpn.put_revoke_list("#{@cert_dir}/crl.pem") + CfnVpn::Log.logger.info("revoked client #{@options['client_cn']} from #{vpn.endpoint_id}") end end diff --git a/lib/cfnvpn/actions/routes.rb b/lib/cfnvpn/actions/routes.rb index 8bfba59..a85929a 100644 --- a/lib/cfnvpn/actions/routes.rb +++ b/lib/cfnvpn/actions/routes.rb @@ -14,7 +14,7 @@ class Routes < Thor::Group class_option :cidr, desc: 'cidr range' class_option :dns, desc: 'dns record to auto lookup ip' - class_option :subnet, desc: 'the target vpc subnet to route through, if none is supplied the default subnet is used' + class_option :subnets, type: :array, desc: 'target vpc subnets to route through, if none is supplied the default subnets are used' class_option :desc, desc: 'description of the route' class_option :groups, type: :array, desc: 'override all authorised groups on thr route' @@ -83,15 +83,15 @@ def set_route CfnVpn::Log.logger.warn "description for this route cannot be updated in place. To alter delete the route and add with the new description" end - if @options[:subnet] - CfnVpn::Log.logger.warn "the target subnet for this route cannot be updated in place. To alter delete the route and add with the new target subnet" + if @options[:subnets] + CfnVpn::Log.logger.warn "the target subnets for this route cannot be updated in place. To alter delete the route and add with the new target subnet" end elsif !@route && @options[:cidr] CfnVpn::Log.logger.info "adding new route for #{@options[:cidr]}" @config[:routes] << { cidr: @options[:cidr], desc: @options.fetch(:desc, ""), - subnet: @options.fetch(:subnet, @config[:subnet_ids].first), + subnets: @options.fetch(:subnets, @config[:subnet_ids]), groups: @options.fetch(:groups, []) + @options.fetch(:add_groups, []) } elsif !@route && @options[:dns] @@ -99,7 +99,7 @@ def set_route @config[:routes] << { dns: @options[:dns], desc: @options.fetch(:desc, ""), - subnet: @options.fetch(:subnet, @config[:subnet_ids].first), + subnets: @options.fetch(:subnets, @config[:subnet_ids]), groups: @options.fetch(:groups, []) + @options.fetch(:add_groups, []) } else @@ -163,27 +163,31 @@ def deploy_vpn end end + def get_routes + @vpn = CfnVpn::ClientVpn.new(@name, @options['region']) + end + def cleanup_dns_routes - @vpn = CfnVpn::ClientVpn.new(@name,@options['region']) unless @dns_route_cleanup.nil? - routes = @vpn.get_routes() CfnVpn::Log.logger.info("Cleaning up expired routes for #{@dns_route_cleanup}") - expired_routes = routes.select {|route| route.description.include?(@dns_route_cleanup) } + expired_routes = @vpn.get_routes(@dns_route_cleanup) expired_routes.each do |route| + CfnVpn::Log.logger.info("Removing expired route #{route.destination_cidr} for target subnet #{route.target_subnet}") @vpn.delete_route(route.destination_cidr, route.target_subnet) - @vpn.revoke_auth(route.destination_cidr) end - end - end - def get_routes - @endpoint = @vpn.get_endpoint_id() - @routes = @vpn.get_routes() + expired_rules = @vpn.get_auth_rules(@dns_route_cleanup) + expired_rules.each do |rule| + CfnVpn::Log.logger.info("Removing expired auth rule for route #{route.destination_cidr}") + @vpn.revoke_auth(rule.destination_cidr) + end + end end def display_routes - rows = @routes.collect do |s| - groups = @vpn.get_groups_for_route(@endpoint, s.destination_cidr) + routes = @vpn.get_routes() + rows = routes.collect do |s| + groups = @vpn.get_groups_for_route(s.destination_cidr) [ s.destination_cidr, s.description, s.status.code, s.target_subnet, s.type, s.origin, (!groups.join("").empty? ? groups.join(' ') : 'AllowAll') ] end table = Terminal::Table.new( diff --git a/lib/cfnvpn/actions/sessions.rb b/lib/cfnvpn/actions/sessions.rb index e43c965..4ed6b9f 100644 --- a/lib/cfnvpn/actions/sessions.rb +++ b/lib/cfnvpn/actions/sessions.rb @@ -29,20 +29,19 @@ def set_directory @build_dir = "#{CfnVpn.cfnvpn_path}/#{@name}" end - def get_endpoint + def setup @vpn = CfnVpn::ClientVpn.new(@name,@options['region']) - @endpoint_id = @vpn.get_endpoint_id() end def kill_session if !@options['kill'].nil? - sessions = @vpn.get_sessions(@endpoint_id) + sessions = @vpn.get_sessions() session = sessions.select { |s| s if s.connection_id == @options['kill'] }.first if session.any? && session.status.code == "active" terminate = yes? "Terminate connection #{@options['kill']} for #{session.common_name}?", :yellow if terminate CfnVpn::Log.logger.info "Terminating connection #{@options['kill']} for #{session.common_name}" - @vpn.kill_session(@endpoint_id,@options['kill']) + @vpn.kill_session(@options['kill']) end else CfnVpn::Log.logger.error "Connection id #{@options['kill']} doesn't exist or is not active" @@ -51,7 +50,7 @@ def kill_session end def display_sessions - sessions = @vpn.get_sessions(@endpoint_id) + sessions = @vpn.get_sessions() rows = sessions.collect do |s| [ s.common_name, s.connection_established_time, s.status.code, s.client_ip, s.connection_id, s.ingress_bytes, s.egress_bytes ] end diff --git a/lib/cfnvpn/actions/share.rb b/lib/cfnvpn/actions/share.rb index ba7b886..bb3fcd2 100644 --- a/lib/cfnvpn/actions/share.rb +++ b/lib/cfnvpn/actions/share.rb @@ -26,11 +26,10 @@ def set_loglevel def copy_config_to_s3 vpn = CfnVpn::ClientVpn.new(@name,@options['region']) - @endpoint_id = vpn.get_endpoint_id() - CfnVpn::Log.logger.debug "downloading client config for #{@endpoint_id}" - @config = vpn.get_config(@endpoint_id) + CfnVpn::Log.logger.debug "downloading client config for #{vpn.endpoint_id}" + @config = vpn.get_config() string = (0...8).map { (65 + rand(26)).chr.downcase }.join - @config.sub!(@endpoint_id, "#{string}.#{@endpoint_id}") + @config.sub!(vpn.endpoint_id, "#{string}.#{vpn.endpoint_id}") end def add_routes diff --git a/lib/cfnvpn/actions/subnets.rb b/lib/cfnvpn/actions/subnets.rb index c04e374..520b356 100644 --- a/lib/cfnvpn/actions/subnets.rb +++ b/lib/cfnvpn/actions/subnets.rb @@ -61,13 +61,9 @@ def disassociate end end - def get_endpoint - @vpn = CfnVpn::ClientVpn.new(@name,@options['region']) - @endpoint_id = @vpn.get_endpoint_id() - end - def associations - associations = @vpn.get_associations(@endpoint_id) + vpn = CfnVpn::ClientVpn.new(@name,@options['region']) + associations = vpn.get_associations() table = Terminal::Table.new( :headings => ['ID', 'Subnet', 'Status', 'CIDR', 'AZ', 'Groups'], :rows => associations.map {|ass| ass.values}) diff --git a/lib/cfnvpn/clientvpn.rb b/lib/cfnvpn/clientvpn.rb index aad4097..2950a6a 100644 --- a/lib/cfnvpn/clientvpn.rb +++ b/lib/cfnvpn/clientvpn.rb @@ -5,6 +5,7 @@ module CfnVpn class ClientVpn + attr_reader :endpoint_id def initialize(name,region) @client = Aws::EC2::Client.new(region: region) @@ -31,56 +32,71 @@ def get_dns_servers() return get_endpoint().dns_servers end - def get_config(endpoint_id) + def get_config() resp = @client.export_client_vpn_client_configuration({ - client_vpn_endpoint_id: endpoint_id + client_vpn_endpoint_id: @endpoint_id }) return resp.client_configuration end - def get_rekove_list(endpoint_id) + def get_rekove_list() resp = @client.export_client_vpn_client_certificate_revocation_list({ - client_vpn_endpoint_id: endpoint_id + client_vpn_endpoint_id: @endpoint_id }) return resp.certificate_revocation_list end - def put_revoke_list(endpoint_id,revoke_list) + def put_revoke_list(revoke_list) list = File.read(revoke_list) @client.import_client_vpn_client_certificate_revocation_list({ - client_vpn_endpoint_id: endpoint_id, + client_vpn_endpoint_id: @endpoint_id, certificate_revocation_list: list }) end - def get_sessions(endpoint_id) + def get_sessions() params = { - client_vpn_endpoint_id: endpoint_id, + client_vpn_endpoint_id: @endpoint_id, max_results: 20 } resp = @client.describe_client_vpn_connections(params) return resp.connections end - def kill_session(endpoint_id, connection_id) + def kill_session(connection_id) @client.terminate_client_vpn_connections({ - client_vpn_endpoint_id: endpoint_id, + client_vpn_endpoint_id: @endpoint_id, connection_id: connection_id }) end - def get_routes() - endpoint_id = get_endpoint_id() - resp = @client.describe_client_vpn_routes({ - client_vpn_endpoint_id: endpoint_id, - max_results: 20 - }) - return resp.routes + def get_routes(dns_route=nil) + routes = [] + @client.describe_client_vpn_routes({client_vpn_endpoint_id: @endpoint_id}).each do |resp| + if dns_route + routes.concat resp.routes.select {|route| route.description.include?(dns_route) } + else + routes.concat resp.routes + end + end + return routes end - def get_groups_for_route(endpoint, cidr) + def get_auth_rules(dns_route=nil) + rules = [] + @client.describe_client_vpn_authorization_rules({client_vpn_endpoint_id: @endpoint_id}) do |resp| + if dns_route + rules.concat resp.authorization_rules.select {|rule| rule.description.include?(dns_route) } + else + rules.concat resp.routes + end + end + return rules + end + + def get_groups_for_route(cidr) auth_resp = @client.describe_client_vpn_authorization_rules({ - client_vpn_endpoint_id: endpoint, + client_vpn_endpoint_id: @endpoint_id, filters: [ { name: 'destination-cidr', @@ -91,10 +107,10 @@ def get_groups_for_route(endpoint, cidr) return auth_resp.authorization_rules.map {|rule| rule.group_id } end - def get_associations(endpoint) + def get_associations() associations = [] resp = @client.describe_client_vpn_target_networks({ - client_vpn_endpoint_id: endpoint + client_vpn_endpoint_id: @endpoint_id }) resp.client_vpn_target_networks.each do |net| @@ -102,7 +118,7 @@ def get_associations(endpoint) subnet_ids: [net.target_network_id] }) subnet = subnet_resp.subnets.first - groups = get_groups_for_route(endpoint, subnet.cidr_block) + groups = get_groups_for_route(subnet.cidr_block) associations.push({ association_id: net.association_id, diff --git a/lib/cfnvpn/templates/lambdas/auto_route_populator/app.py b/lib/cfnvpn/templates/lambdas/auto_route_populator/app.py index c8ec0b3..d1bf8eb 100644 --- a/lib/cfnvpn/templates/lambdas/auto_route_populator/app.py +++ b/lib/cfnvpn/templates/lambdas/auto_route_populator/app.py @@ -26,7 +26,7 @@ def delete_route(client, vpn_endpoint, subnet, cidr): raise e -def create_route(client, event, cidr): +def create_route(client, event, cidr, target_subnet): description = f"cfnvpn auto generated route for endpoint {event['Record']}." if event['Description']: description += f" {event['Description']}" @@ -34,7 +34,7 @@ def create_route(client, event, cidr): client.create_client_vpn_route( ClientVpnEndpointId=event['ClientVpnEndpointId'], DestinationCidrBlock=cidr, - TargetVpcSubnetId=event['TargetSubnet'], + TargetVpcSubnetId=target_subnet, Description=description ) @@ -81,7 +81,8 @@ def authorize_route(client, event, cidr, group = None): def get_routes(client, event): - response = client.describe_client_vpn_routes( + paginator = client.get_paginator('describe_client_vpn_routes') + response_iterator = paginator.paginate( ClientVpnEndpointId=event['ClientVpnEndpointId'], Filters=[ { @@ -90,23 +91,40 @@ def get_routes(client, event): } ] ) - - routes = [route for route in response['Routes'] if event['Record'] in route['Description']] - logger.info(f"found {len(routes)} existing routes for {event['Record']}") - return routes + + return [route for page in response_iterator + for route in page['Routes'] + if event['Record'] in route['Description']] -def get_rules(client, vpn_endpoint, cidr): - response = client.describe_client_vpn_authorization_rules( - ClientVpnEndpointId=vpn_endpoint, - Filters=[ - { - 'Name': 'destination-cidr', - 'Values': [cidr] - } - ] +def get_auth_rules(client, event): + paginator = client.get_paginator('describe_client_vpn_authorization_rules') + response_iterator = paginator.paginate( + ClientVpnEndpointId=event['ClientVpnEndpointId'] ) - return response['AuthorizationRules'] + + return [rule for page in response_iterator + for rule in page['AuthorizationRules'] + if event['Record'] in rule['Description']] + + +def expired_auth_rules(auth_rules, cidrs, groups): + for rule in auth_rules: + # if there is a rule for the record with an old cidr + if rule['DestinationCidr'] not in cidrs: + yield rule + # if there is a rule for a group that is no longer in the event + if groups and rule['GroupId'] not in groups: + yield rule + # if there is a rule for allow all but groups are in the event + if groups and rule['AccessAll']: + yield rule + + +def expired_routes(routes, cidrs): + for route in routes: + if route['DestinationCidr'] not in cidrs: + yield route def handler(event,context): @@ -142,37 +160,65 @@ def handler(event,context): return 'KO' routes = get_routes(client, event) + auth_rules = get_auth_rules(client, event) auto_limit_increase = os.environ.get('AUTO_LIMIT_INCREASE') route_limit_increase_required = False auth_rules_limit_increase_required = False for cidr in cidrs: - route = next((route for route in routes if route['DestinationCidr'] == cidr), None) - - # if there are no existing routes for the endpoint cidr create a new route - if route is None: - try: - create_route(client, event, cidr) - if 'Groups' in event: - for group in event['Groups']: - authorize_route(client, event, cidr, group) + # create route if doesn't exist + for subnet in event['TargetSubnets']: + if not any(route['DestinationCidr'] == cidr and route['TargetSubnet'] == subnet for route in routes): + try: + create_route(client, event, cidr, subnet) + except ClientError as e: + if e.response['Error']['Code'] == 'ClientVpnRouteLimitExceeded': + route_limit_increase_required = True + logger.error("vpn route table has reached the route limit", exc_info=True) + slack.post_event( + message=f"unable to create route {cidr} from {event['Record']}", + state=ROUTE_LIMIT_EXCEEDED, + error="vpn route table has reached the route limit" + ) + elif e.response['Error']['Code'] == 'InvalidClientVpnActiveAssociationNotFound': + logger.warn("no subnets are associated with the vpn", exc_info=True) + slack.post_event( + message=f"unable to create the route {cidr} from {event['Record']}", + state=SUBNET_NOT_ASSOCIATED, + error="no subnets are associated with the vpn" + ) + else: + logger.error("encountered a unexpected client error when creating a route", exc_info=True) else: - authorize_route(client, event, cidr) - except ClientError as e: - if e.response['Error']['Code'] == 'InvalidClientVpnDuplicateRoute': - logger.error(f"route for CIDR {cidr} already exists with a different endpoint") - continue - elif e.response['Error']['Code'] == 'ClientVpnRouteLimitExceeded': - route_limit_increase_required = True - logger.error("vpn route table has reached the route limit", exc_info=True) slack.post_event( - message=f"unable to create route {cidr} from {event['Record']}", - state=ROUTE_LIMIT_EXCEEDED, - error="vpn route table has reached the route limit" + message=f"created new route {cidr} ({event['Record']}) to target subnet {subnet}", + state=NEW_ROUTE ) - continue - elif e.response['Error']['Code'] == 'ClientVpnAuthorizationRuleLimitExceeded': + + # remove route if target subnet has changed + for route in routes: + if route['DestinationCidr'] == cidr and route['TargetSubnet'] not in event['TargetSubnets']: + delete_route(client, event['ClientVpnEndpointId'], route['TargetSubnet'], cidr) + + # collect all rules that matches the current cidr + cidr_auth_rules = [rule for rule in auth_rules if rule['DestinationCidr'] == cidr] + + try: + # create rules for newly added groups + if 'Groups' in event: + existing_groups = list(set(rule['GroupId'] for rule in cidr_auth_rules)) + new_groups = [group for group in event['Groups'] if group not in existing_groups] + + for group in new_groups: + authorize_route(client, event, cidr, group) + + # create an allow all rule + elif 'Groups' not in event and not cidr_auth_rules: + authorize_route(client, event, cidr) + + except ClientError as e: + if e.response['Error']['Code'] == 'ClientVpnAuthorizationRuleLimitExceeded': auth_rules_limit_increase_required = True logger.error("vpn has reached the authorization rule limit", exc_info=True) slack.post_event( @@ -181,62 +227,10 @@ def handler(event,context): error="vpn has reached the authorization rule limit" ) continue - elif e.response['Error']['Code'] == 'ConcurrentMutationLimitExceeded': - logger.error("authorization rule modifications are being rated limited", exc_info=True) - slack.post_event( - message=f"unable to add authorization rule for route {cidr} from {event['Record']}", - state=RATE_LIMIT_EXCEEDED, - error="authorization rule modifications are being rated limited" - ) - continue - elif e.response['Error']['Code'] == 'InvalidClientVpnActiveAssociationNotFound': - logger.error("no subnets are associated with the vpn", exc_info=True) - slack.post_event( - message=f"unable to create the route {cidr} from {event['Record']}", - state=SUBNET_NOT_ASSOCIATED, - error="no subnets are associated with the vpn" - ) - continue - raise e + else: + logger.error("encountered a unexpected client error when creating an auth rule", exc_info=True) - slack.post_event(message=f"added new route {cidr} for DNS entry {event['Record']}", state=NEW_ROUTE) - - # if the route already exists - else: - - logger.info(f"route for cidr {cidr} is already in place") - - # if the target subnet has changed in the payload, recreate the routes to use the new subnet - if route['TargetSubnet'] != event['TargetSubnet']: - logger.info(f"target subnet for route for {cidr} has changed, recreating the route") - delete_route(client, event['ClientVpnEndpointId'], route['TargetSubnet'], cidr) - create_route(client, event, cidr) - - logger.info(f"checking authorization rules for the route") - - # check the rules match the payload - rules = get_rules(client, event['ClientVpnEndpointId'], cidr) - existing_groups = [rule['GroupId'] for rule in rules] - if 'Groups' in event: - # remove expired rules not defined in the payload anymore - expired_rules = [rule for rule in rules if rule['GroupId'] not in event['Groups']] - for rule in expired_rules: - logger.info(f"removing expired authorization rule for group {rule['GroupId']} for route {cidr}") - revoke_route_auth(client, event, cidr, rule['GroupId']) - # add new rules defined in the payload - new_rules = [group for group in event['Groups'] if group not in existing_groups] - for group in new_rules: - logger.info(f"creating new authorization rule for group {rule['GroupId']} for route {cidr}") - authorize_route(client, event, cidr, group) - else: - # if amount of rules for the cidr is greater than 1 when no groups are specified in the payload - # we'll assume that all groups have been removed from the payload so we'll remove all existing rules and add a rule for allow all - if len(rules) > 1: - logger.info(f"creating an allow all rule for route {cidr}") - revoke_route_auth(client, event, cidr) - authorize_route(client, event, cidr) - - # request limit increase + # request route limit increase if route_limit_increase_required and auto_limit_increase: case_id = increase_quota(10, ROUTE_TABLE_QUOTA_CODE, event['ClientVpnEndpointId']) if case_id is not None: @@ -244,18 +238,22 @@ def handler(event,context): else: logger.info(f"routes per vpn service quota increase request pending") + # request auth rule limit increase if auth_rules_limit_increase_required and auto_limit_increase: case_id = increase_quota(20, AUTH_RULE_TABLE_QUOTA_CODE, event['ClientVpnEndpointId']) if case_id is not None: slack.post_event(message=f"requested an increase for the authorization rules per vpn service quota", state=QUOTA_INCREASE_REQUEST, support_case=case_id) else: logger.info(f"authorization rules per vpn service quota increase request pending") - - # clean up any expired routes when the ips for an endpoint change - expired_routes = [route for route in routes if route['DestinationCidr'] not in cidrs] - for route in expired_routes: - logger.info(f"removing expired route {route['DestinationCidr']} for endpoint {event['Record']}") + + # remove expired auth rules + for rule in expired_auth_rules(auth_rules, cidrs, event.get('Groups', [])): + logger.info(f"removing expired auth rule {rule['DestinationCidr']} for endpoint {event['Record']}") revoke_route_auth(client, event, route['DestinationCidr']) + + # remove expired routes + for route in expired_routes(routes, cidrs): + logger.info(f"removing expired route {route['DestinationCidr']} for endpoint {event['Record']}") delete_route(client, event['ClientVpnEndpointId'], route['TargetSubnet'], route['DestinationCidr']) slack.post_event(message=f"removed expired route {route['DestinationCidr']} for endpoint {event['Record']}", state=EXPIRED_ROUTE) diff --git a/lib/cfnvpn/templates/lambdas/auto_route_populator/states.py b/lib/cfnvpn/templates/lambdas/auto_route_populator/states.py index fb5771a..3a44837 100644 --- a/lib/cfnvpn/templates/lambdas/auto_route_populator/states.py +++ b/lib/cfnvpn/templates/lambdas/auto_route_populator/states.py @@ -7,7 +7,6 @@ ROUTE_LIMIT_EXCEEDED: no new routes can be added to the route table due to aws route table limit AUTH_RULE_LIMIT_EXCEEDED: no new authorization rules can be added to the rule list due to aws auth rule limit RESOLVE_FAILED: failed to resolve the provided dns entry -RATE_LIMIT_EXCEEDED: concurrent modifications of the route table is being rated limited SUBNET_NOT_ASSOCIATED: no subnets are associated with the client vpn QUOTA_INCREASE_REQUEST: automatic quota increase made """ @@ -18,6 +17,5 @@ ROUTE_LIMIT_EXCEEDED = 'ROUTE_LIMIT_EXCEEDED' AUTH_RULE_LIMIT_EXCEEDED = 'AUTH_RULE_LIMIT_EXCEEDED' RESOLVE_FAILED = 'RESOLVE_FAILED' -RATE_LIMIT_EXCEEDED = 'RATE_LIMIT_EXCEEDED' SUBNET_NOT_ASSOCIATED = 'SUBNET_NOT_ASSOCIATED' QUOTA_INCREASE_REQUEST = 'QUOTA_INCREASE_REQUEST' \ No newline at end of file diff --git a/lib/cfnvpn/templates/vpn.rb b/lib/cfnvpn/templates/vpn.rb index 5e1a329..55b59db 100644 --- a/lib/cfnvpn/templates/vpn.rb +++ b/lib/cfnvpn/templates/vpn.rb @@ -134,10 +134,19 @@ def render(name, config) auto_route_populator(name, config) dns_routes.each do |route| + # to aide in the migration from single to HA routes if the vpn is HA + if route[:subnets] + target_subnets = route[:subnets] + elsif config[:subnet_ids].include?(route[:subnet]) + target_subnets = config[:subnet_ids] + else + target_subnets = [*route[:subnet]] + end + input = { Record: route[:dns], ClientVpnEndpointId: "${ClientVpnEndpoint}", - TargetSubnet: route[:subnet], + TargetSubnets: target_subnets, Description: route[:desc] } @@ -164,12 +173,23 @@ def render(name, config) if cidr_routes.any? cidr_routes.each do |route| - EC2_ClientVpnRoute(:"#{route[:cidr].resource_safe}VpnRoute") { - Description "cfnvpn static route for #{route[:cidr]}. #{route[:desc]}".strip - ClientVpnEndpointId Ref(:ClientVpnEndpoint) - DestinationCidrBlock route[:cidr] - TargetVpcSubnetId route[:subnet] - } + # to aide in the migration from single to HA routes if the vpn is HA + if route[:subnets] + target_subnets = route[:subnets] + elsif config[:subnet_ids].include?(route[:subnet]) + target_subnets = config[:subnet_ids] + else + target_subnets = [*route[:subnet]] + end + + target_subnets.each do |subnet| + EC2_ClientVpnRoute(:"#{route[:cidr].resource_safe}VpnRouteTo#{subnet.resource_safe}"[0..255]) { + Description "cfnvpn static route for #{route[:cidr]}. #{route[:desc]}".strip + ClientVpnEndpointId Ref(:ClientVpnEndpoint) + DestinationCidrBlock route[:cidr] + TargetVpcSubnetId subnet + } + end if route[:groups].any? route[:groups].each do |group|