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 }