Network Configuration Framework

Active Rust

Contents

Concept

Network Configuration Framework is a Rust CLI that compiles declarative YAML blueprints into vendor-neutral configuration artifacts. A single binary orchestrates the full pipeline: blueprint parsing, topology transformation, DeviceIR generation, template rendering, and traceable config file emission.

The core problem is determinism. Hand-written network configs drift between devices, between sites, between engineers. The Network Configuration Framework treats configuration as a compilation target: a blueprint declares intent (protocol layers, IP pools, policy constraints), and the compiler produces auditable, diffable output files suitable for CI/CD pipelines.

Blueprints are composed from importable fragments. A site blueprint imports protocol definitions (OSPFv3, BFD, VRRP) as separate YAML files, each declaring layer dependencies. The compiler resolves the import graph, orders layers by their requires: declarations, and executes primitives in sequence.


Technical Reports


Code Samples

01_isp_dual_stack_peering.yaml

version: 1
imports: []
layers:
- name: pools
  requires: []
  primitives:
  - type: global_pool
    name: fra-transit-v4
    pool: '198.51.100.0/24'
    vrf: null
  - type: global_pool
    name: fra-transit-v6
    pool: '2001:db8:fra::/48'
    vrf: null
  - type: global_pool
    name: fra-ce-v4
    pool: '203.0.113.0/24'
    vrf: null
  - type: global_pool
    name: fra-ce-v6
    pool: '2001:db8:ce::/48'
    vrf: null
  - type: global_resource_pool
    name: fra-private-asn
    resource_type: asn
    pool: '64512-64530'
- name: peering
  requires:
  - pools
  primitives:
  - type: mesh_nodes
    selector: $border_and_transit
    mesh_type: hub_and_spoke
    hub_selector: $borders
    spoke_selector: $transits
  - type: mesh_nodes
    selector: $border_and_ce
    mesh_type: hub_and_spoke
    hub_selector: $borders
    spoke_selector: $customer_edges
- name: addressing
  requires:
  - peering
  primitives:
  - type: provision_ips
    selector: $transit_links
    pool: fra-transit-v4
    subnet_size: 31
    ipv6_pool: fra-transit-v6
    ipv6_subnet_size: 127
    strategy: dense
    pool_size: null
    vrf: null
  - type: provision_ips
    selector: $ce_links
    pool: fra-ce-v4
    subnet_size: 30
    ipv6_pool: fra-ce-v6
    ipv6_subnet_size: 126
    strategy: dense
    pool_size: null
    vrf: null
  - type: allocate_resources
    selector: $customer_edges
    resource_type: asn
    pool: fra-private-asn
    strategy: dense
    pool_size: null
- name: secrets
  requires:
  - addressing
  primitives:
  - type: inject_secrets
    selector: $borders
    secrets:
      bgp_md5_transit: ${BGP_MD5_TRANSIT_KEY}
      bgp_md5_customer: ${BGP_MD5_CUSTOMER_KEY}
      tacacs_key: ${TACACS_SECRET}
- name: filters
  requires:
  - secrets
  primitives:
  - type: generate_safe_bgp_filters
    selector: $borders
    prefix_list_name: BOGON-FILTER-V4
    policy_name: TRANSIT-INBOUND
    block_rfc1918: true
    permit_default: false
- name: bgp
  requires:
  - filters
  primitives:
  - type: build_protocol_layer
    selector: $all_routers
    layer: bgp
    config:
      asn_base: '65100'
      protocol_type: bgp
    clone_underlying: true
assertions:
- name: border-routers-connected
  severity: error
  select: $borders
  check:
    type: is_connected
  help: A border router is isolated — check peering layer wiring.
- name: ce-reachable-from-border
  severity: error
  select: $customer_edges
  check:
    type: reachability
    target_selector: $borders
  help: A customer-edge router cannot reach any border router.
- name: transit-peering-bipartite
  severity: error
  select: $border_and_transit
  check:
    type: is_bipartite
  help: Transit peering graph is not bipartite — unexpected link between same-role nodes.
- name: links-have-ipv4
  severity: error
  select: edges[true]
  check:
    type: field_exists
    field: src_ip
  help: An edge is missing its IPv4 source address — check provision_ips.
- name: transit-ips-in-range
  severity: warning
  select: edges[link_type='transit']
  check:
    type: field_in_cidr
    field: src_ip
    cidr: '198.51.100.0/24'
  help: A transit link IP is outside the expected 198.51.100.0/24 range.
rules: {}
groups:
  transit_links: edges[link_type=='transit']
  all_routers: $borders | $transits | $customer_edges
  customer_edges: nodes[role=='ce']
  borders: nodes[role=='border']
  border_and_ce: $borders | $customer_edges
  border_and_transit: $borders | $transits
  ce_links: edges[link_type=='ce']
  transits: nodes[role=='transit']
blast_radius:
- name: limit-teardown
  select: null
  check:
    type: max_deletions
    count: 2
