Compare commits
6 Commits
d730028af9
...
279af33fd8
| Author | SHA1 | Date | |
|---|---|---|---|
| 279af33fd8 | |||
| a2d3643d69 | |||
| d1e71f0323 | |||
| 54c804f81f | |||
| 3336a08543 | |||
| 27e15452ee |
+27
-3
@@ -46,6 +46,8 @@ emitted in v1.
|
|||||||
| 0x05 | OPEN_NAK | S → C | 0 | `upstream_id:1, reason:1` |
|
| 0x05 | OPEN_NAK | S → C | 0 | `upstream_id:1, reason:1` |
|
||||||
| 0x06 | DATA | both | session | raw bytes (≤1480) |
|
| 0x06 | DATA | both | session | raw bytes (≤1480) |
|
||||||
| 0x07 | CLOSE | both | session | optional `reason:1` |
|
| 0x07 | CLOSE | both | session | optional `reason:1` |
|
||||||
|
| 0x0B | PING | C → S | 0 | `nonce:8` |
|
||||||
|
| 0x0C | PONG | S → C | 0 | `nonce:8` (echoed) |
|
||||||
|
|
||||||
Reserved (unimplemented in v1; parse returns Err, encode unimplemented):
|
Reserved (unimplemented in v1; parse returns Err, encode unimplemented):
|
||||||
|
|
||||||
@@ -59,18 +61,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 +91,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
|
||||||
@@ -130,6 +142,18 @@ field identifies which session the bytes belong to.
|
|||||||
|
|
||||||
- **reason** (u8, optional): present iff payload length ≥ 1. See reason codes.
|
- **reason** (u8, optional): present iff payload length ≥ 1. See reason codes.
|
||||||
|
|
||||||
|
### PING / PONG payload
|
||||||
|
|
||||||
|
```
|
||||||
|
+ +
|
||||||
|
| nonce (big-endian, 8 bytes) |
|
||||||
|
+ +
|
||||||
|
```
|
||||||
|
|
||||||
|
- **nonce** (u64, big-endian): arbitrary value chosen by the client. The
|
||||||
|
server echoes it verbatim in the PONG reply. Used to correlate RTT
|
||||||
|
measurements.
|
||||||
|
|
||||||
## Reason codes
|
## Reason codes
|
||||||
|
|
||||||
| Value | Meaning |
|
| Value | Meaning |
|
||||||
|
|||||||
+44
-11
@@ -16,6 +16,8 @@ pub const TYPE_CLOSE: u8 = 0x07;
|
|||||||
pub const TYPE_UDP_OPEN: u8 = 0x08;
|
pub const TYPE_UDP_OPEN: u8 = 0x08;
|
||||||
pub const TYPE_UDP_DATA: u8 = 0x09;
|
pub const TYPE_UDP_DATA: u8 = 0x09;
|
||||||
pub const TYPE_UDP_CLOSE: u8 = 0x0A;
|
pub const TYPE_UDP_CLOSE: u8 = 0x0A;
|
||||||
|
pub const TYPE_PING: u8 = 0x0B;
|
||||||
|
pub const TYPE_PONG: u8 = 0x0C;
|
||||||
|
|
||||||
pub const PROTO_TCP: u8 = 1;
|
pub const PROTO_TCP: u8 = 1;
|
||||||
pub const PROTO_UDP: u8 = 2;
|
pub const PROTO_UDP: u8 = 2;
|
||||||
@@ -39,12 +41,14 @@ 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 },
|
||||||
Data { session_id: u32, payload: Vec<u8> },
|
Data { session_id: u32, payload: Vec<u8> },
|
||||||
Close { session_id: u32, reason: Option<u8> },
|
Close { session_id: u32, reason: Option<u8> },
|
||||||
|
Ping { nonce: u64 },
|
||||||
|
Pong { nonce: u64 },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -94,8 +98,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);
|
||||||
}
|
}
|
||||||
@@ -116,6 +124,8 @@ impl Frame {
|
|||||||
};
|
};
|
||||||
build(TYPE_CLOSE, *session_id, p)
|
build(TYPE_CLOSE, *session_id, p)
|
||||||
}
|
}
|
||||||
|
Frame::Ping { nonce } => build(TYPE_PING, 0, nonce.to_be_bytes().to_vec()),
|
||||||
|
Frame::Pong { nonce } => build(TYPE_PONG, 0, nonce.to_be_bytes().to_vec()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,29 +153,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 {
|
||||||
@@ -199,6 +218,20 @@ impl Frame {
|
|||||||
};
|
};
|
||||||
Ok(Frame::Close { session_id, reason })
|
Ok(Frame::Close { session_id, reason })
|
||||||
}
|
}
|
||||||
|
TYPE_PING => {
|
||||||
|
if payload.len() != 8 {
|
||||||
|
return Err(DecodeError::BadPayload("PING payload must be 8 bytes"));
|
||||||
|
}
|
||||||
|
let nonce = u64::from_be_bytes(payload.try_into().unwrap());
|
||||||
|
Ok(Frame::Ping { nonce })
|
||||||
|
}
|
||||||
|
TYPE_PONG => {
|
||||||
|
if payload.len() != 8 {
|
||||||
|
return Err(DecodeError::BadPayload("PONG payload must be 8 bytes"));
|
||||||
|
}
|
||||||
|
let nonce = u64::from_be_bytes(payload.try_into().unwrap());
|
||||||
|
Ok(Frame::Pong { nonce })
|
||||||
|
}
|
||||||
TYPE_UDP_OPEN | TYPE_UDP_DATA | TYPE_UDP_CLOSE => {
|
TYPE_UDP_OPEN | TYPE_UDP_DATA | TYPE_UDP_CLOSE => {
|
||||||
Err(DecodeError::BadPayload("UDP frame types not implemented in v1"))
|
Err(DecodeError::BadPayload("UDP frame types not implemented in v1"))
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-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 } => {
|
||||||
@@ -201,7 +207,12 @@ async fn handle_frame(
|
|||||||
Frame::Close { session_id, reason: _ } => {
|
Frame::Close { session_id, reason: _ } => {
|
||||||
store.lock().expect("store poisoned").remove(&session_id);
|
store.lock().expect("store poisoned").remove(&session_id);
|
||||||
}
|
}
|
||||||
|
Frame::Ping { nonce } => {
|
||||||
|
let pong = Frame::Pong { nonce };
|
||||||
|
let _ = tx.send((src, pong.encode())).await;
|
||||||
|
}
|
||||||
// Not expected from a client; ignore.
|
// Not expected from a client; ignore.
|
||||||
Frame::Manifest(_) | Frame::OpenAck { .. } | Frame::OpenNak { .. } => {}
|
Frame::Manifest { .. } | Frame::OpenAck { .. } | Frame::OpenNak { .. }
|
||||||
|
| Frame::Pong { .. } => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+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)]
|
||||||
|
|||||||
+57
-12
@@ -17,6 +17,8 @@ static class Proto
|
|||||||
public const byte TypeOpenNak = 0x05;
|
public const byte TypeOpenNak = 0x05;
|
||||||
public const byte TypeData = 0x06;
|
public const byte TypeData = 0x06;
|
||||||
public const byte TypeClose = 0x07;
|
public const byte TypeClose = 0x07;
|
||||||
|
public const byte TypePing = 0x0B;
|
||||||
|
public const byte TypePong = 0x0C;
|
||||||
|
|
||||||
public const byte ProtoTcp = 1;
|
public const byte ProtoTcp = 1;
|
||||||
public const byte ProtoUdp = 2;
|
public const byte ProtoUdp = 2;
|
||||||
@@ -40,12 +42,14 @@ 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;
|
||||||
internal record Data(uint SessionId, byte[] Payload) : Frame;
|
internal record Data(uint SessionId, byte[] Payload) : Frame;
|
||||||
internal record Close(uint SessionId, byte? Reason) : Frame;
|
internal record Close(uint SessionId, byte? Reason) : Frame;
|
||||||
|
internal record Ping(ulong Nonce) : Frame;
|
||||||
|
internal record Pong(ulong Nonce) : Frame;
|
||||||
}
|
}
|
||||||
|
|
||||||
static class FrameCodec
|
static class FrameCodec
|
||||||
@@ -74,7 +78,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 =>
|
||||||
@@ -86,13 +90,22 @@ static class FrameCodec
|
|||||||
Frame.Close close =>
|
Frame.Close close =>
|
||||||
Build(Proto.TypeClose, close.SessionId,
|
Build(Proto.TypeClose, close.SessionId,
|
||||||
close.Reason.HasValue ? [close.Reason.Value] : []),
|
close.Reason.HasValue ? [close.Reason.Value] : []),
|
||||||
|
Frame.Ping ping =>
|
||||||
|
Build(Proto.TypePing, 0, EncodeNonce(ping.Nonce)),
|
||||||
|
Frame.Pong pong =>
|
||||||
|
Build(Proto.TypePong, 0, EncodeNonce(pong.Nonce)),
|
||||||
_ => throw new InvalidOperationException($"unknown frame type: {frame.GetType()}"),
|
_ => throw new InvalidOperationException($"unknown frame type: {frame.GetType()}"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
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 ?? "");
|
||||||
@@ -133,33 +146,65 @@ static class FrameCodec
|
|||||||
new Frame.Data(sessionId, payload.ToArray()),
|
new Frame.Data(sessionId, payload.ToArray()),
|
||||||
Proto.TypeClose when payload.Length is 0 or 1 =>
|
Proto.TypeClose when payload.Length is 0 or 1 =>
|
||||||
new Frame.Close(sessionId, payload.Length == 1 ? payload[0] : null),
|
new Frame.Close(sessionId, payload.Length == 1 ? payload[0] : null),
|
||||||
|
Proto.TypePong when payload.Length == 8 =>
|
||||||
|
new Frame.Pong(ParseNonce(payload)),
|
||||||
|
Proto.TypePing when payload.Length == 8 =>
|
||||||
|
new Frame.Ping(ParseNonce(payload)),
|
||||||
Proto.TypeDiscover when payload.Length == 0 =>
|
Proto.TypeDiscover when payload.Length == 0 =>
|
||||||
new Frame.Discover(),
|
new Frame.Discover(),
|
||||||
_ => null,
|
_ => null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static ulong ParseNonce(ReadOnlySpan<byte> payload)
|
||||||
|
{
|
||||||
|
ulong nonce = 0;
|
||||||
|
for (int i = 0; i < 8; i++)
|
||||||
|
nonce = (nonce << 8) | payload[i];
|
||||||
|
return nonce;
|
||||||
|
}
|
||||||
|
|
||||||
|
static byte[] EncodeNonce(ulong nonce)
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
(byte)(nonce >> 56), (byte)(nonce >> 48),
|
||||||
|
(byte)(nonce >> 40), (byte)(nonce >> 32),
|
||||||
|
(byte)(nonce >> 24), (byte)(nonce >> 16),
|
||||||
|
(byte)(nonce >> 8), (byte)(nonce & 0xFF),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
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,23 +7,26 @@ 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 Button _testBtn = new();
|
||||||
|
readonly Label _serverLabel = new();
|
||||||
|
readonly Label _pingStatsLabel = new();
|
||||||
readonly ListView _listView = new();
|
readonly ListView _listView = new();
|
||||||
readonly Label _statusLabel = new();
|
readonly Label _statusLabel = new();
|
||||||
TunnelLink? _link;
|
TunnelLink? _link;
|
||||||
|
PingTest? _pingTest;
|
||||||
|
|
||||||
public MainForm()
|
public MainForm()
|
||||||
{
|
{
|
||||||
Text = "gatuna";
|
Text = "gatuna";
|
||||||
Width = 520;
|
Width = 520;
|
||||||
Height = 380;
|
Height = 420;
|
||||||
FormBorderStyle = FormBorderStyle.FixedSingle;
|
|
||||||
MaximizeBox = false;
|
|
||||||
MinimizeBox = false;
|
|
||||||
StartPosition = FormStartPosition.CenterScreen;
|
StartPosition = FormStartPosition.CenterScreen;
|
||||||
|
Icon = SystemIcons.GetStockIcon(StockIconId.NetworkConnect, 32);
|
||||||
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 +57,27 @@ 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;
|
_testBtn.Text = "Test";
|
||||||
|
_testBtn.Left = _discoverBtn.Right + 8; _testBtn.Top = _discoverBtn.Top;
|
||||||
|
_testBtn.Width = 60;
|
||||||
|
_testBtn.Click += OnTest;
|
||||||
|
Controls.Add(_testBtn);
|
||||||
|
|
||||||
|
_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);
|
||||||
|
|
||||||
|
_pingStatsLabel.Left = pad; _pingStatsLabel.Top = _serverLabel.Bottom + 4;
|
||||||
|
_pingStatsLabel.Width = ClientSize.Width - pad * 2;
|
||||||
|
_pingStatsLabel.AutoEllipsis = true;
|
||||||
|
_pingStatsLabel.Text = "";
|
||||||
|
Controls.Add(_pingStatsLabel);
|
||||||
|
|
||||||
|
_listView.Left = pad; _listView.Top = _pingStatsLabel.Bottom + 8;
|
||||||
_listView.Width = ClientSize.Width - pad * 2;
|
_listView.Width = ClientSize.Width - pad * 2;
|
||||||
_listView.Height = 220;
|
_listView.Height = 180;
|
||||||
_listView.View = View.Details;
|
_listView.View = View.Details;
|
||||||
_listView.FullRowSelect = true;
|
_listView.FullRowSelect = true;
|
||||||
_listView.CheckBoxes = true;
|
_listView.CheckBoxes = true;
|
||||||
@@ -96,11 +117,43 @@ 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 OnTest(object? s, EventArgs e)
|
||||||
{
|
{
|
||||||
|
if (_pingTest != null && _pingTest.Running)
|
||||||
|
{
|
||||||
|
_sessions.StopPing();
|
||||||
|
_pingTest = null;
|
||||||
|
_testBtn.Text = "Test";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_pingTest = _sessions.StartPing();
|
||||||
|
if (_pingTest == null)
|
||||||
|
{
|
||||||
|
MessageBox.Show("Discover a server first.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_pingTest.StatsUpdated += stats => this.Invoke(() =>
|
||||||
|
{
|
||||||
|
_pingStatsLabel.Text =
|
||||||
|
$"sent: {stats.Sent} recv: {stats.Received} " +
|
||||||
|
$"loss: {stats.LossPct:F1}% " +
|
||||||
|
$"avg: {stats.AvgLatencyMs:F1}ms " +
|
||||||
|
$"jitter: {stats.JitterMs:F1}ms";
|
||||||
|
});
|
||||||
|
_testBtn.Text = "Stop";
|
||||||
|
_pingStatsLabel.Text = "pinging...";
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
@@ -134,14 +187,19 @@ public partial class MainForm : Form
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void OnResize(EventArgs e)
|
||||||
|
{
|
||||||
|
base.OnResize(e);
|
||||||
|
if (WindowState == FormWindowState.Minimized)
|
||||||
|
{
|
||||||
|
Hide();
|
||||||
|
WindowState = FormWindowState.Normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected override void OnFormClosing(FormClosingEventArgs e)
|
protected override void OnFormClosing(FormClosingEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.CloseReason == CloseReason.UserClosing)
|
Shutdown();
|
||||||
{
|
|
||||||
e.Cancel = true;
|
|
||||||
Hide();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
base.OnFormClosing(e);
|
base.OnFormClosing(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace gatuna_client;
|
||||||
|
|
||||||
|
sealed class PingTest
|
||||||
|
{
|
||||||
|
readonly TunnelLink _link;
|
||||||
|
readonly byte[] _serverMac;
|
||||||
|
readonly CancellationTokenSource _cts = new();
|
||||||
|
readonly ConcurrentDictionary<ulong, long> _outstanding = new();
|
||||||
|
readonly ConcurrentQueue<double> _rtts = new();
|
||||||
|
long _sent;
|
||||||
|
long _received;
|
||||||
|
double _lastRttMs;
|
||||||
|
double _jitterSum;
|
||||||
|
long _jitterCount;
|
||||||
|
|
||||||
|
public event Action<PingStats>? StatsUpdated;
|
||||||
|
public event Action<string>? Log;
|
||||||
|
|
||||||
|
public bool Running { get; private set; }
|
||||||
|
|
||||||
|
public PingTest(TunnelLink link, byte[] serverMac)
|
||||||
|
{
|
||||||
|
_link = link;
|
||||||
|
_serverMac = serverMac;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
Running = true;
|
||||||
|
_ = RunLoop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Stop()
|
||||||
|
{
|
||||||
|
Running = false;
|
||||||
|
_cts.Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task RunLoop()
|
||||||
|
{
|
||||||
|
var rng = new Random();
|
||||||
|
var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(10));
|
||||||
|
while (!_cts.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
var nonce = (ulong)Interlocked.Increment(ref _sent);
|
||||||
|
var ticks = DateTime.UtcNow.Ticks;
|
||||||
|
_outstanding[nonce] = ticks;
|
||||||
|
|
||||||
|
_link.SendTo(_serverMac, new Frame.Ping(nonce));
|
||||||
|
|
||||||
|
var interval = rng.Next(10, 101);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(interval, _cts.Token);
|
||||||
|
}
|
||||||
|
catch { break; }
|
||||||
|
}
|
||||||
|
Running = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void HandlePong(ulong nonce)
|
||||||
|
{
|
||||||
|
if (_outstanding.TryRemove(nonce, out var sentTicks))
|
||||||
|
{
|
||||||
|
var rttMs = (DateTime.UtcNow.Ticks - sentTicks) / (double)TimeSpan.TicksPerMillisecond;
|
||||||
|
_rtts.Enqueue(rttMs);
|
||||||
|
Interlocked.Increment(ref _received);
|
||||||
|
|
||||||
|
if (_jitterCount > 0)
|
||||||
|
{
|
||||||
|
_jitterSum += Math.Abs(rttMs - _lastRttMs);
|
||||||
|
}
|
||||||
|
_lastRttMs = rttMs;
|
||||||
|
Interlocked.Increment(ref _jitterCount);
|
||||||
|
|
||||||
|
EmitStats();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmitStats()
|
||||||
|
{
|
||||||
|
var sent = Interlocked.Read(ref _sent);
|
||||||
|
var recv = Interlocked.Read(ref _received);
|
||||||
|
var loss = sent > 0 ? (1.0 - (double)recv / sent) * 100.0 : 0;
|
||||||
|
|
||||||
|
var rttList = _rtts.ToArray();
|
||||||
|
var avg = rttList.Length > 0 ? rttList.Average() : 0;
|
||||||
|
var jitter = _jitterCount > 1 ? _jitterSum / (_jitterCount - 1) : 0;
|
||||||
|
|
||||||
|
StatsUpdated?.Invoke(new PingStats(sent, recv, loss, avg, jitter));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly record struct PingStats(
|
||||||
|
long Sent,
|
||||||
|
long Received,
|
||||||
|
double LossPct,
|
||||||
|
double AvgLatencyMs,
|
||||||
|
double JitterMs);
|
||||||
@@ -11,7 +11,7 @@ static class Program
|
|||||||
|
|
||||||
using var tray = new NotifyIcon
|
using var tray = new NotifyIcon
|
||||||
{
|
{
|
||||||
Icon = SystemIcons.Application,
|
Icon = SystemIcons.GetStockIcon(StockIconId.NetworkConnect, 32),
|
||||||
Text = "gatuna",
|
Text = "gatuna",
|
||||||
Visible = true,
|
Visible = true,
|
||||||
};
|
};
|
||||||
@@ -20,18 +20,19 @@ static class Program
|
|||||||
tray.ContextMenuStrip.Items.Add("Show", null, (_, _) =>
|
tray.ContextMenuStrip.Items.Add("Show", null, (_, _) =>
|
||||||
{
|
{
|
||||||
form.Show();
|
form.Show();
|
||||||
|
form.WindowState = FormWindowState.Normal;
|
||||||
form.Activate();
|
form.Activate();
|
||||||
});
|
});
|
||||||
tray.ContextMenuStrip.Items.Add("-");
|
tray.ContextMenuStrip.Items.Add("-");
|
||||||
tray.ContextMenuStrip.Items.Add("Exit", null, (_, _) =>
|
tray.ContextMenuStrip.Items.Add("Exit", null, (_, _) =>
|
||||||
{
|
{
|
||||||
form.Shutdown();
|
|
||||||
tray.Visible = false;
|
tray.Visible = false;
|
||||||
Application.Exit();
|
form.Close();
|
||||||
});
|
});
|
||||||
tray.DoubleClick += (_, _) =>
|
tray.DoubleClick += (_, _) =>
|
||||||
{
|
{
|
||||||
form.Show();
|
form.Show();
|
||||||
|
form.WindowState = FormWindowState.Normal;
|
||||||
form.Activate();
|
form.Activate();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ 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 = [];
|
||||||
|
PingTest? _pingTest;
|
||||||
|
|
||||||
// Serialized OPEN: only one outstanding at a time.
|
// Serialized OPEN: only one outstanding at a time.
|
||||||
readonly object _openLock = new();
|
readonly object _openLock = new();
|
||||||
@@ -20,10 +22,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)
|
||||||
@@ -45,15 +48,32 @@ sealed class SessionManager : IDisposable
|
|||||||
_link.SendBroadcast(new Frame.Discover());
|
_link.SendBroadcast(new Frame.Discover());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PingTest? StartPing()
|
||||||
|
{
|
||||||
|
if (_link == null || _serverMac == null)
|
||||||
|
return null;
|
||||||
|
_pingTest?.Stop();
|
||||||
|
_pingTest = new PingTest(_link, _serverMac);
|
||||||
|
_pingTest.Start();
|
||||||
|
return _pingTest;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StopPing()
|
||||||
|
{
|
||||||
|
_pingTest?.Stop();
|
||||||
|
_pingTest = null;
|
||||||
|
}
|
||||||
|
|
||||||
public void HandleFrame(Frame frame, byte[] srcMac)
|
public void HandleFrame(Frame frame, byte[] srcMac)
|
||||||
{
|
{
|
||||||
switch (frame)
|
switch (frame)
|
||||||
{
|
{
|
||||||
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:
|
||||||
@@ -82,18 +102,58 @@ sealed class SessionManager : IDisposable
|
|||||||
if (_sessions.TryRemove(close.SessionId, out var s))
|
if (_sessions.TryRemove(close.SessionId, out var s))
|
||||||
s.Dispose();
|
s.Dispose();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case Frame.Pong pong:
|
||||||
|
_pingTest?.HandlePong(pong.Nonce);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Frame.Ping _:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Compute a deterministic mirror port from the server MAC and the
|
||||||
|
/// upstream port. XOR the upstream port with (mac[0]<<8 | mac[5]),
|
||||||
|
/// then ensure the result is outside the privileged range.
|
||||||
|
/// </summary>
|
||||||
|
static ushort ComputeMirrorPort(byte[] serverMac, ushort upstreamPort)
|
||||||
|
{
|
||||||
|
var k = (ushort)((serverMac[0] << 8) | serverMac[5]);
|
||||||
|
var port = (ushort)(upstreamPort ^ k);
|
||||||
|
if (port < 1024)
|
||||||
|
port += 1024;
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Start a local TCP listener for the given upstream. Returns the mirror
|
/// Start a local TCP listener for the given upstream. Returns the mirror
|
||||||
/// port, or 0 on failure.
|
/// port, or 0 on failure.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int StartListener(UpstreamEntry upstream)
|
public int StartListener(UpstreamEntry upstream)
|
||||||
{
|
{
|
||||||
var listener = new TcpListener(IPAddress.Loopback, 0);
|
if (_serverMac == null)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
var preferred = ComputeMirrorPort(_serverMac, upstream.Port);
|
||||||
|
|
||||||
|
// Try the deterministic port first; fall back to OS assignment.
|
||||||
|
TcpListener listener;
|
||||||
|
int port;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
listener = new TcpListener(IPAddress.Loopback, preferred);
|
||||||
listener.Start();
|
listener.Start();
|
||||||
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
listener = new TcpListener(IPAddress.Loopback, 0);
|
||||||
|
listener.Start();
|
||||||
|
port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
||||||
|
Log?.Invoke($"port {preferred} in use, fell back to {port}");
|
||||||
|
}
|
||||||
|
|
||||||
var state = new ListenerState(listener, upstream);
|
var state = new ListenerState(listener, upstream);
|
||||||
_listeners[port] = state;
|
_listeners[port] = state;
|
||||||
_ = AcceptLoop(state);
|
_ = AcceptLoop(state);
|
||||||
@@ -191,6 +251,7 @@ sealed class SessionManager : IDisposable
|
|||||||
|
|
||||||
public void StopAll()
|
public void StopAll()
|
||||||
{
|
{
|
||||||
|
StopPing();
|
||||||
foreach (var kv in _listeners)
|
foreach (var kv in _listeners)
|
||||||
kv.Value.Listener.Stop();
|
kv.Value.Listener.Stop();
|
||||||
_listeners.Clear();
|
_listeners.Clear();
|
||||||
|
|||||||
@@ -50,6 +50,11 @@ sealed class TunnelLink : IDisposable
|
|||||||
var et = (ushort)(data[12] << 8 | data[13]);
|
var et = (ushort)(data[12] << 8 | data[13]);
|
||||||
if (et != Proto.EtherType)
|
if (et != Proto.EtherType)
|
||||||
return;
|
return;
|
||||||
|
// Skip our own outgoing frames (Npcap loops them back in promiscuous mode).
|
||||||
|
if (data[6] == _ourMac[0] && data[7] == _ourMac[1]
|
||||||
|
&& data[8] == _ourMac[2] && data[9] == _ourMac[3]
|
||||||
|
&& data[10] == _ourMac[4] && data[11] == _ourMac[5])
|
||||||
|
return;
|
||||||
var srcMac = new byte[6];
|
var srcMac = new byte[6];
|
||||||
Buffer.BlockCopy(data, 6, srcMac, 0, 6);
|
Buffer.BlockCopy(data, 6, srcMac, 0, 6);
|
||||||
var payload = data.AsSpan(Proto.EthHeaderLen);
|
var payload = data.AsSpan(Proto.EthHeaderLen);
|
||||||
|
|||||||
Reference in New Issue
Block a user