Setting Up a RADV and NAT64 Router with Packer and VirtualBox

Setting Up a RADV and NAT64 Router with Packer and VirtualBox

In this post, I'll go through setting up the core functionalities to make the bare bones VM fully operational as an IPv6 router, including configuring RADV and NAT64. After that, I'll secure the VM, power it down, and run some final checks to ensure everything is working as expected. Once validated the build phase will be complete and will be setting up for the final step, packaging it for deployment.

Prevent Premature OVF Export

To ensure that the VM remains available for further customisation before being wrapped into an OVF, I adjusted the Packer HCL file. This change allows me to apply additional configurations directly within the guest environment. By keeping the VM in this state, I have more flexibility to refine the build process before creating the final image.

Here's the adjustment I made to the HCL file:

  skip_export        = true
  keep_registered    = true

The skip_export setting prevents Packer from automatically converting the VM into an OVF, while keep_registered keeps the VM registered in VirtualBox, making it accessible for further configuration. This approach allows me to perform additional setup steps before finalising the image.

I will need to run lots of additional commnads both within the guest VM and on the host to configure the vm before it is ready

Iterative Approach contiues

Before getting into the technical details, I want to reiterate my method of learning and my approach: I’m an IT operations professional and a DBA, not a developer. The script may appear complicated, but it wasn’t crafted in a vacuum. Following the same iterative approach as in my previous posts, I learned by experimentation. This involved testing commands directly in the VM, adding them to the script, booting the VM to see the results, and identifying any issues that arose before repeating the process.

Modifying the Installation Section

Following the initial setup adjustments, I turned my attention to the installation of necessary packages. It became clear that I needed to rearrange the sequence of commands in my provisioning script within the packer file. Specifically, I realised that the security-related configurations, such as fail2ban and user management, should be moved to the very end of the process. This adjustment allows me to interact with the machine during its configuration without being locked out due to security restrictions.

Here’s how I updated the provisioning section of my HCL file:

provisioner "shell" { 
   inline = [
        ### Common commands
        "set -x",    # Log output to Packer debug for visibility
        "dnf config-manager --set-enabled crb", # Enable EPEL (prerequisite)
        "dnf install -y epel-release", # Install EPEL (extra packages for Enterprise Linux)
        "dnf update -y",               # Install any updates
        "dnf install -y vim wget curl fail2ban bind-utils",  # Install essential packages

        ### Install VirtualBox Guest Additions
        "dnf install dkms kernel-devel kernel-headers gcc make bzip2 perl elfutils-libelf-devel -y",
        "mkdir /mnt/cdrom",
        "mount VBoxGuestAdditions.iso /mnt/cdrom/",
        "cd /mnt/cdrom",
        "./VBoxLinuxAdditions.run",

        ### Install packages for NAT64 and RADV
        "dnf install nftables radvd -y",
        "dnf clean all", # Clean up package manager cache
    ]
}

In this updated block, I’ve included commands to enable the EPEL repository, install essential packages, and set up VirtualBox Guest Additions. Additionally, I've prepared the environment for NAT64 and RADV installation.

Integrating VBoxManage Commands

With the provisioning section updated to include the necessary packages and configurations, the next step was to integrate the VBoxManage commands to handle the VirtualBox VM settings effectively. This tool allows for precise control over the VM's configuration, which is crucial for setting up the environment tailored to my requirements.

First, I ensured that the VM settings were adjusted to fit the needs of a router. This included allocating sufficient video memory and changing the graphics controller to avoid any issues with the Guest Additions. Here’s how I configured those settings:

# Modify the VM settings
Write-Host "Modifying the VM settings..."
vboxmanage modifyvm $vmaname --vram 128 --graphicscontroller VMSVGA # Change graphics driver and memory

Next, I set up the network interfaces to prepare the VM for its role as a router. I configured the first network interface as a NAT network, which allows the VM to communicate with external networks, while the second interface was set up as an internal network for communication between virtual machines. The configurations looked like this:

# Configure RADV Router NIC
VBoxManage modifyvm $vmaname --nic1 natnetwork --nat-network1 IPv6TestLabNAT # Named NAT network
VBoxManage modifyvm $vmaname --nic2 intnet --intnet2 IPv6TestLabINT # Named internal network

