You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
832 lines
26 KiB
832 lines
26 KiB
package portservice
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"crypto/ed25519"
|
|
"crypto/rand"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"golang.org/x/crypto/ssh"
|
|
"golang.org/x/crypto/ssh/knownhosts"
|
|
|
|
"tera-clone/services/tera-api/internal/domain"
|
|
)
|
|
|
|
type pipeFactory struct{ peers chan net.Conn }
|
|
|
|
func (f *pipeFactory) Open(context.Context, string) (io.ReadWriteCloser, error) {
|
|
left, right := net.Pipe()
|
|
f.peers <- right
|
|
return left, nil
|
|
}
|
|
|
|
type channelPipeFactory struct {
|
|
mu sync.Mutex
|
|
peers map[string]chan net.Conn
|
|
}
|
|
|
|
func (f *channelPipeFactory) Open(_ context.Context, channel string) (io.ReadWriteCloser, error) {
|
|
left, right := net.Pipe()
|
|
f.mu.Lock()
|
|
peers := f.peers[channel]
|
|
if peers == nil {
|
|
peers = make(chan net.Conn, 1)
|
|
f.peers[channel] = peers
|
|
}
|
|
f.mu.Unlock()
|
|
peers <- right
|
|
return left, nil
|
|
}
|
|
|
|
func (f *channelPipeFactory) peer(t *testing.T, channel string) net.Conn {
|
|
t.Helper()
|
|
deadline := time.Now().Add(time.Second)
|
|
for {
|
|
f.mu.Lock()
|
|
peers := f.peers[channel]
|
|
f.mu.Unlock()
|
|
if peers != nil {
|
|
select {
|
|
case peer := <-peers:
|
|
return peer
|
|
case <-time.After(time.Until(deadline)):
|
|
t.Fatalf("timed out waiting for %s transport", channel)
|
|
}
|
|
}
|
|
if time.Now().After(deadline) {
|
|
t.Fatalf("timed out waiting for %s transport", channel)
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
}
|
|
|
|
type eventRecorder struct {
|
|
mu sync.Mutex
|
|
events []domain.PortLogEvent
|
|
}
|
|
|
|
func (r *eventRecorder) record(_ context.Context, event domain.PortLogEvent) error {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
r.events = append(r.events, event)
|
|
return nil
|
|
}
|
|
|
|
func (r *eventRecorder) snapshot() []domain.PortLogEvent {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
return append([]domain.PortLogEvent(nil), r.events...)
|
|
}
|
|
|
|
func TestReverseTelnetBridgesSerialWritesWelcomeAndRecordsSource(t *testing.T) {
|
|
factory := &pipeFactory{peers: make(chan net.Conn, 1)}
|
|
recorder := &eventRecorder{}
|
|
listeners := make(chan net.Listener, 1)
|
|
gateway, err := New(Options{
|
|
TransportFactory: factory,
|
|
Record: recorder.record,
|
|
Listen: func(_, _ string) (net.Listener, error) {
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err == nil {
|
|
listeners <- listener
|
|
}
|
|
return listener, err
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer gateway.Close()
|
|
port := domain.Port{Channel: "CH12", Operation1: domain.PortOperation{
|
|
Mode: "reverse_telnet", Port: 2012, MaxClients: 2, Authentication: "none", Newline: "crlf",
|
|
}}
|
|
if err := gateway.Apply(context.Background(), []domain.Port{port}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
listener := <-listeners
|
|
connection, err := net.Dial("tcp", listener.Addr().String())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer connection.Close()
|
|
reader := bufio.NewReader(connection)
|
|
peer := <-factory.peers
|
|
defer peer.Close()
|
|
_ = connection.SetReadDeadline(time.Now().Add(time.Second))
|
|
welcome, err := reader.ReadString('\n')
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for !strings.Contains(welcome, "Connected at:") {
|
|
line, readErr := reader.ReadString('\n')
|
|
if readErr != nil {
|
|
t.Fatal(readErr)
|
|
}
|
|
welcome += line
|
|
}
|
|
for _, expected := range []string{"Welcome to TERA CORA-X48 PRO K", "Serial channel: CH12", "Operation: 1", "Protocol: reverse_telnet", "Connected from: 127.0.0.1:"} {
|
|
if !strings.Contains(welcome, expected) {
|
|
t.Fatalf("welcome = %q, missing %q", welcome, expected)
|
|
}
|
|
}
|
|
writeDone := make(chan error, 1)
|
|
go func() { _, err := connection.Write([]byte("show version\n")); writeDone <- err }()
|
|
serialInput := make([]byte, len("show version\r\n"))
|
|
_ = peer.SetReadDeadline(time.Now().Add(time.Second))
|
|
if _, err := io.ReadFull(peer, serialInput); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := <-writeDone; err != nil || string(serialInput) != "show version\r\n" {
|
|
t.Fatalf("serial input = %q, write error = %v", serialInput, err)
|
|
}
|
|
responseDone := make(chan error, 1)
|
|
go func() { _, err := peer.Write([]byte("Cisco IOS\r\n")); responseDone <- err }()
|
|
line, err := reader.ReadString('\n')
|
|
if err != nil || line != "\r\n" {
|
|
// The welcome frame ends in a blank line; consume it before data.
|
|
t.Fatalf("welcome terminator = %q, %v", line, err)
|
|
}
|
|
line, err = reader.ReadString('\n')
|
|
if err != nil || line != "Cisco IOS\r\n" {
|
|
t.Fatalf("serial output = %q, %v", line, err)
|
|
}
|
|
if err := <-responseDone; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
events := recorder.snapshot()
|
|
if len(events) == 0 || events[0].Channel != "CH12" || events[0].RemoteAddress != "127.0.0.1" || !strings.Contains(events[0].Message, "Serial channel: CH12") {
|
|
t.Fatalf("session events = %#v", events)
|
|
}
|
|
if events[0].EventType != "session" || events[0].Direction != "" || events[0].ShowDirection {
|
|
t.Fatalf("invalid session log schema = %#v", events[0])
|
|
}
|
|
}
|
|
|
|
func TestReverseTelnetKeepsSessionOpenWhenLogStoreRejectsEvent(t *testing.T) {
|
|
factory := &pipeFactory{peers: make(chan net.Conn, 1)}
|
|
listeners := make(chan net.Listener, 1)
|
|
gateway, err := New(Options{
|
|
TransportFactory: factory,
|
|
Record: func(context.Context, domain.PortLogEvent) error {
|
|
return fmt.Errorf("log store unavailable")
|
|
},
|
|
Listen: func(_, _ string) (net.Listener, error) {
|
|
listener, listenErr := net.Listen("tcp", "127.0.0.1:0")
|
|
if listenErr == nil {
|
|
listeners <- listener
|
|
}
|
|
return listener, listenErr
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer gateway.Close()
|
|
port := domain.Port{Channel: "CH13", Operation1: domain.PortOperation{
|
|
Mode: "reverse_telnet", Port: 2013, MaxClients: 1, Authentication: "none", Newline: "cr",
|
|
}}
|
|
if err := gateway.Apply(context.Background(), []domain.Port{port}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
connection, err := net.Dial("tcp", (<-listeners).Addr().String())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer connection.Close()
|
|
defer (<-factory.peers).Close()
|
|
_ = connection.SetReadDeadline(time.Now().Add(time.Second))
|
|
welcome := ""
|
|
reader := bufio.NewReader(connection)
|
|
for !strings.Contains(welcome, "Connected at:") {
|
|
line, readErr := reader.ReadString('\n')
|
|
if readErr != nil {
|
|
t.Fatalf("session closed because logging failed: %v", readErr)
|
|
}
|
|
welcome += line
|
|
}
|
|
}
|
|
|
|
func TestApplyKeepsCH01SessionWhenOnlyCH18ListenerChanges(t *testing.T) {
|
|
factory := &channelPipeFactory{peers: make(map[string]chan net.Conn)}
|
|
var listenerMu sync.Mutex
|
|
listeners := make(map[string]net.Listener)
|
|
listenCounts := make(map[string]int)
|
|
gateway, err := New(Options{
|
|
TransportFactory: factory,
|
|
Listen: func(_, address string) (net.Listener, error) {
|
|
listener, listenErr := net.Listen("tcp", "127.0.0.1:0")
|
|
if listenErr != nil {
|
|
return nil, listenErr
|
|
}
|
|
_, port, splitErr := net.SplitHostPort(address)
|
|
if splitErr != nil {
|
|
_ = listener.Close()
|
|
return nil, splitErr
|
|
}
|
|
listenerMu.Lock()
|
|
listeners[port] = listener
|
|
listenCounts[port]++
|
|
listenerMu.Unlock()
|
|
return listener, nil
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer gateway.Close()
|
|
ports := []domain.Port{
|
|
{Channel: "CH01", Operation1: domain.PortOperation{Mode: "reverse_telnet", Port: 10001, MaxClients: 1, Authentication: "none", Newline: "cr"}},
|
|
{Channel: "CH18", Operation1: domain.PortOperation{Mode: "reverse_telnet", Port: 2534, MaxClients: 1, Authentication: "none", Newline: "cr"}},
|
|
}
|
|
if err := gateway.Apply(context.Background(), ports); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
gateway.mu.Lock()
|
|
ch01Before := gateway.services["CH01-operation-1"]
|
|
ch18Before := gateway.services["CH18-operation-1"]
|
|
gateway.mu.Unlock()
|
|
listenerMu.Lock()
|
|
ch01Listener := listeners["10001"]
|
|
listenerMu.Unlock()
|
|
connection, err := net.Dial("tcp", ch01Listener.Addr().String())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer connection.Close()
|
|
peer := factory.peer(t, "CH01")
|
|
defer peer.Close()
|
|
reader := bufio.NewReader(connection)
|
|
_ = connection.SetReadDeadline(time.Now().Add(time.Second))
|
|
welcome := ""
|
|
for !strings.Contains(welcome, "\r\n\r\n") {
|
|
line, readErr := reader.ReadString('\n')
|
|
if readErr != nil {
|
|
t.Fatalf("read CH01 welcome: %v", readErr)
|
|
}
|
|
welcome += line
|
|
}
|
|
ports[1].Operation1.Port = 2535
|
|
if err := gateway.Apply(context.Background(), ports); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
gateway.mu.Lock()
|
|
ch01After := gateway.services["CH01-operation-1"]
|
|
ch18After := gateway.services["CH18-operation-1"]
|
|
gateway.mu.Unlock()
|
|
if ch01After != ch01Before {
|
|
t.Fatal("unchanged CH01 service was restarted")
|
|
}
|
|
if ch18After == ch18Before {
|
|
t.Fatal("changed CH18 service was not restarted")
|
|
}
|
|
listenerMu.Lock()
|
|
ch01ListenCount := listenCounts["10001"]
|
|
ch18OldListenCount := listenCounts["2534"]
|
|
ch18NewListenCount := listenCounts["2535"]
|
|
listenerMu.Unlock()
|
|
if ch01ListenCount != 1 || ch18OldListenCount != 1 || ch18NewListenCount != 1 {
|
|
t.Fatalf("listener starts: CH01=%d CH18-old=%d CH18-new=%d", ch01ListenCount, ch18OldListenCount, ch18NewListenCount)
|
|
}
|
|
writeDone := make(chan error, 1)
|
|
go func() {
|
|
_, writeErr := peer.Write([]byte("session-still-active\r\n"))
|
|
writeDone <- writeErr
|
|
}()
|
|
line, err := reader.ReadString('\n')
|
|
if err != nil || line != "session-still-active\r\n" {
|
|
t.Fatalf("CH01 session after CH18 change = %q, %v", line, err)
|
|
}
|
|
if err := <-writeDone; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestReverseTelnetKeepsFourSharedSessionsBidirectionalAndHandlesShortcuts(t *testing.T) {
|
|
factory := &pipeFactory{peers: make(chan net.Conn, 1)}
|
|
listeners := make(chan net.Listener, 1)
|
|
gateway, err := New(Options{
|
|
TransportFactory: factory,
|
|
Listen: func(_, _ string) (net.Listener, error) {
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err == nil {
|
|
listeners <- listener
|
|
}
|
|
return listener, err
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer gateway.Close()
|
|
port := domain.Port{Channel: "CH24", Operation1: domain.PortOperation{
|
|
Mode: "reverse_telnet", Port: 2024, MaxClients: 4, Authentication: "none", Newline: "cr",
|
|
ControlShortcut: "^]", SwitchShortcut: "^T", QuitShortcut: "^Q",
|
|
}}
|
|
if err := gateway.Apply(context.Background(), []domain.Port{port}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
listener := <-listeners
|
|
connections := make([]net.Conn, 0, 4)
|
|
readers := make([]*bufio.Reader, 0, 4)
|
|
for index := 0; index < 4; index++ {
|
|
connection, dialErr := net.Dial("tcp", listener.Addr().String())
|
|
if dialErr != nil {
|
|
t.Fatal(dialErr)
|
|
}
|
|
connections = append(connections, connection)
|
|
reader := bufio.NewReader(connection)
|
|
readers = append(readers, reader)
|
|
_ = connection.SetReadDeadline(time.Now().Add(time.Second))
|
|
welcome := ""
|
|
for !strings.Contains(welcome, "\r\n\r\n") {
|
|
chunk, readErr := reader.ReadString('\n')
|
|
if readErr != nil {
|
|
t.Fatalf("read welcome %d: %v", index+1, readErr)
|
|
}
|
|
welcome += chunk
|
|
}
|
|
if !strings.Contains(welcome, "Access: shared writer") || !strings.Contains(welcome, "control ^]") {
|
|
t.Fatalf("welcome %d = %q", index+1, welcome)
|
|
}
|
|
_ = connection.SetReadDeadline(time.Time{})
|
|
}
|
|
defer func() {
|
|
for _, connection := range connections {
|
|
_ = connection.Close()
|
|
}
|
|
}()
|
|
peer := <-factory.peers
|
|
defer peer.Close()
|
|
|
|
for index, connection := range connections {
|
|
command := fmt.Sprintf("show-%d\n", index+1)
|
|
go connection.Write([]byte(command))
|
|
want := fmt.Sprintf("show-%d\r", index+1)
|
|
payload := make([]byte, len(want))
|
|
_ = peer.SetReadDeadline(time.Now().Add(time.Second))
|
|
if _, err := io.ReadFull(peer, payload); err != nil || string(payload) != want {
|
|
t.Fatalf("shared command %d = %q, %v", index+1, payload, err)
|
|
}
|
|
}
|
|
|
|
go peer.Write([]byte("fanout\r\n"))
|
|
for index, reader := range readers {
|
|
connections[index].SetReadDeadline(time.Now().Add(time.Second))
|
|
line, readErr := reader.ReadString('\n')
|
|
if readErr != nil || line != "fanout\r\n" {
|
|
t.Fatalf("fanout %d = %q, %v", index+1, line, readErr)
|
|
}
|
|
connections[index].SetReadDeadline(time.Time{})
|
|
}
|
|
|
|
if _, err := connections[0].Write([]byte{0x14}); err != nil { // Ctrl+T
|
|
t.Fatal(err)
|
|
}
|
|
connections[0].SetReadDeadline(time.Now().Add(time.Second))
|
|
status, err := readUntilContains(readers[0], "all connected terminal sessions remain online")
|
|
if err != nil || !strings.Contains(status, "all connected terminal sessions remain online") {
|
|
t.Fatalf("Ctrl+T status = %q, %v", status, err)
|
|
}
|
|
if _, err := connections[0].Write([]byte{0x11}); err != nil { // Ctrl+Q
|
|
t.Fatal(err)
|
|
}
|
|
closed, err := readUntilContains(readers[0], "current session closed")
|
|
if err != nil || !strings.Contains(closed, "current session closed") {
|
|
t.Fatalf("Ctrl+Q response = %q, %v", closed, err)
|
|
}
|
|
}
|
|
|
|
func readUntilContains(reader *bufio.Reader, expected string) (string, error) {
|
|
var result string
|
|
for !strings.Contains(result, expected) {
|
|
line, err := reader.ReadString('\n')
|
|
result += line
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func TestReverseSSHUsesLocalAuthenticationAndBridgesSession(t *testing.T) {
|
|
_, privateKey, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
signer, err := ssh.NewSignerFromKey(privateKey)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
factory := &pipeFactory{peers: make(chan net.Conn, 1)}
|
|
recorder := &eventRecorder{}
|
|
listeners := make(chan net.Listener, 1)
|
|
gateway, err := New(Options{
|
|
TransportFactory: factory, SSHSigner: signer, Record: recorder.record,
|
|
Authenticate: func(_ context.Context, username, password, channel string) (string, error) {
|
|
if username != "operator" || password != "secret" || channel != "CH01" {
|
|
return "", io.EOF
|
|
}
|
|
return "operator", nil
|
|
},
|
|
Listen: func(_, _ string) (net.Listener, error) {
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err == nil {
|
|
listeners <- listener
|
|
}
|
|
return listener, err
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer gateway.Close()
|
|
port := domain.Port{Channel: "CH01", Operation2: domain.PortOperation{
|
|
Mode: "reverse_ssh", Port: 2201, MaxClients: 1, Authentication: "local", Newline: "cr",
|
|
}}
|
|
if err := gateway.Apply(context.Background(), []domain.Port{port}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
listener := <-listeners
|
|
client, err := ssh.Dial("tcp", listener.Addr().String(), &ssh.ClientConfig{
|
|
User: "operator", Auth: []ssh.AuthMethod{ssh.Password("secret")},
|
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(), Timeout: time.Second,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer client.Close()
|
|
session, err := client.NewSession()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer session.Close()
|
|
input, _ := session.StdinPipe()
|
|
output, _ := session.StdoutPipe()
|
|
if err := session.Shell(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
peer := <-factory.peers
|
|
defer peer.Close()
|
|
reader := bufio.NewReader(output)
|
|
welcome := ""
|
|
for !strings.Contains(welcome, "Connected at:") {
|
|
line, readErr := reader.ReadString('\n')
|
|
if readErr != nil {
|
|
t.Fatal(readErr)
|
|
}
|
|
welcome += line
|
|
}
|
|
if !strings.Contains(welcome, "Serial channel: CH01") || !strings.Contains(welcome, "User: operator") || !strings.Contains(welcome, "Protocol: reverse_ssh") {
|
|
t.Fatalf("SSH welcome = %q", welcome)
|
|
}
|
|
go func() { _, _ = input.Write([]byte("show\n")) }()
|
|
serialInput := make([]byte, len("show\r"))
|
|
_ = peer.SetReadDeadline(time.Now().Add(time.Second))
|
|
if _, err := io.ReadFull(peer, serialInput); err != nil || string(serialInput) != "show\r" {
|
|
t.Fatalf("SSH serial input = %q, %v", serialInput, err)
|
|
}
|
|
events := recorder.snapshot()
|
|
if len(events) == 0 || events[0].EventType != "session" || events[0].Direction != "" || events[0].ShowDirection {
|
|
t.Fatalf("invalid SSH session log schema = %#v", events)
|
|
}
|
|
}
|
|
|
|
func TestRecordFailureUsesQueryableCommandEvent(t *testing.T) {
|
|
recorder := &eventRecorder{}
|
|
gateway := &Gateway{options: Options{Record: recorder.record}}
|
|
gateway.recordFailure(serviceConfig{
|
|
channel: "CH18",
|
|
key: "CH18-operation-1",
|
|
operation: domain.PortOperation{
|
|
Mode: "reverse_ssh", Host: "0.0.0.0", Port: 2534,
|
|
},
|
|
}, fmt.Errorf("listener failed"))
|
|
events := recorder.snapshot()
|
|
if len(events) != 1 {
|
|
t.Fatalf("failure events = %#v", events)
|
|
}
|
|
event := events[0]
|
|
if event.EventType != "command" || event.Direction != "" || event.Command != "CH18-operation-1" || event.ReturnValue != "listener failed" {
|
|
t.Fatalf("invalid failure log schema = %#v", event)
|
|
}
|
|
}
|
|
|
|
func TestReverseTelnetUsesProtectedStaticPassword(t *testing.T) {
|
|
factory := &pipeFactory{peers: make(chan net.Conn, 1)}
|
|
listeners := make(chan net.Listener, 1)
|
|
gateway, err := New(Options{
|
|
TransportFactory: factory,
|
|
Listen: func(_, _ string) (net.Listener, error) {
|
|
listener, listenErr := net.Listen("tcp", "127.0.0.1:0")
|
|
if listenErr == nil {
|
|
listeners <- listener
|
|
}
|
|
return listener, listenErr
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer gateway.Close()
|
|
port := domain.Port{Channel: "CH02", Operation1: domain.PortOperation{
|
|
Mode: "reverse_telnet", Port: 2002, MaxClients: 1, Authentication: "password",
|
|
Username: "operator", PasswordStored: true, Credential: "static-secret", Newline: "cr",
|
|
}}
|
|
if err := gateway.Apply(context.Background(), []domain.Port{port}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
listener := <-listeners
|
|
connection, err := net.Dial("tcp", listener.Addr().String())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer connection.Close()
|
|
reader := bufio.NewReader(connection)
|
|
_ = connection.SetDeadline(time.Now().Add(time.Second))
|
|
if prompt, err := reader.ReadString(':'); err != nil || !strings.Contains(prompt, "Username:") {
|
|
t.Fatalf("username prompt = %q, %v", prompt, err)
|
|
}
|
|
_, _ = connection.Write([]byte("operator\r\n"))
|
|
if prompt, err := reader.ReadString(':'); err != nil || !strings.Contains(prompt, "Password:") {
|
|
t.Fatalf("password prompt = %q, %v", prompt, err)
|
|
}
|
|
_, _ = connection.Write([]byte("static-secret\r\n"))
|
|
welcome := ""
|
|
for !strings.Contains(welcome, "Connected at:") {
|
|
line, readErr := reader.ReadString('\n')
|
|
if readErr != nil {
|
|
t.Fatal(readErr)
|
|
}
|
|
welcome += line
|
|
}
|
|
if !strings.Contains(welcome, "User: operator") {
|
|
t.Fatalf("welcome = %q", welcome)
|
|
}
|
|
peer := <-factory.peers
|
|
_ = peer.Close()
|
|
}
|
|
|
|
func TestNormalTelnetAutoLoginAndBidirectionalBridge(t *testing.T) {
|
|
factory := &pipeFactory{peers: make(chan net.Conn, 1)}
|
|
remotePeers := make(chan net.Conn, 1)
|
|
gateway, err := New(Options{
|
|
TransportFactory: factory,
|
|
RetryDelay: time.Second,
|
|
Dial: func(_ context.Context, network, address string) (net.Conn, error) {
|
|
if network != "tcp" || address != "192.0.2.10:23" {
|
|
t.Fatalf("Dial(%q, %q)", network, address)
|
|
}
|
|
left, right := net.Pipe()
|
|
remotePeers <- right
|
|
return left, nil
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer gateway.Close()
|
|
port := domain.Port{Channel: "CH04", Operation1: domain.PortOperation{
|
|
Mode: "telnet", Host: "192.0.2.10", Port: 23, MaxClients: 1,
|
|
Authentication: "password", Username: "cisco", PasswordStored: true, Credential: "enable-secret", Newline: "crlf",
|
|
}}
|
|
if err := gateway.Apply(context.Background(), []domain.Port{port}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
remote := <-remotePeers
|
|
defer remote.Close()
|
|
_ = remote.SetReadDeadline(time.Now().Add(time.Second))
|
|
autoLogin := make([]byte, len("cisco\r\nenable-secret\r\n"))
|
|
if _, err := io.ReadFull(remote, autoLogin); err != nil || string(autoLogin) != "cisco\r\nenable-secret\r\n" {
|
|
t.Fatalf("auto login = %q, %v", autoLogin, err)
|
|
}
|
|
serialPeer := <-factory.peers
|
|
defer serialPeer.Close()
|
|
go func() { _, _ = remote.Write([]byte("show version\r\n")) }()
|
|
serialInput := make([]byte, len("show version\r\n"))
|
|
_ = serialPeer.SetReadDeadline(time.Now().Add(time.Second))
|
|
if _, err := io.ReadFull(serialPeer, serialInput); err != nil || string(serialInput) != "show version\r\n" {
|
|
t.Fatalf("normal Telnet serial input = %q, %v", serialInput, err)
|
|
}
|
|
go func() { _, _ = serialPeer.Write([]byte("Cisco IOS\r\n")) }()
|
|
response := make([]byte, len("Cisco IOS\r\n"))
|
|
if _, err := io.ReadFull(remote, response); err != nil || string(response) != "Cisco IOS\r\n" {
|
|
t.Fatalf("normal Telnet remote response = %q, %v", response, err)
|
|
}
|
|
}
|
|
|
|
func TestNormalSSHVerifiesKnownHostAndBridgesSession(t *testing.T) {
|
|
_, serverPrivateKey, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
serverSigner, err := ssh.NewSignerFromKey(serverPrivateKey)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
knownHostsPath := filepath.Join(t.TempDir(), "known_hosts")
|
|
line := knownhosts.Line([]string{"console.example"}, serverSigner.PublicKey()) + "\n"
|
|
if err := os.WriteFile(knownHostsPath, []byte(line), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
factory := &pipeFactory{peers: make(chan net.Conn, 1)}
|
|
serverChannels := make(chan ssh.Channel, 1)
|
|
serverErrors := make(chan error, 1)
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer listener.Close()
|
|
go func() {
|
|
serverSide, acceptErr := listener.Accept()
|
|
if acceptErr != nil {
|
|
serverErrors <- acceptErr
|
|
return
|
|
}
|
|
serverConfig := &ssh.ServerConfig{NoClientAuth: true}
|
|
serverConfig.AddHostKey(serverSigner)
|
|
serverConnection, channels, requests, serverErr := ssh.NewServerConn(serverSide, serverConfig)
|
|
if serverErr != nil {
|
|
serverErrors <- serverErr
|
|
return
|
|
}
|
|
_ = serverConnection
|
|
go ssh.DiscardRequests(requests)
|
|
request := <-channels
|
|
channel, channelRequests, acceptErr := request.Accept()
|
|
if acceptErr != nil {
|
|
serverErrors <- acceptErr
|
|
return
|
|
}
|
|
go func() {
|
|
for request := range channelRequests {
|
|
_ = request.Reply(request.Type == "shell", nil)
|
|
}
|
|
}()
|
|
serverChannels <- channel
|
|
}()
|
|
gateway, err := New(Options{
|
|
TransportFactory: factory, KnownHostsPath: knownHostsPath, RetryDelay: time.Second,
|
|
Dial: func(_ context.Context, _, address string) (net.Conn, error) {
|
|
if address != "console.example:22" {
|
|
t.Fatalf("SSH address = %q", address)
|
|
}
|
|
return net.Dial("tcp", listener.Addr().String())
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer gateway.Close()
|
|
port := domain.Port{Channel: "CH05", Operation1: domain.PortOperation{
|
|
Mode: "ssh", Host: "console.example", Port: 22, MaxClients: 1,
|
|
Authentication: "none", Username: "operator", Newline: "cr",
|
|
}}
|
|
if err := gateway.Apply(context.Background(), []domain.Port{port}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var remote ssh.Channel
|
|
select {
|
|
case remote = <-serverChannels:
|
|
case serverErr := <-serverErrors:
|
|
t.Fatal(serverErr)
|
|
case <-time.After(time.Second):
|
|
t.Fatal("timed out waiting for normal SSH session")
|
|
}
|
|
defer remote.Close()
|
|
serialPeer := <-factory.peers
|
|
defer serialPeer.Close()
|
|
go func() { _, _ = remote.Write([]byte("remote SSH data\r\n")) }()
|
|
serialInput := make([]byte, len("remote SSH data\r"))
|
|
_ = serialPeer.SetReadDeadline(time.Now().Add(time.Second))
|
|
if _, err := io.ReadFull(serialPeer, serialInput); err != nil || string(serialInput) != "remote SSH data\r" {
|
|
t.Fatalf("normal SSH serial input = %q, %v", serialInput, err)
|
|
}
|
|
go func() { _, _ = serialPeer.Write([]byte("serial response\r\n")) }()
|
|
response := make([]byte, len("serial response\r\n"))
|
|
if _, err := io.ReadFull(remote, response); err != nil || string(response) != "serial response\r\n" {
|
|
t.Fatalf("normal SSH remote response = %q, %v", response, err)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeNewlines(t *testing.T) {
|
|
input := []byte("one\r\ntwo\nthree\rfour")
|
|
if value := string(normalizeNewlines(input, "lf")); value != "one\ntwo\nthree\nfour" {
|
|
t.Fatalf("LF value = %q", value)
|
|
}
|
|
if value := string(normalizeNewlines(input, "crlf")); value != "one\r\ntwo\r\nthree\r\nfour" {
|
|
t.Fatalf("CRLF value = %q", value)
|
|
}
|
|
}
|
|
|
|
func TestRedirectForwardsSourceRXToEveryConfiguredTarget(t *testing.T) {
|
|
factory := &channelPipeFactory{peers: make(map[string]chan net.Conn)}
|
|
gateway, err := New(Options{TransportFactory: factory, RetryDelay: time.Second})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer gateway.Close()
|
|
if err := gateway.Apply(context.Background(), []domain.Port{
|
|
{Channel: "CH01", RedirectTo: []string{"CH02", "CH03"}},
|
|
{Channel: "CH02"}, {Channel: "CH03"},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
source := factory.peer(t, "CH01")
|
|
target2 := factory.peer(t, "CH02")
|
|
target3 := factory.peer(t, "CH03")
|
|
defer source.Close()
|
|
defer target2.Close()
|
|
defer target3.Close()
|
|
payload := []byte("redirect payload\r\n")
|
|
go func() { _, _ = source.Write(payload) }()
|
|
for channel, target := range map[string]net.Conn{"CH02": target2, "CH03": target3} {
|
|
_ = target.SetReadDeadline(time.Now().Add(time.Second))
|
|
actual := make([]byte, len(payload))
|
|
if _, err := io.ReadFull(target, actual); err != nil || !bytes.Equal(actual, payload) {
|
|
t.Fatalf("%s redirected payload = %q, %v", channel, actual, err)
|
|
}
|
|
}
|
|
snapshot := gateway.Snapshot()
|
|
if len(snapshot.Services) != 2 || snapshot.Services[0].Mode != "redirect" || snapshot.Services[1].Mode != "redirect" {
|
|
t.Fatalf("snapshot = %#v", snapshot)
|
|
}
|
|
}
|
|
|
|
func TestRedirectRejectsCycles(t *testing.T) {
|
|
ports := []domain.Port{
|
|
{Channel: "CH01", RedirectTo: []string{"CH02"}},
|
|
{Channel: "CH02", RedirectTo: []string{"CH01"}},
|
|
}
|
|
if _, err := operationConfigs(ports); err == nil {
|
|
t.Fatal("operationConfigs() error = nil")
|
|
}
|
|
}
|
|
|
|
func TestApplyRestoresPreviousServicesWhenReplacementFails(t *testing.T) {
|
|
factory := &pipeFactory{peers: make(chan net.Conn, 1)}
|
|
failNext := false
|
|
gateway, err := New(Options{
|
|
TransportFactory: factory,
|
|
Listen: func(_, _ string) (net.Listener, error) {
|
|
if failNext {
|
|
failNext = false
|
|
return nil, io.ErrClosedPipe
|
|
}
|
|
return net.Listen("tcp", "127.0.0.1:0")
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer gateway.Close()
|
|
oldPort := domain.Port{Channel: "CH01", Operation1: domain.PortOperation{Mode: "reverse_telnet", Port: 2001, MaxClients: 1, Authentication: "none"}}
|
|
if err := gateway.Apply(context.Background(), []domain.Port{oldPort}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
failNext = true
|
|
newPort := domain.Port{Channel: "CH01", Operation1: domain.PortOperation{Mode: "reverse_telnet", Port: 2002, MaxClients: 1, Authentication: "none"}}
|
|
if err := gateway.Apply(context.Background(), []domain.Port{newPort}); err == nil {
|
|
t.Fatal("Apply(replacement) error = nil")
|
|
}
|
|
snapshot := gateway.Snapshot()
|
|
if len(snapshot.Services) != 1 || snapshot.Services[0].Port != 2001 {
|
|
t.Fatalf("rollback snapshot = %#v", snapshot)
|
|
}
|
|
}
|
|
|
|
type chunkReader struct {
|
|
chunks [][]byte
|
|
}
|
|
|
|
func (r *chunkReader) Read(output []byte) (int, error) {
|
|
if len(r.chunks) == 0 {
|
|
return 0, io.EOF
|
|
}
|
|
chunk := r.chunks[0]
|
|
r.chunks = r.chunks[1:]
|
|
return copy(output, chunk), nil
|
|
}
|
|
|
|
func TestTelnetReaderStripsNegotiationAcrossReadBoundaries(t *testing.T) {
|
|
reader := &telnetReader{reader: &chunkReader{chunks: [][]byte{
|
|
{'A', 255}, {251}, {1, 'B', 255, 250, 24}, {1, 255}, {240, 'C', 255}, {255, 'D'},
|
|
}}}
|
|
actual, err := io.ReadAll(reader)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := []byte{'A', 'B', 'C', 255, 'D'}
|
|
if !bytes.Equal(actual, want) {
|
|
t.Fatalf("telnet payload = %v, want %v", actual, want)
|
|
}
|
|
}
|
|
|