Skip to content

DOT File Syntax

sat edited this page Sep 17, 2025 · 2 revisions

DOT File Syntax

This page explains how to write DOT files for dot2net, starting from basic node and interface definitions to advanced connection management and labeling techniques.

Overview

DOT files define network topology using the Graphviz DOT language. In dot2net, DOT files describe physical network components and their relationships:

Network Components in DOT

  • Nodes represent network devices (routers, switches, hosts, etc.)
  • Edges represent physical connections between devices (typically L2 links)
  • Edge endpoints represent network interfaces on each device

Basic Approach

  1. Define devices as nodes in the graph
  2. Define connections as edges between nodes
  3. Specify device and interface roles using attributes (labels)

For example, when you write router1 -> switch1, you're describing:

  • Two devices: router1 and switch1
  • A physical connection between them
  • Two interfaces: one on the router side, one on the switch side

Attributes for Role Description

To specify what type each component is and how it should behave, you assign labels using DOT attributes:

  • Node attributes describe device types (router, switch, host)
  • Edge attributes describe interface types and connection properties
  • These labels determine how each component will be configured

DOT files are treated as directed multigraphs, allowing multiple connections between devices with different configurations.

Basic Approach: Nodes and Interfaces

Now let's see how to actually write DOT files using this network component approach.

1. Defining Network Devices

Start by defining your network devices as nodes, specifying what type each device is:

digraph {
    # Define network devices and their types
    r1 [xlabel="router"];     # Router device
    r2 [xlabel="router"];     # Another router
    sw1 [xlabel="switch"];    # Switch device
    host1 [xlabel="host"];    # Host/computer
}

The xlabel attribute tells dot2net what type of device this is, which determines how it will be configured.

Alternative ways to specify device types:

r1 [xlabel="router"];    # Standard - shows device type in visualizations
r2 [class="router"];     # Optional - doesn't show in visualizations
r3 [conf="router"];      # Optional
r4 [info="router"];      # Optional

2. Defining Connections and Interfaces

When you connect devices with edges, you're creating both a physical link and two interfaces (one on each end). You can specify what type each interface should be:

digraph {
    r1 [xlabel="router"];
    r2 [xlabel="router"];

    # Basic connection with interface classes
    r1 -> r2 [
        dir="none",
        taillabel="ethernet_interface",  # r1-side interface class
        headlabel="ethernet_interface"   # r2-side interface class
    ];
}

Different interface types on each end:

# Router connected to switch with different interface types
r1 -> sw1 [
    dir="none",
    taillabel="router_port",    # Router side
    headlabel="switch_port"     # Switch side
];

3. Edge Notation Best Practices

✅ Recommended: Directed edges with dir="none"

r1 -> r2 [dir="none", taillabel="vlan_interface"];

❌ Avoid: Undirected edges

r1 -- r2 [taillabel="vlan_interface"];  # Limited functionality

Why directed edges?

  • Enables precise headlabel/taillabel control
  • Better support for asymmetric configurations
  • More explicit about connection directionality

4. Basic Complete Example

digraph simple_network {
    # Node definitions
    r1 [xlabel="router"];
    r2 [xlabel="router"];
    sw1 [xlabel="switch"];
    host1 [xlabel="host"];

    # Router-to-router connection
    r1 -> r2 [
        dir="none",
        taillabel="trunk_interface",
        headlabel="trunk_interface"
    ];

    # Router-to-switch connection
    r1 -> sw1 [
        dir="none",
        taillabel="trunk_interface",
        headlabel="switch_trunk"
    ];

    # Switch-to-host connection
    sw1 -> host1 [
        dir="none",
        taillabel="switch_access",
        headlabel="host_interface"
    ];
}

Group Organization

Subgraph (Cluster) Classification

Organize related nodes using subgraphs:

digraph datacenter {
    # Datacenter group
    subgraph cluster_datacenter {
        label="datacenter_east";    # GroupClass specification
        class="datacenter_east";    # equivalent to label
        r1; r2; r3;
    }

    # Host group
    subgraph cluster_hosts {
        label="host_group";
        host1; host2; host3;
    }

    # Connections between groups
    r1 -> host1 [dir="none", taillabel="router_port", headlabel="host_interface"];
}

Variable Assignment