These configurations ensure that the VM has the proper network interfaces set up before it starts. Once these adjustments were in place, I was ready to start the VM:

# Start the VM
Write-Host "Starting the VM..."
VBoxManage startvm $vmaname

By carefully orchestrating these commands, I set the foundation for the VM’s network configurations and ensured it was ready for further modifications once it booted up.

Initializing the VM and Validating Network Configuration

After starting the VM, it was essential to ensure that the system had fully booted and that the necessary services, particularly NetworkManager, were up and running. To achieve this, I employed VBoxManage guestcontrol to check for the active processes within the guest OS. Here’s how I monitored the boot process:

# Wait for the VM to boot up
Write-Host "Waiting for the VM to boot..."
do {
    $processCheck = VBoxManage guestcontrol $vmaname --username $vmUser --password $vmPassword run --exe /usr/bin/bash -- -c `"pgrep NetworkManager`"

    Start-Sleep -Seconds 5  # Wait for a few seconds before checking again
} while (-not $processCheck)

Once I confirmed that NetworkManager was running, I proceeded to reconfigure the network settings for the first network interface. This involved removing any static IP assignments and switching to DHCP, which is crucial for allowing the VM to automatically obtain its IP address from the NAT network.

Here’s the script I used to make these adjustments:

# Run commands to reconfigure NIC 1
Write-Host "Changing Network settings"
$commandstorun = @"
# Log output for debugging
set -x
# Check if NetworkManager has started
systemctl status NetworkManager
# Delete default route
ip route delete default
# Bring down NIC #1
nmcli connection down enp0s3
# Delete NIC #1
nmcli connection delete enp0s3
# Add NIC #1 with new name and DHCP address
nmcli connection add type ethernet ifname enp0s3 con-name enp0s3 ipv4.method auto
# Bring NIC up
nmcli connection up enp0s3
"@ -replace "`r`n", "`n" # Format for Linux line endings

VBoxManage guestcontrol $vmaname --username $vmUser --password $vmPassword run --exe /usr/bin/bash -- -c `"$commandstorun`" 
Write-Host "NIC 1 networking complete"

Following the reconfiguration of NIC 1, I needed to set up the second network interface with a static IPv6 address. This is particularly important for a router setup, where predictable addressing is essential for routing functionality.

I executed the following commands to reconfigure NIC 2:

# Reconfigure NIC 2
Start-Sleep -Seconds 30 # Wait to ensure NIC 1 is fully up
$commandstorun = @"
# Bring down NIC #2
nmcli connection down Wired\ connection\ 1
# Delete NIC #2
nmcli connection delete Wired\ connection\ 1
# Add a new connection with the same name
nmcli connection add type ethernet ifname enp0s8 con-name Internalnetwork ipv4.method disabled
# Set the static IPv6 address
nmcli connection modify Internalnetwork ipv6.addresses fd59:9442:fcd1::1/48
sleep 10s
nmcli connection modify Internalnetwork ipv6.method manual
# Bring the connection up
nmcli connection up Internalnetwork &
"@ -replace "`r`n", "`n" # Format for Linux line endings

VBoxManage guestcontrol $vmaname --username $vmUser --password $vmPassword run --exe /usr/bin/bash -- -c `"$commandstorun`" 
Write-Host "NIC 2 networking complete"

After executing these commands, I needed to validate that the networking configurations were correctly applied. I ran a series of checks to verify the IP settings and test connectivity over the external network:

# Validate networking configuration
Write-Host "Checking IP settings and connectivity over the external network"
$commandstorun = @"
# Log output for debugging
set -x
# Check IP settings
ip addr
# Check default route
ip route
# Check access to the internet
ping -c 2 8.8.8.8
# Check DNS resolution
dig +trace google.com
"@ -replace "`r`n", "`n" # Format for Linux line endings

VBoxManage guestcontrol $vmaname --username $vmUser --password $vmPassword run --exe /usr/bin/bash -- -c `"$commandstorun`" 
Write-Host "Networking config changes validated"

With these commands, I confirmed that the VM's network interfaces were properly configured and that it could communicate with external networks. This validation step is crucial for ensuring the router’s functionality in the intended environment.

