Skip to content
Merged
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
19 changes: 19 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
version: 2
updates:
- package-ecosystem: gomod
directory: /cloudflare-ip-logger
schedule:
interval: weekly
open-pull-requests-limit: 5

- package-ecosystem: docker
directory: /cloudflare-ip-logger
schedule:
interval: weekly
open-pull-requests-limit: 5

- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 5
140 changes: 140 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
shellcheck:
name: ShellCheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run ShellCheck
uses: ludeeus/action-shellcheck@2.0.0
env:
SHELLCHECK_OPTS: -e SC1091 -x
with:
severity: error
scandir: '.'
ignore_paths: >-
.git
.github

python:
name: Python (compile + ruff)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Discover python files
id: pyfiles
run: |
mapfile -t files < <(git ls-files '*.py')
printf '%s\n' "${files[@]}"
{
echo 'files<<EOF'
printf '%s\n' "${files[@]}"
echo EOF
} >> "$GITHUB_OUTPUT"
- name: Byte-compile
if: steps.pyfiles.outputs.files != ''
run: |
while IFS= read -r f; do
[ -z "$f" ] && continue
python -m py_compile "$f"
done <<< "${{ steps.pyfiles.outputs.files }}"
- name: Install ruff
run: pip install ruff==0.6.9
- name: Lint with ruff
run: ruff check --output-format=github .

go:
name: Go (cloudflare-ip-logger)
runs-on: ubuntu-latest
defaults:
run:
working-directory: cloudflare-ip-logger
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: cloudflare-ip-logger/go.mod
cache-dependency-path: cloudflare-ip-logger/go.sum
- name: Install SQLite headers
run: sudo apt-get update && sudo apt-get install -y libsqlite3-dev
- name: go mod download
run: go mod download
- name: go vet
run: go vet ./...
- name: Check formatting
run: |
unformatted=$(gofmt -l .)
if [ -n "$unformatted" ]; then
echo "::error::Files not gofmt-formatted:"
echo "$unformatted"
exit 1
fi
- name: go build
env:
CGO_ENABLED: '1'
run: |
go build -ldflags="-s -w" -o cf-ip-logger .
go build -o cf-log-parser ./cmd/logparser
- name: go test
run: go test ./...

hadolint:
name: Hadolint (Dockerfile)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hadolint/hadolint-action@v3.1.0
with:
dockerfile: cloudflare-ip-logger/Dockerfile
config: cloudflare-ip-logger/.hadolint.yaml
failure-threshold: warning

docker-build:
name: Docker build (cf-ip-logger)
runs-on: ubuntu-latest
needs: [go, hadolint]
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- name: Build image (no push)
uses: docker/build-push-action@v6
with:
context: cloudflare-ip-logger
push: false
load: true
tags: cf-ip-logger:ci
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Smoke-test image
run: |
docker run --rm --entrypoint /app/cf-ip-logger cf-ip-logger:ci -h 2>&1 || true
docker run --rm --entrypoint sh cf-ip-logger:ci -c 'test -x /app/cf-ip-logger && echo OK'

yaml:
name: YAML lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: pip install yamllint==1.35.1
- run: yamllint -c .yamllint.yml .
63 changes: 63 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: Release cf-ip-logger image

on:
push:
tags:
- 'cf-ip-logger-v*'
workflow_dispatch:
inputs:
tag:
description: 'Version tag to publish (e.g. v0.1.0)'
required: true

permissions:
contents: read
packages: write

jobs:
publish:
name: Build & push multi-arch image
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Resolve version
id: version
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
version="${{ inputs.tag }}"
else
version="${GITHUB_REF_NAME#cf-ip-logger-}"
fi
echo "version=${version}" >> "$GITHUB_OUTPUT"
echo "Resolved version: ${version}"

- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Generate image metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository_owner }}/cf-ip-logger
tags: |
type=raw,value=${{ steps.version.outputs.version }}
type=raw,value=latest

- name: Build and push
uses: docker/build-push-action@v6
with:
context: cloudflare-ip-logger
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,12 @@ Thumbs.db
# Temp files
*.tmp
*.bak

# Python
__pycache__/
*.pyc
*.pyo

# Go build artifacts (cloudflare-ip-logger)
cloudflare-ip-logger/cf-ip-logger
cloudflare-ip-logger/cf-log-parser
15 changes: 15 additions & 0 deletions .yamllint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
extends: relaxed

ignore: |
.git/
cloudflare-ip-logger/docker-compose.cloudflared.yml

rules:
line-length: disable
trailing-spaces: disable
comments: disable
document-start: disable
empty-lines:
max: 2
truthy:
check-keys: false
33 changes: 33 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Repository shape

This repo is a *collection of independent utility scripts*, not a single application. Each top-level directory is a self-contained tool with its own README and (usually) a single entry-point script. There is no shared library, no top-level build, no test suite, and no package manifest at the root. Treat each subdirectory as its own micro-project.

The only compiled component is `cloudflare-ip-logger/` (Go + SQLite, shipped as a Docker image). Everything else is bash or Python.

