Skip to content
Permalink
Browse files
Updated documentation
  • Loading branch information
antoniomika committed May 21, 2020
1 parent cd5bebf commit 99920f8b405b3274c45e04de5431e86d270537c3
Showing 16 changed files with 223 additions and 190 deletions.
@@ -96,7 +96,7 @@ I can use an SSH command on my laptop like so to forward the connection:
ssh -R 2222:localhost:22 ssi.sh
```

I can use the forwarded connection access my laptop from anywhere:
I can use the forwarded connection to then access my laptop from anywhere:

```bash
ssh -p 2222 ssi.sh
@@ -117,7 +117,7 @@ ssh -R mylaptop:22:localhost:22 ssi.sh

sish won't publish port 22 or 2222 to the rest of the world anymore, instead it'll retain a pointer saying that TCP connections
made from within SSH after a user has authenticated to `mylaptop:22` should be forwarded to the forwarded TCP tunnel.
And then access then I can use the forwarded connection access my laptop from anywhere using:
Then I can use the forwarded connection access my laptop from anywhere using:

```bash
ssh -o ProxyCommand="ssh -W %h:%p ssi.sh" mylaptop
@@ -126,7 +126,7 @@ ssh -o ProxyCommand="ssh -W %h:%p ssi.sh" mylaptop
Shorthand for which is this with newer SSH versions:

```bash
ssh -J mylaptop:22 ssi.sh
ssh -J ssi.sh mylaptop
```

## Authentication
@@ -161,18 +161,18 @@ sish=SSHKEYFINGERPRINT
```

Where `SSHKEYFINGERPRINT` is the fingerprint of the key used for logging into the server. You can set multiple TXT
records and sish will check all of them to ensure at least matches. You can retrieve your key fingerprint by running:
records and sish will check all of them to ensure at least one is a match. You can retrieve your key fingerprint by running:

```bash
ssh-keygen -lf ~/.ssh/id_rsa | awk '{print $2}'
```

## Loadbalancing
## Load balancing

sish can load balance any type of forwarded connection, but this needs to be enabled when starting sish using the `--http-load-balancer`,
`--http-load-balancer`, and `--http-load-balancer` flags. Let's say you have a few edge nodes (raspberry pis) that
`--tcp-load-balancer`, and `--alias-load-balancer` flags. Let's say you have a few edge nodes (raspberry pis) that
are running a service internally but you want to be able to balance load across these devices from the outside world.
By enabling loadbalancing in sish, this happens automatically when a device with the same forwarded TCP port, alias,
By enabling load balancing in sish, this happens automatically when a device with the same forwarded TCP port, alias,
or HTTP subdomain connects to sish. Connections will then be evenly distributed to whatever nodes are connected to
sish that match the forwarded connection.

