Commit graph

280 commits

Author SHA1 Message Date
08d5957690
fix: address bugs and security gaps found in code audit round 2
- Fix blocking MikroTik channel sends that could leak goroutines
- Default to 10s timeout when check TimeoutMs is 0
- Add payload size limit to check_jobs event
- Add 30s write deadline to WebSocket writes
- Use random ICMP sequence numbers to prevent ping ID collisions
- Atomic host key file writes via temp+rename
- Sanitize update URLs in log output
- Zero correct buffer in sendBinaryResult after MarshalAppend
2026-03-05 12:53:30 -06:00
b4f3dabe8d
fix: remove double SNMP connect and use JoinHostPort for DNS server
- LLDP: remove redundant client.Connect() since newSnmpConn already connects
- DNS check: use net.JoinHostPort for IPv6-safe DNS server addressing
2026-03-05 12:29:02 -06:00
7606c0aa6c
fix: address bugs and security gaps found in code audit
- HTTP checks: use io.ReadAll with LimitReader instead of single Read()
  to handle bodies spanning multiple TCP segments
- HTTP checks: enforce TLS 1.2 minimum, matching websocket and mikrotik
- Ping/credential jobs: use select/default for channel sends to prevent
  goroutine leaks when result channel is full
- LLDP: check strconv.Atoi errors in IPv6 address parsing instead of
  silently producing incorrect addresses
- SNMP: use comma-ok type assertions in snmpValueToString to prevent
  panics from malformed device responses
- WebSocket: read handshake response line-by-line via bufio.Reader
  instead of single conn.Read that could miss multi-segment responses
2026-03-05 12:24:21 -06:00
adad473485
fix: handle errcheck lint violations in test files 2026-03-05 12:13:05 -06:00
b6f60c8fad
feat: add LLDP topology discovery support
Adds LLDP (Link Layer Discovery Protocol) topology discovery capability
to the agent for local network neighbor detection.

New Features:
- LLDP-MIB walker for discovering network neighbors via SNMP
- New LLDP_TOPOLOGY job type for topology discovery jobs
- Automatic IPv4 and IPv6 management address parsing
- Bulk neighbor discovery with full port information

Protocol Buffers:
- Added LldpTopologyResult message for topology data
- Added LldpNeighbor message for individual neighbor info
- Extended JobType enum with LLDP_TOPOLOGY (value 5)

Implementation:
- lldp.go: LLDP discovery logic with SNMP walking
- agent.go: Job dispatch and result channel handling
- Updated handleMessage signature to support LLDP results
- Result sent to Phoenix via "lldp_topology_result" event

LLDP-MIB OIDs:
- 1.0.8802.1.1.2.1.3.3.0 (lldpLocSysName)
- 1.0.8802.1.1.2.1.3.7.1.4 (lldpLocPortDesc)
- 1.0.8802.1.1.2.1.4.1.1.7 (lldpRemPortId)
- 1.0.8802.1.1.2.1.4.1.1.8 (lldpRemPortDesc)
- 1.0.8802.1.1.2.1.4.1.1.9 (lldpRemSysName)
- 1.0.8802.1.1.2.1.4.2.1.3 (lldpRemManAddr)

This enables the Phoenix web application to dispatch LLDP discovery
jobs to agents for local neighbor detection, reducing SNMP load on the
Phoenix server and improving discovery performance.
2026-03-05 11:05:27 -06:00
6bf9584cb2
test: push coverage from 92% to 97.6% (249 tests)
Refactor main.go to extract testable runMain() function, make
icmpListenPacket and syscallExec injectable, fix worker pool tests
to properly fill queue buffers (n*4 capacity). Add comprehensive
tests for CLI argument parsing, ping exec fallback, ICMP timeout
paths, self-update happy path, and SNMP context cancellation.
2026-03-04 16:35:02 -06:00
aef770ea83
test: achieve 92%+ coverage with comprehensive integration and edge case tests
Cover WebSocket session lifecycle (result channels, SNMP batch flushing,
heartbeats, restart), MikroTik TLS TOFU verification mismatch, worker pool
exhaustion, plaintext WebSocket rejection, HTTP/TCP/DNS check execution,
IPv4 fallback, and customer network edge cases.

