Public Access
Add port forwarding: local, remote and dynamic, per terminal session
A host can now carry port forwards (-L, -R, -D), saved inside its encrypted payload as RelayTicketRequest.PortForwardId already assumed, so they sync and merge per forward like tags and pinned paths. The host schema moves to version 8 only for hosts that carry forwards. ISshConnection gains StartForwardAsync over SSH.NET's forwarded ports. Every listener binds loopback and there is no bind-address field: a forward is something any member of a shared keychain can edit. A remote forward's target is resolved here, IPv4 first, rather than by SSH.NET's constructor, which takes the first DNS answer and can send ::1. Forwards start with every terminal opened to the host and stop with its connection; a reconnect starts them again. The session sidebar lists them with state, failure reason and an on/off switch, the status bar counts the listening ones, and the host editor and detail pane gain a PORT FORWARDS section. A forward that cannot start fails alone. The phone does not start forwards yet: it has no surface showing what is listening, and its More screen now says so. No byte counts, since SSH.NET keeps none.
This commit is contained in:
1 parent
c8848ec1e3
commit
f05deb4fbc
36 files changed
+2776
-42
No files matched your search
@@ -29,6 +29,14 @@ internal sealed class FakeSshConnectionFactory : ISshConnectionFactory, ISftpSes
|
||||
/// <summary>Requests this factory was asked for, in order.</summary>
|
||||
internal List<SshConnectionRequest> Requests { get; } = [];
|
||||
|
||||
/// <summary>Every forward started on any connection this factory opened, in order.</summary>
|
||||
internal List<FakeSshPortForward> Forwards { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Decides whether a forward is refused, and with what sentence, when set. Null from it starts the forward.
|
||||
/// </summary>
|
||||
internal Func<SshPortForwardSpec, string?>? ForwardFailure { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Runs against each shell request before it succeeds or fails, when set.
|
||||
/// </summary>
|
||||
@@ -118,7 +126,7 @@ internal sealed class FakeSshConnectionFactory : ISshConnectionFactory, ISftpSes
|
||||
progress?.Report(SshConnectionPhase.CheckingHostKey);
|
||||
progress?.Report(SshConnectionPhase.Authenticating);
|
||||
|
||||
return new FakeSshConnection(request, Shells);
|
||||
return new FakeSshConnection(request, Shells, this);
|
||||
}
|
||||
|
||||
/// <summary>The file-transfer session this factory last handed out, or null when it never has.</summary>
|
||||
@@ -337,7 +345,10 @@ internal sealed class FakeSftpSession(SshConnectionRequest request) : ISftpSessi
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class FakeSshConnection(SshConnectionRequest request, List<FakeSshShellSession> shells)
|
||||
internal sealed class FakeSshConnection(
|
||||
SshConnectionRequest request,
|
||||
List<FakeSshShellSession> shells,
|
||||
FakeSshConnectionFactory factory)
|
||||
: ISshConnection
|
||||
{
|
||||
/// <inheritdoc />
|
||||
@@ -361,6 +372,20 @@ internal sealed class FakeSshConnection(SshConnectionRequest request, List<FakeS
|
||||
return Task.FromResult<ISshShellSession>(shell);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<ISshPortForward> StartForwardAsync(SshPortForwardSpec spec, CancellationToken cancellationToken)
|
||||
{
|
||||
if (factory.ForwardFailure?.Invoke(spec) is { } refusal)
|
||||
{
|
||||
return Task.FromException<ISshPortForward>(new SshPortForwardException(refusal));
|
||||
}
|
||||
|
||||
var forward = new FakeSshPortForward(spec);
|
||||
factory.Forwards.Add(forward);
|
||||
|
||||
return Task.FromResult<ISshPortForward>(forward);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
@@ -369,6 +394,43 @@ internal sealed class FakeSshConnection(SshConnectionRequest request, List<FakeS
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>A forward that listens on nothing, and can be made to report a failed connection through it.</summary>
|
||||
internal sealed class FakeSshPortForward(SshPortForwardSpec spec) : ISshPortForward
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<SshPortForwardErrorEventArgs>? ErrorOccurred;
|
||||
|
||||
/// <inheritdoc />
|
||||
public SshPortForwardSpec Spec { get; } = spec;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int BoundPort => Spec.BindPort;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsListening { get; private set; } = true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string? LastError { get; private set; }
|
||||
|
||||
/// <summary>Whether whoever started it has stopped it.</summary>
|
||||
internal bool IsDisposed { get; private set; }
|
||||
|
||||
/// <summary>Plays SSH.NET reporting a connection through the forward that failed.</summary>
|
||||
internal void Fail(string message)
|
||||
{
|
||||
LastError = message;
|
||||
ErrorOccurred?.Invoke(this, new SshPortForwardErrorEventArgs(message));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
IsDisposed = true;
|
||||
IsListening = false;
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>A write stream that hands its contents over when it is closed.</summary>
|
||||
/// <remarks>
|
||||
/// A <see cref="MemoryStream"/> cannot be read after disposal, and disposal is exactly when a write is
|
||||
|
||||
Reference in new issue
Block a user