diff --git a/.gitignore b/.gitignore index e7461652..2a3a107e 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ cisco*.bin *.vmdk *.iso *cidfile +built-image-sha* .DS_Store */.DS_Store diff --git a/genuscreen/Makefile b/genuscreen/Makefile new file mode 100644 index 00000000..9230bbe6 --- /dev/null +++ b/genuscreen/Makefile @@ -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 \ No newline at end of file diff --git a/genuscreen/README.md b/genuscreen/README.md new file mode 100644 index 00000000..addf149d --- /dev/null +++ b/genuscreen/README.md @@ -0,0 +1,98 @@ +# vrnetlab / Genua genuscreen + +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/). diff --git a/genuscreen/docker/Dockerfile b/genuscreen/docker/Dockerfile new file mode 100644 index 00000000..efa15c4f --- /dev/null +++ b/genuscreen/docker/Dockerfile @@ -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 \ No newline at end of file diff --git a/genuscreen/docker/launch.py b/genuscreen/docker/launch.py new file mode 100755 index 00000000..d391a008 --- /dev/null +++ b/genuscreen/docker/launch.py @@ -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() \ No newline at end of file