mirror of
https://github.com/gregtwallace/apc-p15-tool.git
synced 2025-07-25 16:12:56 +00:00
ssh: breakout ups ssh to its own package
This was done for clearer separation of function. A subsequent update will (hopefully) make the SSL command more robust so it works for both NMC2 and NMC3. The method for sending shell commands was also updated to use an interactive shell instead. This allows capturing responses of the commands which will be needed to deduce if devices are NMC2 or NMC3.
This commit is contained in:
parent
41efc56c62
commit
06c9263bc4
11 changed files with 444 additions and 284 deletions
pkg/apcssh
134
pkg/apcssh/client.go
Normal file
134
pkg/apcssh/client.go
Normal file
|
@ -0,0 +1,134 @@
|
|||
package apcssh
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
const (
|
||||
apcSSHVer = 1
|
||||
|
||||
sshTimeout = 90 * time.Second
|
||||
)
|
||||
|
||||
// APC UPS won't except Go's SSH "Run()" command as the format isn't quite
|
||||
// the same. Therefore, write a custom implementation instead of relying on
|
||||
// something like github.com/bramvdbogaerde/go-scp
|
||||
|
||||
type Config struct {
|
||||
Hostname string
|
||||
Username string
|
||||
Password string
|
||||
ServerFingerprint string
|
||||
InsecureCipher bool
|
||||
}
|
||||
|
||||
// Client is an APC UPS SSH client
|
||||
type Client struct {
|
||||
hostname string
|
||||
sshCfg *ssh.ClientConfig
|
||||
}
|
||||
|
||||
// New creates a new SSH Client for the APC UPS.
|
||||
func New(cfg *Config) (*Client, error) {
|
||||
// make host key callback
|
||||
hk := func(_hostname string, _remote net.Addr, key ssh.PublicKey) error {
|
||||
// calculate server's key's SHA256
|
||||
hasher := sha256.New()
|
||||
_, err := hasher.Write(key.Marshal())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
actualHash := hasher.Sum(nil)
|
||||
|
||||
// log fingerprint for debugging
|
||||
actualHashB64 := base64.RawStdEncoding.EncodeToString(actualHash)
|
||||
actualHashHex := hex.EncodeToString(actualHash)
|
||||
|
||||
// check for fingerprint match (b64 or hex)
|
||||
if actualHashB64 != cfg.ServerFingerprint && actualHashHex != cfg.ServerFingerprint {
|
||||
log.Printf("apcssh: remote server key fingerprint (b64): %s", actualHashB64)
|
||||
log.Printf("apcssh: remote server key fingerprint (hex): %s", actualHashHex)
|
||||
|
||||
return errors.New("apcssh: fingerprint didn't match")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// kex algos
|
||||
// see defaults: https://cs.opensource.google/go/x/crypto/+/refs/tags/v0.18.0:ssh/common.go;l=62
|
||||
kexAlgos := []string{
|
||||
"curve25519-sha256", "curve25519-sha256@libssh.org",
|
||||
"ecdh-sha2-nistp256", "ecdh-sha2-nistp384", "ecdh-sha2-nistp521",
|
||||
"diffie-hellman-group14-sha256", "diffie-hellman-group14-sha1",
|
||||
}
|
||||
// extra for some apc ups
|
||||
kexAlgos = append(kexAlgos, "diffie-hellman-group-exchange-sha256")
|
||||
|
||||
// ciphers
|
||||
// see defaults: https://cs.opensource.google/go/x/crypto/+/master:ssh/common.go;l=37
|
||||
ciphers := []string{
|
||||
"aes128-gcm@openssh.com", "aes256-gcm@openssh.com",
|
||||
"chacha20-poly1305@openssh.com",
|
||||
"aes128-ctr", "aes192-ctr", "aes256-ctr",
|
||||
}
|
||||
|
||||
// insecure cipher options?
|
||||
if cfg.InsecureCipher {
|
||||
log.Println("WARNING: insecure ciphers are enabled (--insecurecipher). SSH with an insecure cipher is NOT secure and should NOT be used.")
|
||||
ciphers = append(ciphers, "aes128-cbc", "3des-cbc")
|
||||
}
|
||||
|
||||
// install file on UPS
|
||||
// ssh config
|
||||
config := &ssh.ClientConfig{
|
||||
User: cfg.Username,
|
||||
Auth: []ssh.AuthMethod{
|
||||
ssh.Password(cfg.Password),
|
||||
},
|
||||
// APC seems to require `Client Version` string to start with "SSH-2" and must be at least
|
||||
// 13 characters long
|
||||
// working examples from other clients:
|
||||
// ClientVersion: "SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.6",
|
||||
// ClientVersion: "SSH-2.0-PuTTY_Release_0.80",
|
||||
ClientVersion: fmt.Sprintf("SSH-2.0-apcssh_v%d %s-%s", apcSSHVer, runtime.GOOS, runtime.GOARCH),
|
||||
Config: ssh.Config{
|
||||
KeyExchanges: kexAlgos,
|
||||
Ciphers: ciphers,
|
||||
},
|
||||
HostKeyCallback: hk,
|
||||
|
||||
// reasonable timeout for file copy
|
||||
Timeout: sshTimeout,
|
||||
}
|
||||
|
||||
// if hostname missing a port, add default
|
||||
if !strings.Contains(cfg.Hostname, ":") {
|
||||
cfg.Hostname = cfg.Hostname + ":22"
|
||||
}
|
||||
|
||||
// connect to ups over SSH (to verify everything works)
|
||||
sshClient, err := ssh.Dial("tcp", cfg.Hostname, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = sshClient.Close()
|
||||
|
||||
// return Client (note: new ssh Dial will be done for each action as the UPS
|
||||
// seems to not do well with more than one Session per Dial)
|
||||
return &Client{
|
||||
hostname: cfg.Hostname,
|
||||
sshCfg: config,
|
||||
}, nil
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue