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
@@ -1,3 +1,6 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using DodoSSH.Client.Ssh;
|
||||
|
||||
@@ -326,6 +329,73 @@ internal sealed partial class TerminalTabViewModel : ObservableObject
|
||||
/// <summary>How many steps there are, for the card's track.</summary>
|
||||
internal int StepCount => Steps.Count;
|
||||
|
||||
/// <summary>
|
||||
/// The host's saved forwards, as they run over this tab's current session.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Replaced whole each time a session opens here — see <see cref="ReplaceForwards"/> — and emptied of
|
||||
/// meaning, though not of rows, when it ends: a row that says it stopped with the session is more use
|
||||
/// than one that silently disappeared.
|
||||
/// </remarks>
|
||||
internal ObservableCollection<PortForwardViewModel> Forwards { get; } = [];
|
||||
|
||||
/// <summary>How many of <see cref="Forwards"/> are listening now.</summary>
|
||||
internal int ListeningForwardCount => Forwards.Count(forward => forward.IsListening);
|
||||
|
||||
/// <summary>
|
||||
/// The status bar's forward count, or null when nothing is listening and there is nothing to say.
|
||||
/// </summary>
|
||||
internal string? ForwardsSummary => ListeningForwardCount switch
|
||||
{
|
||||
0 => null,
|
||||
1 => "⇄ 1 FORWARD",
|
||||
var count => string.Create(CultureInfo.InvariantCulture, $"⇄ {count} FORWARDS"),
|
||||
};
|
||||
|
||||
/// <summary>Puts a new session's forwards on this tab, ending whatever the last session left.</summary>
|
||||
internal void ReplaceForwards(IEnumerable<PortForwardViewModel> forwards)
|
||||
{
|
||||
EndForwards();
|
||||
|
||||
foreach (var forward in Forwards)
|
||||
{
|
||||
forward.PropertyChanged -= OnForwardChanged;
|
||||
}
|
||||
|
||||
Forwards.Clear();
|
||||
|
||||
foreach (var forward in forwards)
|
||||
{
|
||||
forward.PropertyChanged += OnForwardChanged;
|
||||
Forwards.Add(forward);
|
||||
}
|
||||
|
||||
RaiseForwardCount();
|
||||
}
|
||||
|
||||
/// <summary>Tells every forward its session is over; SSH.NET has already stopped the listeners.</summary>
|
||||
private void EndForwards()
|
||||
{
|
||||
foreach (var forward in Forwards)
|
||||
{
|
||||
forward.SessionEnded();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnForwardChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName is nameof(PortForwardViewModel.State))
|
||||
{
|
||||
RaiseForwardCount();
|
||||
}
|
||||
}
|
||||
|
||||
private void RaiseForwardCount()
|
||||
{
|
||||
OnPropertyChanged(nameof(ListeningForwardCount));
|
||||
OnPropertyChanged(nameof(ForwardsSummary));
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="TerminalTabState" />
|
||||
[ObservableProperty]
|
||||
private TerminalTabState state;
|
||||
@@ -603,6 +673,8 @@ internal sealed partial class TerminalTabViewModel : ObservableObject
|
||||
EndDescription = description;
|
||||
IsLive = false;
|
||||
|
||||
EndForwards();
|
||||
|
||||
OnPropertyChanged(nameof(EndedDetail));
|
||||
OnPropertyChanged(nameof(EndedSummary));
|
||||
OnPropertyChanged(nameof(EndDescription));
|
||||
@@ -638,6 +710,9 @@ internal sealed partial class TerminalTabViewModel : ObservableObject
|
||||
EndDescription = null;
|
||||
IsLive = false;
|
||||
|
||||
// The old session is being closed for its pane's sake, and its forwards go with its connection.
|
||||
EndForwards();
|
||||
|
||||
// Cleared with the session they were facts about. Keeping them would put the previous connection's
|
||||
// cipher in the status bar beside a handshake that has not negotiated one yet.
|
||||
StartedAt = null;
|
||||
|
||||
Reference in new issue
Block a user