RADV Configuration

I configured the Router Advertisement Daemon (RADV) to enable IPv6 on the network. First, I backed up the existing configuration files to protect against any data loss. The new setup allowed the router to broadcast essential information, including the local IPv6 prefix, while indicating that clients should use DHCP for address assignments. I enabled IP forwarding for both IPv4 and IPv6 to facilitate seamless data transmission between networks. I established firewall rules to manage traffic effectively and enhance security. Finally, I restarted the RADV service and checked its status to confirm that everything was functioning correctly.

$commandstorun = @"
set -x  # Enable debug mode to display each command before execution

# Backup existing radvd configuration if it exists
if [ -f /etc/radvd.conf ]; then
    cp /etc/radvd.conf /etc/radvd.conf.bak  # Create a backup of the existing radvd configuration file
fi

# Backup existing sysctl.conf configuration if it exists
if [ -f /etc/sysctl.conf ]; then
    cp /etc/sysctl.conf /etc/sysctl.conf.bak  # Create a backup of the existing sysctl configuration file
fi

# Create new radvd configuration file
echo '# Configuration for Router Advertisements
interface enp0s8 {
    AdvSendAdvert on;               # Enable sending Router Advertisements
    AdvManagedFlag on;              # Indicate that clients should use DHCPv6 for address assignment
    AdvOtherConfigFlag on;          # Indicate that clients can use DHCPv6 for additional configuration
    MinRtrAdvInterval 30;           # Minimum Router Advertisement Interval (in seconds)
    MaxRtrAdvInterval 100;          # Maximum Router Advertisement Interval (in seconds)
    prefix fd59:9442:fcd1:1::/64 {   # Define the IPv6 prefix for the local network
        AdvOnLink on;               # Indicate that this prefix is on-link, allowing direct communication
        AdvAutonomous off;          # Disable autonomous address configuration (only use DHCPv6)
    };
};
' | tee /etc/radvd.conf  # Write the new configuration to radvd.conf

# Enable IP forwarding for IPv4 and IPv6
echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.conf  # Allow IPv4 forwarding in sysctl
echo 'net.ipv6.conf.all.forwarding = 1' | sudo tee -a /etc/sysctl.conf  # Allow IPv6 forwarding in sysctl

# Apply sysctl settings to enable the forwarding
sudo sysctl -p  # Reload the sysctl settings to apply changes immediately

# Setup firewall rules for IPv4 forwarding
sudo iptables -A FORWARD -i enp0s3 -o enp0s8 -j ACCEPT  # Allow forwarding from external (enp0s3) to internal (enp0s8)
sudo iptables -A FORWARD -i enp0s8 -o enp0s3 -j ACCEPT  # Allow forwarding from internal (enp0s8) to external (enp0s3)
sudo iptables -t nat -A POSTROUTING -o enp0s3 -j MASQUERADE  # NAT rule to change source IP for outgoing packets

# Setup firewall rules for IPv6 forwarding
sudo ip6tables -A FORWARD -i enp0s3 -o enp0s8 -j ACCEPT  # Allow IPv6 forwarding from external (enp0s3) to internal (enp0s8)
sudo ip6tables -A FORWARD -i enp0s8 -o enp0s3 -j ACCEPT  # Allow IPv6 forwarding from internal (enp0s8) to external (enp0s3)

# Restart the radvd service to apply new configuration
sudo systemctl restart radvd  # Restart the radvd service to ensure the new configuration is in effect

# Check the status of the radvd service
systemctl status radvd  # Display the status of the radvd service to verify it's running correctly

# Check if Router Advertisements are being sent
timeout 30 radvdump -d 4  # Run radvdump for 30 seconds to verify that RAs are being sent

# Set radvd to start on boot
sudo systemctl enable radvd  # Ensure the radvd service starts automatically on system boot