Direct Variable Assignment

Assign specific values directly in the DOT file:

digraph {
    # Node variables
    r1 [xlabel="router; hostname=main-router; router_id=1.1.1.1"];

    # Interface-specific variables
    r1 -> r2 [
        dir="none",
        taillabel="ethernet_interface",
        headlabel="ethernet_interface",
        label="vlan_id=100; bandwidth=1G"
    ];
}

Interface Port Names

Specify interface names explicitly when needed:

# Explicit interface naming
r1:eth0 -> r2:eth1 [dir="none"];
r1:eth1 -> sw1:port1 [dir="none"];

Detailed Specification Reference

Label System Overview

dot2net uses multiple types of labels in DOT files, which fall into two main categories:

Class Assignment Labels

These labels specify which YAML class definitions to use for objects:

Label Type Purpose Example Description
Class Labels Direct class assignment xlabel="router" Assign objects to specific class definitions
Relational Class Labels Related object class assignment label="segment#vlan_seg" Assign classes to automatically detected related objects

Parameter Namespace Control Labels

These labels control parameter assignment and cross-object references:

Label Type Purpose Example Description
Value Labels Direct variable assignment class="router; hostname=main-router" Set specific parameter values directly
Place Labels Object referencing label="@main_router" Make objects referenceable by name
Meta Value Labels Reference aliases label="@backup=main_router" Create alternative names for existing references

Class Label System

DOT class labels correspond to YAML class definitions. Here's the complete mapping:

DOT Component Class Label in DOT YAML Class Definition Purpose
Node xlabel="router" nodeclass: - name: router Device type and configuration
Interface taillabel="trunk" or headlabel="trunk" interfaceclass: - name: trunk Interface type and configuration
Connection label="vlan_conn" connectionclass: - name: vlan_conn Connection-level configuration
Group label="datacenter" (in subgraph) groupclass: - name: datacenter Group/cluster configuration
Segment label="segment#vlan_seg" segmentclass: - name: vlan_seg Network segment configuration

Each class label in your DOT file must have a corresponding class definition in your YAML configuration file.

Node Class Labels

Specify which nodeclass definition to use:

# DOT specification (standard)
r1 [xlabel="router"];
sw1 [xlabel="switch"];
host1 [xlabel="host"];

# Alternative attributes (all equivalent)
r2 [class="router"];
r3 [conf="router"];
r4 [info="router"];

Interface Class Labels

Specify which interfaceclass definition to use:

# Direct interface specification (recommended)
A -> B [
    dir="none",
    taillabel="trunk_interface",    # A-side uses interfaceclass "trunk_interface"
    headlabel="access_interface"    # B-side uses interfaceclass "access_interface"
];

# Alternative attributes
A -> B [
    dir="none",
    tailclass="trunk_interface",    # Equivalent to taillabel
    headclass="access_interface"    # Equivalent to headlabel
];

# Both interfaces use same class (indirect specification)
A -> B [label="interface#ethernet_interface", dir="none"];

Connection Class Labels

Specify which connectionclass definition to use:

# Connection class specification
A -> B [
    dir="none",
    class="vlan_conn",              # Uses connectionclass "vlan_conn"
    taillabel="trunk_interface",    # A-side interface
    headlabel="trunk_interface"     # B-side interface
];

# Alternative attribute
A -> B [label="vlan_conn", dir="none"];

Group Class Labels

Specify which groupclass definition to use for subgraphs:

subgraph cluster_datacenter {
    label="datacenter_east";        # Uses groupclass "datacenter_east"
    # Alternative
    class="datacenter_east";        # Equivalent to label

    r1; r2; r3;
}

Segment Class Labels (Relational)

Specify which segmentclass definition to use:

# Segment specification using hash notation
A -> B [
    dir="none",
    class="vlan_conn; segment#backbone_segment",  # Connection + Segment
    taillabel="trunk_interface"
];

# Segment only
A -> B [label="segment#access_segment", dir="none"];

Note: This is an example of Relational Class Labels - labels that assign classes to related objects that are automatically detected by dot2net. See the Relational Class Labels section below for details.

Multiple Label Specification

Combine multiple class specifications:

