Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/Runner.Common/CommandLineParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ namespace GitHub.Runner.Common
public sealed class CommandLineParser
{
private ISecretMasker _secretMasker;
private RunnerFirewallNotifier _runnerFirewallNotifier;
private Tracing _trace;

public List<string> Commands { get; }
Expand All @@ -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<string>();
Expand Down Expand Up @@ -91,6 +93,9 @@ public void Parse(string[] args)
if (SecretArgNames.Contains(argScope))
{
_secretMasker.AddValue(arg);
_runnerFirewallNotifier.NotifySecretRegistration(
secrets: new List<string> { arg },
secretRegexes: null);
}

_trace.Info("Adding option '{0}': '{1}'", argScope, arg);
Expand Down
9 changes: 9 additions & 0 deletions src/Runner.Common/HostContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public interface IHostContext : IDisposable
CancellationToken RunnerShutdownToken { get; }
ShutdownReason RunnerShutdownReason { get; }
ISecretMasker SecretMasker { get; }
RunnerFirewallNotifier RunnerFirewallNotifier { get; }
List<ProductInfoHeaderValue> UserAgents { get; }
RunnerWebProxy WebProxy { get; }
string GetDirectory(WellKnownDirectory directory);
Expand Down Expand Up @@ -61,6 +62,7 @@ public sealed class HostContext : EventListener, IObserver<DiagnosticListener>,
private readonly ConcurrentDictionary<Type, object> _serviceInstances = new();
private readonly ConcurrentDictionary<Type, Type> _serviceTypes = new();
private readonly ISecretMasker _secretMasker = new SecretMasker();
private readonly RunnerFirewallNotifier _runnerFirewallNotifier = new RunnerFirewallNotifier();
private readonly List<ProductInfoHeaderValue> _userAgents = new() { new ProductInfoHeaderValue($"GitHubActionsRunner-{BuildConstants.RunnerPackage.PackageName}", BuildConstants.RunnerPackage.Version) };
private CancellationTokenSource _runnerShutdownTokenSource = new();
private object _perfLock = new();
Expand Down Expand Up @@ -88,6 +90,7 @@ public sealed class HostContext : EventListener, IObserver<DiagnosticListener>,
public CancellationToken RunnerShutdownToken => _runnerShutdownTokenSource.Token;
public ShutdownReason RunnerShutdownReason { get; private set; }
public ISecretMasker SecretMasker => _secretMasker;
public RunnerFirewallNotifier RunnerFirewallNotifier => _runnerFirewallNotifier;
public List<ProductInfoHeaderValue> UserAgents => _userAgents;
public RunnerWebProxy WebProxy => _webProxy;
public bool AllowAuthMigration => _allowAuthMigration.IsSet;
Expand Down Expand Up @@ -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<string> { WebProxy.HttpProxyPassword },
secretRegexes: null);
}

_trace.Info($"Configuring authenticated proxy {WebProxy.HttpProxyAddress} for all HTTP requests.");
Expand All @@ -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<string> { WebProxy.HttpsProxyPassword },
secretRegexes: null);
}

_trace.Info($"Configuring authenticated proxy {WebProxy.HttpsProxyAddress} for all HTTPS requests.");
Expand Down
150 changes: 150 additions & 0 deletions src/Runner.Common/RunnerFirewallNotifier.cs
Original file line number Diff line number Diff line change
@@ -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<string> secrets, List<string> secretRegexes)
{
if (_vsock == null)
{
return;
}

try
{
string jsonPayload = JsonSerializer.Serialize(new
{
RunnerSecrets = new
{
secrets = secrets ?? new List<string>(),
secretRegexes = secretRegexes ?? new List<string>()
}
});

byte[] payloadBytes = Encoding.UTF8.GetBytes(jsonPayload);
byte[] lengthPrefix = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(payloadBytes.Length));

lock (_vsockSendLock)
{
SendAll(_vsock, lengthPrefix);
SendAll(_vsock, payloadBytes);
Comment on lines +48 to +49

@TingluoHuang TingluoHuang Jul 21, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need to call sendall twice?
it the protocol require us doing this, or this can be combined into 1 Send call?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could combine the byte arrays and do one call to SendAll(), as long as we send the 4 byte length field (network byte order) before we send the payload.

}
}
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));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we probably want to use some input to provide the value, so it's not hardcoded.

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);
}
}
}
}
3 changes: 3 additions & 0 deletions src/Runner.Common/Terminal.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ public string ReadSecret()
if (!string.IsNullOrEmpty(val))
{
HostContext.SecretMasker.AddValue(val);
HostContext.RunnerFirewallNotifier.NotifySecretRegistration(
secrets: new List<string> { val },
secretRegexes: null);
}