Make heartbeat intervals and toWebSocketURL exit injectable for testing.
2026-03-04 16:06:29 -06:00
c4bad51e73
fix: fall back to IPv4 when WebSocket handshake fails
If the initial connection attempt fails (e.g. server returns 403 over
IPv6), retry with tcp4 to force IPv4. This handles cases where DNS
returns AAAA records but the server isn't properly configured on IPv6.
2026-03-04 15:14:58 -06:00
Graham McIntire
4644817d3c
Merge pull request #25 from towerops-app/dependabot/github_actions/actions/download-artifact-8
Bump actions/download-artifact from 7 to 8
2026-03-04 13:37:35 -06:00
dependabot[bot]
6e386767d0
Bump actions/download-artifact from 7 to 8
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 7 to 8.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v7...v8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-04 19:37:25 +00:00
6865f1169c
ci: downgrade checkout action to v4 for Forgejo runner compatibility
Forgejo runner v6.2.2 only supports up to node20; actions/checkout@v6
requires node24.
2026-03-04 13:34:05 -06:00
321c701fd7
fix: avoid copying sync.Once in host key tests
sync.Once contains sync.noCopy and cannot be assigned by value.
Reset with a fresh sync.Once{} in defer instead of save/restore.
Also gitignore known_hosts.json runtime data.
2026-03-04 13:32:53 -06:00
93df6ce0ba
ci: add Renovate for automated Go dependency updates
Add Forgejo workflow and renovate.json config to automatically
check for gomod, Docker, and GitHub Actions dependency updates.
Also bumps golang.org/x/net v0.50.0 → v0.51.0.
2026-03-04 13:27:53 -06:00
4f5be74528 security: implement audit fixes (TOFU host keys, update TOCTOU, TLS enforcement, read timeouts) 2026-02-15 09:06:39 -06:00
33485a0497 dev: add gopls/delve to flake, add Makefile for proto/test/build 2026-02-15 08:58:13 -06:00
ca36ab0c38 ci: add test workflow for Forgejo 2026-02-15 08:57:50 -06:00
c1b24fe38b fix: sync AgentError proto with server (add job_id, message fields) 2026-02-15 08:56:31 -06:00
6682d469a1
fix: handle error return values in checks.go
Fixed errcheck linter issues:
- resp.Body.Close() in HTTP check
- conn.Close() in TCP check
- conn.SetDeadline() in TCP send/expect
2026-02-12 14:38:19 -06:00
7fe07d888d
fix: update test signatures for handleMessage and dispatchJob
- Add missing checkResultCh parameter to all handleMessage() calls in tests
- Add missing checkResultCh parameter to all dispatchJob() calls in tests
- Add checks worker pool to testPools() function
- Fix IPv6 address formatting in executeTCPCheck using net.JoinHostPort
- Add strconv import for port conversion
2026-02-12 14:36:08 -06:00
3ec645a90a
feat: add service check execution (HTTP/TCP/DNS)
Extends agent to execute HTTP, TCP, and DNS service checks.

Agent Changes:
- Extended protobuf schema with Check, CheckResult, CheckList messages
- Added checks.go with HTTP/TCP/DNS executors (completely stateless)
- Added checks worker pool (50 workers)
- Handles check_jobs WebSocket event from Phoenix
- Reports check_result back to Phoenix

Check Executors:
- HTTP: Full request support with regex, SSL, headers
- TCP: Port connectivity with send/expect patterns
- DNS: Resolution for A, AAAA, CNAME, MX, TXT records

