Skip to content

Commit

Permalink
Merge pull request #2 from lykinsbd/feature/command_mapper
Browse files Browse the repository at this point in the history
Add Command Transcript Playback and Make More Modular
  • Loading branch information
tbotnz authored Sep 4, 2020
2 parents 30e8bb2 + 62802f1 commit 7872dca
Show file tree
Hide file tree
Showing 34 changed files with 10,454 additions and 226 deletions.
97 changes: 79 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,39 +1,37 @@
# cisgo-ios
simple concurrent ssh server posing as cisco ios
Simple, small, fast, concurrent SSH server to emulate network equipment (i.e. Cisco IOS) for testing purposes.

## installation
install dependencies
## Usage

All dependencies are included in the `/vendor` folder, so no installation step is necessary.
1. Clone the repository and change into that directory (All dependencies are included in the `/vendor` folder, so no installation step is necessary.)
2. Execute `go run cis.go` as shown below:

## starting
```
go run cis.go
```bash
$ go run cis.go
2020/08/22 00:17:34 starting ssh server on port :10049
2020/08/22 00:17:34 starting ssh server on port :10023
2020/08/22 00:17:34 starting ssh server on port :10024
2020/08/22 00:17:34 starting ssh server on port :10000
2020/08/22 00:17:34 starting ssh server on port :10001
2020/08/22 00:17:34 starting ssh server on port :10025
2020/08/22 00:17:34 starting ssh server on port :10026
2020/08/22 00:17:34 starting ssh server on port :10027
... <snip>
```

alternatively you can compile and run in separate steps (useful for docker images, etc):
Alternatively you can compile and run in separate steps (useful for docker images, etc):

```bash
user@LAPTOP-6PM8GPB2:/mnt/c/projects/cisgo-ios$ go build cis.go
user@LAPTOP-6PM8GPB2:/mnt/c/projects/cisgo-ios$ ./cis
$ go build cisgo-ios cis.go
$ ./cisgo-ios
2020/09/02 15:46:31 starting cis.go ssh server on port :10008
2020/09/02 15:46:31 starting cis.go ssh server on port :10005
2020/09/02 15:46:31 starting cis.go ssh server on port :10000
2020/09/02 15:46:31 starting cis.go ssh server on port :10006
...
... <snip>
```

3. SSH into one of the open ports with `admin` as the password. By default, you can run "show version"
or "show ip interface brief" or "show running-config". Other commands can be added by modifying the
transcript_map.yaml file and supplying transcripts as needed.

Example output:

## using
ssh into one of the open ports with ```admin``` as password and run "show version" or "show ip interface brief" or "show running-config"
```
test_device#show version
Cisco IOS XE Software, Version 16.04.01
Expand All @@ -52,3 +50,66 @@ or the applicable URL provided on the flyer accompanying the IOS-XE
software.
ROM: IOS-XE ROMMON
```

## Advanced Usage

There are several options available to control the behavior
of cisgo-ios see the below output of `-help`:

```
-listners int
How many listeners do you wish to spawn? (default 50)
-startingPort int
What port do you want to start at? (default 10000)
-transcriptMap string
What file contains the map of commands to transcipted output? (default "transcripts/transcript_map.yaml")
```

For example, if you only wish to lauch with a single SSH listner for a testing process,
you could simply apply `-listners 1` to the run command:

```
go run cis.go -listners 1
2020/09/03 19:41:04 Starting cis.go ssh server on port :10000
```

## Expanding Platform Support

cisgo-ios is built modularly to support easy expansion or customization. Potential options for enhancement are outlined below.

### Adding Additional Command Transcripts

If you wish to add additional command transcripts, you simply need to include a plain text file in the appropriate
`vendor/platform` folder, and create an entry in the `transcript_map.yaml` file under the appropriate vendor/platform:

```
---
platforms:
- csr1000v:
command_transcripts:
"my new fancy command": "transcripts/cisco/csr1000v/my_new_fancy_command.txt"
```

On the next execution of cisgo-ios it will read this map and respond to `my new fancy command`

### Adding Additional "Cisco-style" Platforms

If you wish to add a completely new Cisco-style device, that is one with `configure terminal`
leading to a `(config)#` mode for example, you can simply supply additional device types and transcripts
in the transcript_map.yaml file.

This however does not work if a device follows a different interaction pattern than the Cisco standard,
for example a Juniper or F5 device, for that see the following section.

### Adding Additional Non-"Cisco-style" Platforms

**NOTE** This feature is not fully implemented yet!

If you wish to add a platform that is _not_ the "Cisco-style" of interaction, for example a Juniper or F5 device,
you will need to implement a new `handler` module for it under `ssh_server/handlers` and add it to the
device mapping in code in `cis.go` where it chooses the SSH listner and handler.

The "handler" controls the basics of how we will emulate the SSH session, and provides a list of
`if...else if...else if...` options to roughly simulate the device experience. Because many network
devices vary in their CLI and interactions, the conditional tree that each requires will vary.
This is implemented via the "handler" functionality.
188 changes: 21 additions & 167 deletions cis.go
Original file line number Diff line number Diff line change
@@ -1,180 +1,34 @@
package main

import (
"fmt"
"io/ioutil"
"log"
"strconv"
"strings"

"github.com/gliderlabs/ssh"
"golang.org/x/crypto/ssh/terminal"
)

