Files
Jason Staack b840047e19 feat: The Other Dude v9.0.1 — full-featured email system
ci: add GitHub Pages deployment workflow for docs site

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 19:30:44 -05:00

116 lines
4.5 KiB
Go

// Package device handles RouterOS device connections and queries.
package device
import (
"crypto/tls"
"crypto/x509"
"fmt"
"log/slog"
"time"
routeros "github.com/go-routeros/routeros/v3"
)
// buildTLSConfig creates a TLS config using the portal CA cert for verification.
// Falls back to InsecureSkipVerify if caCertPEM is empty or invalid.
func buildTLSConfig(caCertPEM []byte) *tls.Config {
if len(caCertPEM) == 0 {
return &tls.Config{InsecureSkipVerify: true} //nolint:gosec // no CA cert available
}
pool := x509.NewCertPool()
if !pool.AppendCertsFromPEM(caCertPEM) {
slog.Warn("failed to parse CA cert PEM, falling back to insecure TLS")
return &tls.Config{InsecureSkipVerify: true} //nolint:gosec // invalid CA cert
}
return &tls.Config{RootCAs: pool}
}
// ConnectDevice establishes a connection to a RouterOS device.
//
// Connection strategy is governed by tlsMode:
//
// - "auto" (default): Try CA-verified TLS (if caCertPEM provided) ->
// InsecureSkipVerify -> STOP. No plain-text fallback.
// - "portal_ca": Try CA-verified TLS only (strict).
// - "insecure": Skip directly to InsecureSkipVerify TLS (no CA check).
// - "plain": Explicit opt-in for plain-text API connection.
//
// Callers must call CloseDevice when done.
func ConnectDevice(ip string, sslPort, plainPort int, username, password string, timeout time.Duration, caCertPEM []byte, tlsMode string) (*routeros.Client, error) {
sslAddr := fmt.Sprintf("%s:%d", ip, sslPort)
switch tlsMode {
case "plain":
// Explicit opt-in: plain-text connection only
plainAddr := fmt.Sprintf("%s:%d", ip, plainPort)
slog.Debug("connecting to RouterOS device (plain — explicit opt-in)", "address", plainAddr)
client, err := routeros.DialTimeout(plainAddr, username, password, timeout)
if err != nil {
return nil, fmt.Errorf("plain-text connection to %s failed: %w", plainAddr, err)
}
slog.Debug("connected to RouterOS device (plain — explicit opt-in)", "address", plainAddr)
return client, nil
case "insecure":
// Skip CA verification, go straight to InsecureSkipVerify
insecureTLS := &tls.Config{InsecureSkipVerify: true} //nolint:gosec // insecure mode requested
slog.Debug("connecting to RouterOS device (insecure TLS)", "address", sslAddr)
client, err := routeros.DialTLSTimeout(sslAddr, username, password, insecureTLS, timeout)
if err != nil {
return nil, fmt.Errorf("insecure TLS connection to %s failed: %w", sslAddr, err)
}
slog.Debug("connected with insecure TLS", "address", sslAddr)
return client, nil
case "portal_ca":
// Strict CA-verified TLS only
verifiedTLS := buildTLSConfig(caCertPEM)
if verifiedTLS.RootCAs == nil {
return nil, fmt.Errorf("portal_ca mode requires a valid CA cert but none available for %s", sslAddr)
}
slog.Debug("connecting to RouterOS device (CA-verified TLS)", "address", sslAddr)
client, err := routeros.DialTLSTimeout(sslAddr, username, password, verifiedTLS, timeout)
if err != nil {
return nil, fmt.Errorf("CA-verified TLS connection to %s failed: %w", sslAddr, err)
}
slog.Debug("connected with CA-verified TLS", "address", sslAddr)
return client, nil
default:
// "auto" mode: CA-verified -> InsecureSkipVerify -> STOP (no plain-text)
// Tier 1: CA-verified TLS (if CA cert available)
if len(caCertPEM) > 0 {
verifiedTLS := buildTLSConfig(caCertPEM)
if verifiedTLS.RootCAs != nil { // only try if PEM parsed OK
slog.Debug("connecting to RouterOS device (CA-verified TLS)", "address", sslAddr)
client, err := routeros.DialTLSTimeout(sslAddr, username, password, verifiedTLS, timeout)
if err == nil {
slog.Debug("connected with CA-verified TLS", "address", sslAddr)
return client, nil
}
slog.Debug("CA-verified TLS failed, trying insecure TLS", "address", sslAddr, "error", err)
}
}
// Tier 2: InsecureSkipVerify TLS (fallback)
insecureTLS := &tls.Config{InsecureSkipVerify: true} //nolint:gosec // fallback for unprovisioned devices
slog.Debug("connecting to RouterOS device (insecure TLS)", "address", sslAddr)
client, err := routeros.DialTLSTimeout(sslAddr, username, password, insecureTLS, timeout)
if err != nil {
// NO plain-text fallback in auto mode — this is the key security change
return nil, fmt.Errorf("TLS connection to %s failed (auto mode — no plain-text fallback): %w", sslAddr, err)
}
slog.Debug("connected with insecure TLS", "address", sslAddr)
return client, nil
}
}
// CloseDevice closes a RouterOS client connection. Safe to call on a nil client.
func CloseDevice(c *routeros.Client) {
if c == nil {
return
}
c.Close()
}