# Multiple classes with semicolon separator
A -> B [
    dir="none",
    class="trunk_conn; segment#backbone",        # ConnectionClass + SegmentClass
    taillabel="router_trunk",                    # InterfaceClass for A side
    headlabel="switch_trunk"                     # InterfaceClass for B side
];

# With variable assignment
r1 [xlabel="router; hostname=main-router; router_id=1.1.1.1"];

Relational Class Labels

Relational Class Labels allow you to assign classes to related objects that are automatically detected by dot2net, rather than objects explicitly defined in the DOT file.

Segment Class Assignment

The most common relational class label is for network segments:

# Assign SegmentClass to the segment containing this connection
A -> B [
    dir="none",
    class="trunk_conn; segment#backbone_segment",  # ConnectionClass + SegmentClass
    taillabel="trunk_interface"
];

How it works:

  1. dot2net automatically detects network segments from connection topology
  2. The segment#class_name notation assigns the specified SegmentClass to the detected segment
  3. All connections in the same segment share the same SegmentClass configuration

Interface Class Indirect Assignment

Alternative method for assigning the same InterfaceClass to both ends of a connection:

# Both interfaces use the same InterfaceClass
A -> B [label="interface#ethernet_interface", dir="none"];

Note: This is equivalent to:

A -> B [
    dir="none",
    taillabel="ethernet_interface",
    headlabel="ethernet_interface"
];

Syntax Rules

  • Segment labels: Use segment#class_name (hash #, not colon :)
  • Interface labels: Use interface#class_name (hash #, not colon :)
  • Combination: Can combine with regular class labels using semicolon: class="conn_class; segment#seg_class"

Parameter Namespace Control Labels

These labels control how parameters are assigned and referenced across objects.

Value Labels (Direct Variable Assignment)

Assign specific parameter values within class/label attributes using semicolon separation:

DOT Definition:

r1 [xlabel="router; hostname=main-router; router_id=1.1.1.1"];
A -> B [
    dir="none",
    label="vlan_conn; vlan_name=TRUNK_A; bandwidth=1G"
];

Template Parameter Access:

# In node r1's template
template:
  - "hostname {{ .hostname }}"           # → "hostname main-router"
  - "router ospf"
  - " router-id {{ .router_id }}"        # → " router-id 1.1.1.1"

# In connection A->B template
template:
  - "# Connection: {{ .vlan_name }}"     # → "# Connection: TRUNK_A"
  - "interface description {{ .bandwidth }} link"  # → "interface description 1G link"

Effect: Value Labels create custom variables accessible only within the object's own template namespace.

Place Labels (Object References)

Make objects referenceable by name using @ notation within label attributes:

DOT Definition:

r1 [xlabel="router", label="@main_router"];  # r1 can be referenced as "main_router"
r2 [xlabel="router"];
A -> B [label="@backbone_link", dir="none"];  # Connection can be referenced as "backbone_link"

Template Parameter Access:

# In node r2's template (referencing r1)
template:
  - "# Primary router: {{ .main_router_name }}"     # → "# Primary router: r1"
  - "next-hop {{ .main_router_ip_loopback }}"       # → "next-hop 10.0.255.1"

# In interface template (referencing backbone connection)
template:
  - "# Connected via {{ .backbone_link_name }}"     # → "# Connected via conn0"
  - "description Backbone link"

Effect: Place Labels allow any object to reference the labeled object's parameters using label_name_parameter format.

Meta Value Labels (Reference Aliases)

Create alternative names pointing to existing references using @alias=target notation:

DOT Definition:

r1 [xlabel="router", label="@main_router"];      # Original place label
r2 [xlabel="router", label="@backup=main_router"]; # Alias pointing to main_router
r3 [xlabel="router", label="@target=main_router"]; # Another alias to main_router

Template Parameter Access:

# In node r2's template
template:
  - "# Backup of: {{ .backup_name }}"           # → "# Backup of: r1"
  - "router-id {{ .backup_ip_loopback }}"       # → "router-id 10.0.255.1"

# In node r3's template
template:
  - "# Target router: {{ .target_name }}"       # → "# Target router: r1"
  - "peer {{ .target_ip_loopback }}"            # → "peer 10.0.255.1"

Effect: Meta Value Labels create aliases that reference the same object as the original place label, allowing multiple names for the same reference.

Parameter Namespace Summary

