├── .gitignore ├── .rspec ├── .travis.yml ├── Gemfile ├── README.md ├── Rakefile ├── benchmarks └── flapping_interface.sh ├── config.yml ├── examples ├── README.md └── frr-isis-tutorial.yml ├── exe ├── netgen └── netgen-exec ├── lib ├── netgen.rb └── netgen │ ├── autogen.rb │ ├── config.rb │ ├── ipcalc.rb │ ├── libc.rb │ ├── linux_namespace.rb │ ├── node.rb │ ├── plugin.rb │ ├── plugins │ ├── bgpsimple.rb │ ├── bird.rb │ ├── dynamips.rb │ ├── ethernet.rb │ ├── frr.rb │ ├── holod.rb │ ├── iou.rb │ ├── ipv4.rb │ ├── ipv6.rb │ ├── mpls.rb │ ├── netem.rb │ ├── shell.rb │ ├── tcpdump.rb │ └── tmux.rb │ ├── router.rb │ ├── switch.rb │ ├── topology.rb │ └── version.rb ├── netgen.gemspec └── spec ├── netgen_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | 11 | # rspec failure tracking 12 | .rspec_status 13 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | rvm: 4 | - 2.3.1 5 | before_install: gem install bundler -v 1.15.1 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in netgen.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # netgen 2 | 3 | _The swiss army knife of network simulation._ 4 | 5 | `netgen` is a low footprint tool using linux namespaces to set 6 | up a topology of virtual nodes, network interfaces and switches on your 7 | local machine. There is a strong builtin support for `FRR` which can be 8 | configured to run on such a virtual node to simulate routing scenarios. 9 | Nevertheless `netgen` follows a plugin architecture and can be used in 10 | many different ways, not necessarily related to `FRR`. 11 | 12 | ## Installation 13 | 14 | ### Install ruby: 15 | 16 | Debian-based Linux distributions: 17 | 18 | ``` 19 | $ apt-get install ruby ruby-dev 20 | ``` 21 | 22 | RHEL/Fedora-based Linux distributions: 23 | 24 | ``` 25 | $ yum install ruby ruby-devel 26 | ``` 27 | 28 | ### Install the bundler gem (version 1.15 is needed): 29 | 30 | ``` 31 | $ gem install bundler -v 2.5.20 32 | $ bundle _2.5.20_ install 33 | ``` 34 | 35 | If you are getting timeouts you might have run into an [IPv6 issue](https://help.rubygems.org/discussions/problems/31074-timeout-error). 36 | On systemd enabled systems you can use 37 | 38 | ``` 39 | $ sysctl -w net.ipv6.conf.all.disable_ipv6=1 40 | $ sysctl -w net.ipv6.conf.default.disable_ipv6=1 41 | ``` 42 | 43 | to disable IPv6. 44 | 45 | ### Download and install netgen: 46 | 47 | ``` 48 | $ git clone https://github.com/rwestphal/netgen.git 49 | $ cd netgen/ 50 | $ bundle install 51 | $ bundle exec rake install 52 | ``` 53 | 54 | ## Usage 55 | 56 | Two configuration files are needed to set up a `netgen` topology: 57 | 58 | 1. The netgen configuration `config.yml` 59 | 2. The topology configuration, see e.g. `/examples/frr-isis-tutorial.yml` 60 | 61 | Then `netgen` can be started like this (using superuser permissions): 62 | 63 | ``` 64 | $ netgen -c config.yml topology.yml 65 | ``` 66 | 67 | By default the `config.yml` is taken from the current directory, so a 68 | 'quick' way to get something running would be for example: 69 | 70 | ``` 71 | $ netgen examples/frr-isis-tutorial.yml 72 | ``` 73 | 74 | If this doesn't work out, make sure you have `FRR` installed and 75 | executables (like `zebra`) in your `$PATH`. 76 | 77 | `netgen` follows a plugin architecture and those plugins can be 78 | configured in the `config.yml`. The most important plugin here is `frr`. 79 | Have a look into the provided example `config.yml` to get an overview. 80 | By default `netgen` stores all information in `/tmp/netgen` including 81 | PCAP files for all interfaces and `FRR` logs from every node. This 82 | makes introspection quite easy. 83 | 84 | 85 | ### Working with Nodes 86 | 87 | There are two ways of working on the nodes which are configured in the 88 | topology file, the `tmux` plugin or `netgen-exec` (again, you need 89 | superuser permission). 90 | 91 | By default a `tmux` session is created an accessible via: 92 | 93 | ``` 94 | $ /tmp/netgen/tmux.sh 95 | ``` 96 | 97 | Here you will see by default one tab per configured node. The tabs are 98 | named after the node name. 99 | 100 | Run a program directly using `netgen-exec` on a given node: 101 | 102 | ``` 103 | $ netgen-exec rt0 vtysh 104 | $ netgen-exec rt1 bash 105 | $ netgen-exec rt1 ifconfig 106 | ``` 107 | 108 | 109 | ### Topology Configuration 110 | 111 | There is an example topology configuration at `/examples/frr-isis-tutorial.yml` 112 | which will teach you how to 113 | 114 | * setup a node (with and without `FRR`) 115 | * setup interfaces 116 | * setup switches 117 | * use the `frr` plugin 118 | * use the `shell` plugin 119 | * introspect interfaces and nodes 120 | 121 | What is _not_ further explained here are networking and `FRR` related configuration 122 | basics. The example is about IS-IS and it is assumed that the reader is 123 | somewhat familiar with it. `FRR` configuration docu is available 124 | [here](http://docs.frrouting.org/en/latest/isisd.html). 125 | 126 | The example can be run using: 127 | 128 | ``` 129 | $ netgen examples/frr-isis-tutorial.yml 130 | ``` 131 | 132 | As explained above you can use `tmux` or `netgen-exec` to perform e.g. a ping 133 | test on the `src` node to check if the `dst` node is available by executing 134 | `ping 9.9.9.2`. It might take a minute until this test is successful because 135 | IS-IS distribution was not established yet. 136 | 137 | Note that by default the `tmux` session, PCAPs, logs etc. are available in 138 | `/tmp/netgen`: 139 | 140 | ``` 141 | $ ls /tmp/netgen/ 142 | frrlogs/ mounts/ pcaps/ perf/ pids.yml tmux.sh 143 | 144 | $ ls /tmp/netgen/pcaps/ 145 | dst/ rt1/ rt2/ rt3/ rt4/ rt5/ rt6/ src/ sw1/ 146 | ``` 147 | 148 | 149 | #### Basic Configuration Structure 150 | 151 | ``` 152 | routers: 153 | 154 | some_node: 155 | links: 156 | some_interface: 157 | peer: [some_other_node, some_other_interface] 158 | ipv4: 1.2.3.4/32 159 | [further interface configuration] 160 | frr: 161 | zebra: 162 | run: yes 163 | config: 164 | [further zebra config] 165 | [further FRR config] 166 | shell: | 167 | echo "Hello World!" 168 | [further shell commands executed at node start] 169 | some_other_plugin: 170 | [further plugin configuration] 171 | [further node configuration] 172 | 173 | some_other_node 174 | [node configuration] 175 | 176 | switches: 177 | sw1: 178 | links: 179 | some_switch_interface: 180 | peer: [peer-interface1, peer-interface2] 181 | [further interfaces] 182 | [further switch nodes] 183 | 184 | frr: 185 | perf: yes 186 | valgrind: yes 187 | base-configs: 188 | all: | 189 | hostname %{node} 190 | password 12345 191 | [further configuration for all FRR nodes] 192 | zebra: | 193 | debug zebra kernel 194 | [further zebra configuration for all nodes] 195 | [further configuration for other daemons on all nodes] 196 | ``` 197 | 198 | There is one very important thing here to remember: many 199 | configuration parts are forwarded to `FRR` and its daemons 200 | as literal blocks and those blocks must be preserved in 201 | YAML e.g. using the `|` sign. This also means that newlines 202 | must be taken special care of using `!` as connector in the 203 | following sense: 204 | 205 | ``` 206 | config: | 207 | some_config: 208 | [some sub configuration] 209 | ! 210 | some_other_config: 211 | [some other sub configuration] 212 | ``` 213 | 214 | 215 | ## Development 216 | 217 | TODO 218 | 219 | ## Contributing 220 | 221 | Bug reports and pull requests are welcome on GitHub at https://github.com/rwestphal/netgen. 222 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /benchmarks/flapping_interface.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | sleep 30 4 | for i in $(seq 1 100) ; do netgen-exec rt0 ifconfig rt0-stub0 down && sleep 1 && netgen-exec rt0 ifconfig rt0-stub0 up && sleep 1; done 5 | sleep 30 6 | -------------------------------------------------------------------------------- /config.yml: -------------------------------------------------------------------------------- 1 | # Directory used for Netgen's operation. 2 | # Default: "/tmp/netgen" 3 | # netgen_runstatedir: 4 | 5 | # Clean exit. 6 | # Default: 'false' 7 | # clean_exit: 8 | 9 | # Valgrind parameters. 10 | # Default: "--tool=memcheck" 11 | # valgrind_params: "--tool=memcheck --leak-check=full --trace-children=yes" 12 | # valgrind_params: "--tool=memcheck --leak-check=full" 13 | # valgrind_params: "--tool=memcheck --leak-check=full --show-leak-kinds=all" 14 | # valgrind_params: "--tool=callgrind --dump-instr=yes --collect-jumps=yes" 15 | 16 | # Perf directory 17 | # Default: [netgen_runstatedir]/perf 18 | # perf_dir: 19 | 20 | # Plugins configuration. 21 | plugins: 22 | frr: 23 | # FRR's sysconfdir (--sysconfdir). 24 | # Default: "/etc/frr" 25 | # sysconfdir: 26 | 27 | # FRR's localstatedir (--localstatedir). 28 | # Default: "/var/run/frr" 29 | # localstatedir: 30 | 31 | # FRR's user (--enable-user). 32 | # Default: "frr" 33 | # user: 34 | 35 | # FRR's group (--enable-group). 36 | # Default: "frr" 37 | # group: 38 | 39 | # Directory to store FRR logs. 40 | # Default: [netgen_runstatedir]/frrlogs 41 | # logdir: 42 | 43 | holod: 44 | # FRR's sysconfdir (--sysconfdir). 45 | # Default: "/etc/holod" 46 | # sysconfdir: 47 | 48 | # FRR's localstatedir (--localstatedir). 49 | # Default: "/var/run/holo" 50 | # localstatedir: 51 | 52 | # Directory to store FRR logs. 53 | # Default: [netgen_runstatedir]/holod-logs 54 | # logdir: 55 | 56 | # holod's user. 57 | # Default: "holo" 58 | # user: 59 | 60 | # holod's group. 61 | # Default: "holo" 62 | # group: 63 | 64 | tcpdump: 65 | # Directory to store tcpdump captures. 66 | # Default: [netgen_runstatedir]/pcaps 67 | # pcap_dir: 68 | 69 | # Filter on which nodes tcpdump should run. 70 | # Default: [] 71 | # whitelist: 72 | 73 | # Filter on which nodes tcpdump should not run. 74 | # Default: [] 75 | # blacklist: 76 | 77 | tmux: 78 | # Path of tmux script used to open a shell on all routers. 79 | # Default: [netgen_runstatedir]/tmux.sh 80 | # file: 81 | 82 | # Panels per node. 83 | # Default: 1 84 | # panels-per-node: 85 | 86 | bird: 87 | # BIRD's sysconfdir (--sysconfdir). 88 | # Default: "/etc/bird" 89 | # sysconfdir: 90 | 91 | # BIRD's localstatedir (--localstatedir). 92 | # Default: "/var/run/bird" 93 | # localstatedir: 94 | 95 | # BIRD's user (--enable-user). 96 | # Default: "bird" 97 | # user: 98 | 99 | # BIRD's group (--enable-group). 100 | # Default: "bird" 101 | # group: 102 | 103 | # Directory to store BIRD logs. 104 | # Default: [netgen_runstatedir]/birdlogs 105 | # logdir: 106 | 107 | bgpsimple: 108 | # Path to bgp_simple script 109 | # Default: "bgp_simple.pl" 110 | # path: 111 | 112 | iou: 113 | # IOU working directory. 114 | # Default: [netgen_runstatedir]/iou 115 | # dir: 116 | 117 | dynamips: 118 | # dynamips working directory. 119 | # Default: [netgen_runstatedir]/dynamips 120 | # dir: 121 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples for Topology Files 2 | -------------------------------------------------------------------------------- /examples/frr-isis-tutorial.yml: -------------------------------------------------------------------------------- 1 | # 2 | # +---------+ 3 | # | | 4 | # | SRC | 5 | # | 9.9.9.1 | 6 | # | | 7 | # +---------+ 8 | # |eth-rt1 (.1) 9 | # | 10 | # |10.0.10.0/24 11 | # | 12 | # |eth-src (.2) 13 | # +---------+ 14 | # | | 15 | # | RT1 | 16 | # | 1.1.1.1 | 17 | # | | 18 | # +---------+ 19 | # |eth-sw1 20 | # | 21 | # | 22 | # | 23 | # +---------+ | +---------+ 24 | # | | | | | 25 | # | RT2 |eth-sw1 | eth-sw1| RT3 | 26 | # | 2.2.2.2 +----------+----------+ 3.3.3.3 | 27 | # | | 10.0.1.0/24 | | 28 | # +---------+ +---------+ 29 | # eth-rt4-1| |eth-rt4-2 eth-rt5-1| |eth-rt5-2 30 | # | | | | 31 | # 10.0.2.0/24| |10.0.3.0/24 10.0.4.0/24| |10.0.5.0/24 32 | # | | | | 33 | # eth-rt2-1| |eth-rt2-2 eth-rt3-1| |eth-rt3-2 34 | # +---------+ +---------+ 35 | # | | | | 36 | # | RT4 | 10.0.6.0/24 | RT5 | 37 | # | 4.4.4.4 +---------------------+ 5.5.5.5 | 38 | # | |eth-rt5 eth-rt4| | 39 | # +---------+ +---------+ 40 | # eth-rt6| |eth-rt6 41 | # | | 42 | # 10.0.7.0/24| |10.0.8.0/24 43 | # | +---------+ | 44 | # | | | | 45 | # | | RT6 | | 46 | # +----------+ 6.6.6.6 +-----------+ 47 | # eth-rt4| |eth-rt5 48 | # +---------+ 49 | # |eth-dst (.1) 50 | # | 51 | # |10.0.11.0/24 52 | # | 53 | # |eth-rt6 (.2) 54 | # +---------+ 55 | # | | 56 | # | DST | 57 | # | 9.9.9.2 | 58 | # | | 59 | # +---------+ 60 | # 61 | 62 | --- 63 | 64 | routers: 65 | 66 | src: 67 | links: 68 | lo: 69 | ipv4: 9.9.9.1/32 70 | ipv6: 2001:db8:1066::1/128 71 | mpls: yes 72 | eth-rt1: 73 | peer: [rt1, eth-src] 74 | ipv4: 10.0.10.1/24 75 | mpls: yes 76 | shell: | 77 | ip route add 9.9.9.2/32 via inet 10.0.10.2 src 9.9.9.1 78 | 79 | rt1: 80 | links: 81 | lo: 82 | ipv4: 1.1.1.1/32 83 | ipv6: 2001:db8:1000::1/128 84 | mpls: yes 85 | eth-sw1: 86 | peer: [sw1, sw1-rt1] 87 | ipv4: 10.0.1.1/24 88 | mpls: yes 89 | eth-src: 90 | peer: [src, eth-rt1] 91 | ipv4: 10.0.10.2/24 92 | mpls: yes 93 | frr: 94 | zebra: 95 | run: yes 96 | config: 97 | staticd: 98 | run: yes 99 | config: | 100 | ip route 9.9.9.1/32 10.0.10.1 101 | ! 102 | isisd: 103 | run: yes 104 | config: | 105 | interface lo 106 | ip router isis 1 107 | ipv6 router isis 1 108 | isis passive 109 | ! 110 | interface eth-sw1 111 | ip router isis 1 112 | ipv6 router isis 1 113 | isis hello-multiplier 3 114 | ! 115 | interface eth-src 116 | ip router isis 1 117 | ipv6 router isis 1 118 | isis network point-to-point 119 | isis hello-multiplier 3 120 | ! 121 | router isis 1 122 | net 49.0000.0000.0000.0001.00 123 | is-type level-1 124 | redistribute ipv4 static level-1 125 | redistribute ipv4 connected level-1 126 | topology ipv6-unicast 127 | ! 128 | 129 | rt2: 130 | links: 131 | lo: 132 | ipv4: 2.2.2.2/32 133 | ipv6: 2001:db8:1000::2/128 134 | mpls: yes 135 | eth-sw1: 136 | peer: [sw1, sw1-rt2] 137 | ipv4: 10.0.1.2/24 138 | mpls: yes 139 | eth-rt4-1: 140 | peer: [rt4, eth-rt2-1] 141 | ipv4: 10.0.2.2/24 142 | mpls: yes 143 | eth-rt4-2: 144 | peer: [rt4, eth-rt2-2] 145 | ipv4: 10.0.3.2/24 146 | mpls: yes 147 | frr: 148 | zebra: 149 | run: yes 150 | config: 151 | isisd: 152 | run: yes 153 | config: | 154 | interface lo 155 | ip router isis 1 156 | ipv6 router isis 1 157 | isis passive 158 | ! 159 | interface eth-sw1 160 | ip router isis 1 161 | ipv6 router isis 1 162 | isis hello-multiplier 3 163 | ! 164 | interface eth-rt4-1 165 | ip router isis 1 166 | ipv6 router isis 1 167 | isis network point-to-point 168 | isis hello-multiplier 3 169 | ! 170 | interface eth-rt4-2 171 | ip router isis 1 172 | ipv6 router isis 1 173 | isis network point-to-point 174 | isis hello-multiplier 3 175 | ! 176 | router isis 1 177 | net 49.0000.0000.0000.0002.00 178 | is-type level-1 179 | topology ipv6-unicast 180 | ! 181 | 182 | rt3: 183 | links: 184 | lo: 185 | ipv4: 3.3.3.3/32 186 | ipv6: 2001:db8:1000::3/128 187 | mpls: yes 188 | eth-sw1: 189 | peer: [sw1, sw1-rt3] 190 | ipv4: 10.0.1.3/24 191 | mpls: yes 192 | eth-rt5-1: 193 | peer: [rt5, eth-rt3-1] 194 | ipv4: 10.0.4.3/24 195 | mpls: yes 196 | eth-rt5-2: 197 | peer: [rt5, eth-rt3-2] 198 | ipv4: 10.0.5.3/24 199 | mpls: yes 200 | frr: 201 | zebra: 202 | run: yes 203 | config: 204 | isisd: 205 | run: yes 206 | config: | 207 | interface lo 208 | ip router isis 1 209 | ipv6 router isis 1 210 | isis passive 211 | ! 212 | interface eth-sw1 213 | ip router isis 1 214 | ipv6 router isis 1 215 | isis hello-multiplier 3 216 | ! 217 | interface eth-rt5-1 218 | ip router isis 1 219 | ipv6 router isis 1 220 | isis network point-to-point 221 | isis hello-multiplier 3 222 | ! 223 | interface eth-rt5-2 224 | ip router isis 1 225 | ipv6 router isis 1 226 | isis network point-to-point 227 | isis hello-multiplier 3 228 | ! 229 | router isis 1 230 | net 49.0000.0000.0000.0003.00 231 | is-type level-1 232 | topology ipv6-unicast 233 | ! 234 | 235 | rt4: 236 | links: 237 | lo: 238 | ipv4: 4.4.4.4/32 239 | ipv6: 2001:db8:1000::4/128 240 | mpls: yes 241 | eth-rt2-1: 242 | peer: [rt2, eth-rt4-1] 243 | ipv4: 10.0.2.4/24 244 | mpls: yes 245 | eth-rt2-2: 246 | peer: [rt2, eth-rt4-2] 247 | ipv4: 10.0.3.4/24 248 | mpls: yes 249 | eth-rt5: 250 | peer: [rt5, eth-rt4] 251 | ipv4: 10.0.6.4/24 252 | mpls: yes 253 | eth-rt6: 254 | peer: [rt6, eth-rt4] 255 | ipv4: 10.0.7.4/24 256 | mpls: yes 257 | frr: 258 | zebra: 259 | run: yes 260 | config: 261 | isisd: 262 | run: yes 263 | config: | 264 | interface lo 265 | ip router isis 1 266 | ipv6 router isis 1 267 | isis passive 268 | ! 269 | interface eth-rt2-1 270 | ip router isis 1 271 | ipv6 router isis 1 272 | isis network point-to-point 273 | isis hello-multiplier 3 274 | ! 275 | interface eth-rt2-2 276 | ip router isis 1 277 | ipv6 router isis 1 278 | isis network point-to-point 279 | isis hello-multiplier 3 280 | ! 281 | interface eth-rt5 282 | ip router isis 1 283 | ipv6 router isis 1 284 | isis network point-to-point 285 | isis hello-multiplier 3 286 | ! 287 | interface eth-rt6 288 | ip router isis 1 289 | ipv6 router isis 1 290 | isis network point-to-point 291 | isis hello-multiplier 3 292 | ! 293 | router isis 1 294 | net 49.0000.0000.0000.0004.00 295 | is-type level-1 296 | topology ipv6-unicast 297 | ! 298 | 299 | rt5: 300 | links: 301 | lo: 302 | ipv4: 5.5.5.5/32 303 | ipv6: 2001:db8:1000::5/128 304 | mpls: yes 305 | eth-rt3-1: 306 | peer: [rt3, eth-rt5-1] 307 | ipv4: 10.0.4.5/24 308 | mpls: yes 309 | eth-rt3-2: 310 | peer: [rt3, eth-rt5-2] 311 | ipv4: 10.0.5.5/24 312 | mpls: yes 313 | eth-rt4: 314 | peer: [rt4, eth-rt5] 315 | ipv4: 10.0.6.5/24 316 | mpls: yes 317 | eth-rt6: 318 | peer: [rt6, eth-rt5] 319 | ipv4: 10.0.8.5/24 320 | mpls: yes 321 | frr: 322 | zebra: 323 | run: yes 324 | config: 325 | isisd: 326 | run: yes 327 | config: | 328 | interface lo 329 | ip router isis 1 330 | ipv6 router isis 1 331 | isis passive 332 | ! 333 | interface eth-rt3-1 334 | ip router isis 1 335 | ipv6 router isis 1 336 | isis network point-to-point 337 | isis hello-multiplier 3 338 | ! 339 | interface eth-rt3-2 340 | ip router isis 1 341 | ipv6 router isis 1 342 | isis network point-to-point 343 | isis hello-multiplier 3 344 | ! 345 | interface eth-rt4 346 | ip router isis 1 347 | ipv6 router isis 1 348 | isis network point-to-point 349 | isis hello-multiplier 3 350 | ! 351 | interface eth-rt6 352 | ip router isis 1 353 | ipv6 router isis 1 354 | isis network point-to-point 355 | isis hello-multiplier 3 356 | ! 357 | router isis 1 358 | net 49.0000.0000.0000.0005.00 359 | is-type level-1 360 | topology ipv6-unicast 361 | ! 362 | 363 | rt6: 364 | links: 365 | lo: 366 | ipv4: 6.6.6.6/32 367 | ipv6: 2001:db8:1000::6/128 368 | mpls: yes 369 | eth-rt4: 370 | peer: [rt4, eth-rt6] 371 | ipv4: 10.0.7.6/24 372 | mpls: yes 373 | eth-rt5: 374 | peer: [rt5, eth-rt6] 375 | ipv4: 10.0.8.6/24 376 | mpls: yes 377 | eth-dst: 378 | peer: [dst, eth-rt6] 379 | ipv4: 10.0.11.1/24 380 | mpls: yes 381 | frr: 382 | zebra: 383 | run: yes 384 | config: 385 | staticd: 386 | run: yes 387 | config: | 388 | ip route 9.9.9.2/32 10.0.11.2 389 | isisd: 390 | run: yes 391 | config: | 392 | interface lo 393 | ip router isis 1 394 | ipv6 router isis 1 395 | isis passive 396 | ! 397 | interface eth-rt4 398 | ip router isis 1 399 | ipv6 router isis 1 400 | isis network point-to-point 401 | isis hello-multiplier 3 402 | ! 403 | interface eth-rt5 404 | ip router isis 1 405 | ipv6 router isis 1 406 | isis network point-to-point 407 | isis hello-multiplier 3 408 | ! 409 | interface eth-dst 410 | ip router isis 1 411 | ipv6 router isis 1 412 | isis network point-to-point 413 | isis hello-multiplier 3 414 | ! 415 | router isis 1 416 | net 49.0000.0000.0000.0006.00 417 | is-type level-1 418 | redistribute ipv4 static level-1 419 | redistribute ipv4 connected level-1 420 | topology ipv6-unicast 421 | ! 422 | 423 | dst: 424 | links: 425 | lo: 426 | ipv4: 9.9.9.2/32 427 | ipv6: 2001:db8:1066::2/128 428 | mpls: yes 429 | eth-rt6: 430 | peer: [rt6, eth-dst] 431 | ipv4: 10.0.11.2/24 432 | mpls: yes 433 | shell: | 434 | ip route add 9.9.9.1/32 via inet 10.0.11.1 435 | 436 | switches: 437 | sw1: 438 | links: 439 | sw1-rt1: 440 | peer: [rt1, rt1-sw1] 441 | sw1-rt2: 442 | peer: [rt2, rt2-sw1] 443 | sw1-rt3: 444 | peer: [rt3, rt3-sw1] 445 | 446 | 447 | frr: 448 | #perf: yes 449 | #valgrind: yes 450 | base-configs: 451 | all: | 452 | hostname %(node) 453 | password 1 454 | log file %(logdir)/%(node)-%(daemon).log 455 | log commands 456 | zebra: | 457 | debug zebra kernel 458 | debug zebra packet 459 | debug zebra mpls 460 | isisd: | 461 | debug isis events 462 | debug isis route-events 463 | debug isis spf-events 464 | debug isis sr-events 465 | debug isis lsp-gen 466 | 467 | -------------------------------------------------------------------------------- /exe/netgen: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | #$VERBOSE = true 3 | 4 | require 'bundler/setup' 5 | require 'optparse' 6 | require 'yaml' 7 | require 'fileutils' 8 | require 'netgen' 9 | 10 | # signal handler 11 | trap(:INT) do 12 | exit(0) 13 | end 14 | 15 | config_file = 'config.yml' 16 | script = nil 17 | 18 | opts = OptionParser.new do |op| 19 | op.banner = "netgen - the swiss army knife of network simulation.\n\n"\ 20 | "Usage: #{__FILE__} [OPTIONS] topology.yml" 21 | op.separator 'Help menu:' 22 | op.on('-c', '--config FILE', 'Set configuration file name') do |arg| 23 | config_file = arg 24 | end 25 | op.on('-d', '--debug', 'Enable debugging mode') do 26 | Netgen.debug_mode = 1 27 | end 28 | op.on('-h', '--help', 'Display this help and exit') do 29 | puts opts 30 | exit 0 31 | end 32 | op.on('-o', '--output FILE', 'Save auto-generated topology') do |arg| 33 | Netgen.output = arg 34 | end 35 | op.on('-s', '--script FILE', 'Run custom script after topology is started and exit') do |arg| 36 | script = arg 37 | end 38 | op.on('-v', '--version', 'Print program version') do 39 | puts "netgen v#{Netgen::VERSION}" 40 | exit 0 41 | end 42 | op.separator '' 43 | op.separator 'Example:' 44 | op.separator " #{File.basename(__FILE__)} examples/frr-isis-tutorial.yml" 45 | op.separator '' 46 | end 47 | 48 | # Main 49 | begin 50 | opts.parse! 51 | rescue OptionParser::ParseError => e 52 | $stderr.puts e.message 53 | $stderr.puts e.backtrace 54 | exit(1) 55 | end 56 | if ARGV.size != 1 57 | puts opts.help 58 | exit(1) 59 | end 60 | topology_file = ARGV[0] 61 | 62 | Netgen::LibC.prctl(Netgen::LibC::PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0) 63 | 64 | Netgen.config = Netgen::Config.new(config_file) 65 | Netgen.topology = Netgen::Topology.new(topology_file) 66 | Netgen.topology.check_consistency 67 | Netgen.topology.setup 68 | Netgen.topology.start 69 | 70 | if script 71 | system(script) 72 | Netgen.topology.cleanup 73 | exit 74 | end 75 | 76 | at_exit do 77 | Netgen.topology.cleanup 78 | puts 'exiting' 79 | end 80 | 81 | # main loop 82 | # TODO CLI 83 | sleep 84 | -------------------------------------------------------------------------------- /exe/netgen-exec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | #$VERBOSE = true 3 | 4 | require 'bundler/setup' 5 | require 'optparse' 6 | require 'yaml' 7 | require 'fileutils' 8 | require 'netgen' 9 | 10 | # signal handler 11 | trap(:INT) do 12 | exit(0) 13 | end 14 | 15 | opts = OptionParser.new do |op| 16 | op.banner = "netgen-exec - Run command in the given netgen node.\n\n"\ 17 | "Usage: #{__FILE__} [OPTIONS] NODE COMMAND" 18 | op.separator 'Help menu:' 19 | op.on('-h', '--help', 'Display this help and exit') do 20 | puts opts 21 | exit 0 22 | end 23 | op.on('-v', '--version', 'Print program version') do 24 | puts "netgen v#{Netgen::VERSION}" 25 | exit 0 26 | end 27 | op.separator '' 28 | op.separator 'Example:' 29 | op.separator " #{__FILE__} rt1 bash" 30 | op.separator '' 31 | end 32 | 33 | # Main 34 | begin 35 | opts.parse! 36 | rescue OptionParser::ParseError => e 37 | $stderr.puts e.message 38 | $stderr.puts e.backtrace 39 | exit(1) 40 | end 41 | if ARGV.size < 2 42 | puts opts.help 43 | exit(1) 44 | end 45 | node = ARGV[0] 46 | command = ARGV[1..-1].join(" ") 47 | 48 | filename = "#{Netgen::Config::NETGEN_RUNSTATEDIR}/pids.yml" 49 | begin 50 | pids = YAML.load_file(filename) 51 | rescue SystemCallError, Psych::SyntaxError => e 52 | $stderr.puts "error: netgen is not running" 53 | exit(1) 54 | end 55 | pid = pids[node] 56 | unless pid 57 | puts "error: node #{node} doesn't exist" 58 | exit(1) 59 | end 60 | 61 | exec("nsenter -t #{pid} --mount --net --pid --wd=. #{command}") 62 | -------------------------------------------------------------------------------- /lib/netgen.rb: -------------------------------------------------------------------------------- 1 | require_relative 'netgen/autogen' 2 | require_relative 'netgen/config' 3 | require_relative 'netgen/ipcalc' 4 | require_relative 'netgen/libc' 5 | require_relative 'netgen/linux_namespace' 6 | require_relative 'netgen/node' 7 | require_relative 'netgen/router' 8 | require_relative 'netgen/switch' 9 | require_relative 'netgen/topology' 10 | require_relative 'netgen/version' 11 | 12 | module Netgen 13 | class << self 14 | attr_accessor :topology 15 | attr_accessor :config 16 | attr_accessor :output 17 | attr_accessor :plugins 18 | attr_accessor :debug_mode 19 | 20 | Netgen.debug_mode = false 21 | Netgen.plugins = [] 22 | 23 | def log_err(msg, node: node = nil, plugin: plugin = nil) 24 | msg.prepend("node #{node.name}: ") if node 25 | msg.prepend("plugin: #{plugin.name}: ") if plugin 26 | $stderr.puts msg 27 | end 28 | 29 | def log_info(msg, node: node = nil, plugin: plugin = nil) 30 | msg.prepend("node #{node.name}: ") if node 31 | msg.prepend("plugin: #{plugin.name}: ") if plugin 32 | puts msg 33 | end 34 | 35 | def log_debug(msg, node: node = nil, plugin: plugin = nil) 36 | return unless @debug_mode 37 | msg.prepend("plugin: #{plugin.name}: ") if plugin 38 | msg.prepend("node #{node.name}: ") if node 39 | puts "debug: #{msg}" 40 | end 41 | end 42 | end 43 | 44 | require_relative 'netgen/plugin' 45 | require_relative 'netgen/plugins/shell' 46 | require_relative 'netgen/plugins/ethernet' 47 | require_relative 'netgen/plugins/ipv4' 48 | require_relative 'netgen/plugins/ipv6' 49 | require_relative 'netgen/plugins/mpls' 50 | require_relative 'netgen/plugins/netem' 51 | require_relative 'netgen/plugins/tcpdump' 52 | require_relative 'netgen/plugins/bgpsimple' 53 | require_relative 'netgen/plugins/frr' 54 | require_relative 'netgen/plugins/bird' 55 | require_relative 'netgen/plugins/holod' 56 | require_relative 'netgen/plugins/iou' 57 | require_relative 'netgen/plugins/dynamips' 58 | require_relative 'netgen/plugins/tmux' 59 | -------------------------------------------------------------------------------- /lib/netgen/autogen.rb: -------------------------------------------------------------------------------- 1 | module Netgen 2 | module Autogen 3 | # Dynamically generated node 4 | class Node 5 | attr_reader :index 6 | attr_reader :name 7 | attr_reader :connections 8 | 9 | @global_index = 0 10 | def self.next_index 11 | @global_index += 1 12 | end 13 | 14 | def initialize(name) 15 | @index = Node.next_index 16 | @name = name 17 | @link_index = 0 18 | @connections = {} 19 | end 20 | 21 | # Connect to another node using N links (Layout.parallel_links). 22 | def connect(peer) 23 | return if @connections[peer] 24 | @connections[peer] = [] 25 | peer.connections[self] = [] 26 | 27 | Layout.parallel_links.times do 28 | local = add_link 29 | remote = peer.add_link 30 | @connections[peer].push([local, remote]) 31 | peer.connections[self].push([remote, local]) 32 | end 33 | end 34 | 35 | # Add new link 36 | def add_link 37 | name = "#{@name}-eth#{@link_index}" 38 | @link_index += 1 39 | name 40 | end 41 | 42 | # Output node to format understandable by the Netgen main engine 43 | def output 44 | output = {} 45 | 46 | Netgen.plugins.each do |plugin| 47 | plugin.autogen_node(self.class, self.name, output, @index) 48 | end 49 | output['links'] = {} 50 | 51 | @connections.each do |peer| 52 | peer_name = peer[0].name 53 | peer[1].each do |connection| 54 | local, remote = connection 55 | output['links'][local] = {} 56 | output['links'][local]['peer'] = [peer_name, remote] 57 | end 58 | end 59 | output 60 | end 61 | end 62 | 63 | # Dynamically generated router 64 | class Router < Node 65 | attr_reader :loopback 66 | attr_reader :stubs 67 | 68 | def initialize(name) 69 | super(name) 70 | @loopback = "lo" 71 | add_stubs 72 | end 73 | 74 | # Add N (Layout.stub_networks) stub links. 75 | def add_stubs 76 | @stub_index = 0 77 | @stubs = [] 78 | Layout.stub_networks.times do 79 | @stubs.push("#{@name}-stub#{@stub_index}") 80 | @stub_index += 1 81 | end 82 | end 83 | 84 | # Append loopback and stubs to the main output 85 | def output 86 | output = super 87 | output_loopback(output) 88 | output_stubs(output) 89 | output 90 | end 91 | 92 | def output_loopback(output) 93 | links = output['links'] 94 | links[@loopback] = {} 95 | Netgen.plugins.each do |plugin| 96 | plugin.autogen_loopback(output, @index, @loopback, links[@loopback]) 97 | end 98 | end 99 | 100 | def output_stubs(output) 101 | links = output['links'] 102 | @stubs.each_with_index do |stub, stub_index| 103 | links[stub] = {} 104 | Netgen.plugins.each do |plugin| 105 | plugin.autogen_stub(output, @index, stub_index, stub, links[stub]) 106 | end 107 | end 108 | end 109 | end 110 | 111 | # Dynamically generated switch 112 | class Switch < Node 113 | end 114 | 115 | # Layout base class 116 | class Layout 117 | class << self 118 | attr_accessor :parallel_links 119 | attr_accessor :stub_networks 120 | end 121 | 122 | def initialize(description) 123 | @description = description 124 | @routers = [] 125 | @switches = [] 126 | end 127 | 128 | # Parse the 'autogen' section of the topology file 129 | def parse 130 | Layout.parallel_links = @description['parallel-links'] || 1 131 | Layout.stub_networks = @description['stub-networks'] || 0 132 | end 133 | 134 | # Output generated topology to format understandable by the Netgen 135 | # main engine 136 | def output 137 | output = {} 138 | output['routers'] = {} 139 | output['switches'] = {} 140 | @routers.each do |router| 141 | output['routers'][router.name] = router.output 142 | end 143 | @switches.each do |switch| 144 | output['switches'][switch.name] = switch.output 145 | end 146 | output_links(output) 147 | output 148 | end 149 | 150 | # TODO: need to move this to the output method of the Node class and 151 | # unify routers and switches under the same section in the topology 152 | # files. 153 | def output_links(output) 154 | output['routers'].values.each do |router| 155 | router['links'].each do |link| 156 | next unless link[1]['peer'] 157 | local_name = link[0] 158 | local_attr = link[1] 159 | peer_name = local_attr['peer'][0] 160 | peer_link = local_attr['peer'][1] 161 | remote_attr = output['routers'][peer_name]['links'][peer_link] 162 | Netgen.plugins.each do |plugin| 163 | plugin.autogen_link(router, local_name, local_attr, remote_attr) 164 | end 165 | end 166 | end 167 | end 168 | end 169 | 170 | # Line topology layout 171 | # 172 | # Example 173 | # 174 | # Configuration: 175 | # layout: 176 | # type: line 177 | # size: 5 178 | # 179 | # Generated topology: 180 | # rt0---rt1---rt2---rt3---rt4 181 | # 182 | class LayoutLine < Layout 183 | def parse 184 | super 185 | @size = @description['size'] 186 | raise ArgumentError, 'Unspecified size' unless @size 187 | raise ArgumentError, 'Invalid size' unless @size > 0 188 | end 189 | 190 | def generate 191 | generate_routers 192 | generate_links 193 | end 194 | 195 | def generate_routers 196 | @size.times do |i| 197 | @routers[i] = Router.new("rt#{i}") 198 | end 199 | end 200 | 201 | def generate_links 202 | @size.times do |i| 203 | @routers[i].connect(@routers[i - 1]) if i > 0 204 | @routers[i].connect(@routers[i + 1]) if i < @size - 1 205 | end 206 | end 207 | end 208 | 209 | # Ring topology layout 210 | # 211 | # Example 212 | # 213 | # Configuration: 214 | # layout: 215 | # type: ring 216 | # size: 5 217 | # 218 | # Generated topology: 219 | # rt0---rt1---rt2---rt3---rt4 220 | # | | 221 | # +-----------------------+ 222 | # 223 | class LayoutRing < Layout 224 | def parse 225 | super 226 | @size = @description['size'] 227 | raise ArgumentError, 'Unspecified size' unless @size 228 | raise ArgumentError, 'Invalid size' unless @size > 0 229 | end 230 | 231 | def generate 232 | generate_routers 233 | generate_links 234 | end 235 | 236 | def generate_routers 237 | @size.times do |i| 238 | @routers[i] = Router.new("rt#{i}") 239 | end 240 | end 241 | 242 | def generate_links 243 | @size.times do |i| 244 | left = (i == 0 ? @size - 1 : i - 1) 245 | right = (i == @size - 1 ? 0 : i + 1) 246 | @routers[i].connect(@routers[left]) 247 | @routers[i].connect(@routers[right]) 248 | end 249 | end 250 | end 251 | 252 | # Grid topology layout 253 | # 254 | # Example 255 | # 256 | # Configuration: 257 | # layout: 258 | # type: grid 259 | # width: 3 260 | # height: 3 261 | # 262 | # Generated topology: 263 | # rt0x0---rt1x0---rt2x0 264 | # + + + 265 | # | | | 266 | # + + + 267 | # rt0x1---rt1x1---rt2x1 268 | # + + + 269 | # | | | 270 | # + + + 271 | # rt0x2---rt1x2---rt2x2 272 | # 273 | class LayoutGrid < Layout 274 | def parse 275 | super 276 | @width = @description['width'] 277 | raise ArgumentError, 'Unspecified width' unless @width 278 | raise ArgumentError, 'Invalid width' unless @width > 0 279 | @height = @description['height'] 280 | raise ArgumentError, 'Unspecified height' unless @height 281 | raise ArgumentError, 'Invalid height' unless @height > 0 282 | end 283 | 284 | def generate 285 | generate_routers 286 | generate_links 287 | end 288 | 289 | def generate_routers 290 | @width.times do |x| 291 | @height.times do |y| 292 | @routers[index(x, y)] = Router.new("rt#{x}x#{y}") 293 | end 294 | end 295 | end 296 | 297 | def generate_links 298 | @width.times do |x| 299 | @height.times do |y| 300 | router = @routers[index(x, y)] 301 | router.connect(@routers[index(x - 1, y)]) if x > 0 302 | router.connect(@routers[index(x + 1, y)]) if x < @width - 1 303 | router.connect(@routers[index(x, y - 1)]) if y > 0 304 | router.connect(@routers[index(x, y + 1)]) if y < @height - 1 305 | end 306 | end 307 | end 308 | 309 | def index(x, y) 310 | y * @width + x 311 | end 312 | end 313 | 314 | # Tree topology layout 315 | # 316 | # Example 317 | # 318 | # Configuration: 319 | # layout: 320 | # type: tree 321 | # height: 3 322 | # degree: 2 323 | # 324 | # Generated topology: 325 | # rt0 326 | # + 327 | # | 328 | # +-------+-------+ 329 | # | | 330 | # + + 331 | # rt1 rt2 332 | # + + 333 | # | | 334 | # +---+---+ +---+---+ 335 | # | | | | 336 | # + + + + 337 | # rt3 rt4 rt5 rt6 338 | # 339 | class LayoutTree < Layout 340 | def parse 341 | super 342 | @height = @description['height'] 343 | raise ArgumentError, 'Unspecified height' unless @height 344 | raise ArgumentError, 'Invalid height' unless @height > 0 345 | @degree = @description['degree'] 346 | raise ArgumentError, 'Unspecified degree' unless @degree 347 | raise ArgumentError, 'Invalid degree' unless @degree > 0 348 | end 349 | 350 | # Create tree recursively 351 | def create_tree(root, height, degree) 352 | degree.times do |i| 353 | child = add_router 354 | child.connect(root) 355 | create_tree(child, height - 1, degree) if height > 1 356 | end 357 | end 358 | 359 | def generate 360 | @index = 0 361 | root = add_router 362 | create_tree(root, @height - 1, @degree) 363 | end 364 | 365 | def add_router 366 | router = Router.new("rt#{@index}") 367 | @routers[@index] = router 368 | @index += 1 369 | router 370 | end 371 | end 372 | 373 | # Full-mesh topology layout 374 | # 375 | # Example 376 | # 377 | # Configuration: 378 | # layout: 379 | # type: full-mesh 380 | # size: 4 381 | # 382 | # Generated topology: 383 | # +------+rt0+------+ 384 | # | + | 385 | # | | | 386 | # + | + 387 | # rt1+------+------+rt2 388 | # + | + 389 | # | | | 390 | # | + | 391 | # +------+rt3+------+ 392 | # 393 | class LayoutFullMesh < Layout 394 | def parse 395 | super 396 | @size = @description['size'] 397 | raise ArgumentError, 'Unspecified size' unless @size 398 | raise ArgumentError, 'Invalid size' unless @size > 0 399 | end 400 | 401 | def generate 402 | generate_routers 403 | generate_links 404 | end 405 | 406 | def generate_routers 407 | @size.times do |i| 408 | @routers[i] = Router.new("rt#{i}") 409 | end 410 | end 411 | 412 | def generate_links 413 | @size.times do |i| 414 | @size.times do |j| 415 | @routers[i].connect(@routers[j]) if i != j 416 | end 417 | end 418 | end 419 | end 420 | 421 | # Bus topology layout 422 | # 423 | # Example 424 | # 425 | # Configuration: 426 | # layout: 427 | # type: bus 428 | # size: 5 429 | # 430 | # Generated topology: 431 | # rt0 432 | # + 433 | # | 434 | # + 435 | # rt1+---+sw1+---+rt2 436 | # + 437 | # | 438 | # + 439 | # rt3 440 | # 441 | class LayoutBus < Layout 442 | def parse 443 | super 444 | @size = @description['size'] 445 | raise ArgumentError, 'Unspecified size' unless @size 446 | raise ArgumentError, 'Invalid size' unless @size > 0 447 | end 448 | 449 | def generate 450 | generate_bus 451 | generate_routers 452 | generate_links 453 | end 454 | 455 | def generate_bus 456 | @switches[0] = Switch.new('sw1') 457 | end 458 | 459 | def generate_routers 460 | @size.times do |i| 461 | @routers[i] = Router.new("rt#{i}") 462 | end 463 | end 464 | 465 | def generate_links 466 | @size.times do |i| 467 | @routers[i].connect(@switches[0]) 468 | end 469 | end 470 | end 471 | end 472 | end 473 | -------------------------------------------------------------------------------- /lib/netgen/config.rb: -------------------------------------------------------------------------------- 1 | module Netgen 2 | class Config 3 | attr_reader :options 4 | NETGEN_RUNSTATEDIR = '/tmp/netgen' 5 | 6 | def initialize(path) 7 | @options = {} 8 | load(path) 9 | end 10 | 11 | # Load and parse the configuration file given as parameter. 12 | # Use default values for unspecified configuration entries. 13 | def load(path) 14 | config = YAML.load_file(path) 15 | config = default_config.merge(config) 16 | parse_config(config) 17 | rescue SystemCallError => e 18 | $stderr.puts "Error opening configuration file: #{e}" 19 | exit(1) 20 | rescue Psych::SyntaxError, ArgumentError => e 21 | $stderr.puts "Error parsing configuration file: #{e}" 22 | exit(1) 23 | end 24 | 25 | # Default configuration. 26 | def default_config 27 | { 28 | 'netgen_runstatedir' => "#{NETGEN_RUNSTATEDIR}", 29 | 'clean_exit' => 'false', 30 | 'valgrind_params' => '--tool=memcheck --leak-check=full', 31 | 'perf_dir' => "#{NETGEN_RUNSTATEDIR}/perf", 32 | 'plugins' => {} 33 | } 34 | end 35 | 36 | # Parse configuration file. 37 | def parse_config(config) 38 | config_options = { 39 | 'netgen_runstatedir' => String, 40 | 'clean_exit' => String, 41 | 'valgrind_params' => String, 42 | 'perf_dir' => String, 43 | 'plugins' => Hash 44 | } 45 | 46 | config.each do |name, value| 47 | Config.validate(name, value, config_options) 48 | if name == 'plugins' 49 | parse_plugins(value) 50 | else 51 | @options[name] = value 52 | end 53 | end 54 | end 55 | 56 | # Parse the 'plugins' section of the configuration file. 57 | def parse_plugins(config) 58 | config.each do |name, value| 59 | plugin = Netgen.plugins.find { |p| p.name == name } 60 | raise ArgumentError, "unknown plugin: #{plugin}" unless plugin 61 | plugin.parse_config(value) 62 | end 63 | end 64 | 65 | # Validate configuration entry. 66 | def self.validate(name, value, config_options) 67 | option = config_options[name] 68 | raise ArgumentError, "unknown option: #{name}" unless option 69 | unless value.class == option 70 | raise ArgumentError, "invalid value: #{value} (#{name})" 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/netgen/ipcalc.rb: -------------------------------------------------------------------------------- 1 | require 'ipaddr' 2 | 3 | module Netgen 4 | class IPCalc 5 | include Enumerable 6 | 7 | def initialize(family, start, prefixlen, step1, step2 = nil, total = nil) 8 | @family = family 9 | @start = IPAddr.new(start).to_i 10 | @prefixlen = prefixlen 11 | @step1 = IPAddr.new(step1).to_i 12 | @step2 = IPAddr.new(step2).to_i if step2 13 | @total = total 14 | end 15 | 16 | def fetch(step1, step2 = nil, &block) 17 | address = @start 18 | address += @step1 * step1 19 | address += @step2 * step2 if step2 20 | if block 21 | yield(self, address) 22 | else 23 | print(address) 24 | end 25 | end 26 | 27 | def print(address) 28 | "#{IPAddr.new(address, @family)}/#{@prefixlen}" 29 | end 30 | 31 | def each(step2 = nil, &block) 32 | @total.times do |i| 33 | yield(fetch(i, step2)) 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/netgen/libc.rb: -------------------------------------------------------------------------------- 1 | require 'ffi' 2 | 3 | module Netgen 4 | # Bindings for a few libc's functions 5 | class LibC 6 | extend FFI::Library 7 | ffi_lib 'c' 8 | 9 | attach_function :unshare, [:int], :int 10 | attach_function :setns, [:int, :int], :int 11 | attach_function :mount, [:string, :string, :string, :ulong, :pointer], :int 12 | attach_function :prctl, [:int, :long, :long, :long, :long], :int 13 | 14 | # include/uapi/linux/sched.h 15 | CLONE_NEWNS = 0x00020000 16 | CLONE_NEWPID = 0x20000000 17 | CLONE_NEWNET = 0x40000000 18 | 19 | # include/uapi/linux/fs.h 20 | MS_NOSUID = (1 << 1) 21 | MS_NODEV = (1 << 2) 22 | MS_NOEXEC = (1 << 3) 23 | MS_REC = (1 << 14) 24 | MS_PRIVATE = (1 << 18) 25 | 26 | # include/uapi/linux/prctl.h 27 | PR_SET_PDEATHSIG = 1 28 | PR_SET_CHILD_SUBREAPER = 36 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/netgen/linux_namespace.rb: -------------------------------------------------------------------------------- 1 | module Netgen 2 | class LinuxNamespace 3 | attr_accessor :pid 4 | 5 | # Create a child process running on a separate network and mount namespace. 6 | def fork_and_unshare 7 | begin 8 | io_in, io_out = IO.pipe 9 | 10 | pid = Kernel.fork do 11 | unshare(LibC::CLONE_NEWNS | LibC::CLONE_NEWPID | LibC::CLONE_NEWNET) 12 | 13 | # Fork again to use the new PID namespace. 14 | # Need to supress a warning that is irrelevant for us. 15 | warn_level = $VERBOSE 16 | $VERBOSE = nil 17 | pid = Kernel.fork do 18 | # HACK: kill when parent dies 19 | trap(:SIGUSR1) do 20 | LibC.prctl(LibC::PR_SET_PDEATHSIG, 15, 0, 0, 0) 21 | trap(:SIGUSR1, :IGNORE) 22 | end 23 | LibC.prctl(LibC::PR_SET_PDEATHSIG, 10, 0, 0, 0) 24 | 25 | mount_propagation(LibC::MS_REC | LibC::MS_PRIVATE) 26 | mount_proc 27 | yield 28 | end 29 | $VERBOSE = warn_level 30 | io_out.puts "#{pid}" 31 | exit(0) 32 | end 33 | 34 | @pid = io_in.gets.to_i 35 | Process.waitpid(pid) 36 | rescue SystemCallError => e 37 | $stderr.puts "System call error:: #{e.message}" 38 | $stderr.puts e.backtrace 39 | exit(1) 40 | end 41 | end 42 | 43 | # Set the mount propagation of the process. 44 | def mount_propagation(flags) 45 | mount('none', '/', nil, flags, nil) 46 | end 47 | 48 | # Mount the proc filesystem (useful after creating a new PID namespace) 49 | def mount_proc 50 | mount('none', '/proc', nil, LibC::MS_REC | LibC::MS_PRIVATE, nil) 51 | mount('proc', '/proc', 'proc', 52 | LibC::MS_NOSUID | LibC::MS_NOEXEC | LibC::MS_NODEV, nil) 53 | end 54 | 55 | # Switch current process's network namespace. 56 | # The mount namespace (CLONE_NEWNS) can't be changed using setns(2) because 57 | # some ruby interpreters run as multithread applications. The setns(2)'s 58 | # man page says the following: "A process may not be reassociated with a 59 | # new mount namespace if it is multithreaded". 60 | def switch_net_namespace 61 | setns(LibC::CLONE_NEWNET, @pid) 62 | yield 63 | ensure 64 | setns(LibC::CLONE_NEWNET, 1) 65 | end 66 | 67 | # Wrapper for mount(2) 68 | def mount(source, target, fs_type, flags, data) 69 | if LibC.mount(source, target, fs_type, flags, data) < 0 70 | raise SystemCallError.new('mount failed', FFI::LastError.error) 71 | end 72 | end 73 | 74 | # Wrapper for unshare(2). 75 | def unshare(flags) 76 | if LibC.unshare(flags) < 0 77 | raise SystemCallError.new('unshare failed', FFI::LastError.error) 78 | end 79 | end 80 | 81 | # Wrapper for setns(2). 82 | def setns(nstype, pid) 83 | path = "/proc/#{pid}/ns/net" 84 | File.open(path) do |file| 85 | if LibC.setns(file.fileno, nstype) < 0 86 | raise SystemCallError.new('setns failed', FFI::LastError.error) 87 | end 88 | end 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /lib/netgen/node.rb: -------------------------------------------------------------------------------- 1 | module Netgen 2 | class Node 3 | attr_reader :index 4 | attr_reader :name 5 | attr_reader :attributes 6 | attr_reader :ns 7 | 8 | @global_index = -1 9 | def self.next_index 10 | @global_index += 1 11 | end 12 | 13 | def initialize(name, attributes) 14 | @index = Node.next_index 15 | @name = name 16 | @attributes = attributes 17 | @ns = LinuxNamespace.new 18 | @ns.fork_and_unshare do 19 | Process.setproctitle("netgen-#{@name}") 20 | trap(:INT, :IGNORE) 21 | trap(:TERM) { exit } 22 | # This is the init process of this node. We need to reap the zombies. 23 | trap(:CHLD) { Process.wait } 24 | sleep 25 | end 26 | end 27 | 28 | # Node cleanup. 29 | def cleanup 30 | return unless @attributes['links'] 31 | @attributes['links'].each do |link_name, link_attributes| 32 | next unless link_attributes 33 | Netgen.plugins.each do |plugin| 34 | plugin.link_exit(self, link_name, link_attributes) 35 | end 36 | end 37 | Netgen.plugins.each { |plugin| plugin.node_exit(self) } 38 | end 39 | 40 | # Check if the topology is valid or not. 41 | def check_consistency(nodes); end 42 | 43 | # Call the 'node_init' method of all plugins. 44 | def setup 45 | FileUtils.mkdir_p(mount_point) 46 | #execute('sysctl -wq net.core.wmem_max=83886080') 47 | Netgen.plugins.each { |plugin| plugin.node_init(self) } 48 | end 49 | 50 | # Call the 'node_start' method of all plugins. 51 | def start 52 | # XXX MOVE 53 | Netgen.plugins.each { |plugin| plugin.node_start(self) } 54 | end 55 | 56 | # Create node's links. 57 | def setup_links(nodes) 58 | return unless @attributes['links'] 59 | @attributes['links'].each do |link_name, link_attributes| 60 | link_attributes ||= {} 61 | peer = link_attributes.dig('peer') 62 | if peer 63 | setup_p2p_link(link_name, nodes[peer[0]], peer[1]) 64 | elsif link_name != "lo" 65 | setup_stub_link(link_name, link_attributes) 66 | end 67 | execute("ip link set dev #{link_name} up") 68 | 69 | # XXX: should be a plugin 70 | vrf = link_attributes.dig('vrf') 71 | execute("ip link set dev #{link_name} master #{vrf}") if vrf 72 | 73 | Netgen.plugins.each do |plugin| 74 | plugin.link_init(self, link_name, link_attributes) 75 | end 76 | end 77 | end 78 | 79 | # Create a link connecting this node to another one. 80 | def setup_p2p_link(link_name, peer_node, peer_link) 81 | return if @index > peer_node.index 82 | execute("ip link add dev #{link_name} type veth peer name netgen-tmp") 83 | execute("ip link set dev netgen-tmp name #{peer_link} "\ 84 | "netns #{peer_node.ns.pid}", pid: false) 85 | end 86 | 87 | # Create a stub link (i.e. a disconnected link). 88 | def setup_stub_link(link_name, link_attributes) 89 | type = link_attributes.dig('type') || 'none' 90 | case type 91 | when 'vrf' 92 | table = link_attributes.dig('table') 93 | # XXX: check if table is undefined (JSON schema?) 94 | execute("ip link add name #{link_name} type vrf table #{table}") 95 | #execute("ip rule add oif #{link_name} table #{table}") 96 | #execute("ip rule add iif #{link_name} table #{table}") 97 | else 98 | execute("ip link add name #{link_name} type dummy") 99 | end 100 | end 101 | 102 | # Execute a given command on this node and wait until it's done. 103 | def execute(command, options: {}, mnt: true, pid: true, net: true) 104 | Netgen.log_debug("execute '#{command}'", node: self) 105 | command.prepend(nsenter(mnt: mnt, pid: pid, net: net)) 106 | system(command, options) 107 | rescue SystemCallError => e 108 | $stderr.puts "System call error:: #{e.message}" 109 | $stderr.puts e.backtrace 110 | exit(1) 111 | end 112 | 113 | # Spawn the given command under this node. 114 | # When this node is deleted, all non-detached child processes are 115 | # automatically killed. 116 | def spawn(command, env: {}, options: {}, delay: nil, mnt: true, pid: true, net: true) 117 | command.prepend(nsenter(mnt: mnt, pid: pid, net: net)) 118 | 119 | # Sleep on a separate thread if necessary 120 | if delay 121 | Thread.new do 122 | sleep delay 123 | do_spawn(command, env: env, options: options) 124 | end 125 | else 126 | do_spawn(command, env: env, options: options) 127 | end 128 | rescue SystemCallError => e 129 | $stderr.puts "System call error:: #{e.message}" 130 | $stderr.puts e.backtrace 131 | exit(1) 132 | end 133 | 134 | def do_spawn(command, env: {}, options: {}) 135 | Netgen.log_debug("spawn '#{command}'", node: self) 136 | pid = Process.spawn(env, command, options) 137 | Process.detach(pid) 138 | end 139 | 140 | # Workaround to change the mount namespace of the child processes since 141 | # setns(2) with CLONE_NEWNS doesn't work for multithreaded programs. 142 | # nsenter(1) is a standard tool from the util-linux package. 143 | def nsenter(mnt: false, pid: false, net: false) 144 | return "" unless mnt || pid || net 145 | cmd = "nsenter -t #{@ns.pid} " 146 | cmd += "--mount " if mnt 147 | cmd += "--pid " if pid 148 | cmd += "--net " if net 149 | cmd 150 | end 151 | 152 | # Root path to the node's bind mounts. 153 | def mount_point 154 | "#{Netgen.config.options['netgen_runstatedir']}/mounts/#{@name}" 155 | end 156 | 157 | # Bind mount a path under this node's mount namespace (e.g. /etc, /var/run). 158 | def mount(path, user = nil, group = nil) 159 | source = "#{mount_point}/#{path}" 160 | FileUtils.mkdir_p(source) 161 | FileUtils.chown_R(user, group, source) if user || group 162 | execute("mount --bind #{source} #{path}") 163 | end 164 | 165 | # Umount previously mounted path. Use --lazy so this doesn't fail when the 166 | # mount point is still busy (e.g. a shell session on a dead node). 167 | def umount(path) 168 | execute("umount --lazy #{path}") 169 | FileUtils.rm_rf("#{mount_point}/#{path}") 170 | end 171 | 172 | # Suspend or pause the current node. 173 | def suspend; end 174 | 175 | # Resume operation after being paused. 176 | def resume; end 177 | 178 | # Private methods 179 | private :do_spawn 180 | end 181 | end 182 | -------------------------------------------------------------------------------- /lib/netgen/plugin.rb: -------------------------------------------------------------------------------- 1 | module Netgen 2 | class Plugin 3 | attr_reader :cfg 4 | 5 | class << self 6 | def inherited(subclass) 7 | Netgen.plugins.push(subclass.new) 8 | end 9 | end 10 | 11 | # Plugin's name. 12 | def name; end 13 | 14 | # Plugin's configuration options. 15 | def config_options 16 | {} 17 | end 18 | 19 | # Plugin's default configuration. 20 | def default_config 21 | {} 22 | end 23 | 24 | # Parse plugin's configuration section. 25 | def parse_config(config) 26 | @cfg = default_config 27 | return unless config 28 | config.each do |name, value| 29 | Config.validate(name, value, config_options) 30 | @cfg[name] = value 31 | end 32 | end 33 | 34 | # Called after a topology is created. 35 | def topology_init(topology) 36 | @attributes = Hash(topology[name]) 37 | end 38 | 39 | # Called when the topology is being started. 40 | def topology_start; end 41 | 42 | # Called before a topology is deleted. 43 | def topology_exit; end 44 | 45 | # Called after a node is created. 46 | def node_init(node); end 47 | 48 | # Called when the node is being started. 49 | def node_start(node); end 50 | 51 | # Called before a node is deleted. 52 | def node_exit(node); end 53 | 54 | # Called after a link is created. 55 | def link_init(node, link_name, link_attributes); end 56 | 57 | # Called before a link is deleted. 58 | def link_exit(node, link_name, link_attributes); end 59 | 60 | # Parse plugin's autogen section of the topology file. 61 | def autogen_parse(parameters); end 62 | 63 | # Called when a node is generated. 64 | def autogen_node(type, node_name, node, node_index); end 65 | 66 | # Called when a link is generated. 67 | def autogen_link(node, name, local_attr, remote_attr); end 68 | 69 | # Called when a loopback interface is generated. 70 | def autogen_loopback(node, node_index, name, attr); end 71 | 72 | # Called when a stub link is generated. 73 | def autogen_stub(node, node_index, stub_index, name, attr); end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/netgen/plugins/bgpsimple.rb: -------------------------------------------------------------------------------- 1 | module Netgen 2 | class PluginBgpsimple < Plugin 3 | def name 4 | 'bgpsimple' 5 | end 6 | 7 | def config_options 8 | { 9 | 'path' => String, 10 | } 11 | end 12 | 13 | def default_config 14 | { 15 | 'path' => 'bgp_simple.pl', 16 | } 17 | end 18 | 19 | def node_start(node) 20 | attr = node.attributes['bgpsimple'] 21 | return unless attr 22 | # TODO: validate 23 | 24 | delay = attr['delay'] 25 | sleep delay if delay.is_a?(Numeric) 26 | 27 | Netgen.log_info("starting bgp_simple.pl", plugin: self, node: node) 28 | 29 | command = @cfg['path'] 30 | command += " -myas #{attr['myas']}" 31 | command += " -myip #{attr['myip']} -n #{attr['myip']}" 32 | command += " -peeras #{attr['peeras']}" 33 | command += " -peerip #{attr['peerip']}" 34 | command += " -keepalive #{attr['keepalive']}" if attr['keepalive'] 35 | command += " -holdtime #{attr['holdtime']}" if attr['holdtime'] 36 | command += " -p #{attr['file']}" if attr['file'] 37 | command += " -nolisten" 38 | 39 | node.spawn(command, options: { out: '/dev/null', err: '/dev/null' }) 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/netgen/plugins/bird.rb: -------------------------------------------------------------------------------- 1 | module Netgen 2 | class PluginBird < Plugin 3 | def name 4 | 'bird' 5 | end 6 | 7 | def config_options 8 | { 9 | 'sysconfdir' => String, 10 | 'localstatedir' => String, 11 | 'user' => String, 12 | 'group' => String, 13 | 'logdir' => String 14 | } 15 | end 16 | 17 | def default_config 18 | { 19 | 'sysconfdir' => '/etc/bird', 20 | 'localstatedir' => '/var/run/bird', 21 | 'user' => 'bird', 22 | 'group' => 'bird', 23 | 'logdir' => "#{Config::NETGEN_RUNSTATEDIR}/birdlogs" 24 | } 25 | end 26 | 27 | def topology_init(topology) 28 | super 29 | #setup_dirs 30 | end 31 | 32 | def node_init(node) 33 | return unless node.attributes['bird'] 34 | node.mount(@cfg['sysconfdir'], @cfg['user'], @cfg['group']) 35 | node.mount(@cfg['localstatedir'], @cfg['user'], @cfg['group']) 36 | create_config_files(node) 37 | end 38 | 39 | def node_start(node) 40 | attributes = node.attributes['bird'] 41 | return unless attributes 42 | return unless attributes['bird'] 43 | Netgen.log_info("starting bird", plugin: self, node: node) 44 | node.spawn("bird", options: { out: '/dev/null', err: '/dev/null' }) 45 | end 46 | 47 | def node_exit(node) 48 | return unless node.attributes['bird'] 49 | #node.umount(@cfg['sysconfdir']) 50 | #node.umount(@cfg['localstatedir']) 51 | end 52 | 53 | def setup_dirs 54 | FileUtils.rm(Dir.glob("#{@cfg['logdir']}/*.log")) 55 | FileUtils.mkdir_p(@cfg['sysconfdir']) 56 | FileUtils.mkdir_p(@cfg['localstatedir']) 57 | FileUtils.mkdir_p(@cfg['logdir']) 58 | # XXX: might fail 59 | FileUtils.chown_R(@cfg['user'], @cfg['group'], @cfg['logdir']) 60 | FileUtils.rm(Dir.glob("#{@cfg['logdir']}/*.log")) 61 | end 62 | 63 | def create_config_files(node) 64 | attributes = node.attributes['bird'] 65 | create_config_file(node, "bird", attributes["bird"]) 66 | end 67 | 68 | def create_config_file(node, daemon, attributes) 69 | return unless attributes 70 | 71 | config = '' 72 | config = @attributes.dig('base-configs', 'all') || '' 73 | config += @attributes.dig('base-configs', daemon) || '' 74 | config += attributes['config'] || '' 75 | config_replace_variables(config, node.name, @cfg['logdir']) 76 | 77 | path = "#{node.mount_point}/#{@cfg['sysconfdir']}/#{daemon}.conf" 78 | File.open(path, 'w') { |file| file.write(config) } 79 | end 80 | 81 | def config_replace_variables(config, node_name, logdir) 82 | config.gsub!('%(node)', node_name) 83 | config.gsub!('%(logdir)', logdir) 84 | end 85 | 86 | def autogen_parse(parameters) 87 | # TODO: validate 88 | @autogen = parameters || {} 89 | end 90 | 91 | def autogen_node(type, _node_name, node, node_index) 92 | return unless type == Autogen::Router 93 | return unless @autogen["bird"] 94 | node['bird'] ||= {} 95 | node['bird']["bird"] = {} 96 | config = @autogen["bird"]['config'] || '' 97 | node['bird']["bird"]['config'] = config 98 | end 99 | 100 | def autogen_link(node, name, _local_attr, remote_attr) 101 | return unless @autogen["bird"] 102 | config = @autogen["bird"]['config-per-interface'] 103 | return unless config 104 | config = config.gsub('%(interface)', name) 105 | config = config.gsub('%(peer-v4)', remote_attr['ipv4'].split('/').first) if remote_attr['ipv4'] 106 | config = config.gsub('%(peer-v6)', remote_attr['ipv6'].split('/').first) if remote_attr['ipv6'] 107 | node['bird']["bird"]['config'] += config 108 | end 109 | 110 | def autogen_loopback(node, _node_index, name, attr) 111 | return unless @autogen["bird"] 112 | config = @autogen["bird"]['config-per-loopback'] 113 | return unless config 114 | config = config.gsub('%(interface)', name) 115 | config = config.gsub('%(address-v4)', attr['ipv4'].split('/').first) if attr['ipv4'] 116 | config = config.gsub('%(address-v6)', attr['ipv6'].split('/').first) if attr['ipv6'] 117 | node['bird']["bird"]['config'] += config 118 | end 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /lib/netgen/plugins/dynamips.rb: -------------------------------------------------------------------------------- 1 | require 'socket' 2 | 3 | module Netgen 4 | class PluginDynamips < Plugin 5 | def name 6 | 'dynamips' 7 | end 8 | 9 | def config_options 10 | { 11 | 'dir' => String, 12 | 'images' => Hash 13 | } 14 | end 15 | 16 | def default_config 17 | { 18 | 'dir' => "#{Config::NETGEN_RUNSTATEDIR}/dynamips", 19 | 'images' => {} 20 | } 21 | end 22 | 23 | def topology_init(topology) 24 | super 25 | FileUtils.rm_rf(@cfg['dir']) 26 | end 27 | 28 | def node_init(node) 29 | return unless node.attributes['dynamips'] 30 | FileUtils.mkdir_p(node_path(node)) 31 | end 32 | 33 | def node_start(node) 34 | return unless node.attributes['dynamips'] 35 | Netgen.log_info('starting dynamips', node: node, plugin: self) 36 | node.spawn('dynamips -H 7200', 37 | options: { out: '/dev/null', err: '/dev/null' }) 38 | create_config_file(node) 39 | Netgen.log_info('connecting to hypervisor', node: node, plugin: self) 40 | connect(node) 41 | rescue ArgumentError => e 42 | Netgen.log_err("error parsing topology: #{e}", plugin: self, node: node) 43 | exit(1) 44 | end 45 | 46 | # Disable tx checksum offloading 47 | # TODO: find out why this is necessary 48 | def link_init(node, link_name, _link_attributes) 49 | return if @attributes.empty? 50 | node.spawn("ethtool -K #{link_name} tx off", 51 | options: { out: '/dev/null', err: '/dev/null' }) 52 | end 53 | 54 | def connect(node) 55 | node.ns.switch_net_namespace do 56 | sock = TCPSocket.new('127.0.0.1', 7200) 57 | configure(node, sock) 58 | sock.close 59 | end 60 | rescue Errno::ECONNREFUSED 61 | sleep 0.1 62 | retry 63 | end 64 | 65 | def configure(node, sock) 66 | Netgen.log_info('configuring', node: node, plugin: self) 67 | image_name = node.attributes.dig('dynamips', 'image') 68 | raise ArgumentError, "unspecified dynamips image" unless image_name 69 | image = @cfg['images'][image_name] 70 | raise ArgumentError, "image #{image_name} is not configured" unless image 71 | 72 | # initialization 73 | command(node, sock, 'hypervisor version') 74 | command(node, sock, 'hypervisor reset') 75 | command(node, sock, "hypervisor working_dir \"#{node_path(node)}\"") 76 | 77 | # set image parameters according to the configuation 78 | image['parameters'].each_line do |line| 79 | command(node, sock, line) 80 | end 81 | 82 | # set the console listening port and configuration file 83 | command(node, sock, 'vm set_con_tcp_port rt 2000') 84 | command(node, sock, "vm set_config rt \"#{config_path(node)}\"") 85 | 86 | # setup interfaces 87 | node.attributes['links'].keys.each_with_index do |link, index| 88 | slot, port = image['interfaces'][index] 89 | command(node, sock, "nio create_linux_eth #{link} #{link}") 90 | command(node, sock, "vm slot_add_nio_binding rt #{slot} #{port} #{link}") 91 | end 92 | 93 | # start the router 94 | command(node, sock, 'vm start rt') 95 | end 96 | 97 | def command(node, sock, command) 98 | Netgen.log_debug("sending: #{command}", node: node, plugin: self) 99 | sock.puts(command) 100 | Netgen.log_debug("received: #{sock.gets}", node: node, plugin: self) 101 | end 102 | 103 | def node_path(node) 104 | "#{@cfg['dir']}/#{node.name}" 105 | end 106 | 107 | def config_path(node) 108 | "#{node_path(node)}/config.txt" 109 | end 110 | 111 | def create_config_file(node) 112 | config = @attributes.dig('base-config') || '' 113 | config += node.attributes.dig('dynamips', 'config') || '' 114 | config_replace_variables(config, node.name) 115 | File.open(config_path(node), 'w') { |file| file.write(config) } 116 | end 117 | 118 | def config_replace_variables(config, node_name) 119 | config.gsub!('%(node)', node_name) 120 | end 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /lib/netgen/plugins/ethernet.rb: -------------------------------------------------------------------------------- 1 | module Netgen 2 | class PluginEthernet < Plugin 3 | def name 4 | 'ethernet' 5 | end 6 | 7 | def link_init(node, link_name, link_attributes) 8 | mac = link_attributes['mac'] 9 | return unless mac 10 | node.spawn("ip link set #{link_name} address #{mac}") 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/netgen/plugins/frr.rb: -------------------------------------------------------------------------------- 1 | module Netgen 2 | class PluginFrr < Plugin 3 | @@daemons = %w[ 4 | zebra 5 | bgpd 6 | ospfd 7 | ospf6d 8 | isisd 9 | fabricd 10 | ripd 11 | ripngd 12 | eigrpd 13 | pimd 14 | ldpd 15 | nhrpd 16 | babeld 17 | bfdd 18 | sharpd 19 | staticd 20 | pbrd 21 | pathd 22 | mgmtd 23 | vrrpd 24 | ] 25 | 26 | def name 27 | 'frr' 28 | end 29 | 30 | def config_options 31 | { 32 | 'sysconfdir' => String, 33 | 'localstatedir' => String, 34 | 'user' => String, 35 | 'group' => String, 36 | 'logdir' => String 37 | } 38 | end 39 | 40 | def default_config 41 | { 42 | 'sysconfdir' => '/etc/frr', 43 | 'localstatedir' => '/var/run/frr', 44 | 'user' => 'frr', 45 | 'group' => 'frr', 46 | 'logdir' => "#{Config::NETGEN_RUNSTATEDIR}/frrlogs" 47 | } 48 | end 49 | 50 | def topology_init(topology) 51 | super 52 | setup_dirs 53 | end 54 | 55 | def node_init(node) 56 | return unless node.attributes['frr'] 57 | node.mount(@cfg['sysconfdir'], @cfg['user'], @cfg['group']) 58 | node.mount(@cfg['localstatedir'], @cfg['user'], @cfg['group']) 59 | create_config_files(node) 60 | end 61 | 62 | def node_start(node) 63 | frr_attr = node.attributes['frr'] 64 | return unless frr_attr 65 | @@daemons.each do |daemon| 66 | next unless frr_attr.has_key?(daemon) or daemon == "mgmtd" 67 | next if frr_attr.dig(daemon, 'run') == false 68 | delay = frr_attr.dig(daemon, 'delay') || nil 69 | args = frr_attr.dig(daemon, 'args') || '' 70 | 71 | out = "#{@cfg['logdir']}/#{node.name}-#{daemon}.out" 72 | err = "#{@cfg['logdir']}/#{node.name}-#{daemon}.err" 73 | 74 | if delay 75 | Netgen.log_info("scheduling to start #{daemon} in #{delay} seconds", 76 | plugin: self, node: node) 77 | else 78 | Netgen.log_info("starting #{daemon}", plugin: self, node: node) 79 | end 80 | node.spawn("#{valgrind(node, daemon)}#{perf(node, daemon)}#{daemon} #{args}", 81 | options: { out: out, err: err }, delay: delay) 82 | end 83 | node.spawn("vtysh -b", options: { out: '/dev/null', err: '/dev/null' }, delay: 3) 84 | end 85 | 86 | def node_exit(node) 87 | return unless node.attributes['frr'] 88 | #node.umount(@cfg['sysconfdir']) 89 | #node.umount(@cfg['localstatedir']) 90 | 91 | perf_gen_flamegraphs(node) 92 | end 93 | 94 | def setup_dirs 95 | FileUtils.rm(Dir.glob("#{@cfg['logdir']}/*.log")) 96 | FileUtils.mkdir_p(@cfg['sysconfdir']) 97 | FileUtils.mkdir_p(@cfg['localstatedir']) 98 | FileUtils.mkdir_p(@cfg['logdir']) 99 | FileUtils.chown_R(@cfg['user'], @cfg['group'], @cfg['logdir']) 100 | 101 | #FileUtils.rm(Dir.glob("#{@cfg['logdir']}/*.log")) 102 | FileUtils.rm_rf(Netgen.config.options['perf_dir']) 103 | FileUtils.mkdir_p(Netgen.config.options['perf_dir']) 104 | end 105 | 106 | def create_config_integrated(node) 107 | config = @attributes.dig('base-config') || '' 108 | config += node.attributes.dig('frr', 'config') || '' 109 | config_replace_variables(config, node.name, "%(daemon)", @cfg['logdir']) 110 | 111 | path = "#{node.mount_point}/#{@cfg['sysconfdir']}/frr.conf" 112 | File.open(path, 'w') { |file| file.write(config) } 113 | end 114 | 115 | def create_config_per_daemon(node) 116 | @@daemons.each do |daemon| 117 | next unless node.attributes.dig('frr', daemon) 118 | 119 | config = @attributes.dig('base-configs', 'all') || '' 120 | config += @attributes.dig('base-configs', daemon) || '' 121 | config += node.attributes.dig('frr', daemon, 'config') || '' 122 | config_replace_variables(config, node.name, daemon, @cfg['logdir']) 123 | 124 | path = "#{node.mount_point}/#{@cfg['sysconfdir']}/#{daemon}.conf" 125 | File.open(path, 'w') { |file| file.write(config) } 126 | end 127 | end 128 | 129 | def create_config_files(node) 130 | if node.attributes.dig('frr', 'config') 131 | create_config_integrated(node) 132 | else 133 | create_config_per_daemon(node) 134 | end 135 | FileUtils.touch("#{node.mount_point}/#{@cfg['sysconfdir']}/vtysh.conf") 136 | end 137 | 138 | def config_replace_variables(config, node_name, daemon, logdir) 139 | config.gsub!('%(node)', node_name) 140 | config.gsub!('%(daemon)', daemon) 141 | config.gsub!('%(logdir)', logdir) 142 | end 143 | 144 | def valgrind(node, daemon) 145 | return '' unless @attributes['valgrind'] || 146 | node.attributes.dig('frr', 'valgrind') || 147 | node.attributes.dig('frr', daemon, 'valgrind') 148 | 149 | params = Netgen.config.options['valgrind_params'] 150 | logfile = "#{@cfg['logdir']}/#{node.name}-#{daemon}-valgrind.log" 151 | #callgrind_out_file = "#{@cfg['logdir']}/#{node.name}-#{daemon}-callgrind.out" 152 | "valgrind #{params} --log-file='#{logfile}' " 153 | #"valgrind #{params} --log-file='#{logfile}' --callgrind-out-file='#{callgrind_out_file}' " 154 | end 155 | 156 | def perf(node, daemon) 157 | return '' unless @attributes['perf'] || 158 | node.attributes.dig('frr', 'perf') || 159 | node.attributes.dig('frr', daemon, 'perf') 160 | 161 | perf_data = "#{perf_basename(node, daemon)}.data" 162 | "perf record -g --call-graph=dwarf -o #{perf_data} -- " 163 | end 164 | 165 | def perf_basename(node, daemon) 166 | "#{Netgen.config.options['perf_dir']}/#{node.name}-#{daemon}-perf" 167 | end 168 | 169 | def perf_gen_flamegraphs(node) 170 | @@daemons.each do |daemon| 171 | next unless node.attributes.dig('frr', daemon) 172 | return if perf(node, daemon) == '' 173 | perf_data = "#{perf_basename(node, daemon)}.data" 174 | out_perf = "#{perf_basename(node, daemon)}.perf" 175 | out_folded = "#{perf_basename(node, daemon)}.folded" 176 | out_svg = "#{perf_basename(node, daemon)}.svg" 177 | node.execute("perf script -i #{perf_data} > #{out_perf}") 178 | node.execute("stackcollapse-perf.pl #{out_perf} > #{out_folded}") 179 | node.execute("flamegraph.pl #{out_folded} > #{out_svg}") 180 | end 181 | end 182 | 183 | def autogen_parse(parameters) 184 | # TODO: validate 185 | @autogen = parameters || {} 186 | end 187 | 188 | def autogen_node(type, node_name, node, node_index) 189 | return unless type == Autogen::Router 190 | 191 | @@daemons.each do |daemon| 192 | next unless @autogen[daemon] 193 | node['frr'] ||= {} 194 | node['frr'][daemon] = {} 195 | config = @autogen[daemon]['config'] || '' 196 | config = config.gsub('%(bgp-node-index)', "#{node_index}") 197 | config = config.gsub('%(isis-node-index)', node_index.to_s.rjust(4, '0')) 198 | gen_static_routes(config, node_index) if daemon == 'zebra' 199 | node['frr'][daemon]['config'] = config 200 | end 201 | end 202 | 203 | def autogen_link(node, name, _local_attr, remote_attr) 204 | @@daemons.each do |daemon| 205 | next unless @autogen[daemon] 206 | config = @autogen[daemon]['config-per-interface'] 207 | next unless config 208 | config = config.gsub('%(interface)', name) 209 | config = config.gsub('%(peer-v4)', remote_attr['ipv4'].split('/').first) if remote_attr['ipv4'] 210 | config = config.gsub('%(peer-v6)', remote_attr['ipv6'].split('/').first) if remote_attr['ipv6'] 211 | node['frr'][daemon]['config'] += config 212 | end 213 | end 214 | 215 | def autogen_loopback(node, node_index, name, attr) 216 | @@daemons.each do |daemon| 217 | next unless @autogen[daemon] 218 | config = @autogen[daemon]['config-per-loopback'] 219 | next unless config 220 | config = config.gsub('%(interface)', name) 221 | config = config.gsub('%(node-index)', "#{node_index}") 222 | config = config.gsub('%(address-v4)', attr['ipv4'].split('/').first) if attr['ipv4'] 223 | config = config.gsub('%(address-v6)', attr['ipv6'].split('/').first) if attr['ipv6'] 224 | config = config.gsub('%(prefix-v4)', attr['ipv4']) if attr['ipv4'] 225 | config = config.gsub('%(prefix-v6)', attr['ipv6']) if attr['ipv6'] 226 | node['frr'][daemon]['config'] += config 227 | end 228 | end 229 | 230 | def autogen_stub(node, node_index, stub_index, name, attr) 231 | @@daemons.each do |daemon| 232 | next unless @autogen[daemon] 233 | config = @autogen[daemon]['config-per-stub'] 234 | next unless config 235 | config = config.gsub('%(interface)', name) 236 | config = config.gsub('%(node-index)', "#{node_index}") 237 | config = config.gsub('%(stub-index)', "#{stub_index}") 238 | config = config.gsub('%(address-v4)', attr['ipv4'].split('/').first) if attr['ipv4'] 239 | config = config.gsub('%(address-v6)', attr['ipv6'].split('/').first) if attr['ipv6'] 240 | config = config.gsub('%(prefix-v4)', attr['ipv4']) if attr['ipv4'] 241 | config = config.gsub('%(prefix-v6)', attr['ipv6']) if attr['ipv6'] 242 | node['frr'][daemon]['config'] += config 243 | end 244 | end 245 | 246 | def gen_static_routes(config, node_index) 247 | params = @autogen['zebra']['static-route-generator'] 248 | return unless params 249 | gen_static_routes_af(config, node_index, params['ipv4'], Socket::AF_INET) 250 | gen_static_routes_af(config, node_index, params['ipv6'], Socket::AF_INET6) 251 | end 252 | 253 | def gen_static_routes_af(config, node_index, params, af) 254 | return unless params 255 | 256 | routes = IPCalc.new(af, params['start'], params['prefixlen'], 257 | params['step'], params['step-by-router'], 258 | params['number']) 259 | routes.each(node_index) do |prefix| 260 | config << "ip route #{prefix} #{params['nexthop']}\n" 261 | end 262 | end 263 | end 264 | end 265 | -------------------------------------------------------------------------------- /lib/netgen/plugins/holod.rb: -------------------------------------------------------------------------------- 1 | module Netgen 2 | class PluginHolod < Plugin 3 | def name 4 | 'holod' 5 | end 6 | 7 | def config_options 8 | { 9 | 'bindir-daemon' => String, 10 | 'bindir-cli' => String, 11 | 'sysconfdir' => String, 12 | 'localstatedir' => String, 13 | 'user' => String, 14 | 'group' => String, 15 | 'logdir' => String 16 | } 17 | end 18 | 19 | def default_config 20 | { 21 | 'bindir-daemon' => '/usr/bin/', 22 | 'bindir-cli' => '/usr/bin/', 23 | 'sysconfdir' => '/etc/holod', 24 | 'localstatedir' => '/var/run/holo', 25 | 'user' => 'holo', 26 | 'group' => 'holo', 27 | 'logdir' => "#{Config::NETGEN_RUNSTATEDIR}/holod-logs" 28 | } 29 | end 30 | 31 | def topology_init(topology) 32 | super 33 | setup_dirs 34 | end 35 | 36 | def node_init(node) 37 | return unless node.attributes['holod'] 38 | node.mount(@cfg['sysconfdir']) 39 | node.mount(@cfg['localstatedir'], @cfg['user'], @cfg['group']) 40 | node.mount('/var/log') 41 | create_config_files(node) 42 | end 43 | 44 | def node_start(node) 45 | holod_attr = node.attributes['holod'] 46 | return unless holod_attr 47 | return if holod_attr.dig('run') == false 48 | delay = holod_attr.dig('delay') || 3 49 | 50 | Netgen.log_info("starting holod", plugin: self, node: node) 51 | out = "#{@cfg['logdir']}/#{node.name}.out" 52 | err = "#{@cfg['logdir']}/#{node.name}.err" 53 | node.spawn("#{perf(node)} #{@cfg['bindir-daemon']}/holod", env: {"RUST_LOG" => "holo=trace", "RUST_BACKTRACE" => "1"}, options: { out: out, err: err }) 54 | 55 | # Enter configuration. 56 | config_path = "#{node.mount_point}/#{@cfg['sysconfdir']}/holod.config" 57 | node.spawn("#{@cfg['bindir-cli']}/holo-cli --file #{config_path}", delay: delay) 58 | end 59 | 60 | def node_exit(node) 61 | return unless node.attributes['holod'] 62 | #node.umount(@cfg['sysconfdir']) 63 | #node.umount(@cfg['localstatedir']) 64 | 65 | perf_gen_flamegraphs(node) 66 | end 67 | 68 | def setup_dirs 69 | FileUtils.rm(Dir.glob("#{@cfg['logdir']}/*.log")) 70 | FileUtils.mkdir_p(@cfg['sysconfdir']) 71 | FileUtils.mkdir_p(@cfg['localstatedir']) 72 | FileUtils.mkdir_p(@cfg['logdir']) 73 | #FileUtils.chown_R(@cfg['user'], @cfg['group'], @cfg['localstatedir']) 74 | FileUtils.chown_R(@cfg['user'], @cfg['group'], @cfg['logdir']) 75 | 76 | #FileUtils.rm(Dir.glob("#{@cfg['logdir']}/*.log")) 77 | FileUtils.rm_rf(Netgen.config.options['perf_dir']) 78 | FileUtils.mkdir_p(Netgen.config.options['perf_dir']) 79 | end 80 | 81 | def create_config_files(node) 82 | config = @attributes.dig('base-config') || '' 83 | config += node.attributes.dig('holod', 'config') || '' 84 | config_replace_variables(config, node.name, @cfg['logdir']) 85 | 86 | path = "#{node.mount_point}/#{@cfg['sysconfdir']}/holod.config" 87 | File.open(path, 'w') { |file| file.write(config) } 88 | end 89 | 90 | def config_replace_variables(config, node_name, logdir) 91 | config.gsub!('%(node)', node_name) 92 | config.gsub!('%(logdir)', logdir) 93 | end 94 | 95 | def perf(node) 96 | return '' unless @attributes['perf'] || 97 | node.attributes.dig('holod', 'perf') 98 | 99 | perf_data = "#{perf_basename(node)}.data" 100 | "perf record -F 99 -g --call-graph=dwarf -o #{perf_data} -- " 101 | end 102 | 103 | def perf_basename(node) 104 | "#{Netgen.config.options['perf_dir']}/#{node.name}-holod-perf" 105 | end 106 | 107 | def perf_gen_flamegraphs(node) 108 | return unless node.attributes['holod'] 109 | return if perf(node) == '' 110 | perf_data = "#{perf_basename(node)}.data" 111 | out_perf = "#{perf_basename(node)}.perf" 112 | out_folded = "#{perf_basename(node)}.folded" 113 | out_svg = "#{perf_basename(node)}.svg" 114 | sleep 1 115 | node.execute("perf script -i #{perf_data} > #{out_perf}") 116 | node.execute("stackcollapse-perf.pl #{out_perf} > #{out_folded}") 117 | node.execute("flamegraph.pl #{out_folded} > #{out_svg}") 118 | end 119 | 120 | def autogen_parse(parameters) 121 | # TODO: validate 122 | @autogen = parameters || {} 123 | end 124 | 125 | def autogen_node(type, node_name, node, node_index) 126 | return unless type == Autogen::Router 127 | return unless @autogen['config'] 128 | node['holod'] ||= {} 129 | node['holod'] = {} 130 | config = @autogen['config'] || '' 131 | node['holod']['config'] = config 132 | end 133 | 134 | def autogen_link(node, name, _local_attr, remote_attr) 135 | config = @autogen['config-per-interface'] 136 | return unless config 137 | config = config.gsub('%(interface)', name) 138 | config = config.gsub('%(peer-v4)', remote_attr['ipv4'].split('/').first) if remote_attr['ipv4'] 139 | config = config.gsub('%(peer-v6)', remote_attr['ipv6'].split('/').first) if remote_attr['ipv6'] 140 | node['holod']['config'] += config 141 | end 142 | 143 | def autogen_loopback(node, node_index, name, attr) 144 | config = @autogen['config-per-loopback'] 145 | return unless config 146 | config = config.gsub('%(interface)', name) 147 | config = config.gsub('%(node-index)', "#{node_index}") 148 | config = config.gsub('%(address-v4)', attr['ipv4'].split('/').first) if attr['ipv4'] 149 | config = config.gsub('%(address-v6)', attr['ipv6'].split('/').first) if attr['ipv6'] 150 | config = config.gsub('%(prefix-v4)', attr['ipv4']) if attr['ipv4'] 151 | config = config.gsub('%(prefix-v6)', attr['ipv6']) if attr['ipv6'] 152 | node['holod']['config'] += config 153 | end 154 | 155 | #def autogen_stub(node, node_index, stub_index, name, attr) 156 | #end 157 | end 158 | end 159 | -------------------------------------------------------------------------------- /lib/netgen/plugins/iou.rb: -------------------------------------------------------------------------------- 1 | require 'socket' 2 | 3 | module Netgen 4 | class PluginIou < Plugin 5 | def name 6 | 'iou' 7 | end 8 | 9 | def config_options 10 | { 11 | 'dir' => String, 12 | 'images' => Hash 13 | } 14 | end 15 | 16 | def default_config 17 | { 18 | 'dir' => "#{Config::NETGEN_RUNSTATEDIR}/iou", 19 | 'images' => {} 20 | } 21 | end 22 | 23 | def topology_init(topology) 24 | super 25 | FileUtils.rm_rf(@cfg['dir']) 26 | end 27 | 28 | def node_init(node) 29 | return unless node.attributes['iou'] 30 | FileUtils.mkdir_p(node_path(node)) 31 | node.mount("/tmp/netio0") 32 | end 33 | 34 | def node_start(node) 35 | return unless node.attributes['iou'] 36 | Netgen.log_info('starting iou', node: node, plugin: self) 37 | create_netmap_file(node) 38 | create_config_file(node) 39 | 40 | image_name = node.attributes.dig('iou', 'image') 41 | raise ArgumentError, "unspecified iou image" unless image_name 42 | image = @cfg['images'][image_name] 43 | raise ArgumentError, "image #{image_name} is not configured" unless image 44 | 45 | node.ns.switch_net_namespace do 46 | node.spawn("NETIO_NETMAP=#{netmap_path(node)} wrapper.bin -m #{image['file']} -p 2000 -- -e 16 -s 0 -c #{config_path(node)} #{node.index + 1} &", 47 | options: {}, mnt: false, pid: false, net: false) 48 | node.attributes['links'].keys.each_with_index do |link, index| 49 | node.spawn("ioulive86 -n #{netmap_path(node)} -i #{link} #{1000 + index} &", 50 | options: {}, mnt: false, pid: false, net: false) 51 | end 52 | end 53 | rescue ArgumentError => e 54 | Netgen.log_err("error parsing topology: #{e}", plugin: self, node: node) 55 | exit(1) 56 | end 57 | 58 | # Disable tx checksum offloading 59 | # TODO: find out why this is necessary 60 | def link_init(node, link_name, _link_attributes) 61 | return if @attributes.empty? 62 | node.spawn("ethtool -K #{link_name} tx off", 63 | options: { out: '/dev/null', err: '/dev/null' }) 64 | end 65 | 66 | def node_path(node) 67 | "#{@cfg['dir']}/#{node.name}" 68 | end 69 | 70 | def config_path(node) 71 | "#{node_path(node)}/config.txt" 72 | end 73 | 74 | def netmap_path(node) 75 | "#{node_path(node)}/NETMAP" 76 | end 77 | 78 | def create_config_file(node) 79 | config = @attributes.dig('base-config') || '' 80 | config += node.attributes.dig('iou', 'config') || '' 81 | config_replace_variables(config, node.name) 82 | File.open(config_path(node), 'w') { |file| file.write(config) } 83 | end 84 | 85 | def config_replace_variables(config, node_name) 86 | config.gsub!('%(node)', node_name) 87 | end 88 | 89 | def create_netmap_file(node) 90 | hostname = `hostname`.rstrip 91 | content = '' 92 | node.attributes['links'].keys.each_with_index do |link, index| 93 | content += "#{node.index + 1}:0/#{index}@#{hostname} #{1000 + index}:0/0@#{hostname}\n" 94 | end 95 | File.open(netmap_path(node), 'w') { |file| file.write(content) } 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /lib/netgen/plugins/ipv4.rb: -------------------------------------------------------------------------------- 1 | module Netgen 2 | class PluginIpv4 < Plugin 3 | def name 4 | 'ipv4' 5 | end 6 | 7 | def node_init(node) 8 | node.spawn('sysctl -wq net.ipv4.ip_forward=1') 9 | node.spawn('sysctl -wq net.ipv4.igmp_max_memberships=100000') 10 | end 11 | 12 | def node_start(node) 13 | routes = node.attributes.dig('ipv4', 'routes') 14 | return unless routes 15 | routes.each do |route| 16 | node.spawn("ip -4 route add #{route}") 17 | end 18 | end 19 | 20 | def link_init(node, link_name, link_attributes) 21 | ipv4 = link_attributes['ipv4'] 22 | return unless ipv4 23 | node.spawn("ip -4 addr add #{ipv4} dev #{link_name}") 24 | # Disable Reverse Path Filtering to make unnumbered interfaces work 25 | node.spawn("sysctl -wq net.ipv4.conf.#{link_name}.rp_filter=0") 26 | end 27 | 28 | def link_exit(node, link_name, link_attributes) 29 | ipv4 = link_attributes['ipv4'] 30 | return unless ipv4 31 | node.spawn("ip -4 addr del #{ipv4} dev #{link_name}") 32 | end 33 | 34 | @subnet_index = -1 35 | def self.next_subnet 36 | @subnet_index += 1 37 | end 38 | 39 | def autogen_parse(parameters) 40 | # TODO: validate 41 | @autogen = parameters || {} 42 | return unless parameters 43 | 44 | routes = parameters['routes'] 45 | if routes 46 | @autogen['_routes'] = IPCalc.new(Socket::AF_INET, routes['start'], 47 | routes['prefixlen'], routes['step'], 48 | routes['step-by-router'], 49 | routes['number']) 50 | end 51 | 52 | subnets = parameters['subnets'] 53 | if subnets 54 | @autogen['subnets'] = IPCalc.new(Socket::AF_INET, subnets['start'], 55 | subnets['prefixlen'], subnets['step']) 56 | end 57 | 58 | loopbacks = parameters['loopbacks'] 59 | if loopbacks 60 | @autogen['loopbacks'] = IPCalc.new(Socket::AF_INET, loopbacks['start'], 61 | 32, loopbacks['step']) 62 | end 63 | 64 | stubs = parameters['stubs'] 65 | if stubs 66 | @autogen['stubs'] = IPCalc.new(Socket::AF_INET, stubs['start'], 67 | stubs['prefixlen'], stubs['step'], 68 | stubs['step-by-router']) 69 | end 70 | end 71 | 72 | def autogen_node(type, _node_name, node, node_index) 73 | return unless @autogen['_routes'] 74 | node['ipv4'] ||= {} 75 | node['ipv4']['routes'] ||= [] 76 | params = @autogen['routes'] 77 | @autogen['_routes'].each(node_index) do |prefix| 78 | route = "#{prefix}" 79 | route += " via #{params['nexthop-addr']}" if params['nexthop-addr'] 80 | route += " dev #{params['nexthop-if']}" if params['nexthop-if'] 81 | route += " proto #{params['protocol']}" if params['protocol'] 82 | node['ipv4']['routes'].push(route) 83 | end 84 | end 85 | 86 | def autogen_link(_node, _name, local_attr, remote_attr) 87 | return if local_attr['ipv4'] 88 | return unless @autogen['subnets'] 89 | 90 | @autogen['subnets'].fetch(self.class.next_subnet) do |calc, address| 91 | local = address + 1 92 | remote = address + 2 93 | local_attr['ipv4'] = calc.print(local) 94 | remote_attr['ipv4'] = calc.print(remote) 95 | end 96 | end 97 | 98 | def autogen_loopback(_node, node_index, _name, attr) 99 | return unless @autogen['loopbacks'] 100 | attr['ipv4'] = @autogen['loopbacks'].fetch(node_index) 101 | end 102 | 103 | def autogen_stub(_node, node_index, stub_index, _name, attr) 104 | return unless @autogen['stubs'] 105 | attr['ipv4'] = @autogen['stubs'].fetch(stub_index, node_index) 106 | end 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /lib/netgen/plugins/ipv6.rb: -------------------------------------------------------------------------------- 1 | module Netgen 2 | class PluginIpv6 < Plugin 3 | def name 4 | 'ipv6' 5 | end 6 | 7 | def node_init(node) 8 | node.spawn('sysctl -wq net.ipv6.conf.all.forwarding=1') 9 | node.spawn('sysctl -wq net.ipv6.conf.all.keep_addr_on_down=1') 10 | end 11 | 12 | def node_start(node) 13 | routes = node.attributes.dig('ipv6', 'routes') 14 | return unless routes 15 | routes.each do |route| 16 | node.spawn("ip -6 route add #{route}") 17 | end 18 | end 19 | 20 | def link_init(node, link_name, link_attributes) 21 | ipv6 = link_attributes['ipv6'] 22 | return unless ipv6 23 | node.spawn("ip -6 addr add #{ipv6} dev #{link_name}") 24 | end 25 | 26 | def link_exit(node, link_name, link_attributes) 27 | ipv6 = link_attributes['ipv6'] 28 | return unless ipv6 29 | node.spawn("ip -6 addr del #{ipv6} dev #{link_name}") 30 | end 31 | 32 | @subnet_index = -1 33 | def self.next_subnet 34 | @subnet_index += 1 35 | end 36 | 37 | def autogen_parse(parameters) 38 | # TODO: validate 39 | @autogen = parameters || {} 40 | return unless parameters 41 | 42 | routes = parameters['routes'] 43 | if routes 44 | @autogen['_routes'] = IPCalc.new(Socket::AF_INET6, routes['start'], 45 | routes['prefixlen'], routes['step'], 46 | routes['step-by-router'], 47 | routes['number']) 48 | end 49 | 50 | subnets = parameters['subnets'] 51 | if subnets 52 | @autogen['subnets'] = IPCalc.new(Socket::AF_INET6, subnets['start'], 53 | subnets['prefixlen'], subnets['step']) 54 | end 55 | 56 | loopbacks = parameters['loopbacks'] 57 | if loopbacks 58 | @autogen['loopbacks'] = IPCalc.new(Socket::AF_INET6, loopbacks['start'], 59 | 128, loopbacks['step']) 60 | end 61 | 62 | stubs = parameters['stubs'] 63 | if stubs 64 | @autogen['stubs'] = IPCalc.new(Socket::AF_INET6, stubs['start'], 65 | stubs['prefixlen'], stubs['step'], 66 | stubs['step-by-router']) 67 | end 68 | end 69 | 70 | def autogen_node(type, _node_name, node, node_index) 71 | return unless @autogen['_routes'] 72 | node['ipv6'] ||= {} 73 | node['ipv6']['routes'] ||= [] 74 | params = @autogen['routes'] 75 | @autogen['_routes'].each(node_index) do |prefix| 76 | route = "#{prefix}" 77 | route += " via #{params['nexthop-addr']}" if params['nexthop-addr'] 78 | route += " dev #{params['nexthop-if']}" if params['nexthop-if'] 79 | route += " proto #{params['protocol']}" if params['protocol'] 80 | node['ipv6']['routes'].push(route) 81 | end 82 | end 83 | 84 | def autogen_link(_node, _name, local_attr, remote_attr) 85 | return if local_attr['ipv6'] 86 | return unless @autogen['subnets'] 87 | 88 | @autogen['subnets'].fetch(self.class.next_subnet) do |calc, address| 89 | local = address + 1 90 | remote = address + 2 91 | local_attr['ipv6'] = calc.print(local) 92 | remote_attr['ipv6'] = calc.print(remote) 93 | end 94 | end 95 | 96 | def autogen_loopback(_node, node_index, _name, attr) 97 | return unless @autogen['loopbacks'] 98 | attr['ipv6'] = @autogen['loopbacks'].fetch(node_index) 99 | end 100 | 101 | def autogen_stub(_node, node_index, stub_index, _name, attr) 102 | return unless @autogen['stubs'] 103 | attr['ipv6'] = @autogen['stubs'].fetch(stub_index, node_index) 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /lib/netgen/plugins/mpls.rb: -------------------------------------------------------------------------------- 1 | module Netgen 2 | class PluginMpls < Plugin 3 | def name 4 | 'mpls' 5 | end 6 | 7 | def node_init(node) 8 | node.spawn('modprobe mpls_router') 9 | node.spawn('modprobe mpls_iptunnel') 10 | node.spawn('sysctl -wq net.mpls.platform_labels=1048575') 11 | end 12 | 13 | def link_init(node, link_name, link_attributes) 14 | mpls = link_attributes['mpls'] 15 | return unless mpls 16 | node.spawn("sysctl -wq net.mpls.conf.#{link_name}.input=1") 17 | end 18 | 19 | def link_exit(node, link_name, link_attributes) 20 | mpls = link_attributes['mpls'] 21 | return unless mpls 22 | node.spawn("sysctl -wq net.mpls.conf.#{link_name}.input=0") 23 | end 24 | 25 | def autogen_parse(parameters) 26 | # TODO: validate 27 | @autogen = parameters || {} 28 | end 29 | 30 | def autogen_link(_node, name, local_attr, remote_attr) 31 | return unless @autogen['enable'] 32 | local_attr['mpls'] = true 33 | end 34 | 35 | def autogen_loopback(_node, node_index, _name, local_attr) 36 | return unless @autogen['enable'] 37 | local_attr['mpls'] = true 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/netgen/plugins/netem.rb: -------------------------------------------------------------------------------- 1 | module Netgen 2 | class PluginNetem < Plugin 3 | def name 4 | 'netem' 5 | end 6 | 7 | def link_init(node, link_name, link_attributes) 8 | netem = link_attributes['netem'] 9 | return unless netem 10 | node.spawn("tc qdisc add dev #{link_name} root netem #{netem}") 11 | end 12 | 13 | def link_exit(node, link_name, link_attributes) 14 | netem = link_attributes['netem'] 15 | return unless netem 16 | node.spawn("tc qdisc del dev #{link_name} root netem #{netem}") 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/netgen/plugins/shell.rb: -------------------------------------------------------------------------------- 1 | module Netgen 2 | class PluginShell < Plugin 3 | def name 4 | 'shell' 5 | end 6 | 7 | def config_options 8 | { 9 | } 10 | end 11 | 12 | def default_config 13 | { 14 | } 15 | end 16 | 17 | def node_start(node) 18 | cmds = node.attributes['shell'] 19 | return unless cmds 20 | cmds.each_line do |cmd| 21 | next if cmd.start_with?("#") 22 | node.execute("sh -c '" + cmd + "'") 23 | end 24 | end 25 | 26 | def autogen_parse(parameters) 27 | # TODO: validate 28 | @autogen = parameters || {} 29 | end 30 | 31 | def autogen_node(type, _node_name, node, node_index) 32 | return unless @autogen 33 | node['shell'] = @autogen 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/netgen/plugins/tcpdump.rb: -------------------------------------------------------------------------------- 1 | module Netgen 2 | class PluginTcpdump < Plugin 3 | def name 4 | 'tcpdump' 5 | end 6 | 7 | def config_options 8 | { 9 | 'pcap_dir' => String, 10 | 'whitelist' => Array, 11 | 'blacklist' => Array 12 | } 13 | end 14 | 15 | def default_config 16 | { 17 | 'pcap_dir' => "#{Config::NETGEN_RUNSTATEDIR}/pcaps", 18 | 'whitelist' => [], 19 | 'blacklist' => [] 20 | } 21 | end 22 | 23 | def node_init(node) 24 | FileUtils.mkdir_p("#{pcap_dir(node)}") 25 | FileUtils.rm(Dir.glob("#{pcap_dir(node)}/*.pcap")) 26 | end 27 | 28 | def link_init(node, link_name, link_attributes) 29 | # Don't run tcpdump on stub links 30 | return unless link_attributes['peer'] 31 | 32 | # Check whitelist and blacklist 33 | return unless @cfg['whitelist'].empty? || 34 | @cfg['whitelist'].include?(node.name) 35 | return if @cfg['blacklist'].include?(node.name) 36 | 37 | path = "#{pcap_dir(node)}/#{link_name}.pcap" 38 | node.spawn("tcpdump -i #{link_name} -U -w #{path}", 39 | options: { out: '/dev/null', err: '/dev/null' }) 40 | end 41 | 42 | def pcap_dir(node) 43 | "#{@cfg['pcap_dir']}/#{node.name}" 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/netgen/plugins/tmux.rb: -------------------------------------------------------------------------------- 1 | module Netgen 2 | class PluginTmux < Plugin 3 | def name 4 | 'tmux' 5 | end 6 | 7 | def config_options 8 | { 9 | 'file' => String, 10 | 'panels-per-node' => Fixnum 11 | } 12 | end 13 | 14 | def default_config 15 | { 16 | 'file' => "#{Config::NETGEN_RUNSTATEDIR}/tmux.sh", 17 | 'panels-per-node' => 1 18 | } 19 | end 20 | 21 | def topology_exit 22 | FileUtils.rm(@cfg['file']) 23 | end 24 | 25 | def topology_start 26 | Netgen.log_info('generating script', plugin: self) 27 | content = generate_script 28 | write_script(content, @cfg['file']) 29 | end 30 | 31 | def generate_script 32 | content = "#!/bin/sh\n" 33 | content += "export TMUX=\n" 34 | nodes = Netgen.topology.nodes 35 | nodes.values.each do |node| 36 | content += if node == nodes.values.first 37 | 'tmux new-session -d -s netgen ' 38 | else 39 | 'tmux new-window -t netgen ' 40 | end 41 | command = "nsenter -t #{node.ns.pid} --mount --pid --net --wd=. bash" 42 | content += "-n #{node.name} '#{command}'\n" 43 | (@cfg['panels-per-node'] - 1).times do 44 | content += "tmux split-window -h '#{command}'\n" 45 | end 46 | content += "tmux select-layout even-horizontal\n" 47 | end 48 | content += "tmux attach-session -d -t netgen\n" 49 | end 50 | 51 | def write_script(content, path) 52 | File.open(path, 'w') { |file| file.write(content) } 53 | FileUtils.chmod 'u=+x', path 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/netgen/router.rb: -------------------------------------------------------------------------------- 1 | module Netgen 2 | class Router < Node 3 | # Bring up router's loopback interface. 4 | def setup 5 | super 6 | spawn('ip link set dev lo up') 7 | spawn('sysctl net.ipv4.tcp_l3mdev_accept=1') 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/netgen/switch.rb: -------------------------------------------------------------------------------- 1 | module Netgen 2 | class Switch < Node 3 | # Create virtual bridge and bring it up. 4 | def setup 5 | super 6 | execute('ip link add name br0 type bridge') 7 | execute('ip link set dev br0 up') 8 | end 9 | 10 | # Add all links to the bridge. 11 | def setup_links(nodes) 12 | super 13 | @attributes['links'].keys.each do |name| 14 | spawn("ip link set #{name} master br0") 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/netgen/topology.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | 3 | module Netgen 4 | class Topology 5 | attr_accessor :nodes 6 | 7 | def initialize(filename) 8 | @nodes = {} 9 | 10 | topology = YAML.load_file(filename) 11 | 12 | autogen_start(topology) if topology['autogen'] 13 | 14 | Netgen.plugins.each do |plugin| 15 | plugin.topology_init(topology) 16 | end 17 | Hash(topology['routers']).each do |name, attributes| 18 | @nodes[name] = Router.new(name, attributes) 19 | end 20 | Hash(topology['switches']).each do |name, attributes| 21 | @nodes[name] = Switch.new(name, attributes) 22 | end 23 | rescue SystemCallError, Psych::SyntaxError, ArgumentError => e 24 | $stderr.puts e.message 25 | $stderr.puts e.backtrace 26 | exit(1) 27 | end 28 | 29 | # Check if the topology is valid or not. 30 | def check_consistency 31 | @nodes.values.each do |node| 32 | node.check_consistency(@nodes) 33 | end 34 | end 35 | 36 | # Setup topology. All nodes must be created before creating the links. 37 | def setup 38 | Netgen.log_info("netgen: setting up nodes") 39 | @nodes.values.each(&:setup) 40 | Netgen.log_info("netgen: creating links") 41 | @nodes.values.each do |node| 42 | node.setup_links(@nodes) 43 | end 44 | end 45 | 46 | # Cleanup topology 47 | def cleanup 48 | #if Netgen.config.options['clean_exit'] == 'true' 49 | Netgen.log_info("netgen: cleaning up everything") 50 | FileUtils.rm(pids_filename) 51 | Netgen.plugins.each(&:topology_exit) 52 | @nodes.values.each(&:cleanup) 53 | 54 | # Kill all children 55 | @nodes.values.each do |node| 56 | Process.kill(:TERM, node.ns.pid) 57 | end 58 | Netgen.log_info("netgen: waiting for children to terminate") 59 | Process.waitall 60 | end 61 | 62 | # Start the topology once everything is initialized 63 | def start 64 | Netgen.log_info('netgen: starting topology') 65 | generate_pids_file 66 | Netgen.plugins.each(&:topology_start) 67 | @nodes.values.each(&:start) 68 | Netgen.log_info('netgen: topology started') 69 | end 70 | 71 | def pids_filename 72 | "#{Netgen.config.options['netgen_runstatedir']}/pids.yml" 73 | end 74 | 75 | def generate_pids_file 76 | pids = {} 77 | @nodes.each do |name, node| 78 | pids[name] = node.ns.pid 79 | end 80 | File.open(pids_filename, 'w') { |file| file.write(pids.to_yaml) } 81 | end 82 | 83 | # Generate topology 84 | def autogen_start(topology) 85 | description = topology.dig('autogen', 'layout') 86 | case topology.dig('autogen', 'layout', 'type') 87 | when 'line' 88 | autogen = Autogen::LayoutLine.new(description) 89 | when 'ring' 90 | autogen = Autogen::LayoutRing.new(description) 91 | when 'grid' 92 | autogen = Autogen::LayoutGrid.new(description) 93 | when 'tree' 94 | autogen = Autogen::LayoutTree.new(description) 95 | when 'full-mesh' 96 | autogen = Autogen::LayoutFullMesh.new(description) 97 | when 'bus' 98 | autogen = Autogen::LayoutBus.new(description) 99 | else 100 | raise ArgumentError, "Unknown or unspecified layout type" 101 | end 102 | autogen.parse 103 | 104 | Netgen.plugins.each do |plugin| 105 | description = topology.dig('autogen', plugin.name) 106 | plugin.autogen_parse(description) 107 | end 108 | 109 | Netgen.log_info('autogen: generating topology') 110 | autogen.generate 111 | topology.merge!(autogen.output) 112 | Netgen.log_info('autogen: done') 113 | 114 | # Save generated topology 115 | if Netgen.output 116 | topology.delete('autogen') 117 | File.open(Netgen.output, 'w') { |file| file.write(topology.to_yaml) } 118 | end 119 | end 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /lib/netgen/version.rb: -------------------------------------------------------------------------------- 1 | module Netgen 2 | VERSION = '0.1.0' 3 | end 4 | -------------------------------------------------------------------------------- /netgen.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'netgen/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'netgen' 8 | spec.version = Netgen::VERSION 9 | spec.authors = ['Renato Westphal'] 10 | spec.email = ['renato@opensourcerouting.org'] 11 | 12 | spec.summary = %q{XXX: Write a short summary, because Rubygems requires one.} 13 | spec.description = %q{XXX: Write a longer description or delete this line.} 14 | spec.homepage = "https://github.com/rwestphal/netgen" 15 | 16 | # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host' 17 | # to allow pushing to a single host or delete this section to allow pushing to any host. 18 | if spec.respond_to?(:metadata) 19 | spec.metadata['allowed_push_host'] = "XXX: Set to 'http://mygemserver.com'" 20 | else 21 | raise 'RubyGems 2.0 or newer is required to protect against ' \ 22 | 'public gem pushes.' 23 | end 24 | 25 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 26 | f.match(%r{^(test|spec|features)/}) 27 | end 28 | spec.bindir = 'exe' 29 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 30 | spec.require_paths = ['lib'] 31 | 32 | spec.required_ruby_version = '>= 2.3.0' 33 | spec.add_dependency 'ffi' 34 | spec.add_development_dependency 'bundler', '>= 2.5.20' 35 | spec.add_development_dependency 'rake', '~> 13.2.0' 36 | spec.add_development_dependency 'rspec', '~> 3.13.0' 37 | end 38 | -------------------------------------------------------------------------------- /spec/netgen_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Netgen do 4 | it 'has a version number' do 5 | expect(Netgen::VERSION).not_to be nil 6 | end 7 | 8 | it 'does something useful' do 9 | expect(false).to eq(true) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'netgen' 3 | 4 | RSpec.configure do |config| 5 | # Enable flags like --only-failures and --next-failure 6 | config.example_status_persistence_file_path = '.rspec_status' 7 | 8 | # Disable RSpec exposing methods globally on `Module` and `main` 9 | config.disable_monkey_patching! 10 | 11 | config.expect_with :rspec do |c| 12 | c.syntax = :expect 13 | end 14 | end 15 | --------------------------------------------------------------------------------