blob: 663b4adcad5cbfddb986e47c6a48020c70e53048 [file] [log] [blame]
// Copyright 2018 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "shill/icmp_session.h"
#include <arpa/inet.h>
#include <netinet/icmp6.h>
#include <netinet/ip.h>
#include <base/time/default_tick_clock.h>
#include "shill/event_dispatcher.h"
#include "shill/logging.h"
#include "shill/net/byte_string.h"
#include "shill/net/io_handler_factory.h"
#include "shill/net/ip_address.h"
#include "shill/net/sockets.h"
namespace {
const int kIPHeaderLengthUnitBytes = 4;
}
namespace shill {
namespace Logging {
static auto kModuleLogScope = ScopeLogger::kWiFi;
static std::string ObjectID(const IcmpSession* i) {
return "(icmp_session)";
}
} // namespace Logging
uint16_t IcmpSession::kNextUniqueEchoId = 0;
const int IcmpSession::kTotalNumEchoRequests = 3;
const int IcmpSession::kEchoRequestIntervalSeconds = 1; // default for ping
// We should not need more than 1 second after the last request is sent to
// receive the final reply.
const size_t IcmpSession::kTimeoutSeconds =
kEchoRequestIntervalSeconds * kTotalNumEchoRequests + 1;
IcmpSession::IcmpSession(EventDispatcher* dispatcher)
: weak_ptr_factory_(this),
dispatcher_(dispatcher),
io_handler_factory_(IOHandlerFactory::GetInstance()),
icmp_(new Icmp()),
echo_id_(kNextUniqueEchoId),
current_sequence_number_(0),
tick_clock_(&default_tick_clock_) {
// Each IcmpSession will have a unique echo ID to identify requests and reply
// messages.
++kNextUniqueEchoId;
}
IcmpSession::~IcmpSession() {
Stop();
}
bool IcmpSession::Start(const IPAddress& destination,
int interface_index,
const IcmpSessionResultCallback& result_callback) {
if (!dispatcher_) {
LOG(ERROR) << "Invalid dispatcher";
return false;
}
if (IsStarted()) {
LOG(WARNING) << "ICMP session already started";
return false;
}
if (!icmp_->Start(destination, interface_index)) {
return false;
}
echo_reply_handler_.reset(io_handler_factory_->CreateIOInputHandler(
icmp_->socket(),
Bind(&IcmpSession::OnEchoReplyReceived, weak_ptr_factory_.GetWeakPtr()),
Bind(&IcmpSession::OnEchoReplyError, weak_ptr_factory_.GetWeakPtr())));
result_callback_ = result_callback;
timeout_callback_.Reset(Bind(&IcmpSession::ReportResultAndStopSession,
weak_ptr_factory_.GetWeakPtr()));
dispatcher_->PostDelayedTask(FROM_HERE, timeout_callback_.callback(),
kTimeoutSeconds * 1000);
seq_num_to_sent_recv_time_.clear();
received_echo_reply_seq_numbers_.clear();
dispatcher_->PostTask(FROM_HERE, Bind(&IcmpSession::TransmitEchoRequestTask,
weak_ptr_factory_.GetWeakPtr()));
return true;
}
void IcmpSession::Stop() {
if (!IsStarted()) {
return;
}
timeout_callback_.Cancel();
echo_reply_handler_.reset();
icmp_->Stop();
}
bool IcmpSession::IsStarted() const {
return icmp_->IsStarted();
}
// static
bool IcmpSession::AnyRepliesReceived(const IcmpSessionResult& result) {
for (const base::TimeDelta& latency : result) {
if (!latency.is_zero()) {
return true;
}
}
return false;
}
// static
bool IcmpSession::IsPacketLossPercentageGreaterThan(
const IcmpSessionResult& result, int percentage_threshold) {
if (percentage_threshold < 0) {
LOG(ERROR) << __func__ << ": negative percentage threshold ("
<< percentage_threshold << ")";
return false;
}
if (result.empty()) {
return false;
}
int lost_packet_count = 0;
for (const base::TimeDelta& latency : result) {
if (latency.is_zero()) {
++lost_packet_count;
}
}
int packet_loss_percentage = (lost_packet_count * 100) / result.size();
return packet_loss_percentage > percentage_threshold;
}
void IcmpSession::TransmitEchoRequestTask() {
if (!IsStarted()) {
// This might happen when ping times out or is stopped between two calls
// to IcmpSession::TransmitEchoRequestTask.
return;
}
if (icmp_->TransmitEchoRequest(echo_id_, current_sequence_number_)) {
seq_num_to_sent_recv_time_[current_sequence_number_] =
std::make_pair(tick_clock_->NowTicks(), base::TimeTicks());
}
++current_sequence_number_;
// If we fail to transmit the echo request, fall through instead of returning,
// so we continue sending echo requests until |kTotalNumEchoRequests| echo
// requests are sent.
if (seq_num_to_sent_recv_time_.size() != kTotalNumEchoRequests) {
dispatcher_->PostDelayedTask(FROM_HERE,
Bind(&IcmpSession::TransmitEchoRequestTask,
weak_ptr_factory_.GetWeakPtr()),
kEchoRequestIntervalSeconds * 1000);
}
}
int IcmpSession::OnV4EchoReplyReceived(InputData* data) {
DCHECK_GE(data->len, 0);
ByteString message(data->buf, data->len);
if (message.GetLength() < sizeof(struct iphdr) + sizeof(struct icmphdr)) {
LOG(WARNING) << "Received ICMP packet is too short to contain ICMP header";
return -1;
}
const struct iphdr* received_ip_header =
reinterpret_cast<const struct iphdr*>(message.GetConstData());
const struct icmphdr* received_icmp_header =
reinterpret_cast<const struct icmphdr*>(message.GetConstData() +
received_ip_header->ihl *
kIPHeaderLengthUnitBytes);
// We might have received other types of ICMP traffic, so ensure that the
// message is an echo reply before handling it.
if (received_icmp_header->type != ICMP_ECHOREPLY) {
return -1;
}
// Make sure the message is valid and matches a pending echo request.
if (received_icmp_header->code != Icmp::kIcmpEchoCode) {
LOG(WARNING) << "ICMP header code is invalid";
return -1;
}
if (received_icmp_header->un.echo.id != echo_id_) {
SLOG(this, 3) << "received message echo id ("
<< received_icmp_header->un.echo.id
<< ") does not match this ICMP session's echo id ("
<< echo_id_ << ")";
return -1;
}
return received_icmp_header->un.echo.sequence;
}
int IcmpSession::OnV6EchoReplyReceived(InputData* data) {
ByteString message(data->buf, data->len);
if (message.GetLength() < sizeof(struct icmp6_hdr)) {
LOG(WARNING)
<< "Received ICMP packet is too short to contain ICMPv6 header";
return -1;
}
// Per RFC3542 section 3, ICMPv6 raw sockets do not contain the IP header
// (unlike ICMPv4 raw sockets).
const struct icmp6_hdr* received_icmp_header =
reinterpret_cast<const struct icmp6_hdr*>(message.GetConstData());
// We might have received other types of ICMP traffic, so ensure that the
// message is an echo reply before handling it.
if (received_icmp_header->icmp6_type != ICMP6_ECHO_REPLY) {
return -1;
}
// Make sure the message is valid and matches a pending echo request.
if (received_icmp_header->icmp6_code != Icmp::kIcmpEchoCode) {
LOG(WARNING) << "ICMPv6 header code is invalid";
return -1;
}
if (received_icmp_header->icmp6_id != echo_id_) {
SLOG(this, 3) << "received message echo id ("
<< received_icmp_header->icmp6_id
<< ") does not match this ICMPv6 session's echo id ("
<< echo_id_ << ")";
return -1;
}
return received_icmp_header->icmp6_seq;
}
void IcmpSession::OnEchoReplyReceived(InputData* data) {
DCHECK_GE(data->len, 0);
int received_seq_num = -1;
if (icmp_->destination().family() == IPAddress::kFamilyIPv4) {
received_seq_num = OnV4EchoReplyReceived(data);
} else if (icmp_->destination().family() == IPAddress::kFamilyIPv6) {
received_seq_num = OnV6EchoReplyReceived(data);
} else {
NOTREACHED();
}
if (received_seq_num < 0) {
// Could not parse reply.
return;
}
if (received_echo_reply_seq_numbers_.find(received_seq_num) !=
received_echo_reply_seq_numbers_.end()) {
// Echo reply for this message already handled previously.
return;
}
const auto& seq_num_to_sent_recv_time_pair =
seq_num_to_sent_recv_time_.find(received_seq_num);
if (seq_num_to_sent_recv_time_pair == seq_num_to_sent_recv_time_.end()) {
// Echo reply not meant for any sent echo requests.
return;
}
// Record the time that the echo reply was received.
seq_num_to_sent_recv_time_pair->second.second = tick_clock_->NowTicks();
received_echo_reply_seq_numbers_.insert(received_seq_num);
if (received_echo_reply_seq_numbers_.size() == kTotalNumEchoRequests) {
// All requests sent and replies received, so report results and end the
// ICMP session.
ReportResultAndStopSession();
}
}
std::vector<base::TimeDelta> IcmpSession::GenerateIcmpResult() {
std::vector<base::TimeDelta> latencies;
for (const auto& seq_num_to_sent_recv_time_pair :
seq_num_to_sent_recv_time_) {
const SentRecvTimePair& sent_recv_timestamp_pair =
seq_num_to_sent_recv_time_pair.second;
if (sent_recv_timestamp_pair.second.is_null()) {
// Invalid latency if an echo response has not been received.
latencies.push_back(base::TimeDelta());
} else {
latencies.push_back(sent_recv_timestamp_pair.second -
sent_recv_timestamp_pair.first);
}
}
return latencies;
}
void IcmpSession::OnEchoReplyError(const std::string& error_msg) {
LOG(ERROR) << __func__ << ": " << error_msg;
// Do nothing when we encounter an IO error, so we can continue receiving
// other pending echo replies.
}
void IcmpSession::ReportResultAndStopSession() {
if (!IsStarted()) {
LOG(WARNING) << "ICMP session not started";
return;
}
Stop();
// Invoke result callback after calling IcmpSession::Stop, since the callback
// might delete this object. (Any subsequent call to IcmpSession::Stop leads
// to a segfault since this function belongs to the deleted object.)
if (!result_callback_.is_null()) {
result_callback_.Run(GenerateIcmpResult());
}
}
} // namespace shill