├── index.html ├── README.md ├── entrypoint.sh ├── LICENSE ├── .github └── workflows │ └── cicd.yml ├── .bashrc ├── install-bngblaster.sh ├── if-wait.sh ├── nginx.conf ├── Dockerfile └── gotty-service /index.html: -------------------------------------------------------------------------------- 1 | based on WBITT Network MultiTool (https://github.com/wbitt/Network-MultiTool) 2 | 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Network Multitool container image 2 | 3 | This container image is often used in containerlabs to emulate a client. The image contains some network-related tools to assist in testing networks and network applications. 4 | 5 | To connect to this image users can use 6 | 7 | * `ssh admin@`. Password `multit00l`. 8 | * or `docker exec -i -t bash` 9 | 10 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | sh /docker/if-wait.sh 4 | 5 | ifup -a 6 | ######### 7 | # create directories for ssh host keys 8 | mkdir -p /etc/dropbear 9 | # create the host keys 10 | dropbearkey -t rsa -f /etc/dropbear/dropbear_rsa_host_key 2048 11 | dropbearkey -t ecdsa -f /etc/dropbear/dropbear_ecdsa_host_key 512 12 | dropbearkey -t ed25519 -f /etc/dropbear/dropbear_ed25519_host_key 13 | # and start the process 14 | /usr/sbin/dropbear -j -k -E -F & 15 | ######### 16 | # Execute the command specified as CMD in Dockerfile: 17 | exec "$@" 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Praqma 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/cicd.yml: -------------------------------------------------------------------------------- 1 | name: Build and Push Docker Image 2 | 3 | "on": 4 | push: 5 | branches: ["main"] 6 | tags: 7 | - "v*" 8 | pull_request: 9 | 10 | jobs: 11 | docker: 12 | runs-on: ubuntu-latest 13 | permissions: 14 | packages: write 15 | steps: 16 | - name: Set up QEMU 17 | uses: docker/setup-qemu-action@v3 18 | 19 | - name: Set up Docker Buildx 20 | uses: docker/setup-buildx-action@v3 21 | 22 | - name: Docker meta 23 | id: meta 24 | uses: docker/metadata-action@v5 25 | with: 26 | images: | 27 | ghcr.io/${{ github.repository }} 28 | tags: | 29 | type=ref,event=tag 30 | type=ref,event=branch 31 | type=ref,event=pr 32 | # git short commit 33 | type=sha 34 | 35 | - name: Login to GitHub Container Registry 36 | uses: docker/login-action@v3 37 | with: 38 | registry: ghcr.io 39 | username: ${{ github.repository_owner }} 40 | password: ${{ secrets.GITHUB_TOKEN }} 41 | 42 | - name: Build and push 43 | uses: docker/build-push-action@v6 44 | with: 45 | push: true 46 | platforms: linux/amd64,linux/arm64 47 | tags: ${{ steps.meta.outputs.tags }} 48 | -------------------------------------------------------------------------------- /.bashrc: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # lovingly copied from @loudambiance 4 | # https://gist.github.com/loudambiance/a41b42a4295bce6e7304 5 | 6 | WHITE='\[\033[1;37m\]' 7 | LIGHTRED='\[\033[1;31m\]' 8 | LIGHTGREEN='\[\033[1;32m\]' 9 | LIGHTBLUE='\[\033[1;34m\]' 10 | DEFAULT='\[\033[0m\]' 11 | 12 | cLINES=$WHITE #Lines and Arrow 13 | cBRACKETS=$WHITE # Brackets around each data item 14 | cERROR=$LIGHTRED # Error block when previous command did not return 0 15 | cSUCCESS=$LIGHTGREEN # When last command ran successfully and return 0 16 | cHST=$LIGHTGREEN # Color of hostname 17 | cPWD=$LIGHTBLUE # Color of current directory 18 | cCMD=$DEFAULT # Color of the command you type 19 | 20 | function promptcmd() 21 | { 22 | PREVRET=$? 23 | 24 | # new line to clear space from previous command 25 | PS1="\n" 26 | 27 | # prev cmd error 28 | if [ $PREVRET -ne 0 ] ; then 29 | PS1="${PS1}${cBRACKETS}[${cERROR}x${cBRACKETS}]${cLINES}\342\224\200" 30 | else 31 | PS1="${PS1}${cBRACKETS}[${cSUCCESS}*${cBRACKETS}]${cLINES}\342\224\200" 32 | fi 33 | 34 | # user 35 | PS1="${PS1}${cBRACKETS}[${cHST}\h${cBRACKETS}]${cLINES}\342\224\200" 36 | 37 | # dir 38 | PS1="${PS1}[${cPWD}\w${cBRACKETS}]" 39 | 40 | # second line 41 | PS1="${PS1}\n${cLINES}\342\224\224\342\224\200\342\224\200> ${cCMD}" 42 | } 43 | 44 | function load_prompt () { 45 | PROMPT_COMMAND=promptcmd 46 | 47 | export PS1 PROMPT_COMMAND 48 | } 49 | 50 | load_prompt -------------------------------------------------------------------------------- /install-bngblaster.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | LIBDICT_VERSION="1.0.4" 3 | BNGBLASTER_VERSION="0.9.26" 4 | mkdir /bngblaster 5 | cd /bngblaster 6 | wget https://github.com/rtbrick/libdict/archive/refs/tags/$LIBDICT_VERSION.zip 7 | unzip $LIBDICT_VERSION.zip 8 | mkdir libdict-$LIBDICT_VERSION/build 9 | cd /bngblaster/libdict-$LIBDICT_VERSION/build 10 | cmake .. 11 | make -j16 install 12 | cd /bngblaster 13 | wget https://github.com/rtbrick/bngblaster/archive/refs/tags/$BNGBLASTER_VERSION.zip 14 | unzip $BNGBLASTER_VERSION.zip 15 | mkdir bngblaster-$BNGBLASTER_VERSION/build 16 | cd /bngblaster/bngblaster-$BNGBLASTER_VERSION/build 17 | #remove redundant include to avoid preprocessor redirect warning and consequent compilation failure 18 | sed -i '/#include /d' ../code/lwip/contrib/ports/unix/port/netif/sio.c 19 | #typedef for uint to avoid compilation error on alpine musl libc 20 | sed -i '$i typedef unsigned int uint;' ../code/common/src/common.h 21 | # add include to support be32toh and htobe32 on alpine musl libc 22 | sed -i '/#include /i #include ' ../code/common/src/common.h 23 | #replace __time_t with time_t to make it compatible with alpine musl libc 24 | find /bngblaster/bngblaster-0.9.26/code/ -type f \( -name "*.c" -o -name "*.h" \) -exec sed -i 's/\b__time_t\b/time_t/g' {} + 25 | #Don't error on sequence-point errors to allow build to complete on musl libc. Ideally code should be fixed on upstream repo. 26 | sed -i 's/APPEND CMAKE_C_FLAGS "-pthread"/APPEND CMAKE_C_FLAGS "-pthread -Wno-error=sequence-point"/' ../code/bngblaster/CMakeLists.txt 27 | cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo -DBNGBLASTER_VERSION=$BNGBLASTER_VERSION .. 28 | make install -------------------------------------------------------------------------------- /if-wait.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Validate CLAB_INTFS environment variable 4 | REQUIRED_INTFS_NUM=${CLAB_INTFS:-0} 5 | if ! echo "$REQUIRED_INTFS_NUM" | grep -qE '^[0-9]+$' || [ "$REQUIRED_INTFS_NUM" -eq 0 ]; then 6 | echo "Warning: CLAB_INTFS not set or invalid, skipping interface wait" 7 | REQUIRED_INTFS_NUM=0 8 | fi 9 | 10 | SLEEP=0 11 | TIMEOUT=300 # 5 minute timeout 12 | WAIT_TIME=0 13 | 14 | int_calc() { 15 | if [ ! -d "/sys/class/net/" ]; then 16 | echo "Error: /sys/class/net/ not accessible" 17 | AVAIL_INTFS_NUM=0 18 | return 1 19 | fi 20 | 21 | # More comprehensive interface pattern including common container interfaces 22 | AVAIL_INTFS_NUM=$(ls -1 /sys/class/net/ 2>/dev/null | grep -cE '^(eth[1-9]|et[0-9]|ens|eno|enp|e[1-9]|net[0-9])') 23 | return 0 24 | } 25 | 26 | # Only wait for interfaces if CLAB_INTFS is set 27 | if [ "$REQUIRED_INTFS_NUM" -gt 0 ]; then 28 | echo "Waiting for $REQUIRED_INTFS_NUM interfaces to be connected (timeout: ${TIMEOUT}s)" 29 | 30 | while [ "$WAIT_TIME" -lt "$TIMEOUT" ]; do 31 | if ! int_calc; then 32 | echo "Failed to check interfaces, continuing..." 33 | break 34 | fi 35 | 36 | if [ "$AVAIL_INTFS_NUM" -ge "$REQUIRED_INTFS_NUM" ]; then 37 | echo "Found $AVAIL_INTFS_NUM interfaces (required: $REQUIRED_INTFS_NUM)" 38 | break 39 | fi 40 | 41 | echo "Connected $AVAIL_INTFS_NUM interfaces out of $REQUIRED_INTFS_NUM (waited ${WAIT_TIME}s)" 42 | sleep 1 43 | WAIT_TIME=$((WAIT_TIME + 1)) 44 | done 45 | 46 | if [ "$WAIT_TIME" -ge "$TIMEOUT" ]; then 47 | echo "Warning: Timeout reached, proceeding with $AVAIL_INTFS_NUM interfaces" 48 | fi 49 | fi 50 | 51 | if [ "$SLEEP" -ne 0 ]; then 52 | echo "Sleeping $SLEEP seconds before boot" 53 | sleep $SLEEP 54 | fi 55 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | # This file is copied as /etc/nginx/nginx.conf inside the container image. 2 | 3 | user nginx; 4 | worker_processes 1; 5 | 6 | 7 | # Run in foreground by turning off daemon mode. 8 | # Either set it in this file: 9 | # daemon off; 10 | # 11 | # OR 12 | # 13 | # Pass it as an argument on command line. 14 | # CMD ["/usr/sbin/nginx", "-g", "daemon off;"] 15 | 16 | 17 | # Note: The PID directory needs to exist beforehand. 18 | # Also, the pid file is always created as/by user root. 19 | pid /var/run/nginx.pid; 20 | 21 | # Forward error logs to docker log collector, 22 | # by sending it to stderr instead of a log file. 23 | error_log /dev/stderr warn; 24 | 25 | events { 26 | worker_connections 32; 27 | } 28 | 29 | 30 | http { 31 | include /etc/nginx/mime.types; 32 | default_type application/octet-stream; 33 | 34 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 35 | '$status $body_bytes_sent "$http_referer" ' 36 | '"$http_user_agent" "$http_x_forwarded_for"'; 37 | 38 | # Forward access logs to docker log collector, 39 | # by sending it to stdout instead of a log file. 40 | # This directive must be inside the nginx main 'http' section - not outside. 41 | access_log /dev/stdout main; 42 | 43 | sendfile on; 44 | 45 | keepalive_timeout 65; 46 | 47 | #gzip on; 48 | 49 | server { 50 | listen 80; 51 | server_name localhost; 52 | 53 | location / { 54 | root /usr/share/nginx/html; 55 | index index.html index.htm; 56 | } 57 | } 58 | 59 | server { 60 | listen 443 ssl; 61 | server_name localhost; 62 | 63 | location / { 64 | root /usr/share/nginx/html; 65 | index index.html index.htm; 66 | } 67 | 68 | ssl_certificate /certs/server.crt; 69 | ssl_certificate_key /certs/server.key; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.18.6 AS builder 2 | 3 | RUN apk update && apk add --virtual .build-deps \ 4 | build-base gcc wget cmake cunit-dev libpcap-dev \ 5 | ncurses-dev openssl-dev jansson-dev numactl-dev \ 6 | libbsd-dev linux-headers 7 | 8 | #Install bngblaster from source 9 | COPY install-bngblaster.sh / 10 | RUN chmod +x /install-bngblaster.sh 11 | RUN /install-bngblaster.sh 12 | 13 | # Install mcjoin from source 14 | WORKDIR / 15 | RUN wget https://github.com/troglobit/mcjoin/releases/download/v2.12/mcjoin-2.12.tar.gz 16 | RUN tar -xzf mcjoin-2.12.tar.gz 17 | WORKDIR /mcjoin-2.12 18 | RUN ./configure 19 | RUN make -j5 20 | RUN make install-strip 21 | 22 | # Build stage for GoTTY 23 | FROM golang:alpine AS gotty-builder 24 | 25 | # Install git for go install to fetch the repository 26 | RUN apk add --no-cache git 27 | 28 | # Install GoTTY from source 29 | RUN go install github.com/sorenisanerd/gotty@v1.5.0 30 | 31 | # Final image 32 | FROM alpine:3.18.6 33 | 34 | EXPOSE 22 80 443 1180 11443 8080 35 | 36 | # Install some tools in the container and generate self-signed SSL certificates. 37 | # Packages are listed in alphabetical order, for ease of readability and ease of maintenance. 38 | RUN apk update \ 39 | && apk add apache2-utils bash bind-tools busybox-extras bonding curl \ 40 | dnsmasq dropbear ethtool freeradius git go ifupdown-ng iperf iperf3 \ 41 | iproute2 iputils jq lftp mtr mysql-client netcat-openbsd net-snmp-tools \ 42 | net-tools nginx nmap openntpd openssh-client openssl perl-net-telnet \ 43 | postgresql-client procps rsync socat sudo tcpdump tcptraceroute \ 44 | tshark wget envsubst scapy liboping fping bash-completion \ 45 | && mkdir /certs /docker \ 46 | && chmod 700 /certs \ 47 | && openssl req \ 48 | -x509 -newkey rsa:2048 -nodes -days 3650 \ 49 | -keyout /certs/server.key -out /certs/server.crt -subj '/CN=localhost' 50 | 51 | RUN wget https://github.com/osrg/gobgp/releases/download/v3.25.0/gobgp_3.25.0_linux_amd64.tar.gz 52 | RUN mkdir -p /usr/local/gobgp 53 | RUN tar -C /usr/local/gobgp -xzf gobgp_3.25.0_linux_amd64.tar.gz 54 | RUN cp /usr/local/gobgp/gobgp* /usr/bin/ 55 | 56 | # mcjoin binary 57 | COPY --from=builder /usr/local/bin/mcjoin /usr/local/bin/ 58 | # bngblaster binaries and dependencies 59 | RUN apk add ncurses openssl jansson 60 | RUN mkdir /run/lock 61 | COPY --from=builder /usr/sbin/bngblaster-cli /usr/sbin/ 62 | COPY --from=builder /usr/bin/bgpupdate /usr/bin/ 63 | COPY --from=builder /usr/bin/ldpupdate /usr/bin/ 64 | COPY --from=builder /usr/sbin/bngblaster /usr/sbin/ 65 | COPY --from=builder /usr/sbin/lspgen /usr/sbin/ 66 | 67 | RUN rm /etc/motd 68 | 69 | ### 70 | # set a password to SSH into the docker container with 71 | RUN adduser -D -h /home/admin -s /bin/bash admin 72 | RUN adduser admin wheel 73 | RUN sed -i 's/# %wheel ALL=(ALL:ALL) ALL/%wheel ALL=(ALL:ALL) NOPASSWD: ALL/g' /etc/sudoers 74 | RUN echo 'admin:multit00l' | chpasswd 75 | # copy a basic but nicer than standard bashrc for the user 'admin' 76 | COPY .bashrc /home/admin/.bashrc 77 | RUN chown admin:admin /home/admin/.bashrc 78 | # Ensure .bashrc is sourced by creating a .bash_profile that sources .bashrc 79 | RUN echo 'if [ -f ~/.bashrc ]; then . ~/.bashrc; fi' > /home/admin/.bash_profile 80 | # Ensure configs in /etc/ssh/ssh_config.d/ are included 81 | RUN echo "Include /etc/ssh/ssh_config.d/*" >> /etc/ssh/ssh_config 82 | # Change ownership of the home directory to the user 'admin' 83 | RUN chown -R admin:admin /home/admin 84 | ### 85 | 86 | COPY index.html /usr/share/nginx/html/ 87 | COPY nginx.conf /etc/nginx/nginx.conf 88 | 89 | # copy the bashrc file to the root user's home directory 90 | COPY .bashrc /root/.bashrc 91 | RUN echo 'if [ -f ~/.bashrc ]; then . ~/.bashrc; fi' > /root/.bash_profile 92 | 93 | # Copy GoTTY binary from the build stage 94 | COPY --from=gotty-builder /go/bin/gotty /usr/local/bin/ 95 | RUN chmod +x /usr/local/bin/gotty 96 | 97 | # Create directories for GoTTY service 98 | RUN mkdir -p /var/run/gotty /var/log/gotty 99 | 100 | COPY gotty-service /usr/local/bin/gotty-service 101 | RUN chmod +x /usr/local/bin/gotty-service 102 | 103 | COPY entrypoint.sh /docker/entrypoint.sh 104 | COPY if-wait.sh /docker/if-wait.sh 105 | 106 | # Start nginx in foreground (pass CMD to docker entrypoint.sh): 107 | CMD ["/usr/sbin/nginx", "-g", "daemon off;"] 108 | 109 | # Run the startup script as ENTRYPOINT, which does few things and then starts nginx. 110 | ENTRYPOINT ["/bin/sh", "/docker/entrypoint.sh"] 111 | -------------------------------------------------------------------------------- /gotty-service: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # GoTTY service management script 4 | # Usage: gotty-service {start|stop|status|restart} [PORT] [USERNAME] [PASSWORD] [SHELL] 5 | 6 | # Default configuration 7 | PORT=${2:-8080} 8 | USERNAME=${3:-admin} 9 | PASSWORD=${4:-admin} 10 | SHELL=${5:-bash} 11 | TITLE="Network Multitool Terminal" 12 | 13 | # Paths 14 | PID_FILE="/var/run/gotty/gotty-${PORT}.pid" 15 | LOG_FILE="/var/log/gotty/gotty-${PORT}.log" 16 | 17 | # Ensure directories exist 18 | mkdir -p /var/run/gotty /var/log/gotty 19 | 20 | check_port() { 21 | if netstat -tuln | grep -q ":$PORT "; then 22 | if [ -f "$PID_FILE" ]; then 23 | PID=$(cat "$PID_FILE") 24 | if ps -p "$PID" > /dev/null; then 25 | # Our GoTTY service is using this port 26 | return 0 27 | fi 28 | fi 29 | # Another service is using this port 30 | echo "Error: Port $PORT is already in use by another service." 31 | return 1 32 | fi 33 | return 0 34 | } 35 | 36 | start_gotty() { 37 | if [ -f "$PID_FILE" ]; then 38 | PID=$(cat "$PID_FILE") 39 | if ps -p "$PID" > /dev/null; then 40 | echo "GoTTY is already running on port $PORT (PID: $PID)" 41 | return 0 42 | else 43 | # Stale PID file 44 | rm "$PID_FILE" 45 | fi 46 | fi 47 | 48 | check_port || return 1 49 | 50 | echo "Starting GoTTY service on port $PORT..." 51 | gotty -w -p "$PORT" -c "$USERNAME:$PASSWORD" --title-format "$TITLE" "$SHELL" > "$LOG_FILE" 2>&1 & 52 | 53 | PID=$! 54 | echo $PID > "$PID_FILE" 55 | 56 | # Check if process is still running after a short delay 57 | sleep 1 58 | if ps -p "$PID" > /dev/null; then 59 | echo "GoTTY service started successfully (PID: $PID)" 60 | echo "Access web terminal at: http://:$PORT" 61 | echo "Username: $USERNAME" 62 | echo "Password: $PASSWORD" 63 | return 0 64 | else 65 | echo "Failed to start GoTTY service. Check logs at $LOG_FILE" 66 | rm -f "$PID_FILE" 67 | return 1 68 | fi 69 | } 70 | 71 | stop_gotty() { 72 | if [ -f "$PID_FILE" ]; then 73 | PID=$(cat "$PID_FILE") 74 | if ps -p "$PID" > /dev/null; then 75 | echo "Stopping GoTTY service on port $PORT (PID: $PID)..." 76 | kill "$PID" 77 | 78 | # Wait for process to terminate 79 | for i in {1..5}; do 80 | if ! ps -p "$PID" > /dev/null; then 81 | break 82 | fi 83 | sleep 1 84 | done 85 | 86 | # Check if process is still running 87 | if ps -p "$PID" > /dev/null; then 88 | echo "GoTTY service did not stop gracefully, forcing termination..." 89 | kill -9 "$PID" 90 | fi 91 | 92 | rm -f "$PID_FILE" 93 | echo "GoTTY service stopped" 94 | else 95 | echo "GoTTY service is not running on port $PORT (stale PID file)" 96 | rm -f "$PID_FILE" 97 | fi 98 | else 99 | echo "GoTTY service is not running on port $PORT" 100 | fi 101 | } 102 | 103 | check_status() { 104 | if [ -f "$PID_FILE" ]; then 105 | PID=$(cat "$PID_FILE") 106 | if ps -p "$PID" > /dev/null; then 107 | echo "GoTTY service is running on port $PORT (PID: $PID)" 108 | echo "Access web terminal at: http://:$PORT" 109 | echo "Username: $USERNAME" 110 | echo "Password: $PASSWORD" 111 | return 0 112 | else 113 | echo "GoTTY service is not running on port $PORT (stale PID file)" 114 | rm -f "$PID_FILE" 115 | return 1 116 | fi 117 | else 118 | echo "GoTTY service is not running on port $PORT" 119 | return 1 120 | fi 121 | } 122 | 123 | restart_gotty() { 124 | stop_gotty 125 | sleep 2 126 | start_gotty 127 | } 128 | 129 | # Main logic 130 | case "$1" in 131 | start) 132 | start_gotty 133 | ;; 134 | stop) 135 | stop_gotty 136 | ;; 137 | status) 138 | check_status 139 | ;; 140 | restart) 141 | restart_gotty 142 | ;; 143 | *) 144 | echo "Usage: $0 {start|stop|status|restart} [PORT] [USERNAME] [PASSWORD] [SHELL]" 145 | echo "Default: PORT=8080, USERNAME=admin, PASSWORD=admin, SHELL=bash" 146 | exit 1 147 | ;; 148 | esac 149 | 150 | exit 0 --------------------------------------------------------------------------------