Trace.Info($"Read value: '{val}'");
Expand Down
3 changes: 3 additions & 0 deletions src/Runner.Listener/CommandSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,9 @@ public CommandSettings(IHostContext context, string[] args)
if (secret)
{
context.SecretMasker.AddValue(val);
context.RunnerFirewallNotifier.NotifySecretRegistration(
secrets: new List<string> { val },
secretRegexes: null);
}

// Store the value.
Expand Down
6 changes: 6 additions & 0 deletions src/Runner.Listener/Configuration/ConfigurationManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -717,6 +717,9 @@ private async Task<string> 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<string> { jitToken.Token },
secretRegexes: null);
runnerToken = jitToken.Token;
}

Expand Down Expand Up @@ -783,6 +786,9 @@ private async Task<GitHubRunnerRegisterToken> GetJITRunnerTokenAsync(string gith
{
var base64EncodingToken = Convert.ToBase64String(Encoding.UTF8.GetBytes($"github:{githubToken}"));
HostContext.SecretMasker.AddValue(base64EncodingToken);
HostContext.RunnerFirewallNotifier.NotifySecretRegistration(
secrets: new List<string> { 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");
Expand Down
9 changes: 9 additions & 0 deletions src/Runner.Worker/ActionCommandManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> { _stopToken },
secretRegexes: null);
}

context.Output(input);
Expand Down Expand Up @@ -438,13 +441,19 @@ public void ProcessCommand(IExecutionContext context, string line, ActionCommand
}

HostContext.SecretMasker.AddValue(command.Data);
HostContext.RunnerFirewallNotifier.NotifySecretRegistration(
secrets: new List<string> { 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.
var split = command.Data.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
foreach (var item in split)
{
HostContext.SecretMasker.AddValue(item);
HostContext.RunnerFirewallNotifier.NotifySecretRegistration(
secrets: new List<string> { item },
secretRegexes: null);
}
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/Runner.Worker/ActionManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1119,6 +1119,9 @@ private async Task BuildActionContainerAsync(IExecutionContext executionContext,
{
// Add secret
HostContext.SecretMasker.AddValue(actionDownloadInfo.Authentication?.Token);
HostContext.RunnerFirewallNotifier.NotifySecretRegistration(
secrets: new List<string> { actionDownloadInfo.Authentication?.Token },
secretRegexes: null);

// Default auth token
if (string.IsNullOrEmpty(actionDownloadInfo.Authentication?.Token))
Expand Down Expand Up @@ -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<string> { base64EncodingToken },
secretRegexes: null);
return new AuthenticationHeaderValue("Basic", base64EncodingToken);
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/Runner.Worker/ExecutionContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> { base64EncodedToken },
secretRegexes: null);
var githubJob = Global.Variables.Get("system.github.job");
var githubContext = new GitHubContext();
githubContext["token"] = githubAccessToken;
Expand Down
12 changes: 12 additions & 0 deletions src/Runner.Worker/Worker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> { 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<string> { item },
secretRegexes: null);
}
}
}
Expand All @@ -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<string> { maskHint.Value },
secretRegexes: new List<string> { maskHint.Value });
}
else
{
Expand All @@ -192,6 +201,9 @@ private void InitializeSecretMasker(Pipelines.AgentJobRequestMessage message)
if (!string.IsNullOrEmpty(value))
{
HostContext.SecretMasker.AddValue(value);
HostContext.RunnerFirewallNotifier.NotifySecretRegistration(
secrets: new List<string> { value },
secretRegexes: null);
}
}
}
Expand Down
1 change: 1 addition & 0 deletions src/Test/L0/TestHostContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "")
{
Expand Down