diff --git a/src/Runner.Common/CommandLineParser.cs b/src/Runner.Common/CommandLineParser.cs index 7880f65f308..2de343fb57d 100644 --- a/src/Runner.Common/CommandLineParser.cs +++ b/src/Runner.Common/CommandLineParser.cs @@ -13,6 +13,7 @@ namespace GitHub.Runner.Common public sealed class CommandLineParser { private ISecretMasker _secretMasker; + private RunnerFirewallNotifier _runnerFirewallNotifier; private Tracing _trace; public List Commands { get; } @@ -24,6 +25,7 @@ public sealed class CommandLineParser public CommandLineParser(IHostContext hostContext, string[] secretArgNames) { _secretMasker = hostContext.SecretMasker; + _runnerFirewallNotifier = hostContext.RunnerFirewallNotifier; _trace = hostContext.GetTrace(nameof(CommandLineParser)); Commands = new List(); @@ -91,6 +93,9 @@ public void Parse(string[] args) if (SecretArgNames.Contains(argScope)) { _secretMasker.AddValue(arg); + _runnerFirewallNotifier.NotifySecretRegistration( + secrets: new List { arg }, + secretRegexes: null); } _trace.Info("Adding option '{0}': '{1}'", argScope, arg); diff --git a/src/Runner.Common/HostContext.cs b/src/Runner.Common/HostContext.cs index ffb08684a53..52fa245477c 100644 --- a/src/Runner.Common/HostContext.cs +++ b/src/Runner.Common/HostContext.cs @@ -25,6 +25,7 @@ public interface IHostContext : IDisposable CancellationToken RunnerShutdownToken { get; } ShutdownReason RunnerShutdownReason { get; } ISecretMasker SecretMasker { get; } + RunnerFirewallNotifier RunnerFirewallNotifier { get; } List UserAgents { get; } RunnerWebProxy WebProxy { get; } string GetDirectory(WellKnownDirectory directory); @@ -61,6 +62,7 @@ public sealed class HostContext : EventListener, IObserver, private readonly ConcurrentDictionary _serviceInstances = new(); private readonly ConcurrentDictionary _serviceTypes = new(); private readonly ISecretMasker _secretMasker = new SecretMasker(); + private readonly RunnerFirewallNotifier _runnerFirewallNotifier = new RunnerFirewallNotifier(); private readonly List _userAgents = new() { new ProductInfoHeaderValue($"GitHubActionsRunner-{BuildConstants.RunnerPackage.PackageName}", BuildConstants.RunnerPackage.Version) }; private CancellationTokenSource _runnerShutdownTokenSource = new(); private object _perfLock = new(); @@ -88,6 +90,7 @@ public sealed class HostContext : EventListener, IObserver, public CancellationToken RunnerShutdownToken => _runnerShutdownTokenSource.Token; public ShutdownReason RunnerShutdownReason { get; private set; } public ISecretMasker SecretMasker => _secretMasker; + public RunnerFirewallNotifier RunnerFirewallNotifier => _runnerFirewallNotifier; public List UserAgents => _userAgents; public RunnerWebProxy WebProxy => _webProxy; public bool AllowAuthMigration => _allowAuthMigration.IsSet; @@ -190,6 +193,9 @@ public HostContext(string hostType, string logFile = null) if (!string.IsNullOrEmpty(WebProxy.HttpProxyPassword)) { this.SecretMasker.AddValue(WebProxy.HttpProxyPassword); + this.RunnerFirewallNotifier.NotifySecretRegistration( + secrets: new List { WebProxy.HttpProxyPassword }, + secretRegexes: null); } _trace.Info($"Configuring authenticated proxy {WebProxy.HttpProxyAddress} for all HTTP requests."); @@ -208,6 +214,9 @@ public HostContext(string hostType, string logFile = null) if (!string.IsNullOrEmpty(WebProxy.HttpsProxyPassword)) { this.SecretMasker.AddValue(WebProxy.HttpsProxyPassword); + this.RunnerFirewallNotifier.NotifySecretRegistration( + secrets: new List { WebProxy.HttpsProxyPassword }, + secretRegexes: null); } _trace.Info($"Configuring authenticated proxy {WebProxy.HttpsProxyAddress} for all HTTPS requests."); diff --git a/src/Runner.Common/RunnerFirewallNotifier.cs b/src/Runner.Common/RunnerFirewallNotifier.cs new file mode 100644 index 00000000000..d5e5ea844a3 --- /dev/null +++ b/src/Runner.Common/RunnerFirewallNotifier.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using Microsoft.Win32.SafeHandles; +using System.Net; +using System.Net.Sockets; +using System.Runtime.InteropServices; +using System.Text; +using System.Text.Json; + +namespace GitHub.Runner.Common +{ + public sealed class RunnerFirewallNotifier + { + private const AddressFamily VsockAddressFamily = (AddressFamily)40; + internal bool IsLinux { get; } + private readonly Socket _vsock; + private readonly object _vsockSendLock = new object(); + + public RunnerFirewallNotifier() + { + IsLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + _vsock = IsLinux ? TryCreateConnectedVsock() : null; + } + + public void NotifySecretRegistration(List secrets, List secretRegexes) + { + if (_vsock == null) + { + return; + } + + try + { + string jsonPayload = JsonSerializer.Serialize(new + { + RunnerSecrets = new + { + secrets = secrets ?? new List(), + secretRegexes = secretRegexes ?? new List() + } + }); + + byte[] payloadBytes = Encoding.UTF8.GetBytes(jsonPayload); + byte[] lengthPrefix = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(payloadBytes.Length)); + + lock (_vsockSendLock) + { + SendAll(_vsock, lengthPrefix); + SendAll(_vsock, payloadBytes); + } + } + catch + { + // Notification delivery is best-effort + } + } + + private static Socket TryCreateConnectedVsock() + { + Socket socket = null; + try + { + SafeSocketHandle nativeSocket = NativeSocket((int)VsockAddressFamily, (int)SocketType.Stream, 0); + if (nativeSocket.IsInvalid) + { + int error = Marshal.GetLastPInvokeError(); + nativeSocket.Dispose(); + throw new SocketException(error); + } + + socket = new Socket(nativeSocket); + socket.Connect(new HostVsockEndPoint(2, 9999)); + return socket; + } + catch + { + socket?.Dispose(); + return null; + } + } + + [DllImport("libc", SetLastError = true, EntryPoint = "socket")] + private static extern SafeSocketHandle NativeSocket(int domain, int type, int protocol); + + private static void SendAll(Socket socket, byte[] buffer) + { + int offset = 0; + while (offset < buffer.Length) + { + int bytesSent = socket.Send(buffer, offset, buffer.Length - offset, SocketFlags.None); + if (bytesSent <= 0) + { + throw new SocketException((int)SocketError.ConnectionReset); + } + + offset += bytesSent; + } + } + + private sealed class HostVsockEndPoint : EndPoint + { + private const int SocketAddressSize = 16; + private readonly uint _cid; + private readonly uint _port; + + public HostVsockEndPoint(uint cid, uint port) + { + _cid = cid; + _port = port; + } + + public override AddressFamily AddressFamily => VsockAddressFamily; + + public override SocketAddress Serialize() + { + SocketAddress socketAddress = new SocketAddress(AddressFamily.Unspecified, SocketAddressSize); + // sockaddr_vm layout: family(0-1), reserved1(2-3), port(4-7), cid(8-11) + ushort family = (ushort)AddressFamily; + socketAddress[0] = (byte)(family & 0xFF); + socketAddress[1] = (byte)((family >> 8) & 0xFF); + socketAddress[2] = 0; + socketAddress[3] = 0; + socketAddress[4] = (byte)(_port & 0xFF); + socketAddress[5] = (byte)((_port >> 8) & 0xFF); + socketAddress[6] = (byte)((_port >> 16) & 0xFF); + socketAddress[7] = (byte)((_port >> 24) & 0xFF); + socketAddress[8] = (byte)(_cid & 0xFF); + socketAddress[9] = (byte)((_cid >> 8) & 0xFF); + socketAddress[10] = (byte)((_cid >> 16) & 0xFF); + socketAddress[11] = (byte)((_cid >> 24) & 0xFF); + return socketAddress; + } + + public override EndPoint Create(SocketAddress socketAddress) + { + uint port = (uint)socketAddress[4] + | ((uint)socketAddress[5] << 8) + | ((uint)socketAddress[6] << 16) + | ((uint)socketAddress[7] << 24); + + uint cid = (uint)socketAddress[8] + | ((uint)socketAddress[9] << 8) + | ((uint)socketAddress[10] << 16) + | ((uint)socketAddress[11] << 24); + + return new HostVsockEndPoint(cid, port); + } + } + } +} diff --git a/src/Runner.Common/Terminal.cs b/src/Runner.Common/Terminal.cs index 75489bc1853..c4ce405e9ce 100644 --- a/src/Runner.Common/Terminal.cs +++ b/src/Runner.Common/Terminal.cs @@ -85,6 +85,9 @@ public string ReadSecret() if (!string.IsNullOrEmpty(val)) { HostContext.SecretMasker.AddValue(val); + HostContext.RunnerFirewallNotifier.NotifySecretRegistration( + secrets: new List { val }, + secretRegexes: null); } Trace.Info($"Read value: '{val}'"); diff --git a/src/Runner.Listener/CommandSettings.cs b/src/Runner.Listener/CommandSettings.cs index 7c29e2b6d2a..51a4fced9bc 100644 --- a/src/Runner.Listener/CommandSettings.cs +++ b/src/Runner.Listener/CommandSettings.cs @@ -127,6 +127,9 @@ public CommandSettings(IHostContext context, string[] args) if (secret) { context.SecretMasker.AddValue(val); + context.RunnerFirewallNotifier.NotifySecretRegistration( + secrets: new List { val }, + secretRegexes: null); } // Store the value. diff --git a/src/Runner.Listener/Configuration/ConfigurationManager.cs b/src/Runner.Listener/Configuration/ConfigurationManager.cs index 360728a1c9b..d5084d29594 100644 --- a/src/Runner.Listener/Configuration/ConfigurationManager.cs +++ b/src/Runner.Listener/Configuration/ConfigurationManager.cs @@ -717,6 +717,9 @@ private async Task GetRunnerTokenAsync(CommandSettings command, string g var jitToken = await GetJITRunnerTokenAsync(githubUrl, githubPAT, tokenType); Trace.Info($"Retrived runner {tokenType} token is good to {jitToken.ExpiresAt}."); HostContext.SecretMasker.AddValue(jitToken.Token); + HostContext.RunnerFirewallNotifier.NotifySecretRegistration( + secrets: new List { jitToken.Token }, + secretRegexes: null); runnerToken = jitToken.Token; } @@ -783,6 +786,9 @@ private async Task GetJITRunnerTokenAsync(string gith { var base64EncodingToken = Convert.ToBase64String(Encoding.UTF8.GetBytes($"github:{githubToken}")); HostContext.SecretMasker.AddValue(base64EncodingToken); + HostContext.RunnerFirewallNotifier.NotifySecretRegistration( + secrets: new List { base64EncodingToken }, + secretRegexes: null); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("basic", base64EncodingToken); httpClient.DefaultRequestHeaders.UserAgent.AddRange(HostContext.UserAgents); httpClient.DefaultRequestHeaders.Accept.ParseAdd("application/vnd.github.v3+json"); diff --git a/src/Runner.Worker/ActionCommandManager.cs b/src/Runner.Worker/ActionCommandManager.cs index 4b9995fc89d..adf9cc9ef10 100644 --- a/src/Runner.Worker/ActionCommandManager.cs +++ b/src/Runner.Worker/ActionCommandManager.cs @@ -116,6 +116,9 @@ public bool TryProcessCommand(IExecutionContext context, string input, Container if (_stopToken.Length > 6) { HostContext.SecretMasker.AddValue(_stopToken); + HostContext.RunnerFirewallNotifier.NotifySecretRegistration( + secrets: new List { _stopToken }, + secretRegexes: null); } context.Output(input); @@ -438,6 +441,9 @@ public void ProcessCommand(IExecutionContext context, string line, ActionCommand } HostContext.SecretMasker.AddValue(command.Data); + HostContext.RunnerFirewallNotifier.NotifySecretRegistration( + secrets: new List { command.Data }, + secretRegexes: null); Trace.Info($"Add new secret mask with length of {command.Data.Length}"); // Also add each individual line. Typically individual lines are processed from STDOUT of child processes. @@ -445,6 +451,9 @@ public void ProcessCommand(IExecutionContext context, string line, ActionCommand foreach (var item in split) { HostContext.SecretMasker.AddValue(item); + HostContext.RunnerFirewallNotifier.NotifySecretRegistration( + secrets: new List { item }, + secretRegexes: null); } } } diff --git a/src/Runner.Worker/ActionManager.cs b/src/Runner.Worker/ActionManager.cs index d9e590cae37..65cd8183479 100644 --- a/src/Runner.Worker/ActionManager.cs +++ b/src/Runner.Worker/ActionManager.cs @@ -1119,6 +1119,9 @@ private async Task BuildActionContainerAsync(IExecutionContext executionContext, { // Add secret HostContext.SecretMasker.AddValue(actionDownloadInfo.Authentication?.Token); + HostContext.RunnerFirewallNotifier.NotifySecretRegistration( + secrets: new List { actionDownloadInfo.Authentication?.Token }, + secretRegexes: null); // Default auth token if (string.IsNullOrEmpty(actionDownloadInfo.Authentication?.Token)) @@ -1637,6 +1640,9 @@ private AuthenticationHeaderValue CreateAuthHeader(IExecutionContext executionCo Trace.Info("Using Basic token for action archive download."); var base64EncodingToken = Convert.ToBase64String(Encoding.UTF8.GetBytes($"x-access-token:{token}")); HostContext.SecretMasker.AddValue(base64EncodingToken); + HostContext.RunnerFirewallNotifier.NotifySecretRegistration( + secrets: new List { base64EncodingToken }, + secretRegexes: null); return new AuthenticationHeaderValue("Basic", base64EncodingToken); } } diff --git a/src/Runner.Worker/ExecutionContext.cs b/src/Runner.Worker/ExecutionContext.cs index 0f9410821c6..17d1f424815 100644 --- a/src/Runner.Worker/ExecutionContext.cs +++ b/src/Runner.Worker/ExecutionContext.cs @@ -1025,6 +1025,9 @@ public void InitializeJob(Pipelines.AgentJobRequestMessage message, Cancellation var githubAccessToken = new StringContextData(Global.Variables.Get("system.github.token")); var base64EncodedToken = Convert.ToBase64String(Encoding.UTF8.GetBytes($"x-access-token:{githubAccessToken}")); HostContext.SecretMasker.AddValue(base64EncodedToken); + HostContext.RunnerFirewallNotifier.NotifySecretRegistration( + secrets: new List { base64EncodedToken }, + secretRegexes: null); var githubJob = Global.Variables.Get("system.github.job"); var githubContext = new GitHubContext(); githubContext["token"] = githubAccessToken; diff --git a/src/Runner.Worker/Worker.cs b/src/Runner.Worker/Worker.cs index 4784c169314..185b0c3e9e7 100644 --- a/src/Runner.Worker/Worker.cs +++ b/src/Runner.Worker/Worker.cs @@ -154,12 +154,18 @@ private void InitializeSecretMasker(Pipelines.AgentJobRequestMessage message) // Add the entire value, even if it contains CR or LF. During expression tracing, // invidual trace info may contain line breaks. HostContext.SecretMasker.AddValue(value); + HostContext.RunnerFirewallNotifier.NotifySecretRegistration( + secrets: new List { value }, + secretRegexes: null); // Also add each individual line. Typically individual lines are processed from STDOUT of child processes. var split = value.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); foreach (var item in split) { HostContext.SecretMasker.AddValue(item); + HostContext.RunnerFirewallNotifier.NotifySecretRegistration( + secrets: new List { item }, + secretRegexes: null); } } } @@ -174,6 +180,9 @@ private void InitializeSecretMasker(Pipelines.AgentJobRequestMessage message) // We need this because the worker will print out the job message JSON to diag log // and SecretMasker has JsonEscapeEncoder hook up HostContext.SecretMasker.AddValue(maskHint.Value); + HostContext.RunnerFirewallNotifier.NotifySecretRegistration( + secrets: new List { maskHint.Value }, + secretRegexes: new List { maskHint.Value }); } else { @@ -192,6 +201,9 @@ private void InitializeSecretMasker(Pipelines.AgentJobRequestMessage message) if (!string.IsNullOrEmpty(value)) { HostContext.SecretMasker.AddValue(value); + HostContext.RunnerFirewallNotifier.NotifySecretRegistration( + secrets: new List { value }, + secretRegexes: null); } } } diff --git a/src/Test/L0/TestHostContext.cs b/src/Test/L0/TestHostContext.cs index c1cf692204f..7326b2e592d 100644 --- a/src/Test/L0/TestHostContext.cs +++ b/src/Test/L0/TestHostContext.cs @@ -34,6 +34,7 @@ public sealed class TestHostContext : IHostContext, IDisposable public CancellationToken RunnerShutdownToken => _runnerShutdownTokenSource.Token; public ShutdownReason RunnerShutdownReason { get; private set; } public ISecretMasker SecretMasker => _secretMasker; + public RunnerFirewallNotifier RunnerFirewallNotifier { get; } = new RunnerFirewallNotifier(); public TestHostContext(object testClass, [CallerMemberName] string testName = "") {