Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ cisco*.bin
*.vmdk
*.iso
*cidfile
built-image-sha*

.DS_Store
*/.DS_Store
Expand Down
20 changes: 20 additions & 0 deletions genuscreen/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
VENDOR=Genua
NAME=genuscreen
IMAGE_FORMAT=iso
IMAGE_GLOB=*.iso

# match versions like:
# genuscreen-8.0.iso
# genuscreen-1.0.0.iso
# genuscreen-2.1.3.iso
VERSION=$(shell echo $(IMAGE) | sed -E 's/.*genuscreen-([0-9]+\.[0-9]+(\.[0-9]+)?)\.iso/\1/')

# Check if ISO files exist
ISO_FILES=$(wildcard $(IMAGE_GLOB))
ifeq ($(ISO_FILES),)
$(error No ISO files found! Please place genuscreen ISO files (e.g., genuscreen-8.0.iso) in this directory)
endif

-include ../makefile-sanity.include
-include ../makefile.include
-include ../makefile-install.include
98 changes: 98 additions & 0 deletions genuscreen/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# vrnetlab / Genua genuscreen
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @Muddyblack
do you plan to add the relevant PR for containerlab? It is fine if you don't, someone can take this up as well

Copy link
Author

@Muddyblack Muddyblack Aug 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do you mean has something changed in the logic?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

without adding the kind support in clab, it won't work

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aye I think i used linux-kind back then. But I think it would be a good exercise to start with golang for me. I will try it but I can't tell when I will take my time for it to also test the new kind then 😅

Is it just creating new file in nodes folder and and add to register.go?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

M, you are raising a good point about the usage of linux_vm kind. I would prefer it use it given the low popularity of the platform


This is the vrnetlab docker image for Genua genuscreen firewall appliances.

## Building the docker image

Download the genuscreen ISO image and place it in this directory. The expected naming format is:
- `genuscreen-8.0.iso`
- `genuscreen-1.0.0.iso`
- `genuscreen-2.1.3.iso`

After placing the ISO file, run `make` to build the docker image. The resulting image will be called `vrnetlab/genua_genuscreen:X.Y.Z` where X.Y.Z matches the version from the ISO filename.

## Installation Process

Genuscreen requires an initial installation from ISO to create the qcow2 disk image. The build process automatically handles this:

1. The ISO is mounted as a CD-ROM device
2. An empty 20GB qcow2 disk is created for installation
3. The automated installation configures:
- Hostname (from `--hostname` parameter)
- Network interface (eth0 with IP 10.0.0.15/24)
- Default gateway (10.0.0.2)
- Administrative password (from `--password` parameter)
- SSH daemon (enabled)
- Web-GUI access restrictions
- Admin ACL network settings

## Usage

### With containerlab

```yaml
name: genuscreen-lab
topology:
nodes:
gw1:
kind: vr-genuscreen
image: vrnetlab/genua_genuscreen:8.0
```

### Manual docker run

```bash
docker run -d --privileged --name my-genuscreen vrnetlab/genua_genuscreen:8.0
```

## Configuration

The genuscreen image supports the following parameters:

- `--hostname`: Router hostname (default: `vr-genuscreen`)
- `--username`: Login username (default: `root`)
- `--password`: Login password (default: `VR-netlab9`)
- `--connection-mode`: Datapath connection mode (default: `tc`)
- `--install`: Run installation mode (used during build process)
- `--trace`: Enable trace level logging

## Interface mapping

The genuscreen VM exposes 8 network interfaces using virtio-net-pci:
- eth0: Management interface (configured during installation)
- eth1-eth7: Additional data interfaces

## Network Configuration

During installation, the system is configured with:
- **Management IP**: 10.0.0.15/24
- **Default Gateway**: 10.0.0.2
- **DNS**: Default system DNS
- **SSH**: Enabled on port 22
- **Admin ACL**: 192.168.1.0/24 (Only when web-gui restrictions is set true)

## Tested versions

The image has been developed and tested with:
- genuscreen-8.0.iso

