-
Notifications
You must be signed in to change notification settings - Fork 1
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.
DOT files define network topology using the Graphviz DOT language. In dot2net, DOT files describe physical network components and their relationships:
- 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
- Define devices as nodes in the graph
- Define connections as edges between nodes
- Specify device and interface roles using attributes (labels)
For example, when you write router1 -> switch1, you're describing:
- Two devices:
router1andswitch1 - A physical connection between them
- Two interfaces: one on the router side, one on the switch side
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.
Now let's see how to actually write DOT files using this network component approach.
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"]; # OptionalWhen 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
];✅ Recommended: Directed edges with dir="none"
r1 -> r2 [dir="none", taillabel="vlan_interface"];❌ Avoid: Undirected edges
r1 -- r2 [taillabel="vlan_interface"]; # Limited functionalityWhy directed edges?
- Enables precise
headlabel/taillabelcontrol - Better support for asymmetric configurations
- More explicit about connection directionality
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"
];
}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"];
}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"
];
}Specify interface names explicitly when needed:
# Explicit interface naming
r1:eth0 -> r2:eth1 [dir="none"];
r1:eth1 -> sw1:port1 [dir="none"];dot2net uses multiple types of labels in DOT files, which fall into two main categories:
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 |
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 |
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.
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"];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"];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"];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;
}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.
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 allow you to assign classes to related objects that are automatically detected by dot2net, rather than objects explicitly defined in the DOT file.
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:
- dot2net automatically detects network segments from connection topology
- The
segment#class_namenotation assigns the specified SegmentClass to the detected segment - All connections in the same segment share the same SegmentClass configuration
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"
];-
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"
These labels control how parameters are assigned and referenced across objects.
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.
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.
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_routerTemplate 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.
| 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) |
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#primaryNote: 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.
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;
}
}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"
];
}- Begin with NodeClass and InterfaceClass using
headlabel/taillabel - Add ConnectionClass and segments only when needed for complex scenarios
- Always use
->withdir="none"for physical connections - Reserve directed edges (
->withoutdir="none") for logical relationships
- Use
taillabelandheadlabelfor explicit interface control - Choose descriptive interface class names that reflect functionality
- Master basic node and interface definitions first
- Gradually add advanced features (ConnectionClass, segments) as needed
- Assign topology-specific variables in DOT files
- Use YAML configuration for class-wide defaults and templates
# 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# 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.
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:
- Use
label/xlabelwhen you want the device type to appear in visualizations - Use
class/conf/infowhen you want to keep the visualization clean - Avoid conflicts between dot2net configuration needs and visualization requirements
For nodes, dot2net uses xlabel as the standard for several reasons:
-
Consistent labeling system: Maintains consistency with interface labeling (
taillabel/headlabel) and connection labeling (label) -
Avoid DOT syntax conflicts: Some DOT language constructs may conflict with
labelusage on nodes -
Better visualization:
xlabeldisplays near the node externally, whilelabelreplaces the node name internally - 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.
Directed edges with dir="none" provide more control:
- Enable precise
taillabel/headlabelspecification 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.
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.
dot2net supports specifying multiple DOT files in a single command, enabling modular topology management and easier organization of complex networks.
# 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.dotWhen multiple DOT files are provided, dot2net merges all nodes and connections from all files using the following rules:
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
r1combines labels:xlabel="router"+class="bgp_router" - Node
r2from core.dot - Node
r3from access.dot - Connections:
r1->r2andr1->r3
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"
-
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)
# Separate logical components
dot2net build -c config.yaml \
physical_topology.dot \
bgp_overlays.dot \
management_network.dot# Physical and logical layers
dot2net build -c config.yaml \
layer1_physical.dot \
layer2_switching.dot \
layer3_routing.dot# Base + additions
dot2net build -c config.yaml \
base_network.dot \
new_features.dot \
testing_additions.dotFor 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 is determined solely by name. Nodes with different names are always separate, even if they represent the same physical device.
Files are processed in command-line order. Later files can override Value Labels from earlier files.
- Duplicate Place Labels: Error - must be unique across all files
- Duplicate Value Labels: Later files override earlier files
- Duplicate Class Labels: Merged (no conflict)
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.dotResult:
-
r1: Combined router with classesrouter + bgp_router, hostnamecore-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.