File-Tunnel/ft/Commands/Command.cs
fiddyschmitt 0b97eed490 Add SOCKS dynamic forwarding (-D and -R), like SSH
Adds ft equivalents of SSH's dynamic forwarding: -D [bind:]port opens a
local SOCKS proxy (the remote side dials each destination), and a bare
-R [bind:]port (no destination) opens one on the remote side (the local
side dials). Supports the same surface modern OpenSSH implements: SOCKS4,
SOCKS4a and SOCKS5; CONNECT only; no authentication; no BIND or UDP
ASSOCIATE.

A new SocksServer performs the per-connection handshake and produces a
tcp://host:port destination that flows through the existing Connect/relay
path unchanged - so hostnames (SOCKS4a/5) and IPv6 resolve on the exit
side, exactly like SSH. -R rides the existing CreateListener command via
a "socks" protocol sentinel and a listen-spec partition in
ProvideRemoteForwardsToCounterpart.

SOCKS replies carry accurate result codes. Since ft had no positive
per-connection dial signal, a new ConnectResult command (id 12) reports
the outcome (SocketError mapped to a SOCKS reply code) from the dialing
side back to the SOCKS host, which blocks until it arrives before
answering its client. The reply is written before any relaying so it is
the first thing the client sees. No existing wire command changed; -L/-U
behaviour is untouched.

Tested hermetically (SOCKS4/4a/5 negotiator byte-level tests, listen-spec
parsing, ConnectResult serialization, and in-process end-to-end -D/-R
transfers incl. far-side resolution and an accurate-failure-reply case)
and verified with a real curl client in both directions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SjajJ7d3BhSZGfsMrGRS5S
2026-07-23 21:00:08 +10:00

85 lines
2.4 KiB
C#

using ft.Streams;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ft.Commands
{
public abstract class Command
{
public Command()
{
}
public abstract byte CommandId { get; }
public ulong PacketNumber { get; set; } = (ulong)Random.Shared.NextInt64();
public uint CRC { get; set; }
protected abstract void Deserialize(BinaryReader reader);
protected abstract void Serialize(BinaryWriter writer);
public void Serialise(BinaryWriter writer)
{
if (writer.BaseStream is not HashingStream hashingStream) return;
hashingStream.StartHashing();
writer.Write(CommandId);
//PacketNumber = SentPacketCount++;
writer.Write(PacketNumber);
Serialize(writer);
hashingStream.StopHashing();
CRC = hashingStream.GetCrc32();
writer.Write(CRC);
//Don't flush here. This way, we can write multiple commands to the file in one go.
//writer.Flush();
}
public static Command? Deserialise(BinaryReader reader)
{
if (reader.BaseStream is not HashingStream hashingStream) return null;
hashingStream.StartHashing();
var commandId = reader.ReadByte();
Command? result = commandId switch
{
Connect.COMMAND_ID => new Connect(),
ConnectResult.COMMAND_ID => new ConnectResult(),
CreateListener.COMMAND_ID => new CreateListener(),
Forward.COMMAND_ID => new Forward(),
Purge.COMMAND_ID => new Purge(),
TearDown.COMMAND_ID => new TearDown(),
Ping.COMMAND_ID => new Ping(),
_ => null
};
if (result != null)
{
result.PacketNumber = reader.ReadUInt64();
result.Deserialize(reader);
hashingStream.StopHashing();
//check the crc
var actualCrc = hashingStream.GetCrc32();
result.CRC = reader.ReadUInt32();
if (actualCrc != result.CRC)
{
throw new InvalidDataException("Command is invalid (CRC mismatch).");
}
}
return result;
}
}
}