## Conventions for adding/modifying scripts

- New scripts start by copying `_template/` and renaming it. The template's `README.md` defines the documentation shape every tool follows (Overview / Prerequisites / Installation / Usage / Options table / Configuration table / How It Works / Troubleshooting / Changelog).
- After adding a new script directory, update the script table in the top-level `README.md` — the table is the canonical index and is expected to stay in sync with the directory listing.
- Bash scripts use `#!/usr/bin/env bash` (preferred) or `#!/bin/bash`. Several of the larger scripts (`HealthCheck`, `LinuxTroubleshooting`, `chown_throttled`, `File_retention`) carry a banner header comment with name + version — match that style when editing them.
- Python scripts target Python 3 and are written to be stdlib-only (no `requirements.txt` anywhere). Don't introduce third-party deps without flagging it; the "no external dependencies" property is called out explicitly in several READMEs (`dirsync`, `LinuxTroubleshooting`).
- Some directories use kebab-case (`cluster-ssh-key-setup`, `git-update`), others CamelCase (`HealthCheck`, `HungConnections`, `LinuxTroubleshooting`, `File_retention`). Don't rename existing ones — the README table links to them by name.

## cloudflare-ip-logger (the one non-script component)

A Go reverse proxy that sits behind `cloudflared` to capture real visitor IPs from the `CF-Connecting-IP` header before forwarding to backend services. Runs as a Docker container.

- `main.go` — the proxy + REST API + dashboard server. Routes by `Host` header against `proxy-config.json`, persists to SQLite, also tails to a plain-text log.
- `cmd/logparser/main.go` — separate binary that parses `cloudflared`'s JSON logs and ingests them into the same SQLite DB. Used by `run-with-logging.sh` and the `cf-log-parser.service` systemd unit when running cloudflared alongside the logger.
- Build/run: `docker compose up -d --build` from the directory. The Dockerfile uses CGO (required by `mattn/go-sqlite3`), so cross-compilation needs the C toolchain.
- Data layout under `/data` (volume-mounted): `connections.db`, `connections.log`, `proxy-config.json`. The example config lives at `proxy-config.json.example` in the repo and is copied into the data dir on first run.
- Routing depends on cloudflared preserving the original `Host` header via `originRequest.httpHostHeader` — this is the most common misconfiguration, see the README's "Cloudflared Configuration" section.

## Things that look like patterns but aren't

- There is no shared bash helper library. Logging/colors/error-handling are reimplemented per-script. Don't try to refactor across scripts unless the user asks.
- `pwr-temp-monitor/` ships several `*_metrics.sh` scripts (pi/jetson/x86) that look similar but target different hardware — they're meant to be deployed selectively by `setup.sh`, not unified.
- `HungConnections/` intentionally has both `.sh` and `.py` implementations of the same tool. Keep them feature-equivalent if you change one.
Empty file modified File_retention/file_retention.sh
100644 → 100755
Empty file.
Empty file modified HealthCheck/system_health_check.sh
100644 → 100755
Empty file.
9 changes: 5 additions & 4 deletions HungConnections/hung_connection_killer.py
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#!/usr/bin/env python3
"""
Hung Connection Killer
======================
Expand Down Expand Up @@ -374,7 +375,7 @@ def is_hung_connection(self, conn: Connection) -> tuple[bool, str]:
# Without timer info, flag states that are typically problematic
if conn.state == 'CLOSE_WAIT':
# CLOSE_WAIT with no timer is suspicious - app should have closed
return True, f"CLOSE_WAIT without active timer (likely app not closing socket)"
return True, "CLOSE_WAIT without active timer (likely app not closing socket)"
elif conn.state in ('FIN_WAIT1', 'FIN_WAIT2', 'CLOSING', 'LAST_ACK'):
# These should have timers; if not, they may be stuck
return True, f"{conn.state} state detected (typically indicates hung connection)"
Expand All @@ -397,7 +398,7 @@ def should_skip_connection(self, conn: Connection) -> tuple[bool, str]:
# Check port inclusions
if self.include_only_ports:
if conn.local_port not in self.include_only_ports and conn.remote_port not in self.include_only_ports:
return True, f"Port not in include list"
return True, "Port not in include list"

# Check process exclusions
if conn.process_name:
Expand Down Expand Up @@ -450,9 +451,9 @@ def _kill_socket_ss(self, conn: Connection) -> bool:
cmd = [
'ss', '-K',
'dst', f'{conn.remote_addr}',
'dport', f'eq', str(conn.remote_port),
'dport', 'eq', str(conn.remote_port),
'src', f'{conn.local_addr}',
'sport', f'eq', str(conn.local_port),
'sport', 'eq', str(conn.local_port),
]

returncode, stdout, stderr = self._run_command(cmd)
Expand Down
Empty file modified HungConnections/hung_connection_killer.sh
100644 → 100755
Empty file.
Empty file modified LinuxTroubleshooting/linux_troubleshoot.sh
100644 → 100755
Empty file.
Loading
Loading