Move to yaml.v3 and mergo. Refactor config parsing into a package. Refactor SSH connections into a package. Create default YAML directory, and update docs
This commit is contained in:
250
src/ssh.go
Normal file
250
src/ssh.go
Normal file
@ -0,0 +1,250 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/kevinburke/ssh_config"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"golang.org/x/crypto/ssh/agent"
|
||||
)
|
||||
|
||||
// RouterBackup handles SSH connections and command execution
|
||||
type RouterBackup struct {
|
||||
hostname string
|
||||
username string
|
||||
password string
|
||||
keyFile string
|
||||
port int
|
||||
client *ssh.Client
|
||||
}
|
||||
|
||||
// NewRouterBackup creates a new RouterBackup instance
|
||||
func NewRouterBackup(hostname, username, password, keyFile string, port int) *RouterBackup {
|
||||
return &RouterBackup{
|
||||
hostname: hostname,
|
||||
username: username,
|
||||
password: password,
|
||||
keyFile: keyFile,
|
||||
port: port,
|
||||
}
|
||||
}
|
||||
|
||||
// Connect establishes SSH connection to the router
|
||||
func (rb *RouterBackup) Connect() error {
|
||||
// Get SSH config values for this host
|
||||
hostname := ssh_config.Get(rb.hostname, "Hostname")
|
||||
if hostname == "" {
|
||||
hostname = rb.hostname
|
||||
}
|
||||
|
||||
portStr := ssh_config.Get(rb.hostname, "Port")
|
||||
port := rb.port
|
||||
if portStr != "" {
|
||||
if p, err := strconv.Atoi(portStr); err == nil {
|
||||
port = p
|
||||
}
|
||||
}
|
||||
|
||||
username := ssh_config.Get(rb.hostname, "User")
|
||||
if rb.username != "" {
|
||||
username = rb.username
|
||||
}
|
||||
|
||||
keyFile := ssh_config.Get(rb.hostname, "IdentityFile")
|
||||
if rb.keyFile != "" {
|
||||
keyFile = rb.keyFile
|
||||
}
|
||||
|
||||
config := &ssh.ClientConfig{
|
||||
User: username,
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
// Apply SSH config crypto settings with compatibility filtering
|
||||
if kexAlgorithms := ssh_config.Get(rb.hostname, "KexAlgorithms"); kexAlgorithms != "" && !strings.HasPrefix(kexAlgorithms, "+") {
|
||||
// Only apply if it's an explicit list, not a +append
|
||||
algorithms := strings.Split(kexAlgorithms, ",")
|
||||
var finalAlgorithms []string
|
||||
for _, alg := range algorithms {
|
||||
finalAlgorithms = append(finalAlgorithms, strings.TrimSpace(alg))
|
||||
}
|
||||
config.KeyExchanges = finalAlgorithms
|
||||
}
|
||||
|
||||
// Note: Cipher overrides disabled - Go SSH library defaults work better
|
||||
// if ciphers := ssh_config.Get(rb.hostname, "Ciphers"); ciphers != "" {
|
||||
// config.Ciphers = ...
|
||||
// }
|
||||
|
||||
if macs := ssh_config.Get(rb.hostname, "MACs"); macs != "" {
|
||||
macList := strings.Split(macs, ",")
|
||||
for i, mac := range macList {
|
||||
macList[i] = strings.TrimSpace(mac)
|
||||
}
|
||||
config.MACs = macList
|
||||
}
|
||||
|
||||
if hostKeyAlgorithms := ssh_config.Get(rb.hostname, "HostKeyAlgorithms"); hostKeyAlgorithms != "" && !strings.HasPrefix(hostKeyAlgorithms, "+") {
|
||||
// Only apply if it's an explicit list, not a +append
|
||||
algorithms := strings.Split(hostKeyAlgorithms, ",")
|
||||
var finalAlgorithms []string
|
||||
for _, alg := range algorithms {
|
||||
finalAlgorithms = append(finalAlgorithms, strings.TrimSpace(alg))
|
||||
}
|
||||
config.HostKeyAlgorithms = finalAlgorithms
|
||||
}
|
||||
|
||||
// Try SSH agent first if available
|
||||
if sshAuthSock := os.Getenv("SSH_AUTH_SOCK"); sshAuthSock != "" {
|
||||
if conn, err := net.Dial("unix", sshAuthSock); err == nil {
|
||||
agentClient := agent.NewClient(conn)
|
||||
config.Auth = []ssh.AuthMethod{ssh.PublicKeysCallback(agentClient.Signers)}
|
||||
}
|
||||
}
|
||||
|
||||
// If SSH agent didn't work, try key file
|
||||
if len(config.Auth) == 0 && keyFile != "" {
|
||||
// Expand ~ in keyFile path
|
||||
if strings.HasPrefix(keyFile, "~/") {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err == nil {
|
||||
keyFile = filepath.Join(homeDir, keyFile[2:])
|
||||
}
|
||||
}
|
||||
|
||||
key, err := ioutil.ReadFile(keyFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read private key: %v", err)
|
||||
}
|
||||
|
||||
signer, err := ssh.ParsePrivateKey(key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse private key: %v", err)
|
||||
}
|
||||
|
||||
config.Auth = []ssh.AuthMethod{ssh.PublicKeys(signer)}
|
||||
}
|
||||
|
||||
// Fall back to password if available
|
||||
if len(config.Auth) == 0 && rb.password != "" {
|
||||
config.Auth = []ssh.AuthMethod{ssh.Password(rb.password)}
|
||||
}
|
||||
|
||||
if len(config.Auth) == 0 {
|
||||
return fmt.Errorf("no authentication method available")
|
||||
}
|
||||
|
||||
address := fmt.Sprintf("%s:%d", hostname, port)
|
||||
client, err := ssh.Dial("tcp4", address, config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to %s: %v", hostname, err)
|
||||
}
|
||||
|
||||
rb.client = client
|
||||
fmt.Printf("Successfully connected to %s\n", hostname)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RunCommand executes a command on the router and returns the output
|
||||
func (rb *RouterBackup) RunCommand(command string) (string, error) {
|
||||
if rb.client == nil {
|
||||
return "", fmt.Errorf("no active connection")
|
||||
}
|
||||
|
||||
session, err := rb.client.NewSession()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create session: %v", err)
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
output, err := session.CombinedOutput(command)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to execute command '%s': %v", command, err)
|
||||
}
|
||||
|
||||
return string(output), nil
|
||||
}
|
||||
|
||||
// BackupCommands runs multiple commands and saves outputs to files
|
||||
func (rb *RouterBackup) BackupCommands(commands []string, outputDir string) error {
|
||||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create directory %s: %v", outputDir, err)
|
||||
}
|
||||
|
||||
filename := rb.hostname
|
||||
filepath := filepath.Join(outputDir, filename)
|
||||
|
||||
// Truncate file at start
|
||||
file, err := os.Create(filepath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create file %s: %v", filepath, err)
|
||||
}
|
||||
file.Close()
|
||||
|
||||
successCount := 0
|
||||
for i, command := range commands {
|
||||
fmt.Printf("Running command %d/%d: %s\n", i+1, len(commands), command)
|
||||
output, err := rb.RunCommand(command)
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Error executing '%s': %v\n", command, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Append to file
|
||||
file, err := os.OpenFile(filepath, os.O_APPEND|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to open file for writing: %v\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Fprintf(file, "## COMMAND: %s\n", command)
|
||||
file.WriteString(output)
|
||||
file.Close()
|
||||
|
||||
fmt.Printf("Output saved to %s\n", filepath)
|
||||
successCount++
|
||||
}
|
||||
|
||||
fmt.Printf("Summary: %d/%d commands successful\n", successCount, len(commands))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Disconnect closes SSH connection
|
||||
func (rb *RouterBackup) Disconnect() {
|
||||
if rb.client != nil {
|
||||
rb.client.Close()
|
||||
fmt.Printf("Disconnected from %s\n", rb.hostname)
|
||||
}
|
||||
}
|
||||
|
||||
// findDefaultSSHKey looks for default SSH keys
|
||||
func findDefaultSSHKey() string {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
defaultKeys := []string{
|
||||
filepath.Join(homeDir, ".ssh", "id_rsa"),
|
||||
filepath.Join(homeDir, ".ssh", "id_ed25519"),
|
||||
filepath.Join(homeDir, ".ssh", "id_ecdsa"),
|
||||
}
|
||||
|
||||
for _, keyPath := range defaultKeys {
|
||||
if _, err := os.Stat(keyPath); err == nil {
|
||||
fmt.Printf("Using SSH key: %s\n", keyPath)
|
||||
return keyPath
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
Reference in New Issue
Block a user