ci: add GitHub Pages deployment workflow for docs site Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
116 lines
4.5 KiB
Go
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()
|
|
}
|