chromium/chromeos/ash/services/network_health/network_health_service.cc

// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromeos/ash/services/network_health/network_health_service.h"

#include <cstdint>
#include <cstdlib>
#include <map>
#include <utility>
#include <vector>

#include "base/time/time.h"
#include "chromeos/ash/components/mojo_service_manager/connection.h"
#include "chromeos/ash/components/network/network_event_log.h"
#include "chromeos/ash/services/network_config/in_process_instance.h"
#include "chromeos/ash/services/network_health/network_health_constants.h"
#include "chromeos/services/network_config/public/cpp/cros_network_config_util.h"
#include "chromeos/services/network_config/public/mojom/cros_network_config.mojom.h"
#include "chromeos/services/network_health/public/mojom/network_health_types.mojom.h"
#include "third_party/cros_system_api/mojo/service_constants.h"

namespace ash::network_health {

namespace {

namespace mojom = ::chromeos::network_health::mojom;
namespace network_config = ::chromeos::network_config;

constexpr mojom::NetworkState DeviceStateToNetworkState(
    network_config::mojom::DeviceStateType device_state) {
  switch (device_state) {
    case network_config::mojom::DeviceStateType::kUninitialized:
      return mojom::NetworkState::kUninitialized;
    case network_config::mojom::DeviceStateType::kDisabled:
    case network_config::mojom::DeviceStateType::kDisabling:
    case network_config::mojom::DeviceStateType::kEnabling:
      // Disabling and Enabling are intermediate state that we care about in the
      // UI, but not for purposes of network health, we can treat as Disabled.
      return mojom::NetworkState::kDisabled;
    case network_config::mojom::DeviceStateType::kEnabled:
      return mojom::NetworkState::kNotConnected;
    case network_config::mojom::DeviceStateType::kProhibited:
      return mojom::NetworkState::kProhibited;
    case network_config::mojom::DeviceStateType::kUnavailable:
      NOTREACHED_IN_MIGRATION();
      return mojom::NetworkState::kUninitialized;
  }
}

constexpr mojom::NetworkState ConnectionStateToNetworkState(
    network_config::mojom::ConnectionStateType connection_state) {
  switch (connection_state) {
    case network_config::mojom::ConnectionStateType::kOnline:
      return mojom::NetworkState::kOnline;
    case network_config::mojom::ConnectionStateType::kConnected:
      return mojom::NetworkState::kConnected;
    case network_config::mojom::ConnectionStateType::kPortal:
      return mojom::NetworkState::kPortal;
    case network_config::mojom::ConnectionStateType::kConnecting:
      return mojom::NetworkState::kConnecting;
    case network_config::mojom::ConnectionStateType::kNotConnected:
      return mojom::NetworkState::kNotConnected;
  }
}

// Populates a mojom::NetworkPtr based on the given |device_prop| and
// |network_prop|. This function assumes that |device_prop| is populated, while
// |network_prop| could be null.
mojom::NetworkPtr CreateNetwork(
    const network_config::mojom::DeviceStatePropertiesPtr& device_prop,
    const network_config::mojom::NetworkStatePropertiesPtr& net_prop) {
  auto net = mojom::Network::New();
  net->mac_address = device_prop->mac_address;
  net->type = device_prop->type;
  if (device_prop->ipv6_address)
    net->ipv6_addresses.push_back(device_prop->ipv6_address->ToString());
  if (device_prop->ipv4_address)
    net->ipv4_address = device_prop->ipv4_address->ToString();

  if (net_prop) {
    net->state = ConnectionStateToNetworkState(net_prop->connection_state);
    net->name = net_prop->name;
    net->guid = net_prop->guid;
    net->portal_state = net_prop->portal_state;
    net->portal_probe_url = net_prop->portal_probe_url;
    if (network_config::NetworkTypeMatchesType(
            net_prop->type, network_config::mojom::NetworkType::kWireless)) {
      net->signal_strength = mojom::UInt32Value::New(
          network_config::GetWirelessSignalStrength(net_prop.get()));
    }
  } else {
    net->state = DeviceStateToNetworkState(device_prop->device_state);
  }

  return net;
}

// Compares everything except strength properties which are observed only on
// a per-network basis.
bool NetworksMatch(const mojom::NetworkPtr& a, const mojom::NetworkPtr& b) {
  return a->state == b->state && a->guid == b->guid && a->name == b->name &&
         a->mac_address == b->mac_address &&
         a->ipv4_address == b->ipv4_address &&
         a->ipv6_addresses == b->ipv6_addresses &&
         a->portal_state == b->portal_state &&
         a->portal_probe_url == b->portal_probe_url;
}

bool NetworkListsMatch(const std::vector<mojom::NetworkPtr>& networks_a,
                       const std::vector<mojom::NetworkPtr>& networks_b) {
  if (networks_a.size() != networks_b.size())
    return false;
  for (std::size_t i = 0u; i < networks_a.size(); ++i) {
    if (!NetworksMatch(networks_a[i], networks_b[i]))
      return false;
  }
  return true;
}

}  // namespace

NetworkHealthService::NetworkHealthService() {
  ash::network_config::BindToInProcessInstance(
      remote_cros_network_config_.BindNewPipeAndPassReceiver());
  remote_cros_network_config_->AddObserver(
      cros_network_config_observer_receiver_.BindNewPipeAndPassRemote());
  RefreshNetworkHealthState();
  SetTimer(std::make_unique<base::RepeatingTimer>());
  tracked_guids_timer_.Start(FROM_HERE, kUpdateTrackedGuidsInterval, this,
                             &NetworkHealthService::UpdateTrackedGuids);
  if (mojo_service_manager::IsServiceManagerBound()) {
    mojo_service_manager::GetServiceManagerProxy()->Register(
        chromeos::mojo_services::kChromiumNetworkHealth,
        provider_receiver_.BindNewPipeAndPassRemote());
  }
}

void NetworkHealthService::SetTimer(
    std::unique_ptr<base::RepeatingTimer> timer) {
  timer_ = std::move(timer);
  timer_->Start(FROM_HERE, kSignalStrengthSampleRate, this,
                &NetworkHealthService::AnalyzeSignalStrength);
}

NetworkHealthService::~NetworkHealthService() = default;

void NetworkHealthService::BindReceiver(
    mojo::PendingReceiver<mojom::NetworkHealthService> receiver) {
  receivers_.Add(this, std::move(receiver));
}

void NetworkHealthService::Request(
    chromeos::mojo_service_manager::mojom::ProcessIdentityPtr identity,
    mojo::ScopedMessagePipeHandle receiver) {
  BindReceiver(
      mojo::PendingReceiver<mojom::NetworkHealthService>(std::move(receiver)));
}

const mojom::NetworkHealthState& NetworkHealthService::GetNetworkHealthState() {
  NET_LOG(EVENT) << "Network Health State Requested";
  return network_health_state_;
}

const std::map<std::string, base::Time>&
NetworkHealthService::GetTrackedGuidsForTest() {
  return guid_to_active_time_;
}

void NetworkHealthService::AddObserver(
    mojo::PendingRemote<mojom::NetworkEventsObserver> observer) {
  observers_.Add(std::move(observer));
}

void NetworkHealthService::GetNetworkList(GetNetworkListCallback callback) {
  std::move(callback).Run(mojo::Clone(network_health_state_.networks));
}

void NetworkHealthService::GetHealthSnapshot(
    GetHealthSnapshotCallback callback) {
  std::move(callback).Run(network_health_state_.Clone());
}

void NetworkHealthService::GetRecentlyActiveNetworks(
    GetRecentlyActiveNetworksCallback callback) {
  std::vector<std::string> networks;
  for (auto const& [guid, timestamp] : guid_to_active_time_) {
    networks.push_back(guid);
  }
  std::move(callback).Run(std::move(networks));
}

void NetworkHealthService::OnNetworkStateListChanged() {
  RequestNetworkStateList();
}

void NetworkHealthService::OnDeviceStateListChanged() {
  RequestDeviceStateList();
}

void NetworkHealthService::OnActiveNetworksChanged(
    std::vector<network_config::mojom::NetworkStatePropertiesPtr>
        active_networks) {
  HandleNetworkEventsForActiveNetworks(std::move(active_networks));
  RequestNetworkStateList();
}

void NetworkHealthService::OnNetworkStateChanged(
    network_config::mojom::NetworkStatePropertiesPtr network_state) {
  if (!network_state) {
    return;
  }
  HandleNetworkEventsForInactiveNetworks(std::move(network_state));
  RequestNetworkStateList();
}

void NetworkHealthService::OnNetworkStateListReceived(
    std::vector<network_config::mojom::NetworkStatePropertiesPtr> props) {
  network_properties_.swap(props);
  CreateNetworkHealthState();
}

void NetworkHealthService::OnDeviceStateListReceived(
    std::vector<network_config::mojom::DeviceStatePropertiesPtr> props) {
  device_properties_.swap(props);
  CreateNetworkHealthState();
}

void NetworkHealthService::CreateNetworkHealthState() {
  // If the device information has not been collected, the NetworkHealthState
  // cannot be created.
  if (device_properties_.empty())
    return;

  std::vector<mojom::NetworkPtr> prev_networks =
      mojo::Clone(network_health_state_.networks);

  network_health_state_.networks.clear();

  std::map<network_config::mojom::NetworkType,
           network_config::mojom::DeviceStatePropertiesPtr>
      device_type_map;

  // This function only supports one Network structure per underlying device. If
  // this assumption changes, this function will need to be reworked.
  for (const auto& d : device_properties_) {
    device_type_map[d->type] = mojo::Clone(d);
  }

  // For each NetworkStateProperties, create a Network structure using the
  // underlying DeviceStateProperties. Remove devices from the type map that
  // have an associated NetworkStateProperties.
  for (const auto& net_prop : network_properties_) {
    auto device_iter = device_type_map.find(net_prop->type);
    if (device_iter == device_type_map.end()) {
      continue;
    }
    network_health_state_.networks.push_back(
        CreateNetwork(device_iter->second, net_prop));
    device_type_map.erase(device_iter);
  }

  // For the remaining devices that do not have associated
  // NetworkStateProperties, create Network structures.
  for (const auto& device_prop : device_type_map) {
    // Devices that have an kUnavailable state are not valid.
    if (device_prop.second->device_state ==
        network_config::mojom::DeviceStateType::kUnavailable) {
      NET_LOG(ERROR) << "Device in unexpected unavailable state: "
                     << device_prop.second->type;
      continue;
    }

    network_health_state_.networks.push_back(
        CreateNetwork(device_prop.second, nullptr));
  }

  UpdateTrackedGuids();

  if (!NetworkListsMatch(prev_networks, network_health_state_.networks))
    NotifyObserversNetworkListChanged();
}

void NetworkHealthService::RefreshNetworkHealthState() {
  RequestNetworkStateList();
  RequestDeviceStateList();
}

void NetworkHealthService::RequestNetworkStateList() {
  remote_cros_network_config_->GetNetworkStateList(
      network_config::mojom::NetworkFilter::New(
          network_config::mojom::FilterType::kActive,
          network_config::mojom::NetworkType::kAll,
          network_config::mojom::kNoLimit),
      base::BindOnce(&NetworkHealthService::OnNetworkStateListReceived,
                     base::Unretained(this)));
}

void NetworkHealthService::RequestDeviceStateList() {
  remote_cros_network_config_->GetDeviceStateList(
      base::BindOnce(&NetworkHealthService::OnDeviceStateListReceived,
                     base::Unretained(this)));
}

const mojom::NetworkPtr* NetworkHealthService::FindMatchingNetwork(
    const std::string& guid) const {
  for (const mojom::NetworkPtr& network : network_health_state_.networks) {
    if (!network->guid) {
      continue;
    }
    if (network->guid.value() == guid) {
      return &network;
    }
  }
  return nullptr;
}

void NetworkHealthService::HandleNetworkEventsForActiveNetworks(
    std::vector<network_config::mojom::NetworkStatePropertiesPtr>
        active_networks) {
  for (const auto& network_state : active_networks) {
    const mojom::NetworkPtr* const network =
        FindMatchingNetwork(network_state->guid);
    // Fire an event if the network is new or a connection state change
    // occurred.
    if (!network || ConnectionStateChanged(*network, network_state)) {
      NotifyObserversConnectionStateChanged(
          network_state->guid,
          ConnectionStateToNetworkState(network_state->connection_state));
    }
    // Fire an event if the network is new or a signal strength change occurred.
    if (!network || SignalStrengthChanged(*network, network_state)) {
      NotifyObserversSignalStrengthChanged(
          network_state->guid,
          network_config::GetWirelessSignalStrength(network_state.get()));
    }
  }
}

void NetworkHealthService::HandleNetworkEventsForInactiveNetworks(
    network_config::mojom::NetworkStatePropertiesPtr network_state) {
  // Ensure that the connection state is no longer active.
  if (ConnectionStateToNetworkState(network_state->connection_state) !=
      mojom::NetworkState::kNotConnected) {
    return;
  }
  const mojom::NetworkPtr* const network =
      FindMatchingNetwork(network_state->guid);
  if (!network) {
    return;
  }
  // Fire an event if the network was previously active.
  if (ConnectionStateChanged(*network, network_state)) {
    NotifyObserversConnectionStateChanged(
        network_state->guid,
        ConnectionStateToNetworkState(network_state->connection_state));
  }
}

void NetworkHealthService::NotifyObserversConnectionStateChanged(
    const std::string& guid,
    mojom::NetworkState state) {
  for (auto& observer : observers_) {
    observer->OnConnectionStateChanged(guid, state);
  }
}

void NetworkHealthService::NotifyObserversSignalStrengthChanged(
    const std::string& guid,
    int signal_strength) {
  for (auto& observer : observers_) {
    observer->OnSignalStrengthChanged(guid,
                                      mojom::UInt32Value::New(signal_strength));
  }
}

void NetworkHealthService::NotifyObserversNetworkListChanged() {
  for (auto& observer : observers_) {
    observer->OnNetworkListChanged(mojo::Clone(network_health_state_.networks));
  }
}

bool NetworkHealthService::ConnectionStateChanged(
    const mojom::NetworkPtr& network,
    const network_config::mojom::NetworkStatePropertiesPtr& network_state) {
  auto state = ConnectionStateToNetworkState(network_state->connection_state);
  if (state == network->state) {
    return false;
  }
  return true;
}

bool NetworkHealthService::SignalStrengthChanged(
    const mojom::NetworkPtr& network,
    const network_config::mojom::NetworkStatePropertiesPtr& network_state) {
  if (!network_config::NetworkStateMatchesType(
          network_state.get(), network_config::mojom::NetworkType::kWireless)) {
    return false;
  }
  DCHECK(network->signal_strength);

  auto current = network_config::GetWirelessSignalStrength(network_state.get());
  uint32_t previous = network->signal_strength->value;
  if (std::abs(1.0 * (current - (int)previous)) <=
      kMaxSignalStrengthFluctuationTolerance) {
    return false;
  }
  return true;
}

void NetworkHealthService::AnalyzeSignalStrength() {
  std::set<std::string> analyzed_networks;
  for (auto& network : network_health_state_.networks) {
    if (!network->guid.has_value() || network->signal_strength.is_null())
      continue;

    auto guid = network->guid.value();
    auto& tracker = signal_strength_trackers_[guid];
    tracker.AddSample(static_cast<uint8_t>(network->signal_strength->value));

    auto stats = mojom::SignalStrengthStats::New();
    stats->average = tracker.Average();
    stats->deviation = tracker.StdDev();
    stats->samples = tracker.Samples();
    network->signal_strength_stats = std::move(stats);
    analyzed_networks.insert(guid);
  }

  // Remove all entries that are not actively being analyzed.
  for (auto it = signal_strength_trackers_.begin();
       it != signal_strength_trackers_.end();) {
    if (!analyzed_networks.count(it->first)) {
      it = signal_strength_trackers_.erase(it);
    } else {
      ++it;
    }
  }
}

bool NetworkHealthService::IsActive(const mojom::NetworkPtr& network) {
  return network->state == mojom::NetworkState::kConnecting ||
         network->state == mojom::NetworkState::kPortal ||
         network->state == mojom::NetworkState::kConnected ||
         network->state == mojom::NetworkState::kOnline;
}

void NetworkHealthService::UpdateTrackedGuids() {
  for (auto& network : network_health_state_.networks) {
    if (network->guid.has_value() && IsActive(network)) {
      guid_to_active_time_[network->guid.value()] = base::Time::Now();
    }
  }
  for (auto it = guid_to_active_time_.begin();
       it != guid_to_active_time_.end();) {
    if (base::Time::Now() - it->second > kUpdateTrackedGuidsInterval) {
      it = guid_to_active_time_.erase(it);
    } else {
      it++;
    }
  }
}

}  // namespace ash::network_health