Architecture: Agent is stateless - receives jobs, executes, reports back.
Phoenix handles all scheduling and state tracking.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-12 14:05:54 -06:00
Graham McIntire
d141633586
Merge pull request #23 from towerops-app/icmp-fixes
fix ICMP ping in Docker by trying raw sockets before unprivileged UDP
2026-02-12 11:55:11 -06:00
f35be66148
fix ICMP ping in Docker by trying raw sockets before unprivileged UDP
The old code only tried unprivileged UDP ICMP (udp4), which relies on
the ping_group_range sysctl — not CAP_NET_RAW. Docker doesn't set that
sysctl, so pings always timed out and fell through to exec. Now tries
raw ICMP sockets (ip4:icmp) first, which properly uses CAP_NET_RAW.
Also sets cap_net_raw+ep on the agent binary in the Dockerfile.
2026-02-12 11:18:21 -06:00
Graham McIntire
cd6fb4e79e
Merge pull request #22 from towerops-app/security-updates
Security fixes
2026-02-12 11:05:43 -06:00
0f8e8fc183
security: validate join reply before entering main event loop
Previously the agent proceeded to the main loop without confirming
the server accepted the channel join. A rejected join (e.g. invalid
token) left the agent running but silently non-functional. Now waits
for the join reply with a 10s timeout, returns error on rejection.