"@ -replace "`r`n", "`n"  # Ensure Unix-style line endings
$output = $null
do {
    Start-Sleep -Milliseconds 200 
    VBoxManage guestcontrol $vmaname --username $vmUser --password $vmPassword run --exe /usr/bin/bash -- -c "$commandstorun" 2>&1 | Tee-Object -Variable Output
} while (-not $Output)

Write-Host "RADV configuration complete" -ForegroundColor green

NAT64 Setup

I set up NAT64 to enable communication between IPv6-only clients and IPv4 services. I began by installing the necessary dependencies and downloading the Jool software, which is crucial for NAT64 functionality. After compiling and installing the kernel module with DKMS, I configured the user-space tools to ensure the NAT64 gateway was operational, allowing seamless access for IPv6 clients to IPv4 resources. Finally, I created a systemd service to manage the Jool startup script, ensuring that the NAT64 setup would remain resilient and persistent across reboots.

$commandstorun = @'
set -x  # Enable debug mode to display each command before execution

# Install dependencies 
dnf -y install automake autoconf libtool libnl3-devel iptables-devel pkgconfig

# Create a temporary directory
mkdir -p /tmp/jool_install
cd /tmp/jool_install

# Download Jool
rm -f master.zip
wget https://github.com/NICMx/Jool/archive/refs/heads/master.zip

# Unzip the file
rm -rf Jool-main
unzip master.zip

# Install the kernel module using DKMS
dkms install ./Jool-main

# Change directory to Jool source
cd ./Jool-main

# Build and install the user-space tools
./autogen.sh
./configure
make
make install

# Cleanup (optional)
cd ..
# rm -rf Jool-main
# rm -f master.zip
# rm -rf /tmp/jool_install
'@ -replace "`r`n", "`n"  # Ensure Unix-style line endings
$output = $null
do {
    Start-Sleep -Milliseconds 200
    VBoxManage guestcontrol $vmaname --username $vmUser --password $vmPassword run --exe /usr/bin/bash -- -c "$commandstorun" 2>&1 | Tee-Object -Variable Output
} while (-not $Output)

Write-Host "Configuring Jool Service" -ForegroundColor cyan 
$commandstorun = @"
set -x  # Enable debug mode to display each command before execution

# Create the startup script for Jool
cat << 'EOF' > /usr/local/bin/jool_startup.sh
#!/bin/bash

# Disable Offloading Features for Network Interfaces
ethtool --offload enp0s8 tso off
ethtool --offload enp0s8 gso off
ethtool --offload enp0s8 gro off

ethtool --offload enp0s3 tso off
ethtool --offload enp0s3 gso off
ethtool --offload enp0s3 gro off

# Load the Jool module
modprobe jool
sleep 5  # Wait for the module to load properly

# Add Jool instance
/usr/local/bin/jool instance add "default" --netfilter --pool6 64:ff9b::/96
EOF

# Make the script executable
chmod +x /usr/local/bin/jool_startup.sh

# Create the systemd service file for Jool
cat << 'EOF' > /etc/systemd/system/jool.service
[Unit]
Description=Jool Startup Service
After=network.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/jool_startup.sh
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
EOF

# Enable the Jool service to run at boot
systemctl enable jool.service

# Start the Jool service immediately (optional)
systemctl start jool.service

