Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

basic callhome functionality #31

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
210 changes: 210 additions & 0 deletions callhome.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
package netconf

import (
"crypto/tls"
"errors"
"fmt"
"github.com/nemith/netconf/transport"
ncssh "github.com/nemith/netconf/transport/ssh"
nctls "github.com/nemith/netconf/transport/tls"
"golang.org/x/crypto/ssh"
"net"
)

var ErrNoClientConfig = errors.New("missing transport configuration")

// CallHomeTransport interface allows for upgrading an incoming callhome TCP connection into a transport
type CallHomeTransport interface {
DialWithConn(conn net.Conn) (transport.Transport, error)
}

// SSHCallHomeTransport implements the CallHomeTransport on SSH
type SSHCallHomeTransport struct {
Config *ssh.ClientConfig
}

// DialWithConn is same as Dial but creates the transport on top of input net.Conn
func (t *SSHCallHomeTransport) DialWithConn(conn net.Conn) (transport.Transport, error) {
sshConn, chans, reqs, err := ssh.NewClientConn(conn, conn.RemoteAddr().String(), t.Config)
if err != nil {
return nil, err
}
client := ssh.NewClient(sshConn, chans, reqs)
return ncssh.NewTransport(client)
}

// TLSCallHomeTransport implements the CallHomeTransport on TLS
type TLSCallHomeTransport struct {
Config *tls.Config
}

// DialWithConn is same as Dial but creates the transport on top of input net.Conn
func (t *TLSCallHomeTransport) DialWithConn(conn net.Conn) (transport.Transport, error) {
tlsConn := tls.Client(conn, t.Config)
return nctls.NewTransport(tlsConn), nil
}

/*
CallHomeClientConfig holds connecting callhome device information
*/
type CallHomeClientConfig struct {
Transport CallHomeTransport
Address string
}

type CallHomeClient struct {
session *Session
*CallHomeClientConfig
}

func (chc *CallHomeClient) Session() *Session {
return chc.session
}

type ClientError struct {
Address string
Err error
}

func (ce *ClientError) Error() string {
return fmt.Sprintf("client %s: %s", ce.Address, ce.Err.Error())
}

/*
CallHomeServer implements netconf callhome procedure as specified in RFC 8071
*/
type CallHomeServer struct {
listener net.Listener
network string
addr string
clientsConfig map[string]*CallHomeClientConfig
clientsChannel chan *CallHomeClient
errorChannel chan *ClientError
}

type CallHomeOption func(*CallHomeServer)

// WithAddress sets the address (as required by net.Listen) the CallHomeServer server listen to
func WithAddress(addr string) CallHomeOption {
return func(ch *CallHomeServer) {
ch.addr = addr
}
}

// WithNetwork set the network (as required by net.Listen) the CallHomeServer server listen to
func WithNetwork(network string) CallHomeOption {
return func(ch *CallHomeServer) {
ch.network = network
}
}

// WithCallHomeClientConfig set the netconf callhome clientsConfig
func WithCallHomeClientConfig(chc ...*CallHomeClientConfig) CallHomeOption {
return func(chs *CallHomeServer) {
for _, c := range chc {
chs.clientsConfig[c.Address] = c
}
}
}

// NewCallHomeServer creates a CallHomeServer
func NewCallHomeServer(opts ...CallHomeOption) (*CallHomeServer, error) {
const (
defaultAddress = "0.0.0.0:4334"
defaultNetwork = "tcp"
)

ch := &CallHomeServer{
addr: defaultAddress,
network: defaultNetwork,
clientsConfig: map[string]*CallHomeClientConfig{},
clientsChannel: make(chan *CallHomeClient),
errorChannel: make(chan *ClientError),
}

for _, opt := range opts {
opt(ch)
}

if ch.network != "tcp" && ch.network != "tcp4" && ch.network != "tcp6" {
return nil, fmt.Errorf("invalid network, must be one of: tcp, tcp4, tcp6")
}

return ch, nil
}

