BandwidthController.cpp revision 0dad7c2f1f6994fbe5e85b9e1fc72d29d6453211
1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17#include <stdlib.h>
18#include <errno.h>
19#include <fcntl.h>
20#include <string.h>
21
22#include <sys/socket.h>
23#include <sys/stat.h>
24#include <sys/types.h>
25#include <sys/wait.h>
26
27#include <linux/netlink.h>
28#include <linux/rtnetlink.h>
29#include <linux/pkt_sched.h>
30
31#define LOG_TAG "BandwidthController"
32#include <cutils/log.h>
33#include <cutils/properties.h>
34
35extern "C" int logwrap(int argc, const char **argv, int background);
36
37#include "BandwidthController.h"
38
39const int BandwidthController::MAX_CMD_LEN = 1024;
40const int BandwidthController::MAX_IFACENAME_LEN = 64;
41const int BandwidthController::MAX_CMD_ARGS = 32;
42const char BandwidthController::IPTABLES_PATH[] = "/system/bin/iptables";
43const char BandwidthController::IP6TABLES_PATH[] = "/system/bin/ip6tables";
44
45/**
46 * Some comments about the rules:
47 *  * Ordering
48 *    - when an interface is marked as costly it should be INSERTED into the INPUT/OUTPUT chains.
49 *      E.g. "-I INPUT -i rmnet0 --goto costly"
50 *    - quota'd rules in the costly chain should be before penalty_box lookups.
51 *
52 * * global quota vs per interface quota
53 *   - global quota for all costly interfaces uses a single costly chain:
54 *    . initial rules
55 *      iptables -N costly
56 *      iptables -I INPUT -i iface0 --goto costly
57 *      iptables -I OUTPUT -o iface0 --goto costly
58 *      iptables -I costly -m quota \! --quota 500000 --jump REJECT --reject-with icmp-net-prohibited
59 *      iptables -A costly                            --jump penalty_box
60 *      iptables -A costly -m owner --socket-exists
61 *    . adding a new iface to this, E.g.:
62 *      iptables -I INPUT -i iface1 --goto costly
63 *      iptables -I OUTPUT -o iface1 --goto costly
64 *
65 *   - quota per interface. This is achieve by having "costly" chains per quota.
66 *     E.g. adding a new costly interface iface0 with its own quota:
67 *      iptables -N costly_iface0
68 *      iptables -I INPUT -i iface0 --goto costly_iface0
69 *      iptables -I OUTPUT -o iface0 --goto costly_iface0
70 *      iptables -A costly_iface0 -m quota \! --quota 500000 --jump REJECT --reject-with icmp-net-prohibited
71 *      iptables -A costly_iface0                            --jump penalty_box
72 *      iptables -A costly_iface0 -m owner --socket-exists
73 *
74 * * penalty_box handling:
75 *  - only one penalty_box for all interfaces
76 *   E.g  Adding an app:
77 *    iptables -A penalty_box -m owner --uid-owner app_3 --jump REJECT --reject-with icmp-net-prohibited
78 */
79const char *BandwidthController::cleanupCommands[] = {
80    /* Cleanup rules. */
81    "-F",
82    "-t raw -F",
83    "-X costly",
84    "-X penalty_box",
85};
86
87const char *BandwidthController::setupCommands[] = {
88    /* Created needed chains. */
89    "-N costly",
90    "-N penalty_box",
91};
92
93const char *BandwidthController::basicAccountingCommands[] = {
94    "-F INPUT",
95    "-A INPUT -i lo --jump ACCEPT",
96    "-A INPUT -m owner --socket-exists", /* This is a tracking rule. */
97
98    "-F OUTPUT",
99    "-A OUTPUT -o lo --jump ACCEPT",
100    "-A OUTPUT -m owner --socket-exists", /* This is a tracking rule. */
101
102    "-F costly",
103    "-A costly --jump penalty_box",
104    "-A costly -m owner --socket-exists", /* This is a tracking rule. */
105    /* TODO(jpa): Figure out why iptables doesn't correctly return from this
106     * chain. For now, hack the chain exit with an ACCEPT.
107     */
108    "-A costly --jump ACCEPT",
109};
110
111BandwidthController::BandwidthController(void) {
112
113    char value[PROPERTY_VALUE_MAX];
114
115    property_get("persist.bandwidth.enable", value, "0");
116    if (!strcmp(value, "1")) {
117        enableBandwidthControl();
118    }
119
120}
121
122int BandwidthController::runIpxtablesCmd(const char *cmd, bool appendReject) {
123    int res = 0;
124    res |= runIptablesCmd(cmd, appendReject, false);
125    res |= runIptablesCmd(cmd, appendReject, true);
126    return res;
127}
128
129int BandwidthController::runIptablesCmd(const char *cmd, bool appendReject, bool isIp6) {
130    char buffer[MAX_CMD_LEN] = { 0 }; // strncpy() is not filling leftover with '\0'
131    const char *argv[MAX_CMD_ARGS];
132    int argc, nextArg;
133    char *next = buffer;
134    char *tmp;
135
136    std::string fullCmd = cmd;
137    if (appendReject) {
138        fullCmd += " --jump REJECT --reject-with";
139        if (isIp6) {
140            fullCmd += " icmp6-adm-prohibited";
141        } else {
142            fullCmd += " icmp-net-prohibited";
143        }
144        argc = 4; //  --jump ...
145    }
146
147    nextArg = 0;
148    argv[nextArg++] = isIp6 ? IP6TABLES_PATH : IPTABLES_PATH;
149    argc++;
150    LOGD("About to run: %s %s", argv[0], fullCmd.c_str());
151
152    strncpy(buffer, fullCmd.c_str(), sizeof(buffer) - 1);
153    if (buffer[sizeof(buffer) - 1]) {
154        LOGE("iptables command too long");
155        errno = E2BIG;
156        return -1;
157    }
158
159    while ((tmp = strsep(&next, " "))) {
160        argv[nextArg++] = tmp;
161        argc++;
162        if (argc >= MAX_CMD_ARGS) {
163            LOGE("iptables argument overflow");
164            errno = E2BIG;
165            return -1;
166        }
167    }
168
169    argv[argc] = NULL;
170    /* TODO(jpa): Once this stabilizes, remove logwrap() as it tends to wedge netd
171     * Then just talk directly to the kernel via rtnetlink.
172     */
173    return logwrap(argc, argv, 0);
174}
175
176int BandwidthController::enableBandwidthControl(void) {
177    int res;
178    /* Some of the initialCommands are allowed to fail */
179    runCommands(sizeof(cleanupCommands) / sizeof(char*), cleanupCommands, true);
180    runCommands(sizeof(setupCommands) / sizeof(char*), setupCommands, true);
181    res = runCommands(sizeof(basicAccountingCommands) / sizeof(char*), basicAccountingCommands,
182                      false);
183    return res;
184
185}
186
187int BandwidthController::disableBandwidthControl(void) {
188    /* The cleanupCommands are allowed to fail. */
189    runCommands(sizeof(cleanupCommands) / sizeof(char*), cleanupCommands, true);
190    return 0;
191}
192
193int BandwidthController::runCommands(int numCommands, const char *commands[], bool allowFailure) {
194    int res = 0;
195    LOGD("runCommands(): %d commands", numCommands);
196    for (int cmdNum = 0; cmdNum < numCommands; cmdNum++) {
197        res = runIpxtablesCmd(commands[cmdNum], false);
198        if (res && !allowFailure)
199            return res;
200    }
201    return allowFailure ? res : 0;
202}
203
204std::string BandwidthController::makeIptablesNaughtyCmd(IptOp op, int uid) {
205    std::string res;
206    char convBuff[21]; // log10(2^64) ~ 20
207
208    switch (op) {
209        case IptOpInsert:
210            res = "-I";
211            break;
212        case IptOpReplace:
213            res = "-R";
214            break;
215        default:
216        case IptOpDelete:
217            res = "-D";
218            break;
219    }
220    res += " penalty_box";
221    sprintf(convBuff, "%d", uid);
222    res += " -m owner --uid-owner ";
223    res += convBuff;
224    return res;
225}
226
227int BandwidthController::addNaughtyApps(int numUids, char *appUids[]) {
228    return maninpulateNaughtyApps(numUids, appUids, true);
229}
230
231int BandwidthController::removeNaughtyApps(int numUids, char *appUids[]) {
232    return maninpulateNaughtyApps(numUids, appUids, false);
233}
234
235int BandwidthController::maninpulateNaughtyApps(int numUids, char *appStrUids[], bool doAdd) {
236    char cmd[MAX_CMD_LEN];
237    int uidNum;
238    const char *addFailedTemplate = "Failed to add app uid %d to penalty box.";
239    const char *deleteFailedTemplate = "Failed to delete app uid %d from penalty box.";
240    IptOp op = doAdd ? IptOpInsert : IptOpDelete;
241
242    int appUids[numUids];
243    for (uidNum = 0; uidNum < numUids; uidNum++) {
244        appUids[uidNum] = atol(appStrUids[uidNum]);
245        if (appUids[uidNum] == 0) {
246            LOGE((doAdd ? addFailedTemplate : deleteFailedTemplate), appUids[uidNum]);
247            goto fail_parse;
248        }
249    }
250
251    for (uidNum = 0; uidNum < numUids; uidNum++) {
252        std::string naughtyCmd = makeIptablesNaughtyCmd(op, appUids[uidNum]);
253        if (runIpxtablesCmd(naughtyCmd.c_str(), true)) {
254            LOGE((doAdd ? addFailedTemplate : deleteFailedTemplate), appUids[uidNum]);
255            goto fail_with_uidNum;
256        }
257    }
258    return 0;
259
260    fail_with_uidNum:
261    /* Try to remove the uid that failed in any case*/
262    runIpxtablesCmd(makeIptablesNaughtyCmd(IptOpDelete, appUids[uidNum]).c_str(), true);
263    fail_parse: return -1;
264}
265
266std::string BandwidthController::makeIptablesQuotaCmd(IptOp op, char *costName, int64_t quota) {
267    std::string res;
268    char convBuff[21]; // log10(2^64) ~ 20
269
270    LOGD("makeIptablesQuotaCmd(%d, %llu)", op, quota);
271
272    switch (op) {
273        case IptOpInsert:
274            res = "-I";
275            break;
276        case IptOpReplace:
277            res = "-R";
278            break;
279        default:
280        case IptOpDelete:
281            res = "-D";
282            break;
283    }
284    res += " costly";
285    if (costName) {
286        res += "_";
287        res += costName;
288    }
289    sprintf(convBuff, "%lld", quota);
290    /* TODO(jpa): Use -m quota2 --name " + costName + " ! --quota "
291     * once available.
292     */
293    res += " -m quota ! --quota ";
294    res += convBuff;
295    ;
296    // The requried --jump REJECT ... will be added later.
297    return res;
298}
299
300int BandwidthController::prepCostlyIface(const char *ifn, bool isShared) {
301    char cmd[MAX_CMD_LEN];
302    int res = 0;
303    std::string costString;
304    const char *costCString;
305
306    costString = "costly";
307    /* The "-N costly" is created upfront, no need to handle it here. */
308    if (!isShared) {
309        costString += "_";
310        costString += ifn;
311        costCString = costString.c_str();
312        snprintf(cmd, sizeof(cmd), "-N %s", costCString);
313        res |= runIpxtablesCmd(cmd, false);
314        snprintf(cmd, sizeof(cmd), "-A %s -j penalty_box", costCString);
315        res |= runIpxtablesCmd(cmd, false);
316        snprintf(cmd, sizeof(cmd), "-A %s -m owner --socket-exists", costCString);
317        res |= runIpxtablesCmd(cmd, false);
318        /* TODO(jpa): Figure out why iptables doesn't correctly return from this
319         * chain. For now, hack the chain exit with an ACCEPT.
320         */
321        snprintf(cmd, sizeof(cmd), "-A %s --jump ACCEPT", costCString);
322        res |= runIpxtablesCmd(cmd, false);
323    } else {
324        costCString = costString.c_str();
325    }
326
327    snprintf(cmd, sizeof(cmd), "-I INPUT -i %s --goto %s", ifn, costCString);
328    res |= runIpxtablesCmd(cmd, false);
329    snprintf(cmd, sizeof(cmd), "-I OUTPUT -o %s --goto %s", ifn, costCString);
330    res |= runIpxtablesCmd(cmd, false);
331    return res;
332}
333
334int BandwidthController::cleanupCostlyIface(const char *ifn, bool isShared) {
335    char cmd[MAX_CMD_LEN];
336    int res = 0;
337    std::string costString;
338    const char *costCString;
339
340    costString = "costly";
341    if (!isShared) {
342        costString += "_";
343        costString += ifn;
344        costCString = costString.c_str();
345    } else {
346        costCString = costString.c_str();
347    }
348
349    snprintf(cmd, sizeof(cmd), "-D INPUT -i %s --goto %s", ifn, costCString);
350    res |= runIpxtablesCmd(cmd, false);
351    snprintf(cmd, sizeof(cmd), "-D OUTPUT -o %s --goto %s", ifn, costCString);
352    res |= runIpxtablesCmd(cmd, false);
353
354    /* The "-N costly" is created upfront, no need to handle it here. */
355    if (!isShared) {
356        snprintf(cmd, sizeof(cmd), "-F %s", costCString);
357        res |= runIpxtablesCmd(cmd, false);
358    }
359    return res;
360}
361
362int BandwidthController::setInterfaceSharedQuota(const char *iface, int64_t maxBytes) {
363    char cmd[MAX_CMD_LEN];
364    char ifn[MAX_IFACENAME_LEN];
365    int res = 0;
366
367    memset(ifn, 0, sizeof(ifn));
368    strncpy(ifn, iface, sizeof(ifn) - 1);
369
370    if (maxBytes == -1) {
371        return removeInterfaceSharedQuota(ifn);
372    }
373
374    char *costName = NULL; /* Shared quota */
375
376    /* Insert ingress quota. */
377    std::string ifaceName(ifn);
378    std::list<std::string>::iterator it;
379    for (it = sharedQuotaIfaces.begin(); it != sharedQuotaIfaces.end(); it++) {
380        if (*it == ifaceName)
381            break;
382    }
383
384    if (it == sharedQuotaIfaces.end()) {
385        res |= prepCostlyIface(ifn, true);
386        if (sharedQuotaIfaces.empty()) {
387            std::string quotaCmd;
388            quotaCmd = makeIptablesQuotaCmd(IptOpInsert, costName, maxBytes);
389            res |= runIpxtablesCmd(quotaCmd.c_str(), true);
390            if (res) {
391                LOGE("Failed set quota rule.");
392                goto fail;
393            }
394            sharedQuotaBytes = maxBytes;
395        }
396        sharedQuotaIfaces.push_front(ifaceName);
397
398    }
399
400    if (maxBytes != sharedQuotaBytes) {
401        /* Instead of replacing, which requires being aware of the rules in
402         * the kernel, we just add a new one, then delete the older one.
403         */
404        std::string quotaCmd;
405
406        quotaCmd = makeIptablesQuotaCmd(IptOpInsert, costName, maxBytes);
407        res |= runIpxtablesCmd(quotaCmd.c_str(), true);
408
409        quotaCmd = makeIptablesQuotaCmd(IptOpDelete, costName, sharedQuotaBytes);
410        res |= runIpxtablesCmd(quotaCmd.c_str(), true);
411
412        if (res) {
413            LOGE("Failed replace quota rule.");
414            goto fail;
415        }
416        sharedQuotaBytes = maxBytes;
417    }
418    return 0;
419
420    fail:
421    /*
422     * TODO(jpa): once we get rid of iptables in favor of rtnetlink, reparse
423     * rules in the kernel to see which ones need cleaning up.
424     * For now callers needs to choose if they want to "ndc bandwidth enable"
425     * which resets everything.
426     */
427    removeInterfaceSharedQuota(ifn);
428    return -1;
429}
430
431int BandwidthController::removeInterfaceSharedQuota(const char *iface) {
432    char ifn[MAX_IFACENAME_LEN];
433    int res = 0;
434
435    memset(ifn, 0, sizeof(ifn));
436    strncpy(ifn, iface, sizeof(ifn) - 1);
437
438    std::string ifaceName(ifn);
439    std::list<std::string>::iterator it;
440
441    for (it = sharedQuotaIfaces.begin(); it != sharedQuotaIfaces.end(); it++) {
442        if (*it == ifaceName)
443            break;
444    }
445    if (it == sharedQuotaIfaces.end()) {
446        LOGE("No such iface %s to delete.", ifn);
447        return -1;
448    }
449
450    res |= cleanupCostlyIface(ifn, true);
451    sharedQuotaIfaces.erase(it);
452
453    if (sharedQuotaIfaces.empty()) {
454        std::string quotaCmd;
455        quotaCmd = makeIptablesQuotaCmd(IptOpDelete, NULL, sharedQuotaBytes);
456        res |= runIpxtablesCmd(quotaCmd.c_str(), true);
457        sharedQuotaBytes = -1;
458    }
459
460    return res;
461}
462
463int BandwidthController::setInterfaceQuota(const char *iface, int64_t maxBytes) {
464    char ifn[MAX_IFACENAME_LEN];
465    int res = 0;
466
467    memset(ifn, 0, sizeof(ifn));
468    strncpy(ifn, iface, sizeof(ifn) - 1);
469
470    if (maxBytes == -1) {
471        return removeInterfaceQuota(ifn);
472    }
473
474    char *costName = ifn;
475
476    /* Insert ingress quota. */
477    std::string ifaceName(ifn);
478    std::list<QuotaInfo>::iterator it;
479    for (it = quotaIfaces.begin(); it != quotaIfaces.end(); it++) {
480        if (it->first == ifaceName)
481            break;
482    }
483
484    if (it == quotaIfaces.end()) {
485
486        res |= prepCostlyIface(ifn, false);
487
488        std::string quotaCmd;
489        quotaCmd = makeIptablesQuotaCmd(IptOpInsert, costName, maxBytes);
490        res |= runIpxtablesCmd(quotaCmd.c_str(), true);
491        if (res) {
492            LOGE("Failed set quota rule.");
493            goto fail;
494        }
495
496        quotaIfaces.push_front(QuotaInfo(ifaceName, maxBytes));
497
498    } else {
499        /* Instead of replacing, which requires being aware of the rules in
500         * the kernel, we just add a new one, then delete the older one.
501         */
502        std::string quotaCmd;
503
504        quotaCmd = makeIptablesQuotaCmd(IptOpInsert, costName, maxBytes);
505        res |= runIpxtablesCmd(quotaCmd.c_str(), true);
506
507        quotaCmd = makeIptablesQuotaCmd(IptOpDelete, costName, it->second);
508        res |= runIpxtablesCmd(quotaCmd.c_str(), true);
509
510        if (res) {
511            LOGE("Failed replace quota rule.");
512            goto fail;
513        }
514        it->second = maxBytes;
515    }
516    return 0;
517
518    fail:
519    /*
520     * TODO(jpa): once we get rid of iptables in favor of rtnetlink, reparse
521     * rules in the kernel to see which ones need cleaning up.
522     * For now callers needs to choose if they want to "ndc bandwidth enable"
523     * which resets everything.
524     */
525    removeInterfaceSharedQuota(ifn);
526    return -1;
527}
528
529int BandwidthController::removeInterfaceQuota(const char *iface) {
530
531    char ifn[MAX_IFACENAME_LEN];
532    int res = 0;
533
534    memset(ifn, 0, sizeof(ifn));
535    strncpy(ifn, iface, sizeof(ifn) - 1);
536
537    char *costName = ifn;
538
539    std::string ifaceName(ifn);
540    std::list<QuotaInfo>::iterator it;
541    for (it = quotaIfaces.begin(); it != quotaIfaces.end(); it++) {
542        if (it->first == ifaceName)
543            break;
544    }
545
546    if (it == quotaIfaces.end()) {
547        LOGE("No such iface %s to delete.", ifn);
548        return -1;
549    }
550
551    /* This also removes the quota command of CostlyIface chain. */
552    res |= cleanupCostlyIface(ifn, false);
553
554    quotaIfaces.erase(it);
555
556    return res;
557}
558