Skip to content
Merged
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
232 changes: 232 additions & 0 deletions src/Runner.Common/VSockSecretNotifier.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
using System;
using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using GitHub.DistributedTask.Logging;
using GitHub.Runner.Sdk;
using Newtonsoft.Json;

Comment thread
TingluoHuang marked this conversation as resolved.
namespace GitHub.Runner.Common
{

[ServiceLocator(Default = typeof(VSockSecretNotifier))]
public interface IVSockSecretNotifier : IRunnerService, IAsyncDisposable
{
bool TryStartNotifier();

void NotifyNewSecret(NewSecretEventArgs newSecret);
}

public sealed class VSockSecretNotifier : RunnerService, IVSockSecretNotifier
{
private Socket _vsock = null;

private CancellationTokenSource _cancellationTokenSource = null;

private Task _secretNotificationTask = null;

private Channel<byte[]> _channel = Channel.CreateUnbounded<byte[]>(new UnboundedChannelOptions() { SingleReader = true });

public bool TryStartNotifier()
{
if (_vsock != null)
{
Trace.Verbose("VSocket is already connected.");
return true;
}

// `GITHUB_ACTIONS_RUNNER_VSOCK_CID_PORT` is expected to be in the format "CID:PORT", e.g. "2:9999".
string vsockCidPort = Environment.GetEnvironmentVariable("GITHUB_ACTIONS_RUNNER_VSOCK_CID_PORT");
if (string.IsNullOrEmpty(vsockCidPort))
{
Trace.Verbose("VSocket CID/Port environment variable is not set.");
return false;
}

string[] parts = vsockCidPort.Split(':', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
if (parts.Length != 2)
{
Trace.Verbose("VSocket CID/Port environment variable is not in the correct format.");
return false;
}

uint cid, port;
if (!uint.TryParse(parts[0], out cid) || !uint.TryParse(parts[1], out port))
{
Trace.Verbose("VSocket CID/Port environment variable contains invalid numbers.");
return false;
}

Trace.Info($"Attempting to start VSocket secret notifier with CID: {cid}, Port: {port}.");
try
{
SafeSocketHandle nativeSocket = NativeSocket((int)(AddressFamily)40, (int)SocketType.Stream, 0);
if (nativeSocket.IsInvalid)
{
int error = Marshal.GetLastPInvokeError();
nativeSocket.Dispose();
throw new SocketException(error);
}

_vsock = new Socket(nativeSocket);
_vsock.Connect(new HostVsockEndPoint(cid, port));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

should we have a timeout here as it could be blocking?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

(on connect)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

connectasync with timeout doesn't work for the vsock, it will blows up with not support protocol exception, i tried that first. 🫠

}
Comment thread
TingluoHuang marked this conversation as resolved.
catch (Exception ex)
{
Trace.Error($"Failed to create and connect VSocket: {ex}");
_vsock?.Dispose();
_vsock = null;
return false;
}

_cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(HostContext.RunnerShutdownToken);
_secretNotificationTask = ProcessSecretChannel();
Trace.Info($"VSocket secret notifier started successfully.");
return true;
}

public void NotifyNewSecret(NewSecretEventArgs newSecret)
{
if (_vsock == null)
{
Trace.Verbose("VSocket is not connected, skipping secret notification.");
return;
}

byte[] payloadBytes = Encoding.UTF8.GetBytes(StringUtil.ConvertToJson(new { RunnerSecrets = newSecret }, Formatting.None));
byte[] lengthPrefix = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(payloadBytes.Length));
byte[] fullPayload = new byte[lengthPrefix.Length + payloadBytes.Length];
Buffer.BlockCopy(lengthPrefix, 0, fullPayload, 0, lengthPrefix.Length);
Buffer.BlockCopy(payloadBytes, 0, fullPayload, lengthPrefix.Length, payloadBytes.Length);

// we don't need to check return since unbounded channel will always accept the item.
_channel.Writer.TryWrite(fullPayload);
Comment thread
TingluoHuang marked this conversation as resolved.
}

public async ValueTask DisposeAsync()
{
if (_vsock != null && _secretNotificationTask != null)
{
_cancellationTokenSource?.Cancel();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should we have Writer.Complete()` and drain with a timeout before cancelling/disposing the socket?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

await _secretNotificationTask; should do that for us, the last line of ProcessSecretChannel is _channel.Writer.TryComplete();

try
{
await _secretNotificationTask;
}
catch (Exception ex)
{
Trace.Error($"Secret notification task finished with error: {ex}");
}

_cancellationTokenSource?.Dispose();
_cancellationTokenSource = null;
_vsock?.Dispose();
_vsock = null;
}
}

private async Task ProcessSecretChannel()
{
try
{
while (!_cancellationTokenSource.Token.IsCancellationRequested &&
await _channel.Reader.WaitToReadAsync(_cancellationTokenSource.Token))
{
while (_channel.Reader.TryRead(out var payload))
{
try
{
// Socket.SendAsync on a stream socket may send fewer bytes than requested,
// so keep sending until the entire payload has been written.
int totalSent = 0;
while (totalSent < payload.Length)
{
int bytesSent = await _vsock.SendAsync(payload.AsMemory(totalSent), SocketFlags.None, _cancellationTokenSource.Token);
if (bytesSent == 0)
{
throw new SocketException((int)SocketError.ConnectionReset);
}

totalSent += bytesSent;
}
}
catch (OperationCanceledException)
{
Trace.Info("Secret notification task was canceled.");
}
catch (Exception ex)
{
Trace.Error($"Failed to notify new secret over VSocket: {ex}");
}
}
}
}
catch (OperationCanceledException)
{
Trace.Info("Secret notification task was canceled.");
}
catch (Exception ex)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

doesn't a normal shutdown get classed as an error here, might want OperationCanceledException specifically excluded?

{
Trace.Error($"Failed to process secret channel: {ex}");
}

_channel.Writer.TryComplete();
}

[DllImport("libc", SetLastError = true, EntryPoint = "socket")]
private static extern SafeSocketHandle NativeSocket(int domain, int type, int protocol);
}

internal 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 => (AddressFamily)40;

public override SocketAddress Serialize()
{
SocketAddress socketAddress = new SocketAddress(AddressFamily.Unspecified, SocketAddressSize);
Comment thread
TingluoHuang marked this conversation as resolved.
// 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);
}
}
}
22 changes: 15 additions & 7 deletions src/Runner.Worker/Worker.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
using GitHub.DistributedTask.WebApi;
using Pipelines = GitHub.DistributedTask.Pipelines;
using GitHub.Runner.Common.Util;
using Newtonsoft.Json;
using System;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using GitHub.Services.WebApi;
using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Common;
using GitHub.Runner.Common.Util;
using GitHub.Runner.Sdk;
using System.Text;
using Newtonsoft.Json;
using Pipelines = GitHub.DistributedTask.Pipelines;

namespace GitHub.Runner.Worker
{
Expand Down Expand Up @@ -46,6 +45,7 @@ public async Task<int> RunAsync(string pipeIn, string pipeOut)
var jobRunner = HostContext.CreateService<IJobRunner>();
var terminal = HostContext.GetService<ITerminal>();

await using (var secretNotifier = HostContext.GetService<IVSockSecretNotifier>())
using (var channel = HostContext.CreateService<IProcessChannel>())
using (var jobRequestCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(HostContext.RunnerShutdownToken))
using (var channelTokenSource = new CancellationTokenSource())
Expand Down Expand Up @@ -86,6 +86,14 @@ public async Task<int> RunAsync(string pipeIn, string pipeOut)
HostContext.WritePerfCounter($"WorkerJobMessageReceived_{jobMessage.RequestId.ToString()}");

// Initialize the secret masker and set the thread culture.
if (Constants.Runner.Platform == Constants.OSPlatform.Linux &&
secretNotifier.TryStartNotifier())
{
HostContext.SecretMasker.NewSecretAdded += (sender, e) =>
{
secretNotifier.NotifyNewSecret(e);
};
}
InitializeSecretMasker(jobMessage);
SetCulture(jobMessage);

Expand Down
38 changes: 38 additions & 0 deletions src/Sdk/DTLogging/Logging/ISecretMasker.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.Serialization;

namespace GitHub.DistributedTask.Logging
{
Expand All @@ -11,5 +13,41 @@ public interface ISecretMasker
void AddValueEncoder(ValueEncoder encoder);
ISecretMasker Clone();
String MaskSecrets(String input);
public event EventHandler<NewSecretEventArgs> NewSecretAdded;
Comment thread
TingluoHuang marked this conversation as resolved.
}

public abstract class NewSecretEventArgs : EventArgs
{
public abstract String Type { get; }
}

[DataContract]
public sealed class NewRegexSecretEventArgs : NewSecretEventArgs
{
[DataMember]
public override String Type => "regex";

public NewRegexSecretEventArgs(String pattern)
{
Pattern = pattern;
}

[DataMember]
public String Pattern { get; private set; }
}

[DataContract]
public sealed class NewVariableSecretEventArgs : NewSecretEventArgs
{
[DataMember]
public override String Type => "variable";

public NewVariableSecretEventArgs(List<string> values)
{
Values.AddRange(values);
}
Comment thread
TingluoHuang marked this conversation as resolved.

[DataMember]
public List<string> Values { get; private set; } = new List<string>();
}
}
7 changes: 7 additions & 0 deletions src/Sdk/DTLogging/Logging/SecretMasker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ namespace GitHub.DistributedTask.Logging
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class SecretMasker : ISecretMasker, IDisposable
{
public event EventHandler<NewSecretEventArgs> NewSecretAdded;

public SecretMasker()
{
m_originalValueSecrets = new HashSet<ValueSecret>();
Expand Down Expand Up @@ -66,6 +68,8 @@ public void AddRegex(String pattern)
m_lock.ExitWriteLock();
}
}

NewSecretAdded?.Invoke(this, new NewRegexSecretEventArgs(pattern));
Comment thread
TingluoHuang marked this conversation as resolved.
}

/// <summary>
Expand Down Expand Up @@ -133,6 +137,9 @@ public void AddValue(String value)
m_lock.ExitWriteLock();
}
}

// valueSecrets contains all the values run through the encoders.
NewSecretAdded?.Invoke(this, new NewVariableSecretEventArgs(valueSecrets.Select(x => x.m_value).ToList()));
}

/// <summary>
Expand Down
4 changes: 4 additions & 0 deletions src/Test/L0/Worker/WorkerL0.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ public sealed class WorkerL0
{
private Mock<IProcessChannel> _processChannel;
private Mock<IJobRunner> _jobRunner;
private Mock<IVSockSecretNotifier> _vsockSecretNotifier;

public WorkerL0()
{
_processChannel = new Mock<IProcessChannel>();
_jobRunner = new Mock<IJobRunner>();
_vsockSecretNotifier = new Mock<IVSockSecretNotifier>();
}

private Pipelines.AgentJobRequestMessage CreateJobRequestMessage(string jobName)
Expand Down Expand Up @@ -88,6 +90,7 @@ public async void DispatchRunNewJob()
var worker = new GitHub.Runner.Worker.Worker();
hc.EnqueueInstance<IProcessChannel>(_processChannel.Object);
hc.EnqueueInstance<IJobRunner>(_jobRunner.Object);
hc.SetSingleton<IVSockSecretNotifier>(_vsockSecretNotifier.Object);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

maybe we can add more tests for VSockSecretNotifier like connection failure returning false?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

VSockSecretNotifier is best effort, so i think we should be fine here.

worker.Initialize(hc);
var jobMessage = CreateJobRequestMessage("job1");
var arWorkerMessages = new WorkerMessage[]
Expand Down Expand Up @@ -139,6 +142,7 @@ public async void DispatchCancellation()
var worker = new GitHub.Runner.Worker.Worker();
hc.EnqueueInstance<IProcessChannel>(_processChannel.Object);
hc.EnqueueInstance<IJobRunner>(_jobRunner.Object);
hc.SetSingleton<IVSockSecretNotifier>(_vsockSecretNotifier.Object);
worker.Initialize(hc);
var jobMessage = CreateJobRequestMessage("job1");
var cancelMessage = CreateJobCancelMessage(jobMessage.JobId);
Expand Down
Loading