Commit graph

283 commits

Author SHA1 Message Date
297cb8fc91
use UTC timestamp for version instead of git describe 2026-02-11 11:44:19 -06:00
39641b73d4
enable ping in docker container for non-root user
add libcap and setcap cap_net_raw+p on /bin/ping so ICMP works
without container-level capabilities. add NET_RAW cap_add to
docker-compose.example.yml as fallback.
2026-02-11 10:58:30 -06:00
894dcb18e4
skip ping test when raw sockets unavailable 2026-02-11 10:54:15 -06:00
9611cfd620
fix missed errcheck in websocket test 2026-02-11 10:50:00 -06:00
5ad95fac42
fix errcheck lint violations in test files 2026-02-11 10:48:41 -06:00
a36a2b7fd7
increase test coverage and improve logger output 2026-02-11 10:42:19 -06:00
a757bd7615
Optimize CI caching for Blacksmith runners, fix SNMP bulk walk on Mikrotik
- Dockerfile: add BuildKit cache mounts for Go module and build caches,
  pin both stages to Alpine 3.23
- CI: replace BUILDKIT_INLINE_CACHE with registry-based layer caching
  (mode=max) for branch and release builds
- .dockerignore: exclude test files, Rust legacy, nix, and other
  non-build artifacts from Docker context
- SNMP: lower MaxRepetitions from default 50 to 10 to prevent Mikrotik
  routers from timing out during bulk walks
2026-02-11 10:31:10 -06:00
Graham McIntire
86da744e68
Merge pull request #21 from towerops-app/dependabot/github_actions/actions/checkout-6
Bump actions/checkout from 4 to 6
2026-02-11 10:20:14 -06:00
dependabot[bot]
51210a5986
Bump actions/checkout from 4 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-11 16:20:06 +00:00
Graham McIntire
720ccb40e2
Merge pull request #20 from towerops-app/dependabot/github_actions/actions/upload-artifact-6
Bump actions/upload-artifact from 4 to 6
2026-02-11 10:19:59 -06:00
Graham McIntire
62e31e3d05
Merge pull request #18 from towerops-app/dependabot/github_actions/golangci/golangci-lint-action-9
Bump golangci/golangci-lint-action from 7 to 9
2026-02-11 10:19:40 -06:00
Graham McIntire
91820976ae
Merge pull request #19 from towerops-app/dependabot/github_actions/actions/setup-go-6
Bump actions/setup-go from 5 to 6
2026-02-11 10:19:26 -06:00
Graham McIntire
b03edf8f9b
Merge pull request #17 from towerops-app/dependabot/github_actions/actions/download-artifact-7
Bump actions/download-artifact from 4 to 7
2026-02-11 10:19:14 -06:00
dependabot[bot]
c360705fd4
Bump actions/upload-artifact from 4 to 6
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-11 16:18:57 +00:00
dependabot[bot]
e8ae0769a5
Bump actions/setup-go from 5 to 6
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5 to 6.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-11 16:18:53 +00:00
dependabot[bot]
a72ad8a130
Bump golangci/golangci-lint-action from 7 to 9
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 7 to 9.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v7...v9)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-version: '9'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-11 16:18:49 +00:00
dependabot[bot]
5757bd4a13
Bump actions/download-artifact from 4 to 7
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 7.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v4...v7)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-11 16:18:45 +00:00
Graham McIntire
ee29613047
Merge pull request #16 from towerops-app/golang-rewrite
Full rewrite in golang
2026-02-11 10:18:12 -06:00
58d1af4f84
refactor some code 2026-02-11 10:17:01 -06:00
37f411ec55
cleanup 2026-02-11 10:07:27 -06:00
769ac1f623
Full rewrite in golang 2026-02-11 10:04:30 -06:00
99997e0c1f
Add OID count to SNMP job completion logs
Shows how many OID values were collected and sent back, making it
visible at info level whether SNMP walks returned data or not.
2026-02-11 09:14:36 -06:00
477b5f06cb
Promote job lifecycle logging from debug to info level
Job receipt, start, and completion messages were all at debug level,
making it impossible to tell if the agent was processing jobs without
setting LOG_LEVEL=debug.
2026-02-11 09:09:10 -06:00
81a5d26655
Cleanup from dokku testing 2026-02-11 09:01:18 -06:00
a9bf0b0f8b
Switch from Alpine (musl) to Debian slim (glibc) to fix SNMP SIGSEGV
musl libc has weak fork-safety guarantees in multi-threaded processes.
When Tokio's runtime is running and we fork() for SNMP operations, musl's
internal state can be inconsistent in the child, causing SIGSEGV on every
SNMP walk to Ubiquiti devices. glibc handles this via pthread_atfork.

