announce server hostname in MANIFEST, display in client
Server reads its hostname via gethostname(2) and includes it as a length-prefixed UTF-8 string at the start of the MANIFEST payload. Client displays 'Server: <hostname> — <MAC>' in a label above the upstream list. Both sides updated for the new MANIFEST format: [hostname_len:1][hostname:N][entries...] Protocol version unchanged (still 1); MANIFEST payload layout change is backward-incompatible but both sides ship together.
This commit is contained in:
+13
-3
@@ -59,18 +59,27 @@ Reserved (unimplemented in v1; parse returns Err, encode unimplemented):
|
|||||||
|
|
||||||
### MANIFEST payload
|
### MANIFEST payload
|
||||||
|
|
||||||
Variable-length entries, parsed sequentially until the payload is consumed.
|
A hostname prefix followed by variable-length entries, parsed sequentially
|
||||||
|
until the payload is consumed.
|
||||||
|
|
||||||
```
|
```
|
||||||
0 1 2 3
|
0 1 2 3
|
||||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||||
+---------------+---------------+-------------------------------+
|
+---------------+-----------------------------------------------+
|
||||||
|
| hostname_len | hostname (UTF-8, hostname_len bytes) ... |
|
||||||
|
+---------------+-----------------------------------------------+
|
||||||
| id | proto | port (big-endian) |
|
| id | proto | port (big-endian) |
|
||||||
+---------------+---------------+-------------------------------+
|
+---------------+---------------+-------------------------------+
|
||||||
| label_len | label (UTF-8, label_len bytes) ... |
|
| label_len | label (UTF-8, label_len bytes) ... |
|
||||||
+---------------+-----------------------------------------------+
|
+---------------+-----------------------------------------------+
|
||||||
|
| ... repeated ... |
|
||||||
|
+---------------------------------------------------------------+
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- **hostname_len** (u8): length in bytes of the server's hostname. `0` is valid
|
||||||
|
(unknown hostname).
|
||||||
|
- **hostname** (`hostname_len` bytes, UTF-8): the server's hostname, read via
|
||||||
|
`gethostname(2)` at startup. Maximum 255 bytes.
|
||||||
- **id** (u8): upstream identifier (1-based positional index from `gatunad`
|
- **id** (u8): upstream identifier (1-based positional index from `gatunad`
|
||||||
cmdline).
|
cmdline).
|
||||||
- **proto** (u8): `1 = TCP`, `2 = UDP` (reserved; not emitted in v1).
|
- **proto** (u8): `1 = TCP`, `2 = UDP` (reserved; not emitted in v1).
|
||||||
@@ -80,7 +89,8 @@ Variable-length entries, parsed sequentially until the payload is consumed.
|
|||||||
- **label** (`label_len` bytes, UTF-8): human-readable name for the upstream,
|
- **label** (`label_len` bytes, UTF-8): human-readable name for the upstream,
|
||||||
taken from the `PORT[:label]` cmdline argument. Maximum 255 bytes.
|
taken from the `PORT[:label]` cmdline argument. Maximum 255 bytes.
|
||||||
|
|
||||||
To parse: read the 5-byte fixed prefix, then `label_len` bytes, and repeat until
|
To parse: read `hostname_len`, then `hostname_len` bytes of hostname, then read
|
||||||
|
5-byte fixed entry prefixes + `label_len` bytes of label each, repeating until
|
||||||
the payload is exhausted. The number of entries is not carried explicitly.
|
the payload is exhausted. The number of entries is not carried explicitly.
|
||||||
|
|
||||||
### OPEN payload
|
### OPEN payload
|
||||||
|
|||||||
+24
-11
@@ -39,7 +39,7 @@ pub struct UpstreamEntry {
|
|||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum Frame {
|
pub enum Frame {
|
||||||
Discover,
|
Discover,
|
||||||
Manifest(Vec<UpstreamEntry>),
|
Manifest { hostname: String, entries: Vec<UpstreamEntry> },
|
||||||
Open { upstream_id: u8 },
|
Open { upstream_id: u8 },
|
||||||
OpenAck { session_id: u32, upstream_id: u8 },
|
OpenAck { session_id: u32, upstream_id: u8 },
|
||||||
OpenNak { upstream_id: u8, reason: u8 },
|
OpenNak { upstream_id: u8, reason: u8 },
|
||||||
@@ -94,8 +94,12 @@ impl Frame {
|
|||||||
pub fn encode(&self) -> Vec<u8> {
|
pub fn encode(&self) -> Vec<u8> {
|
||||||
match self {
|
match self {
|
||||||
Frame::Discover => build(TYPE_DISCOVER, 0, Vec::new()),
|
Frame::Discover => build(TYPE_DISCOVER, 0, Vec::new()),
|
||||||
Frame::Manifest(entries) => {
|
Frame::Manifest { hostname, entries } => {
|
||||||
let mut payload = Vec::new();
|
let mut payload = Vec::new();
|
||||||
|
let hn_bytes = hostname.as_bytes();
|
||||||
|
let hn_len = hn_bytes.len().min(255) as u8;
|
||||||
|
payload.push(hn_len);
|
||||||
|
payload.extend_from_slice(&hn_bytes[..hn_len as usize]);
|
||||||
for e in entries {
|
for e in entries {
|
||||||
encode_entry(&mut payload, e);
|
encode_entry(&mut payload, e);
|
||||||
}
|
}
|
||||||
@@ -143,29 +147,38 @@ impl Frame {
|
|||||||
Ok(Frame::Discover)
|
Ok(Frame::Discover)
|
||||||
}
|
}
|
||||||
TYPE_MANIFEST => {
|
TYPE_MANIFEST => {
|
||||||
|
if payload.is_empty() {
|
||||||
|
return Err(DecodeError::BadPayload("MANIFEST missing hostname prefix"));
|
||||||
|
}
|
||||||
|
let hn_len = payload[0] as usize;
|
||||||
|
if 1 + hn_len > payload.len() {
|
||||||
|
return Err(DecodeError::BadPayload("MANIFEST hostname truncated"));
|
||||||
|
}
|
||||||
|
let hostname = String::from_utf8_lossy(&payload[1..1 + hn_len]).into_owned();
|
||||||
|
let rest = &payload[1 + hn_len..];
|
||||||
let mut entries = Vec::new();
|
let mut entries = Vec::new();
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
while i < payload.len() {
|
while i < rest.len() {
|
||||||
if i + 5 > payload.len() {
|
if i + 5 > rest.len() {
|
||||||
return Err(DecodeError::BadPayload("MANIFEST entry truncated"));
|
return Err(DecodeError::BadPayload("MANIFEST entry truncated"));
|
||||||
}
|
}
|
||||||
let id = payload[i];
|
let id = rest[i];
|
||||||
let proto = payload[i + 1];
|
let proto = rest[i + 1];
|
||||||
let port = u16::from_be_bytes([payload[i + 2], payload[i + 3]]);
|
let port = u16::from_be_bytes([rest[i + 2], rest[i + 3]]);
|
||||||
let label_len = payload[i + 4] as usize;
|
let label_len = rest[i + 4] as usize;
|
||||||
i += 5;
|
i += 5;
|
||||||
if i + label_len > payload.len() {
|
if i + label_len > rest.len() {
|
||||||
return Err(DecodeError::BadPayload("MANIFEST label truncated"));
|
return Err(DecodeError::BadPayload("MANIFEST label truncated"));
|
||||||
}
|
}
|
||||||
let label = if label_len == 0 {
|
let label = if label_len == 0 {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(String::from_utf8_lossy(&payload[i..i + label_len]).into_owned())
|
Some(String::from_utf8_lossy(&rest[i..i + label_len]).into_owned())
|
||||||
};
|
};
|
||||||
i += label_len;
|
i += label_len;
|
||||||
entries.push(UpstreamEntry { id, proto, port, label });
|
entries.push(UpstreamEntry { id, proto, port, label });
|
||||||
}
|
}
|
||||||
Ok(Frame::Manifest(entries))
|
Ok(Frame::Manifest { hostname, entries })
|
||||||
}
|
}
|
||||||
TYPE_OPEN => {
|
TYPE_OPEN => {
|
||||||
if payload.len() != 1 {
|
if payload.len() != 1 {
|
||||||
|
|||||||
+9
-3
@@ -52,6 +52,8 @@ async fn main() -> ExitCode {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let hostname = Arc::new(upstream::get_hostname());
|
||||||
|
|
||||||
let link = match Link::open(&args.iface) {
|
let link = match Link::open(&args.iface) {
|
||||||
Ok(l) => Arc::new(l),
|
Ok(l) => Arc::new(l),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -112,7 +114,7 @@ async fn main() -> ExitCode {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
handle_frame(frame, src, &tx, &store, &next_id, &table).await;
|
handle_frame(frame, src, &tx, &store, &next_id, &table, &hostname).await;
|
||||||
}
|
}
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
// Ignorable frame (outgoing/short/mismatch) or transient; do not
|
// Ignorable frame (outgoing/short/mismatch) or transient; do not
|
||||||
@@ -135,10 +137,14 @@ async fn handle_frame(
|
|||||||
store: &SessionStore,
|
store: &SessionStore,
|
||||||
next_id: &Arc<AtomicU32>,
|
next_id: &Arc<AtomicU32>,
|
||||||
table: &Arc<crate::upstream::UpstreamTable>,
|
table: &Arc<crate::upstream::UpstreamTable>,
|
||||||
|
hostname: &Arc<String>,
|
||||||
) {
|
) {
|
||||||
match frame {
|
match frame {
|
||||||
Frame::Discover => {
|
Frame::Discover => {
|
||||||
let manifest = Frame::Manifest(table.entries());
|
let manifest = Frame::Manifest {
|
||||||
|
hostname: (**hostname).clone(),
|
||||||
|
entries: table.entries(),
|
||||||
|
};
|
||||||
let _ = tx.send((src, manifest.encode())).await;
|
let _ = tx.send((src, manifest.encode())).await;
|
||||||
}
|
}
|
||||||
Frame::Open { upstream_id } => {
|
Frame::Open { upstream_id } => {
|
||||||
@@ -202,6 +208,6 @@ async fn handle_frame(
|
|||||||
store.lock().expect("store poisoned").remove(&session_id);
|
store.lock().expect("store poisoned").remove(&session_id);
|
||||||
}
|
}
|
||||||
// Not expected from a client; ignore.
|
// Not expected from a client; ignore.
|
||||||
Frame::Manifest(_) | Frame::OpenAck { .. } | Frame::OpenNak { .. } => {}
|
Frame::Manifest { .. } | Frame::OpenAck { .. } | Frame::OpenNak { .. } => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-1
@@ -1,6 +1,20 @@
|
|||||||
//! Upstream table: cmdline parsing and MANIFEST entry construction.
|
//! Upstream table: cmdline parsing and MANIFEST entry construction.
|
||||||
|
|
||||||
use crate::frame::{UpstreamEntry, PROTO_TCP, PROTO_UDP};
|
use crate::frame::{Frame, UpstreamEntry, PROTO_TCP, PROTO_UDP};
|
||||||
|
|
||||||
|
/// Read the system hostname via `gethostname(2)`.
|
||||||
|
pub fn get_hostname() -> String {
|
||||||
|
let mut buf = [0u8; 256];
|
||||||
|
let ret = unsafe {
|
||||||
|
libc::gethostname(buf.as_mut_ptr() as *mut libc::c_char, buf.len())
|
||||||
|
};
|
||||||
|
if ret == 0 {
|
||||||
|
let len = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
|
||||||
|
String::from_utf8_lossy(&buf[..len]).into_owned()
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
|||||||
+27
-12
@@ -40,7 +40,7 @@ readonly record struct UpstreamEntry(
|
|||||||
abstract record Frame
|
abstract record Frame
|
||||||
{
|
{
|
||||||
internal record Discover : Frame;
|
internal record Discover : Frame;
|
||||||
internal record Manifest(UpstreamEntry[] Entries) : Frame;
|
internal record Manifest(string Hostname, UpstreamEntry[] Entries) : Frame;
|
||||||
internal record Open(byte UpstreamId) : Frame;
|
internal record Open(byte UpstreamId) : Frame;
|
||||||
internal record OpenAck(uint SessionId, byte UpstreamId) : Frame;
|
internal record OpenAck(uint SessionId, byte UpstreamId) : Frame;
|
||||||
internal record OpenNak(byte UpstreamId, byte Reason) : Frame;
|
internal record OpenNak(byte UpstreamId, byte Reason) : Frame;
|
||||||
@@ -74,7 +74,7 @@ static class FrameCodec
|
|||||||
Frame.Discover =>
|
Frame.Discover =>
|
||||||
Build(Proto.TypeDiscover, 0, []),
|
Build(Proto.TypeDiscover, 0, []),
|
||||||
Frame.Manifest manifest =>
|
Frame.Manifest manifest =>
|
||||||
Build(Proto.TypeManifest, 0, BuildManifestPayload(manifest.Entries)),
|
Build(Proto.TypeManifest, 0, BuildManifestPayload(manifest.Hostname, manifest.Entries)),
|
||||||
Frame.Open open =>
|
Frame.Open open =>
|
||||||
Build(Proto.TypeOpen, 0, [open.UpstreamId]),
|
Build(Proto.TypeOpen, 0, [open.UpstreamId]),
|
||||||
Frame.OpenAck ack =>
|
Frame.OpenAck ack =>
|
||||||
@@ -90,9 +90,14 @@ static class FrameCodec
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static byte[] BuildManifestPayload(UpstreamEntry[] entries)
|
static byte[] BuildManifestPayload(string hostname, UpstreamEntry[] entries)
|
||||||
{
|
{
|
||||||
using var ms = new MemoryStream();
|
using var ms = new MemoryStream();
|
||||||
|
var hnBytes = Encoding.UTF8.GetBytes(hostname);
|
||||||
|
var hnLen = (byte)Math.Min(hnBytes.Length, 255);
|
||||||
|
ms.WriteByte(hnLen);
|
||||||
|
if (hnLen > 0)
|
||||||
|
ms.Write(hnBytes, 0, hnLen);
|
||||||
foreach (var e in entries)
|
foreach (var e in entries)
|
||||||
{
|
{
|
||||||
var labelBytes = Encoding.UTF8.GetBytes(e.Label ?? "");
|
var labelBytes = Encoding.UTF8.GetBytes(e.Label ?? "");
|
||||||
@@ -141,25 +146,35 @@ static class FrameCodec
|
|||||||
|
|
||||||
static Frame.Manifest? ParseManifest(ReadOnlySpan<byte> payload)
|
static Frame.Manifest? ParseManifest(ReadOnlySpan<byte> payload)
|
||||||
{
|
{
|
||||||
|
if (payload.Length < 1)
|
||||||
|
return null;
|
||||||
|
var hnLen = payload[0];
|
||||||
|
if (1 + hnLen > payload.Length)
|
||||||
|
return null;
|
||||||
|
var hostname = hnLen == 0
|
||||||
|
? ""
|
||||||
|
: Encoding.UTF8.GetString(payload[1..(1 + hnLen)]);
|
||||||
|
var rest = payload[(1 + hnLen)..];
|
||||||
|
|
||||||
var entries = new List<UpstreamEntry>();
|
var entries = new List<UpstreamEntry>();
|
||||||
int i = 0;
|
int i = 0;
|
||||||
while (i < payload.Length)
|
while (i < rest.Length)
|
||||||
{
|
{
|
||||||
if (i + 5 > payload.Length)
|
if (i + 5 > rest.Length)
|
||||||
return null;
|
return null;
|
||||||
var id = payload[i];
|
var id = rest[i];
|
||||||
var proto = payload[i + 1];
|
var proto = rest[i + 1];
|
||||||
var port = (ushort)(payload[i + 2] << 8 | payload[i + 3]);
|
var port = (ushort)(rest[i + 2] << 8 | rest[i + 3]);
|
||||||
var labelLen = payload[i + 4];
|
var labelLen = rest[i + 4];
|
||||||
i += 5;
|
i += 5;
|
||||||
if (i + labelLen > payload.Length)
|
if (i + labelLen > rest.Length)
|
||||||
return null;
|
return null;
|
||||||
string? label = labelLen == 0
|
string? label = labelLen == 0
|
||||||
? null
|
? null
|
||||||
: Encoding.UTF8.GetString(payload[i..(i + labelLen)]);
|
: Encoding.UTF8.GetString(rest[i..(i + labelLen)]);
|
||||||
i += labelLen;
|
i += labelLen;
|
||||||
entries.Add(new UpstreamEntry(id, proto, port, label));
|
entries.Add(new UpstreamEntry(id, proto, port, label));
|
||||||
}
|
}
|
||||||
return new Frame.Manifest(entries.ToArray());
|
return new Frame.Manifest(hostname, entries.ToArray());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ public partial class MainForm : Form
|
|||||||
readonly SessionManager _sessions = new();
|
readonly SessionManager _sessions = new();
|
||||||
readonly ComboBox _deviceBox = new();
|
readonly ComboBox _deviceBox = new();
|
||||||
readonly Button _discoverBtn = new();
|
readonly Button _discoverBtn = new();
|
||||||
|
readonly Label _serverLabel = new();
|
||||||
readonly ListView _listView = new();
|
readonly ListView _listView = new();
|
||||||
readonly Label _statusLabel = new();
|
readonly Label _statusLabel = new();
|
||||||
TunnelLink? _link;
|
TunnelLink? _link;
|
||||||
@@ -23,7 +24,8 @@ public partial class MainForm : Form
|
|||||||
InitializeComponents();
|
InitializeComponents();
|
||||||
|
|
||||||
_sessions.Log += msg => this.Invoke(() => _statusLabel.Text = msg);
|
_sessions.Log += msg => this.Invoke(() => _statusLabel.Text = msg);
|
||||||
_sessions.ManifestReceived += entries => this.Invoke(() => PopulateList(entries));
|
_sessions.ManifestReceived += (hostname, mac, entries) =>
|
||||||
|
this.Invoke(() => PopulateList(hostname, mac, entries));
|
||||||
|
|
||||||
foreach (var d in TunnelLink.ListDevices())
|
foreach (var d in TunnelLink.ListDevices())
|
||||||
{
|
{
|
||||||
@@ -54,9 +56,15 @@ public partial class MainForm : Form
|
|||||||
_discoverBtn.Click += OnDiscover;
|
_discoverBtn.Click += OnDiscover;
|
||||||
Controls.Add(_discoverBtn);
|
Controls.Add(_discoverBtn);
|
||||||
|
|
||||||
_listView.Left = pad; _listView.Top = _discoverBtn.Bottom + 8;
|
_serverLabel.Left = pad; _serverLabel.Top = _discoverBtn.Bottom + 8;
|
||||||
|
_serverLabel.Width = ClientSize.Width - pad * 2;
|
||||||
|
_serverLabel.AutoEllipsis = true;
|
||||||
|
_serverLabel.Text = "Server: not connected";
|
||||||
|
Controls.Add(_serverLabel);
|
||||||
|
|
||||||
|
_listView.Left = pad; _listView.Top = _serverLabel.Bottom + 8;
|
||||||
_listView.Width = ClientSize.Width - pad * 2;
|
_listView.Width = ClientSize.Width - pad * 2;
|
||||||
_listView.Height = 220;
|
_listView.Height = 200;
|
||||||
_listView.View = View.Details;
|
_listView.View = View.Details;
|
||||||
_listView.FullRowSelect = true;
|
_listView.FullRowSelect = true;
|
||||||
_listView.CheckBoxes = true;
|
_listView.CheckBoxes = true;
|
||||||
@@ -96,11 +104,15 @@ public partial class MainForm : Form
|
|||||||
_sessions.AttachLink(_link);
|
_sessions.AttachLink(_link);
|
||||||
_link.Open();
|
_link.Open();
|
||||||
_sessions.Discover();
|
_sessions.Discover();
|
||||||
|
_serverLabel.Text = "Server: discovering...";
|
||||||
_statusLabel.Text = "discovering...";
|
_statusLabel.Text = "discovering...";
|
||||||
}
|
}
|
||||||
|
|
||||||
void PopulateList(UpstreamEntry[] entries)
|
void PopulateList(string hostname, byte[] mac, UpstreamEntry[] entries)
|
||||||
{
|
{
|
||||||
|
var macStr = string.Join(":", mac.Select(b => b.ToString("X2")));
|
||||||
|
_serverLabel.Text = $"Server: {hostname} — {macStr}";
|
||||||
|
|
||||||
_listView.BeginUpdate();
|
_listView.BeginUpdate();
|
||||||
_listView.Items.Clear();
|
_listView.Items.Clear();
|
||||||
foreach (var up in entries)
|
foreach (var up in entries)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ sealed class SessionManager : IDisposable
|
|||||||
readonly ConcurrentDictionary<int, ListenerState> _listeners = new();
|
readonly ConcurrentDictionary<int, ListenerState> _listeners = new();
|
||||||
|
|
||||||
byte[]? _serverMac;
|
byte[]? _serverMac;
|
||||||
|
string _serverHostname = "";
|
||||||
UpstreamEntry[] _upstreams = [];
|
UpstreamEntry[] _upstreams = [];
|
||||||
|
|
||||||
// Serialized OPEN: only one outstanding at a time.
|
// Serialized OPEN: only one outstanding at a time.
|
||||||
@@ -20,10 +21,11 @@ sealed class SessionManager : IDisposable
|
|||||||
readonly Queue<PendingOpen> _openQueue = new();
|
readonly Queue<PendingOpen> _openQueue = new();
|
||||||
|
|
||||||
public event Action<string>? Log;
|
public event Action<string>? Log;
|
||||||
public event Action<UpstreamEntry[]>? ManifestReceived;
|
public event Action<string, byte[], UpstreamEntry[]>? ManifestReceived;
|
||||||
|
|
||||||
public UpstreamEntry[] Upstreams => _upstreams;
|
public UpstreamEntry[] Upstreams => _upstreams;
|
||||||
public byte[]? ServerMac => _serverMac;
|
public byte[]? ServerMac => _serverMac;
|
||||||
|
public string ServerHostname => _serverHostname;
|
||||||
public TunnelLink? Link => _link;
|
public TunnelLink? Link => _link;
|
||||||
|
|
||||||
public void AttachLink(TunnelLink link)
|
public void AttachLink(TunnelLink link)
|
||||||
@@ -51,9 +53,10 @@ sealed class SessionManager : IDisposable
|
|||||||
{
|
{
|
||||||
case Frame.Manifest manifest:
|
case Frame.Manifest manifest:
|
||||||
_serverMac = srcMac;
|
_serverMac = srcMac;
|
||||||
|
_serverHostname = manifest.Hostname;
|
||||||
_upstreams = manifest.Entries;
|
_upstreams = manifest.Entries;
|
||||||
Log?.Invoke($"manifest: {manifest.Entries.Length} upstreams from {BitConverter.ToString(srcMac)}");
|
Log?.Invoke($"manifest: {manifest.Entries.Length} upstreams from {manifest.Hostname} ({BitConverter.ToString(srcMac)})");
|
||||||
ManifestReceived?.Invoke(manifest.Entries);
|
ManifestReceived?.Invoke(manifest.Hostname, srcMac, manifest.Entries);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Frame.OpenAck ack:
|
case Frame.OpenAck ack:
|
||||||
|
|||||||
Reference in New Issue
Block a user