/// <summary> /// Constructs a hive proxy from a hive login. /// </summary> /// <param name="hiveLogin">The hive login.</param> /// <param name="nodeProxyCreator"> /// The optional application supplied function that creates a node proxy /// given the node name, public address or FQDN, private address, and /// the node definition. /// </param> /// <param name="appendLog">Optionally have logs appended to an existing log file rather than creating a new one.</param> /// <param name="useBootstrap"> /// Optionally specifies that the instance should use the HiveMQ client /// should directly eference to the HiveMQ cluster nodes for broadcasting /// proxy update messages rather than routing traffic through the <b>private</b> /// traffic manager. This is used internally to resolve chicken-and-the-egg /// dilemmas for the traffic manager and proxy implementations that rely on /// HiveMQ messaging. /// </param> /// <param name="defaultRunOptions"> /// Optionally specifies the <see cref="RunOptions"/> to be assigned to the /// <see cref="SshProxy{TMetadata}.DefaultRunOptions"/> property for the /// nodes managed by the hive proxy. This defaults to <see cref="RunOptions.None"/>. /// </param> /// <remarks> /// The <paramref name="nodeProxyCreator"/> function will be called for each node in /// the hive definition giving the application the chance to create the management /// proxy using the node's SSH credentials and also to specify logging. A default /// creator that doesn't initialize SSH credentials and logging is used if a <c>null</c> /// argument is passed. /// </remarks> public HiveProxy( HiveLogin hiveLogin, Func <string, string, IPAddress, bool, SshProxy <NodeDefinition> > nodeProxyCreator = null, bool appendLog = false, bool useBootstrap = false, RunOptions defaultRunOptions = RunOptions.None) : this(hiveLogin.Definition, nodeProxyCreator, appendLog : appendLog, useBootstrap : useBootstrap, defaultRunOptions : defaultRunOptions) { Covenant.Requires <ArgumentNullException>(hiveLogin != null); this.HiveLogin = hiveLogin; // This ensures that the local machine is initialized properly for the login. HiveHelper.InitLogin(hiveLogin); }
/// <summary> /// Called internally by <see cref="HiveHelper.OpenHiveRemote(DebugSecrets, DebugConfigs, string, bool)"/> /// to create any requested Vault and Consul credentials and add them to the dictionary. /// </summary> /// <param name="hive">The attached hive.</param> /// <param name="hiveLogin">The hive login.</param> internal void Realize(HiveProxy hive, HiveLogin hiveLogin) { this.hiveLogin = hiveLogin; HiveCredentials credentials; foreach (var request in credentialRequests) { switch (request.Type) { case CredentialType.VaultToken: // Serialize the credentials as JSON and persist. credentials = HiveCredentials.FromVaultToken(request.Token); Add(request.SecretName, NeonHelper.JsonSerialize(credentials, Formatting.Indented)); break; case CredentialType.VaultAppRole: // Serialize the credentials as JSON and persist. credentials = VaultClient.GetAppRoleCredentialsAsync(request.RoleName).Result; Add(request.SecretName, NeonHelper.JsonSerialize(credentials, Formatting.Indented)); break; case CredentialType.ConsulToken: // $todo(jeff.lill): Implement this. break; } } }
/// <summary> /// Ensures that a hive VPN connection is established and healthy. /// </summary> /// <param name="hiveLogin">The hive login.</param> /// <param name="timeoutSeconds">Maximum seconds to wait for the VPN connection (defaults to 120 seconds).</param> /// <param name="onStatus">Optional callback that will be passed a status string.</param> /// <param name="onError">Optional callback that will be passed a error string.</param> /// <param name="show"> /// Optionally prints the OpenVPN connection status to the console for connection /// debugging purposes. /// </param> /// <returns><c>true</c> if the connection was established (or has already been established).</returns> /// <exception cref="TimeoutException"> /// Thrown if the VPN connection could not be established before the timeout expired. /// </exception> /// <exception cref="Exception"> /// Thrown if the VPN connection is unhealthy. /// </exception> public static void VpnOpen(HiveLogin hiveLogin, int timeoutSeconds = 120, Action <string> onStatus = null, Action <string> onError = null, bool show = false) { Covenant.Requires <ArgumentNullException>(hiveLogin != null); var vpnClient = VpnGetClient(hiveLogin.HiveName); string message; if (vpnClient != null) { if (show) { throw new HiveException("A VPN connection already exists for this hive."); } switch (vpnClient.State) { case VpnState.Healthy: return; case VpnState.Unhealthy: message = $"[{hiveLogin.HiveName}] VPN connection is unhealthy."; onError?.Invoke(message); throw new Exception(message); case VpnState.Connecting: onStatus?.Invoke($"Connecting [{hiveLogin.HiveName}] VPN..."); try { NeonHelper.WaitFor( () => { vpnClient = VpnGetClient(hiveLogin.HiveName); if (vpnClient != null) { return(vpnClient.State == VpnState.Healthy); } else { return(false); } }, TimeSpan.FromSeconds(timeoutSeconds)); } catch (TimeoutException) { throw new TimeoutException($"VPN connection could not be established within [{timeoutSeconds}] seconds."); } return; default: throw new NotImplementedException(); } } // Initialize the VPN folder for the hive (deleting any // existing folder). var clientFolder = GetVpnClientFolder(hiveLogin.HiveName); NeonHelper.DeleteFolderContents(clientFolder); Directory.CreateDirectory(clientFolder); File.WriteAllText(Path.Combine(clientFolder, "ca.crt"), hiveLogin.VpnCredentials.CaCert); File.WriteAllText(Path.Combine(clientFolder, "client.crt"), hiveLogin.VpnCredentials.UserCert); File.WriteAllText(Path.Combine(clientFolder, "client.key"), hiveLogin.VpnCredentials.UserKey); File.WriteAllText(Path.Combine(clientFolder, "ta.key"), hiveLogin.VpnCredentials.TaKey); // VPN servers are reached via the manager load balancer or router // using the forwarding port rule assigned to each manager node. Covenant.Assert(hiveLogin.Definition.Network.ManagerPublicAddress != null, "Manager router address is required."); var servers = string.Empty; var firstServer = true; foreach (var manager in hiveLogin.Definition.Managers) { if (firstServer) { firstServer = false; } else { servers += "\r\n"; } servers += $"remote {hiveLogin.Definition.Network.ManagerPublicAddress} {manager.VpnFrontendPort}"; } // Generate the client side configuration. var config = $@"############################################## # Sample client-side OpenVPN 2.0 config file # # for connecting to multi-client server. # # # # This configuration can be used by multiple # # clients, however each client should have # # its own cert and key files. # # # # On Windows, you might want to rename this # # file so it has a .ovpn extension # ############################################## # Specify that we are a client and that we # will be pulling certain config file directives # from the server. client # Use the same setting as you are using on # the server. # On most systems, the VPN will not function # unless you partially or fully disable # the firewall for the TUN/TAP interface. ;dev tap dev tun # Windows needs the TAP-Windows adapter name # from the Network Connections panel # if you have more than one. On XP SP2, # you may need to disable the firewall # for the TAP adapter. ;dev-node MyTap # Are we connecting to a TCP or # UDP server? Use the same setting as # on the server. proto tcp ;proto udp # The hostname/IP and port of the server. # You can have multiple remote entries # to load balance between the servers. {servers} # Choose a random host from the remote # list for load-balancing. Otherwise # try hosts in the order specified. remote-random # Keep trying indefinitely to resolve the # host name of the OpenVPN server. Very useful # on machines which are not permanently connected # to the internet such as laptops. resolv-retry infinite # Most clients don't need to bind to # a specific local port number. nobind # Downgrade privileges after initialization (non-Windows only) ;user nobody ;group nobody # Try to preserve some state across restarts. ;persist-key ;persist-tun # If you are connecting through an # HTTP proxy to reach the actual OpenVPN # server, put the proxy server/IP and # port number here. See the man page # if your proxy server requires # authentication. ;http-proxy-retry # retry on connection failures ;http-proxy [proxy server] [proxy port #] # Wireless networks often produce a lot # of duplicate packets. Set this flag # to silence duplicate packet warnings. ;mute-replay-warnings # SSL/TLS parms. # See the server config file for more # description. It's best to use # a separate .crt/.key file pair # for each client. A single ca # file can be used for all clients. ca ""{EscapeWinBackslash(Path.Combine(clientFolder, "ca.crt"))}"" cert ""{EscapeWinBackslash(Path.Combine(clientFolder, "client.crt"))}"" key ""{EscapeWinBackslash(Path.Combine(clientFolder, "client.key"))}"" # Verify server certificate by checking # that the certicate has the nsCertType # field set to ""server"". This is an # important precaution to protect against # a potential attack discussed here: # http://openvpn.net/howto.html#mitm # # To use this feature, you will need to generate # your server certificates with the nsCertType # field set to ""server"". The build-key-server # script in the easy-rsa folder will do this. remote-cert-tls server # If a tls-auth key is used on the server # then every client must also have the key. tls-auth ""{EscapeWinBackslash(Path.Combine(clientFolder, "ta.key"))}"" 1 # Select a cryptographic cipher. # If the cipher option is used on the server # then you must also specify it here. cipher AES-256-CBC # Enable compression on the VPN link. # Don't enable this unless it is also # enabled in the server config file. # # We're not enabling this due to the # VORACLE security vulnerablity: # # https://community.openvpn.net/openvpn/wiki/VORACLE # #comp-lzo # Set log file verbosity. verb 3 # Silence repeating messages ; mute 20 "; var configPath = Path.Combine(clientFolder, "client.conf"); var statusPath = Path.Combine(clientFolder, "status.txt"); var pidPath = Path.Combine(clientFolder, "pid"); File.WriteAllText(configPath, config.Replace("\r", string.Empty)); // Linux-style line endings // Launch OpenVPN via a script to establish a connection. var startInfo = new ProcessStartInfo("openvpn") { Arguments = $"--config \"{configPath}\" --status \"{statusPath}\" {VpnStatusSeconds}", CreateNoWindow = !show, }; // Write a script for manual debugging VPN purposes. var scriptPath = Path.Combine(clientFolder, NeonHelper.IsWindows ? "open.cmd" : "open.sh"); File.WriteAllText(scriptPath, $"openvpn {startInfo.Arguments}"); // Add the default OpenVPN installation folder to the PATH // environment variable if it's not present already. if (NeonHelper.IsWindows) { var defaultOpenVpnFolder = @"C:\Program Files\OpenVPN\bin"; var path = Environment.GetEnvironmentVariable("PATH"); if (path.IndexOf(defaultOpenVpnFolder, StringComparison.InvariantCultureIgnoreCase) == -1) { Environment.SetEnvironmentVariable("PATH", $"{path};{defaultOpenVpnFolder}"); } } else if (NeonHelper.IsOSX) { throw new NotImplementedException("$todo(jeff.lill): Implement this."); } else { throw new NotSupportedException(); } try { var process = Process.Start(startInfo); File.WriteAllText(pidPath, $"{process.Id}"); // This detaches the OpenVPN process from the current process so OpenVPN // will continue running after the current process terminates. process.Dispose(); } catch (Exception e) { NeonHelper.DeleteFolderContents(clientFolder); throw new Exception($"*** ERROR: Cannot launch [OpenVPN]. Make sure OpenVPN is installed to its default folder or is on the PATH.", e); } // Wait for the VPN connection. onStatus?.Invoke($"Connecting [{hiveLogin.HiveName}] VPN..."); // $hack(jeff.lill): // // Marcus had VPN problems on his workstation when logging into a hive. // The VPN would appear to connect but the SSH connection would fail. The // underlying issue turned out be that the Windows-TAP driver wasn't // installed and the OpenVPN client failed to start. // // https://github.com/jefflill/NeonForge/issues/161 // // I'm not entirely sure why my VPN health checks didn't detect this // problem. I believe this may have occurred because the OpenVPN // process hasn't yet terminated when I did the first health check // and so it appeared healthy (a race condition). // // I'm going to hack around this by moving the 10sec delay // from further down in this method to here so hopefully OpenVPN // will terminate in time to detect the problem. Thread.Sleep(TimeSpan.FromSeconds(10)); // Wait for the VPN to connect. vpnClient = VpnGetClient(hiveLogin.HiveName); if (vpnClient != null) { if (vpnClient.State == VpnState.Healthy) { onStatus?.Invoke($"VPN is connected"); return; } else if (vpnClient.State == VpnState.Unhealthy) { message = $"[{hiveLogin.HiveName}] VPN connection is unhealthy"; if (onError != null) { onError(message); } throw new Exception(message); } } try { NeonHelper.WaitFor( () => { vpnClient = VpnGetClient(hiveLogin.HiveName); if (vpnClient != null) { return(vpnClient.State == VpnState.Healthy); } else { return(false); } }, TimeSpan.FromSeconds(timeoutSeconds)); onStatus?.Invoke($"Connected to [{hiveLogin.HiveName}] VPN"); return; } catch (TimeoutException) { throw new TimeoutException($"VPN connection could not be established within [{timeoutSeconds}] seconds."); } }
/// <summary> /// Called internally by <see cref="HiveHelper.OpenHiveRemote(DebugSecrets, DebugConfigs, string, bool)"/> /// to create any requested configs and add them to the dictionary. /// </summary> /// <param name="hive">The attached hive.</param> /// <param name="hiveLogin">The hive login.</param> internal void Realize(HiveProxy hive, HiveLogin hiveLogin) { // This is a NOP because we already added all of the configs // to the base dictionary in the [Add()] methods. }