BandwidthController.cpp revision bfa7466b328101a8b04807f26e85c84526c9a774
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_shared
55 *      iptables -I INPUT -i iface0 --goto costly_shared
56 *      iptables -I OUTPUT -o iface0 --goto costly_shared
57 *      iptables -I costly_shared -m quota \! --quota 500000 \
58 *          --jump REJECT --reject-with icmp-net-prohibited
59 *      iptables -A costly_shared --jump penalty_box
60 *      iptables -A costly_shared -m owner --socket-exists
61 *    . adding a new iface to this, E.g.:
62 *      iptables -I INPUT -i iface1 --goto costly_shared
63 *      iptables -I OUTPUT -o iface1 --goto costly_shared
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 \
71 *          --jump REJECT --reject-with icmp-net-prohibited
72 *      iptables -A costly_iface0 --jump penalty_box
73 *      iptables -A costly_iface0 -m owner --socket-exists
74 *
75 * * penalty_box handling:
76 *  - only one penalty_box for all interfaces
77 *   E.g  Adding an app:
78 *    iptables -A penalty_box -m owner --uid-owner app_3 \
79 *        --jump REJECT --reject-with icmp-net-prohibited
80 */
81const char *BandwidthController::cleanupCommands[] = {
82    /* Cleanup rules. */
83    "-F",
84    "-t raw -F",
85    /* TODO: If at some point we need more user chains than here, then we will need
86     * a different cleanup approach.
87     */
88    "-X",  /* Should normally only be costly_shared, penalty_box, and costly_<iface>  */
89};
90
91const char *BandwidthController::setupCommands[] = {
92    /* Created needed chains. */
93    "-N costly_shared",
94    "-N penalty_box",
95};
96
97const char *BandwidthController::basicAccountingCommands[] = {
98    "-F INPUT",
99    "-A INPUT -i lo --jump ACCEPT",
100    "-A INPUT -m owner --socket-exists", /* This is a tracking rule. */
101
102    "-F OUTPUT",
103    "-A OUTPUT -o lo --jump ACCEPT",
104    "-A OUTPUT -m owner --socket-exists", /* This is a tracking rule. */
105
106    "-F costly_shared",
107    "-A costly_shared --jump penalty_box",
108    "-A costly_shared -m owner --socket-exists", /* This is a tracking rule. */
109    /* TODO(jpa): Figure out why iptables doesn't correctly return from this
110     * chain. For now, hack the chain exit with an ACCEPT.
111     */
112    "-A costly_shared --jump ACCEPT",
113};
114
115BandwidthController::BandwidthController(void) {
116
117    char value[PROPERTY_VALUE_MAX];
118
119    property_get("persist.bandwidth.enable", value, "0");
120    if (!strcmp(value, "1")) {
121        enableBandwidthControl();
122    }
123
124}
125
126int BandwidthController::runIpxtablesCmd(const char *cmd, IptRejectOp rejectHandling) {
127    int res = 0;
128    LOGD("runIpxtablesCmd(cmd=%s)", cmd);
129    res |= runIptablesCmd(cmd, rejectHandling, IptIpV4);
130    res |= runIptablesCmd(cmd, rejectHandling, IptIpV6);
131    return res;
132}
133
134int BandwidthController::StrncpyAndCheck(char *buffer, const char *src, size_t buffSize) {
135
136    memset(buffer, '\0', buffSize);  // strncpy() is not filling leftover with '\0'
137    strncpy(buffer, src, buffSize);
138    return buffer[buffSize - 1];
139}
140
141int BandwidthController::runIptablesCmd(const char *cmd, IptRejectOp rejectHandling, IptIpVer iptVer) {
142    char buffer[MAX_CMD_LEN];
143    const char *argv[MAX_CMD_ARGS];
144    int argc = 0;
145    char *next = buffer;
146    char *tmp;
147
148    std::string fullCmd = cmd;
149
150    if (rejectHandling == IptRejectAdd) {
151        fullCmd += " --jump REJECT --reject-with";
152        switch (iptVer) {
153        case IptIpV4:
154                fullCmd += " icmp-net-prohibited";
155                break;
156        case IptIpV6:
157                fullCmd += " icmp6-adm-prohibited";
158                break;
159        }
160    }
161
162    argc = 0;
163    argv[argc++] = iptVer == IptIpV4 ? IPTABLES_PATH : IP6TABLES_PATH;
164
165    LOGD("runIptablesCmd(): %s %s", argv[0], fullCmd.c_str());
166    if (StrncpyAndCheck(buffer, fullCmd.c_str(), sizeof(buffer))) {
167        LOGE("iptables command too long");
168        return -1;
169    }
170
171    while ((tmp = strsep(&next, " "))) {
172        argv[argc++] = tmp;
173        if (argc >= MAX_CMD_ARGS) {
174            LOGE("iptables argument overflow");
175            return -1;
176        }
177    }
178
179    argv[argc] = NULL;
180    /* TODO(jpa): Once this stabilizes, remove logwrap() as it tends to wedge netd
181     * Then just talk directly to the kernel via rtnetlink.
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    return res;
235}
236
237int BandwidthController::addNaughtyApps(int numUids, char *appUids[]) {
238    return maninpulateNaughtyApps(numUids, appUids, NaughtyAppOpAdd);
239}
240
241int BandwidthController::removeNaughtyApps(int numUids, char *appUids[]) {
242    return maninpulateNaughtyApps(numUids, appUids, NaughtyAppOpRemove);
243}
244
245int BandwidthController::maninpulateNaughtyApps(int numUids, char *appStrUids[], NaughtyAppOp appOp) {
246    char cmd[MAX_CMD_LEN];
247    int uidNum;
248    const char *failLogTemplate;
249    IptOp op;
250    int appUids[numUids];
251    std::string naughtyCmd;
252    switch (appOp) {
253    case NaughtyAppOpAdd:
254            op = IptOpInsert;
255            failLogTemplate = "Failed to add app uid %d to penalty box.";
256            break;
257    case NaughtyAppOpRemove:
258            op = IptOpDelete;
259            failLogTemplate = "Failed to delete app uid %d from penalty box.";
260            break;
261    }
262
263    for (uidNum = 0; uidNum < numUids; uidNum++) {
264        appUids[uidNum] = atol(appStrUids[uidNum]);
265        if (appUids[uidNum] == 0) {
266            LOGE(failLogTemplate, appUids[uidNum]);
267            goto fail_parse;
268        }
269    }
270
271    for (uidNum = 0; uidNum < numUids; uidNum++) {
272        naughtyCmd = makeIptablesNaughtyCmd(op, appUids[uidNum]);
273        if (runIpxtablesCmd(naughtyCmd.c_str(), IptRejectAdd)) {
274            LOGE(failLogTemplate, appUids[uidNum]);
275            goto fail_with_uidNum;
276        }
277    }
278    return 0;
279
280fail_with_uidNum:
281    /* Try to remove the uid that failed in any case*/
282    naughtyCmd = makeIptablesNaughtyCmd(IptOpDelete, appUids[uidNum]);
283    runIpxtablesCmd(naughtyCmd.c_str(), IptRejectAdd);
284fail_parse:
285    return -1;
286}
287
288std::string BandwidthController::makeIptablesQuotaCmd(IptOp op, const char *costName, int64_t quota) {
289    std::string res;
290    char *convBuff;
291
292    LOGD("makeIptablesQuotaCmd(%d, %llu)", op, quota);
293
294    switch (op) {
295        case IptOpInsert:
296            res = "-I";
297            break;
298        case IptOpReplace:
299            res = "-R";
300            break;
301        default:
302        case IptOpDelete:
303            res = "-D";
304            break;
305    }
306    res += " costly_";
307    res += costName;
308    asprintf(&convBuff, "%lld", quota);
309    res += " -m quota2 --name ";
310    res += costName;
311    res += " ! --quota ";
312    res += convBuff;
313    free(convBuff);
314    // The requried IP version specific --jump REJECT ... will be added later.
315    return res;
316}
317
318int BandwidthController::prepCostlyIface(const char *ifn, QuotaType quotaType) {
319    char cmd[MAX_CMD_LEN];
320    int res = 0;
321    std::string costString;
322    const char *costCString;
323
324    /* The "-N costly" is created upfront, no need to handle it here. */
325    switch (quotaType) {
326    case QuotaUnique:
327        costString = "costly_";
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 = "costly_shared";
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    switch (quotaType) {
361    case QuotaUnique:
362        costString = "costly_";
363        costString += ifn;
364        costCString = costString.c_str();
365        break;
366    case QuotaShared:
367        costCString = "costly_shared";
368        break;
369    }
370
371    snprintf(cmd, sizeof(cmd), "-D INPUT -i %s --goto %s", ifn, costCString);
372    res |= runIpxtablesCmd(cmd, IptRejectNoAdd);
373    snprintf(cmd, sizeof(cmd), "-D OUTPUT -o %s --goto %s", ifn, costCString);
374    res |= runIpxtablesCmd(cmd, IptRejectNoAdd);
375
376    /* The "-N costly_shared" is created upfront, no need to handle it here. */
377    if (quotaType == QuotaUnique) {
378        snprintf(cmd, sizeof(cmd), "-F %s", costCString);
379        res |= runIpxtablesCmd(cmd, IptRejectNoAdd);
380        snprintf(cmd, sizeof(cmd), "-X %s", costCString);
381        res |= runIpxtablesCmd(cmd, IptRejectNoAdd);
382    }
383    return res;
384}
385
386int BandwidthController::setInterfaceSharedQuota(const char *iface, int64_t maxBytes) {
387    char cmd[MAX_CMD_LEN];
388    char ifn[MAX_IFACENAME_LEN];
389    int res = 0;
390    std::string quotaCmd;
391    std::string ifaceName;;
392    const char *costName = "shared";
393    std::list<std::string>::iterator it;
394
395    if (StrncpyAndCheck(ifn, iface, sizeof(ifn))) {
396        LOGE("Interface name longer than %d", MAX_IFACENAME_LEN);
397        return -1;
398    }
399    ifaceName = ifn;
400
401    if (maxBytes == -1) {
402        return removeInterfaceSharedQuota(ifn);
403    }
404
405    /* Insert ingress quota. */
406    for (it = sharedQuotaIfaces.begin(); it != sharedQuotaIfaces.end(); it++) {
407        if (*it == ifaceName)
408            break;
409    }
410
411    if (it == sharedQuotaIfaces.end()) {
412        res |= prepCostlyIface(ifn, QuotaShared);
413        if (sharedQuotaIfaces.empty()) {
414            quotaCmd = makeIptablesQuotaCmd(IptOpInsert, costName, maxBytes);
415            res |= runIpxtablesCmd(quotaCmd.c_str(), IptRejectAdd);
416            if (res) {
417                LOGE("Failed set quota rule.");
418                goto fail;
419            }
420            sharedQuotaBytes = maxBytes;
421        }
422        sharedQuotaIfaces.push_front(ifaceName);
423
424    }
425
426    if (maxBytes != sharedQuotaBytes) {
427        /* Instead of replacing, which requires being aware of the rules in
428         * the kernel, we just add a new one, then delete the older one.
429         */
430
431        quotaCmd = makeIptablesQuotaCmd(IptOpInsert, costName, maxBytes);
432        res |= runIpxtablesCmd(quotaCmd.c_str(), IptRejectAdd);
433
434        quotaCmd = makeIptablesQuotaCmd(IptOpDelete, costName, sharedQuotaBytes);
435        res |= runIpxtablesCmd(quotaCmd.c_str(), IptRejectAdd);
436
437        if (res) {
438            LOGE("Failed replace quota rule.");
439            goto fail;
440        }
441        sharedQuotaBytes = maxBytes;
442    }
443    return 0;
444
445    fail:
446    /*
447     * TODO(jpa): once we get rid of iptables in favor of rtnetlink, reparse
448     * rules in the kernel to see which ones need cleaning up.
449     * For now callers needs to choose if they want to "ndc bandwidth enable"
450     * which resets everything.
451     */
452    removeInterfaceSharedQuota(ifn);
453    return -1;
454}
455
456int BandwidthController::removeInterfaceSharedQuota(const char *iface) {
457    char ifn[MAX_IFACENAME_LEN];
458    int res = 0;
459    std::string ifaceName;
460    std::list<std::string>::iterator it;
461    const char *costName = "shared";
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, costName, 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