-
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"
];
}A group is how a set of nodes shares something. An AS number, an OSPF area, the
machine a set of containers is deployed to — values that belong to the set rather
than to any one node, and that a template reads with {{ .group_<param> }}. A group
can also aggregate its own members only, which is what makes one output file per
machine possible. What a group can carry is
GroupClass.
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"
];
}node:port names that interface. Without it, dot2net names the interface itself
— eth0, eth1, ... — or from the prefix its class carries.
# Explicit interface naming
r1:eth0 -> r2:eth1 [dir="none"];
r1:eth1 -> sw1:port1 [dir="none"];Name them when the name has to be a particular one: a configuration written elsewhere refers to it, or two ends of one link must be told apart in a merged connection. Otherwise leave it — a name dot2net hands out cannot collide and cannot leave a gap.
Naming an interface and generating for Kathara are mutually exclusive.
Kathara reads an interface's name from its position in lab.conf, so the
interfaces it lays are eth0, eth1, ... and a name of the topology's choosing
is rejected — see
Module: Kathara. This applies only
to interfaces Kathara puts there: one the node builds for itself
(deploy: logical — a tunnel end, a bridge) never reaches lab.conf and keeps
whatever name it was given.
example/naming shows the whole family — node, interface and connection names,
written and generated — and says which of them Kathara can take.
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" or xlabel="vlan_conn"
|
connectionclass: - name: vlan_conn |
Connection-level configuration |
| Group |
label="datacenter" or xlabel="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 — what those definitions can say is the class system.
xlabel works on edges and subgraphs too, not only on nodes. Graphviz draws
label on an edge as text along it and on a subgraph as a caption, so a file
that carries classes in label renders with the class names written across the
picture. xlabel keeps the drawing clean. Both are read.
A node's label, on the other hand, is not read as a class: it is the name
Graphviz draws, and taking it as a class would turn every ordinary label into a
class that does not exist.
A lab larger than one machine is drawn as one graph, with each machine as a
subgraph of the worker class — the one class name dot2net reserves:
graph {
subgraph host1 { xlabel="worker"; r1; r2; }
subgraph host2 { xlabel="worker"; r3; r4; }
r1 -- r2;
r2 -- r3; # this link leaves a machine
r3 -- r4;
}dot2net works out which links leave a machine from the drawing, so no edge carries an annotation saying it a second time. What follows — one deployment file per machine, the bridges that replace a segment reaching across — is described in Placing nodes on machines.
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.
Use it to reach one particular device from every template. A route server, a DNS
server, a controller: every node's configuration needs its address, but the node is
not adjacent to them, so {{ .opp_* }} cannot get there. A place label gives that
one object a name, and any template can then read its parameters by that name.
Written with @ notation inside a label attribute:
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.
Use it to keep the choice of device out of the templates. Templates say
{{ .target_ip_loopback }}, and which node is target is decided in the DOT file.
Point the alias somewhere else and every template follows, with nothing in the YAML
to edit.
Written as @alias=target:
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 topologies
- 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 topology.
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 topologies 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 topologies: Large networks with sophisticated requirements
The basic approach handles most network topologies effectively.
Use it when one graph is really several. A physical wiring diagram plus an overlay drawn on top of it; a core network kept apart from the edges that change often; a base topology plus the piece you are currently working on. Each stays a file of its own, and dot2net merges them into one graph — so the pieces can be edited, reviewed and reused separately without ever being pasted together by hand.
# 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.