Label Type DOT Syntax Template Access Scope
Value Label label="class; var=value" {{ .var }} Local object only
Place Label label="@ref_name" {{ .ref_name_parameter }} Global (any object)
Meta Value Label label="@alias=ref_name" {{ .alias_parameter }} Global (any object)

Label Character Restrictions

The following characters have special parsing meaning in dot2net and will be interpreted as label syntax rather than part of class names or variable values:

Character Parsing Role Consequence
@ Place Label prefix detector router@main → interpreted as Place Label @main, not class name
= Value/Meta Value Label separator router=backup → interpreted as Value Label with empty class name
# Relational Class Label separator router#primary → interpreted as Relational Class Label
; Multiple label separator Splits into separate labels
, Alternative multiple label separator Splits into separate labels

Safe characters for class names and values:

  • Letters (a-z, A-Z)
  • Numbers (0-9)
  • Underscore (_)
  • Hyphen (-)

Examples of parsing behavior:

# ✅ Correctly parsed as intended
r1 [xlabel="main_router"];           # Class: "main_router"
r2 [xlabel="router; hostname=main"]; # Class: "router", Value Label: hostname="main"

# ❌ Misinterpreted due to special characters
r3 [xlabel="router@main"];    # Parsed as: Place Label "@main", no class assigned
r4 [xlabel="router=backup"];  # Parsed as: Value Label router="backup", no class assigned
r5 [xlabel="router#primary"]; # Parsed as: Relational Class Label router#primary

Note: Place Labels and Meta Value Labels use the label attribute and may affect visualization. Use class attribute for these when you want to avoid visual display.

Complete Example

OSPF Network Topology (Basic Approach)

This example demonstrates the recommended basic approach using NodeClass and InterfaceClass:

digraph ospf_network {
    # Node definitions with NodeClass
    r1 [xlabel="router; hostname=core-router-1"];
    r2 [xlabel="router; hostname=core-router-2"];
    r3 [xlabel="router; hostname=edge-router-1"];
    sw1 [xlabel="switch; hostname=access-switch-1"];
    host1 [xlabel="host"];
    host2 [xlabel="host"];

    # Core router connections (basic interface class)
    r1 -> r2 [
        dir="none",
        taillabel="core_interface",
        headlabel="core_interface"
    ];

    # Router to edge router
    r2 -> r3 [
        dir="none",
        taillabel="core_interface",
        headlabel="edge_interface"
    ];

    # Router to switch (different interface types)
    r3 -> sw1 [
        dir="none",
        taillabel="router_trunk",
        headlabel="switch_trunk"
    ];

    # Switch to hosts (access ports)
    sw1 -> host1 [
        dir="none",
        taillabel="switch_access",
        headlabel="host_interface"
    ];

    sw1 -> host2 [
        dir="none",
        taillabel="switch_access",
        headlabel="host_interface"
    ];

    # Host grouping
    subgraph cluster_lan {
        label="lan_segment";
        host1; host2;
    }
}

Advanced VLAN Network (Using Advanced Features)

When you need more sophisticated network management, you can combine the basic approach with advanced features:

digraph vlan_network {
    # Router nodes (basic NodeClass)
    r1 [xlabel="router; hostname=core-router-1"];
    r2 [xlabel="router; hostname=core-router-2"];
    sw1 [xlabel="switch; hostname=access-switch-1"];
    sw2 [xlabel="switch; hostname=access-switch-2"];

    # Host groups
    subgraph cluster_hosts_vlan10 {
        label="vlan10_hosts";
        host1 [xlabel="host"];
        host2 [xlabel="host"];
    }

    # Core connections (basic InterfaceClass only)
    r1 -> r2 [
        dir="none",
        taillabel="core_interface",
        headlabel="core_interface",
        bandwidth="10G"
    ];

    # VLAN trunk connections (ConnectionClass + InterfaceClass + Segment)
    r1 -> sw1 [
        dir="none",
        class="trunk_conn; segment#backbone",  # Advanced: ConnectionClass and Segment
        taillabel="router_trunk",              # Basic: InterfaceClass
        headlabel="switch_trunk",              # Basic: InterfaceClass
        vlan_name="TRUNK_A"
    ];

    r2 -> sw2 [
        dir="none",
        class="trunk_conn; segment#backbone",
        taillabel="router_trunk",
        headlabel="switch_trunk",
        vlan_name="TRUNK_B"
    ];

    # Host access connections (combining basic and advanced)
    sw1 -> host1 [
        dir="none",
        class="access_conn; segment#vlan10",   # Advanced: ConnectionClass and Segment
        taillabel="switch_access",             # Basic: InterfaceClass
        headlabel="host_interface"             # Basic: InterfaceClass
    ];

    sw2 -> host2 [
        dir="none",
        class="access_conn; segment#vlan10",
        taillabel="switch_access",
        headlabel="host_interface"
    ];
}

