// Reload [re]initializes the TrafficRulesSet with the rules data // in the specified file. This function obtains a write lock on // the database, blocking all readers. When Reload fails, the previous // state is retained. func (set *TrafficRulesSet) Reload(ruleSetFilename string) error { set.Lock() defer set.Unlock() if ruleSetFilename == "" { // No traffic rules filename in the config return nil } configJSON, err := ioutil.ReadFile(ruleSetFilename) if err != nil { return psiphon.ContextError(err) } var newSet TrafficRulesSet err = json.Unmarshal(configJSON, &newSet) if err != nil { return psiphon.ContextError(err) } set.DefaultRules = newSet.DefaultRules set.RegionalRules = newSet.RegionalRules return nil }
// convertHTTPRequestToAPIRequest converts the HTTP request query // parameters and request body to the JSON object import format // expected by the API request handlers. func convertHTTPRequestToAPIRequest( w http.ResponseWriter, r *http.Request, requestBodyName string) (requestJSONObject, error) { params := make(requestJSONObject) for name, values := range r.URL.Query() { for _, value := range values { params[name] = value // Note: multiple values per name are ignored break } } if requestBodyName != "" { r.Body = http.MaxBytesReader(w, r.Body, MAX_API_PARAMS_SIZE) body, err := ioutil.ReadAll(r.Body) if err != nil { return nil, psiphon.ContextError(err) } var bodyParams requestJSONObject err = json.Unmarshal(body, &bodyParams) if err != nil { return nil, psiphon.ContextError(err) } params[requestBodyName] = bodyParams } return params, nil }
// UpdateRedisForLegacyPsiWeb sets the Psiphon session and discovery records for // a new SSH connection following the conventions of the legacy psi_web component. // This facility is used so psi_web can use the GeoIP values the SSH server has // resolved for the user connection. // The redis database indexes, expiry values, and record schemas all match the // legacy psi_web configuration. func UpdateRedisForLegacyPsiWeb(psiphonSessionID string, geoIPData GeoIPData) error { redisSessionDBIndex := 0 // Discard sessions older than 60 minutes sessionExpireSeconds := 60 * 60 sessionRecord, err := json.Marshal( struct { Country string `json:"region"` City string `json:"city"` ISP string `json:"isp"` }{geoIPData.Country, geoIPData.City, geoIPData.ISP}) if err != nil { return psiphon.ContextError(err) } redisDiscoveryDBIndex := 1 // Discard discovery records older than 5 minutes discoveryExpireSeconds := 60 * 5 discoveryRecord, err := json.Marshal( struct { DiscoveryValue int `json:"client_ip_address_strategy_value"` }{geoIPData.DiscoveryValue}) if err != nil { return psiphon.ContextError(err) } conn := redisPool.Get() // Note: using SET with NX (set if not exists) so as to not clobber // any existing records set by an upstream connection server (i.e., // meek server). We allow expiry deadline extension unconditionally. conn.Send("MULTI") conn.Send("SELECT", redisSessionDBIndex) // http://redis.io/commands/set -- NX/EX options require Redis 2.6.12 //conn.Send("SET", psiphonSessionID, string(sessionRecord), "NX", "EX", sessionExpireSeconds) conn.Send("SETNX", psiphonSessionID, string(sessionRecord)) conn.Send("EXPIRE", psiphonSessionID, sessionExpireSeconds) conn.Send("SELECT", redisDiscoveryDBIndex) //conn.Send("SET", psiphonSessionID, string(discoveryRecord), "NX", "EX", discoveryExpireSeconds) conn.Send("SETNX", psiphonSessionID, string(discoveryRecord)) conn.Send("EXPIRE", psiphonSessionID, discoveryExpireSeconds) _, err = conn.Do("EXEC") if err != nil { return psiphon.ContextError(err) } return nil }
// makeMeekSessionID creates a new session ID. The variable size is intended to // frustrate traffic analysis of both plaintext and TLS meek traffic. func makeMeekSessionID() (string, error) { size := MEEK_MIN_SESSION_ID_LENGTH n, err := psiphon.MakeSecureRandomInt(MEEK_MAX_SESSION_ID_LENGTH - MEEK_MIN_SESSION_ID_LENGTH) if err != nil { return "", psiphon.ContextError(err) } size += n sessionID, err := psiphon.MakeRandomStringBase64(size) if err != nil { return "", psiphon.ContextError(err) } return sessionID, nil }
func (sshClient *sshClient) passwordCallback(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) { var sshPasswordPayload struct { SessionId string `json:"SessionId"` SshPassword string `json:"SshPassword"` } err := json.Unmarshal(password, &sshPasswordPayload) if err != nil { // Backwards compatibility case: instead of a JSON payload, older clients // send the hex encoded session ID prepended to the SSH password. // Note: there's an even older case where clients don't send any session ID, // but that's no longer supported. if len(password) == 2*psiphon.PSIPHON_API_CLIENT_SESSION_ID_LENGTH+2*SSH_PASSWORD_BYTE_LENGTH { sshPasswordPayload.SessionId = string(password[0 : 2*psiphon.PSIPHON_API_CLIENT_SESSION_ID_LENGTH]) sshPasswordPayload.SshPassword = string(password[2*psiphon.PSIPHON_API_CLIENT_SESSION_ID_LENGTH : len(password)]) } else { return nil, psiphon.ContextError(fmt.Errorf("invalid password payload for %q", conn.User())) } } if !isHexDigits(sshClient.sshServer.support, sshPasswordPayload.SessionId) { return nil, psiphon.ContextError(fmt.Errorf("invalid session ID for %q", conn.User())) } userOk := (subtle.ConstantTimeCompare( []byte(conn.User()), []byte(sshClient.sshServer.support.Config.SSHUserName)) == 1) passwordOk := (subtle.ConstantTimeCompare( []byte(sshPasswordPayload.SshPassword), []byte(sshClient.sshServer.support.Config.SSHPassword)) == 1) if !userOk || !passwordOk { return nil, psiphon.ContextError(fmt.Errorf("invalid password for %q", conn.User())) } psiphonSessionID := sshPasswordPayload.SessionId sshClient.Lock() sshClient.psiphonSessionID = psiphonSessionID geoIPData := sshClient.geoIPData sshClient.Unlock() // Store the GeoIP data associated with the session ID. This makes the GeoIP data // available to the web server for web transport Psiphon API requests. sshClient.sshServer.support.GeoIPService.SetSessionCache( psiphonSessionID, geoIPData) return nil, nil }
// NewMeekServer initializes a new meek server. func NewMeekServer( support *SupportServices, listener net.Listener, useTLS bool, clientHandler func(clientConn net.Conn), stopBroadcast <-chan struct{}) (*MeekServer, error) { meekServer := &MeekServer{ support: support, listener: listener, clientHandler: clientHandler, openConns: new(psiphon.Conns), stopBroadcast: stopBroadcast, sessions: make(map[string]*meekSession), } if useTLS { tlsConfig, err := makeMeekTLSConfig(support) if err != nil { return nil, psiphon.ContextError(err) } meekServer.tlsConfig = tlsConfig } return meekServer, nil }
// makeMeekTLSConfig creates a TLS config for a meek HTTPS listener. // Currently, this config is optimized for fronted meek where the nature // of the connection is non-circumvention; it's optimized for performance // assuming the peer is an uncensored CDN. func makeMeekTLSConfig(support *SupportServices) (*tls.Config, error) { certificate, privateKey, err := GenerateWebServerCertificate( support.Config.MeekCertificateCommonName) if err != nil { return nil, psiphon.ContextError(err) } tlsCertificate, err := tls.X509KeyPair( []byte(certificate), []byte(privateKey)) if err != nil { return nil, psiphon.ContextError(err) } return &tls.Config{ Certificates: []tls.Certificate{tlsCertificate}, NextProtos: []string{"http/1.1"}, MinVersion: tls.VersionTLS10, // This is a reordering of the supported CipherSuites in golang 1.6. Non-ephemeral key // CipherSuites greatly reduce server load, and we try to select these since the meek // protocol is providing obfuscation, not privacy/integrity (this is provided by the // tunneled SSH), so we don't benefit from the perfect forward secrecy property provided // by ephemeral key CipherSuites. // https://github.com/golang/go/blob/1cb3044c9fcd88e1557eca1bf35845a4108bc1db/src/crypto/tls/cipher_suites.go#L75 CipherSuites: []uint16{ tls.TLS_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_RSA_WITH_RC4_128_SHA, tls.TLS_RSA_WITH_AES_128_CBC_SHA, tls.TLS_RSA_WITH_AES_256_CBC_SHA, tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA, tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, }, PreferServerCipherSuites: true, }, nil }
// InitLogging configures a logger according to the specified // config params. If not called, the default logger set by the // package init() is used. // When configured, InitLogging also establishes a local syslog // logger specifically for fail2ban integration. // Concurrenty note: should only be called from the main // goroutine. func InitLogging(config *Config) error { level, err := logrus.ParseLevel(config.LogLevel) if err != nil { return psiphon.ContextError(err) } hooks := make(logrus.LevelHooks) var syslogHook *logrus_syslog.SyslogHook if config.SyslogFacility != "" { syslogHook, err = logrus_syslog.NewSyslogHook( "", "", getSyslogPriority(config), config.SyslogTag) if err != nil { return psiphon.ContextError(err) } hooks.Add(syslogHook) } log = &ContextLogger{ &logrus.Logger{ Out: os.Stderr, Formatter: new(logrus.TextFormatter), Hooks: hooks, Level: level, }, } if config.Fail2BanFormat != "" { fail2BanFormat = config.Fail2BanFormat fail2BanWriter, err = syslog.Dial( "", "", syslog.LOG_AUTH|syslog.LOG_INFO, config.SyslogTag) if err != nil { return psiphon.ContextError(err) } } return nil }
func (sshClient *sshClient) passwordCallback(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) { var sshPasswordPayload struct { SessionId string `json:"SessionId"` SshPassword string `json:"SshPassword"` } err := json.Unmarshal(password, &sshPasswordPayload) if err != nil { return nil, psiphon.ContextError(fmt.Errorf("invalid password payload for %q", conn.User())) } userOk := (subtle.ConstantTimeCompare( []byte(conn.User()), []byte(sshClient.sshServer.config.SSHUserName)) == 1) passwordOk := (subtle.ConstantTimeCompare( []byte(sshPasswordPayload.SshPassword), []byte(sshClient.sshServer.config.SSHPassword)) == 1) if !userOk || !passwordOk { return nil, psiphon.ContextError(fmt.Errorf("invalid password for %q", conn.User())) } psiphonSessionID := sshPasswordPayload.SessionId sshClient.Lock() sshClient.psiphonSessionID = psiphonSessionID geoIPData := sshClient.geoIPData sshClient.Unlock() if sshClient.sshServer.config.UseRedis() { err = UpdateRedisForLegacyPsiWeb(psiphonSessionID, geoIPData) if err != nil { log.WithContextFields(LogFields{ "psiphonSessionID": psiphonSessionID, "error": err}).Warning("UpdateRedisForLegacyPsiWeb failed") // Allow the connection to proceed; legacy psi_web will not get accurate GeoIP values. } } return nil, nil }
// InitGeoIP opens a GeoIP2/GeoLite2 MaxMind database and prepares // it for lookups. func InitGeoIP(config *Config) error { discoveryValueHMACKey = config.DiscoveryValueHMACKey if config.GeoIPDatabaseFilename != "" { var err error geoIPReader, err = maxminddb.Open(config.GeoIPDatabaseFilename) if err != nil { return psiphon.ContextError(err) } log.WithContext().Info("GeoIP initialized") } return nil }
func newSSHServer( support *SupportServices, shutdownBroadcast <-chan struct{}) (*sshServer, error) { privateKey, err := ssh.ParseRawPrivateKey([]byte(support.Config.SSHPrivateKey)) if err != nil { return nil, psiphon.ContextError(err) } // TODO: use cert (ssh.NewCertSigner) for anti-fingerprint? signer, err := ssh.NewSignerFromKey(privateKey) if err != nil { return nil, psiphon.ContextError(err) } return &sshServer{ support: support, shutdownBroadcast: shutdownBroadcast, sshHostKey: signer, nextClientID: 1, acceptedClientCounts: make(map[string]int64), clients: make(map[sshClientID]*sshClient), }, nil }
// NewSupportServices initializes a new SupportServices. func NewSupportServices(config *Config) (*SupportServices, error) { trafficRulesSet, err := NewTrafficRulesSet(config.TrafficRulesFilename) if err != nil { return nil, psiphon.ContextError(err) } psinetDatabase, err := psinet.NewDatabase(config.PsinetDatabaseFilename) if err != nil { return nil, psiphon.ContextError(err) } geoIPService, err := NewGeoIPService( config.GeoIPDatabaseFilename, config.DiscoveryValueHMACKey) if err != nil { return nil, psiphon.ContextError(err) } return &SupportServices{ Config: config, TrafficRulesSet: trafficRulesSet, PsinetDatabase: psinetDatabase, GeoIPService: geoIPService, }, nil }
// NewTunnelServer initializes a new tunnel server. func NewTunnelServer( support *SupportServices, shutdownBroadcast <-chan struct{}) (*TunnelServer, error) { sshServer, err := newSSHServer(support, shutdownBroadcast) if err != nil { return nil, psiphon.ContextError(err) } return &TunnelServer{ runWaitGroup: new(sync.WaitGroup), listenerError: make(chan error), shutdownBroadcast: shutdownBroadcast, sshServer: sshServer, }, nil }
// LoadConfig loads and validates a JSON encoded server config. If more than one // JSON config is specified, then all are loaded and values are merged together, // in order. Multiple configs allows for use cases like storing static, server-specific // values in a base config while also deploying network-wide throttling settings // in a secondary file that can be paved over on all server hosts. func LoadConfig(configJSONs [][]byte) (*Config, error) { // Note: default values are set in GenerateConfig var config Config for _, configJSON := range configJSONs { err := json.Unmarshal(configJSON, &config) if err != nil { return nil, psiphon.ContextError(err) } } if config.Fail2BanFormat != "" && strings.Count(config.Fail2BanFormat, "%s") != 1 { return nil, errors.New("Fail2BanFormat must have one '%%s' placeholder") } if config.ServerIPAddress == "" { return nil, errors.New("ServerIPAddress is missing from config file") } if config.WebServerPort > 0 && (config.WebServerSecret == "" || config.WebServerCertificate == "" || config.WebServerPrivateKey == "") { return nil, errors.New( "web server requires WebServerSecret, WebServerCertificate, WebServerPrivateKey") } if config.SSHServerPort > 0 && (config.SSHPrivateKey == "" || config.SSHServerVersion == "" || config.SSHUserName == "" || config.SSHPassword == "") { return nil, errors.New( "SSH server requires SSHPrivateKey, SSHServerVersion, SSHUserName, SSHPassword") } if config.ObfuscatedSSHServerPort > 0 && (config.SSHPrivateKey == "" || config.SSHServerVersion == "" || config.SSHUserName == "" || config.SSHPassword == "" || config.ObfuscatedSSHKey == "") { return nil, errors.New( "Obfuscated SSH server requires SSHPrivateKey, SSHServerVersion, SSHUserName, SSHPassword, ObfuscatedSSHKey") } return &config, nil }
// getMeekCookiePayload extracts the payload from a meek cookie. The cookie // paylod is base64 encoded, obfuscated, and NaCl encrypted. func getMeekCookiePayload(support *SupportServices, cookieValue string) ([]byte, error) { decodedValue, err := base64.StdEncoding.DecodeString(cookieValue) if err != nil { return nil, psiphon.ContextError(err) } // The data consists of an obfuscated seed message prepended // to the obfuscated, encrypted payload. The server obfuscator // will read the seed message, leaving the remaining encrypted // data in the reader. reader := bytes.NewReader(decodedValue[:]) obfuscator, err := psiphon.NewServerObfuscator( reader, &psiphon.ObfuscatorConfig{Keyword: support.Config.MeekObfuscatedKey}) if err != nil { return nil, psiphon.ContextError(err) } offset, err := reader.Seek(0, 1) if err != nil { return nil, psiphon.ContextError(err) } encryptedPayload := decodedValue[offset:] obfuscator.ObfuscateClientToServer(encryptedPayload) var nonce [24]byte var privateKey, ephemeralPublicKey [32]byte decodedPrivateKey, err := base64.StdEncoding.DecodeString( support.Config.MeekCookieEncryptionPrivateKey) if err != nil { return nil, psiphon.ContextError(err) } copy(privateKey[:], decodedPrivateKey) if len(encryptedPayload) < 32 { return nil, psiphon.ContextError(errors.New("unexpected encrypted payload size")) } copy(ephemeralPublicKey[0:32], encryptedPayload[0:32]) payload, ok := box.Open(nil, encryptedPayload[32:], &nonce, &ephemeralPublicKey, &privateKey) if !ok { return nil, psiphon.ContextError(errors.New("open box failed")) } return payload, nil }
func copyWithThrottle(dst io.Writer, src io.Reader, throttleSleepMilliseconds int) (int64, error) { // TODO: use a low-memory io.Copy? if throttleSleepMilliseconds <= 0 { // No throttle return io.Copy(dst, src) } var totalBytes int64 for { bytes, err := io.CopyN(dst, src, SSH_THROTTLED_PORT_FORWARD_MAX_COPY) totalBytes += bytes if err == io.EOF { err = nil break } if err != nil { return totalBytes, psiphon.ContextError(err) } time.Sleep(time.Duration(throttleSleepMilliseconds) * time.Millisecond) } return totalBytes, nil }
func generateWebServerCertificate() (string, string, error) { // Based on https://golang.org/src/crypto/tls/generate_cert.go // TODO: use other key types: anti-fingerprint by varying params rsaKey, err := rsa.GenerateKey(rand.Reader, WEB_SERVER_CERTIFICATE_RSA_KEY_BITS) if err != nil { return "", "", psiphon.ContextError(err) } notBefore := time.Now() notAfter := notBefore.Add(WEB_SERVER_CERTIFICATE_VALIDITY_PERIOD) // TODO: psi_ops_install sets serial number to 0? // TOSO: psi_ops_install sets RSA exponent to 3, digest type to 'sha1', and version to 2? serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) if err != nil { return "", "", psiphon.ContextError(err) } template := x509.Certificate{ // TODO: psi_ops_install leaves subject blank? /* Subject: pkix.Name{ Organization: []string{""}, }, IPAddresses: ... */ SerialNumber: serialNumber, NotBefore: notBefore, NotAfter: notAfter, KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, IsCA: true, } derCert, err := x509.CreateCertificate(rand.Reader, &template, &template, rsaKey.Public(), rsaKey) if err != nil { return "", "", psiphon.ContextError(err) } webServerCertificate := pem.EncodeToMemory( &pem.Block{ Type: "CERTIFICATE", Bytes: derCert, }, ) webServerPrivateKey := pem.EncodeToMemory( &pem.Block{ Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(rsaKey), }, ) return string(webServerCertificate), string(webServerPrivateKey), nil }
// GenerateConfig create a new Psiphon server config. It returns a JSON // encoded config and a client-compatible "server entry" for the server. It // generates all necessary secrets and key material, which are emitted in // the config file and server entry as necessary. func GenerateConfig(params *GenerateConfigParams) ([]byte, []byte, error) { // TODO: support disabling web server or a subset of protocols serverIPaddress := params.ServerIPAddress if serverIPaddress == "" { serverIPaddress = DEFAULT_SERVER_IP_ADDRESS } // Web server config webServerPort := params.WebServerPort if webServerPort == 0 { webServerPort = DEFAULT_WEB_SERVER_PORT } webServerSecret, err := psiphon.MakeRandomString(WEB_SERVER_SECRET_BYTE_LENGTH) if err != nil { return nil, nil, psiphon.ContextError(err) } webServerCertificate, webServerPrivateKey, err := generateWebServerCertificate() if err != nil { return nil, nil, psiphon.ContextError(err) } // SSH config sshServerPort := params.SSHServerPort if sshServerPort == 0 { sshServerPort = DEFAULT_SSH_SERVER_PORT } // TODO: use other key types: anti-fingerprint by varying params rsaKey, err := rsa.GenerateKey(rand.Reader, SSH_RSA_HOST_KEY_BITS) if err != nil { return nil, nil, psiphon.ContextError(err) } sshPrivateKey := pem.EncodeToMemory( &pem.Block{ Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(rsaKey), }, ) signer, err := ssh.NewSignerFromKey(rsaKey) if err != nil { return nil, nil, psiphon.ContextError(err) } sshPublicKey := signer.PublicKey() sshUserNameSuffix, err := psiphon.MakeRandomString(SSH_USERNAME_SUFFIX_BYTE_LENGTH) if err != nil { return nil, nil, psiphon.ContextError(err) } sshUserName := "******" + sshUserNameSuffix sshPassword, err := psiphon.MakeRandomString(SSH_PASSWORD_BYTE_LENGTH) if err != nil { return nil, nil, psiphon.ContextError(err) } // TODO: vary version string for anti-fingerprint sshServerVersion := "SSH-2.0-Psiphon" // Obfuscated SSH config obfuscatedSSHServerPort := params.ObfuscatedSSHServerPort if obfuscatedSSHServerPort == 0 { obfuscatedSSHServerPort = DEFAULT_OBFUSCATED_SSH_SERVER_PORT } obfuscatedSSHKey, err := psiphon.MakeRandomString(SSH_OBFUSCATED_KEY_BYTE_LENGTH) if err != nil { return nil, nil, psiphon.ContextError(err) } // Assemble config and server entry config := &Config{ LogLevel: DEFAULT_LOG_LEVEL, SyslogFacility: "", SyslogTag: DEFAULT_SYSLOG_TAG, Fail2BanFormat: "", DiscoveryValueHMACKey: "", GeoIPDatabaseFilename: DEFAULT_GEO_IP_DATABASE_FILENAME, ServerIPAddress: serverIPaddress, WebServerPort: webServerPort, WebServerSecret: webServerSecret, WebServerCertificate: webServerCertificate, WebServerPrivateKey: webServerPrivateKey, SSHPrivateKey: string(sshPrivateKey), SSHServerVersion: sshServerVersion, SSHUserName: sshUserName, SSHPassword: sshPassword, SSHServerPort: sshServerPort, ObfuscatedSSHKey: obfuscatedSSHKey, ObfuscatedSSHServerPort: obfuscatedSSHServerPort, RedisServerAddress: "", } encodedConfig, err := json.MarshalIndent(config, "\n", " ") if err != nil { return nil, nil, psiphon.ContextError(err) } // Server entry format omits the BEGIN/END lines and newlines lines := strings.Split(webServerCertificate, "\n") strippedWebServerCertificate := strings.Join(lines[1:len(lines)-2], "") capabilities := []string{ psiphon.GetCapability(psiphon.TUNNEL_PROTOCOL_SSH), psiphon.GetCapability(psiphon.TUNNEL_PROTOCOL_OBFUSCATED_SSH), } serverEntry := &psiphon.ServerEntry{ IpAddress: serverIPaddress, WebServerPort: fmt.Sprintf("%d", webServerPort), WebServerSecret: webServerSecret, WebServerCertificate: strippedWebServerCertificate, SshPort: sshServerPort, SshUsername: sshUserName, SshPassword: sshPassword, SshHostKey: base64.RawStdEncoding.EncodeToString(sshPublicKey.Marshal()), SshObfuscatedPort: obfuscatedSSHServerPort, SshObfuscatedKey: obfuscatedSSHKey, Capabilities: capabilities, Region: "US", } encodedServerEntry, err := psiphon.EncodeServerEntry(serverEntry) if err != nil { return nil, nil, psiphon.ContextError(err) } return encodedConfig, []byte(encodedServerEntry), nil }
// RunServices initializes support functions including logging, GeoIP service, and // redis connection pooling; and then starts the server components and runs them // until os.Interrupt or os.Kill signals are received. The config determines // which components are run. func RunServices(encodedConfigs [][]byte) error { config, err := LoadConfig(encodedConfigs) if err != nil { log.WithContextFields(LogFields{"error": err}).Error("load config failed") return psiphon.ContextError(err) } err = InitLogging(config) if err != nil { log.WithContextFields(LogFields{"error": err}).Error("init logging failed") return psiphon.ContextError(err) } err = InitGeoIP(config) if err != nil { log.WithContextFields(LogFields{"error": err}).Error("init GeoIP failed") return psiphon.ContextError(err) } if config.UseRedis() { err = InitRedis(config) if err != nil { log.WithContextFields(LogFields{"error": err}).Error("init redis failed") return psiphon.ContextError(err) } } waitGroup := new(sync.WaitGroup) shutdownBroadcast := make(chan struct{}) errors := make(chan error) if config.RunWebServer() { waitGroup.Add(1) go func() { defer waitGroup.Done() err := RunWebServer(config, shutdownBroadcast) select { case errors <- err: default: } }() } if config.RunSSHServer() { waitGroup.Add(1) go func() { defer waitGroup.Done() err := RunSSHServer(config, shutdownBroadcast) select { case errors <- err: default: } }() } if config.RunObfuscatedSSHServer() { waitGroup.Add(1) go func() { defer waitGroup.Done() err := RunObfuscatedSSHServer(config, shutdownBroadcast) select { case errors <- err: default: } }() } // An OS signal triggers an orderly shutdown systemStopSignal := make(chan os.Signal, 1) signal.Notify(systemStopSignal, os.Interrupt, os.Kill) err = nil select { case <-systemStopSignal: log.WithContext().Info("shutdown by system") case err = <-errors: log.WithContextFields(LogFields{"error": err}).Error("service failed") } close(shutdownBroadcast) waitGroup.Wait() return err }
// runSSHServer runs an SSH or Obfuscated SSH server. In the Obfuscated SSH case, an // ObfuscatedSSHConn is layered in front of the client TCP connection; otherwise, both // modes are identical. // // runSSHServer listens on the designated port and spawns new goroutines to handle // each client connection. It halts when shutdownBroadcast is signaled. A list of active // clients is maintained, and when halting all clients are first shutdown. // // Each client goroutine handles its own obfuscation (optional), SSH handshake, SSH // authentication, and then looping on client new channel requests. At this time, only // "direct-tcpip" channels, dynamic port fowards, are expected and supported. // // A new goroutine is spawned to handle each port forward. Each port forward tracks its // bytes transferred. Overall per-client stats for connection duration, GeoIP, number of // port forwards, and bytes transferred are tracked and logged when the client shuts down. func runSSHServer( config *Config, useObfuscation bool, shutdownBroadcast <-chan struct{}) error { privateKey, err := ssh.ParseRawPrivateKey([]byte(config.SSHPrivateKey)) if err != nil { return psiphon.ContextError(err) } // TODO: use cert (ssh.NewCertSigner) for anti-fingerprint? signer, err := ssh.NewSignerFromKey(privateKey) if err != nil { return psiphon.ContextError(err) } sshServer := &sshServer{ config: config, useObfuscation: useObfuscation, shutdownBroadcast: shutdownBroadcast, sshHostKey: signer, nextClientID: 1, clients: make(map[sshClientID]*sshClient), } var serverPort int if useObfuscation { serverPort = config.ObfuscatedSSHServerPort } else { serverPort = config.SSHServerPort } listener, err := net.Listen( "tcp", fmt.Sprintf("%s:%d", config.ServerIPAddress, serverPort)) if err != nil { return psiphon.ContextError(err) } log.WithContextFields( LogFields{ "useObfuscation": useObfuscation, "port": serverPort, }).Info("starting") err = nil errors := make(chan error) waitGroup := new(sync.WaitGroup) waitGroup.Add(1) go func() { defer waitGroup.Done() loop: for { conn, err := listener.Accept() select { case <-shutdownBroadcast: if err == nil { conn.Close() } break loop default: } if err != nil { if e, ok := err.(net.Error); ok && e.Temporary() { log.WithContextFields(LogFields{"error": err}).Error("accept failed") // Temporary error, keep running continue } select { case errors <- psiphon.ContextError(err): default: } break loop } // process each client connection concurrently go sshServer.handleClient(conn.(*net.TCPConn)) } sshServer.stopClients() log.WithContextFields( LogFields{"useObfuscation": useObfuscation}).Info("stopped") }() select { case <-shutdownBroadcast: case err = <-errors: } listener.Close() waitGroup.Wait() log.WithContextFields( LogFields{"useObfuscation": useObfuscation}).Info("exiting") return err }
// RunWebServer runs a web server which supports tunneled and untunneled // Psiphon API requests. // // The HTTP request handlers are light wrappers around the base Psiphon // API request handlers from the SSH API transport. The SSH API transport // is preferred by new clients; however the web API transport is still // required for untunneled final status requests. The web API transport // may be retired once untunneled final status requests are made obsolete // (e.g., by server-side bytes transferred stats, by client-side local // storage of stats for retry, or some other future development). // // The API is compatible with all tunnel-core clients but not backwards // compatible with older clients. // func RunWebServer( support *SupportServices, shutdownBroadcast <-chan struct{}) error { webServer := &webServer{ support: support, } serveMux := http.NewServeMux() serveMux.HandleFunc("/handshake", webServer.handshakeHandler) serveMux.HandleFunc("/connected", webServer.connectedHandler) serveMux.HandleFunc("/status", webServer.statusHandler) serveMux.HandleFunc("/client_verification", webServer.clientVerificationHandler) certificate, err := tls.X509KeyPair( []byte(support.Config.WebServerCertificate), []byte(support.Config.WebServerPrivateKey)) if err != nil { return psiphon.ContextError(err) } tlsConfig := &tls.Config{ Certificates: []tls.Certificate{certificate}, } // TODO: inherits global log config? logWriter := NewLogWriter() defer logWriter.Close() server := &psiphon.HTTPSServer{ http.Server{ MaxHeaderBytes: MAX_API_PARAMS_SIZE, Handler: serveMux, TLSConfig: tlsConfig, ReadTimeout: WEB_SERVER_READ_TIMEOUT, WriteTimeout: WEB_SERVER_WRITE_TIMEOUT, ErrorLog: golanglog.New(logWriter, "", 0), }, } listener, err := net.Listen( "tcp", fmt.Sprintf("%s:%d", support.Config.ServerIPAddress, support.Config.WebServerPort)) if err != nil { return psiphon.ContextError(err) } log.WithContext().Info("starting") err = nil errors := make(chan error) waitGroup := new(sync.WaitGroup) waitGroup.Add(1) go func() { defer waitGroup.Done() // Note: will be interrupted by listener.Close() err := server.ServeTLS(listener) // Can't check for the exact error that Close() will cause in Accept(), // (see: https://code.google.com/p/go/issues/detail?id=4373). So using an // explicit stop signal to stop gracefully. select { case <-shutdownBroadcast: default: if err != nil { select { case errors <- psiphon.ContextError(err): default: } } } log.WithContext().Info("stopped") }() select { case <-shutdownBroadcast: case err = <-errors: } listener.Close() waitGroup.Wait() log.WithContext().Info("exiting") return err }
// Run runs the tunnel server; this function blocks while running a selection of // listeners that handle connection using various obfuscation protocols. // // Run listens on each designated tunnel port and spawns new goroutines to handle // each client connection. It halts when shutdownBroadcast is signaled. A list of active // clients is maintained, and when halting all clients are cleanly shutdown. // // Each client goroutine handles its own obfuscation (optional), SSH handshake, SSH // authentication, and then looping on client new channel requests. "direct-tcpip" // channels, dynamic port fowards, are supported. When the UDPInterceptUdpgwServerAddress // config parameter is configured, UDP port forwards over a TCP stream, following // the udpgw protocol, are handled. // // A new goroutine is spawned to handle each port forward for each client. Each port // forward tracks its bytes transferred. Overall per-client stats for connection duration, // GeoIP, number of port forwards, and bytes transferred are tracked and logged when the // client shuts down. func (server *TunnelServer) Run() error { type sshListener struct { net.Listener localAddress string tunnelProtocol string } // TODO: should TunnelServer hold its own support pointer? support := server.sshServer.support // First bind all listeners; once all are successful, // start accepting connections on each. var listeners []*sshListener for tunnelProtocol, listenPort := range support.Config.TunnelProtocolPorts { localAddress := fmt.Sprintf( "%s:%d", support.Config.ServerIPAddress, listenPort) listener, err := net.Listen("tcp", localAddress) if err != nil { for _, existingListener := range listeners { existingListener.Listener.Close() } return psiphon.ContextError(err) } log.WithContextFields( LogFields{ "localAddress": localAddress, "tunnelProtocol": tunnelProtocol, }).Info("listening") listeners = append( listeners, &sshListener{ Listener: listener, localAddress: localAddress, tunnelProtocol: tunnelProtocol, }) } for _, listener := range listeners { server.runWaitGroup.Add(1) go func(listener *sshListener) { defer server.runWaitGroup.Done() log.WithContextFields( LogFields{ "localAddress": listener.localAddress, "tunnelProtocol": listener.tunnelProtocol, }).Info("running") server.sshServer.runListener( listener.Listener, server.listenerError, listener.tunnelProtocol) log.WithContextFields( LogFields{ "localAddress": listener.localAddress, "tunnelProtocol": listener.tunnelProtocol, }).Info("stopped") }(listener) } var err error select { case <-server.shutdownBroadcast: case err = <-server.listenerError: } for _, listener := range listeners { listener.Close() } server.sshServer.stopClients() server.runWaitGroup.Wait() log.WithContext().Info("stopped") return err }
// getSession returns the meek client session corresponding the // meek cookie/session ID. If no session is found, the cookie is // treated as a meek cookie for a new session and its payload is // extracted and used to establish a new session. func (server *MeekServer) getSession( request *http.Request, meekCookie *http.Cookie) (string, *meekSession, error) { // Check for an existing session server.sessionsLock.RLock() existingSessionID := meekCookie.Value session, ok := server.sessions[existingSessionID] server.sessionsLock.RUnlock() if ok { session.touch() return existingSessionID, session, nil } // TODO: can multiple http client connections using same session cookie // cause race conditions on session struct? // The session is new (or expired). Treat the cookie value as a new meek // cookie, extract the payload, and create a new session. payloadJSON, err := getMeekCookiePayload(server.support, meekCookie.Value) if err != nil { return "", nil, psiphon.ContextError(err) } // Note: this meek server ignores all but Version MeekProtocolVersion; // the other values are legacy or currently unused. var clientSessionData struct { MeekProtocolVersion int `json:"v"` PsiphonClientSessionId string `json:"s"` PsiphonServerAddress string `json:"p"` } err = json.Unmarshal(payloadJSON, &clientSessionData) if err != nil { return "", nil, psiphon.ContextError(err) } // Determine the client remote address, which is used for geolocation // and stats. When an intermediate proxy of CDN is in use, we may be // able to determine the original client address by inspecting HTTP // headers such as X-Forwarded-For. clientIP := strings.Split(request.RemoteAddr, ":")[0] if len(server.support.Config.MeekProxyForwardedForHeaders) > 0 { for _, header := range server.support.Config.MeekProxyForwardedForHeaders { value := request.Header.Get(header) if len(value) > 0 { // Some headers, such as X-Forwarded-For, are a comma-separated // list of IPs (each proxy in a chain). The first IP should be // the client IP. proxyClientIP := strings.Split(header, ",")[0] if net.ParseIP(clientIP) != nil { clientIP = proxyClientIP break } } } } // Create a new meek conn that will relay the payload // between meek request/responses and the tunnel server client // handler. The client IP is also used to initialize the // meek conn with a useful value to return when the tunnel // server calls conn.RemoteAddr() to get the client's IP address. // Assumes clientIP is a value IP address; the port value is a stub // and is expected to be ignored. clientConn := newMeekConn( &net.TCPAddr{ IP: net.ParseIP(clientIP), Port: 0, }, clientSessionData.MeekProtocolVersion) session = &meekSession{ clientConn: clientConn, meekProtocolVersion: clientSessionData.MeekProtocolVersion, sessionIDSent: false, } session.touch() // Note: MEEK_PROTOCOL_VERSION_1 doesn't support changing the // meek cookie to a session ID; v1 clients always send the // original meek cookie value with each request. The issue with // v1 is that clients which wake after a device sleep will attempt // to resume a meek session and the server can't differentiate // between resuming a session and creating a new session. This // causes the v1 client connection to hang/timeout. sessionID := meekCookie.Value if clientSessionData.MeekProtocolVersion >= MEEK_PROTOCOL_VERSION_2 { sessionID, err = makeMeekSessionID() if err != nil { return "", nil, psiphon.ContextError(err) } } server.sessionsLock.Lock() server.sessions[sessionID] = session server.sessionsLock.Unlock() // Note: from the tunnel server's perspective, this client connection // will close when closeSessionHelper calls Close() on the meekConn. server.clientHandler(session.clientConn) return sessionID, session, nil }
// RunServices initializes support functions including logging and GeoIP services; // and then starts the server components and runs them until os.Interrupt or // os.Kill signals are received. The config determines which components are run. func RunServices(configJSON []byte) error { config, err := LoadConfig(configJSON) if err != nil { log.WithContextFields(LogFields{"error": err}).Error("load config failed") return psiphon.ContextError(err) } err = InitLogging(config) if err != nil { log.WithContextFields(LogFields{"error": err}).Error("init logging failed") return psiphon.ContextError(err) } supportServices, err := NewSupportServices(config) if err != nil { log.WithContextFields(LogFields{"error": err}).Error("init support services failed") return psiphon.ContextError(err) } waitGroup := new(sync.WaitGroup) shutdownBroadcast := make(chan struct{}) errors := make(chan error) tunnelServer, err := NewTunnelServer(supportServices, shutdownBroadcast) if err != nil { log.WithContextFields(LogFields{"error": err}).Error("init tunnel server failed") return psiphon.ContextError(err) } if config.RunLoadMonitor() { waitGroup.Add(1) go func() { waitGroup.Done() ticker := time.NewTicker(time.Duration(config.LoadMonitorPeriodSeconds) * time.Second) defer ticker.Stop() for { select { case <-shutdownBroadcast: return case <-ticker.C: logServerLoad(tunnelServer) } } }() } if config.RunWebServer() { waitGroup.Add(1) go func() { defer waitGroup.Done() err := RunWebServer(supportServices, shutdownBroadcast) select { case errors <- err: default: } }() } // The tunnel server is always run; it launches multiple // listeners, depending on which tunnel protocols are enabled. waitGroup.Add(1) go func() { defer waitGroup.Done() err := tunnelServer.Run() select { case errors <- err: default: } }() // An OS signal triggers an orderly shutdown systemStopSignal := make(chan os.Signal, 1) signal.Notify(systemStopSignal, os.Interrupt, os.Kill) // SIGUSR1 triggers a reload of support services reloadSupportServicesSignal := make(chan os.Signal, 1) signal.Notify(reloadSupportServicesSignal, syscall.SIGUSR1) // SIGUSR2 triggers an immediate load log logServerLoadSignal := make(chan os.Signal, 1) signal.Notify(logServerLoadSignal, syscall.SIGUSR2) err = nil loop: for { select { case <-reloadSupportServicesSignal: supportServices.Reload() case <-logServerLoadSignal: logServerLoad(tunnelServer) case <-systemStopSignal: log.WithContext().Info("shutdown by system") break loop case err = <-errors: log.WithContextFields(LogFields{"error": err}).Error("service failed") break loop } } close(shutdownBroadcast) waitGroup.Wait() return err }
// LoadConfig loads and validates a JSON encoded server config. func LoadConfig(configJSON []byte) (*Config, error) { var config Config err := json.Unmarshal(configJSON, &config) if err != nil { return nil, psiphon.ContextError(err) } if config.Fail2BanFormat != "" && strings.Count(config.Fail2BanFormat, "%s") != 1 { return nil, errors.New("Fail2BanFormat must have one '%%s' placeholder") } if config.ServerIPAddress == "" { return nil, errors.New("ServerIPAddress is missing from config file") } if config.WebServerPort > 0 && (config.WebServerSecret == "" || config.WebServerCertificate == "" || config.WebServerPrivateKey == "") { return nil, errors.New( "Web server requires WebServerSecret, WebServerCertificate, WebServerPrivateKey") } for tunnelProtocol, _ := range config.TunnelProtocolPorts { if psiphon.TunnelProtocolUsesSSH(tunnelProtocol) || psiphon.TunnelProtocolUsesObfuscatedSSH(tunnelProtocol) { if config.SSHPrivateKey == "" || config.SSHServerVersion == "" || config.SSHUserName == "" || config.SSHPassword == "" { return nil, fmt.Errorf( "Tunnel protocol %s requires SSHPrivateKey, SSHServerVersion, SSHUserName, SSHPassword", tunnelProtocol) } } if psiphon.TunnelProtocolUsesObfuscatedSSH(tunnelProtocol) { if config.ObfuscatedSSHKey == "" { return nil, fmt.Errorf( "Tunnel protocol %s requires ObfuscatedSSHKey", tunnelProtocol) } } if psiphon.TunnelProtocolUsesMeekHTTP(tunnelProtocol) || psiphon.TunnelProtocolUsesMeekHTTPS(tunnelProtocol) { if config.MeekCookieEncryptionPrivateKey == "" || config.MeekObfuscatedKey == "" { return nil, fmt.Errorf( "Tunnel protocol %s requires MeekCookieEncryptionPrivateKey, MeekObfuscatedKey", tunnelProtocol) } } if psiphon.TunnelProtocolUsesMeekHTTPS(tunnelProtocol) { if config.MeekCertificateCommonName == "" { return nil, fmt.Errorf( "Tunnel protocol %s requires MeekCertificateCommonName", tunnelProtocol) } } } validateNetworkAddress := func(address string) error { host, port, err := net.SplitHostPort(address) if err == nil && net.ParseIP(host) == nil { err = errors.New("Host must be an IP address") } if err == nil { _, err = strconv.Atoi(port) } return err } if config.UDPForwardDNSServerAddress != "" { if err := validateNetworkAddress(config.UDPForwardDNSServerAddress); err != nil { return nil, fmt.Errorf("UDPForwardDNSServerAddress is invalid: %s", err) } } if config.UDPInterceptUdpgwServerAddress != "" { if err := validateNetworkAddress(config.UDPInterceptUdpgwServerAddress); err != nil { return nil, fmt.Errorf("UDPInterceptUdpgwServerAddress is invalid: %s", err) } } return &config, nil }
// Stub implementation of net.Conn.SetWriteDeadline func (conn *meekConn) SetWriteDeadline(t time.Time) error { return psiphon.ContextError(errors.New("not supported")) }
func readUdpgwMessage( reader io.Reader, buffer []byte) (*udpProtocolMessage, error) { // udpgw message layout: // // | 2 byte size | 3 byte header | 6 or 18 byte address | variable length packet | for { // Read message _, err := io.ReadFull(reader, buffer[0:2]) if err != nil { return nil, psiphon.ContextError(err) } size := uint16(buffer[0]) + uint16(buffer[1])<<8 if int(size) > len(buffer)-2 { return nil, psiphon.ContextError(errors.New("invalid udpgw message size")) } _, err = io.ReadFull(reader, buffer[2:2+size]) if err != nil { return nil, psiphon.ContextError(err) } flags := buffer[2] connID := uint16(buffer[3]) + uint16(buffer[4])<<8 // Ignore udpgw keep-alive messages -- read another message if flags&udpgwProtocolFlagKeepalive == udpgwProtocolFlagKeepalive { continue } // Read address var remoteIP []byte var remotePort uint16 var packetStart, packetEnd int if flags&udpgwProtocolFlagIPv6 == udpgwProtocolFlagIPv6 { if size < 21 { return nil, psiphon.ContextError(errors.New("invalid udpgw message size")) } remoteIP = make([]byte, 16) copy(remoteIP, buffer[5:21]) remotePort = uint16(buffer[21]) + uint16(buffer[22])<<8 packetStart = 23 packetEnd = 23 + int(size) - 2 } else { if size < 9 { return nil, psiphon.ContextError(errors.New("invalid udpgw message size")) } remoteIP = make([]byte, 4) copy(remoteIP, buffer[5:9]) remotePort = uint16(buffer[9]) + uint16(buffer[10])<<8 packetStart = 11 packetEnd = 11 + int(size) - 2 } // Assemble message // Note: udpProtocolMessage.packet references memory in the input buffer message := &udpProtocolMessage{ connID: connID, preambleSize: packetStart, remoteIP: remoteIP, remotePort: remotePort, discardExistingConn: flags&udpgwProtocolFlagRebind == udpgwProtocolFlagRebind, forwardDNS: flags&udpgwProtocolFlagDNS == udpgwProtocolFlagDNS, packet: buffer[packetStart:packetEnd], } return message, nil } }
// runListener is intended to run an a goroutine; it blocks // running a particular listener. If an unrecoverable error // occurs, it will send the error to the listenerError channel. func (sshServer *sshServer) runListener( listener net.Listener, listenerError chan<- error, tunnelProtocol string) { handleClient := func(clientConn net.Conn) { // process each client connection concurrently go sshServer.handleClient(tunnelProtocol, clientConn) } // Note: when exiting due to a unrecoverable error, be sure // to try to send the error to listenerError so that the outer // TunnelServer.Run will properly shut down instead of remaining // running. if psiphon.TunnelProtocolUsesMeekHTTP(tunnelProtocol) || psiphon.TunnelProtocolUsesMeekHTTPS(tunnelProtocol) { meekServer, err := NewMeekServer( sshServer.support, listener, psiphon.TunnelProtocolUsesMeekHTTPS(tunnelProtocol), handleClient, sshServer.shutdownBroadcast) if err != nil { select { case listenerError <- psiphon.ContextError(err): default: } return } meekServer.Run() } else { for { conn, err := listener.Accept() select { case <-sshServer.shutdownBroadcast: if err == nil { conn.Close() } return default: } if err != nil { if e, ok := err.(net.Error); ok && e.Temporary() { log.WithContextFields(LogFields{"error": err}).Error("accept failed") // Temporary error, keep running continue } select { case listenerError <- psiphon.ContextError(err): default: } return } handleClient(conn) } } }
// GenerateConfig creates a new Psiphon server config. It returns JSON // encoded configs and a client-compatible "server entry" for the server. It // generates all necessary secrets and key material, which are emitted in // the config file and server entry as necessary. // GenerateConfig uses sample values for many fields. The intention is for // generated configs to be used for testing or as a template for production // setup, not to generate production-ready configurations. func GenerateConfig(params *GenerateConfigParams) ([]byte, []byte, []byte, error) { // Input validation if net.ParseIP(params.ServerIPAddress) == nil { return nil, nil, nil, psiphon.ContextError(errors.New("invalid IP address")) } if len(params.TunnelProtocolPorts) == 0 { return nil, nil, nil, psiphon.ContextError(errors.New("no tunnel protocols")) } usedPort := make(map[int]bool) if params.WebServerPort != 0 { usedPort[params.WebServerPort] = true } usingMeek := false for protocol, port := range params.TunnelProtocolPorts { if !psiphon.Contains(psiphon.SupportedTunnelProtocols, protocol) { return nil, nil, nil, psiphon.ContextError(errors.New("invalid tunnel protocol")) } if usedPort[port] { return nil, nil, nil, psiphon.ContextError(errors.New("duplicate listening port")) } usedPort[port] = true if psiphon.TunnelProtocolUsesMeekHTTP(protocol) || psiphon.TunnelProtocolUsesMeekHTTPS(protocol) { usingMeek = true } } // Web server config var webServerSecret, webServerCertificate, webServerPrivateKey string if params.WebServerPort != 0 { var err error webServerSecret, err = psiphon.MakeRandomStringHex(WEB_SERVER_SECRET_BYTE_LENGTH) if err != nil { return nil, nil, nil, psiphon.ContextError(err) } webServerCertificate, webServerPrivateKey, err = GenerateWebServerCertificate("") if err != nil { return nil, nil, nil, psiphon.ContextError(err) } } // SSH config // TODO: use other key types: anti-fingerprint by varying params rsaKey, err := rsa.GenerateKey(rand.Reader, SSH_RSA_HOST_KEY_BITS) if err != nil { return nil, nil, nil, psiphon.ContextError(err) } sshPrivateKey := pem.EncodeToMemory( &pem.Block{ Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(rsaKey), }, ) signer, err := ssh.NewSignerFromKey(rsaKey) if err != nil { return nil, nil, nil, psiphon.ContextError(err) } sshPublicKey := signer.PublicKey() sshUserNameSuffix, err := psiphon.MakeRandomStringHex(SSH_USERNAME_SUFFIX_BYTE_LENGTH) if err != nil { return nil, nil, nil, psiphon.ContextError(err) } sshUserName := "******" + sshUserNameSuffix sshPassword, err := psiphon.MakeRandomStringHex(SSH_PASSWORD_BYTE_LENGTH) if err != nil { return nil, nil, nil, psiphon.ContextError(err) } // TODO: vary version string for anti-fingerprint sshServerVersion := "SSH-2.0-Psiphon" // Obfuscated SSH config obfuscatedSSHKey, err := psiphon.MakeRandomStringHex(SSH_OBFUSCATED_KEY_BYTE_LENGTH) if err != nil { return nil, nil, nil, psiphon.ContextError(err) } // Meek config var meekCookieEncryptionPublicKey, meekCookieEncryptionPrivateKey, meekObfuscatedKey string if usingMeek { rawMeekCookieEncryptionPublicKey, rawMeekCookieEncryptionPrivateKey, err := box.GenerateKey(rand.Reader) if err != nil { return nil, nil, nil, psiphon.ContextError(err) } meekCookieEncryptionPublicKey = base64.StdEncoding.EncodeToString(rawMeekCookieEncryptionPublicKey[:]) meekCookieEncryptionPrivateKey = base64.StdEncoding.EncodeToString(rawMeekCookieEncryptionPrivateKey[:]) meekObfuscatedKey, err = psiphon.MakeRandomStringHex(SSH_OBFUSCATED_KEY_BYTE_LENGTH) if err != nil { return nil, nil, nil, psiphon.ContextError(err) } } // Other config discoveryValueHMACKey, err := psiphon.MakeRandomStringBase64(DISCOVERY_VALUE_KEY_BYTE_LENGTH) if err != nil { return nil, nil, nil, psiphon.ContextError(err) } // Assemble configs and server entry // Note: this config is intended for either testing or as an illustrative // example or template and is not intended for production deployment. config := &Config{ LogLevel: "info", Fail2BanFormat: "Authentication failure for psiphon-client from %s", GeoIPDatabaseFilename: "", HostID: "example-host-id", ServerIPAddress: params.ServerIPAddress, DiscoveryValueHMACKey: discoveryValueHMACKey, WebServerPort: params.WebServerPort, WebServerSecret: webServerSecret, WebServerCertificate: webServerCertificate, WebServerPrivateKey: webServerPrivateKey, SSHPrivateKey: string(sshPrivateKey), SSHServerVersion: sshServerVersion, SSHUserName: sshUserName, SSHPassword: sshPassword, ObfuscatedSSHKey: obfuscatedSSHKey, TunnelProtocolPorts: params.TunnelProtocolPorts, UDPForwardDNSServerAddress: "8.8.8.8:53", UDPInterceptUdpgwServerAddress: "127.0.0.1:7300", MeekCookieEncryptionPrivateKey: meekCookieEncryptionPrivateKey, MeekObfuscatedKey: meekObfuscatedKey, MeekCertificateCommonName: "www.example.org", MeekProhibitedHeaders: nil, MeekProxyForwardedForHeaders: []string{"X-Forwarded-For"}, LoadMonitorPeriodSeconds: 300, TrafficRulesFilename: params.TrafficRulesFilename, LogFilename: params.LogFilename, Fail2BanLogFilename: params.Fail2BanLogFilename, } encodedConfig, err := json.MarshalIndent(config, "\n", " ") if err != nil { return nil, nil, nil, psiphon.ContextError(err) } trafficRulesSet := &TrafficRulesSet{ DefaultRules: TrafficRules{ DefaultLimits: RateLimits{ DownstreamUnlimitedBytes: 0, DownstreamBytesPerSecond: 0, UpstreamUnlimitedBytes: 0, UpstreamBytesPerSecond: 0, }, IdleTCPPortForwardTimeoutMilliseconds: 30000, IdleUDPPortForwardTimeoutMilliseconds: 30000, MaxTCPPortForwardCount: 1024, MaxUDPPortForwardCount: 32, AllowTCPPorts: nil, AllowUDPPorts: nil, DenyTCPPorts: nil, DenyUDPPorts: nil, }, } encodedTrafficRulesSet, err := json.MarshalIndent(trafficRulesSet, "\n", " ") if err != nil { return nil, nil, nil, psiphon.ContextError(err) } capabilities := []string{} if params.EnableSSHAPIRequests { capabilities = append(capabilities, psiphon.CAPABILITY_SSH_API_REQUESTS) } if params.WebServerPort != 0 { capabilities = append(capabilities, psiphon.CAPABILITY_UNTUNNELED_WEB_API_REQUESTS) } for protocol, _ := range params.TunnelProtocolPorts { capabilities = append(capabilities, psiphon.GetCapability(protocol)) } sshPort := params.TunnelProtocolPorts["SSH"] obfuscatedSSHPort := params.TunnelProtocolPorts["OSSH"] // Meek port limitations // - fronted meek protocols are hard-wired in the client to be port 443 or 80. // - only one other meek port may be specified. meekPort := params.TunnelProtocolPorts["UNFRONTED-MEEK-OSSH"] if meekPort == 0 { meekPort = params.TunnelProtocolPorts["UNFRONTED-MEEK-HTTPS-OSSH"] } // Note: fronting params are a stub; this server entry will exercise // client and server fronting code paths, but not actually traverse // a fronting hop. serverEntryWebServerPort := "" strippedWebServerCertificate := "" if params.WebServerPort != 0 { serverEntryWebServerPort = fmt.Sprintf("%d", params.WebServerPort) // Server entry format omits the BEGIN/END lines and newlines lines := strings.Split(webServerCertificate, "\n") strippedWebServerCertificate = strings.Join(lines[1:len(lines)-2], "") } serverEntry := &psiphon.ServerEntry{ IpAddress: params.ServerIPAddress, WebServerPort: serverEntryWebServerPort, WebServerSecret: webServerSecret, WebServerCertificate: strippedWebServerCertificate, SshPort: sshPort, SshUsername: sshUserName, SshPassword: sshPassword, SshHostKey: base64.RawStdEncoding.EncodeToString(sshPublicKey.Marshal()), SshObfuscatedPort: obfuscatedSSHPort, SshObfuscatedKey: obfuscatedSSHKey, Capabilities: capabilities, Region: "US", MeekServerPort: meekPort, MeekCookieEncryptionPublicKey: meekCookieEncryptionPublicKey, MeekObfuscatedKey: meekObfuscatedKey, MeekFrontingHosts: []string{params.ServerIPAddress}, MeekFrontingAddresses: []string{params.ServerIPAddress}, MeekFrontingDisableSNI: false, } encodedServerEntry, err := psiphon.EncodeServerEntry(serverEntry) if err != nil { return nil, nil, nil, psiphon.ContextError(err) } return encodedConfig, encodedTrafficRulesSet, []byte(encodedServerEntry), nil }
func (sshServer *sshServer) handleClient(tunnelProtocol string, clientConn net.Conn) { sshServer.registerAcceptedClient(tunnelProtocol) defer sshServer.unregisterAcceptedClient(tunnelProtocol) geoIPData := sshServer.support.GeoIPService.Lookup( psiphon.IPAddressFromAddr(clientConn.RemoteAddr())) // TODO: apply reload of TrafficRulesSet to existing clients sshClient := newSshClient( sshServer, tunnelProtocol, geoIPData, sshServer.support.TrafficRulesSet.GetTrafficRules(geoIPData.Country)) // Wrap the base client connection with an ActivityMonitoredConn which will // terminate the connection if no data is received before the deadline. This // timeout is in effect for the entire duration of the SSH connection. Clients // must actively use the connection or send SSH keep alive requests to keep // the connection active. activityConn := psiphon.NewActivityMonitoredConn( clientConn, SSH_CONNECTION_READ_DEADLINE, false, nil) clientConn = activityConn // Further wrap the connection in a rate limiting ThrottledConn. rateLimits := sshClient.trafficRules.GetRateLimits(tunnelProtocol) clientConn = psiphon.NewThrottledConn( clientConn, rateLimits.DownstreamUnlimitedBytes, int64(rateLimits.DownstreamBytesPerSecond), rateLimits.UpstreamUnlimitedBytes, int64(rateLimits.UpstreamBytesPerSecond)) // Run the initial [obfuscated] SSH handshake in a goroutine so we can both // respect shutdownBroadcast and implement a specific handshake timeout. // The timeout is to reclaim network resources in case the handshake takes // too long. type sshNewServerConnResult struct { conn net.Conn sshConn *ssh.ServerConn channels <-chan ssh.NewChannel requests <-chan *ssh.Request err error } resultChannel := make(chan *sshNewServerConnResult, 2) if SSH_HANDSHAKE_TIMEOUT > 0 { time.AfterFunc(time.Duration(SSH_HANDSHAKE_TIMEOUT), func() { resultChannel <- &sshNewServerConnResult{err: errors.New("ssh handshake timeout")} }) } go func(conn net.Conn) { sshServerConfig := &ssh.ServerConfig{ PasswordCallback: sshClient.passwordCallback, AuthLogCallback: sshClient.authLogCallback, ServerVersion: sshServer.support.Config.SSHServerVersion, } sshServerConfig.AddHostKey(sshServer.sshHostKey) result := &sshNewServerConnResult{} // Wrap the connection in an SSH deobfuscator when required. if psiphon.TunnelProtocolUsesObfuscatedSSH(tunnelProtocol) { // Note: NewObfuscatedSshConn blocks on network I/O // TODO: ensure this won't block shutdown conn, result.err = psiphon.NewObfuscatedSshConn( psiphon.OBFUSCATION_CONN_MODE_SERVER, clientConn, sshServer.support.Config.ObfuscatedSSHKey) if result.err != nil { result.err = psiphon.ContextError(result.err) } } if result.err == nil { result.sshConn, result.channels, result.requests, result.err = ssh.NewServerConn(conn, sshServerConfig) } resultChannel <- result }(clientConn) var result *sshNewServerConnResult select { case result = <-resultChannel: case <-sshServer.shutdownBroadcast: // Close() will interrupt an ongoing handshake // TODO: wait for goroutine to exit before returning? clientConn.Close() return } if result.err != nil { clientConn.Close() // This is a Debug log due to noise. The handshake often fails due to I/O // errors as clients frequently interrupt connections in progress when // client-side load balancing completes a connection to a different server. log.WithContextFields(LogFields{"error": result.err}).Debug("handshake failed") return } sshClient.Lock() sshClient.sshConn = result.sshConn sshClient.activityConn = activityConn sshClient.Unlock() clientID, ok := sshServer.registerEstablishedClient(sshClient) if !ok { clientConn.Close() log.WithContext().Warning("register failed") return } defer sshServer.unregisterEstablishedClient(clientID) sshClient.runClient(result.channels, result.requests) // Note: sshServer.unregisterClient calls sshClient.Close(), // which also closes underlying transport Conn. }