Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d730028af9 | |||
| f09028b135 |
+11
-5
@@ -11,7 +11,9 @@ discriminator; there is no magic number inside the payload.
|
||||
+---------------+---------------+-------------------------------+
|
||||
| version | type | session_id |
|
||||
+---------------+---------------+-------------------------------+
|
||||
| payload ... |
|
||||
| payload_len (big-endian) | payload ... |
|
||||
+-------------------------------+ +
|
||||
| |
|
||||
+---------------------------------------------------------------+
|
||||
```
|
||||
|
||||
@@ -19,15 +21,19 @@ discriminator; there is no magic number inside the payload.
|
||||
- **type** (u8): frame type, see table below.
|
||||
- **session_id** (u32, big-endian): `0` for non-session frames; the
|
||||
server-assigned ID for session-scoped frames.
|
||||
- **payload** (variable): type-dependent. No length field is carried — the
|
||||
Ethernet frame length from the capture gives the payload extent.
|
||||
- **payload_len** (u16, big-endian): number of payload bytes that follow.
|
||||
The receiver reads exactly this many bytes and ignores any trailing
|
||||
Ethernet padding (frames under 60 bytes are zero-padded by the NIC to
|
||||
meet the minimum Ethernet frame size).
|
||||
- **payload** (`payload_len` bytes): type-dependent.
|
||||
|
||||
No CRC, no retransmit, no ordering at this layer. TCP reliability is handled by
|
||||
the endpoints' TCP stacks; UDP (reserved) will rely on application-level
|
||||
mechanisms.
|
||||
|
||||
Maximum payload: 1500 (Ethernet MTU) − 14 (eth header) − 6 (our header) =
|
||||
**1480 bytes**. Larger payloads are not emitted in v1.
|
||||
Maximum payload: 1500 (Ethernet MTU) − 8 (our header) = **1492 bytes**. In
|
||||
practice we cap at **1480** to stay conservative. Larger payloads are not
|
||||
emitted in v1.
|
||||
|
||||
## Frame types
|
||||
|
||||
|
||||
+46
-40
@@ -3,6 +3,7 @@
|
||||
pub const ETHERTYPE: u16 = 0x6969;
|
||||
pub const ETH_HEADER_LEN: usize = 14;
|
||||
pub const VERSION: u8 = 1;
|
||||
pub const HEADER_LEN: usize = 8;
|
||||
pub const MAX_PAYLOAD: usize = 1480;
|
||||
|
||||
pub const TYPE_DISCOVER: u8 = 0x01;
|
||||
@@ -76,50 +77,50 @@ fn encode_entry(buf: &mut Vec<u8>, e: &UpstreamEntry) {
|
||||
buf.extend_from_slice(&label_bytes[..label_len as usize]);
|
||||
}
|
||||
|
||||
impl Frame {
|
||||
pub fn encode(&self) -> Vec<u8> {
|
||||
let mut buf = Vec::new();
|
||||
match self {
|
||||
Frame::Discover => {
|
||||
buf.extend_from_slice(&[VERSION, TYPE_DISCOVER, 0, 0, 0, 0]);
|
||||
}
|
||||
Frame::Manifest(entries) => {
|
||||
buf.extend_from_slice(&[VERSION, TYPE_MANIFEST, 0, 0, 0, 0]);
|
||||
for e in entries {
|
||||
encode_entry(&mut buf, e);
|
||||
}
|
||||
}
|
||||
Frame::Open { upstream_id } => {
|
||||
buf.extend_from_slice(&[VERSION, TYPE_OPEN, 0, 0, 0, 0, *upstream_id]);
|
||||
}
|
||||
Frame::OpenAck { session_id, upstream_id } => {
|
||||
buf.extend_from_slice(&[VERSION, TYPE_OPEN_ACK]);
|
||||
/// Build the 8-byte header + payload. The payload_len field records the
|
||||
/// exact payload length so the receiver can ignore Ethernet padding.
|
||||
fn build(type_byte: u8, session_id: u32, payload: Vec<u8>) -> Vec<u8> {
|
||||
let len = payload.len() as u16;
|
||||
let mut buf = Vec::with_capacity(HEADER_LEN + payload.len());
|
||||
buf.push(VERSION);
|
||||
buf.push(type_byte);
|
||||
buf.extend_from_slice(&session_id.to_be_bytes());
|
||||
buf.push(*upstream_id);
|
||||
}
|
||||
Frame::OpenNak { upstream_id, reason } => {
|
||||
buf.extend_from_slice(&[VERSION, TYPE_OPEN_NAK, 0, 0, 0, 0]);
|
||||
buf.push(*upstream_id);
|
||||
buf.push(*reason);
|
||||
}
|
||||
Frame::Data { session_id, payload } => {
|
||||
buf.extend_from_slice(&[VERSION, TYPE_DATA]);
|
||||
buf.extend_from_slice(&session_id.to_be_bytes());
|
||||
buf.extend_from_slice(payload);
|
||||
}
|
||||
Frame::Close { session_id, reason } => {
|
||||
buf.extend_from_slice(&[VERSION, TYPE_CLOSE]);
|
||||
buf.extend_from_slice(&session_id.to_be_bytes());
|
||||
if let Some(r) = reason {
|
||||
buf.push(*r);
|
||||
}
|
||||
}
|
||||
}
|
||||
buf.extend_from_slice(&len.to_be_bytes());
|
||||
buf.extend_from_slice(&payload);
|
||||
buf
|
||||
}
|
||||
|
||||
impl Frame {
|
||||
pub fn encode(&self) -> Vec<u8> {
|
||||
match self {
|
||||
Frame::Discover => build(TYPE_DISCOVER, 0, Vec::new()),
|
||||
Frame::Manifest(entries) => {
|
||||
let mut payload = Vec::new();
|
||||
for e in entries {
|
||||
encode_entry(&mut payload, e);
|
||||
}
|
||||
build(TYPE_MANIFEST, 0, payload)
|
||||
}
|
||||
Frame::Open { upstream_id } => build(TYPE_OPEN, 0, vec![*upstream_id]),
|
||||
Frame::OpenAck { session_id, upstream_id } => {
|
||||
build(TYPE_OPEN_ACK, *session_id, vec![*upstream_id])
|
||||
}
|
||||
Frame::OpenNak { upstream_id, reason } => {
|
||||
build(TYPE_OPEN_NAK, 0, vec![*upstream_id, *reason])
|
||||
}
|
||||
Frame::Data { session_id, payload } => build(TYPE_DATA, *session_id, payload.clone()),
|
||||
Frame::Close { session_id, reason } => {
|
||||
let p = match reason {
|
||||
Some(r) => vec![*r],
|
||||
None => Vec::new(),
|
||||
};
|
||||
build(TYPE_CLOSE, *session_id, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(buf: &[u8]) -> Result<Frame, DecodeError> {
|
||||
if buf.len() < 6 {
|
||||
if buf.len() < HEADER_LEN {
|
||||
return Err(DecodeError::Short);
|
||||
}
|
||||
let version = buf[0];
|
||||
@@ -128,7 +129,12 @@ impl Frame {
|
||||
}
|
||||
let typ = buf[1];
|
||||
let session_id = u32::from_be_bytes([buf[2], buf[3], buf[4], buf[5]]);
|
||||
let payload = &buf[6..];
|
||||
let payload_len = u16::from_be_bytes([buf[6], buf[7]]) as usize;
|
||||
if buf.len() < HEADER_LEN + payload_len {
|
||||
return Err(DecodeError::BadPayload("payload_len exceeds available data"));
|
||||
}
|
||||
// Slice exactly payload_len bytes, ignoring any trailing Ethernet padding.
|
||||
let payload = &buf[HEADER_LEN..HEADER_LEN + payload_len];
|
||||
match typ {
|
||||
TYPE_DISCOVER => {
|
||||
if !payload.is_empty() {
|
||||
|
||||
+33
-24
@@ -7,6 +7,7 @@ static class Proto
|
||||
public const ushort EtherType = 0x6969;
|
||||
public const int EthHeaderLen = 14;
|
||||
public const byte Version = 1;
|
||||
public const int HeaderLen = 8;
|
||||
public const int MaxPayload = 1480;
|
||||
|
||||
public const byte TypeDiscover = 0x01;
|
||||
@@ -49,34 +50,49 @@ abstract record Frame
|
||||
|
||||
static class FrameCodec
|
||||
{
|
||||
/// Build the 8-byte header + payload. payload_len records the exact
|
||||
/// payload length so the receiver can ignore Ethernet padding.
|
||||
static byte[] Build(byte type, uint sessionId, byte[] payload)
|
||||
{
|
||||
var buf = new byte[Proto.HeaderLen + payload.Length];
|
||||
buf[0] = Proto.Version;
|
||||
buf[1] = type;
|
||||
buf[2] = (byte)(sessionId >> 24);
|
||||
buf[3] = (byte)(sessionId >> 16);
|
||||
buf[4] = (byte)(sessionId >> 8);
|
||||
buf[5] = (byte)(sessionId & 0xFF);
|
||||
buf[6] = (byte)(payload.Length >> 8);
|
||||
buf[7] = (byte)(payload.Length & 0xFF);
|
||||
Buffer.BlockCopy(payload, 0, buf, Proto.HeaderLen, payload.Length);
|
||||
return buf;
|
||||
}
|
||||
|
||||
public static byte[] Encode(Frame frame)
|
||||
{
|
||||
return frame switch
|
||||
{
|
||||
Frame.Discover =>
|
||||
Header(Proto.TypeDiscover, 0),
|
||||
Build(Proto.TypeDiscover, 0, []),
|
||||
Frame.Manifest manifest =>
|
||||
BuildManifest(manifest.Entries),
|
||||
Build(Proto.TypeManifest, 0, BuildManifestPayload(manifest.Entries)),
|
||||
Frame.Open open =>
|
||||
[.. Header(Proto.TypeOpen, 0), open.UpstreamId],
|
||||
Build(Proto.TypeOpen, 0, [open.UpstreamId]),
|
||||
Frame.OpenAck ack =>
|
||||
[.. Header(Proto.TypeOpenAck, ack.SessionId), ack.UpstreamId],
|
||||
Build(Proto.TypeOpenAck, ack.SessionId, [ack.UpstreamId]),
|
||||
Frame.OpenNak nak =>
|
||||
[.. Header(Proto.TypeOpenNak, 0), nak.UpstreamId, nak.Reason],
|
||||
Build(Proto.TypeOpenNak, 0, [nak.UpstreamId, nak.Reason]),
|
||||
Frame.Data data =>
|
||||
[.. Header(Proto.TypeData, data.SessionId), .. data.Payload],
|
||||
Build(Proto.TypeData, data.SessionId, data.Payload),
|
||||
Frame.Close close =>
|
||||
close.Reason.HasValue
|
||||
? [.. Header(Proto.TypeClose, close.SessionId), close.Reason.Value]
|
||||
: Header(Proto.TypeClose, close.SessionId),
|
||||
Build(Proto.TypeClose, close.SessionId,
|
||||
close.Reason.HasValue ? [close.Reason.Value] : []),
|
||||
_ => throw new InvalidOperationException($"unknown frame type: {frame.GetType()}"),
|
||||
};
|
||||
}
|
||||
|
||||
static byte[] BuildManifest(UpstreamEntry[] entries)
|
||||
static byte[] BuildManifestPayload(UpstreamEntry[] entries)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
ms.Write(Header(Proto.TypeManifest, 0));
|
||||
foreach (var e in entries)
|
||||
{
|
||||
var labelBytes = Encoding.UTF8.GetBytes(e.Label ?? "");
|
||||
@@ -92,26 +108,19 @@ static class FrameCodec
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
static byte[] Header(byte type, uint sessionId)
|
||||
{
|
||||
return [
|
||||
Proto.Version, type,
|
||||
(byte)(sessionId >> 24),
|
||||
(byte)(sessionId >> 16),
|
||||
(byte)(sessionId >> 8),
|
||||
(byte)(sessionId & 0xFF),
|
||||
];
|
||||
}
|
||||
|
||||
public static Frame? Parse(ReadOnlySpan<byte> buf)
|
||||
{
|
||||
if (buf.Length < 6)
|
||||
if (buf.Length < Proto.HeaderLen)
|
||||
return null;
|
||||
if (buf[0] != Proto.Version)
|
||||
return null;
|
||||
var type = buf[1];
|
||||
var sessionId = (uint)(buf[2] << 24 | buf[3] << 16 | buf[4] << 8 | buf[5]);
|
||||
var payload = buf[6..];
|
||||
var payloadLen = (ushort)(buf[6] << 8 | buf[7]);
|
||||
if (buf.Length < Proto.HeaderLen + payloadLen)
|
||||
return null;
|
||||
// Slice exactly payloadLen bytes, ignoring any trailing Ethernet padding.
|
||||
var payload = buf.Slice(Proto.HeaderLen, payloadLen);
|
||||
|
||||
return type switch
|
||||
{
|
||||
|
||||
@@ -26,26 +26,37 @@ public partial class MainForm : Form
|
||||
_sessions.ManifestReceived += entries => this.Invoke(() => PopulateList(entries));
|
||||
|
||||
foreach (var d in TunnelLink.ListDevices())
|
||||
_deviceBox.Items.Add($"{d.Name} — {d.Interface?.FriendlyName ?? d.Interface?.Description}");
|
||||
{
|
||||
var friendly = d.Interface?.FriendlyName ?? "";
|
||||
var desc = d.Interface?.Description ?? "";
|
||||
var mac = d.MacAddress?.GetAddressBytes();
|
||||
var macStr = mac != null
|
||||
? string.Join(":", mac.Select(b => b.ToString("X2")))
|
||||
: "??-??-??-??-??-??";
|
||||
_deviceBox.Items.Add($"{friendly} — {desc} — {macStr}");
|
||||
}
|
||||
if (_deviceBox.Items.Count > 0)
|
||||
_deviceBox.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
void InitializeComponents()
|
||||
{
|
||||
Controls.Add(new Label { Text = "Adapter:", Left = 12, Top = 12, AutoSize = true });
|
||||
const int pad = 12;
|
||||
|
||||
_deviceBox.Left = 70; _deviceBox.Top = 9;
|
||||
_deviceBox.Width = 330; _deviceBox.DropDownStyle = ComboBoxStyle.DropDownList;
|
||||
_deviceBox.Left = pad; _deviceBox.Top = 12;
|
||||
_deviceBox.Width = ClientSize.Width - pad * 2;
|
||||
_deviceBox.DropDownStyle = ComboBoxStyle.DropDownList;
|
||||
Controls.Add(_deviceBox);
|
||||
|
||||
_discoverBtn.Text = "Discover";
|
||||
_discoverBtn.Left = 410; _discoverBtn.Top = 8; _discoverBtn.Width = 80;
|
||||
_discoverBtn.Left = pad; _discoverBtn.Top = _deviceBox.Bottom + 8;
|
||||
_discoverBtn.Width = 80;
|
||||
_discoverBtn.Click += OnDiscover;
|
||||
Controls.Add(_discoverBtn);
|
||||
|
||||
_listView.Left = 12; _listView.Top = 40;
|
||||
_listView.Width = 478; _listView.Height = 250;
|
||||
_listView.Left = pad; _listView.Top = _discoverBtn.Bottom + 8;
|
||||
_listView.Width = ClientSize.Width - pad * 2;
|
||||
_listView.Height = 220;
|
||||
_listView.View = View.Details;
|
||||
_listView.FullRowSelect = true;
|
||||
_listView.CheckBoxes = true;
|
||||
@@ -57,8 +68,9 @@ public partial class MainForm : Form
|
||||
_listView.ItemChecked += OnItemChecked;
|
||||
Controls.Add(_listView);
|
||||
|
||||
_statusLabel.Left = 12; _statusLabel.Top = 304;
|
||||
_statusLabel.Width = 478; _statusLabel.AutoEllipsis = true;
|
||||
_statusLabel.Left = pad; _statusLabel.Top = _listView.Bottom + 8;
|
||||
_statusLabel.Width = ClientSize.Width - pad * 2;
|
||||
_statusLabel.AutoEllipsis = true;
|
||||
Controls.Add(_statusLabel);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user