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
@@ -0,0 +1,140 @@
|
||||
using System.Collections;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace DodoSSH.Client.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// The port forwards saved on a host, in the order the user added them.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// A dedicated type rather than a list, for the reason <see cref="PinnedPathList"/> gives: a plain list on a
|
||||
/// record compares by reference, so every host would read as changed on every sync pass.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Ordered and unique by id.</b> Order is how the session sidebar lists them. Uniqueness is by
|
||||
/// <see cref="PortForwardRule.Id"/> rather than by value, keeping the first occurrence, because the id is
|
||||
/// what the merge keys on — two entries with one id would be one forward the merge could not tell apart.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Invalid rules are not rejected at construction, for the reason every collection in this assembly gives:
|
||||
/// they arrive from another client's payload, and a throw here would fail the whole sync pass.
|
||||
/// <see cref="HostSecret.TryValidate"/> catches them.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class PortForwardList : IReadOnlyList<PortForwardRule>, IEquatable<PortForwardList>
|
||||
{
|
||||
private readonly PortForwardRule[] rules;
|
||||
private readonly int hash;
|
||||
|
||||
private PortForwardList(PortForwardRule[] rules)
|
||||
{
|
||||
this.rules = rules;
|
||||
hash = ComputeHash(rules);
|
||||
}
|
||||
|
||||
/// <summary>No forwards.</summary>
|
||||
public static PortForwardList Empty { get; } = new([]);
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Count => rules.Length;
|
||||
|
||||
/// <inheritdoc />
|
||||
public PortForwardRule this[int index] => rules[index];
|
||||
|
||||
/// <summary>Copies a sequence of rules, keeping order and the first rule with any given id.</summary>
|
||||
public static PortForwardList Create(IEnumerable<PortForwardRule> rules)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(rules);
|
||||
|
||||
var kept = new List<PortForwardRule>();
|
||||
var seen = new HashSet<Guid>();
|
||||
|
||||
foreach (var rule in rules)
|
||||
{
|
||||
if (rule is null)
|
||||
{
|
||||
throw new ArgumentException("A forward list cannot contain a null rule.", nameof(rules));
|
||||
}
|
||||
|
||||
if (seen.Add(rule.Id))
|
||||
{
|
||||
kept.Add(rule);
|
||||
}
|
||||
}
|
||||
|
||||
return kept.Count == 0 ? Empty : new PortForwardList([.. kept]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The list as a map from id to rule, in list order, which is the shape <see cref="ThreeWayMerge.Map"/>
|
||||
/// takes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Built in list order for the reason <see cref="PinnedPathList.ToPathMap"/> is: the merge unions keys
|
||||
/// ancestor first, then local, then remote, so an ordered map is what keeps "existing forwards, then
|
||||
/// additions" without the merge ordering anything itself. Unlike a pinned path, a value here can change
|
||||
/// under its key, so a conflict is reachable: both machines editing the same forward differently.
|
||||
/// </remarks>
|
||||
public IReadOnlyDictionary<Guid, PortForwardRule> ToIdMap()
|
||||
{
|
||||
var map = new Dictionary<Guid, PortForwardRule>(rules.Length);
|
||||
|
||||
foreach (var rule in rules)
|
||||
{
|
||||
map[rule.Id] = rule;
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Equals(PortForwardList? other)
|
||||
{
|
||||
if (ReferenceEquals(this, other))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return other is not null
|
||||
&& other.hash == hash
|
||||
&& rules.AsSpan().SequenceEqual(other.rules);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Equals(object? obj) => Equals(obj as PortForwardList);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int GetHashCode() => hash;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerator<PortForwardRule> GetEnumerator() => ((IEnumerable<PortForwardRule>)rules).GetEnumerator();
|
||||
|
||||
/// <inheritdoc />
|
||||
IEnumerator IEnumerable.GetEnumerator() => rules.GetEnumerator();
|
||||
|
||||
/// <summary>Contents equality, tolerating nulls on either side.</summary>
|
||||
[SuppressMessage(
|
||||
"Usage",
|
||||
"CA2225:Operator overloads have named alternates",
|
||||
Justification = "Equals(PortForwardList) is the named alternate.")]
|
||||
public static bool operator ==(PortForwardList? left, PortForwardList? right) =>
|
||||
left is null ? right is null : left.Equals(right);
|
||||
|
||||
/// <summary>Contents inequality.</summary>
|
||||
public static bool operator !=(PortForwardList? left, PortForwardList? right) => !(left == right);
|
||||
|
||||
private static int ComputeHash(PortForwardRule[] rules)
|
||||
{
|
||||
// Order-sensitive, because order is kept.
|
||||
var accumulator = new HashCode();
|
||||
accumulator.Add(rules.Length);
|
||||
|
||||
foreach (var rule in rules)
|
||||
{
|
||||
accumulator.Add(rule);
|
||||
}
|
||||
|
||||
return accumulator.ToHashCode();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user