Reader goroutine starts before join send so the reply can be received.
Writer goroutine starts after join confirmation.
2026-02-12 11:02:38 -06:00
13e923d251
security: zero buffer pool contents before reuse
Previously, resetting slice length to 0 left previous protobuf data
(device IDs, OID values, credentials) in the backing array. Now zeros
the full capacity before returning to the pool.
2026-02-12 10:59:54 -06:00
442b019b0b
security: sanitize token from re-exec log line
os.Args logged during self-update re-exec could expose --token value
in plaintext to log aggregators. sanitizeArgs masks the value.
2026-02-12 10:59:08 -06:00
a0516c2bd1
security: use net.JoinHostPort for SSH addresses to support IPv6
fmt.Sprintf("%s:%d") produces invalid addresses for IPv6. Also adds
mockable sshDial variable for testability.
2026-02-12 10:58:28 -06:00
8b65dffc37
security: graceful shutdown on restart event
handleMessage returns bool to signal session end instead of calling
osExit directly. Defers (pool stop, writeCh close, writerWg wait)
now run before exit, preventing in-flight result loss and panics
from writes to closed channels.
2026-02-12 10:56:56 -06:00
87606bb84a
security: fixes 1-5 — payload limits, non-blocking submit, panic recovery, handshake timeout, write error propagation
Fix 1: Limit inbound job payload size to 4MB to prevent OOM from malicious server
Fix 2: Worker pool submit accepts context and returns bool, prevents blocking event loop
Fix 3: Panic recovery in worker pool goroutines prevents permanent worker death
Fix 4: WebSocket handshake timeout (30s) prevents indefinite blocking on slow server
Fix 5: Writer goroutine propagates errors to main loop via writeErrCh
2026-02-12 10:55:24 -06:00
a9990d59bc
fix staticcheck SA6002: use *[]byte in sync.Pool to avoid allocation 2026-02-12 10:37:34 -06:00
e014e9ccd4
harden security: URL sanitization, security docs, credential zeroing utility
- Mask query parameters in log output to prevent credential leakage
- Document InsecureSkipVerify rationale for MikroTik self-signed certs
- Document InsecureIgnoreHostKey rationale for SSH device connections
- Add zeroBytes utility for future byte-slice credential clearing
2026-02-12 10:35:42 -06:00
aa7ca96e95
harden security: file permissions, WebSocket accept validation, backoff jitter
- Self-update: write binary with 0700 permissions instead of 0755
- WebSocket: validate Sec-WebSocket-Accept per RFC 6455 to prevent MITM
- WebSocket: add computeAcceptKey helper with correct RFC 6455 GUID
- Agent: add nextBackoff with 25% jitter to prevent thundering herd
2026-02-12 10:33:17 -06:00
ab2c28c595
harden security: frame limits, HTTPS enforcement, checksum validation, download bounds
- WebSocket: reject frames exceeding 16 MB to prevent OOM
- MikroTik: reject words exceeding 10 MB to prevent OOM
- Self-update: require HTTPS for download URLs
- Self-update: require non-empty SHA256 checksum
- Self-update: use constant-time comparison for checksum verification
- Self-update: bound download size to 100 MB
2026-02-12 10:24:49 -06:00
7ee88dad81
remove legacy Rust build config and artifacts 2026-02-12 10:06:34 -06:00
4e6dd45415
use raw ICMP ping with exec fallback for unprivileged environments 2026-02-12 10:02:54 -06:00
94192f402a
propagate context to job executors for cancellation on disconnect 2026-02-12 09:59:45 -06:00
04d8349a6c
pool protobuf marshal buffers to reduce allocations
Use sync.Pool to reuse 4KB byte slices for protobuf marshaling via
MarshalAppend instead of allocating fresh buffers per result message.
Reduces GC pressure under high-throughput polling.
2026-02-12 09:55:38 -06:00
da8b644f40
batch SNMP results before sending over WebSocket
Accumulate SNMP results and flush in batches of 50 or every 100ms,
whichever comes first. Reduces per-message overhead when thousands
of devices produce results near-simultaneously. Also flushes
remaining results on disconnect/shutdown.
2026-02-12 09:55:08 -06:00
82f5698802
combine WebSocket frame header and payload into single write
Build the entire frame (header + mask key + masked payload) in one
buffer and write it with a single syscall instead of two separate
writes. Removes the failOnNthWrite test that tested the old
two-write implementation detail.
2026-02-12 09:54:31 -06:00
fbaf7a07dc
buffer WebSocket reads to reduce syscalls
Wrap the WebSocket connection in an 8KB bufio.Reader so that small
reads (2-byte frame headers, length extensions) are served from
buffer instead of making individual syscalls.
2026-02-12 09:53:16 -06:00
c57addc6c7
increase channel buffer sizes to handle thousands of devices
Raise write channel from 500 to 10000, SNMP/monitoring result
channels from 1000 to 10000, MikroTik/credential channels to 5000.
Prevents silent data loss when polling thousands of devices.
2026-02-12 09:50:46 -06:00
8b866ae946
add worker pool with bounded concurrency for job dispatch
Replace unbounded goroutine spawning with fixed-size worker pools:
100 workers for SNMP, 20 for MikroTik, 50 for ping. Prevents file
descriptor exhaustion and device overload when handling thousands
of concurrent jobs. Pool provides backpressure when saturated.
2026-02-12 09:50:17 -06:00
07e7ed2745
replace fmt.Sprintf with strconv in hot paths
Use strconv.FormatInt/FormatUint instead of fmt.Sprintf for integer
conversions in snmpValueToString. Use encoding/hex + strings.Builder
instead of fmt.Sprintf loop in formatHex. Use strconv.FormatUint for
WebSocket ref counter. Reduces allocations per OID significantly.
2026-02-12 09:47:42 -06:00
82df96cd59
batch SNMP GETs into chunks of 60 OIDs per PDU
Previously each OID was fetched individually, meaning 1000 OIDs
required 1000 round trips. Now OIDs are batched into groups of 60
(the SNMP max PDU size), reducing round trips by ~50x. Also
pre-sizes the OID value map to avoid repeated rehashing.
2026-02-12 09:41:48 -06:00
92f7c2ed85
fix SNMPv1 walks failing due to unsupported GETBULK
use GETNEXT-based WalkAll for v1 devices instead of BulkWalkAll
2026-02-11 13:04:32 -06:00
01cd109d10
cleanup 2026-02-11 12:42:49 -06:00
289bc3d4f1
fix docker tags by replacing colons in timestamp version
Docker tags cannot contain colons. Use a separate tag-safe format
(colons replaced with hyphens) for Docker tags while keeping the
RFC 3339 timestamp as the version baked into the binary.
2026-02-11 11:48:05 -06:00
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