puppetlabs/facter-ng

View on GitHub
lib/resolvers/windows/networking_resolver.rb

Summary

Maintainability
A
1 hr
Test Coverage
# frozen_string_literal: true

require 'ipaddr'

module Facter
  module Resolvers
    class Networking < BaseResolver
      @log = Facter::Log.new(self)
      @semaphore = Mutex.new
      @fact_list ||= {}
      class << self
        private

        def post_resolve(fact_name)
          @fact_list.fetch(fact_name) { read_network_information(fact_name) }
        end

        def read_network_information(fact_name)
          require "#{ROOT_DIR}/lib/resolvers/windows/ffi/networking_ffi"

          size_ptr = FFI::MemoryPointer.new(NetworkingFFI::BUFFER_LENGTH)
          adapter_addresses = FFI::MemoryPointer.new(IpAdapterAddressesLh.size, NetworkingFFI::BUFFER_LENGTH)
          flags = NetworkingFFI::GAA_FLAG_SKIP_ANYCAST |
                  NetworkingFFI::GAA_FLAG_SKIP_MULTICAST | NetworkingFFI::GAA_FLAG_SKIP_DNS_SERVER

          return unless (adapter_addresses = get_adapter_addresses(size_ptr, adapter_addresses, flags))

          iterate_list(adapter_addresses)
          set_interfaces_other_facts if @fact_list[:interfaces]
          @fact_list[:primary_interface] = @fact_list[:primary_interface].to_s
          @fact_list[fact_name]
        end

        def get_adapter_addresses(size_ptr, adapter_addresses, flags)
          error = nil
          3.times do
            error = NetworkingFFI::GetAdaptersAddresses(NetworkingFFI::AF_UNSPEC, flags,
                                                        FFI::Pointer::NULL, adapter_addresses, size_ptr)
            break if error == NetworkingFFI::ERROR_SUCCES

            if error == NetworkingFFI::ERROR_BUFFER_OVERFLOW
              adapter_addresses = FFI::MemoryPointer.new(IpAdapterAddressesLh.size, NetworkingFFI::BUFFER_LENGTH)
            else
              @log.debug 'Unable to retrieve networking facts!'
              return nil
            end
          end
          return nil unless error.zero?

          adapter_addresses
        end

        def adapter_down?(adapter)
          adapter[:OperStatus] != NetworkingFFI::IF_OPER_STATUS_UP ||
            ![NetworkingFFI::IF_TYPE_ETHERNET_CSMACD, NetworkingFFI::IF_TYPE_IEEE80211].include?(adapter[:IfType])
        end

        def retrieve_dhcp_server(adapter)
          if !(adapter[:Flags] & NetworkingFFI::IP_ADAPTER_DHCP_ENABLED).zero? &&
             adapter[:Union][:Struct][:Length] >= IpAdapterAddressesLh.size
            NetworkUtils.address_to_string(adapter[:Dhcpv4Server])
          end
        end

        def iterate_list(adapter_addresses)
          net_interface = {}
          IpAdapterAddressesLh.read_list(adapter_addresses) do |adapter_address|
            if adapter_down?(adapter_address)
              adapter_address = IpAdapterAddressesLh.new(adapter_address[:Next])
              next
            end
            @fact_list[:domain] ||= adapter_address[:DnsSuffix].read_wide_string_without_length
            name = adapter_address[:FriendlyName].read_wide_string_without_length.to_sym
            net_interface[name] = build_interface_info(adapter_address, name)
          end

          @fact_list[:interfaces] = net_interface unless net_interface.empty?
        end

        def build_interface_info(adapter_address, name)
          hash = {}

          hash[:dhcp] = retrieve_dhcp_server(adapter_address)
          hash[:mtu] = adapter_address[:Mtu]

          bindings = find_ip_addresses(adapter_address[:FirstUnicastAddress], name)
          hash[:bindings] = bindings[:ipv4] unless bindings[:ipv4].empty?
          hash[:bindings6] = bindings[:ipv6] unless bindings[:ipv6].empty?
          hash[:mac] = NetworkUtils.find_mac_address(adapter_address)
          hash
        end

        def find_ip_addresses(unicast_addresses, name)
          bindings = {}
          bindings[:ipv6] = []
          bindings[:ipv4] = []

          IpAdapterUnicastAddressLH.read_list(unicast_addresses) do |unicast|
            addr = NetworkUtils.address_to_string(unicast[:Address])
            unless addr
              unicast = IpAdapterUnicastAddressLH.new(unicast[:Next])
              next
            end

            sock_addr = SockAddr.new(unicast[:Address][:lpSockaddr])
            add_ip_data(addr, unicast, sock_addr, bindings)
            find_primary_interface(sock_addr, name, addr)
          end
          bindings
        end

        def add_ip_data(addr, unicast, sock_addr, bindings)
          result = find_bindings(sock_addr, unicast, addr)
          return unless result

          bindings[:ipv6] << result if sock_addr[:sa_family] == NetworkingFFI::AF_INET6
          bindings[:ipv4] << result if sock_addr[:sa_family] == NetworkingFFI::AF_INET
        end

        def find_bindings(sock_addr, unicast, addr)
          return unless [NetworkingFFI::AF_INET, NetworkingFFI::AF_INET6].include?(sock_addr[:sa_family])

          NetworkUtils.build_binding(addr, unicast[:OnLinkPrefixLength])
        end

        def find_primary_interface(sock_addr, name, addr)
          if !@fact_list[:primary_interface] &&
             ([NetworkingFFI::AF_INET, NetworkingFFI::AF_INET6].include?(sock_addr[:sa_family]) &&
                 !NetworkUtils.ignored_ip_address(addr))
            @fact_list[:primary_interface] = name
          end
        end

        def set_interfaces_other_facts
          @fact_list[:interfaces].each do |interface_name, value|
            if value[:bindings]
              binding = find_valid_binding(value[:bindings])
              populate_interface(binding, value)
            end
            if value[:bindings6]
              binding = find_valid_binding(value[:bindings6])
              populate_interface(binding, value, true)
            end
            set_networking_other_facts(value, interface_name)
          end
        end

        def find_valid_binding(bindings)
          bindings.each do |binding|
            return binding unless NetworkUtils.ignored_ip_address(binding[:address])
          end
          bindings.empty? ? nil : bindings.first
        end

        def populate_interface(bind, interface, ipv6 = false)
          return if !bind || bind.empty?

          if ipv6
            interface[:ip6] = bind[:address]
            interface[:netmask6] = bind[:netmask]
            interface[:network6] = bind[:network]
            interface[:scope6] =  NetworkUtils.get_scope(bind[:address])
          else
            interface[:network] = bind[:network]
            interface[:netmask] = bind[:netmask]
            interface[:ip] = bind[:address]
          end
        end

        def set_networking_other_facts(value, interface_name)
          return unless @fact_list[:primary_interface] == interface_name

          %i[mtu dhcp mac ip ip6 scope6 netmask netmask6 network network6].each do |key|
            @fact_list[key] = value[key]
          end
        end
      end
    end
  end
end