Best Practices

1. Start Simple

  • Begin with NodeClass and InterfaceClass using headlabel/taillabel
  • Add ConnectionClass and segments only when needed for complex scenarios

2. Consistent Edge Notation

  • Always use -> with dir="none" for physical connections
  • Reserve directed edges (-> without dir="none") for logical relationships

3. Clear Interface Specification

  • Use taillabel and headlabel for explicit interface control
  • Choose descriptive interface class names that reflect functionality

4. Progressive Enhancement

  • Master basic node and interface definitions first
  • Gradually add advanced features (ConnectionClass, segments) as needed

5. Variable Assignment Strategy

  • Assign topology-specific variables in DOT files
  • Use YAML configuration for class-wide defaults and templates

Migration from Complex Syntax

❌ Avoid These Patterns

# Overly complex without clear benefit
A -> B [class="complex_conn; segment#seg1; interface#iface", dir="none"];

# Missing dir="none" for physical connections
A -> B [taillabel="interface"];       # Should have dir="none"

# Inconsistent interface specification
A -> B [label="interface_type"];      # Use taillabel/headlabel instead

✅ Recommended Patterns

# Clear basic approach
A -> B [dir="none", taillabel="trunk_port", headlabel="access_port"];

# Progressive enhancement when needed
A -> B [
    dir="none",
    class="vlan_conn",              # Add ConnectionClass if needed
    taillabel="trunk_port",         # Keep clear InterfaceClass
    headlabel="access_port",
    label="vlan_id=100"             # Add variables as needed
];

The key is to start with the fundamental NodeClass and InterfaceClass approach, then gradually add advanced features only when they provide clear value for your specific network scenario.

Q&A

Why are there multiple ways to specify device types (class, xlabel, conf, info)?

DOT language officially supports certain attributes like label, xlabel, class, etc., which can affect how graphs are visually rendered when using Graphviz for visualization. dot2net provides multiple options to avoid conflicts:

  • Display-affecting attributes: label, xlabel, taillabel, headlabel - these show up in visual graph rendering
  • Non-display attributes: conf, info - these don't affect visual rendering

This allows you to:

  1. Use label/xlabel when you want the device type to appear in visualizations
  2. Use class/conf/info when you want to keep the visualization clean
  3. Avoid conflicts between dot2net configuration needs and visualization requirements

Why use xlabel for nodes instead of label?

For nodes, dot2net uses xlabel as the standard for several reasons:

  1. Consistent labeling system: Maintains consistency with interface labeling (taillabel/headlabel) and connection labeling (label)
  2. Avoid DOT syntax conflicts: Some DOT language constructs may conflict with label usage on nodes
  3. Better visualization: xlabel displays near the node externally, while label replaces the node name internally
  4. Preserve node identity: Users can see both the node name (r1, sw1) and device type (router, switch) clearly

For most users: Use xlabel for nodes and taillabel/headlabel for interfaces to follow the recommended labeling system.

Why use directed edges (→) with dir="none" instead of undirected edges (--)?

Directed edges with dir="none" provide more control:

  • Enable precise taillabel/headlabel specification for each interface
  • Support asymmetric configurations where each end needs different settings
  • Maintain compatibility with advanced features
  • Provide clearer semantics about connection endpoints

Undirected edges (--) work for basic scenarios but have limited functionality for complex network configurations.

When should I use advanced features like ConnectionClass and segments?

Start with the basic NodeClass + InterfaceClass approach. Add advanced features only when you need:

  • ConnectionClass: When the connection itself needs configuration (VLAN trunks, bandwidth settings, etc.)
  • Segments: When multiple connections share properties and should be managed together
  • Complex scenarios: Large networks with sophisticated requirements

