Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .github/workflows/build-and-push.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Build and Push Multi-architecture Docker Image

on:
push:
branches: [ "main" ]
tags: [ "v*" ]
pull_request:
branches: [ "main" ]

jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v4

-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

-
name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

-
name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: netwatchteam/netwatch_telnet-attackpod

-
name: Build and push
uses: docker/build-push-action@v5
with:
context: .
file: ./src/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.idea/
.vscode/
__pycache__/
*.py[cod]
.pytest_cache/
.env
43 changes: 43 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# NetWatch Telnet-AttackPod - Improvements

This document describes the improvements made to the Telnet-AttackPod codebase.

## Changes Made

### 1. Fixed Missing Import
- Added missing `import random` to resolve error in line 393
- This ensures proper functioning of the randomized delay between login attempts

### 2. Enhanced Docker Multi-architecture Support
- Updated Dockerfile with `--platform=linux/amd64` to ensure correct base image selection
- Created `build.sh` script for building multi-architecture images (amd64 and arm64)
- Modified docker-compose.yml to properly reference the Dockerfile location

### 3. Improved API Communication
- Added User-Agent header to requests for better identification
- Maintained existing authentication approach with Authorization header

## Architecture Support

The updated build system now supports building images for:
- AMD64 (x86_64)
- ARM64 (arm64/aarch64)

This enables running the AttackPod on various hardware platforms including Raspberry Pi and servers.

## Build Instructions

1. **Normal Build:**
```bash
docker compose build --no-cache
```

2. **Multi-architecture Build (Recommended):**
```bash
./build.sh
```

3. **Run the Container:**
```bash
docker compose up --force-recreate -d && docker compose logs -tf
```
20 changes: 18 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,16 @@ telnet localhost 23

Enter any username and password when prompted. You should see the attack captured in the logs.

## Running Tests

Unit tests can be run with pytest:

```bash
cd src/
pip install -r requirements-test.txt
pytest tests.py -v
```

## RFC 1918 Private IP Filtering

The Telnet-AttackPod automatically filters out attacks involving private/local IP addresses to prevent false positives and reduce backend processing load. Attacks are **not reported** to the NetWatch collector if either the source IP or destination IP is a private/non-routable address.
Expand Down Expand Up @@ -126,9 +136,7 @@ The NetWatch backend frequently updates lists of IP addresses that have been ide

To get direct access to the data that your sensor submitted to the NetWatch backend, you can use the Community API. You can find the API documentation at: https://community-api.netwatch.team. Currently, the API allows you to retrieve the raw data that your sensor submitted to the NetWatch backend, as well as correlated data from other sensors and customized block lists for your environment. (Please be aware that there is a fair use policy in place for the Community API.)

### Daily Attack Summary

On the Daily Attack Summary GitHub repository, you can find a daily export of all abusive IPs and unique credentials seen by all sensors. You can find the repository at: https://github.com/NetWatch-team/Daily-Attack-Summary

### Research & Collaboration

