├── 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
--------------------------------------------------------------------------------