// 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, common.ContextError(err) } if config.ServerIPAddress == "" { return nil, errors.New("ServerIPAddress is required") } if config.WebServerPort > 0 && (config.WebServerSecret == "" || config.WebServerCertificate == "" || config.WebServerPrivateKey == "") { return nil, errors.New( "Web server requires WebServerSecret, WebServerCertificate, WebServerPrivateKey") } if config.WebServerPortForwardAddress != "" { if err := validateNetworkAddress(config.WebServerPortForwardAddress, false); err != nil { return nil, errors.New("WebServerPortForwardAddress is invalid") } } if config.WebServerPortForwardRedirectAddress != "" { if config.WebServerPortForwardAddress == "" { return nil, errors.New( "WebServerPortForwardRedirectAddress requires WebServerPortForwardAddress") } if err := validateNetworkAddress(config.WebServerPortForwardRedirectAddress, false); err != nil { return nil, errors.New("WebServerPortForwardRedirectAddress is invalid") } } for tunnelProtocol, _ := range config.TunnelProtocolPorts { if !common.Contains(common.SupportedTunnelProtocols, tunnelProtocol) { return nil, fmt.Errorf("Unsupported tunnel protocol: %s", tunnelProtocol) } if common.TunnelProtocolUsesSSH(tunnelProtocol) || common.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 common.TunnelProtocolUsesObfuscatedSSH(tunnelProtocol) { if config.ObfuscatedSSHKey == "" { return nil, fmt.Errorf( "Tunnel protocol %s requires ObfuscatedSSHKey", tunnelProtocol) } } if common.TunnelProtocolUsesMeekHTTP(tunnelProtocol) || common.TunnelProtocolUsesMeekHTTPS(tunnelProtocol) { if config.MeekCookieEncryptionPrivateKey == "" || config.MeekObfuscatedKey == "" { return nil, fmt.Errorf( "Tunnel protocol %s requires MeekCookieEncryptionPrivateKey, MeekObfuscatedKey", tunnelProtocol) } } if common.TunnelProtocolUsesMeekHTTPS(tunnelProtocol) { if config.MeekCertificateCommonName == "" { return nil, fmt.Errorf( "Tunnel protocol %s requires MeekCertificateCommonName", tunnelProtocol) } } } if config.UDPInterceptUdpgwServerAddress != "" { if err := validateNetworkAddress(config.UDPInterceptUdpgwServerAddress, true); err != nil { return nil, fmt.Errorf("UDPInterceptUdpgwServerAddress is invalid: %s", err) } } if config.DNSResolverIPAddress != "" { if net.ParseIP(config.DNSResolverIPAddress) == nil { return nil, fmt.Errorf("DNSResolverIPAddress is invalid") } } return &config, 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 }
// 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 common.TunnelProtocolUsesMeekHTTP(tunnelProtocol) || common.TunnelProtocolUsesMeekHTTPS(tunnelProtocol) { meekServer, err := NewMeekServer( sshServer.support, listener, common.TunnelProtocolUsesMeekHTTPS(tunnelProtocol), handleClient, sshServer.shutdownBroadcast) if err != nil { select { case listenerError <- common.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 <- common.ContextError(err): default: } return } handleClient(conn) } } }