# Check the status of the service (optional)
systemctl status jool.service
"@ -replace "`r`n", "`n"  # Ensure Unix-style line endings

# Execute the command in the guest VM
$output = $null
do {
    Start-Sleep -Milliseconds 200
    VBoxManage guestcontrol $vmaname --username $vmUser --password $vmPassword run --exe /usr/bin/bash -- -c "$commandstorun" 2>&1 | Tee-Object -Variable Output
} while (-not $Output)

Securing and Powering Down the VM

Finally, I focused on securing the virtual machine (VM) by implementing essential security measures. I started by establishing a robust user management system, which involved creating a non-root user and adjusting root access permissions. By initiating the Fail2Ban service, I added an extra layer of protection against potential brute-force attacks. I also disabled root login via SSH to reinforce our security posture. After making these changes, I gracefully powered down the VM, ensuring that all configurations were saved and ready for the next session.

$commandstorun = @"
set -x  # Enable debugging to print each command before executing it
# Set the root password using a hashed password
echo 'root:$hashedpassword' | chpasswd -e 
# Create a non-root user, set their password, add them to the 'wheel' group for sudo access, 
# and force a password change on first login using the same hashed password
adduser $normalusername && \
echo '$normalusername`:$hashedpassword' | sudo chpasswd -e && \
sudo usermod -aG wheel $normalusername && \
sudo chage -d 0 $normalusername 
# Start the fail2ban service to prevent brute force attacks
sudo systemctl start fail2ban 
# Enable the fail2ban service to start on boot
sudo systemctl enable fail2ban 
# Disable root login via SSH for security
sed -i 's/^PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config  
"@ -replace "`r`n", "`n"  # Ensure Unix-style line endings

$output = $null
do {
    Start-Sleep -Milliseconds 200 
    VBoxManage guestcontrol $vmaname --username $vmUser --password $vmPassword run --exe /usr/bin/bash -- -c "$commandstorun" 2>&1 | Tee-Object -Variable Output
} while (-not $Output)

VBoxManage controlvm $vmaname acpipowerbutton

Crucial Gotchas

  1. Service Dependency: A vital point to note is that network changes must occur after confirming that the VM has fully booted and that NetworkManager is operational. Attempting to change network settings before this may result in errors or unexpected behavior, especially if the services are not yet active.

  2. Guest Additions Requirements: For VirtualBox Guest Additions to function properly, ensure sufficient video RAM (at least 128 MB) and the correct graphics card settings (e.g., VMSVGA for Linux VMs). Insufficient video memory can cause crashes or instability, and misconfigured graphics settings may lead to graphical glitches or a lack of enhanced features.

  3. Timing for NIC Configuration: When bringing up NIC 2, it is essential to allow adequate time for NIC 1 to stabilize. The Start-Sleep -Seconds 30 command was intentionally added after configuring NIC 1 to prevent race conditions, ensuring that NIC 2 can successfully acquire its settings without interference from the state of NIC 1.

  4. Line Endings: Given that I am working on a Windows host and executing commands on a Linux VM, it was crucial to ensure that the commands were formatted with Unix-style line endings (LF). I used the -replace "rn", "n"` method to convert any Windows-style line endings before executing the commands in the VM, thereby preventing unexpected behavior or command failures.

  5. Managing Intermittent Guest Control: To address the occasional unreliability of guest control in virtualization environments, I implemented a simple loop mechanism in my script. This allows continual attempts to execute commands until successful, effectively managing intermittent issues.

    Here’s the loop I used:

    $output = $null
    do {
       Start-Sleep -Milliseconds 200 
       VBoxManage guestcontrol $vmaname --username $vmUser --password $vmPassword run --exe /usr/bin/bash -- -c "$commandstorun" 2>&1 | Tee-Object -Variable Output
    } while (-not $Output)
    

    In this example, the command retries every 200 milliseconds until it receives valid output, ensuring smoother automation and effective monitoring of guest control.

Validating the Build Process

After firing up the virtual machine manually following the completion of the process, I ensured that all configurations were running as expected. While I couldn't fully test the functionality until another VM was set up, I confirmed that each service was operational and ready for future testing.

By running and refining each section of the script, I successfully executed the entire process from start to finish. The combination of Packer, PowerShell, and VBoxManage with guest control has allowed me to build a VM purely using code. This approach has proven effective, although the overall process is a bit clunky; it’s essentially the same as configuring the machine on the command line, but the scripted setup handles the whole process from nothing to a working VM. It’s important to note that Packer cannot handle most of the build; PowerShell is necessary to control the VM and fill in the gaps to achieve the desired outcomes.

Notes on Other Options: Scripting vs. Ansible

I came across Ansible while reading up on deployments. I’ve heard it has some great capabilities, but I think it’s more effective to script things out first and then consider Ansible later, especially since this machine is probably one of the most complex I’m working on. While templates can be useful, I find scripts more relatable and effective for what I need right now. I plan to explore Ansible later for smaller, simpler machines and might revisit this build as an advanced step in the project.

Conclusion and Next Steps

With the RADV Router now up and running, the next step will be exporting the virtual machine as an OVA and a Vagrant box. In the following post, I’ll detail how to perform these exports, allowing for rapid deployment of the configuration.

All Files related to this post

Comments

Popular posts from this blog

Installing and Using Git on Windows

Learning How to Deploy a AlmaLinux VM With Packer (Part 1)

Welcome and Introduction