BandwidthController.cpp revision a9f802c23f4c2c53fa1065b75f712ce46f384c3a
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
160    LOGD("runIptablesCmd(): %s %s", argv[0], fullCmd.c_str());
161    if (StrncpyAndCheck(buffer, fullCmd.c_str(), sizeof(buffer))) {
162        LOGE("iptables command too long");
163        return -1;
164    }
165
166    while ((tmp = strsep(&next, " "))) {
167        argv[argc++] = tmp;
168        if (argc >= MAX_CMD_ARGS) {
169            LOGE("iptables argument overflow");
170            return -1;
171        }
172    }
173
174    argv[argc] = NULL;
175    /* TODO(jpa): Once this stabilizes, remove logwrap() as it tends to wedge netd
176     * Then just talk directly to the kernel via rtnetlink.
177     */
178    return logwrap(argc, argv, 0);
179}
180
181int BandwidthController::enableBandwidthControl(void) {
182    int res;
183    /* Some of the initialCommands are allowed to fail */
184    runCommands(sizeof(cleanupCommands) / sizeof(char*), cleanupCommands, RunCmdFailureOk);
185    runCommands(sizeof(setupCommands) / sizeof(char*), setupCommands, RunCmdFailureOk);
186    res = runCommands(sizeof(basicAccountingCommands) / sizeof(char*), basicAccountingCommands, RunCmdFailureBad);
187    return res;
188
189}
190
191int BandwidthController::disableBandwidthControl(void) {
192    /* The cleanupCommands are allowed to fail. */
193    runCommands(sizeof(cleanupCommands) / sizeof(char*), cleanupCommands, RunCmdFailureOk);
194    return 0;
195}
196
197int BandwidthController::runCommands(int numCommands, const char *commands[], RunCmdErrHandling cmdErrHandling) {
198    int res = 0;
199    LOGD("runCommands(): %d commands", numCommands);
200    for (int cmdNum = 0; cmdNum < numCommands; cmdNum++) {
201        res = runIpxtablesCmd(commands[cmdNum], IptRejectNoAdd);
202        if (res && cmdErrHandling != RunCmdFailureBad)
203            return res;
204    }
205    return cmdErrHandling == RunCmdFailureBad ? res : 0;
206}
207
208std::string BandwidthController::makeIptablesNaughtyCmd(IptOp op, int uid) {
209    std::string res;
210    char *convBuff;
211
212    switch (op) {
213        case IptOpInsert:
214            res = "-I";
215            break;
216        case IptOpReplace:
217            res = "-R";
218            break;
219        default:
220        case IptOpDelete:
221            res = "-D";
222            break;
223    }
224    res += " penalty_box";
225    asprintf(&convBuff, "%d", uid);
226    res += " -m owner --uid-owner ";
227    res += convBuff;
228    free(convBuff);
229    return res;
230}
231
232int BandwidthController::addNaughtyApps(int numUids, char *appUids[]) {
233    return maninpulateNaughtyApps(numUids, appUids, NaughtyAppOpAdd);
234}
235
236int BandwidthController::removeNaughtyApps(int numUids, char *appUids[]) {
237    return maninpulateNaughtyApps(numUids, appUids, NaughtyAppOpRemove);
238}
239
240int BandwidthController::maninpulateNaughtyApps(int numUids, char *appStrUids[], NaughtyAppOp appOp) {
241    char cmd[MAX_CMD_LEN];
242    int uidNum;
243    const char *failLogTemplate;
244    IptOp op;
245    int appUids[numUids];
246    std::string naughtyCmd;
247    switch (appOp) {
248    case NaughtyAppOpAdd:
249            op = IptOpInsert;
250            failLogTemplate = "Failed to add app uid %d to penalty box.";
251            break;
252    case NaughtyAppOpRemove:
253            op = IptOpDelete;
254            failLogTemplate = "Failed to delete app uid %d from penalty box.";
255            break;
256    }
257
258    for (uidNum = 0; uidNum < numUids; uidNum++) {
259        appUids[uidNum] = atol(appStrUids[uidNum]);
260        if (appUids[uidNum] == 0) {
261            LOGE(failLogTemplate, appUids[uidNum]);
262            goto fail_parse;
263        }
264    }
265
266    for (uidNum = 0; uidNum < numUids; uidNum++) {
267        naughtyCmd = makeIptablesNaughtyCmd(op, appUids[uidNum]);
268        if (runIpxtablesCmd(naughtyCmd.c_str(), IptRejectAdd)) {
269            LOGE(failLogTemplate, appUids[uidNum]);
270            goto fail_with_uidNum;
271        }
272    }
273    return 0;
274
275fail_with_uidNum:
276    /* Try to remove the uid that failed in any case*/
277    naughtyCmd = makeIptablesNaughtyCmd(IptOpDelete, appUids[uidNum]);
278    runIpxtablesCmd(naughtyCmd.c_str(), IptRejectAdd);
279fail_parse:
280    return -1;
281}
282
283std::string BandwidthController::makeIptablesQuotaCmd(IptOp op, const char *costName, int64_t quota) {
284    std::string res;
285    char convBuff[21]; // log10(2^64) ~ 20
286
287    LOGD("makeIptablesQuotaCmd(%d, %llu)", op, quota);
288
289    switch (op) {
290        case IptOpInsert:
291            res = "-I";
292            break;
293        case IptOpReplace:
294            res = "-R";
295            break;
296        default:
297        case IptOpDelete:
298            res = "-D";
299            break;
300    }
301    res += " costly";
302    if (costName) {
303        res += "_";
304        res += costName;
305    }
306    sprintf(convBuff, "%lld", quota);
307    /* TODO(jpa): Use -m quota2 --name " + costName + " ! --quota "
308     * once available.
309     */
310    res += " -m quota ! --quota ";
311    res += convBuff;
312    ;
313    // The requried --jump REJECT ... will be added later.
314    return res;
315}
316
317int BandwidthController::prepCostlyIface(const char *ifn, QuotaType quotaType) {
318    char cmd[MAX_CMD_LEN];
319    int res = 0;
320    std::string costString;
321    const char *costCString;
322
323    costString = "costly";
324    /* The "-N costly" is created upfront, no need to handle it here. */
325    switch (quotaType) {
326    case QuotaUnique:
327        costString += "_";
328        costString += ifn;
329        costCString = costString.c_str();
330        snprintf(cmd, sizeof(cmd), "-N %s", costCString);
331        res |= runIpxtablesCmd(cmd, IptRejectNoAdd);
332        snprintf(cmd, sizeof(cmd), "-A %s -j penalty_box", costCString);
333        res |= runIpxtablesCmd(cmd, IptRejectNoAdd);
334        snprintf(cmd, sizeof(cmd), "-A %s -m owner --socket-exists", costCString);
335        res |= runIpxtablesCmd(cmd, IptRejectNoAdd);
336        /* TODO(jpa): Figure out why iptables doesn't correctly return from this
337         * chain. For now, hack the chain exit with an ACCEPT.
338         */
339        snprintf(cmd, sizeof(cmd), "-A %s --jump ACCEPT", costCString);
340        res |= runIpxtablesCmd(cmd, IptRejectNoAdd);
341        break;
342    case QuotaShared:
343        costCString = costString.c_str();
344        break;
345    }
346
347    snprintf(cmd, sizeof(cmd), "-I INPUT -i %s --goto %s", ifn, costCString);
348    res |= runIpxtablesCmd(cmd, IptRejectNoAdd);
349    snprintf(cmd, sizeof(cmd), "-I OUTPUT -o %s --goto %s", ifn, costCString);
350    res |= runIpxtablesCmd(cmd, IptRejectNoAdd);
351    return res;
352}
353
354int BandwidthController::cleanupCostlyIface(const char *ifn, QuotaType quotaType) {
355    char cmd[MAX_CMD_LEN];
356    int res = 0;
357    std::string costString;
358    const char *costCString;
359
360    costString = "costly";
361    switch (quotaType) {
362    case QuotaUnique:
363        costString += "_";
364        costString += ifn;
365        costCString = costString.c_str();
366        break;
367    case QuotaShared:
368        costCString = costString.c_str();
369        break;
370    }
371
372    snprintf(cmd, sizeof(cmd), "-D INPUT -i %s --goto %s", ifn, costCString);
373    res |= runIpxtablesCmd(cmd, IptRejectNoAdd);
374    snprintf(cmd, sizeof(cmd), "-D OUTPUT -o %s --goto %s", ifn, costCString);
375    res |= runIpxtablesCmd(cmd, IptRejectNoAdd);
376
377    /* The "-N costly" is created upfront, no need to handle it here. */
378    if (quotaType == QuotaUnique) {
379        snprintf(cmd, sizeof(cmd), "-F %s", costCString);
380        res |= runIpxtablesCmd(cmd, IptRejectNoAdd);
381        snprintf(cmd, sizeof(cmd), "-X %s", costCString);
382        res |= runIpxtablesCmd(cmd, IptRejectNoAdd);
383    }
384    return res;
385}
386
387int BandwidthController::setInterfaceSharedQuota(const char *iface, int64_t maxBytes) {
388    char cmd[MAX_CMD_LEN];
389    char ifn[MAX_IFACENAME_LEN];
390    int res = 0;
391    std::string quotaCmd;
392    std::string ifaceName;;
393    const char *costName = NULL; /* Shared quota */
394    std::list<std::string>::iterator it;
395
396    if (StrncpyAndCheck(ifn, iface, sizeof(ifn))) {
397        LOGE("Interface name longer than %d", MAX_IFACENAME_LEN);
398        return -1;
399    }
400    ifaceName = ifn;
401
402    if (maxBytes == -1) {
403        return removeInterfaceSharedQuota(ifn);
404    }
405
406    /* Insert ingress quota. */
407    for (it = sharedQuotaIfaces.begin(); it != sharedQuotaIfaces.end(); it++) {
408        if (*it == ifaceName)
409            break;
410    }
411
412    if (it == sharedQuotaIfaces.end()) {
413        res |= prepCostlyIface(ifn, QuotaShared);
414        if (sharedQuotaIfaces.empty()) {
415            quotaCmd = makeIptablesQuotaCmd(IptOpInsert, costName, maxBytes);
416            res |= runIpxtablesCmd(quotaCmd.c_str(), IptRejectAdd);
417            if (res) {
418                LOGE("Failed set quota rule.");
419                goto fail;
420            }
421            sharedQuotaBytes = maxBytes;
422        }
423        sharedQuotaIfaces.push_front(ifaceName);
424
425    }
426
427    if (maxBytes != sharedQuotaBytes) {
428        /* Instead of replacing, which requires being aware of the rules in
429         * the kernel, we just add a new one, then delete the older one.
430         */
431
432        quotaCmd = makeIptablesQuotaCmd(IptOpInsert, costName, maxBytes);
433        res |= runIpxtablesCmd(quotaCmd.c_str(), IptRejectAdd);
434
435        quotaCmd = makeIptablesQuotaCmd(IptOpDelete, costName, sharedQuotaBytes);
436        res |= runIpxtablesCmd(quotaCmd.c_str(), IptRejectAdd);
437
438        if (res) {
439            LOGE("Failed replace quota rule.");
440            goto fail;
441        }
442        sharedQuotaBytes = maxBytes;
443    }
444    return 0;
445
446    fail:
447    /*
448     * TODO(jpa): once we get rid of iptables in favor of rtnetlink, reparse
449     * rules in the kernel to see which ones need cleaning up.
450     * For now callers needs to choose if they want to "ndc bandwidth enable"
451     * which resets everything.
452     */
453    removeInterfaceSharedQuota(ifn);
454    return -1;
455}
456
457int BandwidthController::removeInterfaceSharedQuota(const char *iface) {
458    char ifn[MAX_IFACENAME_LEN];
459    int res = 0;
460    std::string ifaceName;
461    std::list<std::string>::iterator it;
462
463    if(StrncpyAndCheck(ifn, iface, sizeof(ifn))) {
464        LOGE("Interface name longer than %d", MAX_IFACENAME_LEN);
465        return -1;
466    }
467    ifaceName =ifn;
468
469    for (it = sharedQuotaIfaces.begin(); it != sharedQuotaIfaces.end(); it++) {
470        if (*it == ifaceName)
471            break;
472    }
473    if (it == sharedQuotaIfaces.end()) {
474        LOGE("No such iface %s to delete.", ifn);
475        return -1;
476    }
477
478    res |= cleanupCostlyIface(ifn, QuotaShared);
479    sharedQuotaIfaces.erase(it);
480
481    if (sharedQuotaIfaces.empty()) {
482        std::string quotaCmd;
483        quotaCmd = makeIptablesQuotaCmd(IptOpDelete, NULL, sharedQuotaBytes);
484        res |= runIpxtablesCmd(quotaCmd.c_str(), IptRejectAdd);
485        sharedQuotaBytes = -1;
486    }
487
488    return res;
489}
490
491int BandwidthController::setInterfaceQuota(const char *iface, int64_t maxBytes) {
492    char ifn[MAX_IFACENAME_LEN];
493    int res = 0;
494    std::string ifaceName;
495    const char *costName;
496    std::list<QuotaInfo>::iterator it;
497    std::string quotaCmd;
498
499    if (maxBytes == -1) {
500        return removeInterfaceQuota(iface);
501    }
502
503    if(StrncpyAndCheck(ifn, iface, sizeof(ifn))) {
504        LOGE("Interface name longer than %d", MAX_IFACENAME_LEN);
505        return -1;
506    }
507    ifaceName = ifn;
508    costName = iface;
509
510
511    /* Insert ingress quota. */
512    for (it = quotaIfaces.begin(); it != quotaIfaces.end(); it++) {
513        if (it->first == ifaceName)
514            break;
515    }
516
517    if (it == quotaIfaces.end()) {
518        res |= prepCostlyIface(ifn, QuotaUnique);
519        quotaCmd = makeIptablesQuotaCmd(IptOpInsert, costName, maxBytes);
520        res |= runIpxtablesCmd(quotaCmd.c_str(), IptRejectAdd);
521        if (res) {
522            LOGE("Failed set quota rule.");
523            goto fail;
524        }
525
526        quotaIfaces.push_front(QuotaInfo(ifaceName, maxBytes));
527
528    } else {
529        /* Instead of replacing, which requires being aware of the rules in
530         * the kernel, we just add a new one, then delete the older one.
531         */
532        quotaCmd = makeIptablesQuotaCmd(IptOpInsert, costName, maxBytes);
533        res |= runIpxtablesCmd(quotaCmd.c_str(), IptRejectAdd);
534
535        quotaCmd = makeIptablesQuotaCmd(IptOpDelete, costName, it->second);
536        res |= runIpxtablesCmd(quotaCmd.c_str(), IptRejectAdd);
537
538        if (res) {
539            LOGE("Failed replace quota rule.");
540            goto fail;
541        }
542        it->second = maxBytes;
543    }
544    return 0;
545
546    fail:
547    /*
548     * TODO(jpa): once we get rid of iptables in favor of rtnetlink, reparse
549     * rules in the kernel to see which ones need cleaning up.
550     * For now callers needs to choose if they want to "ndc bandwidth enable"
551     * which resets everything.
552     */
553    removeInterfaceSharedQuota(ifn);
554    return -1;
555}
556
557int BandwidthController::removeInterfaceQuota(const char *iface) {
558
559    char ifn[MAX_IFACENAME_LEN];
560    int res = 0;
561    std::string ifaceName;
562    const char *costName;
563    std::list<QuotaInfo>::iterator it;
564
565    if(StrncpyAndCheck(ifn, iface, sizeof(ifn))) {
566        LOGE("Interface name longer than %d", MAX_IFACENAME_LEN);
567        return -1;
568    }
569    ifaceName = ifn;
570    costName = iface;
571
572    for (it = quotaIfaces.begin(); it != quotaIfaces.end(); it++) {
573        if (it->first == ifaceName)
574            break;
575    }
576
577    if (it == quotaIfaces.end()) {
578        LOGE("No such iface %s to delete.", ifn);
579        return -1;
580    }
581
582    /* This also removes the quota command of CostlyIface chain. */
583    res |= cleanupCostlyIface(ifn, QuotaUnique);
584
585    quotaIfaces.erase(it);
586
587    return res;
588}
589