upnp和nat-pmp

This commit is contained in:
snltty
2026-03-04 17:32:56 +08:00
parent 065b992871
commit fce114f170
71 changed files with 2022 additions and 168 deletions
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
release_name: v2.0.0.${{ steps.date.outputs.today }}
draft: false
prerelease: false
body: "1. 一些累计更新,一些BUG修复\r\n2. #92 松开打洞loading限制,允许选择中继节点\r\n3. #89 windows下利用任务计划进行进程守护,定时检查服务\r\n4. 优化客户端管理ws并发\r\n5. 减少服务器推送,减轻主服务器压力\r\n6. 优化中继,针对无法连接自己的外网IP的情况"
body: "1. 一些累计更新,一些BUG修复\r\n2. #92 松开打洞loading限制,允许选择中继节点\r\n3. #89 windows下利用任务计划进行进程守护,定时检查服务\r\n4. 优化客户端管理ws并发\r\n5. 减少服务器推送,减轻主服务器压力\r\n6. 优化中继,针对无法连接自己的外网IP的情况\r\n7. 优化upnp,自己实现了一遍"
- name: setup node.js
uses: actions/setup-node@v2
with:
+2 -1
View File
@@ -36,7 +36,8 @@ jobs:
cat src/linker/linker.csproj && \
dotnet nuget locals all --clear && \
uname -m && \
dotnet run src/linker
dotnet publish src/linker -c release -f net9.0 -o public/publish/loongarch64 -r linux-loongarch64 -p:PublishSingleFile=true --self-contained true \
-p:TrimMode=partial -p:TieredPGO=true -p:DebugType=full -p:EventSourceSupport=false -p:DebugSymbols=true -p:EnableCompressionInSingleFile=true -p:DebuggerSupport=false -p:EnableUnsafeBinaryFormatterSerialization=false -p:EnableUnsafeUTF7Encoding=false -p:HttpActivityPropagationSupport=false -p:InvariantGlobalization=true -p:MetadataUpdaterSupport=false -p:UseSystemResourceKeys=true -p:MetricsSupport=false -p:StackTraceSupport=false -p:XmlResolverIsNetworkingEnabledByDefault=false
"
+3
View File
@@ -52,6 +52,7 @@ jobs:
dotnet build ./src/linker.tun -c release
dotnet build ./src/linker.nat -c release
dotnet build ./src/linker.tunnel -c release
dotnet build ./src/linker.upnp -c release
- name: Pack
run: |
@@ -87,6 +88,7 @@ jobs:
dotnet pack ./src/linker.tun -c release
dotnet pack ./src/linker.nat -c release
dotnet pack ./src/linker.tunnel -c release
dotnet pack ./src/linker.upnp -c release
- name: Push
run: |
@@ -122,3 +124,4 @@ jobs:
dotnet nuget push ./src/linker.tun/bin/release/linker.tun.2.0.0.nupkg --source https://api.nuget.org/v3/index.json --skip-duplicate --api-key ${{ secrets.NUGET_KEY }} --no-symbols
dotnet nuget push ./src/linker.nat/bin/release/linker.nat.2.0.0.nupkg --source https://api.nuget.org/v3/index.json --skip-duplicate --api-key ${{ secrets.NUGET_KEY }} --no-symbols
dotnet nuget push ./src/linker.tunnel/bin/release/linker.tunnel.2.0.0.nupkg --source https://api.nuget.org/v3/index.json --skip-duplicate --api-key ${{ secrets.NUGET_KEY }} --no-symbols
dotnet nuget push ./src/linker.upnp/bin/release/linker.upnp.2.0.0.nupkg --source https://api.nuget.org/v3/index.json --skip-duplicate --api-key ${{ secrets.NUGET_KEY }} --no-symbols
+14
View File
@@ -73,6 +73,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "linker.messenger.wlist", "s
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "linker.messenger.node", "src\linker.messenger.node\linker.messenger.node.csproj", "{96BBC3AB-E248-6EE8-4969-4623C5D62E12}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "linker.upnp", "src\linker.upnp\linker.upnp.csproj", "{E360590A-36A3-4454-B8A9-48F35F908C35}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -509,6 +511,18 @@ Global
{96BBC3AB-E248-6EE8-4969-4623C5D62E12}.Release|x64.Build.0 = Release|Any CPU
{96BBC3AB-E248-6EE8-4969-4623C5D62E12}.Release|x86.ActiveCfg = Release|Any CPU
{96BBC3AB-E248-6EE8-4969-4623C5D62E12}.Release|x86.Build.0 = Release|Any CPU
{E360590A-36A3-4454-B8A9-48F35F908C35}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E360590A-36A3-4454-B8A9-48F35F908C35}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E360590A-36A3-4454-B8A9-48F35F908C35}.Debug|x64.ActiveCfg = Debug|Any CPU
{E360590A-36A3-4454-B8A9-48F35F908C35}.Debug|x64.Build.0 = Debug|Any CPU
{E360590A-36A3-4454-B8A9-48F35F908C35}.Debug|x86.ActiveCfg = Debug|Any CPU
{E360590A-36A3-4454-B8A9-48F35F908C35}.Debug|x86.Build.0 = Debug|Any CPU
{E360590A-36A3-4454-B8A9-48F35F908C35}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E360590A-36A3-4454-B8A9-48F35F908C35}.Release|Any CPU.Build.0 = Release|Any CPU
{E360590A-36A3-4454-B8A9-48F35F908C35}.Release|x64.ActiveCfg = Release|Any CPU
{E360590A-36A3-4454-B8A9-48F35F908C35}.Release|x64.Build.0 = Release|Any CPU
{E360590A-36A3-4454-B8A9-48F35F908C35}.Release|x86.ActiveCfg = Release|Any CPU
{E360590A-36A3-4454-B8A9-48F35F908C35}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
+3 -2
View File
@@ -1,8 +1,9 @@
v2.0.0
2026-02-28 15:31:05
2026-03-04 17:32:56
1. 一些累计更新,一些BUG修复
2. #92 松开打洞loading限制,允许选择中继节点
3. #89 windows下利用任务计划进行进程守护,定时检查服务
4. 优化客户端管理ws并发
5. 减少服务器推送,减轻主服务器压力
6. 优化中继,针对无法连接自己的外网IP的情况
6. 优化中继,针对无法连接自己的外网IP的情况
7. 优化upnp,自己实现了一遍
+3
View File
@@ -52,6 +52,7 @@ jobs:
dotnet build ./src/linker.tun -c release
dotnet build ./src/linker.nat -c release
dotnet build ./src/linker.tunnel -c release
dotnet build ./src/linker.upnp -c release
- name: Pack
run: |
@@ -87,6 +88,7 @@ jobs:
dotnet pack ./src/linker.tun -c release
dotnet pack ./src/linker.nat -c release
dotnet pack ./src/linker.tunnel -c release
dotnet pack ./src/linker.upnp -c release
- name: Push
run: |
@@ -122,3 +124,4 @@ jobs:
dotnet nuget push ./src/linker.tun/bin/release/linker.tun.{{version}}.nupkg --source https://api.nuget.org/v3/index.json --skip-duplicate --api-key ${{ secrets.NUGET_KEY }} --no-symbols
dotnet nuget push ./src/linker.nat/bin/release/linker.nat.{{version}}.nupkg --source https://api.nuget.org/v3/index.json --skip-duplicate --api-key ${{ secrets.NUGET_KEY }} --no-symbols
dotnet nuget push ./src/linker.tunnel/bin/release/linker.tunnel.{{version}}.nupkg --source https://api.nuget.org/v3/index.json --skip-duplicate --api-key ${{ secrets.NUGET_KEY }} --no-symbols
dotnet nuget push ./src/linker.upnp/bin/release/linker.upnp.{{version}}.nupkg --source https://api.nuget.org/v3/index.json --skip-duplicate --api-key ${{ secrets.NUGET_KEY }} --no-symbols
+23 -2
View File
@@ -42,6 +42,27 @@ namespace linker.libs
public static partial class NetworkHelper
{
private static readonly HashSet<System.Net.IPNetwork> privateNetworks = new HashSet<System.Net.IPNetwork>
{
// IPv4 私有网络
System.Net.IPNetwork.Parse("127.0.0.0/8"), // 回环
System.Net.IPNetwork.Parse("10.0.0.0/8"), // 私有A类
System.Net.IPNetwork.Parse("172.16.0.0/12"), // 私有B类
System.Net.IPNetwork.Parse("192.168.0.0/16"), // 私有C类
System.Net.IPNetwork.Parse("169.254.0.0/16"), // 链路本地
System.Net.IPNetwork.Parse("100.64.0.0/10"), // CGNAT (可选)
// IPv6 私有网络
System.Net.IPNetwork.Parse("fc00::/7"), // ULA
System.Net.IPNetwork.Parse("fe80::/10"), // 链路本地
System.Net.IPNetwork.Parse("::1/128"), // 回环
};
public static bool IsPrivateIP(IPAddress ip)
{
return privateNetworks.Any(network => network.Contains(ip));
}
public static IPEndPoint TransEndpointFamily(IPEndPoint ep)
{
if (ep.Address.AddressFamily == AddressFamily.InterNetworkV6 && ep.Address.IsIPv4MappedToIPv6)
@@ -172,7 +193,7 @@ namespace linker.libs
for (ushort i = 1; i < lines.Length; i++)
{
string ip = regex.Match(lines[i]).Groups[1].Value;
if (starts.Any(c => ip.ToString().StartsWith(c)))
if (IsPrivateIP(IPAddress.Parse(ip)))
{
result.Add(IPAddress.Parse(ip));
}
@@ -200,7 +221,7 @@ namespace linker.libs
continue;
}
if (starts.Any(c => reply.Address.ToString().StartsWith(c)))
if (IsPrivateIP(reply.Address))
{
result.Add(reply.Address);
}
+1 -1
View File
@@ -321,7 +321,7 @@ namespace linker.libs.web
}
catch (Exception ex)
{
LoggerHelper.Instance.Error($"{model.Path} -> {ex.Message}");
LoggerHelper.Instance.Error($"{model.Path} -> {ex}");
return new ApiControllerResponseInfo
{
Content = ex.Message,
+2 -2
View File
@@ -5,8 +5,8 @@ using linker.messenger.flow.webapi;
using linker.messenger.forward.proxy;
using linker.messenger.relay.server;
using linker.messenger.socks5;
using linker.messenger.tunnel;
using linker.messenger.tuntap;
using linker.messenger.tunnel.server;
using linker.messenger.tuntap.client;
using linker.plugins.sforward.proxy;
using Microsoft.Extensions.DependencyInjection;
namespace linker.messenger.flow
+1 -1
View File
@@ -1,5 +1,5 @@
using linker.libs;
using linker.messenger.tunnel;
using linker.messenger.tunnel.server;
namespace linker.messenger.flow
{
public sealed class FlowExternal : IFlow
+1 -1
View File
@@ -2,7 +2,7 @@
using linker.libs.extends;
using linker.libs.timer;
using linker.messenger.signin;
using linker.messenger.tunnel;
using linker.messenger.tunnel.client;
using System.Buffers;
using System.Collections.Concurrent;
using System.Net;
+1 -1
View File
@@ -3,8 +3,8 @@ using linker.libs.extends;
using linker.messenger.channel;
using linker.messenger.pcp;
using linker.messenger.signin;
using linker.messenger.tuntap;
using linker.messenger.tuntap.cidr;
using linker.messenger.tuntap.client;
using linker.tunnel;
using linker.tunnel.connection;
using System.Collections.Concurrent;
@@ -11,6 +11,7 @@ namespace linker.messenger.listen
{
public sealed class CountryTransfer
{
private string[] countryCodes = ["--","AF", "AX", "AL", "DZ", "AS", "AD", "AO", "AI", "AQ", "AG", "AR", "AM", "AW", "AU", "AT", "AZ",
"BS", "BH", "BD", "BB", "BY", "BE", "BZ", "BJ", "BM", "BT", "BO", "BQ", "BA", "BW", "BV", "BR", "IO", "BN", "BG", "BF", "BI",
"CV", "KH", "CM", "CA", "KY", "CF", "TD", "CL", "CN", "CX", "CC", "CO", "KM", "CG", "CD", "CK", "CR", "CI", "HR", "CU", "CW", "CY", "CZ",
@@ -64,15 +65,10 @@ namespace linker.messenger.listen
Span<byte> address = stackalloc byte[4];
ip.TryWriteBytes(address, out _);
if (address[0] == 127 || address[0] == 10
|| (address[0] == 172 && address[1] >= 16 && address[1] <= 31)
|| (address[0] == 192 && address[1] == 168)
|| (address[0] == 169 && address[1] == 254)
)
if (NetworkHelper.IsPrivateIP(ip) )
{
return true;
}
uint value = BinaryPrimitives.ReadUInt32BigEndian(address);
if (ipCaches.TryGetValue(value, out bool result))
{
@@ -43,6 +43,7 @@ namespace linker.messenger.serializer.memorypack
MemoryPackFormatterProvider.Register(new TunnelInterfaceInfoFormatter());
MemoryPackFormatterProvider.Register(new TunnelNetInfoFormatter());
MemoryPackFormatterProvider.Register(new TunnelTransportItemSetInfoFormatter());
MemoryPackFormatterProvider.Register(new PortMappingInfoFormatter());
MemoryPackFormatterProvider.Register(new DecenterSyncInfoFormatter());
@@ -1,9 +1,11 @@
using linker.tunnel.connection;
using linker.messenger.tunnel;
using linker.tunnel.connection;
using linker.tunnel.transport;
using linker.tunnel.wanport;
using System.Net;
using linker.upnp;
using MemoryPack;
using linker.messenger.tunnel;
using System.Net;
using System.Net.Sockets;
namespace linker.messenger.serializer.memorypack
{
@@ -744,4 +746,92 @@ namespace linker.messenger.serializer.memorypack
value = wrapped.info;
}
}
[MemoryPackable]
public readonly partial struct SerializablePortMappingInfo
{
[MemoryPackIgnore]
public readonly PortMappingInfo info;
[MemoryPackInclude, MemoryPackAllowSerialize]
IPAddress ClientIp => info.ClientIp;
[MemoryPackInclude]
int PublicPort => info.PublicPort;
[MemoryPackInclude]
int PrivatePort => info.PrivatePort;
[MemoryPackInclude]
ProtocolType ProtocolType => info.ProtocolType;
[MemoryPackInclude]
bool Enabled => info.Enabled;
[MemoryPackInclude]
string Description => info.Description;
[MemoryPackInclude]
int LeaseDuration => info.LeaseDuration;
[MemoryPackInclude]
DeviceType DeviceType => info.DeviceType;
[MemoryPackInclude]
bool Deletable => info.Deletable;
[MemoryPackConstructor]
SerializablePortMappingInfo(IPAddress clientIp, int publicPort, int privatePort, ProtocolType protocolType, bool enabled, string description, int leaseDuration, DeviceType deviceType, bool deletable)
{
var info = new PortMappingInfo
{
ClientIp = clientIp,
PublicPort = publicPort,
PrivatePort = privatePort,
ProtocolType = protocolType,
Enabled = enabled,
Description = description,
LeaseDuration = leaseDuration,
DeviceType = deviceType,
Deletable = deletable
};
this.info = info;
}
public SerializablePortMappingInfo(PortMappingInfo info)
{
this.info = info;
}
}
public class PortMappingInfoFormatter : MemoryPackFormatter<PortMappingInfo>
{
public override void Serialize<TBufferWriter>(ref MemoryPackWriter<TBufferWriter> writer, scoped ref PortMappingInfo value)
{
if (value == null)
{
writer.WriteNullObjectHeader();
return;
}
writer.WritePackable(new SerializablePortMappingInfo(value));
}
public override void Deserialize(ref MemoryPackReader reader, scoped ref PortMappingInfo value)
{
if (reader.PeekIsNull())
{
reader.Advance(1); // skip null block
value = null;
return;
}
value = new PortMappingInfo();
reader.TryReadObjectHeader(out byte count);
value.ClientIp = reader.ReadValue<IPAddress>();
value.PublicPort = reader.ReadValue<int>();
value.PrivatePort = reader.ReadValue<int>();
value.ProtocolType = reader.ReadValue<ProtocolType>();
value.Enabled = reader.ReadValue<bool>();
value.Description = reader.ReadValue<string>();
value.LeaseDuration = reader.ReadValue<int>();
value.DeviceType = reader.ReadValue<DeviceType>();
value.Deletable = reader.ReadValue<bool>();
}
}
}
+2 -2
View File
@@ -37,8 +37,8 @@ using linker.messenger.store.file.updater;
using linker.messenger.store.file.wakeup;
using linker.messenger.store.file.wlist;
using linker.messenger.sync;
using linker.messenger.tunnel;
using linker.messenger.tuntap;
using linker.messenger.tunnel.client;
using linker.messenger.tuntap.client;
using linker.messenger.tuntap.lease;
using linker.messenger.updater;
using linker.messenger.wakeup;
@@ -1,5 +1,6 @@
using linker.libs;
using linker.messenger.tunnel;
using linker.messenger.tunnel.client;
using linker.tunnel.transport;
using System.Net;
@@ -1,4 +1,5 @@
using linker.messenger.tuntap;
using linker.messenger.tuntap.client;
namespace linker.messenger.store.file.tuntap
{
+2
View File
@@ -5,6 +5,8 @@ using Microsoft.Extensions.DependencyInjection;
using linker.messenger.signin.args;
using linker.messenger.sync;
using linker.libs.web;
using linker.messenger.tunnel.server;
using linker.messenger.tunnel.client;
namespace linker.messenger.tunnel
{
public static class Entry
@@ -2,6 +2,10 @@
using linker.tunnel.transport;
using linker.libs;
using linker.messenger.signin;
using linker.messenger.tunnel.client;
using linker.libs.extends;
using linker.upnp;
using System.Net.Sockets;
namespace linker.messenger.tunnel
{
@@ -123,6 +127,32 @@ namespace linker.messenger.tunnel
await tunnelMessengerAdapter.SetTunnelTransports(info.MachineId, info.Data);
connection.Write(Helper.TrueArray);
}
[MessengerId((ushort)TunnelMessengerIds.UpnpGet)]
public async Task UpnpGet(IConnection connection)
{
uint requestid = connection.ReceiveRequestWrap.RequestId;
connection.Write(serializer.Serialize(tunnelNetworkTransfer.GetMapping()));
}
[MessengerId((ushort)TunnelMessengerIds.UpnpGetLocal)]
public async Task UpnpGetLocal(IConnection connection)
{
connection.Write(serializer.Serialize(tunnelNetworkTransfer.GetMappingLocal()));
}
[MessengerId((ushort)TunnelMessengerIds.UpnpAdd)]
public async Task UpnpAdd(IConnection connection)
{
KeyValuePair<string, PortMappingInfo> info = serializer.Deserialize<KeyValuePair<string, PortMappingInfo>>(connection.ReceiveRequestWrap.Payload.Span);
_ = tunnelNetworkTransfer.AddMapping(info.Value).ConfigureAwait(false);
}
[MessengerId((ushort)TunnelMessengerIds.UpnpDel)]
public async Task UpnpDel(IConnection connection)
{
KeyValuePair<string, KeyValuePair<int, ProtocolType>> info = serializer.Deserialize<KeyValuePair<string, KeyValuePair<int, ProtocolType>>>(connection.ReceiveRequestWrap.Payload.Span);
_ = tunnelNetworkTransfer.DelMapping(info.Value.Key, info.Value.Value).ConfigureAwait(false);
}
}
/// <summary>
@@ -321,5 +351,103 @@ namespace linker.messenger.tunnel
});
}
}
[MessengerId((ushort)TunnelMessengerIds.UpnpGetForward)]
public void UpnpGetForward(IConnection connection)
{
string machineid = serializer.Deserialize<string>(connection.ReceiveRequestWrap.Payload.Span);
if (signCaching.TryGet(connection.Id, machineid, out SignCacheInfo from, out SignCacheInfo to))
{
uint requestid = connection.ReceiveRequestWrap.RequestId;
_ = messengerSender.SendReply(new MessageRequestWrap
{
Connection = to.Connection,
MessengerId = (ushort)TunnelMessengerIds.UpnpGet,
}).ContinueWith(async (result) =>
{
if (result.Result.Code == MessageResponeCodes.OK && result.Result.Data.Length > 0)
{
await messengerSender.ReplyOnly(new MessageResponseWrap
{
Connection = connection,
Payload = result.Result.Data,
RequestId = requestid,
}, (ushort)TunnelMessengerIds.UpnpGetForward).ConfigureAwait(false);
}
});
}
}
[MessengerId((ushort)TunnelMessengerIds.UpnpGetLocalForward)]
public void UpnpGetLocalForward(IConnection connection)
{
string machineid = serializer.Deserialize<string>(connection.ReceiveRequestWrap.Payload.Span);
if (signCaching.TryGet(connection.Id, machineid, out SignCacheInfo from, out SignCacheInfo to))
{
uint requestid = connection.ReceiveRequestWrap.RequestId;
_ = messengerSender.SendReply(new MessageRequestWrap
{
Connection = to.Connection,
MessengerId = (ushort)TunnelMessengerIds.UpnpGetLocal,
}).ContinueWith(async (result) =>
{
if (result.Result.Code == MessageResponeCodes.OK && result.Result.Data.Length > 0)
{
await messengerSender.ReplyOnly(new MessageResponseWrap
{
Connection = connection,
Payload = result.Result.Data,
RequestId = requestid,
}, (ushort)TunnelMessengerIds.UpnpGetLocalForward).ConfigureAwait(false);
}
});
}
}
[MessengerId((ushort)TunnelMessengerIds.UpnpAddForward)]
public void UpnpAddForward(IConnection connection)
{
KeyValuePair<string, PortMappingInfo> info = serializer.Deserialize<KeyValuePair<string, PortMappingInfo>>(connection.ReceiveRequestWrap.Payload.Span);
if (signCaching.TryGet(connection.Id, info.Key, out SignCacheInfo from, out SignCacheInfo to))
{
uint requestid = connection.ReceiveRequestWrap.RequestId;
_ = messengerSender.SendReply(new MessageRequestWrap
{
Connection = to.Connection,
MessengerId = (ushort)TunnelMessengerIds.UpnpAdd,
Payload = connection.ReceiveRequestWrap.Payload
}).ContinueWith(async (result) =>
{
await messengerSender.ReplyOnly(new MessageResponseWrap
{
Connection = connection,
Payload = Helper.TrueArray,
RequestId = requestid,
}, (ushort)TunnelMessengerIds.UpnpAddForward).ConfigureAwait(false);
});
}
}
[MessengerId((ushort)TunnelMessengerIds.UpnpDelForward)]
public void UpnpDelForward(IConnection connection)
{
KeyValuePair<string, KeyValuePair<int, ProtocolType>> info = serializer.Deserialize<KeyValuePair<string, KeyValuePair<int, ProtocolType>>>(connection.ReceiveRequestWrap.Payload.Span);
if (signCaching.TryGet(connection.Id, info.Key, out SignCacheInfo from, out SignCacheInfo to))
{
uint requestid = connection.ReceiveRequestWrap.RequestId;
_ = messengerSender.SendReply(new MessageRequestWrap
{
Connection = to.Connection,
MessengerId = (ushort)TunnelMessengerIds.UpnpDel,
Payload = connection.ReceiveRequestWrap.Payload
}).ContinueWith(async (result) =>
{
await messengerSender.ReplyOnly(new MessageResponseWrap
{
Connection = connection,
Payload = Helper.TrueArray,
RequestId = requestid,
}, (ushort)TunnelMessengerIds.UpnpDelForward).ConfigureAwait(false);
});
}
}
}
}
@@ -27,6 +27,16 @@
TransportSet = 2017,
TransportSetForward = 2018,
UpnpGet = 2019,
UpnpGetForward = 2020,
UpnpGetLocal = 2021,
UpnpGetLocalForward = 2022,
UpnpAdd = 2023,
UpnpAddForward = 2024,
UpnpDel = 2025,
UpnpDelForward = 2026,
None = 2099
}
}
@@ -1,6 +1,6 @@
using linker.tunnel.transport;
namespace linker.messenger.tunnel
namespace linker.messenger.tunnel.client
{
/// <summary>
/// 打洞排除IP
@@ -1,7 +1,7 @@
using linker.messenger.signin.args;
using linker.libs.extends;
namespace linker.messenger.tunnel
namespace linker.messenger.tunnel.client
{
public sealed class SignInArgsNet : ISignInArgsClient
{
@@ -1,14 +1,16 @@
using linker.tunnel.transport;
using linker.libs;
using linker.libs.extends;
using System.Collections.Concurrent;
using linker.messenger.signin;
using linker.libs;
using linker.messenger.api;
using linker.tunnel.connection;
using linker.tunnel;
using linker.libs.web;
using linker.messenger.api;
using linker.messenger.signin;
using linker.tunnel;
using linker.tunnel.connection;
using linker.tunnel.transport;
using linker.upnp;
using System.Collections.Concurrent;
using System.Net.Sockets;
namespace linker.messenger.tunnel
namespace linker.messenger.tunnel.client
{
/// <summary>
/// 管理接口
@@ -38,6 +40,80 @@ namespace linker.messenger.tunnel
this.tunnelNetworkTransfer = tunnelNetworkTransfer;
this.tunnelTransfer = tunnelTransfer;
this.tunnelMessengerAdapter = tunnelMessengerAdapter;
}
public async Task<List<PortMappingInfo>> GetMapping(ApiControllerParamsInfo param)
{
if (param.Content == signInClientStore.Id || string.IsNullOrWhiteSpace(param.Content))
{
return tunnelNetworkTransfer.GetMapping();
}
MessageResponeInfo resp = await messengerSender.SendReply(new MessageRequestWrap
{
Connection = signInClientState.Connection,
MessengerId = (ushort)TunnelMessengerIds.UpnpGetForward,
Payload = serializer.Serialize(param.Content)
}).ConfigureAwait(false);
if (resp.Code == MessageResponeCodes.OK && resp.Data.Length > 0)
{
return serializer.Deserialize<List<PortMappingInfo>>(resp.Data.Span);
}
return [];
}
public async Task<List<PortMappingInfo>> GetMappingLocal(ApiControllerParamsInfo param)
{
if (param.Content == signInClientStore.Id || string.IsNullOrWhiteSpace(param.Content))
{
return tunnelNetworkTransfer.GetMappingLocal();
}
MessageResponeInfo resp = await messengerSender.SendReply(new MessageRequestWrap
{
Connection = signInClientState.Connection,
MessengerId = (ushort)TunnelMessengerIds.UpnpGetLocalForward,
Payload = serializer.Serialize(param.Content)
}).ConfigureAwait(false);
if (resp.Code == MessageResponeCodes.OK && resp.Data.Length > 0)
{
return serializer.Deserialize<List<PortMappingInfo>>(resp.Data.Span);
}
return [];
}
public async Task<bool> AddMapping(ApiControllerParamsInfo param)
{
KeyValueInfo<string, PortMappingInfo> info = param.Content.DeJson<KeyValueInfo<string, PortMappingInfo>>();
if (info.Key == signInClientStore.Id || string.IsNullOrWhiteSpace(info.Key))
{
await tunnelNetworkTransfer.AddMapping(info.Value).ConfigureAwait(false);
return true;
}
MessageResponeInfo resp = await messengerSender.SendReply(new MessageRequestWrap
{
Connection = signInClientState.Connection,
MessengerId = (ushort)TunnelMessengerIds.UpnpAddForward,
Payload = serializer.Serialize(new KeyValuePair<string, PortMappingInfo>(info.Key, info.Value))
}).ConfigureAwait(false);
return resp.Code == MessageResponeCodes.OK && resp.Data.Span.SequenceEqual(Helper.TrueArray);
}
public async Task<bool> DelMapping(ApiControllerParamsInfo param)
{
KeyValueInfo<string, KeyValueInfo<int, ProtocolType>> info = param.Content.DeJson<KeyValueInfo<string, KeyValueInfo<int, ProtocolType>>>();
if (info.Key == signInClientStore.Id || string.IsNullOrWhiteSpace(info.Key))
{
await tunnelNetworkTransfer.DelMapping(info.Value.Key, info.Value.Value).ConfigureAwait(false);
return true;
}
MessageResponeInfo resp = await messengerSender.SendReply(new MessageRequestWrap
{
Connection = signInClientState.Connection,
MessengerId = (ushort)TunnelMessengerIds.UpnpDelForward,
Payload = serializer.Serialize(new KeyValuePair<string, KeyValuePair<int, ProtocolType>>(info.Key, new KeyValuePair<int, ProtocolType>(info.Value.Key, info.Value.Value)))
}).ConfigureAwait(false);
return resp.Code == MessageResponeCodes.OK && resp.Data.Span.SequenceEqual(Helper.TrueArray);
}
/// <summary>
@@ -176,7 +252,6 @@ namespace linker.messenger.tunnel
return resp.Code == MessageResponeCodes.OK && resp.Data.Span.SequenceEqual(Helper.TrueArray);
}
public async Task<TunnelLocalNetworkInfo> GetNetwork(ApiControllerParamsInfo param)
{
if (param.Content == signInClientStore.Id)
@@ -1,6 +1,6 @@
using linker.tunnel.transport;
namespace linker.messenger.tunnel
namespace linker.messenger.tunnel.client
{
/// <summary>
/// 打洞排除IP
@@ -6,7 +6,7 @@ using System.Security.Cryptography.X509Certificates;
using linker.messenger.signin;
using linker.messenger.decenter;
namespace linker.messenger.tunnel
namespace linker.messenger.tunnel.client
{
/// <summary>
/// 打洞信标适配存储
@@ -2,7 +2,7 @@
using linker.messenger.decenter;
using linker.messenger.signin;
using System.Collections.Concurrent;
namespace linker.messenger.tunnel
namespace linker.messenger.tunnel.client
{
public sealed class TunnelDecenter : IDecenter
{
@@ -1,8 +1,7 @@

using linker.messenger.exroute;
using linker.messenger.exroute;
using System.Net;
namespace linker.messenger.tunnel
namespace linker.messenger.tunnel.client
{
public sealed class TunnelExRoute : IExRoute
{
@@ -1,10 +1,12 @@
using linker.libs;
using linker.libs.extends;
using linker.libs.timer;
using linker.messenger.decenter;
using linker.messenger.signin;
using linker.messenger.tunnel.stun.client;
using linker.messenger.tunnel.stun.enums;
using linker.tunnel;
using linker.upnp;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Quic;
@@ -12,7 +14,7 @@ using System.Net.Sockets;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
namespace linker.messenger.tunnel
namespace linker.messenger.tunnel.client
{
public sealed class TunnelNetworkTransfer
{
@@ -24,7 +26,7 @@ namespace linker.messenger.tunnel
public Action OnChange { get; set; } = () => { };
public TunnelNetworkTransfer(ISignInClientStore signInClientStore, SignInClientState signInClientState, ITunnelClientStore tunnelClientStore, IMessengerSender messengerSender, ISerializer serializer, TunnelTransfer tunnelTransfer)
public TunnelNetworkTransfer(ISignInClientStore signInClientStore, SignInClientState signInClientState, ITunnelClientStore tunnelClientStore, IMessengerSender messengerSender, ISerializer serializer, TunnelTransfer tunnelTransfer, CounterDecenter counterDecenter)
{
this.signInClientStore = signInClientStore;
this.signInClientState = signInClientState;
@@ -32,7 +34,6 @@ namespace linker.messenger.tunnel
this.messengerSender = messengerSender;
this.serializer = serializer;
signInClientState.OnSignInSuccessBefore += async () => { RefreshRouteLevel(); tunnelTransfer.Refresh(); await Task.CompletedTask; };
TestQuic();
@@ -40,7 +41,35 @@ namespace linker.messenger.tunnel
RefreshRouteLevel();
GetNet();
PortMappingUtility.StartDiscovery();
PortMappingUtility.OnChange += () =>
{
counterDecenter.SetValue("upnp-d", PortMappingUtility.DeviceCount);
counterDecenter.SetValue("upnp-r", PortMappingUtility.MappingCount);
counterDecenter.SetValue("upnp-l", PortMappingUtility.LocalMappingCount);
counterDecenter.SetValue("upnp-w", PortMappingUtility.WanCount);
};
}
public List<PortMappingInfo> GetMapping()
{
return PortMappingUtility.Get();
}
public List<PortMappingInfo> GetMappingLocal()
{
return PortMappingUtility.GetLocal();
}
public async Task AddMapping(PortMappingInfo mapping)
{
await PortMappingUtility.Add(mapping).ConfigureAwait(false);
}
public async Task DelMapping(int publicPort, ProtocolType ProtocolType)
{
await PortMappingUtility.Delete(publicPort, ProtocolType).ConfigureAwait(false);
}
/// <summary>
/// 刷新网关等级数据
/// </summary>
@@ -66,7 +95,7 @@ namespace linker.messenger.tunnel
try
{
using HttpClient httpClient = new HttpClient();
string str = await httpClient.GetStringAsync($"http://ip-api.com/json",cts.Token).ConfigureAwait(false);
string str = await httpClient.GetStringAsync($"http://ip-api.com/json", cts.Token).ConfigureAwait(false);
if (string.IsNullOrWhiteSpace(str) == false)
{
@@ -91,7 +120,7 @@ namespace linker.messenger.tunnel
try
{
using HttpClient httpClient = new HttpClient();
string str = await httpClient.GetStringAsync($"https://api.myip.la/en?json",cts.Token).ConfigureAwait(false);
string str = await httpClient.GetStringAsync($"https://api.myip.la/en?json", cts.Token).ConfigureAwait(false);
if (string.IsNullOrWhiteSpace(str) == false)
{
@@ -140,7 +169,6 @@ namespace linker.messenger.tunnel
}
return false;
}
private void GetNet()
{
TimerHelper.Async(async () =>
@@ -182,6 +210,7 @@ namespace linker.messenger.tunnel
Routes = tunnelClientStore.Network.RouteIPs,
};
}
private static byte[] ipv6LocalBytes = new byte[] { 254, 128, 0, 0, 0, 0, 0, 0 };
private TunnelInterfaceInfo[] GetInterfaces()
{
@@ -3,18 +3,16 @@ using linker.messenger.sync;
using linker.tunnel;
using linker.tunnel.transport;
namespace linker.messenger.tunnel
namespace linker.messenger.tunnel.client
{
public sealed class TunnelSyncTransports : ISync
{
public string Name => "TunnelTransports";
private readonly ITunnelClientStore tunnelClientStore;
private readonly ISerializer serializer;
private readonly ITunnelMessengerAdapter tunnelMessengerAdapter;
public TunnelSyncTransports(ITunnelClientStore tunnelClientStore, ISerializer serializer, ITunnelMessengerAdapter tunnelMessengerAdapter)
public TunnelSyncTransports(ISerializer serializer, ITunnelMessengerAdapter tunnelMessengerAdapter)
{
this.tunnelClientStore = tunnelClientStore;
this.serializer = serializer;
this.tunnelMessengerAdapter = tunnelMessengerAdapter;
}
@@ -5,7 +5,7 @@ using linker.libs.extends;
using linker.libs;
using System.Text;
namespace linker.messenger.tunnel
namespace linker.messenger.tunnel.server
{
/// <summary>
/// 外网端口处理器
+2 -1
View File
@@ -4,8 +4,9 @@ using linker.libs.web;
using linker.messenger.decenter;
using linker.messenger.exroute;
using linker.messenger.signin;
using linker.messenger.tunnel;
using linker.messenger.tunnel.client;
using linker.messenger.tuntap.cidr;
using linker.messenger.tuntap.client;
using linker.messenger.tuntap.lease;
using linker.messenger.tuntap.messenger;
using linker.nat;
@@ -1,5 +1,6 @@
using linker.messenger.exroute;
using linker.messenger.tunnel;
using linker.messenger.tunnel.client;
using linker.messenger.tuntap.client;
using linker.tunnel.transport;
using System.Net;
@@ -1,6 +1,7 @@
using linker.libs;
using linker.messenger.exroute;
using linker.messenger.signin;
using linker.messenger.tuntap.client;
using linker.tun.device;
using System.Net;
@@ -1,4 +1,5 @@
using linker.libs;
using linker.messenger.tuntap.client;
using System.Net;
namespace linker.messenger.tuntap.cidr
@@ -1,5 +1,4 @@

namespace linker.messenger.tuntap
namespace linker.messenger.tuntap.client
{
public interface ITuntapClientStore
{
@@ -1,5 +1,4 @@

namespace linker.messenger.tuntap
namespace linker.messenger.tuntap.client
{
public interface ITuntapSystemInformation
{
@@ -9,7 +9,7 @@ using linker.tunnel.connection;
using System.Net;
using static linker.nat.LinkerDstMapping;
namespace linker.messenger.tuntap
namespace linker.messenger.tuntap.client
{
public sealed class TuntapAdapter : ILinkerTunDeviceCallback, ITuntapProxyCallback
{
@@ -9,7 +9,7 @@ using linker.messenger.tuntap.messenger;
using linker.libs.web;
using linker.messenger.tuntap.cidr;
namespace linker.messenger.tuntap
namespace linker.messenger.tuntap.client
{
public sealed class TuntapApiController : IApiController
{
@@ -5,7 +5,7 @@ using linker.messenger.signin;
using linker.messenger.tuntap.lease;
using linker.libs.timer;
namespace linker.messenger.tuntap
namespace linker.messenger.tuntap.client
{
public sealed class TuntapConfigTransfer
{
@@ -5,7 +5,7 @@ using linker.messenger.signin;
using System.Net;
using linker.libs.timer;
namespace linker.messenger.tuntap
namespace linker.messenger.tuntap.client
{
public sealed class TuntapDecenter : IDecenter
{
@@ -7,7 +7,7 @@ using linker.libs.extends;
using linker.messenger.signin;
using linker.libs.timer;
namespace linker.messenger.tuntap
namespace linker.messenger.tuntap.client
{
public sealed class TuntapPingTransfer
{
@@ -9,7 +9,7 @@ using linker.tunnel;
using linker.tunnel.connection;
using System.Buffers.Binary;
namespace linker.messenger.tuntap
namespace linker.messenger.tuntap.client
{
public interface ITuntapProxyCallback
{
@@ -5,7 +5,7 @@ using linker.libs.timer;
using static linker.nat.LinkerDstMapping;
using linker.tun.device;
namespace linker.messenger.tuntap
namespace linker.messenger.tuntap.client
{
public sealed class TuntapTransfer
{
@@ -1,6 +1,7 @@
using linker.libs;
using linker.messenger.signin;
using linker.messenger.tuntap.cidr;
using linker.messenger.tuntap.client;
using linker.messenger.tuntap.lease;
namespace linker.messenger.tuntap.messenger
@@ -31,7 +31,7 @@
UpdateServer170 = 2617,
UpdateServer184 = 2619,
UpdateServer186 = 2020,
UpdateServer186 = 2620,
Max = 2299
}
+17 -85
View File
@@ -1,7 +1,6 @@
using linker.libs.timer;
using linker.tunnel.transport;
using Mono.Nat;
using System.Collections.Concurrent;
using linker.tunnel.transport;
using linker.upnp;
using System.Net.Sockets;
namespace linker.tunnel
{
@@ -10,10 +9,6 @@ namespace linker.tunnel
/// </summary>
public sealed class TunnelUpnpTransfer
{
private readonly SemaphoreSlim locker = new SemaphoreSlim(1, 1);
private readonly ConcurrentDictionary<NatProtocol, INatDevice> natDevices = new ConcurrentDictionary<NatProtocol, INatDevice>();
public MapInfo MapInfo { get; private set; }
public MapInfo MapInfo1 { get; private set; }
@@ -26,78 +21,7 @@ namespace linker.tunnel
this.transportUdpPortMap = transportUdpPortMap;
this.transportTcpPortMap = transportTcpPortMap;
NatUtility.DeviceFound += DeviceFound;
NatUtility.StartDiscovery();
LoopDiscovery();
}
private static void LoopDiscovery()
{
TimerHelper.SetIntervalLong(() =>
{
NatUtility.StopDiscovery();
NatUtility.StartDiscovery();
}, 60 * 1000);
}
private void DeviceFound(object sender, DeviceEventArgs args)
{
INatDevice device = args.Device;
natDevices.AddOrUpdate(device.NatProtocol, device, (a, b) => device);
AddMap();
}
private void AddMap()
{
if (natDevices.IsEmpty || MapInfo == null) return;
TimerHelper.Async(async () =>
{
await locker.WaitAsync().ConfigureAwait(false);
foreach (var device in natDevices.Values)
{
try
{
if (await HasMap(device, Protocol.Tcp, MapInfo.PublicPort).ConfigureAwait(false) == false)
{
Mapping mapping = new Mapping(Protocol.Tcp, MapInfo.PrivatePort, MapInfo.PublicPort, 7 * 24 * 60 * 60, $"linker-tcp-{MapInfo.PublicPort}-{MapInfo.PrivatePort}");
await device.CreatePortMapAsync(mapping).ConfigureAwait(false);
Mapping m = await device.GetSpecificMappingAsync(Protocol.Tcp, mapping.PublicPort).ConfigureAwait(false);
}
}
catch
{
}
try
{
if (await HasMap(device, Protocol.Udp, MapInfo.PublicPort).ConfigureAwait(false) == false)
{
Mapping mapping = new Mapping(Protocol.Udp, MapInfo.PrivatePort, MapInfo.PublicPort, 7 * 24 * 60 * 60, $"linker-udp-{MapInfo.PublicPort}-{MapInfo.PrivatePort}");
await device.CreatePortMapAsync(mapping).ConfigureAwait(false);
Mapping m = await device.GetSpecificMappingAsync(Protocol.Udp, mapping.PublicPort).ConfigureAwait(false);
}
}
catch
{
}
}
locker.Release();
});
}
private async Task<bool> HasMap(INatDevice device, Protocol protocol, int publicPort)
{
try
{
Mapping m = await device.GetSpecificMappingAsync(protocol, publicPort).ConfigureAwait(false);
return (DateTime.Now - m.Expiration).TotalSeconds > 2 * 60;
}
catch (Exception)
{
}
return false;
PortMappingUtility.StartDiscovery();
}
/// <summary>
@@ -107,10 +31,14 @@ namespace linker.tunnel
public void SetMap(int privatePort)
{
MapInfo = new MapInfo { PrivatePort = privatePort, PublicPort = privatePort };
AddMap();
_ = transportTcpPortMap.Listen(privatePort);
_ = transportUdpPortMap.Listen(privatePort);
PortMappingInfo tcp = new PortMappingInfo { PrivatePort = privatePort, PublicPort = privatePort, ProtocolType = ProtocolType.Tcp, Description = $"linker tunnel tcp", DeviceType = DeviceType.Pmp, LeaseDuration = 7 * 24 * 60 * 60, Deletable = false };
_ = PortMappingUtility.Add(tcp).ConfigureAwait(false);
PortMappingInfo udp = new PortMappingInfo { PrivatePort = privatePort, PublicPort = privatePort, ProtocolType = ProtocolType.Udp, Description = $"linker tunnel udp", DeviceType = DeviceType.Pmp, LeaseDuration = 7 * 24 * 60 * 60, Deletable = false };
_ = PortMappingUtility.Add(udp).ConfigureAwait(false);
_ = transportTcpPortMap.Listen(privatePort).ConfigureAwait(false);
_ = transportUdpPortMap.Listen(privatePort).ConfigureAwait(false);
}
/// <summary>
/// 设置端口映射,内网端口和外网端口不一样
@@ -120,9 +48,13 @@ namespace linker.tunnel
public void SetMap(int privatePort, int publicPort)
{
MapInfo1 = new MapInfo { PrivatePort = privatePort, PublicPort = publicPort };
PortMappingInfo tcp = new PortMappingInfo { PrivatePort = privatePort, PublicPort = publicPort, ProtocolType = ProtocolType.Tcp, Description = $"linker tunnel tcp", DeviceType = DeviceType.Pmp, LeaseDuration = 7 * 24 * 60 * 60, Deletable = false };
_ = PortMappingUtility.Add(tcp).ConfigureAwait(false);
PortMappingInfo udp = new PortMappingInfo { PrivatePort = privatePort, PublicPort = publicPort, ProtocolType = ProtocolType.Udp, Description = $"linker tunnel udp", DeviceType = DeviceType.Pmp, LeaseDuration = 7 * 24 * 60 * 60, Deletable = false };
_ = PortMappingUtility.Add(udp).ConfigureAwait(false);
_ = transportTcpPortMap.Listen(privatePort);
_ = transportUdpPortMap.Listen(privatePort);
_ = transportTcpPortMap.Listen(privatePort).ConfigureAwait(false);
_ = transportUdpPortMap.Listen(privatePort).ConfigureAwait(false);
}
}
+1 -1
View File
@@ -37,12 +37,12 @@
<DebugType>embedded</DebugType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Mono.Nat" Version="3.0.4" />
<PackageReference Include="System.IO.Pipelines" Version="9.0.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\linker.libs\linker.libs.csproj" />
<ProjectReference Include="..\linker.upnp\linker.upnp.csproj" />
</ItemGroup>
</Project>
@@ -1,5 +1,4 @@
using linker.libs;
using Mono.Nat;
using System.Net;
namespace linker.tunnel.wanport
+84
View File
@@ -0,0 +1,84 @@
using System.Net;
using System.Net.Sockets;
namespace linker.upnp
{
public enum DeviceType : byte
{
Upnp = 1,
Pmp = 2,
Pcp = 4,
All = 255
}
public sealed class PortMappingInfo
{
/// <summary>
/// 内网ip
/// </summary>
public IPAddress ClientIp { get; set; }
/// <summary>
/// 外网端口
/// </summary>
public int PublicPort { get; set; }
/// <summary>
/// 内网端口
/// </summary>
public int PrivatePort { get; set; }
/// <summary>
/// 协议
/// </summary>
public ProtocolType ProtocolType { get; set; }
/// <summary>
/// 启用
/// </summary>
public bool Enabled { get; set; } = true;
/// <summary>
/// 描述
/// </summary>
public string Description { get; set; }
/// <summary>
/// 存活
/// </summary>
public int LeaseDuration { get; set; }
/// <summary>
/// 设备类型
/// </summary>
public DeviceType DeviceType { get; set; } = DeviceType.All;
public bool Deletable { get; set; } = true;
public override string ToString()
{
return $"外网[*:{PublicPort}]->内网[{ClientIp}:{PrivatePort}]->启用:{Enabled}->协议:{ProtocolType}->存活:{LeaseDuration}->描述:{Description}";
}
}
public interface IPortMappingDevice
{
/// <summary>
/// 类型
/// </summary>
public DeviceType Type { get; }
public IPAddress GatewayIp { get; set; }
public IPAddress WanIp { get; set; }
/// <summary>
/// 获取设备所有映射
/// </summary>
/// <returns></returns>
public Task<List<PortMappingInfo>> Get();
/// <summary>
/// 添加映射
/// </summary>
/// <param name="mapping"></param>
/// <returns></returns>
public Task<bool> Add(PortMappingInfo mapping);
/// <summary>
/// 删除映射
/// </summary>
/// <param name="publicPort"></param>
/// <param name="protocol"></param>
/// <returns></returns>
public Task<bool> Delete(int publicPort, ProtocolType protocol);
}
}
+42
View File
@@ -0,0 +1,42 @@
using System.Net.Sockets;
namespace linker.upnp
{
public interface IPortMappingService
{
public DeviceType Type { get; }
/// <summary>
/// 发现
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
public Task<List<IPortMappingDevice>> Discovery(CancellationToken token);
/// <summary>
/// 获取本类型的所有已发现的设备
/// </summary>
/// <returns></returns>
public List<IPortMappingDevice> GetDevices();
/// <summary>
/// 获取所有设备的所有映射信息
/// </summary>
/// <returns></returns>
public Task<List<PortMappingInfo>> Get();
/// <summary>
/// 添加一条映射
/// </summary>
/// <param name="mapping"></param>
/// <returns></returns>
public Task Add(PortMappingInfo mapping);
/// <summary>
/// 删除一条映射
/// </summary>
/// <param name="publicPort"></param>
/// <param name="protocol"></param>
/// <returns></returns>
public Task Delete(int publicPort, ProtocolType protocol);
}
}
+223
View File
@@ -0,0 +1,223 @@
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Runtime.InteropServices;
namespace linker.upnp
{
public sealed class PmpDevice : IPortMappingDevice
{
public DeviceType Type => DeviceType.Pmp;
public IPAddress GatewayIp { get; set; }
public IPAddress WanIp { get; set; }
public async Task<List<PortMappingInfo>> Get()
{
return [];
}
public async Task<bool> Add(PortMappingInfo mapping)
{
if ((mapping.DeviceType & Type) != Type) return false;
byte[] bytes = new byte[12];
bytes[0] = 0;
bytes[1] = (byte)(mapping.ProtocolType == ProtocolType.Tcp ? 2 : 1);
bytes[2] = 0;
bytes[3] = 0;
BinaryPrimitives.WriteUInt16BigEndian(bytes.AsSpan(4), (ushort)mapping.PrivatePort);
BinaryPrimitives.WriteUInt16BigEndian(bytes.AsSpan(6), (ushort)mapping.PublicPort);
BinaryPrimitives.WriteInt32BigEndian(bytes.AsSpan(8), mapping.LeaseDuration);
using UdpClient client = CreateClient();
try
{
for (int i = 0; i < 3; i++)
{
await client.SendAsync(bytes, bytes.Length, new IPEndPoint(GatewayIp, 5351)).ConfigureAwait(false);
}
var _cts = new CancellationTokenSource(3000);
while (_cts.IsCancellationRequested == false)
{
UdpReceiveResult result = await client.ReceiveAsync(_cts.Token).ConfigureAwait(false);
byte version = result.Buffer[0];
byte opcode = result.Buffer[1];
ushort code = BinaryPrimitives.ReadUInt16BigEndian(result.Buffer.AsSpan(2));
uint time = BinaryPrimitives.ReadUInt32BigEndian(result.Buffer.AsSpan(4));
ushort privatePort = BinaryPrimitives.ReadUInt16BigEndian(result.Buffer.AsSpan(8));
ushort publicPort = BinaryPrimitives.ReadUInt16BigEndian(result.Buffer.AsSpan(10));
uint lease = BinaryPrimitives.ReadUInt32BigEndian(result.Buffer.AsSpan(12));
if (code == 0)
{
return true;
}
}
}
catch (Exception)
{
}
client?.Dispose();
return false;
}
public async Task<bool> Delete(int publicPort, ProtocolType protocol)
{
return true;
}
private UdpClient CreateClient()
{
UdpClient client = new UdpClient();
client.Client.SetSocketOption(SocketOptionLevel.Socket,
SocketOptionName.ReuseAddress, true);
client.Client.Bind(new IPEndPoint(IPAddress.Any, 5350));
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
try
{
const uint IOC_IN = 0x80000000;
int IOC_VENDOR = 0x18000000;
int SIO_UDP_CONNRESET = (int)(IOC_IN | IOC_VENDOR | 12);
client.Client.IOControl((int)SIO_UDP_CONNRESET, new byte[] { Convert.ToByte(false) }, null);
}
catch (Exception)
{
}
}
return client;
}
public override string ToString()
{
return $"PMP:{GatewayIp}->{WanIp}";
}
}
public sealed class PortMappingPmpService : IPortMappingService
{
public DeviceType Type => DeviceType.Pmp;
private readonly ConcurrentDictionary<string, IPortMappingDevice> pmpDevices = new();
public async Task<List<IPortMappingDevice>> Discovery(CancellationToken token)
{
try
{
List<Task<PmpDevice>> tasks = GetGatewayIp().Select(async (ip) =>
{
using UdpClient client = CreateClient();
try
{
for (int i = 0; i < 3; i++)
{
await client.SendAsync([0, 0], 2, new IPEndPoint(ip, 5351)).ConfigureAwait(false);
}
var _cts = new CancellationTokenSource(3000);
while (token.IsCancellationRequested == false && _cts.Token.IsCancellationRequested == false)
{
UdpReceiveResult result = await client.ReceiveAsync(_cts.Token).ConfigureAwait(false);
byte version = result.Buffer[0];
byte opcode = result.Buffer[1];
ushort code = BinaryPrimitives.ReadUInt16BigEndian(result.Buffer.AsSpan(2));
uint time = BinaryPrimitives.ReadUInt32BigEndian(result.Buffer.AsSpan(4));
IPAddress wanip = new IPAddress(result.Buffer.AsSpan(8));
if (code == 0)
{
return new PmpDevice
{
GatewayIp = ip,
WanIp = wanip
};
}
}
}
catch (Exception)
{
}
client?.Dispose();
return null;
}).ToList();
var responses = await Task.WhenAll(tasks).ConfigureAwait(false);
foreach (var device in responses.Where(c => c != null))
{
string key = device.GatewayIp.ToString();
if(pmpDevices.TryGetValue(key,out IPortMappingDevice _device) == false)
{
_device = device;
pmpDevices.TryAdd(key, device);
}
}
return pmpDevices.Values.ToList();
}
catch (Exception)
{
}
return [];
}
private List<IPAddress> GetGatewayIp()
{
return NetworkInterface.GetAllNetworkInterfaces().Where(c => c.OperationalStatus == OperationalStatus.Up)
.SelectMany(c => c.GetIPProperties().GatewayAddresses)
.Select(c => c.Address).Where(c => c.AddressFamily == AddressFamily.InterNetwork)
.Where(c => c.Equals(IPAddress.Loopback) == false).ToList();
}
private UdpClient CreateClient()
{
UdpClient client = new UdpClient();
client.Client.SetSocketOption(SocketOptionLevel.Socket,
SocketOptionName.ReuseAddress, true);
client.Client.Bind(new IPEndPoint(IPAddress.Any, 5350));
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
try
{
const uint IOC_IN = 0x80000000;
int IOC_VENDOR = 0x18000000;
int SIO_UDP_CONNRESET = (int)(IOC_IN | IOC_VENDOR | 12);
client.Client.IOControl((int)SIO_UDP_CONNRESET, new byte[] { Convert.ToByte(false) }, null);
}
catch (Exception)
{
}
}
return client;
}
public List<IPortMappingDevice> GetDevices()
{
return pmpDevices.Values.ToList<IPortMappingDevice>();
}
public async Task<List<PortMappingInfo>> Get()
{
return [];
}
public async Task Add(PortMappingInfo mapping)
{
foreach (var device in pmpDevices.Values)
{
await device.Add(mapping).ConfigureAwait(false);
}
}
public async Task Delete(int publicPort, ProtocolType protocol)
{
foreach (var device in pmpDevices.Values)
{
await device.Delete(publicPort, protocol).ConfigureAwait(false);
}
}
}
}
+357
View File
@@ -0,0 +1,357 @@
using System.Collections.Concurrent;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Text;
using System.Xml;
namespace linker.upnp
{
public sealed class UpnpDevice : IPortMappingDevice
{
/// <summary>
/// 设备类型
/// </summary>
public DeviceType Type => DeviceType.Upnp;
public IPAddress GatewayIp { get; set; }
public IPAddress WanIp { get; set; }
/// <summary>
/// 服务类型描述
/// </summary>
public string ServiceType { get; set; }
/// <summary>
/// 控制地址
/// </summary>
public string ControlUrl { get; set; }
/// <summary>
/// 内网ip
/// </summary>
public IPAddress ClientIp { get; set; }
public async Task<List<PortMappingInfo>> Get()
{
List<PortMappingInfo> result = new List<PortMappingInfo>();
using HttpClient httpClient = new HttpClient();
for (int index = 0; ; index++)
{
string action = BuildGetPortMappingRequest(index);
try
{
using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, ControlUrl);
request.Headers.Add("SOAPACTION", $"\"{ServiceType}#GetGenericPortMappingEntry\"");
request.Content = new StringContent(action, Encoding.UTF8, "text/xml");
using HttpResponseMessage response = await httpClient.SendAsync(request).ConfigureAwait(false);
string responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
if (responseContent.Contains("UPnPError"))
{
break;
}
XmlDocument doc = new XmlDocument();
doc.LoadXml(responseContent);
XmlNamespaceManager nsmgr = new XmlNamespaceManager(doc.NameTable);
nsmgr.AddNamespace("s", "http://schemas.xmlsoap.org/soap/envelope/");
nsmgr.AddNamespace("u", "urn:schemas-upnp-org:service:WANIPConnection:1");
XmlNode resp = doc.SelectSingleNode("//u:GetGenericPortMappingEntryResponse", nsmgr);
result.Add(new PortMappingInfo
{
ClientIp = IPAddress.Parse(resp.SelectSingleNode("NewInternalClient").InnerText),
Description = resp.SelectSingleNode("NewPortMappingDescription").InnerText,
Enabled = resp.SelectSingleNode("NewEnabled").InnerText == "1",
LeaseDuration = int.Parse(resp.SelectSingleNode("NewLeaseDuration").InnerText),
PrivatePort = int.Parse(resp.SelectSingleNode("NewInternalPort").InnerText),
PublicPort = int.Parse(resp.SelectSingleNode("NewExternalPort").InnerText),
ProtocolType = resp.SelectSingleNode("NewProtocol").InnerText == "TCP" ? ProtocolType.Tcp : ProtocolType.Udp,
DeviceType = DeviceType.Upnp
});
}
catch (Exception)
{
}
}
return result;
}
public async Task<bool> Add(PortMappingInfo mapping)
{
if ((mapping.DeviceType & Type) != Type) return false;
string action = BuildAddPortMappingRequest(mapping);
using HttpClient httpClient = new HttpClient();
using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, ControlUrl);
request.Headers.Add("SOAPACTION", $"\"{ServiceType}#AddPortMapping\"");
request.Content = new StringContent(action, Encoding.UTF8, "text/xml");
using HttpResponseMessage response = await httpClient.SendAsync(request).ConfigureAwait(false);
string responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
return responseContent.Contains("AddPortMappingResponse");
}
public async Task<bool> Delete(int publicPort, ProtocolType protocol)
{
string action = BuildDeletePortMappingRequest(publicPort, protocol);
using HttpClient httpClient = new HttpClient();
using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, ControlUrl);
request.Headers.Add("SOAPACTION", $"\"{ServiceType}#DeletePortMapping\"");
request.Content = new StringContent(action, Encoding.UTF8, "text/xml");
using HttpResponseMessage response = await httpClient.SendAsync(request).ConfigureAwait(false);
string responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
return responseContent.Contains("DeletePortMappingResponse");
}
private string BuildAddPortMappingRequest(PortMappingInfo mapping)
{
if (mapping.ClientIp == null || mapping.ClientIp.Equals(IPAddress.Any))
{
mapping.ClientIp = ClientIp;
}
return $@"<?xml version=""1.0""?>
<s:Envelope xmlns:s=""http://schemas.xmlsoap.org/soap/envelope/""
s:encodingStyle=""http://schemas.xmlsoap.org/soap/encoding/"">
<s:Body>
<u:AddPortMapping xmlns:u=""{ServiceType}"">
<NewRemoteHost></NewRemoteHost>
<NewExternalPort>{mapping.PublicPort}</NewExternalPort>
<NewProtocol>{mapping.ProtocolType.ToString().ToUpper()}</NewProtocol>
<NewInternalPort>{mapping.PrivatePort}</NewInternalPort>
<NewInternalClient>{mapping.ClientIp}</NewInternalClient>
<NewEnabled>{(mapping.Enabled ? "1" : "0")}</NewEnabled>
<NewPortMappingDescription>{mapping.Description}</NewPortMappingDescription>
<NewLeaseDuration>{mapping.LeaseDuration}</NewLeaseDuration>
</u:AddPortMapping>
</s:Body>
</s:Envelope> ";
}
private string BuildDeletePortMappingRequest(int publicPort, ProtocolType protocol)
{
return $@"<?xml version=""1.0""?>
<s:Envelope xmlns:s=""http://schemas.xmlsoap.org/soap/envelope/""
s:encodingStyle=""http://schemas.xmlsoap.org/soap/encoding/"">
<s:Body>
<u:DeletePortMapping xmlns:u=""{ServiceType}"">
<NewRemoteHost></NewRemoteHost>
<NewExternalPort>{publicPort}</NewExternalPort>
<NewProtocol>{protocol.ToString().ToUpper()}</NewProtocol>
</u:DeletePortMapping>
</s:Body>
</s:Envelope>";
}
private string BuildGetPortMappingRequest(int index)
{
return $@"<?xml version=""1.0""?>
<s:Envelope xmlns:s=""http://schemas.xmlsoap.org/soap/envelope/""
s:encodingStyle=""http://schemas.xmlsoap.org/soap/encoding/"">
<s:Body>
<u:GetGenericPortMappingEntry xmlns:u=""{ServiceType}"">
<NewPortMappingIndex>{index}</NewPortMappingIndex>
</u:GetGenericPortMappingEntry>
</s:Body>
</s:Envelope>";
}
public override string ToString()
{
return $"类型:UPNP->客户端ip:{ClientIp}->控制地址:{ControlUrl}->服务描述:{ServiceType}";
}
}
public sealed class PortMappingUpnpService : IPortMappingService
{
public DeviceType Type => DeviceType.Upnp;
private readonly ConcurrentDictionary<string, IPortMappingDevice> upnpDevices = new();
private CancellationTokenSource cts;
private const string discoveryMessage =
"M-SEARCH * HTTP/1.1\r\n" +
"HOST: 239.255.255.250:1900\r\n" +
"MAN: \"ssdp:discover\"\r\n" +
"MX: 3\r\n" +
"ST: urn:schemas-upnp-org:device:InternetGatewayDevice:1\r\n" +
"USER-AGENT: UPnP/1.0\r\n" +
"\r\n";
private byte[] messageBytes = Encoding.ASCII.GetBytes(discoveryMessage);
public async Task<List<IPortMappingDevice>> Discovery(CancellationToken token)
{
try
{
List<Task<(string response, IPAddress ip, IPAddress gateway)>> tasks = GetLocalIp().Select(async (ip) =>
{
using UdpClient client = CreateClient(ip);
try
{
for (int i = 0; i < 3; i++)
{
await client.SendAsync(messageBytes, messageBytes.Length, new IPEndPoint(IPAddress.Parse("239.255.255.250"), 1900)).ConfigureAwait(false);
}
var _cts = new CancellationTokenSource(3000);
while (token.IsCancellationRequested == false && _cts.Token.IsCancellationRequested == false)
{
UdpReceiveResult result = await client.ReceiveAsync(_cts.Token).ConfigureAwait(false);
return (Encoding.ASCII.GetString(result.Buffer), ip, (result.RemoteEndPoint as IPEndPoint).Address);
}
}
catch (Exception)
{
}
client?.Dispose();
return (string.Empty, ip, IPAddress.Any);
}).ToList();
var responses = await Task.WhenAll(tasks).ConfigureAwait(false);
foreach (var response in responses.Where(c => string.IsNullOrWhiteSpace(c.response) == false))
{
UpnpDevice device = await PrarseDevice(response.response, response.ip, response.gateway).ConfigureAwait(false);
if (device != null)
{
if (upnpDevices.TryGetValue(device.ControlUrl, out IPortMappingDevice _device) == false)
{
_device = device;
upnpDevices.TryAdd(device.ControlUrl, device);
}
}
}
return upnpDevices.Values.ToList<IPortMappingDevice>();
}
catch (Exception)
{
}
return [];
}
private async Task<UpnpDevice> PrarseDevice(string respStr, IPAddress clientIp, IPAddress gateway)
{
try
{
int start = respStr.AsSpan().IndexOf("LOCATION: ");
int end = respStr.AsSpan().Slice(start + 10).IndexOf("\r\n");
string location = respStr.AsSpan().Slice(start + 10, end).ToString();
using HttpClient webClient = new HttpClient();
string resp = await webClient.GetStringAsync(location).ConfigureAwait(false);
XmlDocument doc = new XmlDocument();
doc.LoadXml(resp);
XmlNamespaceManager ns = new XmlNamespaceManager(doc.NameTable);
ns.AddNamespace("upnp", "urn:schemas-upnp-org:device-1-0");
var serviceNodes = doc.SelectNodes("//upnp:service", ns);
foreach (XmlNode service in serviceNodes)
{
string serviceType = service.SelectSingleNode("upnp:serviceType", ns)?.InnerText;
if (string.IsNullOrWhiteSpace(serviceType) || (serviceType.Contains("WANIPConnection") == false && serviceType.Contains("WANPPPConnection") == false))
{
continue;
}
string controlUrl = service.SelectSingleNode("upnp:controlURL", ns)?.InnerText;
if (string.IsNullOrWhiteSpace(controlUrl))
{
continue;
}
Uri baseUri = new Uri(location);
if (!controlUrl.StartsWith("http"))
{
controlUrl = new Uri(baseUri, controlUrl).ToString();
}
return new UpnpDevice
{
ServiceType = serviceType,
ControlUrl = controlUrl,
ClientIp = clientIp,
GatewayIp = gateway,
WanIp = gateway
};
}
}
catch (Exception)
{
}
return null;
}
private List<IPAddress> GetLocalIp()
{
return NetworkInterface.GetAllNetworkInterfaces().Where(c => c.OperationalStatus == OperationalStatus.Up)
.SelectMany(c => c.GetIPProperties().UnicastAddresses)
.Select(c => c.Address).Where(c => c.AddressFamily == AddressFamily.InterNetwork)
.Where(c => c.Equals(IPAddress.Loopback) == false).ToList();
}
private UdpClient CreateClient(IPAddress ip)
{
UdpClient client = new UdpClient();
client.Client.SetSocketOption(SocketOptionLevel.Socket,
SocketOptionName.ReuseAddress, true);
client.Client.Bind(new IPEndPoint(ip, 0));
client.JoinMulticastGroup(IPAddress.Parse("239.255.255.250"));
client.Client.SetSocketOption(SocketOptionLevel.IP,
SocketOptionName.MulticastTimeToLive, 4);
client.Client.SetSocketOption(SocketOptionLevel.IP,
SocketOptionName.MulticastLoopback, true);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
try
{
const uint IOC_IN = 0x80000000;
int IOC_VENDOR = 0x18000000;
int SIO_UDP_CONNRESET = (int)(IOC_IN | IOC_VENDOR | 12);
client.Client.IOControl((int)SIO_UDP_CONNRESET, new byte[] { Convert.ToByte(false) }, null);
}
catch (Exception)
{
}
}
return client;
}
/// <summary>
/// 获取所有已找到的upnp设备
/// </summary>
/// <returns></returns>
public List<IPortMappingDevice> GetDevices()
{
return upnpDevices.Values.ToList<IPortMappingDevice>();
}
/// <summary>
/// 获取所有upnp设备的所有映射信息
/// </summary>
/// <returns></returns>
public async Task<List<PortMappingInfo>> Get()
{
return (await Task.WhenAll(upnpDevices.Values.Select(c => c.Get()).ToList()).ConfigureAwait(false)).SelectMany(c => c).ToList();
}
/// <summary>
/// 添加一条映射
/// </summary>
/// <param name="mapping"></param>
/// <returns></returns>
public async Task Add(PortMappingInfo mapping)
{
foreach (var device in upnpDevices.Values)
{
await device.Add(mapping).ConfigureAwait(false);
}
}
/// <summary>
/// 删除一条映射
/// </summary>
/// <param name="publicPort"></param>
/// <param name="protocol"></param>
/// <returns></returns>
public async Task Delete(int publicPort, ProtocolType protocol)
{
foreach (var device in upnpDevices.Values)
{
await device.Delete(publicPort, protocol).ConfigureAwait(false);
}
}
}
}
+290
View File
@@ -0,0 +1,290 @@
using System.Collections.Concurrent;
using System.Net;
using System.Net.Sockets;
namespace linker.upnp
{
public static class PortMappingUtility
{
/// <summary>
/// 当发现设备
/// </summary>
public static event Action<IPortMappingDevice> OnDeviceFound
{
add => DeviceFound += value;
remove => DeviceFound -= value;
}
private static Action<IPortMappingDevice> DeviceFound = (device) => { };
/// <summary>
/// 当发生变化,发现设备,添加删除映射什么的
/// </summary>
public static event Action OnChange
{
add => Change += value;
remove => Change -= value;
}
private static Action Change = () => { };
/// <summary>
/// 设备数
/// </summary>
public static int DeviceCount => devices.Count;
/// <summary>
/// 映射数
/// </summary>
public static int MappingCount => mappings.Count;
/// <summary>
/// 本地数
/// </summary>
public static int LocalMappingCount => localMappings.Where(c => c.Value.Deleted == false).Count();
/// <summary>
/// 外网数量
/// </summary>
public static int WanCount => devices.Values.Count(c => IsPrivateIP(c.WanIp) == false);
private static readonly IPortMappingService[] services = [new PortMappingUpnpService(), new PortMappingPmpService()];
private static readonly ConcurrentDictionary<(IPAddress gateway, DeviceType deviceType), IPortMappingDevice> devices = new();
private static readonly ConcurrentDictionary<(int publicPort, ProtocolType protocolType), MappingCacheInfo> localMappings = new();
private static List<PortMappingInfo> mappings = [];
private static CancellationTokenSource cts;
private static CancellationTokenSource _cts;
private static long ticks = Environment.TickCount64;
/// <summary>
/// 开始发现设备
/// </summary>
/// <param name="deviceType"></param>
public static void StartDiscovery(DeviceType deviceType = DeviceType.All)
{
if (cts != null && cts.Token.IsCancellationRequested == false)
{
return;
}
cts = new CancellationTokenSource();
Task.Run(async () =>
{
try
{
while (cts.IsCancellationRequested == false)
{
var _services = services.Where(service => (service.Type & deviceType) == service.Type).Select(c => c.Discovery(cts.Token)).ToList();
await Task.WhenAll(_services).ConfigureAwait(false);
RefreshDevice();
await Add().ConfigureAwait(false);
await Delete().ConfigureAwait(false);
await RefreshMappings().ConfigureAwait(false);
await Delay().ConfigureAwait(false);
}
}
catch (Exception)
{
}
});
}
private static void RefreshDevice()
{
bool changed = false;
foreach (var device in services.Select(c => c.GetDevices()).ToList().SelectMany(c => c))
{
if (devices.TryGetValue((device.GatewayIp, device.Type), out IPortMappingDevice _device) == false)
{
changed = true;
devices.TryAdd((device.GatewayIp, device.Type), device);
DeviceFound?.Invoke(device);
}
}
if (changed)
{
Change?.Invoke();
}
}
private static async Task RefreshMappings()
{
int count = mappings.Count;
mappings = (await Task.WhenAll(services.Select(c => c.Get()).ToList()).ConfigureAwait(false))
.SelectMany(c => c)
.Where(c => localMappings.TryGetValue((c.PublicPort, c.ProtocolType), out MappingCacheInfo cache) == false || cache.Deleted == false)
.ToList();
if (mappings.Count != count)
{
Change?.Invoke();
}
}
private static async Task Delay()
{
_cts = new CancellationTokenSource();
try
{
int delay = Math.Max((int)Math.Min(30000, Environment.TickCount64 - ticks), 3000);
await Task.Delay(delay, _cts.Token).ConfigureAwait(false);
}
catch
{
}
finally
{
_cts.Cancel();
_cts = new CancellationTokenSource();
}
}
private static void RefreshDelay()
{
ticks = Environment.TickCount64;
_cts?.Cancel();
}
/// <summary>
/// 停止发现设备
/// </summary>
public static void StopDiscovery()
{
cts?.Cancel();
_cts?.Cancel();
}
/// <summary>
/// 获取所有已发现设备
/// </summary>
/// <returns></returns>
public static List<IPortMappingDevice> GetDevices()
{
RefreshDelay();
return services.Select(c => c.GetDevices()).ToList().SelectMany(c => c).ToList();
}
/// <summary>
/// 获取所有已发现设备的所有映射信息
/// </summary>
/// <returns></returns>
public static List<PortMappingInfo> Get()
{
RefreshDelay();
return mappings
.Where(c => localMappings.TryGetValue((c.PublicPort, c.ProtocolType), out MappingCacheInfo cache) == false || cache.Deleted == false)
.ToList();
}
/// <summary>
/// 获取本地添加的映射信息(不包含已删除的)
/// </summary>
/// <returns></returns>
public static List<PortMappingInfo> GetLocal()
{
RefreshDelay();
return localMappings.Values.Where(c => c.Deleted == false).Select(c => c.Info).ToList();
}
/// <summary>
/// 添加一条映射
/// </summary>
/// <param name="mapping"></param>
/// <returns></returns>
public static async Task Add(PortMappingInfo mapping)
{
MappingCacheInfo cache = new MappingCacheInfo { Info = mapping };
localMappings.AddOrUpdate((mapping.PublicPort, mapping.ProtocolType), cache, (k, v) => cache);
await AddInternal(cache.Info).ConfigureAwait(false);
RefreshDelay();
}
private static async Task Add()
{
foreach (var cache in localMappings.Values.Where(c => c.Deleted == false))
{
await AddInternal(cache.Info).ConfigureAwait(false);
}
}
private static async Task AddInternal(PortMappingInfo mapping)
{
for (int i = 0; i < services.Length; i++)
{
if ((services[i].Type & mapping.DeviceType) == services[i].Type && services[i].GetDevices().Count > 0)
{
await services[i].Add(mapping).ConfigureAwait(false);
}
}
Change?.Invoke();
}
/// <summary>
/// 删除一条映射
/// </summary>
/// <param name="publicPort"></param>
/// <param name="protocol"></param>
/// <returns></returns>
public static async Task Delete(int publicPort, ProtocolType protocol)
{
if (localMappings.TryGetValue((publicPort, protocol), out MappingCacheInfo cache))
{
cache.Deleted = true;
}
else
{
cache = new MappingCacheInfo { Deleted = true, Info = new PortMappingInfo { PublicPort = publicPort, ProtocolType = protocol } };
localMappings.TryAdd((publicPort, protocol), cache);
}
for (int i = 0; i < services.Length; i++)
{
if ((services[i].Type & cache.Info.DeviceType) == services[i].Type)
{
await services[i].Delete(publicPort, protocol).ConfigureAwait(false);
}
}
RefreshDelay();
Change?.Invoke();
}
private static async Task Delete()
{
foreach (var cache in localMappings.Values.Where(c => c.Deleted))
{
for (int i = 0; i < services.Length; i++)
{
if ((services[i].Type & cache.Info.DeviceType) == services[i].Type)
{
await services[i].Delete(cache.Info.PublicPort, cache.Info.ProtocolType).ConfigureAwait(false);
}
}
}
foreach (var key in localMappings.Where(c => c.Value.Deleted).Select(c => c.Key).ToList())
{
localMappings.TryRemove(key, out _);
}
}
sealed class MappingCacheInfo
{
public PortMappingInfo Info { get; set; }
public bool Deleted { get; set; }
//public HashSet<IPAddress> Added { get; set; } = new HashSet<IPAddress>();
}
private static readonly HashSet<IPNetwork> privateNetworks = new HashSet<IPNetwork>
{
// IPv4 私有网络
IPNetwork.Parse("127.0.0.0/8"), // 回环
IPNetwork.Parse("10.0.0.0/8"), // 私有A类
IPNetwork.Parse("172.16.0.0/12"), // 私有B类
IPNetwork.Parse("192.168.0.0/16"), // 私有C类
IPNetwork.Parse("169.254.0.0/16"), // 链路本地
IPNetwork.Parse("100.64.0.0/10"), // CGNAT (可选)
// IPv6 私有网络
IPNetwork.Parse("fc00::/7"), // ULA
IPNetwork.Parse("fe80::/10"), // 链路本地
IPNetwork.Parse("::1/128"), // 回环
};
private static bool IsPrivateIP(IPAddress ip)
{
return privateNetworks.Any(network => network.Contains(ip));
}
}
}
+39
View File
@@ -0,0 +1,39 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<PublishAot>false</PublishAot>
<JsonSerializerIsReflectionEnabledByDefault>true</JsonSerializerIsReflectionEnabledByDefault>
<Title>linker upnp</Title>
<Authors>snltty</Authors>
<Company>snltty</Company>
<Description>linker upnp</Description>
<Copyright>snltty</Copyright>
<PackageProjectUrl>https://github.com/snltty/linker</PackageProjectUrl>
<RepositoryUrl>https://github.com/snltty/linker</RepositoryUrl>
<PackageReleaseNotes>linker upnp</PackageReleaseNotes>
<Version>1.9.9</Version>
<AssemblyVersion>1.9.9</AssemblyVersion>
<FileVersion>1.9.9</FileVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DebugType>full</DebugType>
<DebugSymbols>true</DebugSymbols>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<DebugType>full</DebugType>
<DebugSymbols>true</DebugSymbols>
<Optimize>True</Optimize>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
</Project>
+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1772524795472" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5280" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="256"><path d="M704.38432 718.72c6.304-21.632 3.872-41.408-6.624-56.128-9.568-13.504-25.792-21.28-45.28-22.208l-369.472-4.8h-0.096a6.688 6.688 0 0 1-5.568-3.008l-0.032-0.032a8.192 8.192 0 0 1-0.896-6.624 10.24 10.24 0 0 1 8.672-6.656l372.736-4.8c44.16-2.08 92.16-37.824 108.96-81.632l21.28-55.52a11.584 11.584 0 0 0 0.64-7.168 242.4 242.4 0 0 0-236.8-189.664 242.656 242.656 0 0 0-229.856 164.768 110.176 110.176 0 0 0-76.544-21.28 109.28 109.28 0 0 0-94.816 135.648 155.04 155.04 0 0 0-150.656 155.168c0 7.456 0.608 15.008 1.504 22.496a7.456 7.456 0 0 0 7.2 6.304h681.888a9.312 9.312 0 0 0 8.672-6.624z m117.6-237.376c-3.296 0-6.88 0-10.176 0.48-2.4 0-4.512 1.76-5.408 4.16l-14.4 50.112c-6.304 21.632-3.904 41.408 6.592 56.16 9.632 13.504 25.824 21.248 45.344 22.176l78.656 4.832c2.368 0 4.512 1.12 5.664 3.008a8.64 8.64 0 0 1 0.928 6.656 10.24 10.24 0 0 1-8.704 6.624l-81.952 4.8c-44.416 2.08-92.096 37.824-108.928 81.664l-5.984 15.296c-1.216 3.04 0.928 6.048 4.192 6.048h281.504a7.36 7.36 0 0 0 7.2-5.376c4.8-17.408 7.488-35.712 7.488-54.624 0-111.04-90.656-201.696-202.016-201.696z" fill="#F38020" p-id="5281"></path></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1772610299623" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="14439" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="256"><path d="M789.333333 264.533333l-217.6-115.2c-38.4-21.333333-81.066667-21.333333-119.466666 0L234.666667 264.533333c-42.666667 21.333333-68.266667 64-68.266667 110.933334v226.133333c0 46.933333 25.6 89.6 68.266667 110.933333l217.6 115.2c38.4 21.333333 81.066667 21.333333 119.466666 0l217.6-115.2c42.666667-21.333333 68.266667-64 68.266667-110.933333V375.466667c0-46.933333-25.6-89.6-68.266667-110.933334z m-298.666666-38.4c12.8-8.533333 25.6-8.533333 38.4 0l187.733333 102.4L512 422.4 311.466667 320l179.2-93.866667zM469.333333 746.666667L277.333333 640c-12.8-8.533333-21.333333-21.333333-21.333333-38.4v-213.333333l217.6 106.666666v251.733334z m29.866667 12.8h21.333333c-4.266667 4.266667-12.8 4.266667-21.333333 0z m273.066667-153.6c0 17.066667-8.533333 29.866667-21.333334 38.4L554.666667 746.666667v-247.466667l217.6-98.133333v204.8z" fill="#008000" p-id="14440"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1772610299623" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="14439" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="256"><path d="M789.333333 264.533333l-217.6-115.2c-38.4-21.333333-81.066667-21.333333-119.466666 0L234.666667 264.533333c-42.666667 21.333333-68.266667 64-68.266667 110.933334v226.133333c0 46.933333 25.6 89.6 68.266667 110.933333l217.6 115.2c38.4 21.333333 81.066667 21.333333 119.466666 0l217.6-115.2c42.666667-21.333333 68.266667-64 68.266667-110.933333V375.466667c0-46.933333-25.6-89.6-68.266667-110.933334z m-298.666666-38.4c12.8-8.533333 25.6-8.533333 38.4 0l187.733333 102.4L512 422.4 311.466667 320l179.2-93.866667zM469.333333 746.666667L277.333333 640c-12.8-8.533333-21.333333-21.333333-21.333333-38.4v-213.333333l217.6 106.666666v251.733334z m29.866667 12.8h21.333333c-4.266667 4.266667-12.8 4.266667-21.333333 0z m273.066667-153.6c0 17.066667-8.533333 29.866667-21.333334 38.4L554.666667 746.666667v-247.466667l217.6-98.133333v204.8z" fill="#da790d" p-id="14440"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1772610299623" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="14439" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="256"><path d="M789.333333 264.533333l-217.6-115.2c-38.4-21.333333-81.066667-21.333333-119.466666 0L234.666667 264.533333c-42.666667 21.333333-68.266667 64-68.266667 110.933334v226.133333c0 46.933333 25.6 89.6 68.266667 110.933333l217.6 115.2c38.4 21.333333 81.066667 21.333333 119.466666 0l217.6-115.2c42.666667-21.333333 68.266667-64 68.266667-110.933333V375.466667c0-46.933333-25.6-89.6-68.266667-110.933334z m-298.666666-38.4c12.8-8.533333 25.6-8.533333 38.4 0l187.733333 102.4L512 422.4 311.466667 320l179.2-93.866667zM469.333333 746.666667L277.333333 640c-12.8-8.533333-21.333333-21.333333-21.333333-38.4v-213.333333l217.6 106.666666v251.733334z m29.866667 12.8h21.333333c-4.266667 4.266667-12.8 4.266667-21.333333 0z m273.066667-153.6c0 17.066667-8.533333 29.866667-21.333334 38.4L554.666667 746.666667v-247.466667l217.6-98.133333v204.8z" fill="#008000" p-id="14440"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+12
View File
@@ -1,5 +1,17 @@
import { sendWebsocketMsg } from './request'
export const getUpnpMappingInfo = (machineid) => {
return sendWebsocketMsg('tunnel/getmapping',machineid);
}
export const getUpnpMappingLocalInfo = (machineid) => {
return sendWebsocketMsg('tunnel/getmappinglocal',machineid);
}
export const addUpnpMappingInfo = (machineid,value) => {
return sendWebsocketMsg('tunnel/addmapping',{Key:machineid,Value:value});
}
export const delUpnpMappingInfo = (machineid,port,protocolType) => {
return sendWebsocketMsg('tunnel/delmapping',{Key:machineid,Value:{Key:port,Value:protocolType}});
}
export const getTunnelInfo = (hashcode = '0') => {
return sendWebsocketMsg('tunnel/get', hashcode);
}
+1
View File
@@ -12,6 +12,7 @@ export default {
'common.p2p': 'P2P',
'common.refresh': 'Refresh',
'common.copied': 'Copied',
'common.deleteText': 'Are you sure delete {0}',
'head.home': 'Home',
'head.server': 'Server',
+1
View File
@@ -12,6 +12,7 @@ export default {
'common.p2p': '打洞',
'common.refresh': '刷新',
'common.copied': '已复制',
'common.deleteText': '确认删除{0}吗?',
'head.home': '首页',
'head.server': '服务器',
@@ -75,7 +75,7 @@ export const provideDevices = () => {
}
}
await Promise.all(Object.values(hooks).map(hook=>dataFn(hook)));
Object.values(hooks).forEach(hook=>dataFn(hook))
devices.timer1 = setTimeout(fn,1000);
}
@@ -7,21 +7,16 @@
<div class="skeleton-animation" :style="`animation-delay:${scope.row.animationDelay}ms`">
<div>
<template v-if="scope.row.hook_tunnel.Net.CountryCode">
<img
:title="`${scope.row.hook_tunnel.Net.CountryCode}、${scope.row.hook_tunnel.Net.City}`"
class="system"
:src="`https://unpkg.com/flag-icons@7.2.3/flags/4x3/${scope.row.hook_tunnel.Net.CountryCode.toLowerCase()}.svg`" />
<img class="system" :title="`${scope.row.hook_tunnel.Net.CountryCode}、${scope.row.hook_tunnel.Net.City}`" :src="`https://unpkg.com/flag-icons@7.2.3/flags/4x3/${scope.row.hook_tunnel.Net.CountryCode.toLowerCase()}.svg`" />
</template>
<template v-else>
<img title="?" class="system" src="/system.svg" />
</template>
<template v-if="scope.row.hook_tunnel.Net.Isp">
<img
:title="`${scope.row.hook_tunnel.Net.Isp}`"
class="system" :src="netImg(scope.row.hook_tunnel.Net)" />
<img class="system" :title="`${scope.row.hook_tunnel.Net.Isp}`" :src="netImg(scope.row.hook_tunnel.Net)" />
</template>
<template v-else>
<img title="?" class="system" src="/system.svg" />
<img class="system" title="?" src="/system.svg" />
</template>
<template v-if="scope.row.hook_tunnel.Net.Nat">
<span class="nat" :title="scope.row.hook_tunnel.Net.Nat">{{ natMap[scope.row.hook_tunnel.Net.Nat] }}</span>
@@ -31,11 +26,24 @@
</template>
</div>
<div class="flex">
<a href="javascript:;" class="a-line" title="upnp,绿色可用,黄色大概率可用,灰色几乎不可用"
@click="handleUpnp(scope.row.hook_tunnel,scope.row,values)"
:class="scope.row.hook_counter['upnp-w'] > 0 && scope.row.hook_counter['upnp-d'] > 0
? 'green' : (scope.row.hook_counter['upnp-w']> 0 || scope.row.hook_counter['upnp-d'] > 0
? 'yellow' : '')">
<img title="upnp" class="system"
:src="scope.row.hook_counter['upnp-w'] > 0 && scope.row.hook_counter['upnp-d'] > 0
? '/upnp-green.svg' : (scope.row.hook_counter['upnp-w']> 0 || scope.row.hook_counter['upnp-d'] > 0
? '/upnp-yellow.svg' : '/upnp.svg')"/>
<span>{{ scope.row.hook_counter['upnp-l'] }}-{{ scope.row.hook_counter['upnp-r'] }}</span>
</a>
<span class="flex-1"></span>
<a href="javascript:;" class="a-line"
:class="{yellow:scope.row.hook_tunnel.NeedReboot}"
:title="$t('home.holeText')"
@click="handleTunnel(scope.row.hook_tunnel,scope.row,values)">
<span>{{$t('home.jump')}}:{{scope.row.hook_tunnel.RouteLevel}}+{{scope.row.hook_tunnel.RouteLevelPlus}}</span>
:class="{yellow:scope.row.hook_tunnel.NeedReboot}"
:title="$t('home.holeText')"
@click="handleTunnel(scope.row.hook_tunnel,scope.row,values)">
{{scope.row.hook_tunnel.RouteLevel}}+{{scope.row.hook_tunnel.RouteLevelPlus}}
</a>
</div>
</div>
@@ -87,6 +95,8 @@ export default {
'aliyun':'aliyun.svg',
'alibaba':'aliyun.svg',
'jdcom':'jdcom.svg',
'jdcom':'jdcom.svg',
'cloudflare':'cloudflare.svg'
}
const regex = new RegExp(Object.keys(imgMap).map(item => `\\b${item}\\b`).join("|"));
const netImg = (item)=>{
@@ -127,8 +137,25 @@ export default {
tunnel.value.current = _tunnel;
tunnel.value.showEdit = true;
}
const handleUpnp = (_tunnel,row,access)=>{
if(machineId.value === _tunnel.MachineId){
if(!access.TunnelChangeSelf){
ElMessage.success(t('common.access'));
return;
}
}else{
if(!access.TunnelChangeOther){
ElMessage.success(t('common.access'));
return;
}
}
_tunnel.device = row;
tunnel.value.current = _tunnel;
tunnel.value.showUpnp = true;
}
return {
handleTunnel,netImg,natMap
handleTunnel,handleUpnp,netImg,natMap
}
}
}
@@ -0,0 +1,50 @@
<template>
<el-dialog v-model="state.show" :close-on-click-modal="false" append-to=".app-wrap" :title="`设置[${state.machineName}]UPNP`" width="98%" top="2vh">
<div>
<el-tabs type="border-card">
<el-tab-pane label="我加的">
<TunnelUpnpLocal :deviceTypes="state.deviceTypes" :protocolTypes="state.protocolTypes" :machineId="state.machineId"></TunnelUpnpLocal>
</el-tab-pane>
<el-tab-pane label="网关里的">
<TunnelUpnpRemote :deviceTypes="state.deviceTypes" :protocolTypes="state.protocolTypes" :machineId="state.machineId"></TunnelUpnpRemote>
</el-tab-pane>
</el-tabs>
</div>
</el-dialog>
</template>
<script>
import {reactive, watch } from 'vue';
import {useTunnel } from './tunnel';
import TunnelUpnpLocal from './TunnelUpnpLocal.vue';
import TunnelUpnpRemote from './TunnelUpnpRemote.vue';
export default {
props: ['modelValue'],
emits: ['update:modelValue'],
components: {TunnelUpnpLocal,TunnelUpnpRemote},
setup(props, { emit }) {
const tunnel = useTunnel();
const state = reactive({
tab: '',
show: true,
machineName: tunnel.value.current.device.MachineName,
machineId: tunnel.value.current.device.MachineId,
deviceTypes:{1:'UPNP',2:'NAT-PMP',4:'PCP',255:'Any'},
protocolTypes:{6:'TCP',17:'UDP'},
});
watch(() => state.show, (val) => {
if (!val) {
setTimeout(() => {
emit('update:modelValue', val);
}, 300);
}
});
return {
state
}
}
}
</script>
<style lang="stylus" scoped>
</style>
@@ -0,0 +1,150 @@
<template>
<div>
<div class="head pdb-6 t-c">
<el-input size="small" v-model="state.name" @change="handleSearch" clearable style="width:10rem;margin-right:1rem"></el-input>
<el-button size="small" :loading="state.loading" @click="handleSearch"><el-icon><Search></Search></el-icon> </el-button>
<el-button size="small" type="primary" :loading="state.loading" @click="handleAdd">添加</el-button>
</div>
<el-table stripe :data="state.list" border size="small" width="100%" height="60vh">
<el-table-column prop="DeviceType" label="类型" width="66" sortable>
<template #default="scope">{{ deviceTypes[scope.row.DeviceType] }}</template>
</el-table-column>
<el-table-column prop="PublicPort" label="外网端口" width="90" sortable></el-table-column>
<el-table-column prop="ClientIp" label="内网ip" width="100" sortable></el-table-column>
<el-table-column prop="PrivatePort" label="内网端口" width="90" sortable></el-table-column>
<el-table-column prop="ProtocolType" label="协议" width="66" sortable>
<template #default="scope">{{ protocolTypes[scope.row.ProtocolType] }}</template>
</el-table-column>
<el-table-column prop="LeaseDuration" label="存活" width="66" sortable></el-table-column>
<el-table-column property="Disabled" label="启用" width="66" sortable>
<template #default="scope">
<el-switch :disabled="scope.row.Deletable==false" v-model="scope.row.Enabled" @change="handleEnabledChange(scope.row)" />
</template>
</el-table-column>
<el-table-column prop="Description" label="描述" width="200"></el-table-column>
<el-table-column property="Oper" label="" width="60" fixed="right">
<template #default="scope">
<el-button size="small" type="danger" v-if="scope.row.Deletable" @click="handleDel(scope.row)"><el-icon><DeleteFilled></DeleteFilled></el-icon> </el-button>
</template>
</el-table-column>
</el-table>
<TunnelUpnpLocalAdd v-if="state.showAdd" v-model="state.showAdd"
:deviceTypes="deviceTypes" :protocolTypes="protocolTypes" :machineId="machineId" @change="handleSearch"></TunnelUpnpLocalAdd>
</div>
</template>
<script>
import { onMounted, onUnmounted, provide, reactive, ref } from 'vue';
import { addUpnpMappingInfo, delUpnpMappingInfo, getUpnpMappingLocalInfo } from '@/apis/tunnel';
import {DeleteFilled,Search} from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus';
import { useI18n } from 'vue-i18n';
import TunnelUpnpLocalAdd from './TunnelUpnpLocalAdd.vue';
export default {
props: ['modelValue','deviceTypes','protocolTypes','machineId'],
emits: ['update:modelValue'],
components: { DeleteFilled,Search,TunnelUpnpLocalAdd },
setup(props, { emit }) {
const { t } = useI18n();
const state = reactive({
name: '',
list: [],
timer:0 ,
loading:false,
showAdd:false
});
const handleDel = (row) => {
ElMessageBox.confirm(t('common.deleteText',[`[${row.PublicPort}:${props.protocolTypes[row.ProtocolType]}]`]), t('common.tips'), {
confirmButtonText: t('common.confirm'),
cancelButtonText: t('common.cancel'),
type: 'warning'
}).then(() => {
delUpnpMappingInfo(props.machineId,row.PublicPort,row.ProtocolType).then(res => {
setTimeout(handleSearch,1000);
ElMessage.success(t('common.oper'));
});
}).catch(() => {
});
}
const getMapping = () => {
if(state.loading) {
state.timer = setTimeout(getMapping,5000);
return;
}
clearTimeout(state.timer);
state.loading = true;
getUpnpMappingLocalInfo(props.machineId).then(res=>{
state.loading = false;
filter(res);
state.timer = setTimeout(getMapping,5000);
}).catch(()=>{
state.loading = false;
state.timer = setTimeout(getMapping,5000);
});
}
const handleSearch = ()=>{
if(state.loading) {
return;
}
state.loading = true;
getUpnpMappingLocalInfo(props.machineId).then(res=>{
state.loading = false;
filter(res);
}).catch(()=>{
state.loading = false;
});
}
const filter = (res) => {
state.list = res.filter(c=>{
return c.Description.indexOf(state.name)>=0 ||
c.ClientIp.indexOf(state.name)>=0 ||
c.PublicPort.toString().indexOf(state.name)>=0 ||
c.PrivatePort.toString().indexOf(state.name)>=0 ||
props.protocolTypes[c.ProtocolType].indexOf(state.name)>=0 ||
props.deviceTypes[c.DeviceType].indexOf(state.name)>=0;
});
}
const addState = ref({});
provide('addState', addState);
const handleAdd = () => {
addState.value = {
PublicPort: 0,
PrivatePort: 0,
ProtocolType: 6,
Enabled: true,
Description: 'linker tunnel',
LeaseDuration: 7200,
DeviceType: 255,
Deletable: false,
};
state.showAdd = true;
}
const handleEnabledChange = (row) => {
delUpnpMappingInfo(props.machineId,row.PublicPort,row.ProtocolType).then(res => {
addUpnpMappingInfo(props.machineId,row).then(res=>{
setTimeout(handleSearch,1000);
ElMessage.success(t('common.oper'));
});
});
}
onMounted(()=>{
getMapping();
});
onUnmounted(()=>{
clearTimeout(state.timer);
});
return {
state,handleDel,handleSearch,handleAdd,handleEnabledChange
}
}
}
</script>
<style lang="stylus" scoped>
</style>
@@ -0,0 +1,125 @@
<template>
<el-dialog v-model="state.show" :close-on-click-modal="false" append-to=".app-wrap" title="UPNP" width="360" top="2vh">
<div>
<el-form ref="ruleFormRef" :model="state.ruleForm" :rules="state.rules" label-width="auto">
<el-form-item label="设备类型" prop="DeviceType">
<el-select v-model="state.ruleForm.DeviceType" placeholder="请选择">
<el-option v-for="(item,key) in deviceTypes" :key="Number(key)" :label="item" :value="Number(key)"></el-option>
</el-select>
</el-form-item>
<el-form-item label="协议" prop="ProtocolType">
<el-select v-model="state.ruleForm.ProtocolType" placeholder="请选择">
<el-option v-for="(item,key) in protocolTypes" :key="Number(key)" :label="item" :value="Number(key)"></el-option>
</el-select>
</el-form-item>
<el-form-item label="外网端口" prop="PublicPort">
<el-input-number v-model="state.ruleForm.PublicPort" />
</el-form-item>
<el-form-item label="外网端口" prop="PrivatePort">
<el-input-number v-model="state.ruleForm.PrivatePort" />
</el-form-item>
<el-form-item label="存活时间" prop="LeaseDuration">
<el-input-number v-model="state.ruleForm.LeaseDuration" />
</el-form-item>
<el-form-item label="描述" prop="Description">
<el-input v-model="state.ruleForm.Description" :maxlength="32" show-word-limit clearable />
</el-form-item>
<el-form-item label="启用" prop="Enabled">
<el-switch v-model="state.ruleForm.Enabled" />
</el-form-item>
<el-form-item label="" prop="Btns">
<div class="t-c w-100">
<el-button @click="state.show = false">取消</el-button>
<el-button type="primary" @click="handleSave">确认</el-button>
</div>
</el-form-item>
</el-form>
</div>
</el-dialog>
</template>
<script>
import { addUpnpMappingInfo, delUpnpMappingInfo } from '@/apis/tunnel';
import { ElMessage } from 'element-plus';
import { inject, reactive, ref, watch } from 'vue';
export default {
props: ['modelValue','deviceTypes','protocolTypes','machineId'],
emits: ['change','update:modelValue'],
setup(props, { emit }) {
const ruleFormRef = ref(null);
const addState = inject('addState');
const deletePort = addState.value.PublicPort || 0;
const deleteProtocolType = addState.value.ProtocolType || 0;
const state = reactive({
show: true,
ruleForm: {
PublicPort: addState.value.PublicPort || 0,
PrivatePort: addState.value.PrivatePort || 0,
ProtocolType: addState.value.ProtocolType || 6,
Enabled: addState.value.Enabled,
Description: addState.value.Description || '',
LeaseDuration: addState.value.LeaseDuration || 7200,
DeviceType: addState.value.DeviceType || 255,
Deletable: true,
},
rules: {
PublicPort: [
{ required: true, message: '请输入外网端口', trigger: 'blur' },
{ type: 'number',min: 1, max: 65535, message: '请输入数字1-65535', trigger: ['blur', 'change'] },
],
PrivatePort: [
{ required: true, message: '请输入内网端口', trigger: 'blur' },
{ type: 'number',min: 1, max: 65535, message: '请输入数字1-65535', trigger: ['blur', 'change'] }
],
LeaseDuration: [
{ required: true, message: '请输入存活时间', trigger: 'blur' },
{ type: 'number',min: 0, max: 2147483647, message: '请输入数字0-2147483647', trigger: ['blur', 'change'] }
]
},
net:{}
});
watch(() => state.show, (val) => {
if (!val) {
setTimeout(() => {
emit('update:modelValue', val);
}, 300);
}
});
const handleSave = () => {
ruleFormRef.value.validate((valid) => {
if(!valid) return;
const json = JSON.parse(JSON.stringify(state.ruleForm));
json.PublicPort = +state.ruleForm.PublicPort;
json.PrivatePort = +state.ruleForm.PrivatePort;
json.ProtocolType = +state.ruleForm.ProtocolType;
json.LeaseDuration = +state.ruleForm.LeaseDuration;
json.DeviceType = +state.ruleForm.DeviceType;
delUpnpMappingInfo(props.machineId,deletePort,deleteProtocolType).then(() => {
addUpnpMappingInfo(props.machineId,json).then(() => {
state.show = false;
ElMessage.success('已操作!');
emit('change')
}).catch((err) => {
console.log(err);
ElMessage.error('操作失败!');
});
}).catch((err) => {
console.log(err);
ElMessage.error('操作失败!');
});
});
}
return {
state, ruleFormRef, handleSave
}
}
}
</script>
<style lang="stylus" scoped>
</style>
@@ -0,0 +1,137 @@
<template>
<div>
<div class="head pdb-6 t-c">
<el-input size="small" v-model="state.name" @change="handleSearch" clearable style="width:10rem;margin-right:1rem"></el-input>
<el-button size="small" :loading="state.loading" @click="handleSearch"><el-icon><Search></Search></el-icon> </el-button>
</div>
<el-table stripe :data="state.list" border size="small" width="100%" height="60vh">
<el-table-column prop="DeviceType" label="类型" width="66" sortable>
<template #default="scope">{{ deviceTypes[scope.row.DeviceType] }}</template>
</el-table-column>
<el-table-column prop="PublicPort" label="外网端口" width="90" sortable></el-table-column>
<el-table-column prop="ClientIp" label="内网ip" width="100" sortable></el-table-column>
<el-table-column prop="PrivatePort" label="内网端口" width="90" sortable></el-table-column>
<el-table-column prop="ProtocolType" label="协议" width="66" sortable>
<template #default="scope">{{ protocolTypes[scope.row.ProtocolType] }}</template>
</el-table-column>
<el-table-column prop="LeaseDuration" label="存活" width="66" sortable>
<template #default="scope">
<span :class="{red:scope.row.LeaseDuration ==0,green:scope.row.LeaseDuration >0}">{{ scope.row.LeaseDuration }}</span>
</template>
</el-table-column>
<el-table-column prop="Description" label="描述" width="200"></el-table-column>
<el-table-column property="Oper" label="" width="60" fixed="right">
<template #default="scope">
<el-button size="small" type="danger" v-if="scope.row.Deletable" @click="handleDel(scope.row)"><el-icon><DeleteFilled></DeleteFilled></el-icon> </el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
import { onMounted, onUnmounted, reactive } from 'vue';
import {delUpnpMappingInfo, getUpnpMappingInfo, getUpnpMappingLocalInfo } from '@/apis/tunnel';
import {DeleteFilled,Search} from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus';
import { useI18n } from 'vue-i18n';
export default {
props: ['modelValue','deviceTypes','protocolTypes','machineId'],
emits: ['update:modelValue'],
components: { DeleteFilled,Search },
setup(props, { emit }) {
const { t } = useI18n();
const state = reactive({
name: '',
list: [],
timer:0 ,
loading:false
});
const handleDel = (row) => {
ElMessageBox.confirm(t('common.deleteText',[`[${row.PublicPort}:${props.protocolTypes[row.ProtocolType]}]`]), t('common.tips'), {
confirmButtonText: t('common.confirm'),
cancelButtonText: t('common.cancel'),
type: 'warning'
}).then(() => {
delUpnpMappingInfo(props.machineId,row.PublicPort,row.ProtocolType).then(res => {
setTimeout(handleSearch,1000);
ElMessage.success(t('common.oper'));
});
}).catch(() => {
});
}
const getMapping = () => {
if(state.loading) {
state.timer = setTimeout(getMapping,5000);
return;
}
clearTimeout(state.timer);
state.loading = true;
Promise.all([
getUpnpMappingInfo(props.machineId),
getUpnpMappingLocalInfo(props.machineId)
]).then(res=>{
state.loading = false;
filter(res);
state.timer = setTimeout(getMapping,5000);
}).catch(()=>{
state.loading = false;
state.timer = setTimeout(getMapping,5000);
});
}
const handleSearch = ()=>{
if(state.loading) {
return;
}
state.loading = true;
Promise.all([
getUpnpMappingInfo(props.machineId),
getUpnpMappingLocalInfo(props.machineId)
]).then(res=>{
state.loading = false;
filter(res);
}).catch(()=>{
state.loading = false;
});
}
const filter = (res) => {
const local = res[1].reduce((json,item,index)=>{
json[`${item.PublicPort}->${item.ClientIp}->${item.ProtocolType}`] = item;
return json;
},{});
state.list = res[0].filter(c=>{
const _local = local[`${c.PublicPort}->${c.ClientIp}->${c.ProtocolType}`];
if(_local){
c['_local'] = true;
c['Deletable'] = _local['Deletable'];
}
return c.Description.indexOf(state.name)>=0 ||
c.ClientIp.indexOf(state.name)>=0 ||
c.PublicPort.toString().indexOf(state.name)>=0 ||
c.PrivatePort.toString().indexOf(state.name)>=0 ||
props.protocolTypes[c.ProtocolType].indexOf(state.name)>=0 ||
props.deviceTypes[c.DeviceType].indexOf(state.name)>=0;
}).sort((a,b)=>b['_local']-a['_local']);
}
onMounted(()=>{
getMapping();
});
onUnmounted(()=>{
clearTimeout(state.timer);
});
return {
state, handleDel,handleSearch
}
}
}
</script>
<style lang="stylus" scoped>
</style>
@@ -11,6 +11,7 @@ export const provideTunnel = () => {
hashcode1: 0,
showEdit: false,
showUpnp: false,
current: null,
});
provide(tunnelSymbol, tunnel);
@@ -22,6 +22,8 @@
<DeviceEdit v-if="devices.showDeviceEdit" v-model="devices.showDeviceEdit" @change="handlePageChange" :data="devices.deviceInfo"></DeviceEdit>
<AccessEdit v-if="devices.showAccessEdit" v-model="devices.showAccessEdit" @change="handlePageChange" :data="devices.deviceInfo"></AccessEdit>
<TunnelEdit v-if="tunnel.showEdit" v-model="tunnel.showEdit" @change="deviceRefreshHook('tunnel')"></TunnelEdit>
<TunnelUpnp v-if="tunnel.showUpnp" v-model="tunnel.showUpnp"></TunnelUpnp>
<ConnectionsEdit v-if="connections.showEdit" v-model="connections.showEdit" ></ConnectionsEdit>
<TuntapEdit v-if="tuntap.showEdit" v-model="tuntap.showEdit" @change="deviceRefreshHook('tuntap')"></TuntapEdit>
<TuntapLease v-if="tuntap.showLease" v-model="tuntap.showLease" @change="deviceRefreshHook('tuntap')"></TuntapLease>
@@ -58,6 +60,7 @@ import { provideSocks5 } from '../../../components/socks5/socks5'
import Tunnel from '../../../components/tunnel/Tunnel.vue'
import TunnelEdit from '../../../components/tunnel/TunnelEdit.vue'
import TunnelUpnp from '../../../components/tunnel/TunnelUpnp.vue'
import { provideTunnel } from '../../../components/tunnel/tunnel'
import { provideUpdater } from '../../../components/updater/updater'
@@ -105,7 +108,7 @@ export default {
AccessEdit,
Tunnel,TunnelEdit,
ConnectionsEdit,
Tuntap,TuntapEdit,TuntapLease,
Tuntap,TuntapEdit,TunnelUpnp,TuntapLease,
Socks5, Socks5Edit,
Forward,ForwardEdit,
SForwardEdit ,UpdaterConfirm,
+2 -1
View File
@@ -20,7 +20,8 @@
3. #89 windows下利用任务计划进行进程守护,定时检查服务
4. 优化客户端管理ws并发
5. 减少服务器推送,减轻主服务器压力
6. 优化中继,针对无法连接自己的外网IP的情况</Description>
6. 优化中继,针对无法连接自己的外网IP的情况
7. 优化upnp,自己实现了一遍</Description>
<Copyright>snltty</Copyright>
<PackageProjectUrl>https://github.com/snltty/linker</PackageProjectUrl>
<RepositoryUrl>https://github.com/snltty/linker</RepositoryUrl>