linkd

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

ifaces.c (17140B)


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