transforms:
- name: junos-interface-rewrite
  when: device_os == 'junos'
  rules:
  - match_expr: stanza.kind == 'interface'
    apply:
      name: '''xe-0/0/'' + string(stanza.fields.abstract_index)'
- name: junos-transit-apply-group
  when: device_os == 'junos'
  rules:
  - match_expr: stanza.kind == 'bgp_neighbor' and stanza.fields.description == 'transit'
    apply:
      apply_groups: '''TRANSIT-DEFAULTS'''
hardware_library:
  chassis: []
  linecards: []

02_campus_compliance_audit.yaml

version: 1
imports: []
layers:
- name: provisioning
  requires: []
  primitives:
  - type: conditional
    condition: topology.node_count >= 3
    then_primitives:
    - type: custom_primitive
      name: building-loopbacks
      parameters:
        pool_cidr: '10.250.0.0/16'
      primitives:
      - type: provision_ips
        selector: $all_switches
        pool: ''
        subnet_size: 32
        ipv6_pool: null
        ipv6_subnet_size: null
        strategy: dense
        pool_size: null
        vrf: null
    else_primitives: null
- name: management
  requires:
  - provisioning
  primitives:
  - type: build_access_policy
    selector: $dist_switches
    policy_name: MGMT-ACCESS
    rules:
    - name: allow-noc-ssh
      action: permit
      source_prefix: '10.0.0.0/8'
      destination_prefix: null
      protocol: tcp
      source_port: null
      destination_port: 22
    - name: allow-noc-https
      action: permit
      source_prefix: '10.0.0.0/8'
      destination_prefix: null
      protocol: tcp
      source_port: null
      destination_port: 443
    - name: deny-all-mgmt
      action: deny
      source_prefix: null
      destination_prefix: null
      protocol: null
      source_port: null
      destination_port: null
assertions:
- name: ntp-compliance
  severity: error
  select: $all_switches
  check:
    type: use_rule
    name: ntp-configured
  help: Node is missing NTP configuration — at least 2 NTP servers required.
- name: syslog-compliance
  severity: error
  select: $all_switches
  check:
    type: use_rule
    name: syslog-configured
  help: Node has no syslog server configured.
- name: management-vlan-audit
  severity: warning
  select: $access_switches
  check:
    type: use_rule
    name: management-vlan-correct
  help: Access switch management VLAN should be 99.
- name: distribution-inventory-schema
  severity: error
  select: $dist_switches
  check:
    type: match_schema
    schema:
      properties:
        firmware_version:
          type: string
        hostname:
          minLength: 1
          type: string
        serial_number:
          pattern: ^[A-Z0-9]{11}$
          type: string
        site:
          enum:
          - eng
          - sci
          - lib
          - admin
          - med
          - arts
          type: string
      required:
      - hostname
      - serial_number
      - firmware_version
      - site
      type: object
  help: Distribution switch is missing required inventory fields or has invalid data.
- name: hostnames-unique-per-building
  severity: error
  select: $all_switches
  check:
    type: unique_per_group
    field: hostname
    group_by: site
  help: Duplicate hostname detected within the same building.
- name: ibgp-acyclic
  severity: error
  select: nodes[protocol='ibgp']
  check:
    type: is_acyclic
  help: iBGP session graph contains a [cycle](../cycle) — check route-reflector hierarchy.
- name: access-dual-homed
  severity: error
  select: $access_switches
  check:
    type: min_edges
    count: 2
  help: Access switch has fewer than 2 uplinks — single point of failure.
- name: all-nodes-have-hostname
  severity: error
  select: nodes[true]
  check:
    type: field_exists
    field: hostname
  help: A node is missing its hostname field.
rules:
  management-vlan-correct: has(node.mgmt_vlan) and node.mgmt_vlan == 99
  syslog-configured: has(node.syslog_server) and node.syslog_server != ''
  ntp-configured: has(node.ntp_servers) and size(node.ntp_servers) >= 2
groups:
  all_switches: $access_switches | $dist_switches
  access_switches: nodes[role=='access']
  dist_switches: nodes[role=='distribution']
  campus_links: edges[link_type=='campus']
blast_radius:
- name: audit-change-cap
  select: null
  check:
    type: max_percentage_changed
    value: 10.0
transforms: []
hardware_library:
  chassis: []
  linecards: []

03_wan_migration.yaml

version: 1
imports: []
layers:
- name: pools
  requires: []
  primitives:
  - type: global_resource_pool
    name: isis-net-pool
    resource_type: isis_net
    pool: '49.0001.0000.0000.0001-49.0001.0000.0000.0200'
  - type: global_resource_pool
    name: sr-sid-pool
    resource_type: segment_id
    pool: '16001-16200'
- name: migration
  requires:
  - pools
  primitives:
  - type: conditional
    condition: topology.node_count < 10
    then_primitives:
    - type: allocate_resources
      selector: $london_pe
      resource_type: isis_net
      pool: isis-net-pool
      strategy: dense
      pool_size: null
    - type: allocate_resources
      selector: $london_pe
      resource_type: segment_id
      pool: sr-sid-pool
      strategy: dense
      pool_size: null
    else_primitives:
    - type: allocate_resources
      selector: $all_pe
      resource_type: isis_net
      pool: isis-net-pool
      strategy: dense
      pool_size: null
    - type: allocate_resources
      selector: $all_pe
      resource_type: segment_id
      pool: sr-sid-pool
      strategy: dense
      pool_size: null
- name: secrets
  requires:
  - migration
  primitives:
  - type: inject_secrets
    selector: nodes[city='london']
    secrets:
      isis_auth_key: ${ISIS_KEY_LONDON}
      tacacs_key: ${TACACS_KEY_EU}
  - type: inject_secrets
    selector: nodes[city='paris']
    secrets:
      isis_auth_key: ${ISIS_KEY_PARIS}
      tacacs_key: ${TACACS_KEY_EU}
  - type: inject_secrets
    selector: nodes[city='frankfurt']
    secrets:
      isis_auth_key: ${ISIS_KEY_FRANKFURT}
      tacacs_key: ${TACACS_KEY_EU}
- name: filters
  requires:
  - secrets
  primitives:
  - type: generate_safe_bgp_filters
    selector: $border_routers
    prefix_list_name: BOGON-INBOUND
    policy_name: EBGP-INBOUND-FILTER
    block_rfc1918: true
    permit_default: false
- name: routing-policy
  requires:
  - filters
  primitives:
  - type: build_routing_policy
    selector: $all_pe
    policy_name: MIGRATION-PREFER-ISIS
    statements:
    - name: prefer-isis-routes
      action: permit
      match_prefix_list: null
      match_community_list: ISIS-MIGRATED
      match_as_path: null
      set_local_preference: 200
      set_metric: null
      set_as_path_prepend: null
      set_community: null
      next_hop: null
    - name: depref-ospf-routes
      action: permit
      match_prefix_list: null
      match_community_list: OSPF-LEGACY
      match_as_path: null
      set_local_preference: 80
      set_metric: null
      set_as_path_prepend: null
      set_community: null
      next_hop: null
    - name: default-permit
      action: permit
      match_prefix_list: null
      match_community_list: null
      match_as_path: null
      set_local_preference: null
      set_metric: null
      set_as_path_prepend: null
      set_community: null
      next_hop: null
- name: isis-overlay
  requires:
  - routing-policy
  primitives:
  - type: build_protocol_layer
    selector: $pe_and_p
    layer: isis
    config:
      level: level-2
      protocol_type: isis
    clone_underlying: true
assertions:
- name: pe-reaches-p-via-isis
  severity: error
  select: $all_pe
  check:
    type: reachability
    target_selector: $p_routers
  help: A PE router cannot reach any P router — IS-IS adjacency may be down.
- name: pe-has-isis-net
  severity: error
  select: $all_pe
  check:
    type: field_exists
    field: isis_net
  help: PE is missing its IS-IS NET address — check allocate_resources.
- name: pe-has-sr-sid
  severity: error
  select: $all_pe
  check:
    type: field_exists
    field: segment_id
  help: PE is missing its SR node SID — check allocate_resources.
- name: border-reachable
  severity: error
  select: $border_routers
  check:
    type: reachability
    target_selector: $all_pe
  help: Border router is unreachable from PE — migration may have broken transit.
rules: {}
groups:
  p_routers: nodes[role=='p']
  london_pe: nodes[city=='london' and role=='pe']
  border_routers: nodes[role=='border']
  pe_and_p: $all_pe | $p_routers
  all_pe: nodes[role=='pe']
blast_radius:
- name: migration-change-limit
  select: null
  check:
    type: max_modifications
    count: 5
transforms:
- name: iosxr-interface-rewrite
  when: device_os == 'iosxr'
  rules:
  - match_expr: stanza.kind == 'interface'
    apply:
      name: '''GigabitEthernet0/0/0/'' + string(stanza.fields.abstract_index)'
- name: iosxr-sr-annotation
  when: device_os == 'iosxr'
  rules:
  - match_expr: stanza.kind == 'isis_neighbor'
    apply:
      metric: '10'
      sr_enabled: 'true'
hardware_library:
  chassis: []
  linecards: []

04_multi_tenant_dc.yaml

version: 1
imports: []
layers:
- name: fabric
  requires: []
  primitives:
  - type: mesh_nodes
    selector: $spines_and_leaves
    mesh_type: hub_and_spoke
    hub_selector: $spines
    spoke_selector: $leaves
- name: addressing
  requires:
  - fabric
  primitives:
  - type: provision_ips
    selector: $equities_links
    pool: '10.100.0.0/16'
    subnet_size: 30
    ipv6_pool: null
    ipv6_subnet_size: null
    strategy: dense
    pool_size: null
    vrf: equities
  - type: provision_ips
    selector: $fixed_income_links
    pool: '10.100.0.0/16'
    subnet_size: 30
    ipv6_pool: null
    ipv6_subnet_size: null
    strategy: dense
    pool_size: null
    vrf: fixed-income
  - type: provision_ips
    selector: $risk_links
    pool: '10.100.0.0/16'
    subnet_size: 30
    ipv6_pool: null
    ipv6_subnet_size: null
    strategy: dense
    pool_size: null
    vrf: risk
  - type: provision_ips
    selector: $fabric_links
    pool: '172.16.0.0/16'
    subnet_size: 31
    ipv6_pool: null
    ipv6_subnet_size: null
    strategy: dense
    pool_size: null
    vrf: null
- name: security
  requires:
  - addressing
  primitives:
  - type: build_access_policy
    selector: $spines
    policy_name: INTER-VRF-DENY
    rules:
    - name: deny-eq-to-fi
      action: deny
      source_prefix: '10.100.0.0/16'
      destination_prefix: '10.100.0.0/16'
      protocol: ip
      source_port: null
      destination_port: null
    - name: deny-eq-to-risk
      action: deny
      source_prefix: '10.100.0.0/16'
      destination_prefix: '10.100.0.0/16'
      protocol: ip
      source_port: null
      destination_port: null
    - name: permit-intra-vrf
      action: permit
      source_prefix: null
      destination_prefix: null
      protocol: null
      source_port: null
      destination_port: null
- name: overlay
  requires:
  - security
  primitives:
  - type: build_protocol_layer
    selector: $spines_and_leaves
    layer: bgp
    config:
      asn_base: '65200'
      protocol_type: bgp
    clone_underlying: true
assertions:
- name: every-leaf-has-tenant
  severity: error
  select: $leaves
  check:
    type: use_rule
    name: tenant-assigned
  help: Leaf switch is missing tenant assignment.
- name: every-leaf-has-vrf
  severity: error
  select: $leaves
  check:
    type: use_rule
    name: vrf-configured
  help: Leaf switch has no VRF configured.
- name: every-leaf-has-vlan
  severity: warning
  select: $leaves
  check:
    type: use_rule
    name: leaf-has-vlan
  help: Leaf switch is missing VLAN ID — check tenant provisioning.