- Dockerfile: Alpine → rust:1.93-bookworm build + debian:12-slim runtime
- CI: Remove ensure-netsnmp-base/manifest jobs, simplify build deps
- Delete Dockerfile.netsnmp and netsnmp-base.yml workflow
- Add NULL pointer guards in snmp_helper.c for fork-safety
- Add SNMP exception type handling (NoSuchObject/Instance/EndOfMibView)
- Add crash reproduction tests
2026-02-11 08:41:06 -06:00
fc14fa7798
Disable embedded perl in net-snmp build 2026-02-11 08:06:37 -06:00
2542909690
Add missing file dependency for net-snmp configure 2026-02-11 08:03:44 -06:00
0b0d326892
Add net-snmp base image as CI prerequisite
The ensure-netsnmp-base jobs check if the pre-built image exists in
GHCR before any Docker builds start. First run compiles from source
on native runners (~5 min), subsequent runs skip with a ~5s check.

build-branch and build-release now depend on ensure-netsnmp-manifest.
2026-02-11 08:01:23 -06:00
2386f0d5a5
Upgrade net-snmp to 5.9.5.2 to fix SIGSEGV in SNMP walks
Alpine 3.23 ships net-snmp 5.9.4 which has segfault bugs in varbind
construction and buffer overflows in octet string handling. Version
5.9.5.2 fixes these issues.

Adds a pre-built base image (Dockerfile.netsnmp) compiled from source
so the net-snmp build doesn't run on every agent deploy. The base
image is built separately via the netsnmp-base workflow and referenced
in the main Dockerfile.
2026-02-10 17:58:43 -06:00
06c45cd404
Fix SIGSEGV in forked SNMP child: remove double init_snmp() call
In Fork mode the parent never calls snmp_init_library(), so
pthread_once remains untriggered. The child called init_snmp()
directly, then snmp_open_session() triggered pthread_once calling
init_snmp() a second time. This double-initialization of libnetsnmp
caused signal 11 in every child process.

Let snmp_open_session() handle initialization via pthread_once,
which correctly initializes exactly once and sets numeric OID format.
2026-02-10 17:35:49 -06:00
0db587338e
Use Blacksmith runner for amd64 release build
Switches from ubuntu-latest (2 vCPU) to blacksmith-4vcpu-ubuntu-2404
to match the arm64 build runner and speed up Rust compilation.
2026-02-10 16:52:59 -06:00
4573869607
Reduce log noise by changing INFO to DEBUG for routine operations
Changed the following from INFO to DEBUG level:
- Channel join messages
- Job execution start/completion messages
- SNMP operation messages (GET/WALK)
- Device health check results
- MikroTik job execution
- Device poller thread lifecycle
- Credential test messages
- OID collection results

These messages were generating excessive noise during normal operation.
Kept INFO level for:
- Initial connection establishment
- Shutdown/restart messages
- Update operations
- Errors (already at ERROR/WARN level)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-10 16:49:58 -06:00
73b75c8aa4
Disable MIB loading in forked SNMP child processes
Net-SNMP was crashing (signal 11) on every operation despite fork isolation.
The issue was that init_snmp() tries to load MIB files by default, which can
cause crashes if MIB files are missing, corrupted, or incompatible.

Since we only use numeric OIDs (e.g., "1.3.6.1.2.1.1.1.0"), we don't need
MIB name resolution at all. This change explicitly disables MIB loading in
the child process by:

1. Setting MIBS and MIBDIRS environment variables to empty
2. Calling netsnmp_set_mib_directory("") to disable MIB directory search
3. These must happen BEFORE calling init_snmp()

This should fix the persistent SIGSEGV crashes in SNMP operations.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-10 16:39:32 -06:00
7b793b315a
speed up tests: reduce SNMP timeout from 1s to 200ms 2026-02-10 16:19:54 -06:00
57ace0297b
fix clippy needless_return warning 2026-02-10 16:15:37 -06:00
55d001d9f8
Add fork()-based process isolation for SNMP operations
Each SNMP GET/WALK now runs in a forked child process. If libnetsnmp
triggers a SIGSEGV or other fatal signal, only the child dies - the
parent agent logs the crash and continues operating for all other devices.

Key changes:
- C helper: snmp_get_isolated() and snmp_walk_isolated() using
  fork+pipe pattern with 60s alarm watchdog and mutex-serialized forks
- Rust: IsolationMode enum (Fork/Direct) controlled by
  TOWEROPS_SNMP_ISOLATION env var, defaults to Fork
- New CrashRecovered error variant with signal info and logging
- Device poller logs crash recovery events at error level
- Startup logs active isolation mode

Set TOWEROPS_SNMP_ISOLATION=direct to disable isolation for debugging.
2026-02-10 16:12:41 -06:00
54d85c7c06
Remove GitLab CI configuration 2026-02-10 16:03:31 -06:00
ab0db979a1
format 2026-02-10 15:48:51 -06:00
7bffbecd21
Fix function_casts_as_integer warnings in crash handler
Cast function pointers through *const () before converting to
sighandler_t, as required by Rust 1.93+.
2026-02-10 15:44:15 -06:00
6bb33d1f8a
Update CI Rust toolchain to 1.93 to match Docker build 2026-02-10 15:30:15 -06:00
c3e411a565 Fix AMD64 FFI crash by using native AMD64 runners
The AMD64 builds were crashing due to cross-compilation. Blacksmith runners
are ARM64, so when building linux/amd64 on them, Docker uses QEMU emulation
which breaks the C FFI code in native/snmp_helper.c.