Expand Down Expand Up @@ -173,6 +181,14 @@ If you want to build Telnet-AttackPod from source you can do so by:
docker compose build --no-cache
```

To build for multiple CPU architectures:

```bash
./build.sh
```

This will create images for both AMD64 and ARM64 architectures.

#### Advanced Configuration

##### Manual IP Configuration
Expand Down
20 changes: 20 additions & 0 deletions build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/bin/bash

# Multi-architecture Docker build script for NetWatch Telnet-AttackPod

echo "Building multi-architecture Docker image for Telnet-AttackPod..."

# Create a builder instance if it doesn't exist
if ! docker buildx ls | grep -q "multiarch"; then
echo "Creating multi-architecture builder..."
docker buildx create --name multiarch --use
fi

# Build for multiple platforms
docker buildx build \
--platform linux/amd64,linux/arm64 \
--tag netwatchteam/netwatch_telnet-attackpod:v0.2 \
--output "type=image,push=false" \
-f src/Dockerfile .

echo "Multi-architecture build complete!"
4 changes: 3 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
services:
NetWatchTelnetAttackPod:
build: .
build:
context: .
dockerfile: src/Dockerfile
image: netwatchteam/netwatch_telnet-attackpod:0.2
container_name: netwatch_telnet-attackpod
environment:
Expand Down
8 changes: 8 additions & 0 deletions src/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
__pycache__/
.idea/
.venv/
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
.vscode
2 changes: 1 addition & 1 deletion src/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.10-slim
FROM --platform=linux/amd64 python:3.10-slim

# Create a non-root user
RUN groupadd -r appuser && useradd -r -g appuser -m appuser
Expand Down
68 changes: 45 additions & 23 deletions src/monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,40 +13,42 @@
import logging
import requests
import os
import random
from datetime import datetime
from typing import Optional, Tuple
import threading
import time
import socket
import queue
import json
import sys

# ============================================================================
# CONFIGURATION CONSTANTS
# ============================================================================

# NetWatch Collector Configuration
DEFAULT_COLLECTOR_URL = "https://api.netwatch.team"
COLLECTOR_REQUEST_TIMEOUT = int(get_env("COLLECTOR_REQUEST_TIMEOUT", "10")) # seconds for API requests
CHECK_IP_TIMEOUT = int(get_env("CHECK_IP_TIMEOUT", "5")) # seconds for IP check requests
COLLECTOR_REQUEST_TIMEOUT = int(os.getenv("COLLECTOR_REQUEST_TIMEOUT", "10")) # seconds for API requests
CHECK_IP_TIMEOUT = int(os.getenv("CHECK_IP_TIMEOUT", "5")) # seconds for IP check requests

# IP Detection Configuration
MAX_IP_RETRY_ATTEMPTS = int(get_env("MAX_IP_RETRY_ATTEMPTS", "50")) # max attempts to get local IP
IP_RETRY_DELAY_SECONDS = int(get_env("IP_RETRY_DELAY_SECONDS", "10")) # delay between IP detection retries
MAX_IP_RETRY_ATTEMPTS = int(os.getenv("MAX_IP_RETRY_ATTEMPTS", "50")) # max attempts to get local IP
IP_RETRY_DELAY_SECONDS = int(os.getenv("IP_RETRY_DELAY_SECONDS", "10")) # delay between IP detection retries

# Telnet Server Configuration
TELNET_INTERNAL_PORT = get_env("TELNET_INTERNAL_PORT", "2323") # non-privileged port inside Docker
TELNET_LISTEN_BACKLOG = int(get_env("TELNET_LISTEN_BACKLOG", "100")) # max queued connections
CLIENT_TIMEOUT_SECONDS = int(get_env("CLIENT_TIMEOUT_SECONDS", "30")) # timeout for client connections
TELNET_INTERNAL_PORT = os.getenv("TELNET_INTERNAL_PORT", "2323") # non-privileged port inside Docker
TELNET_LISTEN_BACKLOG = int(os.getenv("TELNET_LISTEN_BACKLOG", "100")) # max queued connections
CLIENT_TIMEOUT_SECONDS = int(os.getenv("CLIENT_TIMEOUT_SECONDS", "30")) # timeout for client connections

# Telnet Protocol Constants
TELNET_IAC = int(get_env("TELNET_IAC", "255")) # Interpret As Command byte
TELNET_COMMAND_LENGTH = int(get_env("TELNET_COMMAND_LENGTH", "3")) # IAC commands are typically 3 bytes
TELNET_IAC = int(os.getenv("TELNET_IAC", "255")) # Interpret As Command byte
TELNET_COMMAND_LENGTH = int(os.getenv("TELNET_COMMAND_LENGTH", "3")) # IAC commands are typically 3 bytes

# Input Processing Configuration
MAX_INPUT_RETRIES = int(get_env("MAX_INPUT_RETRIES", "30")) # max attempts to read username/password
INPUT_RETRY_DELAY = float(get_env("INPUT_RETRY_DELAY", "0.1")) # delay between empty input retries (seconds)
SOCKET_RECV_BUFFER = int(get_env("SOCKET_RECV_BUFFER", "1024")) # bytes to read at once
MAX_INPUT_RETRIES = int(os.getenv("MAX_INPUT_RETRIES", "30")) # max attempts to read username/password
INPUT_RETRY_DELAY = float(os.getenv("INPUT_RETRY_DELAY", "0.1")) # delay between empty input retries (seconds)
SOCKET_RECV_BUFFER = int(os.getenv("SOCKET_RECV_BUFFER", "1024")) # bytes to read at once

# ============================================================================
# GLOBAL STATE
Expand Down Expand Up @@ -94,14 +96,28 @@ def _check_if_in_test_mode() -> bool:
return get_env("NETWATCH_TEST_MODE", "false").lower() == "true"


# Private and non-routable network ranges per RFC 1918 and related standards
PRIVATE_NETWORKS = (
ipaddress.ip_network("10.0.0.0/8"),
ipaddress.ip_network("172.16.0.0/12"),
ipaddress.ip_network("192.168.0.0/16"),
ipaddress.ip_network("127.0.0.0/8"),
ipaddress.ip_network("169.254.0.0/16"),
ipaddress.ip_network("::1/128"),
ipaddress.ip_network("fc00::/7"),
ipaddress.ip_network("fe80::/10"),
)


def is_private_ip(ip: str) -> bool:
"""
Check if an IP address is private/non-routable per RFC 1918 and related standards.

This includes:
- RFC 1918 Private Networks: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
- Loopback Addresses: 127.0.0.0/8
- Link-Local Addresses: 169.254.0.0/16
- Loopback Addresses: 127.0.0.0/8, ::1/128
- Link-Local Addresses: 169.254.0.0/16, fe80::/10
- Unique Local IPv6: fc00::/7

Args:
ip: IP address string to check
Expand All @@ -111,20 +127,24 @@ def is_private_ip(ip: str) -> bool:
"""
try:
ip_obj = ipaddress.ip_address(ip)
return ip_obj.is_private or ip_obj.is_loopback or ip_obj.is_link_local
return any(ip_obj in net for net in PRIVATE_NETWORKS)
except ValueError:
logging.warning(f"Invalid IP address format: {ip}")
return False


def get_local_ip() -> str:
def get_local_ip(max_retries: int = MAX_IP_RETRY_ATTEMPTS, retry_delay: float = IP_RETRY_DELAY_SECONDS) -> str:
"""
Detect the public IP address of this sensor by querying the NetWatch API.

This function retries multiple times with delays to handle transient
network issues during startup. If the IP cannot be determined after
all retries, the program exits.

Args:
max_retries: Maximum number of detection attempts before giving up.
retry_delay: Delay in seconds between retries.

Returns:
Public IP address as a string

Expand All @@ -133,7 +153,7 @@ def get_local_ip() -> str:
"""
url = f"{get_env('NETWATCH_COLLECTOR_URL', DEFAULT_COLLECTOR_URL)}/check_ip"

for attempt in range(MAX_IP_RETRY_ATTEMPTS):
for attempt in range(max_retries):
try:
response = requests.get(url, timeout=CHECK_IP_TIMEOUT)
if response.status_code == 200:
Expand All @@ -142,21 +162,21 @@ def get_local_ip() -> str:
return local_ip
else:
logging.warning(
f"[!] Attempt {attempt + 1}/{MAX_IP_RETRY_ATTEMPTS}: "
f"[!] Attempt {attempt + 1}/{max_retries}: "
f"API returned status {response.status_code}"
)
except requests.exceptions.RequestException as e:
logging.error(
f"[!] Attempt {attempt + 1}/{MAX_IP_RETRY_ATTEMPTS}: "
f"[!] Attempt {attempt + 1}/{max_retries}: "
f"Error getting local IP: {e}"
)

# Don't sleep after the last failed attempt
if attempt < MAX_IP_RETRY_ATTEMPTS - 1:
time.sleep(IP_RETRY_DELAY_SECONDS)
if attempt < max_retries - 1:
time.sleep(retry_delay)

logging.error("[!] Unable to get local IP after all retries. Exiting.")
exit(1)
sys.exit(1)


# ============================================================================
Expand Down Expand Up @@ -230,8 +250,10 @@ def attack_forward_worker() -> None:
"""
# Use Session for connection pooling (more efficient than individual requests)
session = requests.Session()
# Set a User-Agent header for better identification
session.headers.update({
"Authorization": get_env("NETWATCH_COLLECTOR_AUTHORIZATION", "")
"Authorization": get_env("NETWATCH_COLLECTOR_AUTHORIZATION", ""),
"User-Agent": "NetWatch-Telnet-AttackPod/0.2"
})
url = f"{get_env('NETWATCH_COLLECTOR_URL', DEFAULT_COLLECTOR_URL)}/v2/add_attack/telnet_bruteforce"

Expand Down
3 changes: 3 additions & 0 deletions src/requirements-test.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Testing dependencies
pytest
unittest-xml-reporting
Loading
Loading