- name: tenant-metadata-schema
  severity: error
  select: $leaves
  check:
    type: match_schema
    schema:
      properties:
        hostname:
          minLength: 1
          type: string
        tenant:
          enum:
          - equities
          - fixed-income
          - risk
          type: string
        vlan_id:
          maximum: 4094
          minimum: 1
          type: integer
        vrf_name:
          type: string
      required:
      - hostname
      - tenant
      - vrf_name
      - vlan_id
      type: object
  help: Leaf node data does not match expected tenant metadata schema.
- name: vlans-unique-per-tenant
  severity: error
  select: $leaves
  check:
    type: unique_per_group
    field: vlan_id
    group_by: tenant
  help: Duplicate VLAN ID detected within the same tenant.
- name: equities-connected
  severity: error
  select: $equities_or_spines
  check:
    type: is_connected
  help: Equities tenant subgraph is disconnected — a leaf is unreachable.
- name: fixed-income-connected
  severity: error
  select: $fixed_income_or_spines
  check:
    type: is_connected
  help: Fixed-income tenant subgraph is disconnected — a leaf is unreachable.
- name: risk-connected
  severity: error
  select: $risk_or_spines
  check:
    type: is_connected
  help: Risk tenant subgraph is disconnected — a leaf is unreachable.
rules:
  vrf-configured: has(node.vrf_name) and node.vrf_name != ''
  leaf-has-vlan: has(node.vlan_id) and node.vlan_id > 0 and node.vlan_id < 4095
  tenant-assigned: has(node.tenant) and node.tenant != ''
groups:
  risk_or_spines: nodes[tenant=='risk'] | $spines
  spines_and_leaves: $spines | $leaves
  equities_links: edges[tenant=='equities']
  risk_links: edges[tenant=='risk']
  fixed_income_links: edges[tenant=='fixed-income']
  equities_or_spines: nodes[tenant=='equities'] | $spines
  spines: nodes[role=='spine']
  fabric_links: edges[link_type=='fabric']
  fixed_income_or_spines: nodes[tenant=='fixed-income'] | $spines
  leaves: nodes[role=='leaf']
blast_radius:
- name: tenant-vrf-protection
  select: null
  check:
    type: max_deletions
    count: 3
- name: fabric-change-cap
  select: null
  check:
    type: max_percentage_changed
    value: 25.0
