# frozen_string_literal: true require 'json' require 'netaddr' require 'optimist' require 'resolv' require 'rest_client' require 'semantic_logger' module OpenVPNConfigurator class RouteGatherer include SemanticLogger::Loggable AWS_IP_RANGES_URL = 'https://ip-ranges.amazonaws.com/ip-ranges.json'.freeze def extend_template(options) template = read_file options[:input_path] old_output = read_file options[:output_path], nonexistent_behavior: :empty_string routes = gather_routes options rendered = render_routes routes result = format "%s\n\n\n# Added by OpenVPN Configurator v%s\n%s", template, VERSION, rendered if result != old_output logger.info "Output content changed, rewriting file #{options[:output_path].inspect}" write_file options[:output_path], result change_actions options else logger.info 'Output content unchanged, no actions taken.' end end private def aws_ip_ranges @aws_ip_ranges ||= begin logger.trace "Fetching AWS IP ranges from #{AWS_IP_RANGES_URL.inspect}" data = RestClient.get AWS_IP_RANGES_URL JSON.parse data, symbolize_names: true end end def change_actions(options) options[:restart_systemd].each { |service| restart_systemd service } end # @return [Hash] def gather_routes(options) result = {} options[:route_v4_aws_region].each { |region| result.merge! gather_v4_aws_region(region) } options[:route_v4_dns].each { |name| result.merge! gather_v4_dns(name) } result end def gather_v4_aws_region(region) networks = aws_ip_ranges[:prefixes].select { |p| p[:region] == region } routes = networks.map { |p| NetAddr.parse_net p[:ip_prefix] } { "route-v4-aws-region=#{region}" => routes } end def gather_v4_dns(name) { "route-v4-dns=#{name}" => resolve_v4(name).map { |a| NetAddr.parse_net "#{a}/32" } } end def read_file(path, nonexistent_behavior: :raise) File.read path rescue Errno::ENOENT => e case nonexistent_behavior when :empty_string '' else logger.error "Error reading file #{path.inspect}", e raise end end # For tests, this method is stubbable # @return [Array] def resolve_v4(name) logger.trace "Performing DNS lookup for hostname #{name.inspect}" Resolv.getaddresses(name).map { |a| NetAddr.parse_ip a }.select { |a| a.is_a? NetAddr::IPv4 } end def restart_systemd(service_name) logger.info "Restarting systemd service #{service_name.inspect}" system 'systemctl', 'restart', service_name end def write_file(path, contents) File.write path, contents end # @param routes [Hash>] { "Configuration Source Name" => [NetAddr::IPv4Net, NetAddr::IPv6Net] } def render_routes(routes) result = [] routes.keys.sort.each do |source| result << "##{source}" routes[source].map(&:to_s).sort.each do |route| # raise "Only supporting IPv4 and IPv6 networks presently, got #{route.inspect} instead" unless [NetAddr::IPv4Net, NetAddr::IPv6Net].include?(route.class) result << format("push\t\t\t\"route %s\"", route) end result << '' end result.join "\n" end end end