// Listen waits for incoming callhome connections and handles them.
// Send ClientError messages to the ErrChan whenever a callhome connection to a host fails and
// send a new CallHomeClient every time a callhome connection is successful
func (chs *CallHomeServer) Listen() error {
ln, err := net.Listen(chs.network, chs.addr)
if err != nil {
return err
}
chs.listener = ln
defer func() {
_ = chs.Close()
}()
for {
conn, err := chs.listener.Accept()
if err != nil {
return err
}
go func() {
chc, err := chs.handleConnection(conn)
if err != nil {
chs.errorChannel <- &ClientError{
Address: conn.RemoteAddr().String(),
Err: err,
}
} else {
chs.clientsChannel <- chc
}
}()
}
}

// handleConnection upgrade input net.Conn to establish a netconf session
func (chs *CallHomeServer) handleConnection(conn net.Conn) (*CallHomeClient, error) {
addr, ok := conn.RemoteAddr().(*net.TCPAddr)
if !ok {
return nil, errors.New("invalid network connection, callhome support tcp only")
}
chcc, ok := chs.clientsConfig[addr.IP.String()]
if !ok {
return nil, ErrNoClientConfig
}

t, err := chcc.Transport.DialWithConn(conn)
if err != nil {
return nil, err
}

s, err := Open(t)
if err != nil {
return nil, err
}

return &CallHomeClient{
session: s,
CallHomeClientConfig: chcc,
}, nil
}

// Close terminates the callhome server connection
func (chs *CallHomeServer) Close() error {
return chs.listener.Close()
}

func (chs *CallHomeServer) ErrorChannel() chan *ClientError {
return chs.errorChannel
}

func (chs *CallHomeServer) CallHomeClientChannel() chan *CallHomeClient {
return chs.clientsChannel
}

// SetCallHomeClientConfig adds a new callhome client configuration to the callhome server
func (chs *CallHomeServer) SetCallHomeClientConfig(chcc *CallHomeClientConfig) {
chs.clientsConfig[chcc.Address] = chcc
}
70 changes: 70 additions & 0 deletions examples/callhome_ssh/callhome_ssh.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package main

import (
"context"
"fmt"
"github.com/nemith/netconf"
"golang.org/x/crypto/ssh"
"log"
"os"
"os/signal"
"syscall"
"time"
)

func main() {
sigChannel := make(chan os.Signal, 1)
signal.Notify(sigChannel, os.Interrupt, syscall.SIGTERM)

chcList := []*netconf.CallHomeClientConfig{
{
Transport: &netconf.SSHCallHomeTransport{
Config: &ssh.ClientConfig{
User: "foo",
Auth: []ssh.AuthMethod{
ssh.Password("bar"),
},
// as specified in rfc8071 3.1 C5 netconf client must validate host keys
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
},
},
Address: "192.168.121.17",
},
}

chs, err := netconf.NewCallHomeServer(netconf.WithCallHomeClientConfig(chcList...), netconf.WithAddress("0.0.0.0:4339"))
if err != nil {
panic(err)
}
log.Printf("callhome server listening on: %s", "0.0.0.0:4339")
go func() {
err := chs.Listen()
if err != nil {
panic(err)
}
}()

go func() {
for {
select {
case e := <-chs.ErrorChannel():
fmt.Println(e.Error())
case chc := <-chs.CallHomeClientChannel():
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
deviceConfig, err := chc.Session().GetConfig(ctx, "running")
cancel()
if err != nil {
log.Fatalf("failed to get config: %v", err)
}
log.Printf("Config:\n%s\n", deviceConfig)
}
}
}()
select {
case <-sigChannel:
if err := chs.Close(); err != nil {
log.Print(err)
}
os.Exit(0)
}
}
4 changes: 2 additions & 2 deletions transport/ssh/ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ func newTransport(client *ssh.Client, owned bool) (*Transport, error) {
}, nil
}

// Close will close the underlying transport. If the connection was created
// with Dial then then underlying ssh.Client is closed as well. If not only
// Close will close the underlying transport. If the connection was created
// with Dial then underlying ssh.Client is closed as well. If not only
// the sessions is closed.
func (t *Transport) Close() error {
if err := t.sess.Close(); err != nil {
Expand Down