transforms:
- name: nxos-interface-rewrite
  when: device_os == 'nxos'
  rules:
  - match_expr: stanza.kind == 'interface'
    apply:
      name: '''Ethernet1/'' + string(stanza.fields.abstract_index + 1)'
- name: nxos-vrf-context
  when: device_os == 'nxos'
  rules:
  - match_expr: stanza.kind == 'bgp_neighbor' and has(stanza.fields.vrf)
    apply:
      nxos_vrf_context: stanza.fields.vrf
hardware_library:
  chassis: []
  linecards: []

05_sp_mpls_core.yaml

# Case Study 05 — Service Provider MPLS Core
#
# Models a Tier-2 ISP backbone running IS-IS as the IGP with Segment
# Routing (SR-MPLS) replacing LDP for label distribution.  BFD provides
# sub-second failure detection on all core links.
#
# Topology:
#   3 Provider Edge (PE) routers — customer-facing, originate L3VPN services
#   2 Provider (P) routers — transit core, no customer interfaces
#   PE1–PE3 connect to P1 and P2 in a partial mesh (hub-and-spoke to core)
#   P1–P2 are fully meshed between themselves
#
# Design choices:
#   - IS-IS Level-2 only (flat backbone, no area hierarchy)
#   - SR-MPLS with SRGB 16000–23999; each PE gets a unique Node SID
#   - BFD with 100 ms timers for fast convergence
#   - TACACS+ for device administration with local fallback
#   - syslog + NTP for operational baseline
#   - IOS-XR transforms for GigabitEthernet naming
#
# Address plan:
#   P2P links:    10.0.0.0/16  (/31 subnets)
#   Router IDs:   192.0.2.0/24
#   Node SIDs:    16001–16200

version: 1
imports: []
layers:
- name: pools
  requires: []
  primitives:
  - type: global_resource_pool
    name: router-id-pool
    resource_type: router_id
    pool: '192.0.2.1-192.0.2.254'
  - type: global_resource_pool
    name: node-sid-pool
    resource_type: segment_id
    pool: '16001-16200'

- name: topology
  requires:
  - pools
  primitives:
  # Full mesh between P routers (P1–P2)
  - type: mesh_nodes
    selector: $p_routers
    mesh_type: full
  # PE routers connect to every P router (hub-and-spoke)
  - type: mesh_nodes
    selector: $pe_and_p
    mesh_type: hub_and_spoke
    hub_selector: $p_routers
    spoke_selector: $pe_routers

- name: addressing
  requires:
  - topology
  primitives:
  - type: provision_ips
    selector: $core_links
    pool: '10.0.0.0/16'
    subnet_size: 31
    strategy: dense
  - type: allocate_resources
    selector: $all_routers
    resource_type: router_id
    pool: router-id-pool
    strategy: dense
  - type: allocate_resources
    selector: $all_routers
    resource_type: segment_id
    pool: node-sid-pool
    strategy: dense

- name: underlay
  requires:
  - addressing
  primitives:
  - type: build_protocol_layer
    selector: $all_routers
    layer: isis
    config:
      level: level_2
      metric_style: wide
      protocol_type: isis
    clone_underlying: true
  - type: build_protocol_layer
    selector: $all_routers
    layer: sr_mpls
    config:
      srgb_start: 16000
      srgb_end: 23999
      prefer_sr: true
      ti_lfa: true
  - type: build_protocol_layer
    selector: $all_routers
    layer: bfd
    config:
      min_tx_ms: 100
      min_rx_ms: 100
      detect_mult: 3

- name: services
  requires:
  - underlay
  primitives:
  - type: build_protocol_layer
    selector: $pe_routers
    layer: bgp
    config:
      asn_base: '65100'
      protocol_type: bgp
      peer_type: ibgp
    clone_underlying: true
  - type: build_protocol_layer
    selector: $all_routers
    layer: tacacs
    config:
      servers: ["10.99.0.1", "10.99.0.2"]
      fallback_local: true
  - type: build_protocol_layer
    selector: $all_routers
    layer: ntp
    config:
      servers: ["10.99.0.10", "10.99.0.11"]
      source_interface: loopback0
  - type: build_protocol_layer
    selector: $all_routers
    layer: syslog
    config:
      servers: ["10.99.0.20"]
      facility: local7
      severity: informational

- name: secrets
  requires:
  - services
  primitives:
  - type: inject_secrets
    selector: $all_routers
    secrets:
      tacacs_key: ${TACACS_KEY}
      isis_auth_key: ${ISIS_AUTH_KEY}

assertions:
- name: pe-reaches-every-p
  severity: error
  select: $pe_routers
  check:
    type: reachability
    target_selector: $p_routers
  help: A PE router cannot reach a P router — check IS-IS adjacencies.

- name: core-is-connected
  severity: error
  select: $all_routers
  check:
    type: is_connected
  help: Core network is partitioned — a router is isolated.

- name: every-router-has-sid
  severity: error
  select: $all_routers
  check:
    type: field_exists
    field: segment_id
  help: Router is missing its SR Node SID — check allocate_resources.

- name: every-router-has-rid
  severity: error
  select: $all_routers
  check:
    type: field_exists
    field: router_id
  help: Router is missing its router-id — check allocate_resources.

- name: ntp-compliance
  severity: error
  select: $all_routers
  check:
    type: use_rule
    name: ntp-configured
  help: Router is missing NTP configuration.

rules:
  ntp-configured: has(node.ntp_servers) and size(node.ntp_servers) >= 2

groups:
  pe_routers: nodes[role=='pe']
  p_routers: nodes[role=='p']
  pe_and_p: $pe_routers | $p_routers
  all_routers: $pe_routers | $p_routers
  core_links: edges[true]

blast_radius:
- name: core-change-limit
  select: null
  check:
    type: max_modifications
    count: 5

transforms:
- name: iosxr-interface-naming
  when: device_os == 'iosxr'
  rules:
  - match_expr: stanza.kind == 'interface'
    apply:
      name: "'GigabitEthernet0/0/0/' + string(stanza.fields.abstract_index)"
- name: iosxr-isis-sr
  when: device_os == 'iosxr'
  rules:
  - match_expr: stanza.kind == 'isis_neighbor'
    apply:
      sr_enabled: 'true'
      metric: '10'

hardware_library:
  chassis: []
  linecards: []

06_campus_nac.yaml

# Case Study 06 — Campus Network with 802.1X NAC
#
# Models a university campus network with three-tier architecture
# (core → distribution → access), 802.1X port-based access control,
# DHCP relay, VRRP gateway redundancy, and full compliance auditing.
#
# Topology:
#   2 Core routers          — OSPF area 0 backbone, VRRP gateway pair
#   2 Distribution switches — aggregate access layer, DHCP relay
#   4 Access switches       — host-facing, 802.1X + MAB enforcement
#
# Design choices:
#   - OSPF area 0 flat backbone (single area for simplicity)
#   - VRRP on core for default gateway redundancy
#   - RSTP with BPDU guard on access ports
#   - 802.1X with MAB fallback and guest VLAN 999
#   - RADIUS for 802.1X backend, TACACS+ for admin access
#   - DHCP relay on access switches → central DHCP server
#   - LLDP for neighbour discovery and topology verification
#   - NTP + syslog compliance assertions
#
# Address plan:
#   Core links:          10.0.0.0/24  (/30 subnets)
#   Distribution links:  10.0.1.0/24  (/30 subnets)
#   Access links:        10.0.2.0/24  (/30 subnets)

version: 1
imports: []
layers:
- name: topology
  requires: []
  primitives:
  # Core pair — full mesh (single link between core1 and core2)
  - type: mesh_nodes
    selector: $core
    mesh_type: full
  # Distribution connects to both core switches
  - type: mesh_nodes
    selector: $core_and_dist
    mesh_type: hub_and_spoke
    hub_selector: $core
    spoke_selector: $distribution
  # Access connects to distribution (dual-homed)
  - type: mesh_nodes
    selector: $dist_and_access
    mesh_type: hub_and_spoke
    hub_selector: $distribution
    spoke_selector: $access

- name: addressing
  requires:
  - topology
  primitives:
  - type: provision_ips
    selector: $core_links
    pool: '10.0.0.0/24'
    subnet_size: 30
    strategy: dense
  - type: provision_ips
    selector: $dist_links
    pool: '10.0.1.0/24'
    subnet_size: 30
    strategy: dense
  - type: provision_ips
    selector: $access_links
    pool: '10.0.2.0/24'
    subnet_size: 30
    strategy: dense

- name: routing
  requires:
  - addressing
  primitives:
  - type: build_protocol_layer
    selector: $all_switches
    layer: ospf
    config:
      area: '0.0.0.0'
      network_type: point_to_point
    clone_underlying: true
  - type: build_protocol_layer
    selector: $core
    layer: vrrp
    config:
      version: 3
      vrid: 10
      priority: 110
      preempt: true
      advertisement_interval_ms: 1000
  - type: build_protocol_layer
    selector: $all_switches
    layer: bfd
    config:
      min_tx_ms: 300
      min_rx_ms: 300
      detect_mult: 3

- name: switching
  requires:
  - routing
  primitives:
  - type: build_protocol_layer
    selector: $all_switches
    layer: stp
    config:
      mode: rstp
      bpdu_guard: true
  - type: build_protocol_layer
    selector: $all_switches
    layer: lldp
    config:
      tx_interval_s: 30
      hold_multiplier: 4
  - type: build_protocol_layer
    selector: $access
    layer: lacp
    config:
      mode: active
      min_links: 1
      lacp_rate: fast

- name: security
  requires:
  - switching
  primitives:
  # 802.1X on access layer with guest VLAN and MAB fallback
  - type: build_protocol_layer
    selector: $access
    layer: dot1x
    config:
      mode: multi-auth
      reauth_period_s: 3600
      guest_vlan: 999
      auth_fail_vlan: 998
      critical_vlan: 997
      mac_bypass: true
  # RADIUS for 802.1X backend
  - type: build_protocol_layer
    selector: $access
    layer: radius
    config:
      servers: ["10.99.0.50", "10.99.0.51"]
      auth_port: 1812
      acct_port: 1813
      use_accounting: true
  # TACACS+ for admin access on all devices
  - type: build_protocol_layer
    selector: $all_switches
    layer: tacacs
    config:
      servers: ["10.99.0.1", "10.99.0.2"]
      fallback_local: true
      authorization: true
      accounting: true
  # Management ACL restricting SSH/HTTPS access
  - type: build_access_policy
    selector: $all_switches
    policy_name: MGMT-ACCESS
    rules:
    - name: allow-noc-ssh
      action: permit
      source_prefix: '10.99.0.0/24'
      destination_prefix: null
      protocol: tcp
      source_port: null
      destination_port: 22
    - name: allow-noc-https
      action: permit
      source_prefix: '10.99.0.0/24'
      destination_prefix: null
      protocol: tcp
      source_port: null
      destination_port: 443
    - name: deny-all-mgmt
      action: deny
      source_prefix: null
      destination_prefix: null
      protocol: null
      source_port: null
      destination_port: null

- name: services
  requires:
  - security
  primitives:
  # DHCP relay on access switches pointing to central DHCP server
  - type: build_protocol_layer
    selector: $access
    layer: dhcp_relay
    config:
      servers: ["10.99.0.100"]
      option_82: true
      source_interface: loopback0
  # NTP on all devices
  - type: build_protocol_layer
    selector: $all_switches
    layer: ntp
    config:
      servers: ["10.99.0.10", "10.99.0.11"]
      source_interface: loopback0
  # Syslog on all devices
  - type: build_protocol_layer
    selector: $all_switches
    layer: syslog
    config:
      servers: ["10.99.0.20"]
      facility: local7
      severity: informational

- name: secrets
  requires:
  - services
  primitives:
  - type: inject_secrets
    selector: $all_switches
    secrets:
      tacacs_key: ${TACACS_KEY}
      radius_secret: ${RADIUS_SECRET}

assertions:
- name: access-dual-homed
  severity: error
  select: $access
  check:
    type: min_edges
    count: 2
  help: Access switch has fewer than 2 uplinks — single point of failure.

- name: core-connected
  severity: error
  select: $all_switches
  check:
    type: is_connected
  help: Campus network is partitioned — a switch is isolated.

- name: ntp-compliance
  severity: error
  select: $all_switches
  check:
    type: use_rule
    name: ntp-configured
  help: Switch is missing NTP configuration — at least 2 NTP servers required.

- name: syslog-compliance
  severity: error
  select: $all_switches
  check:
    type: use_rule
    name: syslog-configured
  help: Switch has no syslog server configured.

- name: hostnames-unique
  severity: error
  select: $all_switches
  check:
    type: field_exists
    field: hostname
  help: A switch is missing its hostname field.

- name: access-bipartite
  severity: error
  select: $dist_and_access
  check:
    type: is_bipartite
  help: Access-to-distribution graph is not bipartite — unexpected link between same-tier nodes.

rules:
  ntp-configured: has(node.ntp_servers) and size(node.ntp_servers) >= 2
  syslog-configured: has(node.syslog_server) and node.syslog_server != ''

groups:
  core: nodes[role=='core']
  distribution: nodes[role=='distribution']
  access: nodes[role=='access']
  core_and_dist: $core | $distribution
  dist_and_access: $distribution | $access
  all_switches: $core | $distribution | $access
  core_links: edges[tier=='core']
  dist_links: edges[tier=='distribution']
  access_links: edges[tier=='access']

blast_radius:
- name: campus-change-cap
  select: null
  check:
    type: max_percentage_changed
    value: 15.0

transforms: []
hardware_library:
  chassis: []
  linecards: []

07_sdwan_ipsec.yaml

# Case Study 07 — Multi-Site SD-WAN with IPsec Overlay
#
# Models a hub-and-spoke SD-WAN connecting branch offices to dual DC
# hubs via IPsec tunnels, with OSPF running over the encrypted overlay
# for dynamic routing and BFD for fast failover.
#
# Topology:
#   2 DC hub routers   — terminate all branch tunnels, fully meshed
#   4 Branch routers   — each connects to both hubs (dual-homed)
#
# Design choices:
#   - IKEv2 with AES-256-GCM and DH group 20 (ECDH-384)
#   - OSPF over IPsec tunnels (point-to-point network type)
#   - BFD with relaxed timers (300 ms) for WAN tolerance
#   - PSK injected via inject_secrets (never hardcoded)
#   - NTP + syslog + SNMP for operational baseline
#   - WireGuard alternative shown as conditional (for VyOS branches)
#   - Blast radius limits: max 2 deletions to prevent mass decommission
#
# Address plan:
#   Hub-to-hub:    172.16.0.0/24  (/31 subnets)
#   Hub-to-branch: 172.16.1.0/24  (/30 subnets)

version: 1
imports: []
layers:
- name: topology
  requires: []
  primitives:
  # Full mesh between DC hub routers
  - type: mesh_nodes
    selector: $hubs
    mesh_type: full
  # Each branch connects to both hubs
  - type: mesh_nodes
    selector: $hubs_and_branches
    mesh_type: hub_and_spoke
    hub_selector: $hubs
    spoke_selector: $branches

- name: addressing
  requires:
  - topology
  primitives:
  - type: provision_ips
    selector: $hub_links
    pool: '172.16.0.0/24'
    subnet_size: 31
    strategy: dense
  - type: provision_ips
    selector: $branch_links
    pool: '172.16.1.0/24'
    subnet_size: 30
    strategy: dense

- name: encryption
  requires:
  - addressing
  primitives:
  # IPsec IKEv2 overlay on all routers
  - type: build_protocol_layer
    selector: $all_routers
    layer: ipsec
    config:
      ike_version: 2
      encryption: aes256gcm
      dh_group: 20
      ike_lifetime_s: 86400
      ipsec_lifetime_s: 3600
      dpd_interval_s: 30
      dpd_retries: 5
      tunnel_mode: tunnel
    clone_underlying: true
  # Conditional: use WireGuard instead of IPsec for VyOS branches
  - type: conditional
    condition: topology.node_count < 0
    then_primitives:
    - type: build_protocol_layer
      selector: nodes[device_os=='vyos']
      layer: wireguard
      config:
        listen_port: 51820
        mtu: 1420
        persistent_keepalive_s: 25
    else_primitives: null

- name: routing
  requires:
  - encryption
  primitives:
  - type: build_protocol_layer
    selector: $all_routers
    layer: ospf
    config:
      area: '0.0.0.0'
      network_type: point_to_point
    clone_underlying: true
  - type: build_protocol_layer
    selector: $all_routers
    layer: bfd
    config:
      min_tx_ms: 300
      min_rx_ms: 300
      detect_mult: 3

- name: management
  requires:
  - routing
  primitives:
  - type: build_protocol_layer
    selector: $all_routers
    layer: ntp
    config:
      servers: ["10.99.0.10", "10.99.0.11"]
      source_interface: loopback0
  - type: build_protocol_layer
    selector: $all_routers
    layer: syslog
    config:
      servers: ["10.99.0.20"]
      facility: local7
  - type: build_protocol_layer
    selector: $all_routers
    layer: snmp
    config:
      version: "3"
      v3_auth_protocol: sha
      v3_priv_protocol: aes128
      v3_security_level: authPriv
      trap_targets: ["10.99.0.30"]

- name: secrets
  requires:
  - management
  primitives:
  - type: inject_secrets
    selector: $all_routers
    secrets:
      ipsec_psk: ${IPSEC_PSK}
      snmp_auth_pass: ${SNMP_AUTH_PASS}
      snmp_priv_pass: ${SNMP_PRIV_PASS}

assertions:
- name: branch-dual-homed
  severity: error
  select: $branches
  check:
    type: min_edges
    count: 2
  help: Branch router has fewer than 2 hub connections — no failover path.

- name: hubs-connected
  severity: error
  select: $hubs
  check:
    type: is_connected
  help: DC hub routers are not directly connected.

- name: branch-reaches-hub
  severity: error
  select: $branches
  check:
    type: reachability
    target_selector: $hubs
  help: A branch router cannot reach any DC hub — check IPsec tunnel.

- name: ntp-compliance
  severity: error
  select: $all_routers
  check:
    type: use_rule
    name: ntp-configured
  help: Router is missing NTP configuration.

- name: all-have-ipsec-psk
  severity: error
  select: $all_routers
  check:
    type: field_exists
    field: ipsec_psk
  help: Router is missing IPsec pre-shared key — check inject_secrets.

rules:
  ntp-configured: has(node.ntp_servers) and size(node.ntp_servers) >= 2

groups:
  hubs: nodes[role=='hub']
  branches: nodes[role=='branch']
  hubs_and_branches: $hubs | $branches
  all_routers: $hubs | $branches
  hub_links: edges[tier=='hub']
  branch_links: edges[tier=='branch']

blast_radius:
- name: prevent-mass-decommission
  select: null
  check:
    type: max_deletions
    count: 2

transforms: []
hardware_library:
  chassis: []
  linecards: []

08_multicast_video.yaml

# Case Study 08 — Multicast Video Distribution Network
#
# Models an IPTV / video distribution network using PIM Sparse Mode
# with a dedicated Rendezvous Point, IGMP on access-facing interfaces,
# and OSPF as the unicast underlay for RPF lookups.
#
# Topology:
#   1 Rendezvous Point (RP) — PIM-SM RP, BSR candidate
#   2 Core routers           — PIM transit, OSPF backbone
#   2 Distribution routers   — aggregate multicast streams
#   3 Access routers         — IGMP snooping, last-hop PIM
#
# Design choices:
#   - PIM-SM with static RP (RP address set on rp1's loopback)
#   - IGMPv3 on access layer for source-specific multicast (SSM) readiness
#   - OSPF area 0 as unicast underlay for RPF validation
#   - MLAG on distribution pairs for resilient multicast forwarding
#   - LLDP for topology discovery
#   - NetFlow/IPFIX on core for multicast traffic accounting
#   - BFD for fast PIM neighbour failure detection
#
# Address plan:
#   Core links:  10.10.0.0/24 (/31)
#   Dist links:  10.10.1.0/24 (/30)
#   Access links: 10.10.2.0/24 (/30)
#   Multicast:   239.1.0.0/16 (admin-scoped)

version: 1
imports: []
layers:
- name: pools
  requires: []
  primitives:
  - type: global_resource_pool
    name: router-id-pool
    resource_type: router_id
    pool: '10.255.0.1-10.255.0.50'

- name: topology
  requires:
  - pools
  primitives:
  # RP connects to both core routers
  - type: mesh_nodes
    selector: $rp_and_core
    mesh_type: hub_and_spoke
    hub_selector: $core
    spoke_selector: $rp
  # Core full mesh
  - type: mesh_nodes
    selector: $core
    mesh_type: full
  # Distribution connects to core
  - type: mesh_nodes
    selector: $core_and_dist
    mesh_type: hub_and_spoke
    hub_selector: $core
    spoke_selector: $distribution
  # Access connects to distribution
  - type: mesh_nodes
    selector: $dist_and_access
    mesh_type: hub_and_spoke
    hub_selector: $distribution
    spoke_selector: $access

- name: addressing
  requires:
  - topology
  primitives:
  - type: provision_ips
    selector: $core_links
    pool: '10.10.0.0/24'
    subnet_size: 31
    strategy: dense
  - type: provision_ips
    selector: $dist_links
    pool: '10.10.1.0/24'
    subnet_size: 30
    strategy: dense
  - type: provision_ips
    selector: $access_links
    pool: '10.10.2.0/24'
    subnet_size: 30
    strategy: dense
  - type: allocate_resources
    selector: $all_routers
    resource_type: router_id
    pool: router-id-pool
    strategy: dense

- name: underlay
  requires:
  - addressing
  primitives:
  - type: build_protocol_layer
    selector: $all_routers
    layer: ospf
    config:
      area: '0.0.0.0'
      network_type: point_to_point
    clone_underlying: true
  - type: build_protocol_layer
    selector: $all_routers
    layer: bfd
    config:
      min_tx_ms: 200
      min_rx_ms: 200
      detect_mult: 3

- name: multicast
  requires:
  - underlay
  primitives:
  # PIM-SM on all routers — RP address is rp1's loopback
  - type: build_protocol_layer
    selector: $all_routers
    layer: pim_sm
    config:
      mode: sparse
      rp_address: "10.255.0.1"
      hello_interval_s: 30
      join_prune_interval_s: 60
      spt_threshold_kbps: 0
  # BSR candidacy on RP node
  - type: build_protocol_layer
    selector: $rp
    layer: pim_sm
    config:
      use_bsr: true
      bsr_candidate: true
      bsr_priority: 200
      rp_candidate: true
  # IGMP on access layer for host-facing interfaces
  - type: build_protocol_layer
    selector: $access
    layer: igmp
    config:
      version: 3
      snooping: true
      querier: true
      query_interval_s: 125
      fast_leave: false
  # IGMP snooping only on distribution (no querier)
  - type: build_protocol_layer
    selector: $distribution
    layer: igmp
    config:
      version: 3
      snooping: true
      querier: false

- name: resilience
  requires:
  - multicast
  primitives:
  # MLAG on distribution pairs for dual-homed access switches
  - type: build_protocol_layer
    selector: $distribution
    layer: mlag
    config:
      domain_id: 1
      dual_primary_detection: true
      peer_gateway: true
      reload_delay_s: 300
  - type: build_protocol_layer
    selector: $all_routers
    layer: lldp
    config:
      tx_interval_s: 30
      hold_multiplier: 4

- name: telemetry
  requires:
  - resilience
  primitives:
  # NetFlow/IPFIX on core for multicast traffic accounting
  - type: build_protocol_layer
    selector: $core
    layer: netflow
    config:
      format: ipfix
      collectors: ["10.99.0.30"]
      collector_port: 4739
      sampling_rate: 1000
      active_timeout_s: 60

assertions:
- name: rp-reachable-from-access
  severity: error
  select: $access
  check:
    type: reachability
    target_selector: $rp
  help: Access router cannot reach RP — multicast joins will fail.

- name: network-connected
  severity: error
  select: $all_routers
  check:
    type: is_connected
  help: Network is partitioned — a router is isolated.

- name: access-dual-homed
  severity: error
  select: $access
  check:
    type: min_edges
    count: 2
  help: Access router has fewer than 2 uplinks — no multicast redundancy.

- name: every-router-has-rid
  severity: error
  select: $all_routers
  check:
    type: field_exists
    field: router_id
  help: Router is missing its router-id.

- name: dist-to-access-bipartite
  severity: error
  select: $dist_and_access
  check:
    type: is_bipartite
  help: Distribution-to-access graph is not bipartite.

rules: {}

groups:
  rp: nodes[role=='rp']
  core: nodes[role=='core']
  distribution: nodes[role=='distribution']
  access: nodes[role=='access']
  rp_and_core: $rp | $core
  core_and_dist: $core | $distribution
  dist_and_access: $distribution | $access
  all_routers: $rp | $core | $distribution | $access
  core_links: edges[tier=='core']
  dist_links: edges[tier=='distribution']
  access_links: edges[tier=='access']

blast_radius:
- name: multicast-change-cap
  select: null
  check:
    type: max_percentage_changed
    value: 20.0

transforms: []
hardware_library:
  chassis: []
  linecards: []

09_zerotrust_dc.yaml

# Case Study 09 — Zero-Trust DC Microsegmentation
#
# Models a security-first datacentre design with zone-based firewall
# policies enforcing strict east-west traffic control between application
# tiers, NAT for external access, and MACsec on all inter-switch links.
#
# Topology:
#   2 Firewall nodes  — central policy enforcement point
#   2 Web servers     — DMZ tier, internet-facing
#   2 App servers     — application tier, internal APIs
#   2 DB servers      — data tier, strictly isolated
#
# Design choices:
#   - Zone-based firewall: web→app (TCP 8080), app→db (TCP 5432), mgmt→all (SSH)
#   - Default deny between all zones (explicit permit only)
#   - DNAT for inbound HTTPS to web tier
#   - SNAT for outbound internet access from app tier
#   - MACsec on all links for Layer 2 encryption
#   - SNMP + syslog + NTP for monitoring baseline
#   - NetFlow on firewalls for traffic visibility
#   - Strict blast radius: max 1 deletion, max  change
#
# Address plan:
#   Web tier:    10.10.1.0/24
#   App tier:    10.10.2.0/24
#   DB tier:     10.10.3.0/24
#   Management:  10.99.0.0/24
#   External:    203.0.113.0/24

version: 1
imports: []
layers:
- name: topology
  requires: []
  primitives:
  # Each tier connects through the firewall pair
  - type: mesh_nodes
    selector: $fw_and_web
    mesh_type: hub_and_spoke
    hub_selector: $firewalls
    spoke_selector: $web
  - type: mesh_nodes
    selector: $fw_and_app
    mesh_type: hub_and_spoke
    hub_selector: $firewalls
    spoke_selector: $app
  - type: mesh_nodes
    selector: $fw_and_db
    mesh_type: hub_and_spoke
    hub_selector: $firewalls
    spoke_selector: $db
  # Firewall pair interconnect
  - type: mesh_nodes
    selector: $firewalls
    mesh_type: full

- name: addressing
  requires:
  - topology
  primitives:
  - type: provision_ips
    selector: $web_links
    pool: '10.10.1.0/24'
    subnet_size: 30
    strategy: dense
  - type: provision_ips
    selector: $app_links
    pool: '10.10.2.0/24'
    subnet_size: 30
    strategy: dense
  - type: provision_ips
    selector: $db_links
    pool: '10.10.3.0/24'
    subnet_size: 30
    strategy: dense

- name: encryption
  requires:
  - addressing
  primitives:
  # MACsec on all inter-switch links
  - type: build_protocol_layer
    selector: $all_nodes
    layer: macsec
    config:
      cipher: gcm_aes_256
      confidentiality: must-encrypt
      include_sci: true
      replay_protection: true

- name: zone_policy
  requires:
  - encryption
  primitives:
  # Web → App: only HTTP API traffic
  - build_zone_policy:
      name: "web-to-app"
      select:
        role: firewall
      from_zone: web
      to_zone: app
      default_action: deny
      rules:
        - name: allow-api
          action: permit
          protocol: tcp
          destination_port: 8080
          source_prefixes: ["10.10.1.0/24"]
          destination_prefixes: ["10.10.2.0/24"]
          stateful: true
          log: true
        - name: allow-health-check
          action: permit
          protocol: tcp
          destination_port: 8081
          source_prefixes: ["10.10.1.0/24"]
          destination_prefixes: ["10.10.2.0/24"]
          stateful: true
          log: false

  # App → DB: only database traffic
  - build_zone_policy:
      name: "app-to-db"
      select:
        role: firewall
      from_zone: app
      to_zone: db
      default_action: deny
      rules:
        - name: allow-postgres
          action: permit
          protocol: tcp
          destination_port: 5432
          source_prefixes: ["10.10.2.0/24"]
          destination_prefixes: ["10.10.3.0/24"]
          stateful: true
          log: true
        - name: allow-redis
          action: permit
          protocol: tcp
          destination_port: 6379
          source_prefixes: ["10.10.2.0/24"]
          destination_prefixes: ["10.10.3.0/24"]
          stateful: true
          log: false

  # Management → All: SSH from NOC
  - build_zone_policy:
      name: "mgmt-to-all"
      select:
        role: firewall
      from_zone: management
      to_zone: web
      default_action: deny
      rules:
        - name: allow-ssh
          action: permit
          protocol: tcp
          destination_port: 22
          source_prefixes: ["10.99.0.0/24"]
          destination_prefixes: ["0.0.0.0/0"]
          stateful: true
          log: true
        - name: allow-icmp
          action: permit
          protocol: icmp
          stateful: false
          log: false

  # DB → anything: deny all (data tier never initiates)
  - build_zone_policy:
      name: "db-to-any"
      select:
        role: firewall
      from_zone: db
      to_zone: app
      default_action: deny
      rules: []

- name: nat
  requires:
  - zone_policy
  primitives:
  # DNAT: external HTTPS → web tier
  - build_nat_policy:
      name: "inbound-nat"
      select:
        role: firewall
      rules:
        - name: https-to-web
          nat_type: dnat
          external_address: "203.0.113.10"
          external_port: 443
          internal_address: "10.10.1.10"
          internal_port: 8443
          log: true

  # SNAT: app tier → internet via interface masquerade
  - build_nat_policy:
      name: "outbound-nat"
      select:
        role: firewall
      rules:
        - name: app-internet-snat
          nat_type: snat
          mode: interface
          source_prefixes: ["10.10.2.0/24"]
          from_zone: app
          to_zone: untrust
          log: false

- name: monitoring
  requires:
  - nat
  primitives:
  - type: build_protocol_layer
    selector: $all_nodes
    layer: syslog
    config:
      servers: ["10.99.0.20"]
      facility: local7
      severity: informational
  - type: build_protocol_layer
    selector: $all_nodes
    layer: ntp
    config:
      servers: ["10.99.0.10", "10.99.0.11"]
  - type: build_protocol_layer
    selector: $all_nodes
    layer: snmp
    config:
      version: "3"
      v3_security_level: authPriv
      trap_targets: ["10.99.0.30"]
  # NetFlow on firewalls for traffic analysis
  - type: build_protocol_layer
    selector: $firewalls
    layer: netflow
    config:
      format: ipfix
      collectors: ["10.99.0.40"]
      sampling_rate: 100

- name: secrets
  requires:
  - monitoring
  primitives:
  - type: inject_secrets
    selector: $all_nodes
    secrets:
      macsec_cak: ${MACSEC_CAK}
      macsec_ckn: ${MACSEC_CKN}
      snmp_auth_pass: ${SNMP_AUTH_PASS}
      snmp_priv_pass: ${SNMP_PRIV_PASS}

assertions:
- name: zone-policy-web-to-app
  severity: error
  select: $firewalls
  check:
    type: zone_policy_exists
    from_zone: web
    to_zone: app
  help: Missing zone policy from web to app — east-west traffic is uncontrolled.

- name: zone-policy-app-to-db
  severity: error
  select: $firewalls
  check:
    type: zone_policy_exists
    from_zone: app
    to_zone: db
  help: Missing zone policy from app to db — database access is uncontrolled.

- name: web-reachable-from-fw
  severity: error
  select: $web
  check:
    type: reachability
    target_selector: $firewalls
  help: Web server cannot reach firewall — check topology wiring.

- name: db-isolated-from-web
  severity: warning
  select: $db
  check:
    type: use_rule
    name: no-direct-web-link
  help: DB server has a direct link to web tier — violates zero-trust model.

- name: all-links-encrypted
  severity: error
  select: $all_nodes
  check:
    type: use_rule
    name: macsec-enabled
  help: A node is missing MACsec — all links must be encrypted in zero-trust.

rules:
  no-direct-web-link: "!has(node.direct_web_link) or node.direct_web_link == false"
  macsec-enabled: has(node.macsec_config) and node.macsec_config != ''

groups:
  firewalls: nodes[role=='firewall']
  web: nodes[role=='web']
  app: nodes[role=='app']
  db: nodes[role=='db']
  fw_and_web: $firewalls | $web
  fw_and_app: $firewalls | $app
  fw_and_db: $firewalls | $db
  all_nodes: $firewalls | $web | $app | $db
  web_links: edges[zone=='web']
  app_links: edges[zone=='app']
  db_links: edges[zone=='db']

blast_radius:
- name: strict-deletion-limit
  select: null
  check:
    type: max_deletions
    count: 1
- name: strict-change-cap
  select: null
  check:
    type: max_percentage_changed
    value: 10.0

transforms: []
hardware_library:
  chassis: []
  linecards: []

10_financial_compliance.yaml

# Case Study 10 — Compliance-Hardened Financial Network
#
# Models a PCI-DSS / SOX-compliant financial trading network with strict
# access controls, encrypted management plane, comprehensive auditing,
# and MACsec on all inter-switch links.
#
# Topology:
#   2 Core routers     — OSPF backbone, route reflectors
#   1 DMZ router       — internet-facing, heavily filtered
#   2 Trading switches — low-latency trading floor, MACsec encrypted
#   1 Management node  — out-of-band management, TACACS+ / RADIUS
#
# Design choices:
#   - OSPF backbone with BGP safety filters on DMZ (bogon + RFC1918 block)
#   - MACsec (AES-256-GCM) on trading links for wire-speed encryption
#   - TACACS+ for all admin access with command authorisation + accounting
#   - RADIUS for 802.1X on trading floor ports
#   - NTP with authentication (compliance requirement)
#   - Syslog over TLS to centralised SIEM
#   - SNMP v3 with authPriv (no v2c permitted)
#   - NetFlow/IPFIX for PCI-DSS audit trail
#   - Strict schema assertions: hostname, serial, firmware, site required
#   - Zone policy: DMZ → trading denied; management → all permitted (SSH only)
#   - Blast radius: max 1 deletion, max  change (change-averse environment)
#
# Address plan:
#   Core:        10.1.0.0/24
#   DMZ:         10.2.0.0/24
#   Trading:     10.3.0.0/24
#   Management:  10.99.0.0/24
#   External:    203.0.113.0/28

version: 1
imports: []
layers:
- name: topology
  requires: []
  primitives:
  # Core full mesh
  - type: mesh_nodes
    selector: $core
    mesh_type: full
  # DMZ connects to core
  - type: mesh_nodes
    selector: $core_and_dmz
    mesh_type: hub_and_spoke
    hub_selector: $core
    spoke_selector: $dmz
  # Trading connects to core
  - type: mesh_nodes
    selector: $core_and_trading
    mesh_type: hub_and_spoke
    hub_selector: $core
    spoke_selector: $trading
  # Management connects to core (out-of-band path)
  - type: mesh_nodes
    selector: $core_and_mgmt
    mesh_type: hub_and_spoke
    hub_selector: $core
    spoke_selector: $management

- name: addressing
  requires:
  - topology
  primitives:
  - type: provision_ips
    selector: $core_links
    pool: '10.1.0.0/24'
    subnet_size: 30
    strategy: dense
  - type: provision_ips
    selector: $dmz_links
    pool: '10.2.0.0/24'
    subnet_size: 30
    strategy: dense
  - type: provision_ips
    selector: $trading_links
    pool: '10.3.0.0/24'
    subnet_size: 30
    strategy: dense

- name: routing
  requires:
  - addressing
  primitives:
  - type: build_protocol_layer
    selector: $all_routers
    layer: ospf
    config:
      area: '0.0.0.0'
    clone_underlying: true
  - type: build_protocol_layer
    selector: $all_routers
    layer: bfd
    config:
      min_tx_ms: 100
      min_rx_ms: 100
      detect_mult: 3

- name: bgp_safety
  requires:
  - routing
  primitives:
  # BGP safety filters on DMZ router — block bogons and RFC1918
  - type: generate_safe_bgp_filters
    selector: $dmz
    prefix_list_name: BOGON-FILTER-V4
    policy_name: DMZ-INBOUND
    block_rfc1918: true
    permit_default: false

- name: encryption
  requires:
  - bgp_safety
  primitives:
  # MACsec on trading links — wire-speed L2 encryption
  - type: build_protocol_layer
    selector: $trading
    layer: macsec
    config:
      cipher: gcm_aes_256
      confidentiality: must-encrypt
      include_sci: true
      replay_protection: true
      replay_window: 64

- name: zone_security
  requires:
  - encryption
  primitives:
  # DMZ → Trading: deny all (PCI-DSS requirement)
  - build_zone_policy:
      name: "dmz-to-trading"
      select:
        role: core
      from_zone: dmz
      to_zone: trading
      default_action: deny
      rules: []

  # Management → All: SSH only
  - build_zone_policy:
      name: "mgmt-to-all"
      select:
        role: core
      from_zone: management
      to_zone: trading
      default_action: deny
      rules:
        - name: allow-ssh
          action: permit
          protocol: tcp
          destination_port: 22
          source_prefixes: ["10.99.0.0/24"]
          destination_prefixes: ["0.0.0.0/0"]
          stateful: true
          log: true
        - name: allow-icmp-diag
          action: permit
          protocol: icmp
          log: true

- name: aaa
  requires:
  - zone_security
  primitives:
  # TACACS+ for device admin — command auth + accounting
  - type: build_protocol_layer
    selector: $all_routers
    layer: tacacs
    config:
      servers: ["10.99.0.1", "10.99.0.2"]
      fallback_local: true
      authorization: true
      accounting: true
      single_connection: true
  # RADIUS for 802.1X on trading floor
  - type: build_protocol_layer
    selector: $trading
    layer: radius
    config:
      servers: ["10.99.0.50", "10.99.0.51"]
      use_accounting: true
  # 802.1X on trading floor ports
  - type: build_protocol_layer
    selector: $trading
    layer: dot1x
    config:
      mode: single
      reauth_period_s: 1800
      mac_bypass: false
      guest_vlan: null

- name: compliance_services
  requires:
  - aaa
  primitives:
  # NTP with authentication (PCI-DSS 10.4)
  - type: build_protocol_layer
    selector: $all_routers
    layer: ntp
    config:
      servers: ["10.99.0.10", "10.99.0.11"]
      authentication: true
      source_interface: loopback0
  # Syslog over TLS to SIEM (PCI-DSS 10.5)
  - type: build_protocol_layer
    selector: $all_routers
    layer: syslog
    config:
      servers: ["10.99.0.20"]
      facility: local7
      severity: informational
      transport: tcp
      use_tls: true
  # SNMP v3 only — no v2c permitted
  - type: build_protocol_layer
    selector: $all_routers
    layer: snmp
    config:
      version: "3"
      v3_auth_protocol: sha
      v3_priv_protocol: aes256
      v3_security_level: authPriv
      trap_targets: ["10.99.0.30"]
      source_interface: loopback0
  # NetFlow/IPFIX for audit trail (PCI-DSS 10.2)
  - type: build_protocol_layer
    selector: $core
    layer: netflow
    config:
      format: ipfix
      collectors: ["10.99.0.40"]
      collector_port: 4739
      sampling_rate: 1
      active_timeout_s: 60

- name: secrets
  requires:
  - compliance_services
  primitives:
  - type: inject_secrets
    selector: $all_routers
    secrets:
      tacacs_key: ${TACACS_KEY}
      radius_secret: ${RADIUS_SECRET}
      macsec_cak: ${MACSEC_CAK}
      macsec_ckn: ${MACSEC_CKN}
      ntp_auth_key: ${NTP_AUTH_KEY}
      snmp_auth_pass: ${SNMP_AUTH_PASS}
      snmp_priv_pass: ${SNMP_PRIV_PASS}

assertions:
# --- Topology assertions ---
- name: core-connected
  severity: error
  select: $all_routers
  check:
    type: is_connected
  help: Network is partitioned — a router is isolated.

- name: dmz-reaches-core
  severity: error
  select: $dmz
  check:
    type: reachability
    target_selector: $core
  help: DMZ cannot reach core routers.

- name: trading-reaches-core
  severity: error
  select: $trading
  check:
    type: reachability
    target_selector: $core
  help: Trading switches cannot reach core routers.

# --- Security assertions ---
- name: zone-policy-dmz-trading
  severity: error
  select: $core
  check:
    type: zone_policy_exists
    from_zone: dmz
    to_zone: trading
  help: Missing zone policy blocking DMZ→trading — PCI-DSS violation.

# --- Compliance assertions ---
- name: ntp-compliance
  severity: error
  select: $all_routers
  check:
    type: use_rule
    name: ntp-configured
  help: Router is missing NTP — PCI-DSS 10.4 requires synchronised clocks.

- name: syslog-compliance
  severity: error
  select: $all_routers
  check:
    type: use_rule
    name: syslog-configured
  help: Router is missing syslog — PCI-DSS 10.5 requires centralised logging.

- name: all-have-hostname
  severity: error
  select: $all_routers
  check:
    type: field_exists
    field: hostname
  help: A router is missing its hostname field.

- name: inventory-schema
  severity: error
  select: $all_routers
  check:
    type: match_schema
    schema:
      type: object
      required:
      - hostname
      - serial_number
      - firmware_version
      - site
      properties:
        hostname:
          type: string
          minLength: 1
        serial_number:
          type: string
          pattern: "^[A-Z0-9]{9,14}$"
        firmware_version:
          type: string
        site:
          type: string
          enum: [london-primary, london-dr, new-york]
  help: Router is missing required inventory fields — SOX audit trail requirement.

- name: hostnames-unique-per-site
  severity: error
  select: $all_routers
  check:
    type: unique_per_group
    field: hostname
    group_by: site
  help: Duplicate hostname within the same site.

rules:
  ntp-configured: has(node.ntp_servers) and size(node.ntp_servers) >= 2
  syslog-configured: has(node.syslog_server) and node.syslog_server != ''

groups:
  core: nodes[role=='core']
  dmz: nodes[role=='dmz']
  trading: nodes[role=='trading']
  management: nodes[role=='management']
  core_and_dmz: $core | $dmz
  core_and_trading: $core | $trading
  core_and_mgmt: $core | $management
  all_routers: $core | $dmz | $trading | $management
  core_links: edges[tier=='core']
  dmz_links: edges[tier=='dmz']
  trading_links: edges[tier=='trading']

blast_radius:
- name: strict-deletion-limit
  select: null
  check:
    type: max_deletions
    count: 1
- name: strict-change-cap
  select: null
  check:
    type: max_percentage_changed
    value: 5.0

transforms:
- name: junos-interface-rewrite
  when: device_os == 'junos'
  rules:
  - match_expr: stanza.kind == 'interface'
    apply:
      name: "'xe-0/0/' + string(stanza.fields.abstract_index)"

hardware_library:
  chassis: []
  linecards: []


Usage

DSL Transformation Example

# DSL Transformation Rule
# Applied when device_os matches 'nxos'
transformations:
  - name: nxos_lowering
    when: "device_os == 'nxos'"
    rules:
      - match: "kind == 'interface' && name.startsWith('Ethernet')"
        apply:
          name: "name + '/1'"
          mtu: 9216  # Force jumbo frames

Architecture

flowchart LR P[Parse] --> T[Transform] T --> L[Lower] L --> R[Render] R --> E[Emit] style P fill:#e3f2fd style T fill:#e3f2fd style L fill:#e3f2fd style R fill:#e3f2fd style E fill:#e3f2fd

The pipeline runs in five phases:

  1. Parse — Read the blueprint YAML, resolve imports, validate structure.
  2. Transform — Execute primitives (mesh_nodes, provision_ips, build_protocol_layer) against the topology graph. Each primitive reads from and writes to node/edge data.
  3. Lower — Apply mapping rules that convert graph data into vendor-intermediate representation (DeviceIR). Selectors match nodes by role, site, or layer.
  4. Render — MiniJinja templates consume DeviceIR stanzas and produce vendor-specific CLI syntax. Template selection is driven by device_os metadata.
  5. Emit — Write .cfg files per device, using atomic rename for transactional output.

Vendor abstraction lives in the lowering and rendering phases. The same blueprint produces Arista EOS, Cisco NX-OS, or Juniper Junos output by swapping the mapping rules and template set. The topology graph and primitive execution are vendor-agnostic.

flowchart TD BGP["bgp layer
requires: addressing"] ADDR["addressing layer
requires: pools, topology"] POOLS["pools layer"] TOPO["topology layer"] BGP --> ADDR ADDR --> POOLS ADDR --> TOPO

Error reporting uses miette for source-snippet diagnostics: blueprint validation errors, IP pool exhaustion, and selector mismatches all produce span-highlighted terminal output pointing to the relevant YAML lines.


Quick Facts

   
Status Active
Stack Rust

What This Is

An implementation of the Multi-Abstraction Layer Topology (MALT), built as an enterprise Rust compiler for deterministic network configuration generation. NetCfg treats configuration as a compilation problem rather than a string templating exercise, translating high-level Whiteboard-to-Plan (W2P) architectural intent into target-specific configurations via AST lowering.

The netcfg binary orchestrates: blueprint parsing → topology transformation → DeviceIR generation → AST lowering (TSDM) → template rendering → traceable config file emission.


Current Milestone: v3.0 Policy Verification, QoS & IPv6

Goal: Complete the security policy verification engine, add interface-level zone membership, introduce a full QoS policy DSL (classification, marking, queuing, policing, shaping), and add IPv6 support (NAT64, NPTv6, IPv6 IPAM pool provisioning).

Target features:


Previous State (v2.2 shipped 2026-03-09)

The compiler delivers a complete security policy DSL with full vendor rendering:


Core Value

Single-binary network compiler: design, transform, and generate configs from YAML blueprints without Python.


Requirements


# Active (v3.0)


# Validated (v2.2)


# Deferred


# Validated (v1.0 - v2.1)


Key Decisions

Decision Rationale Outcome
Topology::from_core() wrapper bridges CoreTopology into Evaluator Evaluator tightly coupled to Python-wrapper type; wrapping at phase boundary is cleanest ✓ Good
NamedTempFile::new_in(parent) for transactional output Same-filesystem guarantee enables atomic POSIX rename ✓ Good
Clap 4 Args wrapper struct (ConfigCommand) containing Subcommand enum Matches Clap 4 nested subcommand pattern, consistent with BlueprintCommand ✓ Good
RenderEngine::render_node per-node API Clean separation between transformation and configuration generation ✓ Good
miette for diagnostic reporting Provides out-of-the-box snippet and span highlighting for YAML errors ✓ Good
Groups as first-class Blueprint section Zone declarations are group variants — no separate primitive required ✓ Good
GroupSpec untagged enum Selector(String) shorthand + Full{} for backward compat ✓ Good
Zone auto-tagging before primitives Metadata stamped in runner before primitive loop — primitives query via selectors ✓ Good
NAT separate from zone policy Distinct stanza kinds mirror vendor evaluation order (NAT → security policy) ✓ Good
Objects section for reuse Named address/service objects resolved to concrete values before stanza emission ✓ Good
Protocol library YAML-only No Rust changes needed — build_protocol_layer is already protocol-agnostic ✓ Good

Constraints

Last updated: 2026-03-09 — v3.0 milestone started