blob: 4938bf5454aad41bb6363f04c9f57a7ec05864e6 [file] [log] [blame]
//
// Copyright 2016 The Chromium OS Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
#include <stdlib.h>
#include <base/strings/string_number_conversions.h>
#include <algorithm>
#include <vector>
#include "shill/event_dispatcher.h"
#include "shill/logging.h"
#include "shill/process_manager.h"
#include "shill/throttler.h"
namespace shill {
namespace Logging {
static auto kModuleLogScope = ScopeLogger::kTC;
static std::string ObjectID(Throttler* t) {
return "throttler";
}
}
const char Throttler::kTCPath[] = "/sbin/tc";
const char* const Throttler::kTCCleanUpCmds[] = {
"qdisc del dev ${INTERFACE} root\n",
"qdisc del dev ${INTERFACE} ingress\n"};
// For fq_codel quantum 300 gives a boost to interactive flows
// Only works for bandwidths < 50 Mbps.
const char* const Throttler::kTCThrottleUplinkCmds[] = {
"qdisc add dev ${INTERFACE} root handle 1: htb default 11\n",
"class add dev ${INTERFACE} parent 1: classid 1:1 htb rate ${ULRATE}\n",
"class add dev ${INTERFACE} parent 1:1 classid 1:11 htb rate "
"${ULRATE} prio 0 quantum 300\n"};
const char* const Throttler::kTCThrottleDownlinkCmds[] = {
"qdisc add dev ${INTERFACE} handle ffff: ingress\n",
"filter add dev ${INTERFACE} parent ffff: protocol all "
" prio 50 u32 match ip"
" src 0.0.0.0/0 police rate ${DLRATE} burst ${BURST}k mtu 66000"
" drop flowid :1\n"};
const char kTemplateInterface[] = "${INTERFACE}";
const char kTemplateULRate[] = "${ULRATE}";
const char kTemplateDLRate[] = "${DLRATE}";
const char kTemplateBurst[] = "${BURST}";
const char Throttler::kTCUser[] = "nobody";
const char Throttler::kTCGroup[] = "nobody";
Throttler::Throttler(EventDispatcher* dispatcher, Manager* manager)
: file_io_(FileIO::GetInstance()),
tc_pid_(0),
dispatcher_(dispatcher),
manager_(manager),
process_manager_(ProcessManager::GetInstance()) {
SLOG(this, 2) << __func__;
}
Throttler::~Throttler() {
SLOG(this, 2) << __func__;
}
void Throttler::ClearTCState() {
tc_pid_ = 0;
tc_commands_.clear();
tc_current_interface_.clear();
tc_interfaces_to_throttle_.clear();
callback_.Reset();
}
bool Throttler::DisableThrottlingOnAllInterfaces(
const ResultCallback& callback) {
bool result = false;
std::vector<std::string> interfaces = manager_->GetDeviceInterfaceNames();
std::vector<std::string> commands;
for (const auto& interface_name : interfaces) {
for (std::string command : kTCCleanUpCmds) {
base::ReplaceSubstringsAfterOffset(&command, 0, kTemplateInterface,
interface_name);
commands.push_back(command);
}
}
if (commands.empty()) {
Done(callback, Error::kSuccess, "");
ClearThrottleStatus();
return true;
}
callback_ = callback;
result = StartTCForCommands(commands);
if (result) {
ClearThrottleStatus();
}
return result;
}
void Throttler::Done(const ResultCallback& callback,
Error::Type error_type,
const std::string& message) {
Error error;
Error::PopulateAndLog(FROM_HERE, &error, error_type, message);
if (!callback.is_null()) {
callback.Run(error);
SLOG(this, 4) << "ran callback";
} else {
SLOG(this, 4) << "null callback";
}
ClearTCState();
return;
}
bool Throttler::ThrottleInterfaces(const ResultCallback& callback,
uint32_t upload_rate_kbits,
uint32_t download_rate_kbits) {
// At least one of upload/download should be throttled.
// 0 value indicates no throttling.
if ((upload_rate_kbits == 0) && (download_rate_kbits == 0)) {
Done(callback, Error::kInvalidArguments,
"One of download/upload rates should be set");
return false;
}
tc_interfaces_to_throttle_ = manager_->GetDeviceInterfaceNames();
std::string interface_name = GetNextInterface();
if (interface_name.empty()) {
Done(callback, Error::kOperationFailed,
"No interfaces available for throttling");
return false;
}
// Set state here, OnProcessExited will clear in case of failure
desired_throttling_enabled_ = true;
desired_upload_rate_kbits_ = upload_rate_kbits;
desired_download_rate_kbits_ = download_rate_kbits;
return Throttle(callback, interface_name, upload_rate_kbits,
download_rate_kbits);
}
bool Throttler::Throttle(const ResultCallback& callback,
const std::string& interface_name,
uint32_t upload_rate_kbits,
uint32_t download_rate_kbits) {
SLOG(this, 4) << __func__ << " : " << interface_name << "("
<< upload_rate_kbits << ", " << download_rate_kbits << ")";
if (tc_pid_ || !tc_commands_.empty() || !tc_current_interface_.empty()) {
Done(callback, Error::kWrongState, "Cannot run concurrent TC operations");
return false;
}
std::string throttle_file;
std::vector<std::string> commands;
// Easier to clean up first and start afresh than issue tc changes.
for (std::string command : kTCCleanUpCmds) {
base::ReplaceSubstringsAfterOffset(&command, 0, kTemplateInterface,
interface_name);
commands.push_back(command);
}
// Add commands for upload(egress) queueing disciplines
// and filters
if (upload_rate_kbits) {
for (std::string command : kTCThrottleUplinkCmds) {
std::string ulrate(base::UintToString(upload_rate_kbits) + "kbit");
base::ReplaceSubstringsAfterOffset(&command, 0, kTemplateInterface,
interface_name);
base::ReplaceSubstringsAfterOffset(&command, 0, kTemplateULRate, ulrate);
commands.push_back(command);
}
}
// Add commands for download(ingress) queueing disciplines
// and filters
if (download_rate_kbits) {
for (std::string command : kTCThrottleDownlinkCmds) {
std::string dlrate(base::UintToString(download_rate_kbits) + "kbit");
std::string to_burst(base::UintToString(download_rate_kbits * 2));
base::ReplaceSubstringsAfterOffset(&command, 0, kTemplateInterface,
interface_name);
base::ReplaceSubstringsAfterOffset(&command, 0, kTemplateDLRate, dlrate);
base::ReplaceSubstringsAfterOffset(&command, 0, kTemplateBurst, to_burst);
commands.push_back(command);
}
}
callback_ = callback;
tc_current_interface_ = interface_name;
return StartTCForCommands(commands);
}
bool Throttler::ApplyThrottleToNewInterface(const std::string& interface_name) {
if (!desired_throttling_enabled_) {
// Nothing to do if no throttling is desired
return false;
}
// An operation is currently in progress, append to list of interfaces
if (tc_pid_ != 0) {
tc_interfaces_to_throttle_.push_back(interface_name);
return true;
}
// No operation currently in progress, start a new tc process
ResultCallback dummy;
return Throttle(dummy, interface_name, desired_upload_rate_kbits_,
desired_download_rate_kbits_);
}
bool Throttler::StartTCForCommands(const std::vector<std::string>& commands) {
CHECK_EQ(tc_pid_, 0);
CHECK(!commands.empty());
std::vector<std::string> args = {
"-f", // Continue if there is a failure or no-op
"-b", // Batch mode
"-" // Use stdin for input
};
uint64_t capmask = CAP_TO_MASK(CAP_NET_ADMIN);
tc_commands_ = commands;
tc_pid_ = process_manager_->StartProcessInMinijailWithPipes(
FROM_HERE, base::FilePath(kTCPath), args, kTCUser, kTCGroup, capmask,
base::Bind(&Throttler::OnProcessExited, AsWeakPtr()), &tc_stdin_, nullptr,
// shill's stderr is wired to syslog, so nullptr
// here implies throttling errors show up in /var/log/net.log.
nullptr);
SLOG(this, 1) << "Spawned tc with pid: " << tc_pid_;
if (file_io_->SetFdNonBlocking(tc_stdin_)) {
Done(callback_, Error::kOperationFailed,
"Unable to set TC pipes to be non-blocking");
return false;
}
tc_stdin_handler_.reset(dispatcher_->CreateReadyHandler(
tc_stdin_, IOHandler::kModeOutput,
Bind(&Throttler::WriteTCCommands, AsWeakPtr())));
return true;
}
void Throttler::WriteTCCommands(int fd) {
CHECK_EQ(fd, tc_stdin_);
CHECK(tc_pid_);
for (const auto& command : tc_commands_) {
SLOG(this, 2) << "Issuing tc command: " << command;
ssize_t bytes_written =
file_io_->Write(tc_stdin_, command.data(), command.size());
if (bytes_written != static_cast<ssize_t>(command.size())) {
LOG(ERROR) << "Bytes written: " << bytes_written
<< "v/s Command size: " << command.size();
}
}
tc_stdin_handler_.reset();
file_io_->Close(tc_stdin_);
tc_stdin_ = -1;
return;
}
void Throttler::ClearThrottleStatus() {
desired_throttling_enabled_ = false;
desired_upload_rate_kbits_ = 0;
desired_download_rate_kbits_ = 0;
}
std::string Throttler::GetNextInterface() {
std::string interface_name;
if (!tc_interfaces_to_throttle_.empty()) {
interface_name = tc_interfaces_to_throttle_.back();
tc_interfaces_to_throttle_.pop_back();
}
return interface_name;
}
void Throttler::OnProcessExited(int exit_status) {
CHECK(tc_pid_);
CHECK(!tc_commands_.empty());
// Should keep track of interface names if throttling, but not if disabling
CHECK(!desired_throttling_enabled_ || !tc_current_interface_.empty());
Error::Type error_type =
(exit_status == EXIT_SUCCESS) ? Error::kSuccess : Error::kOperationFailed;
std::string message =
((desired_throttling_enabled_) ? "throttling " : "disabling throttle ") +
((exit_status == EXIT_SUCCESS)
? std::string("succeeded")
: (std::string("failed: ") + base::IntToString(exit_status)));
Error error;
Error::PopulateAndLog(FROM_HERE, &error, error_type, message);
std::string next_interface = GetNextInterface();
if (next_interface.empty()) {
Done(callback_, Error::kSuccess, "");
} else {
SLOG(this, 2) << "Done with " << tc_current_interface_ << " now calling "
<< next_interface;
tc_pid_ = 0;
tc_commands_.clear();
tc_current_interface_.clear();
Throttle(callback_, next_interface, desired_upload_rate_kbits_,
desired_download_rate_kbits_);
}
}
} // namespace shill