## Troubleshooting

### Installation Issues

If installation fails:
1. Check that the ISO file is properly named and in the correct directory
2. Ensure sufficient disk space (>25GB)
3. Verify the ISO image is not corrupted
4. Enable trace logging with `--trace` for detailed output

### SSH Access

Default SSH credentials:
- Username: `root`
- Password: `VR-netlab9`
- Port: 22

## License

This vrnetlab image is provided under the same license terms as the main vrnetlab project. The actual genuscreen software requires appropriate licensing from [Genua GmbH](https://www.genua.de/).
7 changes: 7 additions & 0 deletions genuscreen/docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
FROM ghcr.io/srl-labs/vrnetlab-base:0.2.1

ARG IMAGE
COPY $IMAGE* /
COPY *.py /

EXPOSE 22 161/udp 830 5000 10000-10099
257 changes: 257 additions & 0 deletions genuscreen/docker/launch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
#!/usr/bin/env -S uv run

import datetime
import logging
import os
import re
import signal
import sys
import time

import vrnetlab


def handle_SIGCHLD(signal, frame):
os.waitpid(-1, os.WNOHANG)


def handle_SIGTERM(signal, frame):
sys.exit(0)


signal.signal(signal.SIGINT, handle_SIGTERM)
signal.signal(signal.SIGTERM, handle_SIGTERM)
signal.signal(signal.SIGCHLD, handle_SIGCHLD)

TRACE_LEVEL_NUM = 9
logging.addLevelName(TRACE_LEVEL_NUM, "TRACE")


def trace(self, message, *args, **kws):
# Yes, logger takes its '*args' as 'args'.
if self.isEnabledFor(TRACE_LEVEL_NUM):
self._log(TRACE_LEVEL_NUM, message, args, **kws)


logging.Logger.trace = trace


class GENUSCREEN_vm(vrnetlab.VM):
def __init__(self, hostname, username, password, conn_mode, install_mode=False):
disk_image = None
iso_image = None

for e in os.listdir("/"):
if re.search(r"\.qcow2$", e):
disk_image = "/" + e
elif re.search(r"\.iso$", e):
iso_image = "/" + e

self.install_mode = install_mode

if self.install_mode and iso_image:
# Create empty disk for installation
disk_image = "/genuscreen-disk.qcow2"
if not os.path.exists(disk_image):
vrnetlab.run_command([
"qemu-img", "create", "-f", "qcow2", disk_image, "20G"
])
self.iso_image = iso_image
elif not disk_image:
raise ValueError("No disk image found")

super(GENUSCREEN_vm, self).__init__(
username, password, disk_image=disk_image, ram=4096, smp="2"
)

self.hostname = hostname
self.conn_mode = conn_mode
self.num_nics = 8
self.nic_type = "virtio-net-pci"

# Add entropy sources for better performance
self.qemu_args.extend([
"-object", "rng-random,filename=/dev/urandom,id=rng0",
"-device", "virtio-rng-pci,rng=rng0,max-bytes=1024,period=1000"
])

if self.install_mode and hasattr(self, 'iso_image'):
self.qemu_args.extend([
"-boot", "order=cd",
"-cdrom", self.iso_image
])

def bootstrap_spin(self):
"""This function should be called periodically to do work."""

if self.spins > 1200:
# too many spins with no result -> give up
self.stop()
self.start()
return

if self.install_mode:
return self._handle_installation()
else:
return self._handle_normal_boot()

def _handle_installation(self):
"""Handle the installation process"""
installation_prompts = [
b"proceed",
b"32-bit appliance",
b"Keyboard mapping",
b"Fully Qualified Domain Name",
b"Which interface",
b"Address?",
b"Netmask length",
b"Media",
b"Default gateway",
b"New password:",
b"Retype new password:",
b"Enable SSH daemon",
b"Restrict access to Web-GUI",
b"Admin-ACL network",
b"Admin-ACL netmask length",
b"Save configuration to disk",
b"wait for more?",
b"login:"
]

installation_responses = [
"yes", # proceed
"no", # 32-bit appliance
"de", # Keyboard mapping
self.hostname, # FQDN
"", # Which interface (default)
"10.0.0.15", # Address
"24", # Netmask length
"", # Media (default)
"10.0.0.2", # Default gateway
self.password, # New password
self.password, # Retype password
"yes", # Enable SSH
"no", # Restrict Web-GUI
"192.168.1.0", # Admin-ACL network
"24", # Admin-ACL netmask length
"yes", # Save configuration
"no", # wait for more
None # login prompt - installation complete
]

(ridx, match, res) = self.tn.expect(installation_prompts, 1)

if match:
if ridx == len(installation_prompts) - 1: # login prompt - installation complete
self.logger.info("Installation completed successfully")
install_time = datetime.datetime.now() - self.start_time
self.logger.info("Install complete in: %s", install_time)
self.running = True
return
elif ridx < len(installation_responses) and installation_responses[ridx] is not None:
self.wait_write(installation_responses[ridx], wait=None)

# no match, if we saw some output it's probably still installing
if res != b"":
self.logger.trace("INSTALL OUTPUT: %s", res.decode())
self.spins = 0

self.spins += 1

def _handle_normal_boot(self):
"""Handle normal boot process"""
(ridx, match, res) = self.tn.expect([b"login:"], 1)

if match and ridx == 0:
self.logger.info("Genuscreen boot completed")
self.wait_write(self.username, wait=None)
self.wait_write(self.password, wait="Password:")

# close telnet connection
self.tn.close()
startup_time = datetime.datetime.now() - self.start_time
self.logger.info("Startup complete in: %s", startup_time)
self.running = True
return

# no match, if we saw some output from the router it's probably
# booting, so let's give it some more time
if res != b"":
self.logger.trace("BOOT OUTPUT: %s", res.decode())
# reset spins if we saw some output
self.spins = 0

self.spins += 1


class GENUSCREEN(vrnetlab.VR):
def __init__(self, hostname, username, password, conn_mode):
super(GENUSCREEN, self).__init__(username, password)
self.vms = [GENUSCREEN_vm(hostname, username, password, conn_mode)]


class GENUSCREEN_installer(GENUSCREEN):
"""GENUSCREEN installer

Will start Genuscreen with ISO mounted and perform installation
to create the final QCOW2 image for subsequent boots.
"""

def __init__(self, hostname, username, password, conn_mode):
super(GENUSCREEN, self).__init__(username, password)
self.vms = [
GENUSCREEN_vm(hostname, username, password, conn_mode, install_mode=True)
]

def install(self):
"""Run the installation process"""
self.logger.info("Installing Genuscreen")
genuscreen = self.vms[0]
while not genuscreen.running:
genuscreen.work()

time.sleep(10)
genuscreen.stop()
self.logger.info("Installation complete")


if __name__ == "__main__":
import argparse

parser = argparse.ArgumentParser(description="")
parser.add_argument(
"--trace", action="store_true", help="enable trace level logging"
)
parser.add_argument("--hostname", default="vr-genuscreen", help="Router hostname")
parser.add_argument("--username", default="root", help="Username")
parser.add_argument("--password", default="VR-netlab9", help="Password")
parser.add_argument(
"--connection-mode",
default="tc",
help="Connection mode to use in the datapath",
)
parser.add_argument("--install", action="store_true", help="Install Genuscreen")

args = parser.parse_args()

LOG_FORMAT = "%(asctime)s: %(module)-10s %(levelname)-8s %(message)s"
logging.basicConfig(format=LOG_FORMAT)
logger = logging.getLogger()

logger.setLevel(logging.DEBUG)
if args.trace:
logger.setLevel(1)

logger.debug(f"Environment variables: {os.environ}")
vrnetlab.boot_delay()

if args.install:
vr = GENUSCREEN_installer(
args.hostname, args.username, args.password, args.connection_mode
)
vr.install()
else:
vr = GENUSCREEN(
args.hostname, args.username, args.password, args.connection_mode
)
vr.start()