linkd

Control plane daemon for unos
git clone git://git.finwo.net/app/linkd
Log | Files | Refs | README

ifaces.c (17472B)


      1 #include <stdio.h>
      2 #include <stdlib.h>
      3 #include <string.h>
      4 #include <strings.h>
      5 
      6 #include "finwo/cnfparse.h"
      7 
      8 #include "util/config.h"
      9 
     10 #include "ifaces.h"
     11 
     12 static struct linkd_iface *ifaces = NULL;
     13 static struct linkd_iface *ifaces_tail = NULL;
     14 
     15 // Allocate an iface with every "unset" field at its sentinel.
     16 //
     17 // calloc() zeroes, which is the correct "unset" for pointers and for counters
     18 // like mtu, but WRONG for any field whose valid range includes 0. Those use -1
     19 // as the sentinel and must be set explicitly.
     20 //
     21 // This exists because bridge_vlan_aware was documented as "-1 unset" but never
     22 // initialised, so it sat at 0 and the `bridge_vlan_aware >= 0` test classified
     23 // EVERY interface as a bridge. It was masked only because creating a bridge on
     24 // an existing device fails harmlessly. Centralising the sentinels here means a
     25 // new tri-state field cannot repeat that by being forgotten at one of the two
     26 // allocation sites.
     27 static struct linkd_iface * iface_new(const char *name) {
     28   struct linkd_iface *ifc = calloc(1, sizeof(*ifc));
     29   if (!ifc) return NULL;
     30   ifc->name              = name ? strdup(name) : NULL;
     31   ifc->vlan_id           = -1;
     32   ifc->bridge_vlan_aware = -1;
     33   ifc->vrf_table         = -1;
     34   return ifc;
     35 }
     36 
     37 // Is this interface a bridge?
     38 //
     39 // Single source of truth: this was duplicated in cli/linkd.c and ipc.c, which
     40 // is how the two could drift. Explicit bridge-* directives win; the name
     41 // prefix is only a fallback, and never applies to something already declared
     42 // a VLAN or enslaved to a VRF.
     43 bool iface_is_bridge(const struct linkd_iface *ifc) {
     44   if (!ifc) return false;
     45   if (ifc->bridge_ports) return true;
     46   if (ifc->bridge_stp) return true;
     47   if (ifc->bridge_vlan_aware >= 0) return true;
     48   if (ifc->vlan_id >= 0) return false;
     49   if (ifc->vrf_table >= 0) return false;
     50   return !strncmp(ifc->name, "br", 2);
     51 }
     52 
     53 // Is this interface a VRF? `vrf-table` makes it one; `vrf <name>` makes it a
     54 // MEMBER of one, which is a different thing entirely.
     55 bool iface_is_vrf(const struct linkd_iface *ifc) {
     56   return ifc && ifc->vrf_table >= 0;
     57 }
     58 
     59 // NULL method is `static`: `iface <name>` without `inet` is the preferred
     60 // spelling, so it must not read as an unset method.
     61 bool iface_method_is(const struct linkd_iface *ifc, const char *method) {
     62   if (!ifc) return false;
     63   if (!ifc->method) return !strcasecmp(method, "static");
     64   return !strcasecmp(ifc->method, method);
     65 }
     66 
     67 // `vrf-table auto`: lowest free id at or above VRF_TABLE_AUTO_BASE.
     68 //
     69 // 1001 is not arbitrary -- it is where Cumulus starts, and the S5248F-ON we
     70 // captured has its `mgmt` VRF on exactly table 1001 (PROGRESS.md 2.1). Matching
     71 // that means a config moved off a Cumulus box lands on the same tables.
     72 // Deliberately skips 253/254/255 by construction, and skips any id already
     73 // claimed explicitly, so `auto` and a hardcoded id cannot collide.
     74 #define VRF_TABLE_AUTO_BASE 1001
     75 static int vrf_table_auto(const char *name) {
     76   struct linkd_iface *cur;
     77   int candidate;
     78   for (candidate = VRF_TABLE_AUTO_BASE; candidate < 2147483647; candidate++) {
     79     int taken = 0;
     80     for (cur = ifaces; cur; cur = cur->next) {
     81       // An interface re-parsing its own stanza must keep its table.
     82       if (name && cur->name && !strcmp(cur->name, name)) continue;
     83       if (cur->vrf_table == candidate) { taken = 1; break; }
     84     }
     85     if (!taken) return candidate;
     86   }
     87   return -1;
     88 }
     89 
     90 struct linkd_iface * ifaces_list(void) {
     91   return ifaces;
     92 }
     93 
     94 struct linkd_iface * ifaces_find(const char *name) {
     95   struct linkd_iface *cur;
     96   for (cur = ifaces; cur; cur = cur->next) {
     97     if (!strcmp(cur->name, name)) return cur;
     98   }
     99   return NULL;
    100 }
    101 
    102 void ifaces_set_auto(const char *name, int flag) {
    103   struct linkd_iface *cur = ifaces_find(name);
    104   if (cur) {
    105     cur->auto_flag = flag;
    106     return;
    107   }
    108   // Create placeholder for auto without iface block yet (common in Debian: auto comes before iface)
    109   cur = iface_new(name);
    110   cur->auto_flag = flag;
    111   if (ifaces_tail) {
    112     ifaces_tail->next = cur;
    113   } else {
    114     ifaces = cur;
    115   }
    116   ifaces_tail = cur;
    117 }
    118 
    119 void ifaces_free(void) {
    120   struct linkd_iface *cur = ifaces;
    121   struct linkd_iface *next;
    122   struct iface_addr *a, *anext;
    123   while (cur) {
    124     next = cur->next;
    125     free(cur->name);
    126     free(cur->method);
    127     free(cur->family);
    128     free(cur->gateway);
    129     free(cur->broadcast);
    130     free(cur->hwaddress);
    131     free(cur->vlan_raw_device);
    132     free(cur->pre_up);
    133     free(cur->post_up);
    134     free(cur->pre_down);
    135     free(cur->post_down);
    136     free(cur->bridge_ports);
    137     free(cur->bridge_stp);
    138     free(cur->vrf_master);
    139     for (a = cur->addrs; a; a = anext) {
    140       anext = a->next;
    141       free(a->address);
    142       free(a->netmask);
    143       free(a);
    144     }
    145     free(cur);
    146     cur = next;
    147   }
    148   ifaces = NULL;
    149   ifaces_tail = NULL;
    150 }
    151 
    152 static void iface_add_addr(struct linkd_iface *iface, const char *addr, const char *netmask) {
    153   struct iface_addr *a = calloc(1, sizeof(*a));
    154   a->address = strdup(addr);
    155   if (netmask) a->netmask = strdup(netmask);
    156   a->next = NULL;
    157   // append
    158   if (!iface->addrs) {
    159     iface->addrs = a;
    160   } else {
    161     struct iface_addr *cur = iface->addrs;
    162     while (cur->next) cur = cur->next;
    163     cur->next = a;
    164   }
    165 }
    166 
    167 // auto <if> [<if> ...]
    168 struct cnf_directive * cfg_parse_auto(FILE *fd, struct cnf_directive *dir, void *user) {
    169   (void)fd; (void)user;
    170   int i;
    171   for (i = 0; i < (int)dir->argc; i++) {
    172     ifaces_set_auto(dir->argv[i], 1);
    173   }
    174   cnf_directive_free(dir);
    175   return NULL;
    176 }
    177 
    178 // iface <name> [inet|inet6 <method>] -- cumulus style `iface <name>` preferred (no inet)
    179 struct cnf_directive * cfg_parse_iface(FILE *fd, struct cnf_directive *dir, void *user) {
    180   struct linkd_iface *iface;
    181   (void)user;
    182 
    183   if (dir->argc < 1 || dir->argc > 3) {
    184     cfg_error("`iface` directive: expected `iface <name>` or `iface <name> inet <method>`");
    185     goto fail;
    186   }
    187 
    188   const char *name = dir->argv[0];
    189   const char *family = NULL;
    190   const char *method = NULL;
    191 
    192   if (dir->argc == 1) {
    193     // Cumulus style: iface <name> -- defaults to static, no family
    194     method = "static";
    195   } else if (dir->argc == 3) {
    196     // Traditional: iface <name> inet static
    197     family = dir->argv[1];
    198     method = dir->argv[2];
    199     if (strcasecmp(family, "inet") && strcasecmp(family, "inet6")) {
    200       cfg_error("`iface %s` unknown family: %s (expected inet/inet6)", name, family);
    201       goto fail;
    202     }
    203   } else {
    204     cfg_error("`iface` directive: expected `iface <name>` or `iface <name> inet <method>`");
    205     goto fail;
    206   }
    207 
    208   // Find existing placeholder from auto, or create new
    209   iface = ifaces_find(name);
    210   int was_placeholder = 0;
    211   if (iface && !iface->method && !iface->family && !iface->addrs) {
    212     // placeholder from auto -- reuse
    213     was_placeholder = 1;
    214   } else if (iface) {
    215     // Duplicate iface block -- for now we merge? Cumulus allows multiple iface stanzas for same if
    216     // but we treat as error to keep simple, unless it's same name with different family
    217     // Allow merging: if family/method differ, we keep as separate? For now, error on exact duplicate
    218     // Check if same iface already has addrs/method -- if so, create new entry with same name but as separate node
    219     // To keep diff-apply simple, we allow multiple entries with same name as separate list nodes
    220     // So we will create a new node even if name exists, unless it was placeholder
    221     was_placeholder = 0;
    222   }
    223 
    224   if (iface && was_placeholder) {
    225     // reuse placeholder
    226     free(iface->method);
    227     free(iface->family);
    228     iface->method = method ? strdup(method) : NULL;
    229     iface->family = family ? strdup(family) : NULL;
    230     // keep auto_flag
    231   } else {
    232     // create new
    233     iface = iface_new(name);
    234     iface->method = method ? strdup(method) : NULL;
    235     iface->family = family ? strdup(family) : NULL;
    236     // check if auto was previously set for this name via placeholder? Already handled, but if not placeholder, check if any placeholder had auto
    237     // Actually ifaces_set_auto may have created placeholder with auto, we already handled.
    238     // If no placeholder, auto_flag stays 0 until auto directive is parsed (order may be auto before or after iface)
    239     // So we need to check if there was a prior auto for this name that wasn't yet linked -- but our placeholder logic already covers.
    240     if (ifaces_tail) {
    241       ifaces_tail->next = iface;
    242     } else {
    243       ifaces = iface;
    244     }
    245     ifaces_tail = iface;
    246   }
    247 
    248   // Handle vlan dot notation: swp1.100 -> raw_device swp1, vlan_id 100
    249   char *dot = strrchr(iface->name, '.');
    250   if (dot && !iface->vlan_raw_device) {
    251     char *end = NULL;
    252     long vid = strtol(dot+1, &end, 10);
    253     if (end != dot+1 && *end == '\0' && vid >= 1 && vid <= 4094) {
    254       iface->vlan_id = (int)vid;
    255       // Only set raw_device if not already set via explicit vlan-raw-device
    256       // Keep as is, will be resolved later if needed
    257     }
    258   }
    259 
    260   for(;;) {
    261     if (dir) cnf_directive_free(dir);
    262     dir = cnf_directive_read(fd);
    263     if (!dir) break;
    264 
    265     if(0) {}
    266     else if (!strcasecmp("address", dir->name)) {
    267       if (dir->argc < 1 || dir->argc > 2) {
    268         cfg_error("`address` expects 1 or 2 args (address + optional netmask)");
    269         goto fail;
    270       }
    271       const char *addr = dir->argv[0];
    272       const char *nm = dir->argc > 1 ? dir->argv[1] : NULL;
    273       iface_add_addr(iface, addr, nm);
    274     }
    275     else if (!strcasecmp("netmask", dir->name)) {
    276       if (dir->argc != 1) {
    277         cfg_error("`netmask` expects 1 arg");
    278         goto fail;
    279       }
    280       // Attach netmask to last address without netmask
    281       struct iface_addr *a = iface->addrs;
    282       if (!a) {
    283         cfg_error("`netmask` without preceding `address`");
    284         goto fail;
    285       }
    286       while (a->next) a = a->next;
    287       free(a->netmask);
    288       a->netmask = strdup(dir->argv[0]);
    289     }
    290     else if (!strcasecmp("broadcast", dir->name)) {
    291       if (dir->argc != 1) { cfg_error("`broadcast` expects 1 arg"); goto fail; }
    292       free(iface->broadcast);
    293       iface->broadcast = strdup(dir->argv[0]);
    294     }
    295     else if (!strcasecmp("gateway", dir->name)) {
    296       if (dir->argc != 1) { cfg_error("`gateway` expects 1 arg"); goto fail; }
    297       free(iface->gateway);
    298       iface->gateway = strdup(dir->argv[0]);
    299     }
    300     else if (!strcasecmp("mtu", dir->name)) {
    301       if (dir->argc != 1) { cfg_error("`mtu` expects 1 arg"); goto fail; }
    302       iface->mtu = atoi(dir->argv[0]);
    303     }
    304     else if (!strcasecmp("hwaddress", dir->name) || !strcasecmp("hw-address", dir->name)) {
    305       if (dir->argc != 1) { cfg_error("`hwaddress` expects 1 arg"); goto fail; }
    306       free(iface->hwaddress);
    307       iface->hwaddress = strdup(dir->argv[0]);
    308     }
    309     else if (!strcasecmp("vlan-raw-device", dir->name) || !strcasecmp("vlan_raw_device", dir->name)) {
    310       if (dir->argc != 1) { cfg_error("`vlan-raw-device` expects 1 arg"); goto fail; }
    311       free(iface->vlan_raw_device);
    312       iface->vlan_raw_device = strdup(dir->argv[0]);
    313     }
    314     else if (!strcasecmp("vlan-id", dir->name) || !strcasecmp("vlan_id", dir->name)) {
    315       if (dir->argc != 1) { cfg_error("`vlan-id` expects 1 arg"); goto fail; }
    316       iface->vlan_id = atoi(dir->argv[0]);
    317     }
    318     else if (!strcasecmp("pre-up", dir->name) || !strcasecmp("pre_up", dir->name)) {
    319       if (dir->argc < 1) { cfg_error("`pre-up` expects at least 1 arg"); goto fail; }
    320       // Join argv with spaces
    321       size_t len = 0;
    322       for (int i=0;i<(int)dir->argc;i++) len += strlen(dir->argv[i])+1;
    323       char *cmd = calloc(1, len+1);
    324       for (int i=0;i<(int)dir->argc;i++) {
    325         strcat(cmd, dir->argv[i]);
    326         if (i+1 < (int)dir->argc) strcat(cmd, " ");
    327       }
    328       free(iface->pre_up);
    329       iface->pre_up = cmd;
    330     }
    331     else if (!strcasecmp("post-up", dir->name) || !strcasecmp("post_up", dir->name)) {
    332       if (dir->argc < 1) { cfg_error("`post-up` expects at least 1 arg"); goto fail; }
    333       size_t len = 0;
    334       for (int i=0;i<(int)dir->argc;i++) len += strlen(dir->argv[i])+1;
    335       char *cmd = calloc(1, len+1);
    336       for (int i=0;i<(int)dir->argc;i++) {
    337         strcat(cmd, dir->argv[i]);
    338         if (i+1 < (int)dir->argc) strcat(cmd, " ");
    339       }
    340       free(iface->post_up);
    341       iface->post_up = cmd;
    342     }
    343     else if (!strcasecmp("pre-down", dir->name) || !strcasecmp("pre_down", dir->name)) {
    344       if (dir->argc < 1) { cfg_error("`pre-down` expects at least 1 arg"); goto fail; }
    345       size_t len = 0;
    346       for (int i=0;i<(int)dir->argc;i++) len += strlen(dir->argv[i])+1;
    347       char *cmd = calloc(1, len+1);
    348       for (int i=0;i<(int)dir->argc;i++) {
    349         strcat(cmd, dir->argv[i]);
    350         if (i+1 < (int)dir->argc) strcat(cmd, " ");
    351       }
    352       free(iface->pre_down);
    353       iface->pre_down = cmd;
    354     }
    355     else if (!strcasecmp("post-down", dir->name) || !strcasecmp("post_down", dir->name)) {
    356       if (dir->argc < 1) { cfg_error("`post-down` expects at least 1 arg"); goto fail; }
    357       size_t len = 0;
    358       for (int i=0;i<(int)dir->argc;i++) len += strlen(dir->argv[i])+1;
    359       char *cmd = calloc(1, len+1);
    360       for (int i=0;i<(int)dir->argc;i++) {
    361         strcat(cmd, dir->argv[i]);
    362         if (i+1 < (int)dir->argc) strcat(cmd, " ");
    363       }
    364       free(iface->post_down);
    365       iface->post_down = cmd;
    366     }
    367     else if (!strcasecmp("up", dir->name)) {
    368       // alias for post-up
    369       if (dir->argc < 1) { cfg_error("`up` expects at least 1 arg"); goto fail; }
    370       size_t len = 0;
    371       for (int i=0;i<(int)dir->argc;i++) len += strlen(dir->argv[i])+1;
    372       char *cmd = calloc(1, len+1);
    373       for (int i=0;i<(int)dir->argc;i++) {
    374         strcat(cmd, dir->argv[i]);
    375         if (i+1 < (int)dir->argc) strcat(cmd, " ");
    376       }
    377       free(iface->post_up);
    378       iface->post_up = cmd;
    379     }
    380     else if (!strcasecmp("down", dir->name)) {
    381       if (dir->argc < 1) { cfg_error("`down` expects at least 1 arg"); goto fail; }
    382       size_t len = 0;
    383       for (int i=0;i<(int)dir->argc;i++) len += strlen(dir->argv[i])+1;
    384       char *cmd = calloc(1, len+1);
    385       for (int i=0;i<(int)dir->argc;i++) {
    386         strcat(cmd, dir->argv[i]);
    387         if (i+1 < (int)dir->argc) strcat(cmd, " ");
    388       }
    389       free(iface->pre_down);
    390       iface->pre_down = cmd;
    391     }
    392     else if (!strcasecmp("bridge-ports", dir->name) || !strcasecmp("bridge_ports", dir->name)) {
    393       if (dir->argc < 1) { cfg_error("`bridge-ports` expects at least 1 arg"); goto fail; }
    394       size_t len = 0;
    395       for (int i=0;i<(int)dir->argc;i++) len += strlen(dir->argv[i])+1;
    396       char *cmd = calloc(1, len+1);
    397       for (int i=0;i<(int)dir->argc;i++) {
    398         strcat(cmd, dir->argv[i]);
    399         if (i+1 < (int)dir->argc) strcat(cmd, " ");
    400       }
    401       free(iface->bridge_ports);
    402       iface->bridge_ports = cmd;
    403     }
    404     else if (!strcasecmp("bridge-stp", dir->name) || !strcasecmp("bridge_stp", dir->name)) {
    405       if (dir->argc != 1) { cfg_error("`bridge-stp` expects 1 arg (on/off)"); goto fail; }
    406       free(iface->bridge_stp);
    407       iface->bridge_stp = strdup(dir->argv[0]);
    408     }
    409     else if (!strcasecmp("bridge-vlan-aware", dir->name) || !strcasecmp("bridge_vlan_aware", dir->name)) {
    410       if (dir->argc != 1) { cfg_error("`bridge-vlan-aware` expects 1 arg (yes/no)"); goto fail; }
    411       if (!strcasecmp(dir->argv[0], "yes") || !strcasecmp(dir->argv[0], "on") || !strcasecmp(dir->argv[0], "1")) iface->bridge_vlan_aware = 1;
    412       else if (!strcasecmp(dir->argv[0], "no") || !strcasecmp(dir->argv[0], "off") || !strcasecmp(dir->argv[0], "0")) iface->bridge_vlan_aware = 0;
    413       else { cfg_error("`bridge-vlan-aware` expects yes/no"); goto fail; }
    414     }
    415     // `vrf <name>`: enslave this interface to an existing VRF device.
    416     else if (!strcasecmp("vrf", dir->name)) {
    417       if (dir->argc != 1) { cfg_error("`vrf` expects 1 arg (vrf device name)"); goto fail; }
    418       free(iface->vrf_master);
    419       iface->vrf_master = strdup(dir->argv[0]);
    420     }
    421     // `vrf-table <id|auto>`: THIS interface is a VRF device with that table.
    422     // Cumulus spells the automatic case `auto`; we allocate from a private
    423     // range rather than colliding with main(254)/local(255)/default(253).
    424     else if (!strcasecmp("vrf-table", dir->name) || !strcasecmp("vrf_table", dir->name)) {
    425       if (dir->argc != 1) { cfg_error("`vrf-table` expects 1 arg (table id or `auto`)"); goto fail; }
    426       if (!strcasecmp(dir->argv[0], "auto")) {
    427         iface->vrf_table = vrf_table_auto(iface->name);
    428         if (iface->vrf_table < 0) {
    429           cfg_error("`vrf-table auto`: no free table id");
    430           goto fail;
    431         }
    432       } else {
    433         char *end = NULL;
    434         long v = strtol(dir->argv[0], &end, 10);
    435         if (!end || *end || v <= 0 || v > 2147483647L) {
    436           cfg_error("`vrf-table` expects a positive table id or `auto`, got `%s`", dir->argv[0]);
    437           goto fail;
    438         }
    439         if (v == 253 || v == 254 || v == 255) {
    440           cfg_error("`vrf-table` %ld is a reserved table (default/main/local)", v);
    441           goto fail;
    442         }
    443         iface->vrf_table = (int)v;
    444       }
    445     }
    446     else {
    447       break;
    448     }
    449   }
    450 
    451   return dir;
    452 
    453 // The iface is already linked into the module list by this point, so
    454 // ifaces_free() owns it; only the directive needs releasing here.
    455 fail:
    456   cnf_directive_free(dir);
    457   return NULL;
    458 }
    459 
    460 void ifaces_register(void) {
    461   struct cfg_ns *ns = cfg_ns_get("interfaces");
    462   cfg_register_directive(ns, "auto", cfg_parse_auto);
    463   cfg_register_directive(ns, "allow-auto", cfg_parse_auto);
    464   cfg_register_directive(ns, "allow-hotplug", cfg_parse_auto);
    465   cfg_register_directive(ns, "iface", cfg_parse_iface);
    466 }