The basic approach handles most network scenarios effectively.

Multiple DOT File Input

dot2net supports specifying multiple DOT files in a single command, enabling modular topology management and easier organization of complex networks.

Basic Usage

# Multiple DOT files
dot2net build -c config.yaml topology1.dot topology2.dot topology3.dot

# Mixed with other arguments
dot2net build -c config.yaml core_network.dot access_layer.dot wan_links.dot

Merge Behavior

When multiple DOT files are provided, dot2net merges all nodes and connections from all files using the following rules:

1. Node Merging

Nodes with identical names across files are treated as the same node:

File 1 (core.dot):

digraph {
    r1 [xlabel="router"];
    r2 [xlabel="router"];
    r1 -> r2;
}

File 2 (access.dot):

digraph {
    r1 [class="bgp_router"];  # Same node as in core.dot
    r3 [xlabel="router"];
    r1 -> r3;
}

Merged Result:

  • Node r1 combines labels: xlabel="router" + class="bgp_router"
  • Node r2 from core.dot
  • Node r3 from access.dot
  • Connections: r1->r2 and r1->r3

2. Connection Merging

Connections between identical interface endpoints are treated as the same connection:

File 1:

digraph {
    r1:eth0 -> r2:eth0 [label="trunk"];
}

File 2:

digraph {
    r1:eth0 -> r2:eth0 [class="vlan_100"];  # Same connection
}

Merged Result:

  • Single connection r1:eth0 -> r2:eth0
  • Combined labels: label="trunk" + class="vlan_100"

3. Label Merging Rules

  • Multiple class labels: Separated by semicolons (router; bgp_router)
  • Value labels: Later files override earlier files for same variable names
  • Place labels: Must be unique across all files (conflict causes error)

Practical Use Cases

1. Modular Network Design

# Separate logical components
dot2net build -c config.yaml \
    physical_topology.dot \
    bgp_overlays.dot \
    management_network.dot

2. Layer Separation

# Physical and logical layers
dot2net build -c config.yaml \
    layer1_physical.dot \
    layer2_switching.dot \
    layer3_routing.dot

3. Incremental Development

# Base + additions
dot2net build -c config.yaml \
    base_network.dot \
    new_features.dot \
    testing_additions.dot

Important Considerations

Manual Interface Naming Required

For connection merging to work, interface names must be explicitly specified in DOT files:

# ✅ Correct - explicit interface names
r1:eth0 -> r2:eth1 [label="trunk"];

# ❌ Incorrect - auto-generated names won't merge properly
r1 -> r2 [label="trunk"];

Node Identity

Node identity is determined solely by name. Nodes with different names are always separate, even if they represent the same physical device.

Processing Order

Files are processed in command-line order. Later files can override Value Labels from earlier files.

Conflict Resolution

  • Duplicate Place Labels: Error - must be unique across all files
  • Duplicate Value Labels: Later files override earlier files
  • Duplicate Class Labels: Merged (no conflict)

Example: Multi-File Network

core.dot:

digraph core {
    subgraph cluster_dc1 {
        label = "datacenter";
        r1 [xlabel="router"];
        r2 [xlabel="router"];
    }
    r1:eth0 -> r2:eth0 [dir="none"];
}

access.dot:

digraph access {
    r1 [class="bgp_router"];  # Extends core r1
    s1 [xlabel="switch"];
    s2 [xlabel="switch"];
    r1:eth1 -> s1:eth0 [dir="none"];
    r1:eth2 -> s2:eth0 [dir="none"];
}

wan.dot:

digraph wan {
    r1 [hostname="core-router-1"];  # Value label for core r1
    r3 [xlabel="router", region="remote"];
    r1:wan0 -> r3:wan0 [label="wan_link", dir="none"];
}

Command:

dot2net build -c config.yaml core.dot access.dot wan.dot

Result:

  • r1: Combined router with classes router + bgp_router, hostname core-router-1
  • r2: Core router only
  • s1, s2: Access switches
  • r3: Remote router with region parameter
  • All connections merged appropriately

This approach enables modular network design while maintaining the simplicity of single-file topology when that's sufficient.

Clone this wiki locally