const (
defaultHostname = "cisgo1000v"
defaultContextState = ">"
password = "admin"
"github.com/tbotnz/cisgo-ios/fakedevices"
"github.com/tbotnz/cisgo-ios/ssh_server/handlers"
"github.com/tbotnz/cisgo-ios/ssh_server/sshlistners"
"github.com/tbotnz/cisgo-ios/utils"
)

func readFile(filename string) string {
content, err := ioutil.ReadFile(filename)
if err != nil {
log.Fatal(err)
}
return string(content)
}

type commandGroup struct {
basic map[string]string
hostname map[string]string
mode map[string]string
}

func newCommandGroup() *commandGroup {
cmds := new(commandGroup)
cmds.basic = make(map[string]string)
cmds.basic["terminal length 0"] = " "
cmds.basic["terminal width 511"] = " "
cmds.basic["show ip interface brief"] = readFile("config/show_ip_int_bri.txt")

cmds.hostname = make(map[string]string)
cmds.hostname["show version"] = readFile("config/show_version.txt")
cmds.hostname["show running-config"] = readFile("config/show_running-config.txt")

cmds.mode = make(map[string]string)
cmds.mode["conf t"] = "(config)#"
cmds.mode["configure terminal"] = "(config)#"
cmds.mode["configure t"] = "(config)#"
cmds.mode["enable"] = "#"
cmds.mode["en"] = "#"
cmds.mode["base"] = ">"
return cmds
}
func main() {

type internalState struct {
hostname string
currentMode string // >, #, or (config)#
prompt string
}
// Parse the command line arguments
numListners, startingPortPtr, myTranscriptMap := utils.ParseArgs()

func (s *internalState) setMode(mode string) {
s.currentMode = mode
s.prompt = s.hostname + s.currentMode
}
// Initialize our fake device
myFakeDevice := fakedevices.InitGenric(
"cisco", // Vendor
"csr1000v", // Platform
myTranscriptMap, // Transcript map with locations of command output to play back
)

func (s *internalState) setHostname(hostname string) {
s.hostname = hostname
s.prompt = s.hostname + s.currentMode
}
// Make a Channel named "done" for handling Goroutines, which expects a bool as return value
done := make(chan bool, 1)

func (s *internalState) exit() bool {
switch s.currentMode {
case ">":
return false
case "#":
s.setMode(">")
case "(config)#":
s.setMode("#")
// Iterate through the server ports and spawn a Goroutine for each
for portNumber := *startingPortPtr; portNumber < numListners; portNumber++ {
// Today this is just spawning a generic listner.
// In the future, this is where we could split out listners/handlers by device type.
go sshlistners.GenericListner(myFakeDevice, portNumber, handlers.GenericCiscoHandler, done)
}
return true
}

func newState() *internalState {
// log.Println("created new internalState")
return &internalState{defaultHostname, defaultContextState, defaultHostname + defaultContextState}
}

// ssh listener
func sshListener(portNumber int, done chan bool) {

commandGroup := newCommandGroup()

contextHierarchy := make(map[string]string)

contextHierarchy["(config)#"] = "#"
contextHierarchy["#"] = ">"
contextHierarchy[">"] = "exit"

thisState := newState()

ssh.Handle(func(s ssh.Session) {

term := terminal.NewTerminal(s, thisState.prompt)
for {
response, err := term.ReadLine()
if err != nil {
break
}

log.Println(response)
if response == "reset state" {
log.Println("resetting internal state")
thisState = newState()
term.SetPrompt(thisState.prompt)

} else if response == "" {
// return if nothing is entered
term.Write(append([]byte(response)))

} else if commandGroup.basic[response] != "" {
// lookup supported commands for response
term.Write(append([]byte(commandGroup.basic[response]), '\n'))

} else if commandGroup.mode[response] != "" {
// switch contexts as needed
thisState.setMode(commandGroup.mode[response])
term.SetPrompt(thisState.prompt)

} else if response == "exit" || response == "end" {
// drop down configs if required
if thisState.exit() { // "true" means we're still active, "false" means we're done
term.SetPrompt(thisState.prompt)
} else {
break
}

} else if commandGroup.hostname[response] != "" {
term.Write([]byte(fmt.Sprintf(commandGroup.hostname[response], thisState.hostname)))

} else if thisState.currentMode != ">" { // we're in config mode
fields := strings.Fields(response)
command := fields[0]
if command == "hostname" {
thisState.setHostname(strings.Join(fields[1:], " "))
log.Printf("Setting hostname to %s\n", thisState.hostname)
term.SetPrompt(thisState.prompt)

} else {
term.Write([]byte("% Ambiguous command: \"" + response + "\"\n"))
}

} else {
term.Write([]byte("% Ambiguous command: \"" + response + "\"\n"))
}

}
log.Println("terminal closed")

})

portString := ":" + strconv.Itoa(portNumber)
//prt := portString
log.Printf("starting cis.go ssh server on port %s\n", portString)

log.Fatal(ssh.ListenAndServe(portString, nil,
ssh.PasswordAuth(func(ctx ssh.Context, pass string) bool {
return pass == password
}),
))

done <- true
}

func main() {
done := make(chan bool, 1)
for portNumber := 10000; portNumber < 10050; portNumber++ {
go sshListener(portNumber, done)
}
// Recieve all the values from the channel (essentially wait on it to be empty)
<-done
}
Loading

0 comments on commit 7872dca

Please sign in to comment.