Compare commits
8 Commits
6f7367e36d
...
mistress
| Author | SHA1 | Date | |
|---|---|---|---|
| fcd172f341 | |||
| b4222df349 | |||
| 527b462291 | |||
| 781fe959eb | |||
| ee6b121370 | |||
| f476f6b145 | |||
| a700514849 | |||
| bf2915d8bd |
@@ -0,0 +1,55 @@
|
|||||||
|
name: build
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: hugmaster
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
run: |
|
||||||
|
git clone "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git" .
|
||||||
|
git checkout "$GITHUB_SHA"
|
||||||
|
|
||||||
|
- name: Build Rust
|
||||||
|
run: |
|
||||||
|
cd gatunad
|
||||||
|
cargo build --release
|
||||||
|
|
||||||
|
- name: Build .NET
|
||||||
|
run: |
|
||||||
|
cd gatuna-win
|
||||||
|
dotnet build
|
||||||
|
|
||||||
|
- name: Publish Windows (framework-dependent)
|
||||||
|
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||||
|
run: |
|
||||||
|
cd gatuna-win
|
||||||
|
dotnet publish -c Release -r win-x64 --no-self-contained -o ../publish/win
|
||||||
|
|
||||||
|
- name: Package release assets
|
||||||
|
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||||
|
run: |
|
||||||
|
mkdir -p release
|
||||||
|
cp gatunad/target/release/gatunad release/gatunad-linux-amd64
|
||||||
|
cd publish/win && zip -r ../../release/gatuna-win.zip . && cd ../..
|
||||||
|
|
||||||
|
- name: Create Gitea release
|
||||||
|
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||||
|
run: |
|
||||||
|
TAG="${GITHUB_REF#refs/tags/}"
|
||||||
|
RESP=$(curl -sS -X POST \
|
||||||
|
-H "Authorization: token ${GITHUB_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"tag_name\":\"$TAG\",\"name\":\"$TAG\",\"body\":\"Automated build for $TAG\"}" \
|
||||||
|
"${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/releases")
|
||||||
|
RID=$(echo "$RESP" | jq .id)
|
||||||
|
curl -sS -X POST \
|
||||||
|
-H "Authorization: token ${GITHUB_TOKEN}" \
|
||||||
|
-H "Content-Type: application/octet-stream" \
|
||||||
|
--data-binary @release/gatunad-linux-amd64 \
|
||||||
|
"${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/releases/${RID}/assets?name=gatunad-linux-amd64"
|
||||||
|
curl -sS -X POST \
|
||||||
|
-H "Authorization: token ${GITHUB_TOKEN}" \
|
||||||
|
-H "Content-Type: application/octet-stream" \
|
||||||
|
--data-binary @release/gatuna-win.zip \
|
||||||
|
"${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/releases/${RID}/assets?name=gatuna-win.zip"
|
||||||
@@ -85,7 +85,7 @@ sealed class SessionManager : IDisposable
|
|||||||
lock (_openLock)
|
lock (_openLock)
|
||||||
{
|
{
|
||||||
if (_pending != null)
|
if (_pending != null)
|
||||||
_pending.Client.Dispose();
|
NetUtil.RstClose(_pending.Client);
|
||||||
}
|
}
|
||||||
ProcessQueue();
|
ProcessQueue();
|
||||||
break;
|
break;
|
||||||
@@ -100,7 +100,7 @@ sealed class SessionManager : IDisposable
|
|||||||
|
|
||||||
case Frame.Close close:
|
case Frame.Close close:
|
||||||
if (_sessions.TryRemove(close.SessionId, out var s))
|
if (_sessions.TryRemove(close.SessionId, out var s))
|
||||||
s.Dispose();
|
s.OnRemoteClose();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Frame.Pong pong:
|
case Frame.Pong pong:
|
||||||
@@ -394,6 +394,7 @@ sealed class Session(
|
|||||||
readonly RecvState _recv = new();
|
readonly RecvState _recv = new();
|
||||||
readonly object _sendLock = new();
|
readonly object _sendLock = new();
|
||||||
readonly object _recvLock = new();
|
readonly object _recvLock = new();
|
||||||
|
volatile bool _closeSent;
|
||||||
|
|
||||||
public void Start()
|
public void Start()
|
||||||
{
|
{
|
||||||
@@ -403,6 +404,17 @@ sealed class Session(
|
|||||||
_ = RetransmitTimer();
|
_ = RetransmitTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when a CLOSE frame arrives from the server. The server has
|
||||||
|
/// already torn down its side — we just need to flush queued data to
|
||||||
|
/// the local socket and close gracefully. Do NOT echo CLOSE back.
|
||||||
|
/// </summary>
|
||||||
|
public void OnRemoteClose()
|
||||||
|
{
|
||||||
|
_cts.Cancel();
|
||||||
|
_deliverChannel.Writer.TryComplete();
|
||||||
|
}
|
||||||
|
|
||||||
/// Handle a DATA frame from the tunnel.
|
/// Handle a DATA frame from the tunnel.
|
||||||
public void HandleData(Frame.Data data)
|
public void HandleData(Frame.Data data)
|
||||||
{
|
{
|
||||||
@@ -440,10 +452,10 @@ sealed class Session(
|
|||||||
|
|
||||||
async Task PumpSocketToTunnel()
|
async Task PumpSocketToTunnel()
|
||||||
{
|
{
|
||||||
|
var buf = new byte[Proto.MaxPayload];
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var stream = client.GetStream();
|
var stream = client.GetStream();
|
||||||
var buf = new byte[Proto.MaxPayload];
|
|
||||||
using var reg = _cts.Token.Register(() => client.Dispose());
|
using var reg = _cts.Token.Register(() => client.Dispose());
|
||||||
while (!_cts.IsCancellationRequested)
|
while (!_cts.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
@@ -476,6 +488,8 @@ sealed class Session(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
|
_cts.Cancel();
|
||||||
|
_deliverChannel.Writer.TryComplete();
|
||||||
SendClose();
|
SendClose();
|
||||||
onClosed();
|
onClosed();
|
||||||
}
|
}
|
||||||
@@ -485,10 +499,11 @@ sealed class Session(
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var stream = client.GetStream();
|
var stream = client.GetStream();
|
||||||
await foreach (var payload in _deliverChannel.Reader.ReadAllAsync(_cts.Token))
|
await foreach (var payload in _deliverChannel.Reader.ReadAllAsync())
|
||||||
await stream.WriteAsync(payload, _cts.Token);
|
await stream.WriteAsync(payload);
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
|
finally { client.Close(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task RetransmitTimer()
|
async Task RetransmitTimer()
|
||||||
@@ -539,14 +554,33 @@ sealed class Session(
|
|||||||
link.SendTo(serverMac, new Frame.Data(sessionId, seq, ackSeq, []));
|
link.SendTo(serverMac, new Frame.Data(sessionId, seq, ackSeq, []));
|
||||||
}
|
}
|
||||||
|
|
||||||
void SendClose() => link.SendTo(serverMac, new Frame.Close(sessionId, null));
|
void SendClose()
|
||||||
|
{
|
||||||
|
if (_closeSent) return;
|
||||||
|
_closeSent = true;
|
||||||
|
link.SendTo(serverMac, new Frame.Close(sessionId, null));
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_cts.Cancel();
|
_cts.Cancel();
|
||||||
_deliverChannel.Writer.TryComplete();
|
_deliverChannel.Writer.TryComplete();
|
||||||
SendClose();
|
SendClose();
|
||||||
try { client.Dispose(); } catch { }
|
NetUtil.RstClose(client);
|
||||||
_cts.Dispose();
|
_cts.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Close a TcpClient with a TCP RST instead of a FIN.
|
||||||
|
static partial class NetUtil
|
||||||
|
{
|
||||||
|
public static void RstClose(TcpClient c)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
c.LingerState = new LingerOption(true, 0);
|
||||||
|
c.Close();
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
<assemblyIdentity version="2.0.0.0" name="gatuna" />
|
<assemblyIdentity version="2.1.0.0" name="gatuna" />
|
||||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||||
<security>
|
<security>
|
||||||
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<TargetFramework>net8.0-windows</TargetFramework>
|
<TargetFramework>net8.0-windows</TargetFramework>
|
||||||
<AssemblyName>gatuna</AssemblyName>
|
<AssemblyName>gatuna</AssemblyName>
|
||||||
<RootNamespace>gatuna</RootNamespace>
|
<RootNamespace>gatuna</RootNamespace>
|
||||||
<Version>2.0.0</Version>
|
<Version>2.1.0</Version>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<UseWindowsForms>true</UseWindowsForms>
|
<UseWindowsForms>true</UseWindowsForms>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "gatuna"
|
name = "gatuna"
|
||||||
version = "2.0.0"
|
version = "2.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "CC0-1.0"
|
license = "CC0-1.0"
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ path = "src/main.rs"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "sync", "io-util"] }
|
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "sync", "io-util", "time"] }
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
pnet_datalink = "0.35"
|
pnet_datalink = "0.35"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
|
|||||||
@@ -12,6 +12,16 @@ use crate::frame::{ETHERTYPE, ETH_HEADER_LEN};
|
|||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub struct MacAddr(pub [u8; 6]);
|
pub struct MacAddr(pub [u8; 6]);
|
||||||
|
|
||||||
|
impl std::fmt::Display for MacAddr {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}",
|
||||||
|
self.0[0], self.0[1], self.0[2], self.0[3], self.0[4], self.0[5]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl MacAddr {
|
impl MacAddr {
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn broadcast() -> Self {
|
pub fn broadcast() -> Self {
|
||||||
|
|||||||
+18
-7
@@ -16,7 +16,7 @@ use std::sync::{Arc, Mutex};
|
|||||||
use tokio::net::TcpStream;
|
use tokio::net::TcpStream;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tokio::sync::Mutex as AsyncMutex;
|
use tokio::sync::Mutex as AsyncMutex;
|
||||||
use tracing::{error, Level};
|
use tracing::{error, info, Level};
|
||||||
|
|
||||||
use crate::frame::{
|
use crate::frame::{
|
||||||
Frame, REASON_CONNECT_FAILED, REASON_UNKNOWN_SESSION, REASON_UNKNOWN_UPSTREAM,
|
Frame, REASON_CONNECT_FAILED, REASON_UNKNOWN_SESSION, REASON_UNKNOWN_UPSTREAM,
|
||||||
@@ -33,17 +33,21 @@ struct Args {
|
|||||||
/// One or more TCP upstreams as PORT[:label], relayed to 127.0.0.1:PORT.
|
/// One or more TCP upstreams as PORT[:label], relayed to 127.0.0.1:PORT.
|
||||||
#[arg(num_args = 1..)]
|
#[arg(num_args = 1..)]
|
||||||
ports: Vec<String>,
|
ports: Vec<String>,
|
||||||
|
/// Verbose logging (lifecycle events to stdout).
|
||||||
|
#[arg(short, long)]
|
||||||
|
verbose: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> ExitCode {
|
async fn main() -> ExitCode {
|
||||||
|
let args = Args::parse();
|
||||||
|
|
||||||
|
let level = if args.verbose { Level::INFO } else { Level::ERROR };
|
||||||
tracing_subscriber::fmt()
|
tracing_subscriber::fmt()
|
||||||
.with_max_level(Level::ERROR)
|
.with_max_level(level)
|
||||||
.with_writer(|| std::io::stdout())
|
.with_writer(|| std::io::stdout())
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
let args = Args::parse();
|
|
||||||
|
|
||||||
let table = match build_table(&args.ports) {
|
let table = match build_table(&args.ports) {
|
||||||
Ok(t) => Arc::new(t),
|
Ok(t) => Arc::new(t),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -110,7 +114,7 @@ async fn main() -> ExitCode {
|
|||||||
let frame = match Frame::parse(payload) {
|
let frame = match Frame::parse(payload) {
|
||||||
Ok(f) => f,
|
Ok(f) => f,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("decode from {src:?}: {e}");
|
error!("decode from {src}: {e}");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -141,13 +145,16 @@ async fn handle_frame(
|
|||||||
) {
|
) {
|
||||||
match frame {
|
match frame {
|
||||||
Frame::Discover => {
|
Frame::Discover => {
|
||||||
|
info!("DISCOVER from {src}");
|
||||||
let manifest = Frame::Manifest {
|
let manifest = Frame::Manifest {
|
||||||
hostname: (**hostname).clone(),
|
hostname: (**hostname).clone(),
|
||||||
entries: table.entries(),
|
entries: table.entries(),
|
||||||
};
|
};
|
||||||
let _ = tx.send((src, manifest.encode())).await;
|
let _ = tx.send((src, manifest.encode())).await;
|
||||||
|
info!("MANIFEST sent to {src} ({} upstreams)", table.0.len());
|
||||||
}
|
}
|
||||||
Frame::Open { upstream_id, proto } => {
|
Frame::Open { upstream_id, proto: _ } => {
|
||||||
|
info!("OPEN upstream {upstream_id} from {src}");
|
||||||
let upstream = table.get(upstream_id);
|
let upstream = table.get(upstream_id);
|
||||||
match upstream {
|
match upstream {
|
||||||
Some(upstream) => {
|
Some(upstream) => {
|
||||||
@@ -176,10 +183,12 @@ async fn handle_frame(
|
|||||||
);
|
);
|
||||||
let ack = Frame::OpenAck { session_id: sid, upstream_id, proto };
|
let ack = Frame::OpenAck { session_id: sid, upstream_id, proto };
|
||||||
let _ = tx.send((src, ack.encode())).await;
|
let _ = tx.send((src, ack.encode())).await;
|
||||||
|
info!("session {sid} established (upstream {upstream_id})");
|
||||||
spawn_pump(r, sid, proto, src, tx, store);
|
spawn_pump(r, sid, proto, src, tx, store);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("connect 127.0.0.1:{port} failed: {e}");
|
error!("connect 127.0.0.1:{port} failed: {e}");
|
||||||
|
info!("OPEN_NAK upstream {upstream_id} (connect_failed) to {src}");
|
||||||
let nak =
|
let nak =
|
||||||
Frame::OpenNak { upstream_id, reason: REASON_CONNECT_FAILED };
|
Frame::OpenNak { upstream_id, reason: REASON_CONNECT_FAILED };
|
||||||
let _ = tx.send((src, nak.encode())).await;
|
let _ = tx.send((src, nak.encode())).await;
|
||||||
@@ -215,7 +224,9 @@ async fn handle_frame(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Frame::Close { session_id, reason: _ } => {
|
Frame::Close { session_id, reason: _ } => {
|
||||||
store.lock().expect("store poisoned").remove(&session_id);
|
if store.lock().expect("store poisoned").remove(&session_id).is_some() {
|
||||||
|
info!("CLOSE session {session_id} from {src}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Frame::Ping { nonce } => {
|
Frame::Ping { nonce } => {
|
||||||
let pong = Frame::Pong { nonce };
|
let pong = Frame::Pong { nonce };
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ pub struct RecvState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl SendState {
|
impl SendState {
|
||||||
fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
SendState {
|
SendState {
|
||||||
send_seq: 0,
|
send_seq: 0,
|
||||||
acked_seq: 0,
|
acked_seq: 0,
|
||||||
@@ -83,7 +83,7 @@ impl SendState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl RecvState {
|
impl RecvState {
|
||||||
fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
RecvState {
|
RecvState {
|
||||||
expected_seq: 0,
|
expected_seq: 0,
|
||||||
deliver_seq: 0,
|
deliver_seq: 0,
|
||||||
@@ -129,6 +129,7 @@ impl RecvState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
#[allow(dead_code)]
|
||||||
pub struct SessionHandle {
|
pub struct SessionHandle {
|
||||||
pub upstream_id: u8,
|
pub upstream_id: u8,
|
||||||
pub proto: u8,
|
pub proto: u8,
|
||||||
@@ -155,7 +156,7 @@ pub async fn handle_data(
|
|||||||
seq: u32,
|
seq: u32,
|
||||||
ack_seq: u32,
|
ack_seq: u32,
|
||||||
payload: &[u8],
|
payload: &[u8],
|
||||||
tx: &TxChan,
|
_tx: &TxChan,
|
||||||
) -> Result<bool, WriteError> {
|
) -> Result<bool, WriteError> {
|
||||||
let handle = {
|
let handle = {
|
||||||
let store = store.lock().expect("store lock poisoned");
|
let store = store.lock().expect("store lock poisoned");
|
||||||
@@ -274,12 +275,12 @@ pub fn spawn_pump(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if should_close {
|
if should_close {
|
||||||
|
store.lock().expect("store poisoned").remove(&session_id);
|
||||||
let close = Frame::Close {
|
let close = Frame::Close {
|
||||||
session_id,
|
session_id,
|
||||||
reason: Some(REASON_MAX_RETRIES),
|
reason: Some(REASON_MAX_RETRIES),
|
||||||
};
|
};
|
||||||
let _ = tx.try_send((peer_mac, close.encode()));
|
let _ = tx.try_send((peer_mac, close.encode()));
|
||||||
store.lock().expect("store poisoned").remove(&session_id);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -329,8 +330,8 @@ pub fn spawn_pump(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
store.lock().expect("store poisoned").remove(&session_id);
|
||||||
let close = Frame::Close { session_id, reason: None };
|
let close = Frame::Close { session_id, reason: None };
|
||||||
let _ = tx.send((peer_mac, close.encode())).await;
|
let _ = tx.send((peer_mac, close.encode())).await;
|
||||||
store.lock().expect("store poisoned").remove(&session_id);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user