Solution:
- AMD64 builds now use GitHub's ubuntu-latest runners (native x86_64)
- ARM64 builds continue using Blacksmith ARM64 runners (native)
- Both architectures now build natively, eliminating FFI segfaults
2026-02-10 15:07:53 -06:00
29b94c09d4
Fix segfault from Alpine version mismatch, add crash handler
The builder (rust:1.93-alpine) uses Alpine 3.23 but the runtime used
Alpine 3.19. The net-snmp-dev headers from 3.23 don't match
net-snmp-libs from 3.19, causing a segfault on the first SNMP call.

Also adds a SIGSEGV/SIGBUS/SIGABRT signal handler that prints a
diagnostic message instead of silently exiting with code 139.
2026-02-10 14:14:07 -06:00
fa46d279a7
Use short SNMP timeouts in tests to avoid 90s waits
Tests against unreachable hosts (192.0.2.1) were waiting 10s * 3 attempts
per request. With cfg(test) overrides (1s timeout, 0 retries), the full
test suite runs in ~3s instead of ~90s.
2026-02-10 14:00:56 -06:00
d91effd3eb
Add integration tests for TLS CryptoProvider configuration
Verifies that ring is properly installed as the default crypto provider
and that all TLS-dependent operations work: rustls ClientConfig, reqwest
client, tokio-rustls connector, and WebSocket TLS with webpki roots.
These tests catch the CryptoProvider panic before it reaches production.
2026-02-10 13:55:32 -06:00
32a60beadb
Fix rustls CryptoProvider panic by explicitly selecting ring
Both ring and aws-lc-rs were enabled transitively (webpki-roots enables
rustls default features), causing rustls to panic at runtime because it
couldn't auto-detect which crypto provider to use.

- Add rustls as direct dep with ring only, disable default features
- Switch russh from aws-lc-rs (default) to ring crypto backend
- Call ring::default_provider().install_default() at startup
- Add macOS build fix: detect Homebrew OpenSSL path for netsnmp's libcrypto
2026-02-10 13:50:33 -06:00
28571b0243
Add webhook notification step to release workflow
After publishing a GitHub Release, notify towerops.net to trigger
mass agent updates via the new webhook endpoint.
2026-02-10 13:40:49 -06:00
9db47e22d1
Remove CAP_NET_RAW requirement from documentation
Since we now use command-line ping (setuid root) instead of surge-ping
library, the agent no longer requires CAP_NET_RAW capability.

Reverted documentation changes from previous commit that added the
capability requirement.
2026-02-10 13:27:08 -06:00
91c407ed5b
Replace surge-ping library with command-line ping
Switched from surge-ping Rust library to system ping command (iputils)
to avoid requiring CAP_NET_RAW capability for ICMP ping.

Changes:
- Use tokio::process::Command to execute /bin/ping or /bin/ping6
- Parse response time from ping output (time=X.XX ms)
- Removed surge-ping and rand dependencies from Cargo.toml
- Added tokio "process" feature

Benefits:
- No special capabilities required (ping binary has setuid root)
- Works in restricted container environments
- Already available in Docker image (iputils package)
- More portable across different container runtimes

The system ping command is already installed in the Docker image
and has setuid root permissions, allowing it to create raw sockets
without requiring the container to have CAP_NET_RAW.
2026-02-10 13:24:32 -06:00
4da510dff1
Add NET_RAW capability requirement for ICMP ping
Agent was crashing with segmentation fault (exit code 139) when
attempting ICMP ping health checks. The surge-ping library requires
CAP_NET_RAW capability to create raw sockets for ICMP.

Changes:
- Added cap_add: NET_RAW to docker-compose.example.yml
- Updated README docker-compose example
- Added troubleshooting section for exit code 139 crashes

Without this capability, the agent crashes immediately after
successful ping execution when the surge-ping library attempts
to clean up raw socket resources.
2026-02-10 13:22:37 -06:00
b0f3b57671
Add restart and self-update handlers, publish standalone binaries
Handle server-initiated restart (exit for Docker restart) and
self-update (download binary, verify SHA256, replace via exec)
commands received over the WebSocket channel. Report architecture
in heartbeat so the server can select the correct binary.

- Add restart and update handlers in websocket_client.rs
- Add self_update() with download, checksum verification, and exec
- Add arch field to AgentHeartbeat protobuf
- Add reqwest and sha2 dependencies
- chown binary in Dockerfile for non-root self-update
- Extract standalone binaries from Docker images in CI
- Create GitHub Releases with binary assets on version tags
2026-02-10 13:06:23 -06:00