From b1ee46916514779b5d8f001bfd2a2f4fdf2bf141 Mon Sep 17 00:00:00 2001 From: Damon Barry Date: Tue, 13 Nov 2018 15:18:00 -0800 Subject: [PATCH] Support Unix domain sockets in IoT Edge on Windows (#518) Changes to Edge Agent: - When Edge Agent builds the Docker "createOptions" string to send to iotedged, the bind-mount specification on Windows needs to point to the _parent directory_ for each socket file, not to the files themselves. On Windows, Docker can't bind-mount a Unix domain socket file. Changes to edgelet: - Change the default management and workload URIs (YAML config) for Windows to specify Unix domain sockets instead of HTTP endpoints. - Deserialize the management and workload URIs into file paths on Windows by parsing them with the `file://` scheme instead of `unix://`. This is because the `url` crate does lots of special handling of Windows paths for the `file://` scheme, but doesn't do it for "non-special" schemes like `unix://` (per the URL spec). - Depend on `mio-uds-windows`, `tokio-uds-windows`, and `hyperlocal-windows` crates on Windows. These are adaptations of `mio-uds`, `tokio-uds`, and `hyperlocal` respectively. - Where appropriate, remove the `#[cfg(unix)]` attribute from Unix domain socket-specific features that expected a Unix-only implementation, and add it in places where we still need to differentiate on platform. Notable examples: - In `edgelet-http::unix::listener(path)`, only use umask to reset socket file permissions on Unix platforms. - On Unix platforms check for existence of the Unix domain socket file with `file.exists()`. On Windows, use `fs::symlink_metadata()` instead because it works on reparse points (Unix domain sockets in Windows are reparse points). - Implement `UnixStreamExt::pid(&self)` for Windows - A test that previously used `UnixStream::pair()` to do its setup doesn't work on Windows because Winsock2 doesn't implement BSD's `pair` function. So create an ugly workaround for the test on Windows. - Use TempDir more pervasively in tests that create a socket file. In a Unix-only world we got away with simpler solutions that don't work if you can't rely on the existence of `/tmp`. - Move the test helper routine `run_uds_server` out of a linux-specific source file and into the parent `mod.rs`. - When the Edge Agent container is created in Windows, bind-mount the parent directory for each socket file rather than the file itself, just as Edge Agent does for other modules. In the Windows installer script: - Create the parent directory for each socket file, and give Modify rights to a well-known group (`NT AUTHORITY\Authenticated Users`) that will exist in any container so that non-privileged modules can access it. Since we give the rights to the parent folder, we don't need to recreate the permissions every time iotedged restarts, like we do for Unix platforms. (_Note: this part of the script is disabled for now, until we have everything in place to fully support UDS in IoT Edge on Windows, specifically RS5-based module images and the ability to use process-isolated containers on non-Server Windows._) --- .../CombinedEdgeletConfigProvider.cs | 16 +++- .../CombinedEdgeletConfigProviderTest.cs | 30 +++++-- edgelet/Cargo.lock | 60 ++++++++++++++ edgelet/contrib/config/windows/config.yaml | 24 +++++- edgelet/edgelet-docker/src/runtime.rs | 16 ++-- .../edgelet-http-mgmt/src/client/module.rs | 13 +-- edgelet/edgelet-http-mgmt/src/error.rs | 2 + edgelet/edgelet-http/Cargo.toml | 11 ++- edgelet/edgelet-http/src/lib.rs | 49 ++++++++++- edgelet/edgelet-http/src/pid.rs | 66 ++++++++++++--- edgelet/edgelet-http/src/unix.rs | 32 +++++--- edgelet/edgelet-http/src/util/connector.rs | 34 ++++---- edgelet/edgelet-http/src/util/incoming.rs | 4 +- edgelet/edgelet-http/src/util/mod.rs | 81 +++++++++++++++---- edgelet/edgelet-http/tests/connector.rs | 49 +++++------ edgelet/edgelet-test-utils/Cargo.toml | 2 + edgelet/edgelet-test-utils/src/lib.rs | 5 +- edgelet/edgelet-test-utils/src/web/linux.rs | 38 --------- edgelet/edgelet-test-utils/src/web/mod.rs | 45 +++++++++-- .../iotedged/src/config/windows/default.yaml | 8 +- edgelet/iotedged/src/error.rs | 4 +- edgelet/iotedged/src/lib.rs | 18 ++++- .../windows/setup/IotEdgeSecurityDaemon.ps1 | 45 +++++++++-- 23 files changed, 478 insertions(+), 174 deletions(-) delete mode 100644 edgelet/edgelet-test-utils/src/web/linux.rs diff --git a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Edgelet.Docker/CombinedEdgeletConfigProvider.cs b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Edgelet.Docker/CombinedEdgeletConfigProvider.cs index f60e7b153dc..b6f48f01d3a 100644 --- a/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Edgelet.Docker/CombinedEdgeletConfigProvider.cs +++ b/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Edgelet.Docker/CombinedEdgeletConfigProvider.cs @@ -4,6 +4,8 @@ namespace Microsoft.Azure.Devices.Edge.Agent.Edgelet.Docker { using System; using System.Collections.Generic; + using System.IO; + using System.Runtime.InteropServices; using global::Docker.DotNet.Models; using Microsoft.Azure.Devices.Edge.Agent.Core; using Microsoft.Azure.Devices.Edge.Agent.Docker; @@ -78,7 +80,7 @@ void MountSockets(IModule module, CreateContainerParameters createOptions) SetMountOptions(createOptions, workloadUri); } - // If Management URI is Unix domain socket, and the module is the EdgeAgent, then mount it ino the container. + // If Management URI is Unix domain socket, and the module is the EdgeAgent, then mount it into the container. var managementUri = new Uri(this.configSource.Configuration.GetValue(Constants.EdgeletManagementUriVariableName)); if (managementUri.Scheme == "unix" && module.Name.Equals(Constants.EdgeAgentModuleName, StringComparison.OrdinalIgnoreCase)) @@ -91,10 +93,20 @@ static void SetMountOptions(CreateContainerParameters createOptions, Uri uri) { HostConfig hostConfig = createOptions.HostConfig ?? new HostConfig(); IList binds = hostConfig.Binds ?? new List(); - binds.Add($"{uri.AbsolutePath}:{uri.AbsolutePath}"); + string path = BindPath(uri); + binds.Add($"{path}:{path}"); hostConfig.Binds = binds; createOptions.HostConfig = hostConfig; } + + static String BindPath(Uri uri) + { + // On Windows we need to bind to the parent folder. We can't bind + // directly to the socket file. + return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? Path.GetDirectoryName(uri.LocalPath) + : uri.AbsolutePath; + } } } diff --git a/edge-agent/test/Microsoft.Azure.Devices.Edge.Agent.Edgelet.Docker.Test/CombinedEdgeletConfigProviderTest.cs b/edge-agent/test/Microsoft.Azure.Devices.Edge.Agent.Edgelet.Docker.Test/CombinedEdgeletConfigProviderTest.cs index 71f0d223a80..9a1b16aa313 100644 --- a/edge-agent/test/Microsoft.Azure.Devices.Edge.Agent.Edgelet.Docker.Test/CombinedEdgeletConfigProviderTest.cs +++ b/edge-agent/test/Microsoft.Azure.Devices.Edge.Agent.Edgelet.Docker.Test/CombinedEdgeletConfigProviderTest.cs @@ -2,6 +2,7 @@ namespace Microsoft.Azure.Devices.Edge.Agent.Edgelet.Docker.Test { using System; using System.Collections.Generic; + using System.Runtime.InteropServices; using global::Docker.DotNet.Models; using Microsoft.Azure.Devices.Edge.Agent.Core; using Microsoft.Azure.Devices.Edge.Agent.Docker; @@ -31,12 +32,21 @@ public void TestVolMount() module.SetupGet(m => m.Config).Returns(new DockerConfig("nginx:latest")); module.SetupGet(m => m.Name).Returns(Constants.EdgeAgentModuleName); + var unixUris = new Dictionary + { + {Constants.EdgeletWorkloadUriVariableName, "unix:///path/to/workload.sock" }, + {Constants.EdgeletManagementUriVariableName, "unix:///path/to/mgmt.sock" } + }; + + var windowsUris = new Dictionary + { + {Constants.EdgeletWorkloadUriVariableName, "unix:///C:/path/to/workload/sock" }, + {Constants.EdgeletManagementUriVariableName, "unix:///C:/path/to/mgmt/sock" } + }; + IConfigurationRoot configRoot = new ConfigurationBuilder().AddInMemoryCollection( - new Dictionary - { - {Constants.EdgeletWorkloadUriVariableName, "unix:///var/run/iotedgedworkload.sock" }, - {Constants.EdgeletManagementUriVariableName, "unix:///var/run/iotedgedmgmt.sock" } - }).Build(); + RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? windowsUris : unixUris + ).Build(); var configSource = Mock.Of(s => s.Configuration == configRoot); ICombinedConfigProvider provider = new CombinedEdgeletConfigProvider(new[] { new AuthConfig() }, configSource); @@ -48,8 +58,14 @@ public void TestVolMount() Assert.NotNull(config.CreateOptions.HostConfig); Assert.NotNull(config.CreateOptions.HostConfig.Binds); Assert.Equal(2, config.CreateOptions.HostConfig.Binds.Count); - Assert.Equal("/var/run/iotedgedworkload.sock:/var/run/iotedgedworkload.sock", config.CreateOptions.HostConfig.Binds[0]); - Assert.Equal("/var/run/iotedgedmgmt.sock:/var/run/iotedgedmgmt.sock", config.CreateOptions.HostConfig.Binds[1]); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Assert.Equal("C:\\path\\to\\workload:C:\\path\\to\\workload", config.CreateOptions.HostConfig.Binds[0]); + Assert.Equal("C:\\path\\to\\mgmt:C:\\path\\to\\mgmt", config.CreateOptions.HostConfig.Binds[1]); + } else { + Assert.Equal("/path/to/workload.sock:/path/to/workload.sock", config.CreateOptions.HostConfig.Binds[0]); + Assert.Equal("/path/to/mgmt.sock:/path/to/mgmt.sock", config.CreateOptions.HostConfig.Binds[1]); + } } [Fact] diff --git a/edgelet/Cargo.lock b/edgelet/Cargo.lock index d68a0f0fe13..6aba08d550b 100644 --- a/edgelet/Cargo.lock +++ b/edgelet/Cargo.lock @@ -376,9 +376,11 @@ dependencies = [ "hyper-proxy 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "hyper-tls 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "hyperlocal 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "hyperlocal-windows 0.1.0 (git+https://github.com/Azure/hyperlocal-windows)", "lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", + "mio-uds-windows 0.1.0 (git+https://github.com/Azure/mio-uds-windows.git)", "nix 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", "percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", @@ -387,12 +389,15 @@ dependencies = [ "serde 1.0.43 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", "systemd 0.1.0", + "tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", "tempfile 3.0.3 (registry+https://github.com/rust-lang/crates.io-index)", "tokio 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-named-pipe 0.1.0", "tokio-uds 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-uds-windows 0.1.0 (git+https://github.com/Azure/tokio-uds-windows.git)", "typed-headers 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -475,8 +480,10 @@ dependencies = [ "httparse 1.2.4 (registry+https://github.com/rust-lang/crates.io-index)", "hyper 0.12.10 (registry+https://github.com/rust-lang/crates.io-index)", "hyperlocal 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "hyperlocal-windows 0.1.0 (git+https://github.com/Azure/hyperlocal-windows)", "mio 0.6.14 (registry+https://github.com/rust-lang/crates.io-index)", "mio-named-pipes 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "mio-uds-windows 0.1.0 (git+https://github.com/Azure/mio-uds-windows.git)", "serde 1.0.43 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.43 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", @@ -750,6 +757,20 @@ dependencies = [ "tokio-uds 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "hyperlocal-windows" +version = "0.1.0" +source = "git+https://github.com/Azure/hyperlocal-windows#2bd432bbbfb5b1cf38429733dd9a593c7b97a850" +dependencies = [ + "futures 0.1.24 (registry+https://github.com/rust-lang/crates.io-index)", + "hex 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "hyper 0.12.10 (registry+https://github.com/rust-lang/crates.io-index)", + "mio-uds-windows 0.1.0 (git+https://github.com/Azure/mio-uds-windows.git)", + "tokio 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-io 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-uds-windows 0.1.0 (git+https://github.com/Azure/tokio-uds-windows.git)", +] + [[package]] name = "idna" version = "0.1.4" @@ -889,6 +910,11 @@ name = "lazycell" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "lazycell" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "libc" version = "0.2.42" @@ -1013,6 +1039,21 @@ dependencies = [ "mio 0.6.14 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "mio-uds-windows" +version = "0.1.0" +source = "git+https://github.com/Azure/mio-uds-windows.git#67c2c785112b79ff212c018f124196d7297c5d88" +dependencies = [ + "iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "lazycell 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", + "mio 0.6.14 (registry+https://github.com/rust-lang/crates.io-index)", + "miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "miow" version = "0.2.1" @@ -1788,6 +1829,21 @@ dependencies = [ "tokio-reactor 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "tokio-uds-windows" +version = "0.1.0" +source = "git+https://github.com/Azure/tokio-uds-windows.git#b689a914dbaa905f359f89200c01fed7a6c8df3f" +dependencies = [ + "bytes 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.24 (registry+https://github.com/rust-lang/crates.io-index)", + "iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", + "mio 0.6.14 (registry+https://github.com/rust-lang/crates.io-index)", + "mio-uds-windows 0.1.0 (git+https://github.com/Azure/mio-uds-windows.git)", + "tokio-io 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-reactor 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "toml" version = "0.4.6" @@ -2086,6 +2142,7 @@ dependencies = [ "checksum hyper-proxy 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1ca88dbc8a46e9a7530321ae8196527673d24f6ad08ce5b35078fe6a7d4001c6" "checksum hyper-tls 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "caaee4dea92794a9e697038bd401e264307d1f22c883dbcb6f6618ba0d3b3bd3" "checksum hyperlocal 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d063d6d5658623c6ef16f452e11437c0e7e23a6d327470573fe78892dafbc4fb" +"checksum hyperlocal-windows 0.1.0 (git+https://github.com/Azure/hyperlocal-windows)" = "" "checksum idna 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "014b298351066f1512874135335d62a789ffe78a9974f94b43ed5621951eaf7d" "checksum indexmap 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "08173ba1e906efb6538785a8844dd496f5d34f0a2d88038e95195172fc667220" "checksum iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dbe6e417e7d0975db6512b90796e8ce223145ac4e33c377e4a42882a0e88bb08" @@ -2094,6 +2151,7 @@ dependencies = [ "checksum lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "76f033c7ad61445c5b347c7382dd1237847eb1bce590fe50365dcb33d546be73" "checksum lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c8f31047daa365f19be14b47c29df4f7c3b581832407daabe6ae77397619237d" "checksum lazycell 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a6f08839bc70ef4a3fe1d566d5350f519c5912ea86be0df1740a7d247c7fc0ef" +"checksum lazycell 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ddba4c30a78328befecec92fc94970e53b3ae385827d28620f0f5bb2493081e0" "checksum libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)" = "b685088df2b950fccadf07a7187c8ef846a959c142338a48f9dc0b94517eb5f1" "checksum linked-hash-map 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6d262045c5b87c0861b3f004610afd0e2c851e2908d08b6c870cbb9d5f494ecd" "checksum linked-hash-map 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "70fb39025bc7cdd76305867c4eccf2f2dcf6e9a57f5b21a93e1c2d86cd03ec9e" @@ -2107,6 +2165,7 @@ dependencies = [ "checksum mio 0.6.14 (registry+https://github.com/rust-lang/crates.io-index)" = "6d771e3ef92d58a8da8df7d6976bfca9371ed1de6619d9d5a5ce5b1f29b85bfe" "checksum mio-named-pipes 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "f5e374eff525ce1c5b7687c4cef63943e7686524a387933ad27ca7ec43779cb3" "checksum mio-uds 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)" = "966257a94e196b11bb43aca423754d87429960a768de9414f3691d6957abf125" +"checksum mio-uds-windows 0.1.0 (git+https://github.com/Azure/mio-uds-windows.git)" = "" "checksum miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919" "checksum miow 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9224c91f82b3c47cf53dcf78dfaa20d6888fbcc5d272d5f2fcdf8a697f3c987d" "checksum native-tls 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8b0a7bd714e83db15676d31caf968ad7318e9cc35f93c85a90231c8f22867549" @@ -2187,6 +2246,7 @@ dependencies = [ "checksum tokio-tls 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e53fdbf3156f588be1676022fe794232b24922d426e8c14f4e46891c1e31c440" "checksum tokio-udp 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "137bda266504893ac4774e0ec4c2108f7ccdbcb7ac8dced6305fe9e4e0b5041a" "checksum tokio-uds 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "22e3aa6d1fcc19e635418dc0a30ab5bd65d347973d6f43f1a37bf8d9d1335fc9" +"checksum tokio-uds-windows 0.1.0 (git+https://github.com/Azure/tokio-uds-windows.git)" = "" "checksum toml 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "a0263c6c02c4db6c8f7681f9fd35e90de799ebd4cfdeab77a38f4ff6b3d8c0d9" "checksum try-lock 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e604eb7b43c06650e854be16a2a03155743d3752dd1c943f6829e26b7a36e382" "checksum typed-headers 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9fb9c89066fda8824c7c9570d0f5c0dddd2a0b272b189855f89450e830f196eb" diff --git a/edgelet/contrib/config/windows/config.yaml b/edgelet/contrib/config/windows/config.yaml index 751ed0aec80..59162a52923 100644 --- a/edgelet/contrib/config/windows/config.yaml +++ b/edgelet/contrib/config/windows/config.yaml @@ -102,12 +102,20 @@ hostname: "" # # The following uri schemes are supported: # http - connect over TCP +# unix - connect over Unix domain socket +# +# If the 'unix' scheme is selected, the daemon expects that the parent +# directory of the specified socket file already exists, and that the Windows +# group 'NT AUTHORITY\Authenticated Users' has been given 'Modify' rights on +# the directory. For example, if the URI "unix:///C:/path/to/sock.file" is +# specified, then the directory "C:\path\to" must exist with the correct +# permissions. # ############################################################################### connect: - management_uri: "http://:15580" - workload_uri: "http://:15581" + management_uri: "unix:///C:/ProgramData/iotedge/mgmt/sock" + workload_uri: "unix:///C:/ProgramData/iotedge/workload/sock" ############################################################################### # Listen settings @@ -120,12 +128,20 @@ connect: # # The following uri schemes are supported: # http - listen over TCP +# unix - listen over Unix domain socket +# +# If the 'unix' scheme is selected, the daemon expects that the parent +# directory of the specified socket file already exists, and that the Windows +# group 'NT AUTHORITY\Authenticated Users' has been given 'Modify' rights on +# the directory. For example, if the URI "unix:///C:/path/to/sock.file" is +# specified, then the directory "C:\path\to" must exist with the correct +# permissions. # ############################################################################### listen: - management_uri: "http://:15580" - workload_uri: "http://:15581" + management_uri: "unix:///C:/ProgramData/iotedge/mgmt/sock" + workload_uri: "unix:///C:/ProgramData/iotedge/workload/sock" ############################################################################### # Home Directory diff --git a/edgelet/edgelet-docker/src/runtime.rs b/edgelet/edgelet-docker/src/runtime.rs index 34b4a405090..91878d2f440 100644 --- a/edgelet/edgelet-docker/src/runtime.rs +++ b/edgelet/edgelet-docker/src/runtime.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use std::convert::From; use std::ops::Deref; +use std::path::PathBuf; use std::time::Duration; use base64; @@ -22,7 +23,7 @@ use edgelet_core::{ LogOptions, Module, ModuleRegistry, ModuleRuntime, ModuleRuntimeState, ModuleSpec, SystemInfo as CoreSystemInfo, }; -use edgelet_http::UrlConnector; +use edgelet_http::{UrlConnector, UrlExt}; use edgelet_utils::log_failure; use error::{Error, ErrorKind, Result}; @@ -53,9 +54,12 @@ impl DockerModuleRuntime { let client = Client::builder().build(UrlConnector::new(docker_url)?); // extract base path - the bit that comes after the scheme - let base_path = get_base_path(docker_url); + let base_path = get_base_path(docker_url)?; let mut configuration = Configuration::new(client); - configuration.base_path = base_path.to_string(); + configuration.base_path = base_path + .to_str() + .expect("URL points to a path that cannot be represented in UTF-8") + .to_string(); let scheme = docker_url.scheme().to_string(); configuration.uri_composer = Box::new(move |base_path, path| { @@ -96,10 +100,10 @@ impl DockerModuleRuntime { } } -fn get_base_path(url: &Url) -> &str { +fn get_base_path(url: &Url) -> Result { match url.scheme() { - "unix" => url.path(), - _ => url.as_str(), + "unix" => Ok(url.to_uds_file_path()?), + _ => Ok(url.as_str().into()), } } diff --git a/edgelet/edgelet-http-mgmt/src/client/module.rs b/edgelet/edgelet-http-mgmt/src/client/module.rs index c2affeba643..1fa646f3057 100644 --- a/edgelet/edgelet-http-mgmt/src/client/module.rs +++ b/edgelet/edgelet-http-mgmt/src/client/module.rs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. use std::fmt; +use std::path::PathBuf; use std::str::FromStr; use std::sync::Arc; use std::time::Duration; @@ -8,7 +9,7 @@ use std::time::Duration; use edgelet_core::SystemInfo as CoreSystemInfo; use edgelet_core::*; use edgelet_docker::{self, DockerConfig}; -use edgelet_http::{UrlConnector, API_VERSION}; +use edgelet_http::{UrlConnector, UrlExt, API_VERSION}; use futures::future::{self, FutureResult}; use futures::prelude::*; use futures::stream; @@ -29,9 +30,9 @@ impl ModuleClient { pub fn new(url: &Url) -> Result { let client = Client::builder().build(UrlConnector::new(url)?); - let base_path = get_base_path(url); + let base_path = get_base_path(url)?; let mut configuration = Configuration::new(client); - configuration.base_path = base_path.to_string(); + configuration.base_path = base_path.to_str().ok_or(ErrorKind::Utf8)?.to_string(); let scheme = url.scheme().to_string(); configuration.uri_composer = Box::new(move |base_path, path| { @@ -45,10 +46,10 @@ impl ModuleClient { } } -fn get_base_path(url: &Url) -> &str { +fn get_base_path(url: &Url) -> Result { match url.scheme() { - "unix" => url.path(), - _ => url.as_str(), + "unix" => Ok(url.to_uds_file_path()?), + _ => Ok(url.as_str().into()), } } diff --git a/edgelet/edgelet-http-mgmt/src/error.rs b/edgelet/edgelet-http-mgmt/src/error.rs index 35f397d2aa4..344b86acb43 100644 --- a/edgelet/edgelet-http-mgmt/src/error.rs +++ b/edgelet/edgelet-http-mgmt/src/error.rs @@ -50,6 +50,8 @@ pub enum ErrorKind { NotModified, #[fail(display = "Parse error")] Parse, + #[fail(display = "UTF-8 encode/decode error")] + Utf8, } impl Fail for Error { diff --git a/edgelet/edgelet-http/Cargo.toml b/edgelet/edgelet-http/Cargo.toml index b0ba4190010..1a57ed91b29 100644 --- a/edgelet/edgelet-http/Cargo.toml +++ b/edgelet/edgelet-http/Cargo.toml @@ -19,7 +19,7 @@ percent-encoding = "1.0" regex = "0.2" serde = "1.0" serde_json = "1.0" -tokio = "0.1.8" +tokio = "0.1.11" typed-headers = "0.1" url = "1.7" @@ -36,16 +36,19 @@ tokio-uds = "0.2" [target.'cfg(windows)'.dependencies] hyper-named-pipe = { path = "../hyper-named-pipe" } +hyperlocal-windows = { git = "https://github.com/Azure/hyperlocal-windows" } +mio-uds-windows = { git = "https://github.com/Azure/mio-uds-windows.git" } tokio-named-pipe = { path = "../tokio-named-pipe" } +tokio-uds-windows = { git = "https://github.com/Azure/tokio-uds-windows.git" } +winapi = { version = "0.3.5", features = ["winsock2"] } [dev-dependencies] lazy_static = "1.0" +tempfile = "3" +tempdir = "0.3.7" edgelet-test-utils = { path = "../edgelet-test-utils" } -[target.'cfg(unix)'.dev-dependencies] -tempfile = "3" - [target.'cfg(windows)'.dev-dependencies] httparse = "1.2" rand = "0.4" diff --git a/edgelet/edgelet-http/src/lib.rs b/edgelet/edgelet-http/src/lib.rs index ab9e9346093..2973ad40471 100644 --- a/edgelet/edgelet-http/src/lib.rs +++ b/edgelet/edgelet-http/src/lib.rs @@ -28,11 +28,15 @@ extern crate hyper_proxy; extern crate hyper_tls; #[cfg(unix)] extern crate hyperlocal; +#[cfg(windows)] +extern crate hyperlocal_windows; #[cfg(target_os = "linux")] #[cfg(unix)] extern crate libc; #[macro_use] extern crate log; +#[cfg(windows)] +extern crate mio_uds_windows; #[cfg(unix)] extern crate nix; extern crate percent_encoding; @@ -44,7 +48,9 @@ extern crate serde; #[macro_use] extern crate serde_json; extern crate systemd; -#[cfg(unix)] +#[cfg(test)] +#[cfg(windows)] +extern crate tempdir; #[cfg(test)] extern crate tempfile; extern crate tokio; @@ -52,8 +58,12 @@ extern crate tokio; extern crate tokio_named_pipe; #[cfg(unix)] extern crate tokio_uds; +#[cfg(windows)] +extern crate tokio_uds_windows; extern crate typed_headers; extern crate url; +#[cfg(windows)] +extern crate winapi; #[macro_use] extern crate edgelet_utils; @@ -64,6 +74,9 @@ use std::net; use std::net::ToSocketAddrs; #[cfg(unix)] use std::os::unix::io::FromRawFd; +#[cfg(unix)] +use std::path::Path; +use std::path::PathBuf; use std::sync::Arc; use futures::{future, Future, Poll, Stream}; @@ -97,7 +110,6 @@ use self::util::incoming::Incoming; const HTTP_SCHEME: &str = "http"; const TCP_SCHEME: &str = "tcp"; -#[cfg(unix)] const UNIX_SCHEME: &str = "unix"; #[cfg(unix)] const FD_SCHEME: &str = "fd"; @@ -219,9 +231,8 @@ impl HyperExt for Http { let listener = TcpListener::bind(&addr)?; Incoming::Tcp(listener) } - #[cfg(unix)] UNIX_SCHEME => { - let path = url.path(); + let path = url.to_uds_file_path()?; unix::listener(path)? } #[cfg(unix)] @@ -257,3 +268,33 @@ impl HyperExt for Http { }) } } + +pub trait UrlExt { + fn to_uds_file_path(&self) -> Result; +} + +impl UrlExt for Url { + #[cfg(unix)] + fn to_uds_file_path(&self) -> Result { + debug_assert_eq!(self.scheme(), UNIX_SCHEME); + Ok(Path::new(self.path()).to_path_buf()) + } + + #[cfg(windows)] + fn to_uds_file_path(&self) -> Result { + // We get better handling of Windows file syntax if we parse a + // unix:// URL as a file:// URL. Specifically: + // - On Unix, `Url::parse("unix:///path")?.to_file_path()` succeeds and + // returns "/path". + // - On Windows, `Url::parse("unix:///C:/path")?.to_file_path()` fails + // with Err(()). + // - On Windows, `Url::parse("file:///C:/path")?.to_file_path()` succeeds + // and returns "C:\\path". + debug_assert_eq!(self.scheme(), UNIX_SCHEME); + let mut s = self.to_string(); + s.replace_range(..4, "file"); + let url = Url::parse(&s).map_err(|_| Error::from(ErrorKind::InvalidUri(s.clone())))?; + url.to_file_path() + .map_err(|()| ErrorKind::InvalidUri(s.clone()).into()) + } +} diff --git a/edgelet/edgelet-http/src/pid.rs b/edgelet/edgelet-http/src/pid.rs index 8cfdd32c1d2..576701b484b 100644 --- a/edgelet/edgelet-http/src/pid.rs +++ b/edgelet/edgelet-http/src/pid.rs @@ -1,9 +1,15 @@ // Copyright (c) Microsoft. All rights reserved. +use std::io; + use edgelet_core::pid::Pid; use futures::prelude::*; use hyper::service::Service; use hyper::{Body, Error as HyperError, Request}; +#[cfg(unix)] +use tokio_uds::UnixStream; +#[cfg(windows)] +use tokio_uds_windows::UnixStream; #[derive(Clone)] pub struct PidService { @@ -35,28 +41,31 @@ where } } +pub trait UnixStreamExt { + fn pid(&self) -> io::Result; +} + +impl UnixStreamExt for UnixStream { + fn pid(&self) -> io::Result { + get_pid(self) + } +} + #[cfg(unix)] -pub use self::impl_unix::UnixStreamExt; +use self::impl_unix::get_pid; #[cfg(unix)] mod impl_unix { use libc::{c_void, getsockopt, ucred, SOL_SOCKET, SO_PEERCRED}; use std::os::unix::io::AsRawFd; use std::{io, mem}; + #[cfg(unix)] use tokio_uds::UnixStream; + #[cfg(windows)] + use tokio_uds_windows::UnixStream; use super::*; - pub trait UnixStreamExt { - fn pid(&self) -> io::Result; - } - - impl UnixStreamExt for UnixStream { - fn pid(&self) -> io::Result { - get_pid(self) - } - } - pub fn get_pid(sock: &UnixStream) -> io::Result { let raw_fd = sock.as_raw_fd(); let mut ucred = ucred { @@ -89,3 +98,38 @@ mod impl_unix { } } } + +#[cfg(windows)] +use self::impl_windows::get_pid; + +#[cfg(windows)] +mod impl_windows { + use std::io; + use std::os::windows::io::AsRawSocket; + use winapi::ctypes::c_long; + use winapi::um::winsock2::{ioctlsocket, WSAGetLastError, SOCKET_ERROR}; + + use super::*; + + // SIO_AF_UNIX_GETPEERPID is defined in the Windows header afunix.h. + const SIO_AF_UNIX_GETPEERPID: c_long = 0x5800_0100; + + pub fn get_pid(sock: &UnixStream) -> io::Result { + let raw_socket = sock.as_raw_socket(); + let mut pid = 0_u32; + let ret = unsafe { + #[cfg_attr(feature = "cargo-clippy", allow(cast_possible_truncation))] + ioctlsocket( + raw_socket as _, + SIO_AF_UNIX_GETPEERPID, + &mut pid as *mut u32, + ) + }; + if ret == SOCKET_ERROR { + Err(io::Error::from_raw_os_error(unsafe { WSAGetLastError() })) + } else { + #[cfg_attr(feature = "cargo-clippy", allow(cast_possible_wrap))] + Ok(Pid::Value(pid as _)) + } + } +} diff --git a/edgelet/edgelet-http/src/unix.rs b/edgelet/edgelet-http/src/unix.rs index c3dff82238f..4716c7ae541 100644 --- a/edgelet/edgelet-http/src/unix.rs +++ b/edgelet/edgelet-http/src/unix.rs @@ -1,13 +1,16 @@ // Copyright (c) Microsoft. All rights reserved. -#![cfg(unix)] - use std::fs; +#[cfg(unix)] use std::os::unix::fs::MetadataExt; use std::path::Path; +#[cfg(unix)] use nix::sys::stat::{umask, Mode}; +#[cfg(unix)] use tokio_uds::UnixListener; +#[cfg(windows)] +use tokio_uds_windows::UnixListener; use error::Error; use util::incoming::Incoming; @@ -26,16 +29,9 @@ pub fn listener>(path: P) -> Result { fs::remove_file(&path)?; debug!("unlinked {}", path.as_ref().display()); - let mode = Mode::from_bits_truncate(metadata.mode()); - let mut mask = Mode::all(); - mask.toggle(mode); - - debug!( - "settings permissions {:#o} for {}...", - mode, - path.as_ref().display() - ); - let prev = umask(mask); + #[cfg(unix)] + let prev = set_umask(&metadata, path.as_ref()); + #[cfg(unix)] defer! {{ umask(prev); }} debug!("binding {}...", path.as_ref().display()); @@ -51,7 +47,19 @@ pub fn listener>(path: P) -> Result { Ok(listener) } +#[cfg(unix)] +fn set_umask(metadata: &fs::Metadata, path: &Path) -> Mode { + let mode = Mode::from_bits_truncate(metadata.mode()); + let mut mask = Mode::all(); + mask.toggle(mode); + + debug!("settings permissions {:#o} for {}...", mode, path.display()); + + umask(mask) +} + #[cfg(test)] +#[cfg(unix)] mod tests { use super::*; diff --git a/edgelet/edgelet-http/src/util/connector.rs b/edgelet/edgelet-http/src/util/connector.rs index 420a3ffee69..a2f5110e5d2 100644 --- a/edgelet/edgelet-http/src/util/connector.rs +++ b/edgelet/edgelet-http/src/util/connector.rs @@ -14,7 +14,6 @@ //! HTTP and Unix sockets respectively. use std::io; -#[cfg(unix)] use std::path::Path; use futures::{future, Future}; @@ -25,12 +24,14 @@ use hyper::Uri; use hyper_named_pipe::{PipeConnector, Uri as PipeUri}; #[cfg(unix)] use hyperlocal::{UnixConnector, Uri as HyperlocalUri}; +#[cfg(windows)] +use hyperlocal_windows::{UnixConnector, Uri as HyperlocalUri}; use url::{ParseError, Url}; use error::{Error, ErrorKind}; use util::StreamSelector; +use UrlExt; -#[cfg(unix)] const UNIX_SCHEME: &str = "unix"; #[cfg(windows)] const PIPE_SCHEME: &str = "npipe"; @@ -40,19 +41,30 @@ pub enum UrlConnector { Http(HttpConnector), #[cfg(windows)] Pipe(PipeConnector), - #[cfg(unix)] Unix(UnixConnector), } +fn socket_file_exists(path: &Path) -> bool { + if cfg!(windows) { + use std::fs; + // Unix domain socket files in Windows are reparse points, so path.exists() + // (which calls fs::metadata(path)) won't work. Use fs::symlink_metadata() + // instead. + fs::symlink_metadata(path).is_ok() + } else { + path.exists() + } +} + impl UrlConnector { pub fn new(url: &Url) -> Result { match url.scheme() { #[cfg(windows)] PIPE_SCHEME => Ok(UrlConnector::Pipe(PipeConnector)), - #[cfg(unix)] UNIX_SCHEME => { - if Path::new(url.path()).exists() { + let file_path = url.to_uds_file_path()?; + if socket_file_exists(&file_path) { Ok(UrlConnector::Unix(UnixConnector::new())) } else { Err(ErrorKind::InvalidUri(url.to_string()))? @@ -73,7 +85,6 @@ impl UrlConnector { match scheme { #[cfg(windows)] PIPE_SCHEME => Ok(PipeUri::new(base_path, path)?.into()), - #[cfg(unix)] UNIX_SCHEME => Ok(HyperlocalUri::new(base_path, path).into()), HTTP_SCHEME => Ok(Url::parse(base_path) .and_then(|base| base.join(path)) @@ -96,7 +107,6 @@ impl Connect for UrlConnector { #[cfg(windows)] (UrlConnector::Pipe(_), PIPE_SCHEME) => (), - #[cfg(unix)] (UrlConnector::Unix(_), UNIX_SCHEME) => (), (_, scheme) => { @@ -121,7 +131,6 @@ impl Connect for UrlConnector { })) as Self::Future } - #[cfg(unix)] UrlConnector::Unix(connector) => { Box::new(connector.connect(dst).and_then(|(unix_stream, connected)| { Ok((StreamSelector::Unix(unix_stream), connected)) @@ -133,7 +142,6 @@ impl Connect for UrlConnector { #[cfg(test)] mod tests { - #[cfg(unix)] use tempfile::NamedTempFile; use url::Url; @@ -146,7 +154,6 @@ mod tests { UrlConnector::new(&Url::parse("foo:///this/is/not/valid").unwrap()).unwrap(); } - #[cfg(unix)] #[test] #[should_panic(expected = "Invalid uri")] fn invalid_uds_url() { @@ -154,13 +161,12 @@ mod tests { UrlConnector::new(&Url::parse("unix:///this/file/does/not/exist").unwrap()).unwrap(); } - #[cfg(unix)] #[test] fn create_uds_succeeds() { let file = NamedTempFile::new().unwrap(); - let file_path = file.path().to_str().unwrap(); - let _connector = - UrlConnector::new(&Url::parse(&format!("unix://{}", file_path)).unwrap()).unwrap(); + let mut url = Url::from_file_path(file.path()).unwrap(); + url.set_scheme("unix").unwrap(); + let _connector = UrlConnector::new(&url).unwrap(); } #[test] diff --git a/edgelet/edgelet-http/src/util/incoming.rs b/edgelet/edgelet-http/src/util/incoming.rs index 1640c936492..90b3e162b72 100644 --- a/edgelet/edgelet-http/src/util/incoming.rs +++ b/edgelet/edgelet-http/src/util/incoming.rs @@ -6,12 +6,13 @@ use futures::{Poll, Stream}; use tokio::net::TcpListener; #[cfg(unix)] use tokio_uds::UnixListener; +#[cfg(windows)] +use tokio_uds_windows::UnixListener; use util::{IncomingSocketAddr, StreamSelector}; pub enum Incoming { Tcp(TcpListener), - #[cfg(unix)] Unix(UnixListener), } @@ -33,7 +34,6 @@ impl Stream for Incoming { Some((StreamSelector::Tcp(stream), IncomingSocketAddr::Tcp(addr))) }) } - #[cfg(unix)] Incoming::Unix(ref mut listener) => { let accept = match listener.poll_accept() { Ok(accept) => accept, diff --git a/edgelet/edgelet-http/src/util/mod.rs b/edgelet/edgelet-http/src/util/mod.rs index 345a5e04849..5a3f81e57c6 100644 --- a/edgelet/edgelet-http/src/util/mod.rs +++ b/edgelet/edgelet-http/src/util/mod.rs @@ -9,14 +9,17 @@ use std::os::unix::net::SocketAddr as UnixSocketAddr; use bytes::{Buf, BufMut}; use edgelet_core::pid::Pid; use futures::Poll; +#[cfg(windows)] +use mio_uds_windows::net::SocketAddr as UnixSocketAddr; use tokio::io::{AsyncRead, AsyncWrite}; use tokio::net::TcpStream; #[cfg(windows)] use tokio_named_pipe::PipeStream; #[cfg(unix)] use tokio_uds::UnixStream; +#[cfg(windows)] +use tokio_uds_windows::UnixStream; -#[cfg(unix)] use pid::UnixStreamExt; pub mod connector; @@ -31,7 +34,6 @@ pub enum StreamSelector { Tcp(TcpStream), #[cfg(windows)] Pipe(PipeStream), - #[cfg(unix)] Unix(UnixStream), } @@ -42,7 +44,6 @@ impl StreamSelector { StreamSelector::Tcp(_) => Ok(Pid::Any), #[cfg(windows)] StreamSelector::Pipe(_) => Ok(Pid::Any), - #[cfg(unix)] StreamSelector::Unix(ref stream) => stream.pid(), } } @@ -54,7 +55,6 @@ impl Read for StreamSelector { StreamSelector::Tcp(ref mut stream) => stream.read(buf), #[cfg(windows)] StreamSelector::Pipe(ref mut stream) => stream.read(buf), - #[cfg(unix)] StreamSelector::Unix(ref mut stream) => stream.read(buf), } } @@ -66,7 +66,6 @@ impl Write for StreamSelector { StreamSelector::Tcp(ref mut stream) => stream.write(buf), #[cfg(windows)] StreamSelector::Pipe(ref mut stream) => stream.write(buf), - #[cfg(unix)] StreamSelector::Unix(ref mut stream) => stream.write(buf), } } @@ -76,7 +75,6 @@ impl Write for StreamSelector { StreamSelector::Tcp(ref mut stream) => stream.flush(), #[cfg(windows)] StreamSelector::Pipe(ref mut stream) => stream.flush(), - #[cfg(unix)] StreamSelector::Unix(ref mut stream) => stream.flush(), } } @@ -89,7 +87,6 @@ impl AsyncRead for StreamSelector { StreamSelector::Tcp(ref stream) => stream.prepare_uninitialized_buffer(buf), #[cfg(windows)] StreamSelector::Pipe(ref stream) => stream.prepare_uninitialized_buffer(buf), - #[cfg(unix)] StreamSelector::Unix(ref stream) => stream.prepare_uninitialized_buffer(buf), } } @@ -100,7 +97,6 @@ impl AsyncRead for StreamSelector { StreamSelector::Tcp(ref mut stream) => stream.read_buf(buf), #[cfg(windows)] StreamSelector::Pipe(ref mut stream) => stream.read_buf(buf), - #[cfg(unix)] StreamSelector::Unix(ref mut stream) => stream.read_buf(buf), } } @@ -112,7 +108,6 @@ impl AsyncWrite for StreamSelector { StreamSelector::Tcp(ref mut stream) => <&TcpStream>::shutdown(&mut &*stream), #[cfg(windows)] StreamSelector::Pipe(ref mut stream) => PipeStream::shutdown(stream), - #[cfg(unix)] StreamSelector::Unix(ref mut stream) => <&UnixStream>::shutdown(&mut &*stream), } } @@ -123,7 +118,6 @@ impl AsyncWrite for StreamSelector { StreamSelector::Tcp(ref mut stream) => stream.write_buf(buf), #[cfg(windows)] StreamSelector::Pipe(ref mut stream) => stream.write_buf(buf), - #[cfg(unix)] StreamSelector::Unix(ref mut stream) => stream.write_buf(buf), } } @@ -131,7 +125,6 @@ impl AsyncWrite for StreamSelector { pub enum IncomingSocketAddr { Tcp(SocketAddr), - #[cfg(unix)] Unix(UnixSocketAddr), } @@ -139,7 +132,6 @@ impl fmt::Display for IncomingSocketAddr { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { IncomingSocketAddr::Tcp(ref socket) => socket.fmt(f), - #[cfg(unix)] IncomingSocketAddr::Unix(ref socket) => { if let Some(path) = socket.as_pathname() { write!(f, "{}", path.display()) @@ -151,21 +143,76 @@ impl fmt::Display for IncomingSocketAddr { } } -#[cfg(unix)] #[cfg(test)] mod tests { + #[cfg(windows)] + use tempdir::TempDir; + #[cfg(windows)] + use tokio::runtime::current_thread::Runtime; + #[cfg(unix)] + use tokio_uds::UnixStream; + #[cfg(windows)] + use tokio_uds_windows::{UnixListener, UnixStream}; + use super::*; + struct Pair { + #[cfg(windows)] + _dir: TempDir, + #[cfg(windows)] + _rt: Runtime, + + a: UnixStream, + b: UnixStream, + } + + #[cfg(unix)] + fn socket_pair() -> Pair { + let (a, b) = UnixStream::pair().unwrap(); + Pair { a, b } + } + + #[cfg(windows)] + fn socket_pair() -> Pair { + // 'pair' not implemented on Windows + use futures::sync::oneshot; + use futures::{Future, Stream}; + let dir = TempDir::new("uds").unwrap(); + let addr = dir.path().join("sock"); + let mut rt = Runtime::new().unwrap(); + let server = UnixListener::bind(&addr).unwrap(); + let (tx, rx) = oneshot::channel(); + rt.spawn( + server + .incoming() + .into_future() + .and_then(move |(sock, _)| { + tx.send(sock.unwrap()).unwrap(); + Ok(()) + }).map_err(|e| panic!("err={:?}", e)), + ); + + let a = rt.block_on(UnixStream::connect(&addr)).unwrap(); + let b = rt.block_on(rx).unwrap(); + Pair { + _dir: dir, + _rt: rt, + a, + b, + } + } + #[test] + #[cfg_attr(windows, ignore)] // TODO: remove when windows build servers are upgraded to RS5 fn test_pid() { - let (a, b) = UnixStream::pair().unwrap(); - assert_eq!(a.pid().unwrap(), b.pid().unwrap()); - match a.pid().unwrap() { + let pair = socket_pair(); + assert_eq!(pair.a.pid().unwrap(), pair.b.pid().unwrap()); + match pair.a.pid().unwrap() { Pid::None => panic!("no pid 'a'"), Pid::Any => panic!("any pid 'a'"), Pid::Value(_) => (), } - match b.pid().unwrap() { + match pair.b.pid().unwrap() { Pid::None => panic!("no pid 'b'"), Pid::Any => panic!("any pid 'b'"), Pid::Value(_) => (), diff --git a/edgelet/edgelet-http/tests/connector.rs b/edgelet/edgelet-http/tests/connector.rs index 1888f885c31..200eaeaddc3 100644 --- a/edgelet/edgelet-http/tests/connector.rs +++ b/edgelet/edgelet-http/tests/connector.rs @@ -19,15 +19,14 @@ extern crate hyper_named_pipe; #[cfg(unix)] extern crate hyperlocal; #[cfg(windows)] +extern crate hyperlocal_windows; +#[cfg(windows)] extern crate rand; -#[cfg(unix)] -#[macro_use(defer)] -extern crate scopeguard; +extern crate tempdir; extern crate tokio; extern crate typed_headers; extern crate url; -#[cfg(unix)] use std::io; #[cfg(windows)] use std::sync::mpsc::channel; @@ -37,7 +36,6 @@ use std::thread; use edgelet_http::UrlConnector; #[cfg(windows)] use edgelet_test_utils::run_pipe_server; -#[cfg(unix)] use edgelet_test_utils::run_uds_server; use edgelet_test_utils::{get_unused_tcp_port, run_tcp_server}; use futures::future; @@ -52,7 +50,10 @@ use hyper_named_pipe::Uri as PipeUri; #[cfg(unix)] use hyperlocal::Uri as HyperlocalUri; #[cfg(windows)] +use hyperlocal_windows::Uri as HyperlocalUri; +#[cfg(windows)] use rand::Rng; +use tempdir::TempDir; use typed_headers::mime; use typed_headers::{ContentLength, ContentType, HeaderMapExt}; use url::Url; @@ -91,23 +92,20 @@ fn tcp_get() { runtime.block_on(task).unwrap(); } -#[cfg(unix)] #[test] +#[cfg_attr(windows, ignore)] // TODO: remove when windows build servers are upgraded to RS5 fn uds_get() { - let file_path = "/tmp/edgelet_test_uds_get.sock"; - - // make sure file gets deleted when test is done - defer! {{ - ::std::fs::remove_file(&file_path).unwrap_or(()); - }} + let dir = TempDir::new("uds").unwrap(); + let file_path = dir.path().join("sock"); + let file_path = file_path.to_str().unwrap(); - let path_copy = file_path.to_string(); - let server = run_uds_server(&path_copy, |req| { + let server = run_uds_server(&file_path, |req| { hello_handler(req).map_err(|err| io::Error::new(io::ErrorKind::Other, err)) }).map_err(|err| eprintln!("{}", err)); - let connector = - UrlConnector::new(&Url::parse(&format!("unix://{}", file_path)).unwrap()).unwrap(); + let mut url = Url::from_file_path(file_path).unwrap(); + url.set_scheme("unix").unwrap(); + let connector = UrlConnector::new(&url).unwrap(); let client = Client::builder().build::<_, Body>(connector); let task = client @@ -228,23 +226,20 @@ fn tcp_post() { runtime.block_on(task).unwrap(); } -#[cfg(unix)] #[test] +#[cfg_attr(windows, ignore)] // TODO: remove when windows build servers are upgraded to RS5 fn uds_post() { - let file_path = "/tmp/edgelet_test_uds_post.sock"; - - // make sure file gets deleted when test is done - defer! {{ - ::std::fs::remove_file(&file_path).unwrap_or(()); - }} + let dir = TempDir::new("uds").unwrap(); + let file_path = dir.path().join("sock"); + let file_path = file_path.to_str().unwrap(); - let path_copy = file_path.to_string(); - let server = run_uds_server(&path_copy, |req| { + let server = run_uds_server(&file_path, |req| { hello_handler(req).map_err(|err| io::Error::new(io::ErrorKind::Other, err)) }).map_err(|err| eprintln!("{}", err)); - let connector = - UrlConnector::new(&Url::parse(&format!("unix://{}", file_path)).unwrap()).unwrap(); + let mut url = Url::from_file_path(file_path).unwrap(); + url.set_scheme("unix").unwrap(); + let connector = UrlConnector::new(&url).unwrap(); let client = Client::builder().build::<_, Body>(connector); diff --git a/edgelet/edgelet-test-utils/Cargo.toml b/edgelet/edgelet-test-utils/Cargo.toml index d304ef484f4..a9912c78083 100644 --- a/edgelet/edgelet-test-utils/Cargo.toml +++ b/edgelet/edgelet-test-utils/Cargo.toml @@ -21,5 +21,7 @@ hyperlocal = "0.6" [target.'cfg(windows)'.dependencies] httparse = "1.2" +hyperlocal-windows = { git = "https://github.com/Azure/hyperlocal-windows" } mio = "0.6" mio-named-pipes = "0.1" +mio-uds-windows = { git = "https://github.com/Azure/mio-uds-windows.git" } diff --git a/edgelet/edgelet-test-utils/src/lib.rs b/edgelet/edgelet-test-utils/src/lib.rs index b281a2732c7..9020fefd0a1 100644 --- a/edgelet/edgelet-test-utils/src/lib.rs +++ b/edgelet/edgelet-test-utils/src/lib.rs @@ -23,9 +23,13 @@ extern crate hyper; #[cfg(unix)] extern crate hyperlocal; #[cfg(windows)] +extern crate hyperlocal_windows; +#[cfg(windows)] extern crate mio; #[cfg(windows)] extern crate mio_named_pipes; +#[cfg(windows)] +extern crate mio_uds_windows; extern crate serde; #[macro_use] extern crate serde_derive; @@ -43,7 +47,6 @@ pub mod web; pub use json_connector::{JsonConnector, StaticStream}; pub use web::run_tcp_server; -#[cfg(unix)] pub use web::run_uds_server; #[cfg(windows)] diff --git a/edgelet/edgelet-test-utils/src/web/linux.rs b/edgelet/edgelet-test-utils/src/web/linux.rs deleted file mode 100644 index 612103b35d5..00000000000 --- a/edgelet/edgelet-test-utils/src/web/linux.rs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -use std::fs; -use std::io; -use std::os::unix::net::UnixListener as StdUnixListener; - -use futures::prelude::*; -use hyper::service::service_fn; -use hyper::{self, Body, Request, Response}; -use hyperlocal::server::{Http as UdsHttp, Incoming as UdsIncoming}; - -pub fn run_uds_server(path: &str, handler: F) -> impl Future -where - F: 'static + Fn(Request) -> R + Clone + Send + Sync, - R: 'static + Future, Error = io::Error> + Send, -{ - fs::remove_file(&path).unwrap_or(()); - - // Bind a listener synchronously, so that the caller's client will not fail to connect - // regardless of when the asynchronous server accepts the connection - let listener = StdUnixListener::bind(path).unwrap(); - let incoming = UdsIncoming::from_std(listener, &Default::default()).unwrap(); - let serve = UdsHttp::new().serve_incoming(incoming, move || service_fn(handler.clone())); - - serve.for_each(|connecting| { - connecting - .then(|connection| { - let connection = connection.unwrap(); - Ok::<_, hyper::Error>(connection) - }).flatten() - .map_err(|e| { - io::Error::new( - io::ErrorKind::Other, - format!("failed to serve connection: {}", e), - ) - }) - }) -} diff --git a/edgelet/edgelet-test-utils/src/web/mod.rs b/edgelet/edgelet-test-utils/src/web/mod.rs index 985f36385dd..56214dc29a8 100644 --- a/edgelet/edgelet-test-utils/src/web/mod.rs +++ b/edgelet/edgelet-test-utils/src/web/mod.rs @@ -1,21 +1,26 @@ // Copyright (c) Microsoft. All rights reserved. -#[cfg(unix)] -mod linux; - #[cfg(windows)] mod windows; -#[cfg(unix)] -pub use self::linux::run_uds_server; - #[cfg(windows)] pub use self::windows::run_pipe_server; +use std::fs; +use std::io; +#[cfg(unix)] +use std::os::unix::net::UnixListener as StdUnixListener; + use futures::prelude::*; use hyper::server::conn::Http; use hyper::service::service_fn; use hyper::{self, Body, Request, Response}; +#[cfg(unix)] +use hyperlocal::server::{Http as UdsHttp, Incoming as UdsIncoming}; +#[cfg(windows)] +use hyperlocal_windows::server::{Http as UdsHttp, Incoming as UdsIncoming}; +#[cfg(windows)] +use mio_uds_windows::net::UnixListener as StdUnixListener; pub fn run_tcp_server( ip: &str, @@ -39,3 +44,31 @@ where }).flatten() }) } + +pub fn run_uds_server(path: &str, handler: F) -> impl Future +where + F: 'static + Fn(Request) -> R + Clone + Send + Sync, + R: 'static + Future, Error = io::Error> + Send, +{ + fs::remove_file(&path).unwrap_or(()); + + // Bind a listener synchronously, so that the caller's client will not fail to connect + // regardless of when the asynchronous server accepts the connection + let listener = StdUnixListener::bind(path).unwrap(); + let incoming = UdsIncoming::from_std(listener, &Default::default()).unwrap(); + let serve = UdsHttp::new().serve_incoming(incoming, move || service_fn(handler.clone())); + + serve.for_each(|connecting| { + connecting + .then(|connection| { + let connection = connection.unwrap(); + Ok::<_, hyper::Error>(connection) + }).flatten() + .map_err(|e| { + io::Error::new( + io::ErrorKind::Other, + format!("failed to serve connection: {}", e), + ) + }) + }) +} diff --git a/edgelet/iotedged/src/config/windows/default.yaml b/edgelet/iotedged/src/config/windows/default.yaml index b267f049bd9..05858d558ed 100644 --- a/edgelet/iotedged/src/config/windows/default.yaml +++ b/edgelet/iotedged/src/config/windows/default.yaml @@ -13,12 +13,12 @@ agent: hostname: "localhost" connect: - management_uri: "http://localhost:15580" - workload_uri: "http://localhost:15581" + management_uri: "unix:///C:/ProgramData/iotedge/mgmt/sock" + workload_uri: "unix:///C:/ProgramData/iotedge/workload/sock" listen: - management_uri: "http://0.0.0.0:15580" - workload_uri: "http://0.0.0.0:15581" + management_uri: "unix:///C:/ProgramData/iotedge/mgmt/sock" + workload_uri: "unix:///C:/ProgramData/iotedge/workload/sock" homedir: "C:\\ProgramData\\iotedge" diff --git a/edgelet/iotedged/src/error.rs b/edgelet/iotedged/src/error.rs index 8c17856e5eb..2bfcdbb5682 100644 --- a/edgelet/iotedged/src/error.rs +++ b/edgelet/iotedged/src/error.rs @@ -31,7 +31,7 @@ pub struct Error { inner: Context, } -#[derive(Clone, Copy, Debug, Fail, PartialEq)] +#[derive(Clone, Debug, Fail, PartialEq)] pub enum ErrorKind { #[fail(display = "Invalid configuration file")] Settings, @@ -80,6 +80,8 @@ pub enum ErrorKind { #[cfg(target_os = "windows")] #[fail(display = "Windows service error")] WindowsService, + #[fail(display = "Invalid uri {}", _0)] + InvalidUri(String), } impl Error { diff --git a/edgelet/iotedged/src/lib.rs b/edgelet/iotedged/src/lib.rs index 866ad7ec8c5..561d576d7ac 100644 --- a/edgelet/iotedged/src/lib.rs +++ b/edgelet/iotedged/src/lib.rs @@ -92,7 +92,7 @@ use edgelet_hsm::tpm::{TpmKey, TpmKeyStore}; use edgelet_hsm::Crypto; use edgelet_http::client::{Client as HttpClient, ClientImpl}; use edgelet_http::logging::LoggingService; -use edgelet_http::{ApiVersionService, HyperExt, MaybeProxyClient, API_VERSION}; +use edgelet_http::{ApiVersionService, HyperExt, MaybeProxyClient, UrlExt, API_VERSION}; use edgelet_http_mgmt::ManagementService; use edgelet_http_workload::WorkloadService; use edgelet_iothub::{HubIdentityManager, SasTokenSource}; @@ -682,7 +682,21 @@ fn vol_mount_uri(config: &mut DockerConfig, uris: &[&Url]) -> Result<(), Error> // if the url is a domain socket URL then vol mount it into the container for uri in uris { if uri.scheme() == UNIX_SCHEME { - binds.push(format!("{}:{}", uri.path(), uri.path())); + let path = uri.to_uds_file_path()?; + // On Windows we mount the parent folder because we can't mount the + // socket files directly + #[cfg(windows)] + let path = path + .parent() + .ok_or_else(|| ErrorKind::InvalidUri(uri.to_string()))?; + let path = path + .to_str() + .expect("URL points to a path that cannot be represented in UTF-8") + .to_string(); + let bind = format!("{}:{}", &path, &path); + if !binds.contains(&bind) { + binds.push(bind); + } } } diff --git a/scripts/windows/setup/IotEdgeSecurityDaemon.ps1 b/scripts/windows/setup/IotEdgeSecurityDaemon.ps1 index 380e2eed203..2791ec84a81 100644 --- a/scripts/windows/setup/IotEdgeSecurityDaemon.ps1 +++ b/scripts/windows/setup/IotEdgeSecurityDaemon.ps1 @@ -49,6 +49,10 @@ function Install-SecurityDaemon { $ErrorActionPreference = "Stop" Set-StrictMode -Version 5 + Set-Variable Windows1607 -Value 14393 -Option Constant + Set-Variable Windows1803 -Value 17134 -Option Constant + Set-Variable Windows1809 -Value 17763 -Option Constant + if (Test-EdgeAlreadyInstalled) { Write-Host ("`nIoT Edge is already installed. To reinstall, run 'Uninstall-SecurityDaemon' first.") ` -ForegroundColor "Red" @@ -87,8 +91,10 @@ function Install-SecurityDaemon { Set-ProvisioningMode Set-AgentImage Set-Hostname - Set-GatewayAddress Set-MobyNetwork + if (-not (Test-UdsSupport)) { + Set-GatewayAddress + } Install-IotEdgeService Write-Host ("`nThis device is now provisioned with the IoT Edge runtime.`n" + @@ -173,18 +179,28 @@ function Test-IsDockerRunning { return $true } +function Get-WindowsBuild { + return (Get-Item "HKLM:\Software\Microsoft\Windows NT\CurrentVersion").GetValue("CurrentBuild") +} + +function Test-UdsSupport { + # TODO: Enable when we have RS5-based modules in process-isolated containers + # $MinBuildForUnixDomainSockets = $Windows1809 + # $CurrentBuild = Get-WindowsBuild + # return ($ContainerOs -eq "Windows" -and $CurrentBuild -ge $MinBuildForUnixDomainSockets) + return $false +} + function Test-IsKernelValid { - $MinBuildForLinuxContainers = 14393 - $SupportedBuildsForWindowsContainers = @(17134, 17763) - $CurrentBuild = (Get-Item "HKLM:\Software\Microsoft\Windows NT\CurrentVersion").GetValue("CurrentBuild") + $MinBuildForLinuxContainers = $Windows1607 + $SupportedBuildsForWindowsContainers = @($Windows1803, $Windows1809) + $CurrentBuild = Get-WindowsBuild - # If using Linux containers, any Windows 10 version >14393 will suffice. if (($ContainerOs -eq "Linux" -and $CurrentBuild -ge $MinBuildForLinuxContainers) -or ` ($ContainerOs -eq "Windows" -and $SupportedBuildsForWindowsContainers -contains $CurrentBuild)) { Write-Host "The container host is on supported build version $CurrentBuild." -ForegroundColor "Green" return $true } else { - Write-Host ("The container host is on unsupported build version $CurrentBuild. `n" + "Please use a container host running build $MinBuildForLinuxContainers newer when using Linux containers" + "or with one of the following supported build versions when using Windows containers:`n" + @@ -255,6 +271,23 @@ function Get-SecurityDaemon { Copy-Item "C:\ProgramData\iotedge\iotedged-windows\*" "C:\ProgramData\iotedge" -Force -Recurse } + if (Test-UdsSupport) { + foreach ($Name in "mgmt", "workload") + { + # We can't bind socket files directly in Windows, so create a folder + # and bind to that. The folder needs to give Modify rights to a + # well-known group that will exist in any container so that + # non-privileged modules can access it. + $Path = "C:\ProgramData\iotedge\$Name" + New-Item "$Path" -ItemType "Directory" -Force + $Rule = New-Object -TypeName System.Security.AccessControl.FileSystemAccessRule(` + "NT AUTHORITY\Authenticated Users", 'Modify', 'ObjectInherit', 'InheritOnly', 'Allow') + $Acl = [System.IO.Directory]::GetAccessControl($Path) + $Acl.AddAccessRule($Rule) + [System.IO.Directory]::SetAccessControl($Path, $Acl) + } + } + if (Test-Path 'C:\ProgramData\iotedge\iotedged_eventlog_messages.dll') { # This release uses iotedged_eventlog_messages.dll as the eventlog message file