Updated documentation

This commit is contained in:
Antonio Mika
2020-05-21 02:26:07 -04:00
parent cd5bebf248
commit 99920f8b40
16 changed files with 223 additions and 190 deletions

View File

@@ -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:

View File

@@ -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()
}

View File

@@ -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,7 +36,10 @@ 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)
@@ -41,6 +47,7 @@ func Start(state *utils.State) {
}
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()

View File

@@ -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") {

View File

@@ -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 {

View File

@@ -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())),

View File

@@ -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,6 +183,7 @@ 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") {
@@ -185,6 +191,7 @@ func writeToSession(connection ssh.Channel, c string) {
}
}
// 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")

View File

@@ -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":

View File

@@ -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())),

View File

@@ -15,11 +15,15 @@ import (
"golang.org/x/crypto/ssh"
)
// channelForwardMsg is the message sent by SSH
// to init a forwarded connection.
type channelForwardMsg struct {
Addr string
Rport uint32
}
// forwardedTCPPayload is the payload sent by SSH
// to init a forwarded connection.
type forwardedTCPPayload struct {
Addr string
Port uint32
@@ -27,6 +31,8 @@ type forwardedTCPPayload struct {
OriginPort uint32
}
// handleRemoteForward will handle a remote forward request
// and stand up the relevant listeners.
func handleRemoteForward(newRequest *ssh.Request, sshConn *utils.SSHConnection, state *utils.State) {
check := &channelForwardMsg{}
@@ -117,7 +123,7 @@ func handleRemoteForward(newRequest *ssh.Request, sshConn *utils.SSHConnection,
log.Println("Unable to add server to balancer")
}
pH.SSHConns.Delete(listenerHolder.Addr().String())
pH.SSHConnections.Delete(listenerHolder.Addr().String())
if len(pH.Balancer.Servers()) == 0 {
state.HTTPListeners.Delete(host)
@@ -141,7 +147,7 @@ func handleRemoteForward(newRequest *ssh.Request, sshConn *utils.SSHConnection,
log.Println("Unable to add server to balancer")
}
aH.SSHConns.Delete(listenerHolder.Addr().String())
aH.SSHConnections.Delete(listenerHolder.Addr().String())
if len(aH.Balancer.Servers()) == 0 {
state.AliasListeners.Delete(validAlias)
@@ -163,7 +169,7 @@ func handleRemoteForward(newRequest *ssh.Request, sshConn *utils.SSHConnection,
log.Println("Unable to add server to balancer")
}
tH.SSHConns.Delete(listenerHolder.Addr().String())
tH.SSHConnections.Delete(listenerHolder.Addr().String())
if len(tH.Balancer.Servers()) == 0 {
tH.Listener.Close()

View File

@@ -1,3 +1,5 @@
// Package sshmuxer handles the underlying SSH server
// and multiplexing forwarding sessions.
package sshmuxer
import (
@@ -18,11 +20,15 @@ import (
)
var (
httpPort int
// httpPort is used as a string override for the used HTTP port.
httpPort int
// httpsPort is used as a string override for the used HTTPS port.
httpsPort int
)
// Start initializes the ssh muxer service
// Start initializes the ssh muxer service. It will start necessary components
// and begin listening for SSH connections.
func Start() {
_, httpPortString, err := net.SplitHostPort(viper.GetString("http-address"))
if err != nil {
@@ -174,7 +180,7 @@ func Start() {
Session: make(chan bool),
}
state.SSHConnections.Store(sshConn.RemoteAddr(), holderConn)
state.SSHConnections.Store(sshConn.RemoteAddr().String(), holderConn)
go func() {
err := sshConn.Wait()

View File

@@ -15,6 +15,8 @@ import (
"github.com/spf13/viper"
)
// handleTCPListener handles the creation of the tcpHandler
// (or addition for load balancing) and set's up the underlying listeners.
func handleTCPListener(check *channelForwardMsg, bindPort uint32, requestMessages string, listenerHolder *utils.ListenerHolder, state *utils.State, sshConn *utils.SSHConnection) (*utils.TCPHolder, *url.URL, string, string, error) {
tcpAddr, _, tH := utils.GetOpenPort(check.Addr, bindPort, state, sshConn)
@@ -27,9 +29,9 @@ func handleTCPListener(check *channelForwardMsg, bindPort uint32, requestMessage
}
tH = &utils.TCPHolder{
TCPHost: tcpAddr,
SSHConns: &sync.Map{},
Balancer: lb,
TCPHost: tcpAddr,
SSHConnections: &sync.Map{},
Balancer: lb,
}
l, err := net.Listen("tcp", tcpAddr)
@@ -48,7 +50,7 @@ func handleTCPListener(check *channelForwardMsg, bindPort uint32, requestMessage
state.TCPListeners.Store(tcpAddr, tH)
}
tH.SSHConns.Store(listenerHolder.Addr().String(), sshConn)
tH.SSHConnections.Store(listenerHolder.Addr().String(), sshConn)
serverURL := &url.URL{
Host: base64.StdEncoding.EncodeToString([]byte(listenerHolder.Addr().String())),

View File

@@ -11,7 +11,9 @@ import (
"golang.org/x/crypto/ssh"
)
// SSHConnection handles state for a SSHConnection
// SSHConnection handles state for a SSHConnection. It wraps an ssh.ServerConn
// and allows us to pass other state around the application.
// Listeners is a map[string]net.Listener
type SSHConnection struct {
SSHConn *ssh.ServerConn
Listeners *sync.Map
@@ -22,7 +24,9 @@ type SSHConnection struct {
CleanupHandler bool
}
// SendMessage sends a console message to the connection
// SendMessage sends a console message to the connection. If block is true, it
// will block until the message is sent. If it is false, it will try to send the
// message 5 times, waiting 100ms each time.
func (s *SSHConnection) SendMessage(message string, block bool) {
if block {
s.Messages <- message
@@ -42,15 +46,15 @@ func (s *SSHConnection) SendMessage(message string, block bool) {
}
}
// CleanUp closes all allocated resources and cleans them up
// CleanUp closes all allocated resources for a SSH session and cleans them up.
func (s *SSHConnection) CleanUp(state *State) {
close(s.Close)
s.SSHConn.Close()
state.SSHConnections.Delete(s.SSHConn.RemoteAddr())
log.Println("Closed SSH connection for:", s.SSHConn.RemoteAddr(), "user:", s.SSHConn.User())
state.SSHConnections.Delete(s.SSHConn.RemoteAddr().String())
log.Println("Closed SSH connection for:", s.SSHConn.RemoteAddr().String(), "user:", s.SSHConn.User())
}
// IdleTimeoutConn handles the connection with a context deadline
// IdleTimeoutConn handles the connection with a context deadline.
// code adapted from https://qiita.com/kwi/items/b38d6273624ad3f6ae79
type IdleTimeoutConn struct {
Conn net.Conn
@@ -66,7 +70,7 @@ func (i IdleTimeoutConn) Read(buf []byte) (int, error) {
return i.Conn.Read(buf)
}
// Write is needed to implement the writer part
// Write is needed to implement the writer part.
func (i IdleTimeoutConn) Write(buf []byte) (int, error) {
err := i.Conn.SetWriteDeadline(time.Now().Add(viper.GetDuration("idle-connection-timeout")))
if err != nil {
@@ -76,7 +80,7 @@ func (i IdleTimeoutConn) Write(buf []byte) (int, error) {
return i.Conn.Write(buf)
}
// CopyBoth copies betwen a reader and writer
// CopyBoth copies betwen a reader and writer and will cleanup each.
func CopyBoth(writer net.Conn, reader io.ReadWriteCloser) {
closeBoth := func() {
reader.Close()

View File

@@ -4,7 +4,6 @@ import (
"encoding/base64"
"fmt"
"log"
"net"
"net/http"
"strings"
"sync"
@@ -14,12 +13,14 @@ import (
"github.com/spf13/viper"
)
// upgrader is the default WS upgrader that we use for webconsole clients.
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
// WebClient represents a primitive web console client
// WebClient represents a primitive web console client. It maintains
// references that allow us to communicate and track a client connection.
type WebClient struct {
Conn *websocket.Conn
Console *WebConsole
@@ -27,15 +28,16 @@ type WebClient struct {
Route string
}
// WebConsole represents the data structure that stores web console client information
// Clients is a map[string][]*WebClient
// WebConsole represents the data structure that stores web console client information.
// Clients is a map[string][]*WebClient.
// RouteTokens is a map[string]string.
type WebConsole struct {
Clients *sync.Map
RouteTokens *sync.Map
State *State
}
// NewWebConsole set's up the WebConsole
// NewWebConsole sets up the WebConsole.
func NewWebConsole() *WebConsole {
return &WebConsole{
Clients: &sync.Map{},
@@ -43,7 +45,7 @@ func NewWebConsole() *WebConsole {
}
}
// HandleRequest handles an incoming WS request
// HandleRequest handles an incoming web request, handles auth, and then routes it.
func (c *WebConsole) HandleRequest(hostname string, hostIsRoot bool, g *gin.Context) {
userAuthed := false
userIsAdmin := false
@@ -72,19 +74,13 @@ func (c *WebConsole) HandleRequest(hostname string, hostIsRoot bool, g *gin.Cont
} else if strings.HasPrefix(g.Request.URL.Path, "/_sish/api/disconnectroute/") && userIsAdmin {
c.HandleDisconnectRoute(hostname, g)
return
} else if strings.HasPrefix(g.Request.URL.Path, "/_sish/api/routes") && hostIsRoot && userIsAdmin {
c.HandleRoutes(hostname, g)
return
} else if strings.HasPrefix(g.Request.URL.Path, "/_sish/api/allroutes") && hostIsRoot && userIsAdmin {
c.HandleAllRoutes(hostname, g)
return
} else if strings.HasPrefix(g.Request.URL.Path, "/_sish/api/clients") && hostIsRoot && userIsAdmin {
c.HandleClients(hostname, g)
return
}
}
// HandleTemplate handles rendering the console template
// HandleTemplate handles rendering the console templates.
func (c *WebConsole) HandleTemplate(hostname string, hostIsRoot bool, userIsAdmin bool, g *gin.Context) {
if hostIsRoot && userIsAdmin {
g.HTML(http.StatusOK, "routes", nil)
@@ -122,14 +118,14 @@ func (c *WebConsole) HandleWebSocket(hostname string, g *gin.Context) {
go client.Handle()
}
// HandleDisconnectClient handles the disconnection request for a client
// HandleDisconnectClient handles the disconnection request for a SSH client.
func (c *WebConsole) HandleDisconnectClient(hostname string, g *gin.Context) {
client := strings.TrimPrefix(g.Request.URL.Path, "/_sish/api/disconnectclient/")
c.State.SSHConnections.Range(func(key interface{}, val interface{}) bool {
clientName := key.(*net.TCPAddr)
clientName := key.(string)
if clientName.String() == client {
if clientName == client {
holderConn := val.(*SSHConnection)
holderConn.CleanUp(c.State)
@@ -146,7 +142,7 @@ func (c *WebConsole) HandleDisconnectClient(hostname string, g *gin.Context) {
g.JSON(http.StatusOK, data)
}
// HandleDisconnectRoute handles the disconnection request for a route
// HandleDisconnectRoute handles the disconnection request for a forwarded route.
func (c *WebConsole) HandleDisconnectRoute(hostname string, g *gin.Context) {
route := strings.Split(strings.TrimPrefix(g.Request.URL.Path, "/_sish/api/disconnectroute/"), "/")
encRouteName := route[1]
@@ -180,26 +176,9 @@ func (c *WebConsole) HandleDisconnectRoute(hostname string, g *gin.Context) {
g.JSON(http.StatusOK, data)
}
// HandleRoutes handles returning available http routes to join
func (c *WebConsole) HandleRoutes(hostname string, g *gin.Context) {
data := map[string]interface{}{
"status": true,
}
routes := []string{}
c.Clients.Range(func(key interface{}, val interface{}) bool {
routeName := key.(string)
routes = append(routes, routeName)
return true
})
data["routes"] = routes
g.JSON(http.StatusOK, data)
}
// HandleClients handles returning all connected clients
// HandleClients handles returning all connected SSH clients. This will
// also go through all of the forwarded connections for the SSH client and
// return them.
func (c *WebConsole) HandleClients(hostname string, g *gin.Context) {
data := map[string]interface{}{
"status": true,
@@ -207,7 +186,7 @@ func (c *WebConsole) HandleClients(hostname string, g *gin.Context) {
clients := map[string]map[string]interface{}{}
c.State.SSHConnections.Range(func(key interface{}, val interface{}) bool {
clientName := key.(*net.TCPAddr)
clientName := key.(string)
sshConn := val.(*SSHConnection)
listeners := []string{}
@@ -277,7 +256,7 @@ func (c *WebConsole) HandleClients(hostname string, g *gin.Context) {
aliasAddress := val.(*HTTPHolder)
listenerHandlers := []string{}
aliasAddress.SSHConns.Range(func(key interface{}, val interface{}) bool {
aliasAddress.SSHConnections.Range(func(key interface{}, val interface{}) bool {
aliasAddr := key.(string)
for _, v := range listeners {
@@ -308,7 +287,7 @@ func (c *WebConsole) HandleClients(hostname string, g *gin.Context) {
}
}
clients[clientName.String()] = map[string]interface{}{
clients[clientName] = map[string]interface{}{
"remoteAddr": sshConn.SSHConn.RemoteAddr().String(),
"user": sshConn.SSHConn.User(),
"version": string(sshConn.SSHConn.ClientVersion()),
@@ -327,53 +306,7 @@ func (c *WebConsole) HandleClients(hostname string, g *gin.Context) {
g.JSON(http.StatusOK, data)
}
// HandleAllRoutes handles returning all connected routes (tunnels)
func (c *WebConsole) HandleAllRoutes(hostname string, g *gin.Context) {
data := map[string]interface{}{
"status": true,
}
tcpAliases := []string{}
c.State.AliasListeners.Range(func(key interface{}, val interface{}) bool {
tcpAlias := key.(string)
tcpAliases = append(tcpAliases, tcpAlias)
return true
})
listeners := []string{}
c.State.Listeners.Range(func(key interface{}, val interface{}) bool {
var tcpListener *net.TCPAddr
unixListener, ok := key.(*net.UnixAddr)
if !ok {
tcpListener = key.(*net.TCPAddr)
}
if unixListener != nil {
listeners = append(listeners, unixListener.String())
} else {
listeners = append(listeners, tcpListener.String())
}
return true
})
httpListeners := []string{}
c.State.HTTPListeners.Range(func(key interface{}, val interface{}) bool {
httpListener := key.(string)
httpListeners = append(httpListeners, httpListener)
return true
})
data["tcpAliases"] = tcpAliases
data["listeners"] = listeners
data["httpListeners"] = httpListeners
g.JSON(http.StatusOK, data)
}
// RouteToken returns the route token for a specific route
// RouteToken returns the route token for a specific route.
func (c *WebConsole) RouteToken(route string) (string, bool) {
token, ok := c.RouteTokens.Load(route)
routeToken := ""
@@ -385,19 +318,19 @@ func (c *WebConsole) RouteToken(route string) (string, bool) {
return routeToken, ok
}
// RouteExists check if a route exists
// RouteExists check if a route token exists.
func (c *WebConsole) RouteExists(route string) bool {
_, ok := c.RouteToken(route)
return ok
}
// AddRoute adds a route to the console
// AddRoute adds a route token to the console.
func (c *WebConsole) AddRoute(route string, token string) {
c.Clients.LoadOrStore(route, []*WebClient{})
c.RouteTokens.Store(route, token)
}
// RemoveRoute adds a route to the console
// RemoveRoute removes a route token from the console.
func (c *WebConsole) RemoveRoute(route string) {
data, ok := c.Clients.Load(route)
@@ -419,7 +352,7 @@ func (c *WebConsole) RemoveRoute(route string) {
c.RouteTokens.Delete(route)
}
// AddClient adds a client to the console
// AddClient adds a client to the console route.
func (c *WebConsole) AddClient(route string, w *WebClient) {
data, ok := c.Clients.Load(route)
@@ -438,7 +371,7 @@ func (c *WebConsole) AddClient(route string, w *WebClient) {
c.Clients.Store(route, clients)
}
// RemoveClient removes a client from the console
// RemoveClient removes a client from the console route.
func (c *WebConsole) RemoveClient(route string, w *WebClient) {
data, ok := c.Clients.Load(route)
@@ -468,7 +401,7 @@ func (c *WebConsole) RemoveClient(route string, w *WebClient) {
}
}
// BroadcastRoute sends a message to all clients on a route
// BroadcastRoute sends a message to all clients on a route.
func (c *WebConsole) BroadcastRoute(route string, message []byte) {
data, ok := c.Clients.Load(route)
@@ -487,7 +420,7 @@ func (c *WebConsole) BroadcastRoute(route string, message []byte) {
}
}
// Handle is the only place socket reads and writes happen
// Handle is the only place socket reads and writes happen.
func (c *WebClient) Handle() {
defer func() {
c.Conn.Close()

View File

@@ -7,6 +7,7 @@ import (
"log"
"net"
"sync"
"time"
"github.com/antoniomika/oxy/forward"
"github.com/antoniomika/oxy/roundrobin"
@@ -14,24 +15,36 @@ import (
"github.com/spf13/viper"
)
// ListenerType represents any listener sish supports
// ListenerType represents any listener sish supports.
type ListenerType int
const (
// AliasListener represents a tcp alias
// AliasListener represents a tcp alias.
AliasListener ListenerType = iota
// HTTPListener represents a HTTP proxy
// HTTPListener represents a HTTP proxy.
HTTPListener
// TCPListener represents a generic tcp listener
// TCPListener represents a generic tcp listener.
TCPListener
// ProcessListener represents a process specific listener
// ProcessListener represents a process specific listener.
ProcessListener
)
// ListenerHolder represents a generic listener
// LogWriter represents a writer that is used for writing logs in multiple locations.
type LogWriter struct {
TimeFmt string
MultiWriter io.Writer
}
// Write implements the write function for the LogWriter. It will add a time in a
// specific format to logs.
func (w LogWriter) Write(bytes []byte) (int, error) {
return fmt.Fprintf(w.MultiWriter, "%v | %s", time.Now().Format(w.TimeFmt), string(bytes))
}
// ListenerHolder represents a generic listener.
type ListenerHolder struct {
net.Listener
ListenAddr string
@@ -39,31 +52,34 @@ type ListenerHolder struct {
SSHConn *SSHConnection
}
// HTTPHolder holds proxy and connection info
// HTTPHolder holds proxy and connection info.
// SSHConnections is a map[string]*SSHConnection.
type HTTPHolder struct {
HTTPHost string
Scheme string
SSHConns *sync.Map
Forward *forward.Forwarder
Balancer *roundrobin.RoundRobin
HTTPHost string
Scheme string
SSHConnections *sync.Map
Forward *forward.Forwarder
Balancer *roundrobin.RoundRobin
}
// AliasHolder holds alias and connection info
// AliasHolder holds alias and connection info.
// SSHConnections is a map[string]*SSHConnection.
type AliasHolder struct {
AliasHost string
SSHConns *sync.Map
Balancer *roundrobin.RoundRobin
AliasHost string
SSHConnections *sync.Map
Balancer *roundrobin.RoundRobin
}
// TCPHolder holds proxy and connection info
// TCPHolder holds proxy and connection info.
// SSHConnections is a map[string]*SSHConnection.
type TCPHolder struct {
TCPHost string
Listener net.Listener
SSHConns *sync.Map
Balancer *roundrobin.RoundRobin
TCPHost string
Listener net.Listener
SSHConnections *sync.Map
Balancer *roundrobin.RoundRobin
}
// Handle will copy connections from one handler to a roundrobin server
// Handle will copy connections from one handler to a roundrobin server.
func (tH *TCPHolder) Handle(state *State) {
for {
cl, err := tH.Listener.Accept()
@@ -98,7 +114,7 @@ func (tH *TCPHolder) Handle(state *State) {
log.Println(logLine)
if viper.GetBool("log-to-client") {
tH.SSHConns.Range(func(key, val interface{}) bool {
tH.SSHConnections.Range(func(key, val interface{}) bool {
sshConn := val.(*SSHConnection)
sshConn.Listeners.Range(func(key, val interface{}) bool {
@@ -128,7 +144,13 @@ func (tH *TCPHolder) Handle(state *State) {
}
}
// State handles overall state
// State handles overall state. It retains mutexed maps for various
// datastructures and shared objects.
// SSHConnections is a map[string]*SSHConnection.
// Listeners is a map[string]net.Listener.
// HTTPListeners is a map[string]HTTPHolder.
// AliasListeners is a map[string]AliasHolder.
// TCPListeners is a map[string]TCPHolder.
type State struct {
Console *WebConsole
SSHConnections *sync.Map
@@ -140,7 +162,7 @@ type State struct {
LogWriter io.Writer
}
// NewState returns a new state struct
// NewState returns a new State struct.
func NewState() *State {
return &State{
SSHConnections: &sync.Map{},

View File

@@ -1,3 +1,6 @@
// Package utils implements utilities used across different
// areas of the sish application. There are utility functions
// that help with overall state management and are core to the application.
package utils
import (
@@ -29,20 +32,29 @@ import (
)
const (
// sishDNSPrefix is the prefix used for DNS TXT records.
sishDNSPrefix = "sish="
)
var (
// Filter is the IPFilter used to block connections
// Filter is the IPFilter used to block connections.
Filter *ipfilter.IPFilter
certHolder = make([]ssh.PublicKey, 0)
holderLock = sync.Mutex{}
// certHolder is a slice of publickeys for auth.
certHolder = make([]ssh.PublicKey, 0)
// holderLock is the mutex used to update the certHolder slice.
holderLock = sync.Mutex{}
// bannedSubdomainList is a list of subdomains that cannot be bound.
bannedSubdomainList = []string{""}
multiWriter io.Writer
// multiWriter is the writer that can be used for writing to multiple locations.
multiWriter io.Writer
)
// Setup main utils
// Setup main utils. This initializes, whitelists, blacklists,
// and log writers.
func Setup(logWriter io.Writer) {
multiWriter = logWriter
@@ -78,12 +90,13 @@ func Setup(logWriter io.Writer) {
}
}
// CommaSplitFields is a function used by strings.FieldsFunc to split around commas
// CommaSplitFields is a function used by strings.FieldsFunc to split around commas.
func CommaSplitFields(c rune) bool {
return c == ','
}
// GetRandomPortInRange returns a random port in the provided range
// GetRandomPortInRange returns a random port in the provided range.
// The port range is a comma separated list of ranges or ports.
func GetRandomPortInRange(portRange string) uint32 {
var bindPort uint32
@@ -133,7 +146,9 @@ func GetRandomPortInRange(portRange string) uint32 {
return bindPort
}
// CheckPort verifies if a port exists within the port range
// CheckPort verifies if a port exists within the port range.
// It will return 0 and an error if not (0 allows the kernel to select)
// the port.
func CheckPort(port uint32, portRanges string) (uint32, error) {
ranges := strings.Split(strings.TrimSpace(portRanges), ",")
checks := false
@@ -175,7 +190,7 @@ func CheckPort(port uint32, portRanges string) (uint32, error) {
return 0, fmt.Errorf("not a safe port")
}
// WatchCerts watches ssh keys for changes
// WatchCerts watches ssh keys for changes and will load them.
func WatchCerts() {
loadCerts()
watcher, err := fsnotify.NewWatcher()
@@ -214,6 +229,8 @@ func WatchCerts() {
}
}
// loadCerts loads public keys from the keys directory into a slice that is used
// authenticating a user.
func loadCerts() {
tmpCertHolder := make([]ssh.PublicKey, 0)
@@ -252,7 +269,8 @@ func loadCerts() {
certHolder = tmpCertHolder
}
// GetSSHConfig Returns an SSH config for the ssh muxer
// GetSSHConfig Returns an SSH config for the ssh muxer.
// It handles auth and storing user connection information.
func GetSSHConfig() *ssh.ServerConfig {
sshConfig := &ssh.ServerConfig{
NoClientAuth: !viper.GetBool("authentication"),
@@ -290,6 +308,8 @@ func GetSSHConfig() *ssh.ServerConfig {
return sshConfig
}
// generatePrivateKey creates a new ed25519 private key to be used by the
// the SSH server as the host key.
func generatePrivateKey(passphrase string) []byte {
_, pk, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
@@ -324,7 +344,8 @@ func generatePrivateKey(passphrase string) []byte {
return pemData
}
// ParsePrivateKey pareses the PrivateKey into a ssh.Signer and let's it be used by CASigner
// ParsePrivateKey parses the PrivateKey into a ssh.Signer and
// let's it be used by the SSH server.
func loadPrivateKey(passphrase string) ssh.Signer {
var signer ssh.Signer
@@ -348,6 +369,8 @@ func loadPrivateKey(passphrase string) ssh.Signer {
return signer
}
// inBannedList is used to scan whether or not something exists
// in a slice of data.
func inBannedList(host string, bannedList []string) bool {
for _, v := range bannedList {
if strings.TrimSpace(v) == host {
@@ -358,6 +381,9 @@ func inBannedList(host string, bannedList []string) bool {
return false
}
// verifyDNS will verify that a specific domain/subdomain combo matches
// the specific TXT entry that exists for the domain. It will check that the
// publickey used for auth is at least included in the TXT records for the domain.
func verifyDNS(addr string, sshConn *SSHConnection) (bool, string, error) {
if !viper.GetBool("verify-dns") || sshConn.SSHConn.Permissions == nil {
return false, "", nil
@@ -383,7 +409,9 @@ func verifyDNS(addr string, sshConn *SSHConnection) (bool, string, error) {
return false, "", nil
}
// GetOpenPort returns open ports
// GetOpenPort returns open ports that can be bound. It verifies the host to
// bind the port to and attempts to listen to the port to ensure it is open.
// If load balancing is enabled, it will return the port if used.
func GetOpenPort(addr string, port uint32, state *State, sshConn *SSHConnection) (string, uint32, *TCPHolder) {
getUnusedPort := func() (string, uint32, *TCPHolder) {
var tH *TCPHolder
@@ -449,7 +477,8 @@ func GetOpenPort(addr string, port uint32, state *State, sshConn *SSHConnection)
return getUnusedPort()
}
// GetOpenHost returns a random open host
// GetOpenHost returns an open host or a random host if that one is unavailable.
// If load balancing is enabled, it will return the requested domain.
func GetOpenHost(addr string, state *State, sshConn *SSHConnection) (string, *HTTPHolder) {
dnsMatch, _, err := verifyDNS(addr, sshConn)
if err != nil && viper.GetBool("debug") {
@@ -510,7 +539,8 @@ func GetOpenHost(addr string, state *State, sshConn *SSHConnection) (string, *HT
return getUnusedHost()
}
// GetOpenAlias returns open aliases
// GetOpenAlias returns open aliases or a random one if it is not enabled.
// If load balancing is enabled, it will return the requested alias.
func GetOpenAlias(addr string, port string, state *State, sshConn *SSHConnection) (string, *AliasHolder) {
getUnusedAlias := func() (string, *AliasHolder) {
var aH *AliasHolder