func newSSHServer( support *SupportServices, shutdownBroadcast <-chan struct{}) (*sshServer, error) { privateKey, err := ssh.ParseRawPrivateKey([]byte(support.Config.SSHPrivateKey)) if err != nil { return nil, common.ContextError(err) } // TODO: use cert (ssh.NewCertSigner) for anti-fingerprint? signer, err := ssh.NewSignerFromKey(privateKey) if err != nil { return nil, common.ContextError(err) } return &sshServer{ support: support, shutdownBroadcast: shutdownBroadcast, sshHostKey: signer, acceptedClientCounts: make(map[string]int64), clients: make(map[string]*sshClient), }, nil }
// 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, common.ContextError(errors.New("invalid IP address")) } if len(params.TunnelProtocolPorts) == 0 { return nil, nil, nil, common.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 !common.Contains(common.SupportedTunnelProtocols, protocol) { return nil, nil, nil, common.ContextError(errors.New("invalid tunnel protocol")) } if usedPort[port] { return nil, nil, nil, common.ContextError(errors.New("duplicate listening port")) } usedPort[port] = true if common.TunnelProtocolUsesMeekHTTP(protocol) || common.TunnelProtocolUsesMeekHTTPS(protocol) { usingMeek = true } } // Web server config var webServerSecret, webServerCertificate, webServerPrivateKey, webServerPortForwardAddress string if params.WebServerPort != 0 { var err error webServerSecret, err = common.MakeRandomStringHex(WEB_SERVER_SECRET_BYTE_LENGTH) if err != nil { return nil, nil, nil, common.ContextError(err) } webServerCertificate, webServerPrivateKey, err = GenerateWebServerCertificate("") if err != nil { return nil, nil, nil, common.ContextError(err) } webServerPortForwardAddress = net.JoinHostPort( params.ServerIPAddress, strconv.Itoa(params.WebServerPort)) } // 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, common.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, common.ContextError(err) } sshPublicKey := signer.PublicKey() sshUserNameSuffix, err := common.MakeRandomStringHex(SSH_USERNAME_SUFFIX_BYTE_LENGTH) if err != nil { return nil, nil, nil, common.ContextError(err) } sshUserName := "******" + sshUserNameSuffix sshPassword, err := common.MakeRandomStringHex(SSH_PASSWORD_BYTE_LENGTH) if err != nil { return nil, nil, nil, common.ContextError(err) } // TODO: vary version string for anti-fingerprint sshServerVersion := "SSH-2.0-Psiphon" // Obfuscated SSH config obfuscatedSSHKey, err := common.MakeRandomStringHex(SSH_OBFUSCATED_KEY_BYTE_LENGTH) if err != nil { return nil, nil, nil, common.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, common.ContextError(err) } meekCookieEncryptionPublicKey = base64.StdEncoding.EncodeToString(rawMeekCookieEncryptionPublicKey[:]) meekCookieEncryptionPrivateKey = base64.StdEncoding.EncodeToString(rawMeekCookieEncryptionPrivateKey[:]) meekObfuscatedKey, err = common.MakeRandomStringHex(SSH_OBFUSCATED_KEY_BYTE_LENGTH) if err != nil { return nil, nil, nil, common.ContextError(err) } } // Other config discoveryValueHMACKey, err := common.MakeRandomStringBase64(DISCOVERY_VALUE_KEY_BYTE_LENGTH) if err != nil { return nil, nil, nil, common.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", LogFilename: params.LogFilename, GeoIPDatabaseFilenames: nil, HostID: "example-host-id", ServerIPAddress: params.ServerIPAddress, DiscoveryValueHMACKey: discoveryValueHMACKey, WebServerPort: params.WebServerPort, WebServerSecret: webServerSecret, WebServerCertificate: webServerCertificate, WebServerPrivateKey: webServerPrivateKey, WebServerPortForwardAddress: webServerPortForwardAddress, SSHPrivateKey: string(sshPrivateKey), SSHServerVersion: sshServerVersion, SSHUserName: sshUserName, SSHPassword: sshPassword, ObfuscatedSSHKey: obfuscatedSSHKey, TunnelProtocolPorts: params.TunnelProtocolPorts, DNSResolverIPAddress: "8.8.8.8", 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, } encodedConfig, err := json.MarshalIndent(config, "\n", " ") if err != nil { return nil, nil, nil, common.ContextError(err) } intPtr := func(i int) *int { return &i } trafficRulesSet := &TrafficRulesSet{ DefaultRules: TrafficRules{ RateLimits: RateLimits{ ReadUnthrottledBytes: new(int64), ReadBytesPerSecond: new(int64), WriteUnthrottledBytes: new(int64), WriteBytesPerSecond: new(int64), }, IdleTCPPortForwardTimeoutMilliseconds: intPtr(DEFAULT_IDLE_TCP_PORT_FORWARD_TIMEOUT_MILLISECONDS), IdleUDPPortForwardTimeoutMilliseconds: intPtr(DEFAULT_IDLE_UDP_PORT_FORWARD_TIMEOUT_MILLISECONDS), MaxTCPPortForwardCount: intPtr(DEFAULT_MAX_TCP_PORT_FORWARD_COUNT), MaxUDPPortForwardCount: intPtr(DEFAULT_MAX_UDP_PORT_FORWARD_COUNT), AllowTCPPorts: nil, AllowUDPPorts: nil, }, } encodedTrafficRulesSet, err := json.MarshalIndent(trafficRulesSet, "\n", " ") if err != nil { return nil, nil, nil, common.ContextError(err) } capabilities := []string{} if params.EnableSSHAPIRequests { capabilities = append(capabilities, common.CAPABILITY_SSH_API_REQUESTS) } if params.WebServerPort != 0 { capabilities = append(capabilities, common.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, common.ContextError(err) } return encodedConfig, encodedTrafficRulesSet, []byte(encodedServerEntry), nil }
func TestObfuscatedSSHConn(t *testing.T) { keyword, _ := MakeRandomStringHex(32) serverAddress := "127.0.0.1:2222" listener, err := net.Listen("tcp", serverAddress) if err != nil { t.Fatalf("Listen failed: %s", err) } rsaKey, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { t.Fatalf("GenerateKey failed: %s", err) } hostKey, err := ssh.NewSignerFromKey(rsaKey) if err != nil { t.Fatalf("NewSignerFromKey failed: %s", err) } sshCertChecker := &ssh.CertChecker{ HostKeyFallback: func(addr string, remote net.Addr, publicKey ssh.PublicKey) error { if !bytes.Equal(hostKey.PublicKey().Marshal(), publicKey.Marshal()) { return errors.New("unexpected host public key") } return nil }, } result := make(chan error, 1) go func() { conn, err := listener.Accept() if err == nil { conn, err = NewObfuscatedSshConn( OBFUSCATION_CONN_MODE_SERVER, conn, keyword) } if err == nil { config := &ssh.ServerConfig{ NoClientAuth: true, } config.AddHostKey(hostKey) _, _, _, err = ssh.NewServerConn(conn, config) } if err != nil { select { case result <- err: default: } } }() go func() { conn, err := net.DialTimeout("tcp", serverAddress, 5*time.Second) if err == nil { conn, err = NewObfuscatedSshConn( OBFUSCATION_CONN_MODE_CLIENT, conn, keyword) } if err == nil { config := &ssh.ClientConfig{ HostKeyCallback: sshCertChecker.CheckHostKey, } _, _, _, err = ssh.NewClientConn(conn, "", config) } // Sends nil on success select { case result <- err: default: } }() err = <-result if err != nil { t.Fatalf("obfuscated SSH handshake failed: %s", err) } }