From 1abc55d3114378b4e73ec315eac6b122e55148c4 Mon Sep 17 00:00:00 2001 From: Bart De Schuymer Date: Sat, 1 Jun 2002 19:23:47 +0000 Subject: Initial revision --- ebtables.c | 1655 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1655 insertions(+) create mode 100644 ebtables.c (limited to 'ebtables.c') diff --git a/ebtables.c b/ebtables.c new file mode 100644 index 0000000..e28fd96 --- /dev/null +++ b/ebtables.c @@ -0,0 +1,1655 @@ +/* + * ebtables.c, v2.0 April 2002 + * + * Author: Bart De Schuymer + * + * This code is stongly inspired on the iptables code which is + * Copyright (C) 1999 Paul `Rusty' Russell & Michael J. Neuling + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include // the database +#include +#include +#include "include/ebtables_u.h" + +// here are the number-name correspondences kept for the ethernet +// frame type field +#define PROTOCOLFILE "/etc/ethertypes" + +#define DATABASEHOOKNR NF_BR_NUMHOOKS +#define DATABASEHOOKNAME "DB" + +static char *prog_name = PROGNAME; +static char *prog_version = PROGVERSION; +char* hooknames[NF_BR_NUMHOOKS] = { + [NF_BR_PRE_ROUTING]"PREROUTING", + [NF_BR_LOCAL_IN]"INPUT", + [NF_BR_FORWARD]"FORWARD", + [NF_BR_LOCAL_OUT]"OUTPUT", + [NF_BR_POST_ROUTING]"POSTROUTING", + [NF_BR_BROUTING]"BROUTING" +}; + +// default command line options +static struct option ebt_original_options[] = { + { "append" , required_argument, 0, 'A' }, + { "insert" , required_argument, 0, 'I' }, + { "delete" , required_argument, 0, 'D' }, + { "list" , optional_argument, 0, 'L' }, + { "zero" , optional_argument, 0, 'Z' }, + { "flush" , optional_argument, 0, 'F' }, + { "policy" , required_argument, 0, 'P' }, + { "in-interface" , required_argument, 0, 'i' }, + { "in-if" , required_argument, 0, 'i' }, + { "logical-in" , required_argument, 0, 2 }, + { "logical-out" , required_argument, 0, 3 }, + { "out-interface" , required_argument, 0, 'o' }, + { "out-if" , required_argument, 0, 'o' }, + { "version" , no_argument , 0, 'V' }, + { "help" , no_argument , 0, 'h' }, + { "jump" , required_argument, 0, 'j' }, + { "proto" , required_argument, 0, 'p' }, + { "protocol" , required_argument, 0, 'p' }, + { "db" , required_argument, 0, 'b' }, + { "source" , required_argument, 0, 's' }, + { "src" , required_argument, 0, 's' }, + { "destination" , required_argument, 0, 'd' }, + { "dst" , required_argument, 0, 'd' }, + { "table" , required_argument, 0, 't' }, + { 0 } +}; + +static struct option *ebt_options = ebt_original_options; + +// yup, all the possible target names +char* standard_targets[NUM_STANDARD_TARGETS] = { + "ACCEPT", + "DROP", + "CONTINUE", +}; + +unsigned char mac_type_unicast[ETH_ALEN] = {0,0,0,0,0,0}; +unsigned char msk_type_unicast[ETH_ALEN] = {1,0,0,0,0,0}; +unsigned char mac_type_multicast[ETH_ALEN] = {1,0,0,0,0,0}; +unsigned char msk_type_multicast[ETH_ALEN] = {1,0,0,0,0,0}; +unsigned char mac_type_broadcast[ETH_ALEN] = {255,255,255,255,255,255}; +unsigned char msk_type_broadcast[ETH_ALEN] = {255,255,255,255,255,255}; + +// tells what happened to the old rules +static unsigned short *counterchanges; +// holds all the data +static struct ebt_u_replace replace; + +// the chosen table +static struct ebt_u_table *table = NULL; +// the lists of supported tables, matches, watchers and targets +static struct ebt_u_table *tables = NULL; +static struct ebt_u_match *matches = NULL; +static struct ebt_u_watcher *watchers = NULL; +static struct ebt_u_target *targets = NULL; + +struct ebt_u_target *find_target(const char *name) +{ + struct ebt_u_target *t = targets; + + while(t && strcmp(t->name, name)) + t = t->next; + return t; +} + +struct ebt_u_match *find_match(const char *name) +{ + struct ebt_u_match *m = matches; + + while(m && strcmp(m->name, name)) + m = m->next; + return m; +} + +struct ebt_u_watcher *find_watcher(const char *name) +{ + struct ebt_u_watcher *w = watchers; + + while(w && strcmp(w->name, name)) + w = w->next; + return w; +} + +struct ebt_u_table *find_table(char *name) +{ + struct ebt_u_table *t = tables; + + while (t && strcmp(t->name, name)) + t = t->next; + return t; +} + +// The pointers in here are special: +// The struct ebt_target * pointer is actually a struct ebt_u_target * pointer. +// instead of making yet a few other structs, we just do a cast. +// We need a struct ebt_u_target pointer because we know the address of the data +// they point to won't change. We want to allow that the struct ebt_u_target.t +// member can change. +// Same holds for the struct ebt_match and struct ebt_watcher pointers +struct ebt_u_entry *new_entry; + +void initialize_entry(struct ebt_u_entry *e) +{ + e->bitmask = EBT_NOPROTO; + e->invflags = 0; + e->ethproto = 0; + strcpy(e->in, ""); + strcpy(e->out, ""); + strcpy(e->logical_in, ""); + strcpy(e->logical_out, ""); + e->m_list = NULL; + e->w_list = NULL; + // the init function of the standard target should have put the verdict + // on CONTINUE + e->t = (struct ebt_entry_target *)find_target(EBT_STANDARD_TARGET); + if (!e->t) + print_bug("Couldn't load standard target\n"); +} + +// this doesn't free e, becoz the calling function might need e->next +void free_u_entry(struct ebt_u_entry *e) +{ + struct ebt_u_match_list *m_l, *m_l2; + struct ebt_u_watcher_list *w_l, *w_l2; + + m_l = e->m_list; + while (m_l) { + m_l2 = m_l->next; + free(m_l->m); + free(m_l); + m_l = m_l2; + } + w_l = e->w_list; + while (w_l) { + w_l2 = w_l->next; + free(w_l->w); + free(w_l); + w_l = w_l2; + } + free(e->t); +} + +// the user will use the match, so put it in new_entry +static void add_match(struct ebt_u_match *m) +{ + struct ebt_u_match_list **m_list, *new; + + m->used = 1; + for (m_list = &new_entry->m_list; + *m_list; m_list = &(*m_list)->next); + new = (struct ebt_u_match_list *) + malloc(sizeof(struct ebt_u_match_list)); + if (!new) + print_memory(); + *m_list = new; + new->next = NULL; + new->m = (struct ebt_entry_match *)m; +} + +static void add_watcher(struct ebt_u_watcher *w) +{ + struct ebt_u_watcher_list **w_list; + struct ebt_u_watcher_list *new; + + w->used = 1; + for (w_list = &new_entry->w_list; + *w_list; w_list = &(*w_list)->next); + new = (struct ebt_u_watcher_list *) + malloc(sizeof(struct ebt_u_watcher_list)); + if (!new) + print_memory(); + *w_list = new; + new->next = NULL; + new->w = (struct ebt_entry_watcher *)w; +} + +static int global_option_offset = 0; +#define OPTION_OFFSET 256 +static struct option * +merge_options(struct option *oldopts, const struct option *newopts, + unsigned int *options_offset) +{ + unsigned int num_old, num_new, i; + struct option *merge; + + if (!newopts || !oldopts || !options_offset) + print_bug("merge wrong"); + for (num_old = 0; oldopts[num_old].name; num_old++); + for (num_new = 0; newopts[num_new].name; num_new++); + + global_option_offset += OPTION_OFFSET; + *options_offset = global_option_offset; + + merge = malloc(sizeof(struct option) * (num_new + num_old + 1)); + if (!merge) + print_memory(); + memcpy(merge, oldopts, num_old * sizeof(struct option)); + for (i = 0; i < num_new; i++) { + merge[num_old + i] = newopts[i]; + merge[num_old + i].val += *options_offset; + } + memset(merge + num_old + num_new, 0, sizeof(struct option)); + // only free dynamically allocated stuff + if (oldopts != ebt_original_options) + free(oldopts); + + return merge; +} + +void register_match(struct ebt_u_match *m) +{ + int size = m->size + sizeof(struct ebt_entry_match); + struct ebt_u_match **i; + + m->m = (struct ebt_entry_match *)malloc(size); + if (!m->m) + print_memory(); + strcpy(m->m->u.name, m->name); + m->m->match_size = m->size; + ebt_options = merge_options + (ebt_options, m->extra_ops, &(m->option_offset)); + m->init(m->m); + + for (i = &matches; *i; i = &((*i)->next)); + m->next = NULL; + *i = m; +} + +void register_watcher(struct ebt_u_watcher *w) +{ + int size = w->size + sizeof(struct ebt_entry_watcher); + struct ebt_u_watcher **i; + + w->w = (struct ebt_entry_watcher *)malloc(size); + if (!w->w) + print_memory(); + strcpy(w->w->u.name, w->name); + w->w->watcher_size = w->size; + ebt_options = merge_options + (ebt_options, w->extra_ops, &(w->option_offset)); + w->init(w->w); + + for (i = &watchers; *i; i = &((*i)->next)); + w->next = NULL; + *i = w; +} + +void register_target(struct ebt_u_target *t) +{ + int size = t->size + sizeof(struct ebt_entry_target); + struct ebt_u_target **i; + + t->t = (struct ebt_entry_target *)malloc(size); + if (!t->t) + print_memory(); + strcpy(t->t->u.name, t->name); + t->t->target_size = t->size; + ebt_options = merge_options + (ebt_options, t->extra_ops, &(t->option_offset)); + t->init(t->t); + for (i = &targets; *i; i = &((*i)->next)); + t->next = NULL; + *i = t; +} + +void register_table(struct ebt_u_table *t) +{ + t->next = tables; + tables = t; +} + +// used to parse /etc/etherproto +int disregard_whitespace(char *buffer, FILE *ifp) +{ + int hlp; + buffer[0] = '\t'; + while (buffer[0] == '\t' || buffer[0] == '\n' || buffer[0] == ' ') { + hlp = fscanf(ifp, "%c", buffer); + if (hlp == EOF || hlp == 0) return -1; + } + return 0; +} + +// used to parse /etc/etherproto +int disregard_tabspace(char *buffer, FILE *ifp) +{ + int hlp; + buffer[0] = '\t'; + while (buffer[0] == '\t' || buffer[0] == ' ') { + hlp = fscanf(ifp, "%c", buffer); + if (hlp == EOF || hlp == 0) return -1; + } + return 0; +} + +// helper function: processes a line of data from the file /etc/etherproto +int get_a_line(char *buffer, char *value, FILE *ifp) +{ + int i, hlp; + char anotherhlp; + + /* discard comment lines && whitespace*/ + while (1) { + if (disregard_whitespace(buffer, ifp)) return -1; + if (buffer[0] == '#') + while (1) { + hlp = fscanf(ifp, "%c", &anotherhlp); + if (!hlp || hlp == EOF) + return -1; + if (anotherhlp == '\n') + break; + } + else break; + } + + // buffer[0] already contains the first letter + for (i = 1; i < 21; i++) { + hlp = fscanf(ifp, "%c", buffer + i); + if (hlp == EOF || hlp == 0) return -1; + if (buffer[i] == '\t' || buffer[i] == ' ') + break; + } + if (i == 21) return -1; + buffer[i] = '\0'; + if (disregard_tabspace(value, ifp)) + return -1; + // maybe I should allow 0x0800 instead of 0800, but I'm feeling lazy + // buffer[0] already contains the first letter + for (i = 1; i < 5; i++) { + hlp = fscanf(ifp, "%c", value+i); + if (value[i] == '\n' || value[i] == '\t' || + value[i] == ' ' || hlp == EOF) + break; + } + if (i == 5) return -1; + /* discard comments at the end of a line */ + if (value[i] == '\t' || value[i] == ' ') + while (1) { + hlp = fscanf(ifp, "%c", &anotherhlp); + if (!hlp || hlp == EOF || anotherhlp == '\n') + break; + } + value[i] = '\0'; + return 0; +} + +// helper function for list_em() +int number_to_name(unsigned short proto, char *name) +{ + FILE *ifp; + char buffer[21], value[5], *bfr; + unsigned short i; + + if ( !(ifp = fopen(PROTOCOLFILE, "r")) ) + return -1; + while (1) { + if (get_a_line(buffer, value, ifp)) { + fclose(ifp); + return -1; + } + i = (unsigned short) strtol(value, &bfr, 16); + if (*bfr != '\0' || i != proto) + continue; + strcpy(name, buffer); + fclose(ifp); + return 0; + } +} + +// helper function for list_rules() +static void list_em(int hooknr) +{ + int i, j, space = 0, digits; + struct ebt_u_entry *hlp; + struct ebt_u_match_list *m_l; + struct ebt_u_watcher_list *w_l; + struct ebt_u_match *m; + struct ebt_u_watcher *w; + struct ebt_u_target *t; + char name[21]; + + hlp = replace.hook_entry[hooknr]->entries; + printf("\nBridge chain: %s\nPolicy: %s\n", hooknames[hooknr], + standard_targets[replace.hook_entry[hooknr]->policy]); + printf("nr. of entries: %d \n", replace.hook_entry[hooknr]->nentries); + + i = replace.hook_entry[hooknr]->nentries; + while (i >9) { + space++; + i /= 10; + } + + for (i = 0; i < replace.hook_entry[hooknr]->nentries; i++) { + digits = 0; + // A little work to get nice rule numbers. + while (j > 9) { + digits++; + j /= 10; + } + for (j = 0; j < space - digits; j++) + printf(" "); + printf("%d. ", i + 1); + + // Don't print anything about the protocol if no protocol was + // specified, obviously this means any protocol will do. + if (!(hlp->bitmask & EBT_NOPROTO)) { + printf("eth proto: "); + if (hlp->invflags & EBT_IPROTO) + printf("! "); + if (hlp->bitmask & EBT_802_3) + printf("Length, "); + else { + if (number_to_name(ntohs(hlp->ethproto), name)) + printf("0x%x, ", ntohs(hlp->ethproto)); + else + printf("%s, ", name); + } + } + if (hlp->bitmask & EBT_SOURCEMAC) { + char hlpmsk[6] = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff}; + + printf("source mac: "); + if (hlp->invflags & EBT_ISOURCE) + printf("! "); + if (!memcmp(hlp->sourcemac, mac_type_unicast, 6) && + !memcmp(hlp->sourcemsk, msk_type_unicast, 6)) { + printf("Unicast"); + goto endsrc; + } + if (!memcmp(hlp->sourcemac, mac_type_multicast, 6) && + !memcmp(hlp->sourcemsk, msk_type_multicast, 6)) { + printf("Multicast"); + goto endsrc; + } + if (!memcmp(hlp->sourcemac, mac_type_broadcast, 6) && + !memcmp(hlp->sourcemsk, msk_type_broadcast, 6)) { + printf("Broadcast"); + goto endsrc; + } + for (j = 0; j < ETH_ALEN; j++) + printf("%02x%s", hlp->sourcemac[j], + (j == ETH_ALEN - 1) ? "" : ":"); + if (memcmp(hlp->sourcemsk, hlpmsk, 6)) { + printf("/"); + for (j = 0; j < ETH_ALEN; j++) + printf("%02x%s", hlp->sourcemsk[j], + (j == ETH_ALEN - 1) ? "" : ":"); + } +endsrc: + printf(", "); + } + if (hlp->bitmask & EBT_DESTMAC) { + char hlpmsk[6] = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff}; + + printf("dest mac: "); + if (hlp->invflags & EBT_IDEST) + printf("! "); + if (!memcmp(hlp->destmac, mac_type_unicast, 6) && + !memcmp(hlp->destmsk, msk_type_unicast, 6)) { + printf("Unicast"); + goto enddst; + } + if (!memcmp(hlp->destmac, mac_type_multicast, 6) && + !memcmp(hlp->destmsk, msk_type_multicast, 6)) { + printf("Multicast"); + goto enddst; + } + if (!memcmp(hlp->destmac, mac_type_broadcast, 6) && + !memcmp(hlp->destmsk, msk_type_broadcast, 6)) { + printf("Broadcast"); + goto enddst; + } + for (j = 0; j < ETH_ALEN; j++) + printf("%02x%s", hlp->destmac[j], + (j == ETH_ALEN - 1) ? "" : ":"); + if (memcmp(hlp->destmsk, hlpmsk, 6)) { + printf("/"); + for (j = 0; j < ETH_ALEN; j++) + printf("%02x%s", hlp->destmsk[j], + (j == ETH_ALEN - 1) ? "" : ":"); + } +enddst: + printf(", "); + } + if (hlp->in[0] != '\0') { + if (hlp->invflags & EBT_IIN) + printf("! "); + printf("in-if: %s, ", hlp->in); + } + if (hlp->logical_in[0] != '\0') { + if (hlp->invflags & EBT_ILOGICALIN) + printf("! "); + printf("logical in-if: %s, ", hlp->logical_in); + } + if (hlp->logical_out[0] != '\0') { + if (hlp->invflags & EBT_ILOGICALOUT) + printf("! "); + printf("logical out-if: %s, ", hlp->logical_out); + } + if (hlp->out[0] != '\0') { + if (hlp->invflags & EBT_IOUT) + printf("! "); + printf("out-if: %s, ", hlp->out); + } + + m_l = hlp->m_list; + while (m_l) { + m = find_match(m_l->m->u.name); + if (!m) + print_bug("Match not found"); + m->print(hlp, m_l->m); + m_l = m_l->next; + } + w_l = hlp->w_list; + while (w_l) { + w = find_watcher(w_l->w->u.name); + if (!w) + print_bug("Watcher not found"); + w->print(hlp, w_l->w); + w_l = w_l->next; + } + + printf("target: "); + t = find_target(hlp->t->u.name); + if (!t) + print_bug("Target not found"); + t->print(hlp, hlp->t); + printf(", count = %llu", + replace.counters[replace.counter_entry[hooknr] + i].pcnt); + printf("\n"); + hlp = hlp->next; + } +} + +// parse the chain name and return the corresponding nr +int get_hooknr(char* arg) +{ + int i; + + // database is special case (not really a chain) + if (!strcmp(arg, DATABASEHOOKNAME)) + return DATABASEHOOKNR; + + for (i = 0; i < NF_BR_NUMHOOKS; i++) + if (!strcmp(arg, hooknames[i])) + return i; + return -1; +} + +// yup, print out help +void print_help() +{ + struct ebt_u_match_list *m_l; + struct ebt_u_watcher_list *w_l; + + printf( +"%s v%s\n" +"Usage:\n" +"ebtables -[ADI] chain rule-specification [options]\n" +"ebtables -P chain target\n" +"ebtables -[LFZ] [chain]\n" +"ebtables -[b] [y,n]\n" +"Commands:\n" +"--append -A chain : Append to chain\n" +"--delete -D chain : Delete matching rule from chain\n" +"--delete -D chain rulenum : Delete rule at position rulenum from chain\n" +"--insert -I chain rulenum : insert rule at position rulenum in chain\n" +"--list -L [chain] : List the rules in a chain or in all chains\n" +"--list -L "DATABASEHOOKNAME" : List the database (if present)\n" +"--flush -F [chain] : Delete all rules in chain or in all chains\n" +"--zero -Z [chain] : Put counters on zero in chain or in all chains\n" +"--policy -P chain target : Change policy on chain to target\n" +"Options:\n" +"--proto -p [!] proto : protocol hexadecimal, by name or LENGTH\n" +"--src -s [!] address[/mask]: source mac address\n" +"--dst -d [!] address[/mask]: destination mac address\n" +"--in-if -i [!] name : network input interface name\n" +"--out-if -o [!] name : network output interface name\n" +"--logical-in [!] name : logical bridge input interface name\n" +"--logical-out [!] name : logical bridge output interface name\n" +"--version -V : print package version\n" +"\n" , + prog_name, + prog_version); + + m_l = new_entry->m_list; + while (m_l) { + ((struct ebt_u_match *)m_l->m)->help(); + printf("\n"); + m_l = m_l->next; + } + w_l = new_entry->w_list; + while (w_l) { + ((struct ebt_u_watcher *)w_l->w)->help(); + printf("\n"); + w_l = w_l->next; + } + ((struct ebt_u_target *)new_entry->t)->help(); + printf("\n"); + if (table->help) + table->help(hooknames); + exit(0); +} + +// execute command L +static void list_rules() +{ + int i; + + printf("Bridge table: %s\n", table->name); + if (replace.selected_hook != -1) list_em(replace.selected_hook); + else + for (i = 0; i < NF_BR_NUMHOOKS; i++) + if (replace.valid_hooks & (1 << i)) + list_em(i); + return; +} + +// execute command P +static void change_policy(int policy) +{ + int i; + + // don't do anything if the policy is the same + if (replace.hook_entry[replace.selected_hook]->policy != policy) { + replace.hook_entry[replace.selected_hook]->policy = policy; + replace.num_counters = replace.nentries; + if (replace.nentries) { + // '+ 1' for the CNT_END + if (!(counterchanges = (unsigned short *) malloc( + (replace.nentries + 1) * sizeof(unsigned short)))) + print_memory(); + // done nothing special to the rules + for (i = 0; i < replace.nentries; i++) + counterchanges[i] = CNT_NORM; + counterchanges[replace.nentries] = CNT_END; + } + else + counterchanges = NULL; + } + else + exit(0); +} + +// flush one chain or the complete table +static void flush_chains() +{ + int i, j, oldnentries; + unsigned short *cnt; + struct ebt_u_entry *u_e, *tmp; + + // flush whole table + if (replace.selected_hook == -1) { + if (replace.nentries == 0) + exit(0); + replace.nentries = 0; + // no need for the kernel to give us counters back + replace.num_counters = 0; + // free everything and zero (n)entries + for (i = 0; i < NF_BR_NUMHOOKS; i++) { + if (!(replace.valid_hooks & (1 << i))) + continue; + replace.hook_entry[i]->nentries = 0; + u_e = replace.hook_entry[i]->entries; + while (u_e) { + free_u_entry(u_e); + tmp = u_e->next; + free(u_e); + u_e = tmp; + } + replace.hook_entry[i]->entries = NULL; + } + return; + } + + if (replace.hook_entry[replace.selected_hook]->nentries == 0) + exit(0); + oldnentries = replace.nentries; + replace.nentries = replace.nentries - + replace.hook_entry[replace.selected_hook]->nentries; + + // delete the counters belonging to the specified chain + if (replace.nentries) { + // +1 for CNT_END + if ( !(counterchanges = (unsigned short *) + malloc((oldnentries + 1) * sizeof(unsigned short))) ) + print_memory(); + cnt = counterchanges; + for (i = 0; i < NF_BR_NUMHOOKS; i++) { + if (!(replace.valid_hooks & (1 << i))) + continue; + for (j = 0; j < replace.hook_entry[i]->nentries; j++) { + if (i != replace.selected_hook) + *cnt = CNT_NORM; + else + *cnt = CNT_DEL; + cnt++; + } + } + *cnt = CNT_END; + replace.num_counters = oldnentries; + } + else + replace.num_counters = 0; + + replace.hook_entry[replace.selected_hook]->nentries = 0; + u_e = replace.hook_entry[replace.selected_hook]->entries; + while (u_e) { + free_u_entry(u_e); + tmp = u_e->next; + free(u_e); + u_e = tmp; + } + replace.hook_entry[replace.selected_hook]->entries = NULL; +} + +// -1 == no match +static int check_rule_exists(int rule_nr) +{ + struct ebt_u_entry *u_e; + struct ebt_u_match_list *m_l, *m_l2; + struct ebt_u_match *m; + struct ebt_u_watcher_list *w_l, *w_l2; + struct ebt_u_watcher *w; + struct ebt_u_target *t = (struct ebt_u_target *)new_entry->t; + int i, j, k; + + // handle '-D chain rulenr' command + if (rule_nr != -1) { + if (rule_nr > + replace.hook_entry[replace.selected_hook]->nentries) + return 0; + // user starts counting from 1 + return rule_nr - 1; + } + u_e = replace.hook_entry[replace.selected_hook]->entries; + // check for an existing rule (if there are duplicate rules, + // take the first occurance) + for (i = 0; i < replace.hook_entry[replace.selected_hook]->nentries; + i++, u_e = u_e->next) { + if (!u_e) + print_bug("Hmm, trouble"); + if ( u_e->ethproto == new_entry->ethproto + && !strcmp(u_e->in, new_entry->in) + && !strcmp(u_e->out, new_entry->out) + && u_e->bitmask == new_entry->bitmask) { + if (new_entry->bitmask & EBT_SOURCEMAC && + strcmp(u_e->sourcemac, new_entry->sourcemac)) + continue; + if (new_entry->bitmask & EBT_DESTMAC && + strcmp(u_e->destmac, new_entry->destmac)) + continue; + if (new_entry->bitmask != u_e->bitmask || + new_entry->invflags != u_e->invflags) + continue; + // compare all matches + m_l = new_entry->m_list; + j = 0; + while (m_l) { + m = (struct ebt_u_match *)(m_l->m); + m_l2 = u_e->m_list; + while (m_l2 && + strcmp(m_l2->m->u.name, m->m->u.name)) + m_l2 = m_l2->next; + if (!m_l2 || !m->compare(m->m, m_l2->m)) + goto letscontinue; + j++; + m_l = m_l->next; + } + // now be sure they have the same nr of matches + k = 0; + m_l = u_e->m_list; + while (m_l) { + k++; + m_l = m_l->next; + } + if (j != k) + continue; + + // compare all watchers + w_l = new_entry->w_list; + j = 0; + while (w_l) { + w = (struct ebt_u_watcher *)(w_l->w); + w_l2 = u_e->w_list; + while (w_l2 && + strcmp(w_l2->w->u.name, w->w->u.name)) + w_l2 = w_l2->next; + if (!w_l2 || !w->compare(w->w, w_l2->w)) + goto letscontinue; + j++; + w_l = w_l->next; + } + k = 0; + w_l = u_e->w_list; + while (w_l) { + k++; + w_l = w_l->next; + } + if (j != k) + continue; + if (strcmp(t->t->u.name, u_e->t->u.name)) + continue; + if (!t->compare(t->t, u_e->t)) + continue; + return i; + } +letscontinue: + } + return -1; +} + +// execute command A +static void add_rule(int rule_nr) +{ + int i, j; + struct ebt_u_entry *u_e, *u_e2; + unsigned short *cnt; + struct ebt_u_match_list *m_l; + struct ebt_u_watcher_list *w_l; + + if (rule_nr != -1) { // command -I + if (--rule_nr > + replace.hook_entry[replace.selected_hook]->nentries) + print_error("rule nr too high: %d > %d", rule_nr, + replace.hook_entry[replace.selected_hook]->nentries); + } else + rule_nr = replace.hook_entry[replace.selected_hook]->nentries; + // we're adding one rule + replace.num_counters = replace.nentries; + replace.nentries++; + replace.hook_entry[replace.selected_hook]->nentries++; + + // handle counter stuff + // +1 for CNT_END + if ( !(counterchanges = (unsigned short *) + malloc((replace.nentries + 1) * sizeof(unsigned short))) ) + print_memory(); + cnt = counterchanges; + for (i = 0; i < replace.selected_hook; i++) { + if (!(replace.valid_hooks & (1 << i))) + continue; + for (j = 0; j < replace.hook_entry[i]->nentries; j++) { + *cnt = CNT_NORM; + cnt++; + } + } + for (i = 0; i < rule_nr; i++) { + *cnt = CNT_NORM; + cnt++; + } + *cnt = CNT_ADD; + cnt++; + while (cnt != counterchanges + replace.nentries) { + *cnt = CNT_NORM; + cnt++; + } + *cnt = CNT_END; + + // go to the right position in the chain + u_e2 = NULL; + u_e = replace.hook_entry[replace.selected_hook]->entries; + for (i = 0; i < rule_nr; i++) { + u_e2 = u_e; + u_e = u_e->next; + } + // insert the rule + if (u_e2) + u_e2->next = new_entry; + else + replace.hook_entry[replace.selected_hook]->entries = new_entry; + new_entry->next = u_e; + + // put the ebt_[match, watcher, target] pointers in place + m_l = new_entry->m_list; + while (m_l) { + m_l->m = ((struct ebt_u_match *)m_l->m)->m; + m_l = m_l->next; + } + w_l = new_entry->w_list; + while (w_l) { + w_l->w = ((struct ebt_u_watcher *)w_l->w)->w; + w_l = w_l->next; + } + new_entry->t = ((struct ebt_u_target *)new_entry->t)->t; +} + +// execute command D +static void delete_rule(int rule_nr) +{ + int i, j, lentmp = 0; + unsigned short *cnt; + struct ebt_u_entry *u_e, *u_e2; + + if ( (i = check_rule_exists(rule_nr)) == -1 ) + print_error("Sorry, rule does not exists"); + + // we're deleting a rule + replace.num_counters = replace.nentries; + replace.nentries--; + + if (replace.nentries) { + for (j = 0; j < replace.selected_hook; j++) { + if (!(replace.valid_hooks & (1 << j))) + continue; + lentmp += replace.hook_entry[j]->nentries; + } + lentmp += i; + // +1 for CNT_END + if ( !(counterchanges = (unsigned short *)malloc( + (replace.num_counters + 1) * sizeof(unsigned short))) ) + print_memory(); + cnt = counterchanges; + for (j = 0; j < lentmp; j++) { + *cnt = CNT_NORM; + cnt++; + } + *cnt = CNT_DEL; + cnt++; + for (j = 0; j < replace.num_counters - lentmp; j++) { + *cnt = CNT_NORM; + cnt++; + } + *cnt = CNT_END; + } + else + replace.num_counters = 0; + + // go to the right position in the chain + u_e2 = NULL; + u_e = replace.hook_entry[replace.selected_hook]->entries; + for (j = 0; j < i; j++) { + u_e2 = u_e; + u_e = u_e->next; + } + + // remove from the chain + if (u_e2) + u_e2->next = u_e->next; + else + replace.hook_entry[replace.selected_hook]->entries = u_e->next; + + replace.hook_entry[replace.selected_hook]->nentries--; + // free everything + free_u_entry(u_e); + free(u_e); +} + +// execute command Z +void zero_counters(int zerochain) +{ + + if (zerochain == -1) { + // tell main() we don't update the counters + // this results in tricking the kernel to zero his counters, + // naively expecting userspace to update its counters. Muahahaha + counterchanges = NULL; + replace.num_counters = 0; + } else { + int i, j; + unsigned short *cnt; + + if (replace.hook_entry[zerochain]->nentries == 0) + exit(0); + counterchanges = (unsigned short *) + malloc((replace.nentries + 1) * sizeof(unsigned short)); + if (!counterchanges) + print_memory(); + cnt = counterchanges; + for (i = 0; i < zerochain; i++) { + if (!(replace.valid_hooks & (1 << i))) + continue; + for (j = 0; j < replace.hook_entry[i]->nentries; j++) { + *cnt = CNT_NORM; + cnt++; + } + } + for (i = 0; i < replace.hook_entry[zerochain]->nentries; i++) { + *cnt = CNT_ZERO; + cnt++; + } + while (cnt != counterchanges + replace.nentries) { + *cnt = CNT_NORM; + cnt++; + } + *cnt = CNT_END; + } +} + +// list the database (optionally compiled into the kernel) +static void list_db() +{ + struct brdb_dbinfo nr; + struct brdb_dbentry *db; + char name[21]; + int i; + + get_dbinfo(&nr); + + // 0 : database disabled (-db n) + if (!(nr.nentries)) + print_error("Database not present" + " (disabled), try ebtables --db y"); + nr.nentries--; + if (!nr.nentries) print_error("Database empty"); + if ( !(db = (struct brdb_dbentry *) + malloc(nr.nentries * sizeof(struct brdb_dbentry))) ) + print_memory(); + + get_db(nr.nentries, db); + printf("number of entries: %d\n", nr.nentries); + for (i = 0; i < nr.nentries; i++) { + printf( + "%d:\n" + "hook : %s\n" + "in-if : %s\n" + "out-if : %s\n" + "protocol: ", i + 1, hooknames[db->hook], db->in, db->out); + if (db->ethproto == IDENTIFY802_3) + printf("802.2/802.3 STYLE LENGTH FIELD\n"); + else { + if (number_to_name(ntohs(db->ethproto), name)) + printf("%x\n",ntohs(db->ethproto)); + else + printf("%s\n", name); + } + db++; + } + exit(0); +} + +// handle db [dis,en]abling +static void allowdb(char yorn) +{ + __u16 decision; + + if (yorn != 'y' && yorn != 'n') + print_error("Option [y] or [n] needed"); + + if (yorn == 'y') + decision = BRDB_DB; + else + decision = BRDB_NODB; + + deliver_allowdb(&decision); + + exit(0); +} + +// set ethproto +int name_to_protocol(char *name) +{ + FILE *ifp; + char buffer[21], value[5], *bfr; + unsigned short i; + + if (!strcasecmp("LENGTH", name)) { + new_entry->ethproto = 0; + new_entry->bitmask |= EBT_802_3; + return 1; + } + if ( !(ifp = fopen(PROTOCOLFILE, "r")) ) + return -1; + while (1) { + if (get_a_line(buffer, value, ifp)) return -1; + if (strcasecmp(buffer, name)) + continue; + i = (unsigned short) strtol(value, &bfr, 16); + if (*bfr != '\0') + return -1; + new_entry->ethproto = i; + fclose(ifp); + return 0; + } + return -1; +} + +// put the mac address into 6 (ETH_ALEN) bytes +int getmac(char *from, char *to) +{ + int i, tmp; + char *buffer; + + if (strlen(from) != 3 * ETH_ALEN - 1) + return -1; + for (i = 1; i < ETH_ALEN; i++) { + if (from[i*3 - 1] != ':') + return -1; + from[i*3 - 1] = '\0'; + } + for (i = 0; i < ETH_ALEN; i++) { + tmp = strtol(from + i*3, &buffer, 16); + if (*buffer != '\0' || tmp > 255 || tmp < 0) + return -1; + to[i] = (unsigned char) tmp; + } + return 0; +} + +int getmac_and_mask(char *from, char *to, char *mask) +{ + char *p; + int i; + + if (strcasecmp(from, "Unicast") == 0) { + memcpy(to, mac_type_unicast, ETH_ALEN); + memcpy(mask, msk_type_unicast, ETH_ALEN); + return 0; + } + if (strcasecmp(from, "Multicast") == 0) { + memcpy(to, mac_type_multicast, ETH_ALEN); + memcpy(mask, msk_type_multicast, ETH_ALEN); + return 0; + } + if (strcasecmp(from, "Broadcast") == 0) { + memcpy(to, mac_type_broadcast, ETH_ALEN); + memcpy(mask, msk_type_broadcast, ETH_ALEN); + return 0; + } + if ( (p = strrchr(from, '/')) != NULL) { + *p = '\0'; + if (getmac(p + 1, mask)) + return -1; + } else + memset(mask, 0xff, ETH_ALEN); + if (getmac(from, to)) + return -1; + for (i = 0; i < ETH_ALEN; i++) + to[i] &= mask[i]; + return 0; +} + +int check_inverse(const char option[]) +{ + if (strcmp(option, "!") == 0) { + optind++; + return 1; + } + return 0; +} + +void check_option(unsigned int *flags, unsigned int mask) +{ + if (*flags & mask) + print_error("Multiple use of same option not allowed"); + *flags |= mask; +} + +#define OPT_COMMAND 0x01 +#define OPT_TABLE 0x02 +#define OPT_IN 0x04 +#define OPT_OUT 0x08 +#define OPT_JUMP 0x10 +#define OPT_PROTOCOL 0x20 +#define OPT_SOURCE 0x40 +#define OPT_DEST 0x80 +#define OPT_ZERO 0x100 +#define OPT_LOGICALIN 0x200 +#define OPT_LOGICALOUT 0x400 +// the main thing +int main(int argc, char *argv[]) +{ + char *buffer, allowbc = 'n'; + int c, i; + // this special one for the -Z option (we can have -Z -L ) + int zerochain = -1; + int policy = -1; + int rule_nr = -1;// used for -D chain number + struct ebt_u_target *t; + struct ebt_u_match *m; + struct ebt_u_watcher *w; + struct ebt_u_match_list *m_l; + struct ebt_u_watcher_list *w_l; + + // initialize the table name, OPT_ flags, selected hook and command + strcpy(replace.name, "filter"); + replace.flags = 0; + replace.selected_hook = -1; + replace.command = 'h'; + + new_entry = (struct ebt_u_entry *)malloc(sizeof(struct ebt_u_entry)); + if (!new_entry) + print_memory(); + // put some sane values in our new entry + initialize_entry(new_entry); + + // getopt saves the day + while ((c = getopt_long(argc, argv, + "-A:D:I:L::Z::F::P:Vhi:o:j:p:b:s:d:t:", ebt_options, NULL)) != -1) { + switch (c) { + + case 'A': // add a rule + case 'D': // delete a rule + case 'P': // define policy + case 'I': // insert a rule + replace.command = c; + if (replace.flags & OPT_COMMAND) + print_error("Multiple commands not allowed"); + replace.flags |= OPT_COMMAND; + if ((replace.selected_hook = get_hooknr(optarg)) == -1) + print_error("Bad chain"); + if (c == 'D' && optind < argc && + argv[optind][0] != '-') { + rule_nr = strtol(argv[optind], &buffer, 10); + if (*buffer != '\0' || rule_nr < 0) + print_error("Problem with the " + "specified rule number"); + optind++; + } + if (c == 'P') { + if (optind >= argc) + print_error("No policy specified"); + for (i = 0; i < 2; i++) + if (!strcmp(argv[optind], + standard_targets[i])) { + policy = i; + break; + } + if (policy == -1) + print_error("Wrong policy"); + optind++; + } + if (c == 'I') { + if (optind >= argc) + print_error("No rulenr for -I" + " specified"); + rule_nr = strtol(argv[optind], &buffer, 10); + if (*buffer != '\0' || rule_nr < 0) + print_error("Problem with the specified" + " rule number"); + optind++; + } + break; + + case 'L': // list + case 'F': // flush + case 'Z': // zero counters + if (c == 'Z') { + if (replace.flags & OPT_ZERO) + print_error("Multiple commands" + " not allowed"); + if ( (replace.flags & OPT_COMMAND && + replace.command != 'L')) + print_error("command -Z only allowed " + "together with command -L"); + replace.flags |= OPT_ZERO; + } else { + replace.command = c; + if (replace.flags & OPT_COMMAND) + print_error("Multiple commands" + " not allowed"); + replace.flags |= OPT_COMMAND; + } + i = -1; + if (optarg) { + if ( (i = get_hooknr(optarg)) == -1 ) + print_error("Bad chain"); + } else + if (optind < argc && argv[optind][0] != '-') { + if ((i = get_hooknr(argv[optind])) + == -1) + print_error("Bad chain"); + optind++; + } + if (i != -1) { + if (c == 'Z') + zerochain = i; + else + replace.selected_hook = i; + } + break; + + case 'V': // version + replace.command = 'V'; + if (replace.flags & OPT_COMMAND) + print_error("Multiple commands not allowed"); + printf("%s, %s\n", prog_name, prog_version); + exit(0); + + case 'h': // help + if (replace.flags & OPT_COMMAND) + print_error("Multiple commands not allowed"); + replace.command = 'h'; + // All other arguments should be extension names + while (optind < argc) { + struct ebt_u_match *m; + struct ebt_u_watcher *w; + + if ((m = find_match(argv[optind]))) + add_match(m); + else if ((w = find_watcher(argv[optind]))) + add_watcher(w); + else { + if (!(t = find_target(argv[optind]))) + print_error("Extension %s " + "not found", argv[optind]); + if (replace.flags & OPT_JUMP) + print_error("Sorry, you can " + "only see help for one " + "target extension each time"); + replace.flags |= OPT_JUMP; + new_entry->t = + (struct ebt_entry_target *)t; + } + optind++; + } + break; + + case 't': // table + check_option(&replace.flags, OPT_TABLE); + if (strlen(optarg) > EBT_TABLE_MAXNAMELEN) + print_error("Table name too long"); + strcpy(replace.name, optarg); + break; + + case 'i': // input interface + case 2 : // logical input interface + case 'o': // output interface + case 3 : // logical output interface + case 'j': // target + case 'p': // net family protocol + case 's': // source mac + case 'd': // destination mac + if ((replace.flags & OPT_COMMAND) == 0) + print_error("No command specified"); + if ( replace.command != 'A' && + replace.command != 'D' && replace.command != 'I') + print_error("Command and option do not match"); + if (c == 'i') { + check_option(&replace.flags, OPT_IN); + if (replace.selected_hook > 2 && + replace.selected_hook < NF_BR_BROUTING) + print_error("Use in-interface only in " + "INPUT, FORWARD, PREROUTING and" + "BROUTING chains"); + if (check_inverse(optarg)) + new_entry->invflags |= EBT_IIN; + + if (optind > argc) + print_error("No in-interface " + "specified"); + if (strlen(argv[optind - 1]) >= IFNAMSIZ) + print_error("Illegal interfacelength"); + strcpy(new_entry->in, argv[optind - 1]); + break; + } + if (c == 2) { + check_option(&replace.flags, OPT_LOGICALIN); + if (replace.selected_hook > 2 && + replace.selected_hook < NF_BR_BROUTING) + print_error("Use logical in-interface " + "only in INPUT, FORWARD, " + "PREROUTING and BROUTING chains"); + if (check_inverse(optarg)) + new_entry->invflags |= EBT_ILOGICALIN; + + if (optind > argc) + print_error("No logical in-interface " + "specified"); + if (strlen(argv[optind - 1]) >= IFNAMSIZ) + print_error("Illegal interfacelength"); + strcpy(new_entry->logical_in, argv[optind - 1]); + break; + } + if (c == 'o') { + check_option(&replace.flags, OPT_OUT); + if (replace.selected_hook < 2) + print_error("Use out-interface only" + " in OUTPUT, FORWARD and " + "POSTROUTING chains"); + if (check_inverse(optarg)) + new_entry->invflags |= EBT_IOUT; + + if (optind > argc) + print_error("No out-interface " + "specified"); + + if (strlen(argv[optind - 1]) >= IFNAMSIZ) + print_error("Illegal interface " + "length"); + strcpy(new_entry->out, argv[optind - 1]); + break; + } + if (c == 3) { + check_option(&replace.flags, OPT_LOGICALOUT); + if (replace.selected_hook < 2) + print_error("Use logical out-interface " + "only in OUTPUT, FORWARD and " + "POSTROUTING chains"); + if (check_inverse(optarg)) + new_entry->invflags |= EBT_ILOGICALOUT; + + if (optind > argc) + print_error("No logical out-interface " + "specified"); + + if (strlen(argv[optind - 1]) >= IFNAMSIZ) + print_error("Illegal interface " + "length"); + strcpy(new_entry->logical_out, + argv[optind - 1]); + break; + } + if (c == 'j') { + + check_option(&replace.flags, OPT_JUMP); + for (i = 0; i < NUM_STANDARD_TARGETS; i++) + if (!strcmp(optarg, + standard_targets[i])) { + t = find_target( + EBT_STANDARD_TARGET); + ((struct ebt_standard_target *) + t->t)->verdict = i; + break; + } + // must be an extension then + if (i == NUM_STANDARD_TARGETS) { + struct ebt_u_target *t; + t = find_target(optarg); + // -j standard not allowed either + if (!t || t == + (struct ebt_u_target *)new_entry->t) + print_error("Illegal target " + "name"); + new_entry->t = + (struct ebt_entry_target *)t; + } + break; + } + if (c == 's') { + check_option(&replace.flags, OPT_SOURCE); + if (check_inverse(optarg)) + new_entry->invflags |= EBT_ISOURCE; + + if (optind > argc) + print_error("No source mac " + "specified"); + if (getmac_and_mask(argv[optind - 1], + new_entry->sourcemac, new_entry->sourcemsk)) + print_error("Problem with specified " + "source mac"); + new_entry->bitmask |= EBT_SOURCEMAC; + break; + } + if (c == 'd') { + check_option(&replace.flags, OPT_DEST); + if (check_inverse(optarg)) + new_entry->invflags |= EBT_IDEST; + + if (optind > argc) + print_error("No destination mac " + "specified"); + if (getmac_and_mask(argv[optind - 1], + new_entry->destmac, new_entry->destmsk)) + print_error("Problem with specified " + "destination mac"); + new_entry->bitmask |= EBT_DESTMAC; + break; + } + check_option(&replace.flags, OPT_PROTOCOL); + if (check_inverse(optarg)) + new_entry->invflags |= EBT_IPROTO; + + if (optind > argc) + print_error("No protocol specified"); + new_entry->bitmask &= ~((unsigned int)EBT_NOPROTO); + i = strtol(argv[optind - 1], &buffer, 16); + if (*buffer == '\0' && (i < 0 || i > 0xFFFF)) + print_error("Problem with the specified " + "protocol"); + new_entry->ethproto = i; + if (*buffer != '\0') + if (name_to_protocol(argv[optind - 1]) == -1) + print_error("Problem with the specified" + " protocol"); + if (new_entry->ethproto < 1536 && + !(new_entry->bitmask & EBT_802_3)) + print_error("Sorry, protocols have values above" + " or equal to 1536 (0x0600)"); + break; + + case 'b': // allow database? + if (replace.flags & OPT_COMMAND) + print_error("Multiple commands not allowed"); + replace.command = c; + allowbc = *optarg; + break; + + default: + + // is it a target option? + t = (struct ebt_u_target *)new_entry->t; + if ((t->parse(c - t->option_offset, argv, argc, + new_entry, &t->flags, &t->t))) + continue; + + // is it a match_option? + for (m = matches; m; m = m->next) + if (m->parse(c - m->option_offset, argv, + argc, new_entry, &m->flags, &m->m)) + break; + + if (m != NULL) { + if (m->used == 0) + add_match(m); + continue; + } + + // is it a watcher option? + for (w = watchers; w; w = w->next) + if (w->parse(c-w->option_offset, argv, + argc, new_entry, &w->flags, &w->w)) + break; + + if (w == NULL) + print_error("Unknown argument"); + if (w->used == 0) + add_watcher(w); + } + } + + // database stuff before ebtables stuff + if (replace.command == 'b') + allowdb(allowbc); + if (replace.command == 'L' && replace.selected_hook == DATABASEHOOKNR) + list_db(); + + if ( (replace.flags & OPT_COMMAND) && replace.command != 'L' && + replace.flags & OPT_ZERO ) + print_error("Command -Z only allowed together with command -L"); + + if (replace.command == 'A' || replace.command == 'I' || + replace.command == 'D') { + if (replace.selected_hook == -1) + print_error("Not enough information"); + } + + if ( !(table = find_table(replace.name)) ) + print_error("Bad table name"); + + // do this after parsing everything, so we can print specific info + if (replace.command == 'h' && !(replace.flags & OPT_ZERO)) + print_help(); + + // do the final checks + m_l = new_entry->m_list; + w_l = new_entry->w_list; + t = (struct ebt_u_target *)new_entry->t; + while (m_l) { + m = (struct ebt_u_match *)(m_l->m); + m->final_check(new_entry, m->m, replace.name, + replace.selected_hook); + m_l = m_l->next; + } + while (w_l) { + w = (struct ebt_u_watcher *)(w_l->w); + w->final_check(new_entry, w->w, replace.name, + replace.selected_hook); + w_l = w_l->next; + } + t->final_check(new_entry, t->t, replace.name, replace.selected_hook); + + // so, the extensions can work with the host endian + // the kernel does not have to do this ofcourse + new_entry->ethproto = htons(new_entry->ethproto); + + // get the kernel's information + get_table(&replace); + // check if selected_hook is a valid_hook + if (replace.selected_hook >= 0 && + !(replace.valid_hooks & (1 << replace.selected_hook))) + print_error("Bad chain name"); + if (replace.command == 'P') + change_policy(policy); + else if (replace.command == 'L') { + list_rules(); + if (replace.flags & OPT_ZERO) + zero_counters(zerochain); + else + exit(0); + } + if (replace.flags & OPT_ZERO) + zero_counters(zerochain); + else if (replace.command == 'F') + flush_chains(); + else if (replace.command == 'A' || replace.command == 'I') + add_rule(rule_nr); + else if (replace.command == 'D') + delete_rule(rule_nr); + + if (table->check) + table->check(&replace); + + deliver_table(&replace); + + if (counterchanges) + deliver_counters(&replace, counterchanges); + return 0; +} -- cgit v1.2.3