@@ -210,7 +210,7 @@ or on [freenode IRC #sish](https://kiwiirc.com/client/chat.freenode.net:6697/#si
## CLI Flags

```text
sish is a command line utility that implements an SSH server that can handle HTTP(S)/WS(S)/TCP multiplexing, forwarding and loadbalancing.
sish is a command line utility that implements an SSH server that can handle HTTP(S)/WS(S)/TCP multiplexing, forwarding and load balancing.
It can handle multiple vhosting and reverse tunneling endpoints for a large number of clients.
Usage:
@@ -1,3 +1,4 @@
// Package cmd implements the sish CLI command.
package cmd

import (
@@ -17,35 +18,29 @@ import (
)

var (
// Version describes the version of the current build
// Version describes the version of the current build.
Version = "dev"

// Commit describes the commit of the current build
// Commit describes the commit of the current build.
Commit = "none"

// Date describes the date of the current build
// Date describes the date of the current build.
Date = "unknown"

// configFile holds the location of the config file from CLI flags.
configFile string

// rootCmd is the root cobra command.
rootCmd = &cobra.Command{
Use: "sish",
Short: "The sish command initializes and runs the sish ssh multiplexer",
Long: "sish is a command line utility that implements an SSH server that can handle HTTP(S)/WS(S)/TCP multiplexing, forwarding and loadbalancing.\nIt can handle multiple vhosting and reverse tunneling endpoints for a large number of clients.",
Long: "sish is a command line utility that implements an SSH server that can handle HTTP(S)/WS(S)/TCP multiplexing, forwarding and load balancing.\nIt can handle multiple vhosting and reverse tunneling endpoints for a large number of clients.",
Run: runCommand,
Version: Version,
}
)

type logWriter struct {
TimeFmt string
MultiWriter io.Writer
}

func (w logWriter) Write(bytes []byte) (int, error) {
return fmt.Fprintf(w.MultiWriter, "%v | %s", time.Now().Format(w.TimeFmt), string(bytes))
}

// init initializes flags used by the root command.
func init() {
cobra.OnInitialize(initConfig)

@@ -118,6 +113,8 @@ func init() {
rootCmd.PersistentFlags().DurationP("cleanup-unbound-timeout", "", 5*time.Second, "Duration to wait before cleaning up an unbound (unforwarded) connection")
}

// initConfig initializes the configuration and loads needed
// values. It initializes logging and other vars.
func initConfig() {
viper.SetConfigFile(configFile)

@@ -156,7 +153,7 @@ func initConfig() {
log.Println("Reloaded configuration file.")

log.SetFlags(0)
log.SetOutput(logWriter{
log.SetOutput(utils.LogWriter{
TimeFmt: viper.GetString("time-format"),
MultiWriter: multiWriter,
})
@@ -167,7 +164,7 @@ func initConfig() {
})

log.SetFlags(0)
log.SetOutput(logWriter{
log.SetOutput(utils.LogWriter{
TimeFmt: viper.GetString("time-format"),
MultiWriter: multiWriter,
})
@@ -186,6 +183,7 @@ func Execute() error {
return rootCmd.Execute()
}

// runCommand is used to start the root muxer.
func runCommand(cmd *cobra.Command, args []string) {
sshmuxer.Start()
}
@@ -1,3 +1,6 @@
// Package httpmuxer handles all of the HTTP connections made
// to sish. This implements the http multiplexing necessary for
// sish's core feature.
package httpmuxer

import (
@@ -20,7 +23,7 @@ import (
"github.com/gin-gonic/gin"
)

// Start initializes the HTTP service
// Start initializes the HTTP service.
func Start(state *utils.State) {
releaseMode := gin.ReleaseMode
if viper.GetBool("debug") {
@@ -33,14 +36,18 @@ func Start(state *utils.State) {
r := gin.New()
r.LoadHTMLGlob("templates/*")
r.Use(func(c *gin.Context) {
// startTime is used for calculating latencies.
c.Set("startTime", time.Now())

// Here is where we check whether or not an IP is blocked.
clientIPAddr, _, err := net.SplitHostPort(c.Request.RemoteAddr)
if state.IPFilter.Blocked(c.ClientIP()) || state.IPFilter.Blocked(clientIPAddr) || err != nil {
c.AbortWithStatus(http.StatusForbidden)
return
}
c.Next()
}, gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
// Here is the logger we use to format each incoming request.
var statusColor, methodColor, resetColor string
if param.IsOutputColor() {
statusColor = param.StatusCodeColor()
@@ -77,12 +84,12 @@ func Start(state *utils.State) {
loc, ok := state.HTTPListeners.Load(hostname)
if ok {
proxyHolder := loc.(*utils.HTTPHolder)
sshConnTmp, ok := proxyHolder.SSHConns.Load(param.Keys["proxySocket"])
sshConnTmp, ok := proxyHolder.SSHConnections.Load(param.Keys["proxySocket"])
if ok {
sshConn := sshConnTmp.(*utils.SSHConnection)
sshConn.SendMessage(strings.TrimSpace(logLine), true)
} else {
proxyHolder.SSHConns.Range(func(key, val interface{}) bool {
proxyHolder.SSHConnections.Range(func(key, val interface{}) bool {
sshConn := val.(*utils.SSHConnection)
sshConn.SendMessage(strings.TrimSpace(logLine), true)
return true
@@ -93,6 +100,7 @@ func Start(state *utils.State) {

return logLine
}), gin.Recovery(), func(c *gin.Context) {
// Return a 404 for the favicon.
if strings.HasPrefix(c.Request.URL.Path, "/favicon.ico") {
c.AbortWithStatus(http.StatusNotFound)
return
@@ -138,6 +146,9 @@ func Start(state *utils.State) {
gin.WrapH(proxyHolder.Balancer)(c)
})

// If HTTPS is enabled, setup certmagic to allow us to provision HTTPS certs on the fly.
// You can use sish without a wildcard cert, but you really should. If you get a lot of clients
// with many random subdomains, you'll burn through your Let's Encrypt quota. Be careful!
if viper.GetBool("https") {
certManager := certmagic.NewDefault()

@@ -18,7 +18,8 @@ import (
"github.com/spf13/viper"
)

// RoundTripper returns the specific handler for unix connections
// RoundTripper returns the specific handler for unix connections. This
// will allow us to use our created sockets cleanly.
func RoundTripper() *http.Transport {
dialer := func(network, addr string) (net.Conn, error) {
realAddr, err := base64.StdEncoding.DecodeString(strings.Split(addr, ":")[0])
@@ -39,7 +40,9 @@ func RoundTripper() *http.Transport {
}
}

// ResponseModifier implements a response modifier for the specified request
// ResponseModifier implements a response modifier for the specified request.
// We don't actually modify any requests, but we do want to record the request
// so we can send it to the web console.
func ResponseModifier(state *utils.State, hostname string, reqBody []byte, c *gin.Context) func(*http.Response) error {
return func(response *http.Response) error {
if viper.GetBool("admin-console") || viper.GetBool("service-console") {
@@ -1,3 +1,4 @@
// Package main represents the main entrypoint of the sish application.
package main

import (
@@ -6,6 +7,7 @@ import (
"github.com/antoniomika/sish/cmd"
)

// main will start the sish command lifecycle and spawn the sish services.
func main() {
err := cmd.Execute()
if err != nil {
@@ -12,6 +12,8 @@ import (
"github.com/logrusorgru/aurora"
)

// handleAliasListener handles the creation of the aliasHandler
// (or addition for load balancing) and set's up the underlying listeners.
func handleAliasListener(check *channelForwardMsg, stringPort string, requestMessages string, listenerHolder *utils.ListenerHolder, state *utils.State, sshConn *utils.SSHConnection) (*utils.AliasHolder, *url.URL, string, string, error) {
validAlias, aH := utils.GetOpenAlias(check.Addr, stringPort, state, sshConn)

@@ -24,15 +26,15 @@ func handleAliasListener(check *channelForwardMsg, stringPort string, requestMes
}

aH = &utils.AliasHolder{
AliasHost: validAlias,
SSHConns: &sync.Map{},
Balancer: lb,
AliasHost: validAlias,
SSHConnections: &sync.Map{},
Balancer: lb,
}

state.AliasListeners.Store(validAlias, aH)
}

aH.SSHConns.Store(listenerHolder.Addr().String(), sshConn)
aH.SSHConnections.Store(listenerHolder.Addr().String(), sshConn)

serverURL := &url.URL{
Host: base64.StdEncoding.EncodeToString([]byte(listenerHolder.Addr().String())),
@@ -14,8 +14,12 @@ import (
"golang.org/x/crypto/ssh"
)

// proxyProtoPrefix is used when deciding what proxy protocol
// version to use.
var proxyProtoPrefix = "proxyproto:"

// handleSession handles the channel when a user requests a session.
// This is how we send console messages.
func handleSession(newChannel ssh.NewChannel, sshConn *utils.SSHConnection, state *utils.State) {
connection, requests, err := newChannel.Accept()
if err != nil {
@@ -89,6 +93,7 @@ func handleSession(newChannel ssh.NewChannel, sshConn *utils.SSHConnection, stat
}()
}

// handleAlias is used when handling a SSH connection to attach to an alias listener.
func handleAlias(newChannel ssh.NewChannel, sshConn *utils.SSHConnection, state *utils.State) {
connection, requests, err := newChannel.Accept()
if err != nil {
@@ -140,7 +145,7 @@ func handleAlias(newChannel ssh.NewChannel, sshConn *utils.SSHConnection, state
log.Println(logLine)

if viper.GetBool("log-to-client") {
aH.SSHConns.Range(func(key, val interface{}) bool {
aH.SSHConnections.Range(func(key, val interface{}) bool {
sshConn := val.(*utils.SSHConnection)

sshConn.Listeners.Range(func(key, val interface{}) bool {
@@ -178,13 +183,15 @@ func handleAlias(newChannel ssh.NewChannel, sshConn *utils.SSHConnection, state
}
}

// writeToSession is where we write to the underlying session channel.
func writeToSession(connection ssh.Channel, c string) {
_, err := connection.Write(append([]byte(c), []byte{'\r', '\n'}...))
if err != nil && viper.GetBool("debug") {
log.Println("Error trying to write message to socket:", err)
}
}

// getProxyProtoVersion returns the proxy proto version selected by the client.
func getProxyProtoVersion(proxyProtoUserVersion string) byte {
if viper.GetString("proxy-protocol-version") != "userdefined" {
proxyProtoUserVersion = viper.GetString("proxy-protocol-version")
@@ -10,6 +10,7 @@ import (
"golang.org/x/crypto/ssh"
)

// handleRequests handles incoming requests from an SSH connection.
func handleRequests(reqs <-chan *ssh.Request, sshConn *utils.SSHConnection, state *utils.State) {
for req := range reqs {
if viper.GetBool("debug") {
@@ -19,6 +20,7 @@ func handleRequests(reqs <-chan *ssh.Request, sshConn *utils.SSHConnection, stat
}
}

// handleRequest handles a incoming request from a SSH connection.
func handleRequest(newRequest *ssh.Request, sshConn *utils.SSHConnection, state *utils.State) {
switch req := newRequest.Type; req {
case "tcpip-forward":
@@ -37,6 +39,7 @@ func handleRequest(newRequest *ssh.Request, sshConn *utils.SSHConnection, state
}
}

// checkSession will check a session to see that it has a session.
func checkSession(newRequest *ssh.Request, sshConn *utils.SSHConnection, state *utils.State) {
if sshConn.CleanupHandler {
return
@@ -55,6 +58,7 @@ func checkSession(newRequest *ssh.Request, sshConn *utils.SSHConnection, state *
}
}

// handleChannels handles a SSH connection's channel requests.
func handleChannels(chans <-chan ssh.NewChannel, sshConn *utils.SSHConnection, state *utils.State) {
for newChannel := range chans {
if viper.GetBool("debug") {
@@ -64,6 +68,7 @@ func handleChannels(chans <-chan ssh.NewChannel, sshConn *utils.SSHConnection, s
}
}

// handleChannel handles a SSH connection's channel request.
func handleChannel(newChannel ssh.NewChannel, sshConn *utils.SSHConnection, state *utils.State) {
switch channel := newChannel.ChannelType(); channel {
case "session":
@@ -15,6 +15,8 @@ import (
"github.com/spf13/viper"
)

// handleHTTPListener handles the creation of the httpHandler
// (or addition for load balancing) and set's up the underlying listeners.
func handleHTTPListener(check *channelForwardMsg, stringPort string, requestMessages string, listenerHolder *utils.ListenerHolder, state *utils.State, sshConn *utils.SSHConnection) (*utils.HTTPHolder, *url.URL, string, string, error) {
scheme := "http"
if stringPort == "443" {
@@ -45,17 +47,17 @@ func handleHTTPListener(check *channelForwardMsg, stringPort string, requestMess
}

pH = &utils.HTTPHolder{
HTTPHost: host,
Scheme: scheme,
SSHConns: &sync.Map{},
Forward: fwd,
Balancer: lb,
HTTPHost: host,
Scheme: scheme,
SSHConnections: &sync.Map{},
Forward: fwd,
Balancer: lb,
}

state.HTTPListeners.Store(host, pH)
}

pH.SSHConns.Store(listenerHolder.Addr().String(), sshConn)
pH.SSHConnections.Store(listenerHolder.Addr().String(), sshConn)

serverURL := &url.URL{
Host: base64.StdEncoding.EncodeToString([]byte(listenerHolder.Addr().String())),

0 comments on commit 99920f8

Please sign in to comment.