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