diff --git a/linker.sln b/linker.sln
index 2d2273fc..f7dd5a43 100644
--- a/linker.sln
+++ b/linker.sln
@@ -13,6 +13,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "linker.service", "linker.se
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "linker.tunnel", "linker.tunnel\linker.tunnel.csproj", "{AFADE8D6-AB00-456B-9F43-53BC95B7B608}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "linker.tun", "linker.tun\linker.tun.csproj", "{0DE134E0-7CD8-4DCF-8D2A-325CEBE5895F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "linker.tun.test", "linker.tun.test\linker.tun.test.csproj", "{4A660D3B-76DE-4E6F-9E90-90BA0DBE906A}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -83,6 +87,30 @@ Global
{AFADE8D6-AB00-456B-9F43-53BC95B7B608}.Release|x64.Build.0 = Release|Any CPU
{AFADE8D6-AB00-456B-9F43-53BC95B7B608}.Release|x86.ActiveCfg = Release|Any CPU
{AFADE8D6-AB00-456B-9F43-53BC95B7B608}.Release|x86.Build.0 = Release|Any CPU
+ {0DE134E0-7CD8-4DCF-8D2A-325CEBE5895F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0DE134E0-7CD8-4DCF-8D2A-325CEBE5895F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0DE134E0-7CD8-4DCF-8D2A-325CEBE5895F}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {0DE134E0-7CD8-4DCF-8D2A-325CEBE5895F}.Debug|x64.Build.0 = Debug|Any CPU
+ {0DE134E0-7CD8-4DCF-8D2A-325CEBE5895F}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {0DE134E0-7CD8-4DCF-8D2A-325CEBE5895F}.Debug|x86.Build.0 = Debug|Any CPU
+ {0DE134E0-7CD8-4DCF-8D2A-325CEBE5895F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0DE134E0-7CD8-4DCF-8D2A-325CEBE5895F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0DE134E0-7CD8-4DCF-8D2A-325CEBE5895F}.Release|x64.ActiveCfg = Release|Any CPU
+ {0DE134E0-7CD8-4DCF-8D2A-325CEBE5895F}.Release|x64.Build.0 = Release|Any CPU
+ {0DE134E0-7CD8-4DCF-8D2A-325CEBE5895F}.Release|x86.ActiveCfg = Release|Any CPU
+ {0DE134E0-7CD8-4DCF-8D2A-325CEBE5895F}.Release|x86.Build.0 = Release|Any CPU
+ {4A660D3B-76DE-4E6F-9E90-90BA0DBE906A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4A660D3B-76DE-4E6F-9E90-90BA0DBE906A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4A660D3B-76DE-4E6F-9E90-90BA0DBE906A}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {4A660D3B-76DE-4E6F-9E90-90BA0DBE906A}.Debug|x64.Build.0 = Debug|Any CPU
+ {4A660D3B-76DE-4E6F-9E90-90BA0DBE906A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {4A660D3B-76DE-4E6F-9E90-90BA0DBE906A}.Debug|x86.Build.0 = Debug|Any CPU
+ {4A660D3B-76DE-4E6F-9E90-90BA0DBE906A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4A660D3B-76DE-4E6F-9E90-90BA0DBE906A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4A660D3B-76DE-4E6F-9E90-90BA0DBE906A}.Release|x64.ActiveCfg = Release|Any CPU
+ {4A660D3B-76DE-4E6F-9E90-90BA0DBE906A}.Release|x64.Build.0 = Release|Any CPU
+ {4A660D3B-76DE-4E6F-9E90-90BA0DBE906A}.Release|x86.ActiveCfg = Release|Any CPU
+ {4A660D3B-76DE-4E6F-9E90-90BA0DBE906A}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/linker.tun.test/Program.cs b/linker.tun.test/Program.cs
new file mode 100644
index 00000000..c10ae1c8
--- /dev/null
+++ b/linker.tun.test/Program.cs
@@ -0,0 +1,18 @@
+using System.Net;
+
+namespace linker.tun.test
+{
+ internal class Program
+ {
+ static void Main(string[] args)
+ {
+
+ LinkerWinTunDevice linkerWinTunDevice = new LinkerWinTunDevice("linker111", Guid.Parse("d9f71e4f-ba49-4cba-be5e-69a5694df8cb"));
+ linkerWinTunDevice.SetUp(IPAddress.Parse("192.168.55.2"), IPAddress.Parse("192.168.55.1"), 24, out string error);
+
+ Console.WriteLine(error);
+
+ Console.ReadLine();
+ }
+ }
+}
diff --git a/linker.tun.test/linker.tun.test.csproj b/linker.tun.test/linker.tun.test.csproj
new file mode 100644
index 00000000..998cba7c
--- /dev/null
+++ b/linker.tun.test/linker.tun.test.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+ true
+
+
+
+
+
+
+
diff --git a/linker.tun/ILinkerTunDevice.cs b/linker.tun/ILinkerTunDevice.cs
new file mode 100644
index 00000000..ce61d0dd
--- /dev/null
+++ b/linker.tun/ILinkerTunDevice.cs
@@ -0,0 +1,26 @@
+using System.Net;
+
+namespace linker.tun
+{
+ public interface ILinkerTunDevice
+ {
+ public string Name { get; }
+ public bool Running { get; }
+
+ public bool SetUp(IPAddress address, IPAddress gateway, byte prefixLength, out string error);
+ public void Shutdown();
+
+ public void AddRoute(LinkerTunDeviceRouteItem[] ips, IPAddress ip);
+ public void DelRoute(LinkerTunDeviceRouteItem[] ip);
+
+ public ReadOnlyMemory Read();
+ public bool Write(ReadOnlyMemory buffer);
+ }
+
+
+ public sealed class LinkerTunDeviceRouteItem
+ {
+ public IPAddress Address { get; }
+ public byte Mask { get; }
+ }
+}
diff --git a/linker.tun/LinkerLinuxTunDevice.cs b/linker.tun/LinkerLinuxTunDevice.cs
new file mode 100644
index 00000000..95b99ac4
--- /dev/null
+++ b/linker.tun/LinkerLinuxTunDevice.cs
@@ -0,0 +1,147 @@
+using linker.libs;
+using Microsoft.Win32.SafeHandles;
+using System.Net;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace linker.tun
+{
+ public sealed class LinkerLinuxTunDevice : ILinkerTunDevice
+ {
+
+ private string name = string.Empty;
+ public string Name => name;
+ public bool Running => fs != null;
+
+ private string interfaceLinux = string.Empty;
+ private FileStream fs = null;
+
+ public LinkerLinuxTunDevice(string name)
+ {
+ this.name = name;
+ }
+
+ public bool SetUp(IPAddress address, IPAddress gateway, byte prefixLength, out string error)
+ {
+ error = string.Empty;
+ if (fs != null)
+ {
+ error = ($"Adapter already exists");
+ return false;
+ }
+
+ CommandHelper.Linux(string.Empty, new string[] {
+ $"ip tuntap add mode tun dev {Name}",
+ $"ip addr del {address}/{prefixLength} dev {Name}",
+ $"ip addr add {address}/{prefixLength} dev {Name}",
+ $"ip link set dev {Name} up"
+ });
+
+ string str = CommandHelper.Linux(string.Empty, new string[] { $"ifconfig" });
+ if (str.Contains(Name) == false)
+ {
+ error = CommandHelper.Linux(string.Empty, new string[] { $"ip tuntap add mode tun dev {Name}" });
+ return false;
+ }
+
+ interfaceLinux = GetLinuxInterfaceNum();
+
+ SafeFileHandle safeFileHandle = File.OpenHandle("/dev/net/tun", FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite, FileOptions.Asynchronous);
+
+ byte[] ifreqFREG0 = Encoding.ASCII.GetBytes(this.Name);
+ Array.Resize(ref ifreqFREG0, 16);
+ byte[] ifreqFREG1 = { 0x01, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
+ byte[] ifreq = BytesPlusBytes(ifreqFREG0, ifreqFREG1);
+ Ioctl(safeFileHandle, 1074025674, ifreq);
+ fs = new FileStream(safeFileHandle, FileAccess.ReadWrite, 1500);
+
+ return true;
+ }
+ public void Shutdown()
+ {
+ if (fs != null)
+ {
+ interfaceLinux = string.Empty;
+ fs.Close();
+ fs.Dispose();
+ fs = null;
+ }
+ CommandHelper.Linux(string.Empty, new string[] { $"ip tuntap del mode tun dev {Name}" });
+ }
+
+ public void AddRoute(LinkerTunDeviceRouteItem[] ips, IPAddress ip)
+ {
+ string[] commands = ips.Select(item =>
+ {
+ uint maskValue = NetworkHelper.MaskValue(item.Mask);
+ IPAddress _ip = NetworkHelper.ToNetworkIp(item.Address, maskValue);
+
+ return $"ip route add {_ip}/{item.Mask} via {ip} dev {Name} metric 1 ";
+ }).ToArray();
+ if (commands.Length > 0)
+ {
+ CommandHelper.Linux(string.Empty, commands);
+ }
+ }
+ public void DelRoute(LinkerTunDeviceRouteItem[] ip)
+ {
+ string[] commands = ip.Select(item =>
+ {
+ uint maskValue = NetworkHelper.MaskValue(item.Mask);
+ IPAddress _ip = NetworkHelper.ToNetworkIp(item.Address, maskValue);
+ return $"ip route del {_ip}/{item.Mask}";
+ }).ToArray();
+ CommandHelper.Linux(string.Empty, commands);
+ }
+
+
+ private byte[] buffer = new byte[2 * 1024];
+ public ReadOnlyMemory Read()
+ {
+ int length = fs.Read(buffer, 0, buffer.Length);
+ return buffer.AsMemory(0, length);
+ }
+ public bool Write(ReadOnlyMemory buffer)
+ {
+ fs.Write(buffer.Span);
+ return true;
+ }
+
+ private string GetLinuxInterfaceNum()
+ {
+ string output = CommandHelper.Linux(string.Empty, new string[] { "ip route" });
+ foreach (var item in output.Split(Environment.NewLine))
+ {
+ if (item.StartsWith("default via"))
+ {
+ var strs = item.Split(' ');
+ for (int i = 0; i < strs.Length; i++)
+ {
+ if (strs[i] == "dev")
+ {
+ return strs[i + 1];
+ }
+ }
+ }
+ }
+ return string.Empty;
+ }
+
+
+ [DllImport("libc.so.6", EntryPoint = "ioctl", SetLastError = true)]
+ private static extern int Ioctl(SafeHandle device, UInt32 request, byte[] dat);
+ private byte[] BytesPlusBytes(byte[] A, byte[] B)
+ {
+ byte[] ret = new byte[A.Length + B.Length - 1 + 1];
+ int k = 0;
+ for (var i = 0; i <= A.Length - 1; i++)
+ ret[i] = A[i];
+ k = A.Length;
+ for (var i = k; i <= ret.Length - 1; i++)
+ ret[i] = B[i - k];
+ return ret;
+ }
+
+
+ }
+}
diff --git a/linker.tun/LinkerWinTunDevice.cs b/linker.tun/LinkerWinTunDevice.cs
new file mode 100644
index 00000000..a874f623
--- /dev/null
+++ b/linker.tun/LinkerWinTunDevice.cs
@@ -0,0 +1,289 @@
+using linker.libs;
+using System.Buffers.Binary;
+using System.Net;
+using System.Net.NetworkInformation;
+using System.Runtime.InteropServices;
+
+namespace linker.tun
+{
+ public sealed class LinkerWinTunDevice : ILinkerTunDevice
+ {
+ private string name = string.Empty;
+ public string Name => name;
+ public bool Running => session != 0;
+
+ private IntPtr waitHandle = IntPtr.Zero, adapter = IntPtr.Zero, session = IntPtr.Zero;
+ private Guid guid;
+ private int interfaceNumber = 0;
+
+ public LinkerWinTunDevice(string name, Guid guid)
+ {
+ this.name = name;
+ this.guid = guid;
+ }
+
+ public bool SetUp(IPAddress address, IPAddress gateway, byte prefixLength, out string error)
+ {
+ error = string.Empty;
+ if (adapter != 0)
+ {
+ error = ($"Adapter already exists");
+ return false;
+ }
+
+ adapter = WintunCreateAdapter(name, name, ref guid);
+ if (adapter == 0)
+ {
+ error = ($"Failed to create adapter {Marshal.GetLastWin32Error():x2}");
+ return false;
+ }
+ uint version = WintunGetRunningDriverVersion();
+ session = WintunStartSession(adapter, 0x400000);
+ if (session == 0)
+ {
+ error = ($"Failed to create adapter");
+ return false;
+ }
+
+ waitHandle = WintunGetReadWaitEvent(session);
+
+ WintunGetAdapterLUID(adapter, out ulong luid);
+ {
+ MIB_UNICASTIPADDRESS_ROW AddressRow = default;
+ InitializeUnicastIpAddressEntry(ref AddressRow);
+ AddressRow.sin_family = 2;
+ AddressRow.sin_addr = BinaryPrimitives.ReadUInt32LittleEndian(address.GetAddressBytes());
+ AddressRow.OnLinkPrefixLength = prefixLength;
+ AddressRow.DadState = 4;
+ AddressRow.InterfaceLuid = luid;
+ uint LastError = CreateUnicastIpAddressEntry(ref AddressRow);
+ if (LastError != 0) throw new InvalidOperationException();
+ }
+ {
+ MIB_IPFORWARD_ROW2 row = default;
+ InitializeIpForwardEntry(ref row);
+ row.InterfaceLuid = luid;
+ row.PrefixLength = 0;
+ row.si_family = 2;
+ row.NextHop_si_family = 2;
+ row.sin_addr = 0;
+ row.NextHop_sin_addr = BinaryPrimitives.ReadUInt32LittleEndian(gateway.GetAddressBytes());
+ uint LastError = CreateIpForwardEntry2(ref row);
+ if (LastError != 0) throw new InvalidOperationException();
+ }
+
+ GetWindowsInterfaceNum();
+
+ return true;
+ }
+ public void Shutdown()
+ {
+ if (session != 0)
+ {
+ WintunEndSession(session);
+ WintunCloseAdapter(adapter);
+ WintunDeleteDriver();
+ }
+ session = 0;
+ adapter = 0;
+ interfaceNumber = 0;
+ }
+
+ public void AddRoute(LinkerTunDeviceRouteItem[] ips, IPAddress ip)
+ {
+ if (interfaceNumber > 0)
+ {
+ string[] commands = ips.Select(item =>
+ {
+ uint maskValue = NetworkHelper.MaskValue(item.Mask);
+ IPAddress mask = NetworkHelper.GetMaskIp(maskValue);
+ IPAddress _ip = NetworkHelper.ToNetworkIp(item.Address, maskValue);
+
+ return $"route add {_ip} mask {mask} {ip} metric 5 if {interfaceNumber}";
+ }).ToArray();
+ if (commands.Length > 0)
+ {
+ CommandHelper.Windows(string.Empty, commands);
+ }
+ }
+ }
+ public void DelRoute(LinkerTunDeviceRouteItem[] ip)
+ {
+ string[] commands = ip.Select(item =>
+ {
+ uint maskValue = NetworkHelper.MaskValue(item.Mask);
+ IPAddress mask = NetworkHelper.GetMaskIp(maskValue);
+ IPAddress _ip = NetworkHelper.ToNetworkIp(item.Address, maskValue);
+ return $"route delete {_ip}";
+ }).ToArray();
+ if (commands.Length > 0)
+ {
+ CommandHelper.Windows(string.Empty, commands.ToArray());
+ }
+ }
+
+
+ private byte[] buffer = new byte[2 * 1024];
+ public unsafe ReadOnlyMemory Read()
+ {
+ if (session == 0) return Helper.EmptyArray;
+ for (; ; )
+ {
+ IntPtr packet = WintunReceivePacket(session, out var packetSize);
+ if (packet != 0)
+ {
+ new Span((byte*)packet, (int)packetSize).CopyTo(buffer.AsSpan(0, (int)packetSize));
+ WintunReleaseReceivePacket(session, packet);
+ return buffer.AsMemory(0, (int)packetSize);
+ }
+ else
+ {
+ if (Marshal.GetLastWin32Error() == 259L)
+ {
+ WaitForSingleObject(waitHandle, 0xFFFFFFFF);
+ }
+ }
+ }
+ }
+ public unsafe bool Write(ReadOnlyMemory buffer)
+ {
+ if (session == 0) return false;
+
+ IntPtr packet = WintunAllocateSendPacket(session, (uint)buffer.Length);
+ if (packet != 0)
+ {
+ buffer.Span.CopyTo(new Span((byte*)packet, buffer.Length));
+ WintunSendPacket(session, packet);
+ }
+ else
+ {
+ if (Marshal.GetLastWin32Error() == 111L)
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private void GetWindowsInterfaceNum()
+ {
+ NetworkInterface adapter = NetworkInterface.GetAllNetworkInterfaces()
+ .FirstOrDefault(c => c.Name == Name);
+ if (adapter != null)
+ {
+ interfaceNumber = adapter.GetIPProperties().GetIPv4Properties().Index;
+ }
+ }
+
+ [StructLayout(LayoutKind.Explicit, Pack = 1, Size = 80)]
+ private struct MIB_UNICASTIPADDRESS_ROW
+ {
+ [FieldOffset(0)]
+ public ushort sin_family;
+ [FieldOffset(4)]
+ public uint sin_addr;
+ [FieldOffset(32)]
+ public ulong InterfaceLuid;
+ [FieldOffset(60)]
+ public byte OnLinkPrefixLength;
+ [FieldOffset(64)]
+ public int DadState;
+ }
+
+ [StructLayout(LayoutKind.Explicit, Pack = 1, Size = 104)]
+ private struct MIB_IPFORWARD_ROW2
+ {
+ [FieldOffset(0)]
+ public ulong InterfaceLuid;
+ [FieldOffset(12)]
+ public ushort si_family;
+ [FieldOffset(16)]
+ public uint sin_addr;
+ [FieldOffset(40)]
+ public byte PrefixLength;
+ [FieldOffset(48)]
+ public uint NextHop_sin_addr;
+ [FieldOffset(44)]
+ public ushort NextHop_si_family;
+ }
+
+ [DllImport("iphlpapi.dll", SetLastError = true)]
+ private static extern void InitializeUnicastIpAddressEntry(ref MIB_UNICASTIPADDRESS_ROW Row);
+
+ [DllImport("iphlpapi.dll", SetLastError = true)]
+ private static extern uint CreateUnicastIpAddressEntry(ref MIB_UNICASTIPADDRESS_ROW Row);
+
+ [DllImport("iphlpapi.dll", SetLastError = true)]
+ private static extern void InitializeIpForwardEntry(ref MIB_IPFORWARD_ROW2 Row);
+
+ [DllImport("iphlpapi.dll", SetLastError = true)]
+ private static extern uint CreateIpForwardEntry2(ref MIB_IPFORWARD_ROW2 Row);
+ [DllImport("kernel32.dll")]
+ private static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);
+
+ [DllImport("wintun.dll", SetLastError = true)]
+ private static extern IntPtr WintunCreateAdapter(
+ [MarshalAs(UnmanagedType.LPWStr)]
+ string name,
+ [MarshalAs(UnmanagedType.LPWStr)]
+ string tunnelType,
+ ref Guid guid);
+
+ [DllImport("wintun.dll", SetLastError = true)]
+ private static extern uint WintunGetRunningDriverVersion();
+
+ [DllImport("wintun.dll", SetLastError = true)]
+ private static extern void WintunGetAdapterLUID(IntPtr adapter, out ulong luid);
+
+ [DllImport("wintun.dll", SetLastError = true)]
+ private static extern IntPtr WintunStartSession(IntPtr adapter, uint capacity);
+
+ [DllImport("wintun.dll", SetLastError = true)]
+ private static extern IntPtr WintunGetReadWaitEvent(IntPtr session);
+
+ [DllImport("wintun.dll", SetLastError = true)]
+ private static extern IntPtr WintunReceivePacket(IntPtr session, out uint packetSize);
+
+ [DllImport("wintun.dll", SetLastError = true)]
+ private static extern void WintunSendPacket(IntPtr session, IntPtr packet);
+
+ [DllImport("wintun.dll", SetLastError = true)]
+ private static extern void WintunEndSession(IntPtr session);
+
+ [DllImport("wintun.dll", SetLastError = true)]
+ private static extern void WintunCloseAdapter(IntPtr adapter);
+
+ [DllImport("wintun.dll", SetLastError = true)]
+ private static extern IntPtr WintunAllocateSendPacket(IntPtr session, uint packetSize);
+
+ [DllImport("wintun.dll", SetLastError = true)]
+ private static extern IntPtr WintunOpenAdapter(
+ [MarshalAs(UnmanagedType.LPWStr)]
+ string name);
+
+ [DllImport("wintun.dll", SetLastError = true)]
+ private static extern bool WintunDeleteDriver();
+
+ [DllImport("wintun.dll", SetLastError = true)]
+ private static extern void WintunReleaseReceivePacket(IntPtr session, IntPtr packet);
+
+ [DllImport("wintun.dll", SetLastError = true)]
+ private static extern void WintunSetLogger(WINTUN_LOGGER_CALLBACK newLogger);
+
+
+ private delegate void WINTUN_LOGGER_CALLBACK(
+ WINTUN_LOGGER_LEVEL level,
+ ulong timestamp,
+ [MarshalAs(UnmanagedType.LPWStr)]
+ string message);
+
+ private enum WINTUN_LOGGER_LEVEL
+ {
+ WINTUN_LOG_INFO, /**< Informational */
+ WINTUN_LOG_WARN, /**< Warning */
+ WINTUN_LOG_ERR /**< Error */
+ }
+
+
+ }
+}
diff --git a/linker.tun/linker.tun.csproj b/linker.tun/linker.tun.csproj
new file mode 100644
index 00000000..6137061d
--- /dev/null
+++ b/linker.tun/linker.tun.csproj
@@ -0,0 +1,37 @@
+
+
+
+ net8.0
+ enable
+ disable
+ true
+ false
+ true
+ true
+ linker tun
+ 1.2.0
+ snltty
+ snltty
+ linker tunnel
+ snltty
+ https://github.com/snltty/linker
+ https://github.com/snltty/linker
+ linker tun
+ 1.2.0.2
+ 1.2.0.2
+
+
+
+ full
+ true
+
+
+ none
+ false
+ True
+
+
+
+
+
+
diff --git a/linker.tunnel/linker.tunnel.csproj b/linker.tunnel/linker.tunnel.csproj
index 845c3e4c..5b77e360 100644
--- a/linker.tunnel/linker.tunnel.csproj
+++ b/linker.tunnel/linker.tunnel.csproj
@@ -1,25 +1,25 @@
-
- net8.0
- enable
- disable
- true
- false
- true
- true
- linker tunnel
- 1.2.0
- snltty
- snltty
- linker tunnel
- snltty
- https://github.com/snltty/linker
- https://github.com/snltty/linker
- linker tunnel
- 1.2.0.2
- 1.2.0.2
-
+
+ net8.0
+ enable
+ disable
+ true
+ false
+ true
+ true
+ linker tunnel
+ 1.2.0
+ snltty
+ snltty
+ linker tunnel
+ snltty
+ https://github.com/snltty/linker
+ https://github.com/snltty/linker
+ linker tunnel
+ 1.2.0.2
+ 1.2.0.2
+
full
@@ -31,8 +31,8 @@
True
-
-
-
+
+
+