github.com/devops-filetransfer/sshego@v7.0.4+incompatible/config.go (about)

     1  package sshego
     2  
     3  import (
     4  	"bufio"
     5  	"flag"
     6  	"fmt"
     7  	"io"
     8  	"net"
     9  	"os"
    10  	"regexp"
    11  	"strconv"
    12  	"strings"
    13  	"sync"
    14  	"time"
    15  
    16  	ssh "github.com/glycerine/sshego/xendor/github.com/glycerine/xcryptossh"
    17  )
    18  
    19  // SshegoConfig is the top level, main config
    20  type SshegoConfig struct {
    21  	Nickname string
    22  	Halt     *ssh.Halter
    23  
    24  	KeepAliveEvery time.Duration // default 1 second.
    25  	SkipKeepAlive  bool
    26  
    27  	IdleTimeoutDur time.Duration
    28  
    29  	ConfigPath string
    30  
    31  	SSHdServer    AddrHostPort // the sshd host we are logging into remotely.
    32  	LocalToRemote TunnelSpec
    33  	RemoteToLocal TunnelSpec
    34  
    35  	Debug bool
    36  
    37  	AddIfNotKnown bool
    38  
    39  	// user login creds for client
    40  	Username             string // for client to login with.
    41  	PrivateKeyPath       string // path to user's RSA private key
    42  	ClientKnownHostsPath string // path to user's/client's known hosts
    43  
    44  	TotpUrl string
    45  	Pw      string
    46  
    47  	KnownHosts *KnownHosts
    48  
    49  	WriteConfigOut string
    50  
    51  	// if -write-config is all we are doing
    52  	WriteConfigOnly bool
    53  
    54  	Quiet bool
    55  
    56  	Esshd                  *Esshd
    57  	EmbeddedSSHdHostDbPath string
    58  	EmbeddedSSHd           AddrHostPort // optional local sshd, embedded.
    59  
    60  	HostDb *HostDb
    61  
    62  	AddUser string
    63  	DelUser string
    64  
    65  	SshegoSystemMutexPortString string
    66  	SshegoSystemMutexPort       int
    67  
    68  	MailCfg MailgunConfig
    69  
    70  	// allow less than 3FA
    71  	// Not recommended, but possible.
    72  	SkipTOTP       bool
    73  	SkipPassphrase bool
    74  	SkipRSA        bool
    75  
    76  	BitLenRSAkeys int
    77  
    78  	DirectTcp   bool
    79  	ShowVersion bool
    80  
    81  	//
    82  	// ==== testing support ====
    83  	//
    84  	Origdir, Tempdir string
    85  
    86  	// TestAllowOneshotConnect is
    87  	// a convenience for testing.
    88  	//
    89  	// If we discover and add a new
    90  	// sshd host key on this first,
    91  	// allow the connection to
    92  	// continue on without
    93  	// erroring out -- the gosshtun
    94  	// command line does this to
    95  	// teach users safe run
    96  	// practices, but under test
    97  	// it is just annoying.
    98  	TestAllowOneshotConnect bool
    99  
   100  	// for "custom-inproc-stream", etc.
   101  	CustomChannelHandlers map[string]CustomChannelHandlerCB
   102  
   103  	// SkipCommandRecv if true, says don't
   104  	// start up the CommandRecv goroutine
   105  	// on the SshegoSystemMutexPort port.
   106  	// Commandline adding users won't work.
   107  	SkipCommandRecv bool
   108  
   109  	Mut sync.Mutex
   110  
   111  	// once running:
   112  
   113  	// Underling TCP network connection
   114  	Underlying net.Conn
   115  
   116  	// once started, the SSHConnect() call
   117  	// will set this, so that cfg becomes
   118  	// all self-contained.
   119  	SshClient *ssh.Client
   120  
   121  	// NoAutoReconnect if true, turns off
   122  	// our automatic reconnect attempts when the
   123  	// connection is lost.
   124  	NoAutoReconnect bool
   125  
   126  	ClientReconnectNeededTower *UHPTower
   127  }
   128  
   129  func (cfg *SshegoConfig) ChannelHandlerSummary() (s string) {
   130  	if cfg.CustomChannelHandlers != nil {
   131  		for name := range cfg.CustomChannelHandlers {
   132  			s += fmt.Sprintf("%s, ", name)
   133  		}
   134  	}
   135  	return
   136  }
   137  
   138  func NewSshegoConfig() *SshegoConfig {
   139  
   140  	cfg := &SshegoConfig{
   141  		BitLenRSAkeys: 4096,
   142  	}
   143  	cfg.ClientReconnectNeededTower = NewUHPTower(cfg.Halt)
   144  	cfg.Reset()
   145  	return cfg
   146  }
   147  
   148  func (cfg *SshegoConfig) Reset() {
   149  	cfg.Halt = ssh.NewHalter()
   150  }
   151  
   152  // AddrHostPort is used to specify tunnel endpoints.
   153  type AddrHostPort struct {
   154  	Title          string
   155  	Addr           string
   156  	Host           string
   157  	Port           int64
   158  	UnixDomainPath string
   159  	Required       bool
   160  }
   161  
   162  // ParseAddr fills Host and Port from Addr, breaking Addr apart at the ':'
   163  // using net.SplitHostPort()
   164  func (a *AddrHostPort) ParseAddr() error {
   165  
   166  	if a.Addr == "" {
   167  		if a.Required {
   168  			return fmt.Errorf("provide -%s ip:port", a.Title)
   169  		}
   170  		return nil
   171  	}
   172  
   173  	host, port, err := net.SplitHostPort(a.Addr)
   174  	if err != nil {
   175  		return fmt.Errorf("bad -%s ip:port given; net.SplitHostPort() gave: %s", a.Title, err)
   176  	}
   177  	a.Host = host
   178  	if host == "" {
   179  		//p("defaulting empty host to 127.0.0.1")
   180  		a.Host = "127.0.0.1"
   181  	} else {
   182  		//p("in ParseAddr(%s), host is '%v'", a.Title, host)
   183  	}
   184  	if len(port) == 0 {
   185  		return fmt.Errorf("empty -%s port; no port found in '%s'", a.Title, a.Addr)
   186  	}
   187  	if port[0] == '/' {
   188  		a.UnixDomainPath = port
   189  	} else {
   190  		prt, err := strconv.ParseUint(port, 10, 16)
   191  		a.Port = int64(prt)
   192  		if err != nil {
   193  			return fmt.Errorf("bad -%s port given; could not convert "+
   194  				"to integer: %s", a.Title, err)
   195  		}
   196  	}
   197  	return nil
   198  }
   199  
   200  // TunnelSpec represents either a forward or a reverse tunnel in SshegoConfig.
   201  type TunnelSpec struct {
   202  	Listen AddrHostPort
   203  	Remote AddrHostPort
   204  }
   205  
   206  // DefineFlags should be called before myflags.Parse().
   207  func (c *SshegoConfig) DefineFlags(fs *flag.FlagSet) {
   208  
   209  	fs.StringVar(&c.ConfigPath, "cfg", "", "path to our config file")
   210  	fs.StringVar(&c.WriteConfigOut, "write-config", "", "(optional) write our config to this path before doing connections")
   211  	fs.StringVar(&c.LocalToRemote.Listen.Addr, "listen", "", "(forward tunnel) We listen on this host:port locally, securely tunnel that traffic to sshd, then send it cleartext to -remote. The forward tunnel is active if and only if -listen is given. If host starts with a '/' then we treat it as the path to a unix-domain socket to listen on, and the port can be omitted.")
   212  	fs.StringVar(&c.LocalToRemote.Remote.Addr, "remote", "", "(forward tunnel) After traversing the secured forward tunnel, -listen traffic flows in cleartext from the sshd to this host:port. The foward tunnel is active only if -listen is given too.  If host starts with a '/' then we treat it as the path to a unix-domain socket to forward to, and the port can be omitted.")
   213  
   214  	fs.StringVar(&c.RemoteToLocal.Listen.Addr, "revlisten", "", "(reverse tunnel) The sshd will listen on this host:port, securely tunnel those connections to the gosshtun application, whence they will cleartext connect to the -revfwd address. The reverse tunnel is active if and only if -revlisten is given.")
   215  	fs.StringVar(&c.RemoteToLocal.Remote.Addr, "revfwd", "127.0.0.1:22", "(reverse tunnel) The gosshtun application will receive securely tunneled connections from -revlisten on the sshd side, and cleartext forward them to this host:port. For security, it is recommended that this be 127.0.0.1:22, so that the sshd service on your gosshtun host authenticates all remotely initiated traffic. See also the -esshd option which can be used to secure the -revfwd connection as well. The reverse tunnel is active only if -revlisten is given too.")
   216  
   217  	fs.StringVar(&c.SSHdServer.Addr, "sshd", "", "The remote sshd host:port that we establish a secure tunnel to; our public key must have been already deployed there.")
   218  	fs.BoolVar(&c.AddIfNotKnown, "new", false, "allow connecting to a new sshd host key, and store it for future reference. Otherwise prevent Man-In-The-Middle attacks by rejecting unknown hosts.")
   219  	fs.BoolVar(&c.Debug, "v", false, "verbose debug mode")
   220  
   221  	user := os.Getenv("USER")
   222  	fs.StringVar(&c.Username, "user", user, "username for sshd login (default is $USER)")
   223  
   224  	home := os.Getenv("HOME")
   225  	fs.StringVar(&c.PrivateKeyPath, "key", home+"/.ssh/id_rsa_nopw", "private key for sshd login")
   226  	fs.StringVar(&c.ClientKnownHostsPath, "known-hosts", home+"/.ssh/.sshego.cli.known.hosts", "path to sshego's own known-hosts file")
   227  
   228  	fs.BoolVar(&c.Quiet, "quiet", false, "if -quiet is given, we don't log to stdout as each connection is made. The default is false; we log each tunneled connection.")
   229  	fs.StringVar(&c.EmbeddedSSHd.Addr, "esshd", "", "(optional) start an in-process embedded sshd (server), binding this host:port, with both RSA key and 2FA checking; useful for securing -revfwd connections. Example: 127.0.0.1:2022")
   230  	fs.StringVar(&c.EmbeddedSSHdHostDbPath, "esshd-host-db", home+"/.ssh/.sshego.sshd.db", "(only matters if -esshd is given) path to database holding sshd persistent state such as our host key, registered 2FA secrets, etc.")
   231  	fs.StringVar(&c.AddUser, "adduser", "", "we will add this user to the known users database, generate a password, RSA key, and a 2FA secret/QR code.")
   232  	fs.StringVar(&c.DelUser, "deluser", "", "we will delete this user from the known users database.")
   233  	fs.IntVar(&c.SshegoSystemMutexPort, "xport", 33355, "localhost tcp-port used for internal syncrhonization and commands such as adding users to running esshd; we must be able to acquire this exclusively for our use on 127.0.0.1. If negative then we don't bind it.")
   234  
   235  	fs.BoolVar(&c.SkipTOTP, "skip-totp", false, "(under -esshd and -adduser) skip time-based-one-time-password authentication requirement.")
   236  	fs.BoolVar(&c.SkipPassphrase, "skip-pass", false, "(under -esshd and -adduser) skip passphrase authentication requirement.")
   237  	fs.BoolVar(&c.SkipRSA, "skip-rsa", false, "(under -esshd and -adduser) skip RSA key authentication requirement.")
   238  	fs.IntVar(&c.BitLenRSAkeys, "bits", 4096, "(under -adduser and for new host keys) number of bits in the generated RSA keys. note the one-time wait to generate: 10000 bits would offer terrific security, but will take between 1-8 minutes to generate such a key.")
   239  	fs.BoolVar(&c.ShowVersion, "version", false, "show the code version")
   240  	c.MailCfg.DefineFlags(fs)
   241  
   242  	c.SSHdServer.Title = "sshd"
   243  	c.EmbeddedSSHd.Title = "esshd"
   244  	c.LocalToRemote.Listen.Title = "listen"
   245  	c.LocalToRemote.Remote.Title = "remote"
   246  	c.RemoteToLocal.Listen.Title = "revlisten"
   247  	c.RemoteToLocal.Remote.Title = "revremote"
   248  }
   249  
   250  // ValidateConfig should be called after myflags.Parse().
   251  func (c *SshegoConfig) ValidateConfig() error {
   252  
   253  	if c.ConfigPath != "" {
   254  		err := c.LoadConfig(c.ConfigPath)
   255  		if err != nil {
   256  			return err
   257  		}
   258  	}
   259  
   260  	// Verbose causes a data race, make it constant for now.
   261  	//	if c.Debug {
   262  	//      Verbose = true
   263  	//	}
   264  
   265  	var err error
   266  	err = c.LocalToRemote.Listen.ParseAddr()
   267  	if err != nil {
   268  		return err
   269  	}
   270  
   271  	err = c.LocalToRemote.Remote.ParseAddr()
   272  	if err != nil {
   273  		return err
   274  	}
   275  
   276  	if c.LocalToRemote.Listen.Addr != "" && c.LocalToRemote.Remote.Addr == "" {
   277  		return fmt.Errorf("incomplete config: have -listen but not -remote")
   278  	}
   279  
   280  	err = c.RemoteToLocal.Listen.ParseAddr()
   281  	if err != nil {
   282  		return err
   283  	}
   284  
   285  	err = c.RemoteToLocal.Remote.ParseAddr()
   286  	if err != nil {
   287  		return err
   288  	}
   289  
   290  	if c.RemoteToLocal.Listen.Addr != "" && c.RemoteToLocal.Remote.Addr == "" {
   291  		return fmt.Errorf("incomplete config: have -revlisten but not -revfwd")
   292  	}
   293  
   294  	if c.RemoteToLocal.Listen.Addr == "" &&
   295  		c.LocalToRemote.Listen.Addr == "" &&
   296  		c.EmbeddedSSHd.Addr == "" &&
   297  		c.AddUser == "" &&
   298  		c.DelUser == "" {
   299  
   300  		if c.WriteConfigOut == "" {
   301  			return fmt.Errorf("no tunnels requested; one of -listen or -revlisten or -esshd is required")
   302  		} else {
   303  			c.WriteConfigOnly = true
   304  		}
   305  	}
   306  
   307  	err = c.SSHdServer.ParseAddr()
   308  	if err != nil {
   309  		return err
   310  	}
   311  
   312  	// MailgunConfig
   313  	err = c.MailCfg.ValidateConfig()
   314  	if err != nil {
   315  		return err
   316  	}
   317  
   318  	return nil
   319  }
   320  
   321  // LoadConfig reads configuration from a file, expecting
   322  // KEY=value pair on each line;
   323  // values optionally enclosed in double quotes.
   324  func (c *SshegoConfig) LoadConfig(path string) error {
   325  	if !fileExists(path) {
   326  		return fmt.Errorf("path '%s' does not exist", path)
   327  	}
   328  
   329  	file, err := os.OpenFile(path, os.O_RDONLY, 0)
   330  	if err != nil {
   331  		return err
   332  	}
   333  	defer file.Close()
   334  
   335  	bufIn := bufio.NewReader(file)
   336  	lineNum := int64(1)
   337  	for {
   338  		lastLine, err := bufIn.ReadBytes('\n')
   339  		if err != nil && err != io.EOF {
   340  			return err
   341  		}
   342  
   343  		if err == io.EOF && len(lastLine) == 0 {
   344  			break
   345  		}
   346  		line := string(lastLine)
   347  		line = strings.Trim(line, "\n\r\t ")
   348  
   349  		if len(line) > 0 && line[0] == '#' {
   350  			// comment, ignore
   351  		} else {
   352  
   353  			splt := strings.SplitN(line, "=", 2)
   354  			if len(splt) != 2 {
   355  				/*fmt.Fprintf(os.Stderr, "ignoring malformed (path: '%s') "+
   356  				"config line(%v): '%s'\n",
   357  				path, lineNum, line)
   358  				*/
   359  				continue
   360  			}
   361  			key := strings.Trim(splt[0], "\t\n\r ")
   362  			val := strings.Trim(splt[1], "\t\n\r ")
   363  
   364  			val = trim(val)
   365  
   366  			switch key {
   367  			case "SSHD_ADDR":
   368  				c.SSHdServer.Addr = val
   369  			case "FWD_LISTEN_ADDR":
   370  				c.LocalToRemote.Listen.Addr = val
   371  			case "FWD_REMOTE_ADDR":
   372  				c.LocalToRemote.Remote.Addr = val
   373  			case "REV_LISTEN_ADDR":
   374  				c.RemoteToLocal.Listen.Addr = val
   375  			case "REV_REMOTE_ADDR":
   376  				c.RemoteToLocal.Remote.Addr = val
   377  			case "SSHD_LOGIN_USERNAME":
   378  				c.Username = subEnv(val, "USER")
   379  			case "SSH_PRIVATE_KEY_PATH":
   380  				c.PrivateKeyPath = subEnv(val, "HOME")
   381  			case "SSH_KNOWN_HOSTS_PATH":
   382  				c.ClientKnownHostsPath = subEnv(val, "HOME")
   383  			case "QUIET":
   384  				c.Quiet = stringToBool(val)
   385  			case "EMBEDDED_SSHD_HOST_DB_PATH":
   386  				c.EmbeddedSSHdHostDbPath = subEnv(val, "HOME")
   387  			case "EMBEDDED_SSHD_LISTEN_ADDR":
   388  				c.EmbeddedSSHd.Addr = val
   389  			case "EMBEDDED_SSHD_COMMAND_XPORT":
   390  				c.SshegoSystemMutexPortString = val
   391  				prt, err := strconv.Atoi(val)
   392  				panicOn(err)
   393  				c.SshegoSystemMutexPort = prt
   394  			case "AUTH_OPTION_SKIP_TOTP":
   395  				c.SkipTOTP = stringToBool(val)
   396  			case "AUTH_OPTION_SKIP_PASSPHRASE":
   397  				c.SkipPassphrase = stringToBool(val)
   398  			case "AUTH_OPTION_SKIP_RSA":
   399  				c.SkipRSA = stringToBool(val)
   400  			case "KEYGEN_RSA_BITS":
   401  				bits, err := strconv.Atoi(val)
   402  				panicOn(err)
   403  				c.BitLenRSAkeys = bits
   404  			}
   405  		}
   406  		lineNum++
   407  
   408  		if err == io.EOF {
   409  			break
   410  		}
   411  	}
   412  
   413  	err = c.MailCfg.LoadConfig(path)
   414  	if err != nil {
   415  		return fmt.Errorf("path '%s' gave error on "+
   416  			"loading MailgunConfig: %s",
   417  			path, err)
   418  	}
   419  
   420  	return nil
   421  }
   422  
   423  // SaveConfig writes the config structs to the given io.Writer
   424  func (c *SshegoConfig) SaveConfig(fd io.Writer) error {
   425  
   426  	_, err := fmt.Fprintf(fd, `#
   427  # config file sshego:
   428  #
   429  `)
   430  	if err != nil {
   431  		return err
   432  	}
   433  
   434  	fmt.Fprintf(fd, "SSHD_ADDR=\"%s\"\n", c.SSHdServer.Addr)
   435  	fmt.Fprintf(fd, "FWD_LISTEN_ADDR=\"%s\"\n", c.LocalToRemote.Listen.Addr)
   436  	fmt.Fprintf(fd, "FWD_REMOTE_ADDR=\"%s\"\n", c.LocalToRemote.Remote.Addr)
   437  	fmt.Fprintf(fd, "REV_LISTEN_ADDR=\"%s\"\n", c.RemoteToLocal.Listen.Addr)
   438  	fmt.Fprintf(fd, "REV_REMOTE_ADDR=\"%s\"\n", c.RemoteToLocal.Remote.Addr)
   439  	fmt.Fprintf(fd, "SSHD_LOGIN_USERNAME=\"%s\"\n", c.Username)
   440  	fmt.Fprintf(fd, "SSH_PRIVATE_KEY_PATH=\"%s\"\n", c.PrivateKeyPath)
   441  	fmt.Fprintf(fd, "SSH_KNOWN_HOSTS_PATH=\"%s\"\n", c.ClientKnownHostsPath)
   442  	fmt.Fprintf(fd, "QUIET=\"%s\"\n", boolToString(c.Quiet))
   443  
   444  	fmt.Fprintf(fd, "#\n# optional sshd server config\n#\n")
   445  	fmt.Fprintf(fd, "EMBEDDED_SSHD_HOST_DB_PATH=\"%s\"\n", c.EmbeddedSSHdHostDbPath)
   446  	fmt.Fprintf(fd, "EMBEDDED_SSHD_LISTEN_ADDR=\"%s\"\n", c.EmbeddedSSHd.Addr)
   447  	c.SshegoSystemMutexPortString = fmt.Sprintf(
   448  		"%v", c.SshegoSystemMutexPort)
   449  	fmt.Fprintf(fd, "EMBEDDED_SSHD_COMMAND_XPORT=\"%s\"\n", c.SshegoSystemMutexPortString)
   450  
   451  	fmt.Fprintf(fd, "#\n# auth config\n#\n")
   452  	fmt.Fprintf(fd, "AUTH_OPTION_SKIP_TOTP=\"%s\"\n",
   453  		boolToString(c.SkipTOTP))
   454  	fmt.Fprintf(fd, "AUTH_OPTION_SKIP_PASSPHRASE=\"%s\"\n",
   455  		boolToString(c.SkipPassphrase))
   456  	fmt.Fprintf(fd, "AUTH_OPTION_SKIP_RSA=\"%s\"\n",
   457  		boolToString(c.SkipRSA))
   458  	fmt.Fprintf(fd, "KEYGEN_RSA_BITS=\"%v\"\n", c.BitLenRSAkeys)
   459  
   460  	err = c.MailCfg.SaveConfig(fd)
   461  	return err
   462  }
   463  
   464  func trim(s string) string {
   465  	if s == "" {
   466  		return s
   467  	}
   468  	n := len(s)
   469  	if s[n-1] == '\n' {
   470  		s = s[:n-1]
   471  		n--
   472  	}
   473  	if len(s) < 2 {
   474  		return s
   475  	}
   476  	if s[0] == '"' && s[n-1] == '"' {
   477  		s = s[1 : n-1]
   478  	}
   479  	return s
   480  }
   481  
   482  func subEnv(src string, fromEnv string) string {
   483  	homeRegex := regexp.MustCompile(`\$` + fromEnv)
   484  	home := os.Getenv(fromEnv)
   485  	return homeRegex.ReplaceAllString(src, home)
   486  }
   487  
   488  func boolToString(b bool) string {
   489  	if b {
   490  		return "true"
   491  	}
   492  	return "false"
   493  }
   494  
   495  func stringToBool(s string) bool {
   496  	if strings.ToLower(s) == "true" {
   497  		return true
   498  	}
   499  	return false
   500  }
   501  
   502  func (cfg *SshegoConfig) GenAuthString() string {
   503  	s := ""
   504  	// "RSA, phone-app, and memorable pass-phrase"
   505  
   506  	count := 0
   507  	if !cfg.SkipRSA {
   508  		count++
   509  	}
   510  	if !cfg.SkipTOTP {
   511  		count++
   512  	}
   513  	if !cfg.SkipPassphrase {
   514  		count++
   515  	}
   516  	added := 0
   517  	if !cfg.SkipRSA {
   518  		s = "RSA"
   519  		added++
   520  	}
   521  	if !cfg.SkipTOTP {
   522  		if added > 0 {
   523  			switch count {
   524  			case 1:
   525  			case 2:
   526  				s += " and "
   527  			default:
   528  				s += ", "
   529  			}
   530  		}
   531  		s += "phone-app"
   532  		added++
   533  	}
   534  	if !cfg.SkipPassphrase {
   535  		switch added {
   536  		case 0:
   537  		case 1:
   538  			s += " and "
   539  		case 2:
   540  			s += ", and"
   541  		}
   542  		s += "memorable pass-phrase"
   543  	}
   544  
   545  	return s
   546  }