增加打洞协议,自定义认证参数管理,增加防火墙同步

This commit is contained in:
snltty
2025-06-16 17:35:01 +08:00
parent d28d46f837
commit f5a16b0d8a
49 changed files with 1113 additions and 200 deletions

View File

@@ -12,8 +12,9 @@ sidebar_position: 2
:::tip[1、情况1你的设备支持NAT转发时]
1. linux已经自动添加NAT转发(在`OpenWrt`,需要在`防火墙 - 区域设置`中将`转发`设置为`接受`)
2. windows优先使用系统`NetNat``NetNat`失败则启用`内置SNAT`,但是性能应该没有`NetNat`
1. linux已经自动添加NAT转发
2. linux软路由docker,在`OpenWrt、群晖`或者其它软路由系统可能需要在宿主机允许IP转发可以在UI设置也可以尝试`sysctl -w net.ipv4.ip_forward=1`允许IP转发然后`iptables -t nat -A POSTROUTING -s 10.18.18.0/24 -j MASQUERADE`然后添加NAT10.18.18.0/24是你的虚拟网卡网段
3. windows优先使用系统`NetNat``NetNat`失败则启用`内置SNAT`,但是性能应该没有`NetNat`
1. 由于`内置SNAT`依赖`WinDivert驱动`,如果报错`Windows 无法验证此文件的数字签名`什么的,可以尝试以下两种解决办法
2. 使用`管理员身份运行cmd`执行以下两条命令,然后重启系统
```
@@ -30,8 +31,6 @@ sidebar_position: 2
:::
:::tip[2、情况2你的设备无法使用NAT转发时]
1. 你的设备无法使用NAT转发(一般出现在低版本windows下win10以下),那你只能使用端口转发功能来访问你当前设备局域网下的其它设备

View File

@@ -1,30 +1,70 @@
using linker.libs.extends;
using linker.libs;
using linker.libs.extends;
using linker.libs.web;
using linker.messenger.api;
using linker.messenger.signin;
using linker.tunnel.transport;
namespace linker.messenger.action
{
public sealed class ActionApiController : IApiController
{
private readonly IActionClientStore actionStore;
public ActionApiController(IActionClientStore actionStore)
private readonly ActionTransfer actionTransfer;
private readonly SignInClientState signInClientState;
private readonly ISignInClientStore signInClientStore;
private readonly IMessengerSender messengerSender;
private readonly ISerializer serializer;
public ActionApiController(ActionTransfer actionTransfer, SignInClientState signInClientState, ISignInClientStore signInClientStore,
IMessengerSender messengerSender, ISerializer serializer)
{
this.actionStore = actionStore;
this.actionTransfer = actionTransfer;
this.signInClientState = signInClientState;
this.signInClientStore = signInClientStore;
this.messengerSender = messengerSender;
this.serializer = serializer;
}
[Access(AccessValue.Action)]
public bool SetArgs(ApiControllerParamsInfo param)
{
actionStore.SetActionArg(param.Content);
return actionStore.Confirm();
return actionTransfer.SetActionDynamicArg(param.Content);
}
[Access(AccessValue.Action)]
public bool SetServerArgs(ApiControllerParamsInfo param)
public async Task<string> GetServerArgs(ApiControllerParamsInfo param)
{
actionStore.SetActionArgs(param.Content.DeJson<Dictionary<string, string>>());
return actionStore.Confirm();
if (param.Content == signInClientStore.Id || string.IsNullOrWhiteSpace(param.Content))
{
return actionTransfer.GetActionStaticArg();
}
MessageResponeInfo resp = await messengerSender.SendReply(new MessageRequestWrap
{
Connection = signInClientState.Connection,
MessengerId = (ushort)ActionMessengerIds.GetForward,
Payload = serializer.Serialize(param.Content)
}).ConfigureAwait(false);
if (resp.Code == MessageResponeCodes.OK && resp.Data.Length > 0)
{
return serializer.Deserialize<string>(resp.Data.Span);
}
return string.Empty;
}
[Access(AccessValue.Action)]
public async Task<bool> SetServerArgs(ApiControllerParamsInfo param)
{
KeyValuePair<string, string> keyValue = param.Content.DeJson<KeyValuePair<string, string>>();
if (keyValue.Key == signInClientStore.Id || string.IsNullOrWhiteSpace(keyValue.Key))
{
return actionTransfer.SetActionStaticArg(keyValue.Value);
}
MessageResponeInfo resp = await messengerSender.SendReply(new MessageRequestWrap
{
Connection = signInClientState.Connection,
MessengerId = (ushort)ActionMessengerIds.SetForward,
Payload = serializer.Serialize(keyValue)
}).ConfigureAwait(false);
return resp.Code == MessageResponeCodes.OK && resp.Data.Span.SequenceEqual(Helper.TrueArray);
}
}

View File

@@ -0,0 +1,95 @@
using linker.libs;
using linker.messenger.signin;
namespace linker.messenger.action
{
public class ActionClientMessenger : IMessenger
{
private readonly IMessengerSender messengerSender;
private readonly ISerializer serializer;
private readonly ActionTransfer actionTransfer;
public ActionClientMessenger(IMessengerSender messengerSender, ISerializer serializer, ActionTransfer actionTransfer)
{
this.messengerSender = messengerSender;
this.serializer = serializer;
this.actionTransfer = actionTransfer;
}
[MessengerId((ushort)ActionMessengerIds.Get)]
public void Get(IConnection connection)
{
connection.Write(serializer.Serialize(actionTransfer.GetActionStaticArg()));
}
[MessengerId((ushort)ActionMessengerIds.Set)]
public void Set(IConnection connection)
{
actionTransfer.SetActionStaticArg(serializer.Deserialize<string>(connection.ReceiveRequestWrap.Payload.Span));
}
}
public class ActionServerMessenger : IMessenger
{
private readonly IMessengerSender messengerSender;
private readonly SignInServerCaching signCaching;
private readonly ISerializer serializer;
public ActionServerMessenger(IMessengerSender messengerSender, SignInServerCaching signCaching, ISerializer serializer)
{
this.messengerSender = messengerSender;
this.signCaching = signCaching;
this.serializer = serializer;
}
[MessengerId((ushort)ActionMessengerIds.GetForward)]
public void GetForward(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)ActionMessengerIds.Get
}).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)ActionMessengerIds.GetForward).ConfigureAwait(false);
}
});
}
}
[MessengerId((ushort)ActionMessengerIds.SetForward)]
public void SetForward(IConnection connection)
{
KeyValuePair<string, string> info = serializer.Deserialize<KeyValuePair<string,string>>(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)ActionMessengerIds.Set,
Payload = serializer.Serialize(info.Value)
}).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)ActionMessengerIds.SetForward).ConfigureAwait(false);
}
});
}
}
}
}

View File

@@ -0,0 +1,14 @@
namespace linker.messenger.action
{
public enum ActionMessengerIds : ushort
{
_ = 3600,
Get = 3601,
GetForward = 3602,
Set = 3603,
SetForward = 3604,
None = 3699
}
}

View File

@@ -0,0 +1,33 @@
using linker.libs;
using linker.messenger.sync;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace linker.messenger.action
{
public sealed class ActionSync : ISync
{
public string Name => "ActionStatic";
private readonly ActionTransfer actionTransfer;
private readonly ISerializer serializer;
public ActionSync(ActionTransfer actionTransfer, ISerializer serializer)
{
this.actionTransfer = actionTransfer;
this.serializer = serializer;
}
public Memory<byte> GetData()
{
return serializer.Serialize(actionTransfer.GetActionStaticArg());
}
public void SetData(Memory<byte> data)
{
actionTransfer.SetActionStaticArg(serializer.Deserialize<string>(data.Span));
}
}
}

View File

@@ -1,9 +1,17 @@
using System.Net.Http.Json;
using linker.messenger.signin;
using System.Net.Http.Json;
using System.Text.Json.Nodes;
namespace linker.messenger.action
{
public sealed class ActionTransfer
{
private readonly ISignInClientStore signInClientStore;
private readonly IActionClientStore actionClientStore;
public ActionTransfer(ISignInClientStore signInClientStore, IActionClientStore actionClientStore)
{
this.signInClientStore = signInClientStore;
this.actionClientStore = actionClientStore;
}
public async Task<string> ExcuteActions(string actionJson, string url)
{
if (string.IsNullOrWhiteSpace(url)) return string.Empty;
@@ -31,5 +39,24 @@ namespace linker.messenger.action
}
return string.Empty;
}
public bool SetActionDynamicArg(string value)
{
actionClientStore.SetActionDynamicArg(value);
return actionClientStore.Confirm();
}
public bool SetActionStaticArg(string value)
{
actionClientStore.SetActionStaticArg(signInClientStore.Server.Host, value);
return actionClientStore.Confirm();
}
public string GetActionStaticArg()
{
return actionClientStore.GetActionStaticArg(signInClientStore.Server.Host);
}
public bool TryAddActionArg(Dictionary<string, string> args)
{
return actionClientStore.TryAddActionArg(signInClientStore.Server.Host, args);
}
}
}

View File

@@ -3,6 +3,7 @@ using linker.messenger.action;
using linker.messenger.relay.server.validator;
using linker.messenger.sforward.server.validator;
using linker.messenger.signin.args;
using linker.messenger.sync;
using Microsoft.Extensions.DependencyInjection;
namespace linker.messenger.api
{
@@ -14,6 +15,11 @@ namespace linker.messenger.api
serviceCollection.AddSingleton<ActionTransfer>();
serviceCollection.AddSingleton<SignInArgsAction>();
serviceCollection.AddSingleton<ActionClientMessenger>();
serviceCollection.AddSingleton<ActionSync>();
return serviceCollection;
}
public static ServiceProvider UseActionClient(this ServiceProvider serviceProvider)
@@ -24,6 +30,12 @@ namespace linker.messenger.api
SignInArgsTransfer signInArgsTransfer = serviceProvider.GetService<SignInArgsTransfer>();
signInArgsTransfer.AddArgs(new List<ISignInArgs> { serviceProvider.GetService<SignInArgsAction>() });
IMessengerResolver messengerResolver = serviceProvider.GetService<IMessengerResolver>();
messengerResolver.AddMessenger(new List<IMessenger> { serviceProvider.GetService<ActionClientMessenger>() });
SyncTreansfer syncTransfer = serviceProvider.GetService<SyncTreansfer>();
syncTransfer.AddSyncs(new List<ISync> { serviceProvider.GetService<ActionSync>() });
return serviceProvider;
}
@@ -33,6 +45,8 @@ namespace linker.messenger.api
serviceCollection.AddSingleton<SignInArgsAction>();
serviceCollection.AddSingleton<RelayValidatorAction>();
serviceCollection.AddSingleton<SForwardValidatorAction>();
serviceCollection.AddSingleton<ActionServerMessenger>();
return serviceCollection;
}
public static ServiceProvider UseActionServer(this ServiceProvider serviceProvider)
@@ -45,6 +59,10 @@ namespace linker.messenger.api
SForwardValidatorTransfer sForwardValidatorTransfer = serviceProvider.GetService<SForwardValidatorTransfer>();
sForwardValidatorTransfer.AddValidators(new List<ISForwardValidator> { serviceProvider.GetService<SForwardValidatorAction>() });
IMessengerResolver messengerResolver = serviceProvider.GetService<IMessengerResolver>();
messengerResolver.AddMessenger(new List<IMessenger> { serviceProvider.GetService<ActionServerMessenger>() });
return serviceProvider;
}
}

View File

@@ -1,30 +1,39 @@
namespace linker.messenger.action
using System.Collections.Concurrent;
namespace linker.messenger.action
{
public sealed class ActionInfo
{
public string Arg { get; set; } = string.Empty;
public Dictionary<string, string> Args { get; set; } = new Dictionary<string, string>();
public ConcurrentDictionary<string, string> Args { get; set; } = new ConcurrentDictionary<string, string>();
}
public interface IActionClientStore
{
/// <summary>
/// 设置动态验证参数,优先使用
/// </summary>
/// <param name="action"></param>
public void SetActionArg(string action);
/// <param name="value"></param>
public void SetActionDynamicArg(string value);
/// <summary>
/// 设置静态验证参数,动态参数为空时使用
/// </summary>
/// <param name="actions">action参数列表host->arg不同的服务器不同的参数</param>
public void SetActionArgs(Dictionary<string, string> actions);
/// <param name="key"></param>
/// <param name="value"></param>
public void SetActionStaticArg(string key,string value);
/// <summary>
/// 从配置里获取验证参数添加到args
/// 获取静态参数
/// </summary>
/// <param name="host">当前服务器地址</param>
/// <param name="args">一个字典</param>
/// <param name="key"></param>
/// <returns></returns>
public bool TryAddActionArg(string host, Dictionary<string, string> args);
public string GetActionStaticArg(string key);
/// <summary>
/// 网args里添加指定key的值
/// </summary>
/// <param name="key"></param>
/// <param name="args"></param>
/// <returns></returns>
public bool TryAddActionArg(string key, Dictionary<string, string> args);
/// <summary>
/// 提交更新
/// </summary>

View File

@@ -99,7 +99,7 @@ namespace linker.messenger.api
[AccessDisplay("修改打洞协议")]
Transport = 28,
[AccessDisplay("修改验证参数")]
[AccessDisplay("修改本机验证参数")]
Action = 29,
[AccessDisplay("查看内网穿透流量")]
@@ -149,6 +149,9 @@ namespace linker.messenger.api
WakeupSelf = 46,
[AccessDisplay("唤醒所有设备")]
WakeupOther = 47,
[AccessDisplay("修改所有验证参数")]
ActionOther = 48,
}
public sealed class AccessTextInfo

View File

@@ -1,5 +1,6 @@

using linker.libs.web;
using linker.messenger.sync;
using linker.snat;
using Microsoft.Extensions.DependencyInjection;
@@ -13,6 +14,7 @@ namespace linker.messenger.firewall
serviceCollection.AddSingleton<FirewallClientMessenger>();
serviceCollection.AddSingleton<FirewallTransfer>();
serviceCollection.AddSingleton<FirewallApiController>();
serviceCollection.AddSingleton<FirewallSync>();
return serviceCollection;
}
@@ -26,6 +28,9 @@ namespace linker.messenger.firewall
linker.messenger.api.IWebServer apiServer = serviceProvider.GetService<linker.messenger.api.IWebServer>();
apiServer.AddPlugins(new List<IApiController> { serviceProvider.GetService<FirewallApiController>() });
SyncTreansfer syncTransfer = serviceProvider.GetService<SyncTreansfer>();
syncTransfer.AddSyncs(new List<ISync> { serviceProvider.GetService<FirewallSync>() });
return serviceProvider;
}

View File

@@ -39,7 +39,7 @@ namespace linker.messenger.firewall
if (accessStore.HasAccess(AccessValue.FirewallSelf) == false) return new FirewallListInfo();
return firewallTransfer.Get(info.Data);
}
if (accessStore.HasAccess(AccessValue.FirewallOther) == false) return new FirewallListInfo ();
if (accessStore.HasAccess(AccessValue.FirewallOther) == false) return new FirewallListInfo();
var resp = await messengerSender.SendReply(new MessageRequestWrap
{
@@ -121,5 +121,15 @@ namespace linker.messenger.firewall
Payload = serializer.Serialize(info)
}).ConfigureAwait(false);
}
/// <summary>
/// 选中
/// </summary>
/// <param name="param"></param>
/// <returns></returns>
public bool Check(ApiControllerParamsInfo param)
{
FirewallCheckInfo info = param.Content.DeJson<FirewallCheckInfo>();
return firewallTransfer.Check(info);
}
}
}

View File

@@ -0,0 +1,36 @@
using linker.libs;
using linker.messenger.sync;
namespace linker.messenger.firewall
{
public sealed class FirewallSync : ISync
{
public string Name => "Firewall";
private readonly FirewallTransfer firewallTransfer;
private readonly ISerializer serializer;
public FirewallSync(FirewallTransfer firewallTransfer, ISerializer serializer)
{
this.firewallTransfer = firewallTransfer;
this.serializer = serializer;
}
public Memory<byte> GetData()
{
return serializer.Serialize(firewallTransfer.Get().Where(c => c.Checked).ToList());
}
public void SetData(Memory<byte> data)
{
List<string> ids = firewallTransfer.Get().Select(c => c.Id).ToList();
List<FirewallRuleInfo> list = serializer.Deserialize<List<FirewallRuleInfo>>(data.Span);
foreach (FirewallRuleInfo rule in list)
{
rule.Id = string.Empty;
}
firewallTransfer.Add(list);
firewallTransfer.Remove(ids);
}
}
}

View File

@@ -34,7 +34,15 @@ namespace linker.messenger.firewall
BuildRules();
return true;
}
public bool Check(FirewallCheckInfo info)
{
return firewallClientStore.Check(info);
}
public List<FirewallRuleInfo> Get()
{
return firewallClientStore.GetAll().ToList();
}
public FirewallListInfo Get(FirewallSearchInfo info)
{
return new FirewallListInfo
@@ -50,12 +58,29 @@ namespace linker.messenger.firewall
BuildRules();
return true;
}
public bool Add(List<FirewallRuleInfo> infos)
{
foreach (var item in infos)
{
item.GroupId = signInClientStore.Group.Id;
}
firewallClientStore.Add(infos);
BuildRules();
return true;
}
public bool Remove(string id)
{
firewallClientStore.Remove(id);
BuildRules();
return true;
}
public bool Remove(List<string> ids)
{
firewallClientStore.Remove(ids);
BuildRules();
return true;
}
}
}

View File

@@ -7,10 +7,14 @@ namespace linker.messenger.firewall
public LinkerFirewallState State { get; }
public void SetState(LinkerFirewallState state);
public IEnumerable<FirewallRuleInfo> GetAll();
public IEnumerable<FirewallRuleInfo> GetAll(FirewallSearchInfo searchInfo);
public IEnumerable<FirewallRuleInfo> GetEnabled(string groupId);
public bool Add(FirewallRuleInfo rule);
public bool Add(List<FirewallRuleInfo> rules);
public bool Remove(string id);
public bool Remove(List<string> ids);
public bool Check(FirewallCheckInfo info);
}
public sealed class FirewallRuleInfo : linker.snat.LinkerFirewallRuleInfo
@@ -21,6 +25,7 @@ namespace linker.messenger.firewall
public bool Disabled { get; set; }
public int OrderBy { get; set; }
public string Remark { get; set; }
public bool Checked { get; set; }
}
@@ -59,4 +64,15 @@ namespace linker.messenger.firewall
public string MachineId { get; set; }
public LinkerFirewallState State { get; set; }
}
public sealed partial class FirewallCheckForwardInfo
{
public string MachineId { get; set; }
public FirewallCheckInfo Data { get; set; }
}
public sealed partial class FirewallCheckInfo
{
public List<string> Ids { get; set; }
public bool IsChecked { get; set; }
}
}

View File

@@ -40,6 +40,7 @@
<ItemGroup>
<ProjectReference Include="..\linker.messenger.api\linker.messenger.api.csproj" />
<ProjectReference Include="..\linker.messenger.signin\linker.messenger.signin.csproj" />
<ProjectReference Include="..\linker.messenger.sync\linker.messenger.sync.csproj" />
<ProjectReference Include="..\linker.messenger\linker.messenger.csproj" />
<ProjectReference Include="..\linker.snat\linker.snat.csproj" />
</ItemGroup>

View File

@@ -39,6 +39,7 @@ namespace linker.messenger.serializer.memorypack
MemoryPackFormatterProvider.Register(new TunnelSetRouteLevelInfoFormatter());
MemoryPackFormatterProvider.Register(new TunnelInterfaceInfoFormatter());
MemoryPackFormatterProvider.Register(new TunnelNetInfoFormatter());
MemoryPackFormatterProvider.Register(new TunnelTransportItemSetInfoFormatter());
MemoryPackFormatterProvider.Register(new DecenterSyncInfoFormatter());
@@ -66,7 +67,7 @@ namespace linker.messenger.serializer.memorypack
MemoryPackFormatterProvider.Register(new RelayAskResultInfo170Formatter());
MemoryPackFormatterProvider.Register(new RelayCacheInfoFormatter());
MemoryPackFormatterProvider.Register(new RelayMessageInfoFormatter());
MemoryPackFormatterProvider.Register(new RelayTrafficUpdateInfoFormatter());
MemoryPackFormatterProvider.Register(new RelayServerNodeUpdateInfoFormatter());
MemoryPackFormatterProvider.Register(new RelayServerNodeUpdateWrapInfoFormatter());
@@ -143,6 +144,8 @@ namespace linker.messenger.serializer.memorypack
MemoryPackFormatterProvider.Register(new FirewallAddForwardInfoFormatter());
MemoryPackFormatterProvider.Register(new FirewallRemoveForwardInfoFormatter());
MemoryPackFormatterProvider.Register(new FirewallStateForwardInfoFormatter());
MemoryPackFormatterProvider.Register(new FirewallCheckInfoFormatter());
MemoryPackFormatterProvider.Register(new FirewallCheckForwardInfoFormatter());
MemoryPackFormatterProvider.Register(new WakeupInfoFormatter());
MemoryPackFormatterProvider.Register(new WakeupSearchInfoFormatter());

View File

@@ -441,4 +441,112 @@ namespace linker.messenger.serializer.memorypack
value = wrapped.info;
}
}
[MemoryPackable]
public readonly partial struct SerializableFirewallCheckInfo
{
[MemoryPackIgnore]
public readonly FirewallCheckInfo info;
[MemoryPackInclude]
List<string> Ids => info.Ids;
[MemoryPackInclude, MemoryPackAllowSerialize]
bool IsChecked => info.IsChecked;
[MemoryPackConstructor]
SerializableFirewallCheckInfo(List<string> ids, bool isChecked)
{
this.info = new FirewallCheckInfo
{
Ids = ids,
IsChecked = isChecked
};
}
public SerializableFirewallCheckInfo(FirewallCheckInfo info)
{
this.info = info;
}
}
public class FirewallCheckInfoFormatter : MemoryPackFormatter<FirewallCheckInfo>
{
public override void Serialize<TBufferWriter>(ref MemoryPackWriter<TBufferWriter> writer, scoped ref FirewallCheckInfo value)
{
if (value == null)
{
writer.WriteNullObjectHeader();
return;
}
writer.WritePackable(new SerializableFirewallCheckInfo(value));
}
public override void Deserialize(ref MemoryPackReader reader, scoped ref FirewallCheckInfo value)
{
if (reader.PeekIsNull())
{
reader.Advance(1); // skip null block
value = null;
return;
}
var wrapped = reader.ReadPackable<SerializableFirewallCheckInfo>();
value = wrapped.info;
}
}
[MemoryPackable]
public readonly partial struct SerializableFirewallCheckForwardInfo
{
[MemoryPackIgnore]
public readonly FirewallCheckForwardInfo info;
[MemoryPackInclude]
string MachineId => info.MachineId;
[MemoryPackInclude, MemoryPackAllowSerialize]
FirewallCheckInfo Data => info.Data;
[MemoryPackConstructor]
SerializableFirewallCheckForwardInfo(string machineId, FirewallCheckInfo data)
{
this.info = new FirewallCheckForwardInfo
{
MachineId = machineId,
Data = data
};
}
public SerializableFirewallCheckForwardInfo(FirewallCheckForwardInfo info)
{
this.info = info;
}
}
public class FirewallCheckForwardInfoFormatter : MemoryPackFormatter<FirewallCheckForwardInfo>
{
public override void Serialize<TBufferWriter>(ref MemoryPackWriter<TBufferWriter> writer, scoped ref FirewallCheckForwardInfo value)
{
if (value == null)
{
writer.WriteNullObjectHeader();
return;
}
writer.WritePackable(new SerializableFirewallCheckForwardInfo(value));
}
public override void Deserialize(ref MemoryPackReader reader, scoped ref FirewallCheckForwardInfo value)
{
if (reader.PeekIsNull())
{
reader.Advance(1); // skip null block
value = null;
return;
}
var wrapped = reader.ReadPackable<SerializableFirewallCheckForwardInfo>();
value = wrapped.info;
}
}
}

View File

@@ -615,4 +615,54 @@ namespace linker.messenger.serializer.memorypack
value = wrapped.info;
}
}
[MemoryPackable]
public readonly partial struct SerializableTunnelTransportItemSetInfo
{
[MemoryPackIgnore]
public readonly TunnelTransportItemSetInfo info;
[MemoryPackInclude]
string MachineId => info.MachineId;
[MemoryPackInclude]
List<TunnelTransportItemInfo> Data => info.Data;
[MemoryPackConstructor]
SerializableTunnelTransportItemSetInfo(string machineId, List<TunnelTransportItemInfo> data)
{
var info = new TunnelTransportItemSetInfo { MachineId = machineId, Data = data };
this.info = info;
}
public SerializableTunnelTransportItemSetInfo(TunnelTransportItemSetInfo info)
{
this.info = info;
}
}
public class TunnelTransportItemSetInfoFormatter : MemoryPackFormatter<TunnelTransportItemSetInfo>
{
public override void Serialize<TBufferWriter>(ref MemoryPackWriter<TBufferWriter> writer, scoped ref TunnelTransportItemSetInfo value)
{
if (value == null)
{
writer.WriteNullObjectHeader();
return;
}
writer.WritePackable(new SerializableTunnelTransportItemSetInfo(value));
}
public override void Deserialize(ref MemoryPackReader reader, scoped ref TunnelTransportItemSetInfo value)
{
if (reader.PeekIsNull())
{
reader.Advance(1); // skip null block
value = null;
return;
}
var wrapped = reader.ReadPackable<SerializableTunnelTransportItemSetInfo>();
value = wrapped.info;
}
}
}

View File

@@ -1,4 +1,5 @@
using linker.messenger.action;
using linker.libs.extends;
using linker.messenger.action;
namespace linker.messenger.store.file.action
{
public sealed class ActionClientStore : IActionClientStore
@@ -11,15 +12,25 @@ namespace linker.messenger.store.file.action
this.config = config;
}
public void SetActionArg(string action)
public void SetActionDynamicArg(string value)
{
config.Data.Client.Action.Arg = action;
config.Data.Client.Action.Arg = value;
}
public void SetActionArgs(Dictionary<string, string> actions)
public void SetActionStaticArg(string key, string value)
{
config.Data.Client.Action.Args = actions;
config.Data.Client.Action.Args.AddOrUpdate(key, value, (a, b) => value);
}
public string GetActionStaticArg(string key)
{
if (config.Data.Client.Action.Args.TryGetValue(key, out string arg))
{
return arg;
}
return string.Empty;
}
public bool TryAddActionArg(string host, Dictionary<string, string> args)
{
if (string.IsNullOrWhiteSpace(config.Data.Client.Action.Arg) == false)
@@ -37,6 +48,7 @@ namespace linker.messenger.store.file.action
config.Data.Update();
return true;
}
}
}

View File

@@ -23,6 +23,10 @@ namespace linker.messenger.store.file.firewall
runningConfig.Data.Update();
}
public IEnumerable<FirewallRuleInfo> GetAll()
{
return liteCollection.FindAll();
}
public IEnumerable<FirewallRuleInfo> GetAll(FirewallSearchInfo info)
{
IEnumerable<FirewallRuleInfo> list = liteCollection.FindAll()
@@ -77,10 +81,50 @@ namespace linker.messenger.store.file.firewall
}, c => c.Id == rule.Id) > 0;
}
}
public bool Add(List<FirewallRuleInfo> rules)
{
foreach (var rule in rules)
{
if (string.IsNullOrWhiteSpace(rule.Id))
{
rule.Id = ObjectId.NewObjectId().ToString();
liteCollection.Insert(rule);
}
else
{
liteCollection.UpdateMany(p => new FirewallRuleInfo
{
SrcId = rule.SrcId,
SrcName = rule.SrcName,
GroupId = rule.GroupId,
DstCIDR = rule.DstCIDR,
DstPort = rule.DstPort,
Protocol = rule.Protocol,
Action = rule.Action,
OrderBy = rule.OrderBy,
Disabled = rule.Disabled,
Remark = rule.Remark
}, c => c.Id == rule.Id);
}
}
return true;
}
public bool Remove(string id)
{
return liteCollection.Delete(id);
}
public bool Remove(List<string> ids)
{
return liteCollection.DeleteMany(c => ids.Contains(c.Id)) > 0;
}
public bool Check(FirewallCheckInfo info)
{
return liteCollection.UpdateMany(p => new FirewallRuleInfo
{
Checked = info.IsChecked
}, c => info.Ids.Contains(c.Id)) > 0;
}
}
}

View File

@@ -1,4 +1,5 @@
using System.Net;
using linker.tunnel.transport;
using System.Net;
namespace linker.messenger.tunnel
{
@@ -72,4 +73,11 @@ namespace linker.messenger.tunnel
public TunnelNetInfo Net { get; set; } = new TunnelNetInfo();
}
public sealed class TunnelTransportItemSetInfo
{
public string MachineId { get; set; }
public List<TunnelTransportItemInfo> Data { get; set; }
}
}

View File

@@ -120,7 +120,24 @@ namespace linker.messenger.tunnel
/// <returns></returns>
public async Task<List<TunnelTransportItemInfo>> GetTransports(ApiControllerParamsInfo param)
{
return await tunnelClientStore.GetTunnelTransports().ConfigureAwait(false);
if (param.Content == signInClientStore.Id || string.IsNullOrWhiteSpace(param.Content))
{
return await tunnelClientStore.GetTunnelTransports().ConfigureAwait(false);
}
MessageResponeInfo resp = await messengerSender.SendReply(new MessageRequestWrap
{
Connection = signInClientState.Connection,
MessengerId = (ushort)TunnelMessengerIds.TransportGetForward,
Payload = serializer.Serialize(param.Content)
}).ConfigureAwait(false);
if (resp.Code == MessageResponeCodes.OK && resp.Data.Length > 0)
{
return serializer.Deserialize<List<TunnelTransportItemInfo>>(resp.Data.Span);
}
return [];
}
/// <summary>
/// 设置打洞协议
@@ -130,12 +147,21 @@ namespace linker.messenger.tunnel
[Access(AccessValue.Transport)]
public async Task<bool> SetTransports(ApiControllerParamsInfo param)
{
List<TunnelTransportItemInfo> info = param.Content.DeJson<List<TunnelTransportItemInfo>>();
await tunnelClientStore.SetTunnelTransports(info).ConfigureAwait(false);
return true;
TunnelTransportItemSetInfo info = param.Content.DeJson<TunnelTransportItemSetInfo>();
if (info.MachineId == signInClientStore.Id || string.IsNullOrWhiteSpace(info.MachineId))
{
await tunnelClientStore.SetTunnelTransports(info.Data).ConfigureAwait(false);
return true;
}
MessageResponeInfo resp = await messengerSender.SendReply(new MessageRequestWrap
{
Connection = signInClientState.Connection,
MessengerId = (ushort)TunnelMessengerIds.TransportSetForward,
Payload = serializer.Serialize(info)
}).ConfigureAwait(false);
return resp.Code == MessageResponeCodes.OK && resp.Data.Span.SequenceEqual(Helper.TrueArray);
}
public async Task<TunnelLocalNetworkInfo> GetNetwork(ApiControllerParamsInfo param)
{
if (param.Content == signInClientStore.Id)

View File

@@ -104,6 +104,17 @@ namespace linker.messenger.tunnel
{
connection.Write(serializer.Serialize(tunnelNetworkTransfer.GetLocalNetwork()));
}
[MessengerId((ushort)TunnelMessengerIds.TransportGet)]
public void TransportGet(IConnection connection)
{
connection.Write(serializer.Serialize(tunnelClientStore.GetTunnelTransports()));
}
[MessengerId((ushort)TunnelMessengerIds.TransportSet)]
public void TransportSet(IConnection connection)
{
tunnelClientStore.SetTunnelTransports(serializer.Deserialize<List<TunnelTransportItemInfo>>(connection.ReceiveRequestWrap.Payload.Span));
}
}
/// <summary>
@@ -247,5 +258,58 @@ namespace linker.messenger.tunnel
});
}
}
[MessengerId((ushort)TunnelMessengerIds.TransportGetForward)]
public void TransportGetForward(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.TransportGet
}).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.TransportGetForward).ConfigureAwait(false);
}
});
}
}
[MessengerId((ushort)TunnelMessengerIds.TransportSetForward)]
public void TransportSetForward(IConnection connection)
{
TunnelTransportItemSetInfo info = serializer.Deserialize<TunnelTransportItemSetInfo>(connection.ReceiveRequestWrap.Payload.Span);
if (signCaching.TryGet(connection.Id, info.MachineId, out SignCacheInfo from, out SignCacheInfo to))
{
uint requestid = connection.ReceiveRequestWrap.RequestId;
_ = messengerSender.SendReply(new MessageRequestWrap
{
Connection = to.Connection,
MessengerId = (ushort)TunnelMessengerIds.TransportSet,
Payload = serializer.Serialize(info.Data)
}).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.TransportSetForward).ConfigureAwait(false);
}
});
}
}
}
}

View File

@@ -22,6 +22,11 @@
Network = 2013,
NetworkForward = 2014,
TransportGet = 2015,
TransportGetForward = 2016,
TransportSet = 2017,
TransportSetForward = 2018,
None = 2099
}
}

View File

@@ -1,6 +1,4 @@
using linker.libs;
using linker.libs.extends;
using System;
using System.Buffers;
using System.Buffers.Binary;
using System.Collections.Concurrent;
@@ -35,6 +33,7 @@ namespace linker.snat
mapDic = maps.ToFrozenDictionary(x => NetworkHelper.ToNetworkValue(x.FakeIP, x.PrefixLength), x => NetworkHelper.ToNetworkValue(x.RealIP, x.PrefixLength));
masks = maps.Select(x => NetworkHelper.ToPrefixValue(x.PrefixLength)).ToArray();
}
/// <summary>
@@ -94,6 +93,7 @@ namespace linker.snat
if (packet.Span[19] == 255) return;
uint fakeDist = NetworkHelper.ToValue(packet.Span.Slice(16, 4));
for (int i = 0; i < masks.Length; i++)
{
//目标IP网络号存在映射表中找到映射后的真实网络号替换网络号得到最终真实的IP

View File

@@ -88,6 +88,8 @@ namespace linker.snat
error = string.Empty;
try
{
CommandHelper.Windows(string.Empty, ["reg add \"HKLM\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\" /v IPEnableRouter /t REG_DWORD /d 1 /f"]);
Shutdown();
srcIp = NetworkHelper.ToValue(info.Src);
@@ -117,7 +119,9 @@ namespace linker.snat
private static string BuildFilter(AddrInfo[] dsts)
{
IEnumerable<string> ipRanges = dsts.Select(c => $"(ip.SrcAddr >= {c.NetworkIP} and ip.SrcAddr <= {c.BroadcastIP})");
return $"inbound and ({string.Join(" or ", ipRanges)})";
string filter = $"inbound and ({string.Join(" or ", ipRanges)})";
//Console.WriteLine($"filter:{filter}");
return filter;
}
/// <summary>
/// 开始接收数据包
@@ -211,10 +215,13 @@ namespace linker.snat
};
if (result == false) return false;
//Console.WriteLine($"snat inject :{p.IPv4Hdr->SrcAddr}->{p.IPv4Hdr->DstAddr} 替换为 {interfaceAddr}->{p.IPv4Hdr->DstAddr}");
//改写源地址为网卡地址
p.IPv4Hdr->SrcAddr = interfaceAddr;
}
WinDivert.CalcChecksums(p.Packet.Span, ref addr, 0);
winDivert.SendEx(p.Packet.Span, new ReadOnlySpan<WinDivertAddress>(ref addr));
}
}
@@ -260,6 +267,8 @@ namespace linker.snat
natMapInfo.LastTime = Environment.TickCount64;
natMapInfo.Timeout = 15 * 1000;
//Console.WriteLine($"snat inject icmp:{*ptr0}->{identifier0},{*ptr1}->{identifier1}");
//改写为新的标识符
*ptr0 = identifier0;
*ptr1 = identifier1;
@@ -286,10 +295,13 @@ namespace linker.snat
ValueTuple<uint, ushort, uint, ushort, ProtocolType> key = (p.IPv4Hdr->DstAddr.Raw, *ptr0, p.IPv4Hdr->SrcAddr.Raw, *ptr1, ProtocolType.Icmp);
if (natMap.TryGetValue(key, out NatMapInfo natMapInfo))
{
//Console.WriteLine($"snat recv icmp:{*ptr0}->{natMapInfo.Identifier0},{*ptr1}->{natMapInfo.Identifier1}");
//改回原来的标识符
*ptr0 = natMapInfo.Identifier0;
*ptr1 = natMapInfo.Identifier1;
//Console.WriteLine($"icmp recv:{p.IPv4Hdr->SrcAddr}->{p.IPv4Hdr->DstAddr} 替换为 {p.IPv4Hdr->SrcAddr}->{natMapInfo.SrcAddr}");
p.IPv4Hdr->DstAddr = natMapInfo.SrcAddr;
return true;
}
@@ -645,6 +657,7 @@ namespace linker.snat
uint value = NetworkHelper.ToValue(Address);
uint network = NetworkHelper.ToNetworkValue(value, NetworkHelper.ToValue(IPv4Mask));
network2ipMap.TryAdd(network, (value, IPv4Addr.Parse(Address.ToString())));
}
}

View File

@@ -166,9 +166,13 @@ namespace linker.tunnel.connection
}
catch (Exception ex)
{
LoggerHelper.Instance.Error(ex);
if (LoggerHelper.Instance.LoggerLevel <= LoggerTypes.DEBUG)
{
LoggerHelper.Instance.Error(ex);
LoggerHelper.Instance.Error(Encoding.UTF8.GetString(memory.Span));
LoggerHelper.Instance.Error(string.Join(",", memory.ToArray()));
}
}
}
}

View File

@@ -1,5 +1,9 @@
import { sendWebsocketMsg } from './request'
export const setArgs = (args) => {
return sendWebsocketMsg('action/SetServerArgs', args);
export const getArgs = (machineid) => {
return sendWebsocketMsg('action/GetServerArgs', machineid);
}
export const setArgs = (keyvalue) => {
return sendWebsocketMsg('action/SetServerArgs', keyvalue);
}

View File

@@ -11,4 +11,7 @@ export const removeFirewall = (data) => {
}
export const stateFirewall = (data) => {
return sendWebsocketMsg('firewall/state', data);
}
export const checkFirewall = (data) => {
return sendWebsocketMsg('firewall/check', data);
}

View File

@@ -17,8 +17,8 @@ export const setTunnelRouteLevel = (data) => {
return sendWebsocketMsg('tunnel/SetRouteLevel', data);
}
export const getTunnelTransports = () => {
return sendWebsocketMsg('tunnel/GetTransports');
export const getTunnelTransports = (machineid) => {
return sendWebsocketMsg('tunnel/GetTransports',machineid);
}
export const setTunnelTransports = (data) => {
return sendWebsocketMsg('tunnel/SetTransports', data);

View File

@@ -254,6 +254,8 @@ export default {
'server.asyncUpdaterSecretKey': 'Update secretKey',
'server.asyncTunnelTransports': 'Tunnel transports',
'server.asyncSignInUserId': 'User Id',
'server.asyncActionStatic': 'Action args',
'server.asyncFirewall': 'Firewall selected item',
'firewall.rule': 'Firewall rule',
'firewall.srcName': 'Src Device',

View File

@@ -344,6 +344,8 @@ export default {
'server.asyncUpdaterSecretKey': '更新密钥',
'server.asyncTunnelTransports': '打洞协议',
'server.asyncSignInUserId': '用户唯一标识',
'server.asyncActionStatic': '自定义验证参数',
'server.asyncFirewall': '防火墙选中项',
'firewall.rule': '防火墙协议',
'firewall.srcName': '源设备',

View File

@@ -0,0 +1,73 @@
<template>
<el-card shadow="never">
<template #header>
<div class="flex">
<span class="flex-1">{{$t('action.text')}}</span>
<Sync name="ActionStatic" v-if="state.isSelf"></Sync>
</div>
</template>
<div>
<el-input v-model="state.data" :rows="10" type="textarea" resize="none" @change="handleSave" />
</div>
<template #footer>
<div class="t-c">
<el-button type="success" @click="handleSave">{{$t('common.confirm')}}</el-button>
</div>
</template>
</el-card>
</template>
<script>
import { getArgs, setArgs } from '@/apis/action';
import { injectGlobalData } from '@/provide';
import { ElMessage } from 'element-plus';
import { computed, onMounted, reactive } from 'vue'
import { useI18n } from 'vue-i18n'
import Sync from '../sync/Index.vue'
export default {
props:['machineId'],
components: { Sync },
setup(props) {
const { t } = useI18n();
const globalData = injectGlobalData();
const state = reactive({
data: '',
machineId: props.machineId || globalData.value.config.Client.Id,
isSelf:computed(()=>{
return state.machineId == globalData.value.config.Client.Id;
})
});
const loadData = () => {
getArgs(state.machineId).then((res) => {
state.data = res;
});
}
const handleSave = () => {
try {
if (state.data && typeof (JSON.parse(state.data)) != 'object') {
ElMessage.error(t('action.jsonError'));
return;
}
} catch (e) {
ElMessage.error(t('action.jsonError'));
return;
}
setArgs({
Key: state.machineId,
Value: state.data
}).then(() => {
ElMessage.success(t('common.oper'));
}).catch((err) => {
console.log(err);
ElMessage.error(t('common.operFail'));
});
}
onMounted(()=>{
loadData();
})
return { state, handleSave }
}
}
</script>
<style lang="stylus" scoped>
</style>

View File

@@ -1,52 +1,19 @@
<template>
<div class="action-wrap">
<el-card shadow="never">
<template #header>{{$t('action.text')}}</template>
<div>
<el-input v-model="state.list" :rows="10" type="textarea" resize="none" @change="handleSave" />
</div>
<template #footer>
<div class="t-c">
<el-button type="success" @click="handleSave">{{$t('common.confirm')}}</el-button>
</div>
</template>
</el-card>
<Action :machineId="state.machineId"></Action>
</div>
</template>
<script>
import { setArgs } from '@/apis/action';
import { injectGlobalData } from '@/provide';
import { ElMessage } from 'element-plus';
import { reactive } from 'vue'
import { useI18n } from 'vue-i18n'
import Action from './Action.vue';
export default {
props:['machineId'],
components:{Action},
setup(props) {
const { t } = useI18n();
const globalData = injectGlobalData();
const state = reactive({
list: globalData.value.config.Client.Action.Args[globalData.value.config.Client.Server.Host] || ''
machineId: props.machineId,
});
const handleSave = () => {
try {
if (state.list && typeof (JSON.parse(state.list)) != 'object') {
ElMessage.error(t('action.jsonError'));
return;
}
} catch (e) {
ElMessage.error(t('action.jsonError'));
return;
}
const json = JSON.parse(JSON.stringify(globalData.value.config.Client.Action.Args));
json[globalData.value.config.Client.Server.Host] = state.list;
setArgs(json).then(() => {
ElMessage.success(t('common.oper'));
}).catch((err) => {
console.log(err);
ElMessage.error(t('common.operFail'));
});;
}
return { state, handleSave }
return { state }
}
}
</script>

View File

@@ -74,7 +74,7 @@
</el-table>
</div>
</el-dialog>
<el-dialog v-model="state.showNodes" :title="$t('server.relayTitle')" width="760" top="2vh">
<el-dialog v-model="state.showNodes" :title="$t('server.relayTitle')" width="98%" top="2vh">
<div>
<el-table :data="state.nodes" size="small" border height="600">
<el-table-column property="Name" :label="$t('server.relayName')">

View File

@@ -33,6 +33,8 @@
<OperRoutes v-if="oper.showRoutes" v-model="oper.showRoutes" ></OperRoutes>
<OperFirewall v-if="oper.showFirewall" v-model="oper.showFirewall" ></OperFirewall>
<OperWakeup v-if="oper.showWakeup" v-model="oper.showWakeup" ></OperWakeup>
<OperTransport v-if="oper.showTransport" v-model="oper.showTransport" ></OperTransport>
<OperAction v-if="oper.showAction" v-model="oper.showAction" ></OperAction>
</div>
</template>
<script>
@@ -85,6 +87,8 @@ import { provideOper } from './oper'
import OperRoutes from './OperRoutes.vue'
import OperFirewall from './OperFirewall.vue'
import OperWakeup from './OperWakeup.vue'
import OperTransport from './OperTransport.vue'
import OperAction from './OperAction.vue'
export default {
@@ -98,7 +102,7 @@ export default {
Forward,ForwardEdit,
SForwardEdit ,UpdaterConfirm,
Stopwatch,
Oper,OperRoutes,OperFirewall,OperWakeup
Oper,OperRoutes,OperFirewall,OperWakeup ,OperTransport,OperAction
},
setup(props) {

View File

@@ -24,6 +24,11 @@
<el-dropdown-item v-if="scope.row.isSelf && hasWakeupSelf" @click="handleWakeup(scope.row.MachineId,scope.row.MachineName)"><el-icon><VideoPlay /></el-icon>唤醒</el-dropdown-item>
<el-dropdown-item v-else-if="hasWakeupOther" @click="handleWakeup(scope.row.MachineId,scope.row.MachineName)"><el-icon><VideoPlay /></el-icon>唤醒</el-dropdown-item>
<el-dropdown-item v-if="hasTransport" @click="handleTransport(scope.row.MachineId,scope.row.MachineName)"><el-icon><Orange /></el-icon>打洞协议</el-dropdown-item>
<el-dropdown-item v-if="scope.row.isSelf && hasActionSelf" @click="handleAction(scope.row.MachineId,scope.row.MachineName)"><el-icon><Lock /></el-icon>验证参数</el-dropdown-item>
<el-dropdown-item v-else-if="hasActionOther" @click="handleAction(scope.row.MachineId,scope.row.MachineName)"><el-icon><Lock /></el-icon>验证参数</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
@@ -36,18 +41,17 @@
import { signInDel } from '@/apis/signin';
import { exit } from '@/apis/updater';
import { injectGlobalData } from '@/provide';
import { Delete,SwitchButton,ArrowDown, Flag,HelpFilled,Platform,Paperclip,CircleCheck,VideoPlay } from '@element-plus/icons-vue'
import { Delete,SwitchButton,ArrowDown, Flag,HelpFilled,Platform,Paperclip,CircleCheck,VideoPlay,Orange,Lock } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus';
import { computed } from 'vue';
import { useAccess } from './access';
import { setApiPassword } from '@/apis/access';
import { useFlow } from './flow';
import { useTuntap } from './tuntap';
import { useOper } from './oper';
export default {
emits:['refresh','access'],
components:{Delete,SwitchButton,ArrowDown,Flag,HelpFilled,Platform,Paperclip,CircleCheck,VideoPlay},
components:{Delete,SwitchButton,ArrowDown,Flag,HelpFilled,Platform,Paperclip,CircleCheck,VideoPlay,Orange,Lock},
setup (props,{emit}) {
const globalData = injectGlobalData();
@@ -68,6 +72,10 @@ export default {
const hasWakeupSelf = computed(()=>globalData.value.hasAccess('WakeupSelf'));
const hasWakeupOther = computed(()=>globalData.value.hasAccess('WakeupOther'));
const hasTransport = computed(()=>globalData.value.hasAccess('Transport'));
const hasActionSelf = computed(()=>globalData.value.hasAccess('Action'));
const hasActionOther = computed(()=>globalData.value.hasAccess('ActionOther'));
const flow = useFlow();
const oper = useOper();
@@ -145,10 +153,21 @@ export default {
oper.value.device.name = name;
oper.value.showWakeup = true;
}
const handleTransport = (id,name)=>{
oper.value.device.id = id;
oper.value.device.name = name;
oper.value.showTransport = true;
}
const handleAction = (id,name)=>{
oper.value.device.id = id;
oper.value.device.name = name;
oper.value.showAction = true;
}
return {accessList,handleDel,handleExit,hasReboot,hasRemove,hasAccess,handleShowAccess,handleAccess,
hasApiPassword,hasApiPasswordOther,handleApiPassword,handleStopwatch,handleRoutes,
hasFirewallSelf,hasFirewallOther,handleFirewall,hasWakeupSelf,hasWakeupOther,handleWakeup
hasFirewallSelf,hasFirewallOther,handleFirewall,hasWakeupSelf,hasWakeupOther,handleWakeup,
hasTransport,handleTransport,hasActionSelf,hasActionOther,handleAction
}
}
}

View File

@@ -0,0 +1,42 @@
<template>
<el-dialog v-model="state.show" append-to=".app-wrap" :title="`[${state.machineName}]上的验证参数`" top="1vh" width="98%">
<div>
<Action :machineId="state.machineId"></Action>
</div>
</el-dialog>
</template>
<script>
import { reactive, watch } from 'vue';
import Action from '../action/Action.vue'
import { useOper } from './oper';
export default {
props: ['modelValue'],
emits: ['update:modelValue'],
components: {
Action
},
setup(props, { emit }) {
const oper = useOper();
const state = reactive({
show: true,
machineId: oper.value.device.id,
machineName: oper.value.device.name
});
watch(() => state.show, (val) => {
if (!val) {
setTimeout(() => {
emit('update:modelValue', val);
emit('change')
}, 300);
}
});
return {
state
}
}
}
</script>
<style lang="stylus" scoped>
</style>

View File

@@ -1,5 +1,5 @@
<template>
<el-dialog v-model="state.show" append-to=".app-wrap" :title="`[${state.machineName}]上的防火墙`" top="1vh" width="760">
<el-dialog v-model="state.show" append-to=".app-wrap" :title="`[${state.machineName}]上的防火墙`" top="1vh" width="98%">
<div>
<Firewall :machineId="state.machineId"></Firewall>
</div>

View File

@@ -0,0 +1,42 @@
<template>
<el-dialog v-model="state.show" append-to=".app-wrap" :title="`[${state.machineName}]上的打洞协议`" top="1vh" width="98%">
<div>
<Transport :machineId="state.machineId"></Transport>
</div>
</el-dialog>
</template>
<script>
import { reactive, watch } from 'vue';
import Transport from '../transport/Transport.vue'
import { useOper } from './oper';
export default {
props: ['modelValue'],
emits: ['update:modelValue'],
components: {
Transport
},
setup(props, { emit }) {
const oper = useOper();
const state = reactive({
show: true,
machineId: oper.value.device.id,
machineName: oper.value.device.name
});
watch(() => state.show, (val) => {
if (!val) {
setTimeout(() => {
emit('update:modelValue', val);
emit('change')
}, 300);
}
});
return {
state
}
}
}
</script>
<style lang="stylus" scoped>
</style>

View File

@@ -1,5 +1,5 @@
<template>
<el-dialog v-model="state.show" append-to=".app-wrap" :title="`[${state.machineName}]上的唤醒`" top="1vh" width="760">
<el-dialog v-model="state.show" append-to=".app-wrap" :title="`[${state.machineName}]上的唤醒`" top="1vh" width="98%">
<div>
<Wakeup :machineId="state.machineId"></Wakeup>
</div>

View File

@@ -1,6 +1,6 @@
<template>
<PlanList ref="planDom" :machineid="machineId" category="sforward" :handles="state.handles">
<el-dialog v-model="state.show" @open="handleOnShowList" append-to=".app-wrap" :title="`【${machineName}】的内网穿透`" top="2vh" width="760">
<el-dialog v-model="state.show" @open="handleOnShowList" append-to=".app-wrap" :title="`【${machineName}】的内网穿透`" top="2vh" width="98%">
<div>
<div class="t-c head">
<el-button type="success" size="small" @click="handleAdd" :loading="state.loading">添加</el-button>

View File

@@ -6,6 +6,8 @@ export const provideOper = () => {
showRoutes:false,
showFirewall:false,
showWakeup:false,
showTransport:false,
showAction:false,
});
provide(operSymbol, oper);
return {

View File

@@ -30,12 +30,23 @@
<el-button type="success" size="small" :loading="state.loading" @click="handleAdd()">+</el-button>
</div>
</div>
<div class="flex-1"></div>
<div class="flex-1"></div>
<div class="mgt-1" v-if="state.isSelf">
<Sync name="Firewall"></Sync>
</div>
</div>
</div>
<div class="body flex-1 relative">
<el-table class="firewall" stripe border :data="state.data" size="small" :height="`${state.height}px`" :row-class-name="tableRowClassName">
<el-table-column prop="SrcName" :label="$t('firewall.srcName')" >
<el-table-column prop="Checked" width="30" v-if="state.isSelf" >
<template #header>
<el-checkbox size="small" v-model="state.checkAll" :indeterminate="state.checkAllIndeterminate" @change="handleCheckAllChange"></el-checkbox>
</template>
<template #default="scope">
<el-checkbox v-model="scope.row.Checked" size="small" @change="handleChecked(scope.row)"></el-checkbox>
</template>
</el-table-column>
<el-table-column prop="SrcName" :label="$t('firewall.srcName')" >
<template v-slot="scope">
<div class="ellipsis" :title="scope.row.SrcName">{{ scope.row.SrcName }}</div>
</template>
@@ -45,11 +56,11 @@
<el-table-column prop="Protocol" :label="$t('firewall.protocol')" width="70">
<template v-slot="scope">{{handleShowProtocol(scope.row.Protocol)}}</template>
</el-table-column>
<el-table-column prop="Action" :label="$t('firewall.action')" width="50">
<el-table-column prop="Action" :label="$t('firewall.action')" width="56">
<template v-slot="scope">{{handleShowAction(scope.row.Action)}}</template>
</el-table-column>
<el-table-column prop="OrderBy" :label="$t('firewall.orderby')" width="60"></el-table-column>
<el-table-column prop="Disabled" :label="$t('firewall.disabled')" width="70">
<el-table-column prop="OrderBy" :label="$t('firewall.orderby')" width="56"></el-table-column>
<el-table-column prop="Disabled" :label="$t('firewall.disabled')" width="66">
<template v-slot="scope">
<div>
<el-switch v-model="scope.row.Disabled" size="small"
@@ -63,7 +74,7 @@
<div class="ellipsis" :title="scope.row.Remark">{{ scope.row.Remark }}</div>
</template>
</el-table-column>
<el-table-column width="80" fixed="right">
<el-table-column width="60" fixed="right">
<template #header>
<div class="flex">
<el-switch v-model="state.state" size="small" :title="$t('firewall.switch')"
@@ -73,14 +84,15 @@
</template>
<template #default="scope">
<div>
<a href="javascript:void(0);" class="a-line mgr-1" @click="handleAdd(scope.row)">{{$t('firewall.edit')}}</a>
<el-popconfirm
:confirm-button-text="$t('common.confirm')" :cancel-button-text="$t('common.cancel')"
:title="$t('firewall.delConfirm')" @confirm="handleDel(scope.row)">
<template #reference>
<a href="javascript:void(0);" class="a-line">{{$t('firewall.del')}}</a>
<el-dropdown>
<span class="el-dropdown-link">{{$t('common.option')}}<el-icon><ArrowDown /></el-icon></span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="handleAdd(scope.row)">{{$t('firewall.edit')}}</el-dropdown-item>
<el-dropdown-item @click="handleDel(scope.row)">{{$t('firewall.del')}}</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-popconfirm>
</el-dropdown>
</div>
</template>
</el-table-column>
@@ -93,13 +105,15 @@
import { reactive,computed, ref } from '@vue/reactivity'
import { onMounted, provide } from '@vue/runtime-core'
import { injectGlobalData } from '@/provide'
import { addFirewall, getFirewall, removeFirewall, stateFirewall } from '@/apis/firewall';
import { addFirewall, checkFirewall, getFirewall, removeFirewall, stateFirewall } from '@/apis/firewall';
import { useI18n } from 'vue-i18n';
import { ElMessage } from 'element-plus';
import { ElMessage, ElMessageBox } from 'element-plus';
import {ArrowDown} from'@element-plus/icons-vue'
import Add from './Add.vue';
import Sync from '../sync/Index.vue'
export default {
props: ['machineId','machineName'],
components:{Add},
components:{Add,Sync,ArrowDown},
setup(props,{emit}) {
const {t} = useI18n();
@@ -107,6 +121,8 @@ export default {
const globalData = injectGlobalData();
const state = reactive({
loading: true,
checkAll:false,
checkAllIndeterminate:false,
search:{
MachineId:props.machineId || globalData.value.config.Client.Id,
Data:{
@@ -135,7 +151,10 @@ export default {
data:[],
state:1,
height:computed(()=>globalData.value.height - 140),
showAdd:false
showAdd:false,
isSelf:computed(()=>{
return state.search.MachineId == globalData.value.config.Client.Id;
})
})
const loadData = () => {
state.loading = true;
@@ -143,12 +162,40 @@ export default {
state.loading = false;
state.data = res.List;
state.state = res.State;
checkAllStatus();
}).catch((err) => {
console.log(err);
state.loading = false;
});
}
const checkAllStatus = ()=>{
state.checkAll = state.data.some(item=>item.Checked);
state.checkAllIndeterminate = state.checkAll && state.data.every(item=>item.Checked) == false;
}
const handleCheckAllChange = (value)=>{
state.data.forEach(c=>{
c.Checked = value;
});
checkAllStatus();
const ids = state.data.map(item=>item.Id);
if(ids.length > 0){
_checkFirewall(ids,value);
}
}
const handleChecked = (value)=>{
checkAllStatus();
_checkFirewall([value.Id],value.Checked);
}
const _checkFirewall = (ids,value)=>{
state.loading = true;
checkFirewall({
Ids:ids,
IsChecked:value
}).then(()=>{state.loading = false;}).catch(()=>{state.loading = false;});
}
const handleSetState = ()=>{
state.loading = true;
stateFirewall({
@@ -164,11 +211,17 @@ export default {
});
}
const handleDel = (row) => {
state.loading = true;
removeFirewall({
MachineId:state.search.MachineId,
Id:row.Id
}).then(()=>{loadData(); state.loading = false;}).catch(()=>{state.loading = false;});
ElMessageBox.confirm(t('firewall.delConfirm'), t('common.confirm'), {
confirmButtonText: t('common.confirm'),
cancelButtonText: t('common.cancel'),
type: 'warning',
}).then(() => {
state.loading = true;
removeFirewall({
MachineId:state.search.MachineId,
Id:row.Id
}).then(()=>{loadData(); state.loading = false;}).catch(()=>{state.loading = false})
}).catch(()=>{});
}
const handleDsiabled = (row)=>{
state.loading = true;
@@ -223,7 +276,8 @@ export default {
return {
state, loadData, tableRowClassName,handleSetState,handleAdd,handleDel,
handleShowProtocol,handleShowAction,handleDsiabled
handleShowProtocol,handleShowAction,handleDsiabled,
handleCheckAllChange,handleChecked
}
}
}
@@ -246,4 +300,8 @@ export default {
color: #c83f08;
}
}
.el-dropdown-link{
font-size:1.2rem;
padding-top:.3rem;
}
</style>

View File

@@ -1,9 +1,6 @@
<template>
<div>
<el-form ref="formDom" :model="state.form" :rules="state.rules" label-width="12rem">
<el-form-item label="" label-width="0">
<div class="t-c w-100">端口为0则不监听</div>
</el-form-item>
<el-form-item label="" label-width="0">
<el-row>
<el-col :span="12">

View File

@@ -21,7 +21,7 @@
</div>
</el-form-item>
<el-dialog v-model="state.show" :title="$t('server.relayTitle')" width="760" top="2vh">
<el-dialog v-model="state.show" :title="$t('server.relayTitle')" width="98%" top="2vh">
<div>
<el-table :data="state.nodes" size="small" border height="500">
<el-table-column property="Name" :label="$t('server.relayName')">

View File

@@ -1,93 +1,27 @@
<template>
<div class="transport-wrap">
<el-table stripe :data="state.list" border size="small" width="100%" :height="`${state.height}px`" >
<el-table-column prop="Name" :label="$t('status.tunnelName')" width="120"></el-table-column>
<el-table-column prop="Label" :label="$t('status.tunnelLabel')"></el-table-column>
<el-table-column prop="ProtocolType" :label="$t('status.tunnelProtocol')" width="60"></el-table-column>
<el-table-column prop="BufferSize" :label="$t('status.tunnelBuffer')" width="100">
<template #default="scope">
<el-select v-model="scope.row.BufferSize" placeholder="Select" size="small" @change="handleSave">
<el-option v-for="(item,index) in state.bufferSize" :key="index" :label="item" :value="index"/>
</el-select>
</template>
</el-table-column>
<el-table-column property="Reverse" :label="$t('status.tunnelReverse')" width="64">
<template #default="scope">
<el-switch :disabled="scope.row.DisableReverse" v-model="scope.row.Reverse" @change="handleSave" inline-prompt :active-text="$t('status.tunnelYes')" :inactive-text="$t('status.tunnelNo')" />
</template>
</el-table-column>
<el-table-column property="SSL" :label="$t('status.tunnelSSL')" width="60">
<template #default="scope">
<el-switch :disabled="scope.row.DisableSSL" v-model="scope.row.SSL" @change="handleSave" inline-prompt :active-text="$t('status.tunnelYes')" :inactive-text="$t('status.tunnelNo')" />
</template>
</el-table-column>
<el-table-column property="Disabled" :label="$t('status.tunnelDisanbled')" width="64">
<template #default="scope">
<el-switch v-model="scope.row.Disabled" @change="handleSave" inline-prompt :active-text="$t('status.tunnelYes')" :inactive-text="$t('status.tunnelNo')" style="--el-switch-on-color: red; --el-switch-off-color: #ddd" />
</template>
</el-table-column>
<el-table-column prop="Order" :label="$t('status.tunnelSort')" width="104" fixed="right">
<template #header>
<div class="flex">
<strong>{{ $t('status.tunnelSort') }}</strong><span class="flex-1"></span><Sync name="TunnelTransports"></Sync>
</div>
</template>
<template #default="scope">
<div>
<el-input-number v-model="scope.row.Order" :min="1" :max="255" @change="handleOrderChange" size="small" />
</div>
</template>
</el-table-column>
</el-table>
<Transport :machineId="state.machineId" :height="state.height"></Transport>
</div>
</template>
<script>
import { setTunnelTransports } from '@/apis/tunnel';
import { computed, reactive } from 'vue';
import Transport from './Transport.vue'
import { injectGlobalData } from '@/provide';
import { ElMessage } from 'element-plus';
import { computed,reactive, watch } from 'vue'
import { Delete,Plus,Top,Bottom } from '@element-plus/icons-vue';
import { useI18n } from 'vue-i18n';
import Sync from '../sync/Index.vue'
export default {
label:'打洞协议',
name:'transports',
order:2,
components:{Delete,Plus,Top,Bottom,Sync},
setup(props) {
const {t} = useI18n();
props:['machineId'],
components:{Transport},
setup(props) {
const globalData = injectGlobalData();
const state = reactive({
list:globalData.value.config.Client.Tunnel.Transports.sort((a,b)=>a.Order - b.Order),
height: computed(()=>globalData.value.height-20),
bufferSize:globalData.value.bufferSize
});
watch(()=>globalData.value.config.Client.Tunnel.Transports,()=>{
state.list = globalData.value.config.Client.Tunnel.Transports.sort((a,b)=>a.Order - b.Order);
});
const handleOrderChange = ()=>{
handleSave(state.list);
}
const handleSave = ()=>{
state.list = state.list.slice().sort((a,b)=>a.Order - b.Order);
setTunnelTransports(state.list).then(()=>{
ElMessage.success(t('common.oper'));
}).catch((err)=>{
console.log(err);
ElMessage.error(t('common.operFail'));
});
}
return {state,handleOrderChange,handleSave}
machineId:props.machineId,
height:computed(()=>globalData.value.height-20)
})
return {state}
}
}
</script>
<style lang="stylus" scoped>
.transport-wrap{
padding:1rem
font-size:1.3rem;
color:#555;
a{color:#333;}
}
</style>

View File

@@ -0,0 +1,96 @@
<template>
<el-table stripe :data="state.list" border size="small" width="100%" :height="`${state.height}px`" >
<el-table-column prop="Name" :label="$t('status.tunnelName')" width="120"></el-table-column>
<el-table-column prop="Label" :label="$t('status.tunnelLabel')"></el-table-column>
<el-table-column prop="ProtocolType" :label="$t('status.tunnelProtocol')" width="60"></el-table-column>
<el-table-column prop="BufferSize" :label="$t('status.tunnelBuffer')" width="100">
<template #default="scope">
<el-select v-model="scope.row.BufferSize" placeholder="Select" size="small" @change="handleSave">
<el-option v-for="(item,index) in state.bufferSize" :key="index" :label="item" :value="index"/>
</el-select>
</template>
</el-table-column>
<el-table-column property="Reverse" :label="$t('status.tunnelReverse')" width="64">
<template #default="scope">
<el-switch :disabled="scope.row.DisableReverse" v-model="scope.row.Reverse" @change="handleSave" inline-prompt :active-text="$t('status.tunnelYes')" :inactive-text="$t('status.tunnelNo')" />
</template>
</el-table-column>
<el-table-column property="SSL" :label="$t('status.tunnelSSL')" width="60">
<template #default="scope">
<el-switch :disabled="scope.row.DisableSSL" v-model="scope.row.SSL" @change="handleSave" inline-prompt :active-text="$t('status.tunnelYes')" :inactive-text="$t('status.tunnelNo')" />
</template>
</el-table-column>
<el-table-column property="Disabled" :label="$t('status.tunnelDisanbled')" width="64">
<template #default="scope">
<el-switch v-model="scope.row.Disabled" @change="handleSave" inline-prompt :active-text="$t('status.tunnelYes')" :inactive-text="$t('status.tunnelNo')" style="--el-switch-on-color: red; --el-switch-off-color: #ddd" />
</template>
</el-table-column>
<el-table-column prop="Order" :label="$t('status.tunnelSort')" width="104" fixed="right">
<template #header>
<div class="flex">
<strong>{{ $t('status.tunnelSort') }}</strong><span class="flex-1"></span><Sync name="TunnelTransports" v-if="state.isSelf"></Sync>
</div>
</template>
<template #default="scope">
<div>
<el-input-number v-model="scope.row.Order" :min="1" :max="255" @change="handleOrderChange" size="small" />
</div>
</template>
</el-table-column>
</el-table>
</template>
<script>
import { getTunnelTransports, setTunnelTransports } from '@/apis/tunnel';
import { injectGlobalData } from '@/provide';
import { ElMessage } from 'element-plus';
import { computed,onMounted,reactive, watch } from 'vue'
import { Delete,Plus,Top,Bottom } from '@element-plus/icons-vue';
import { useI18n } from 'vue-i18n';
import Sync from '../sync/Index.vue'
export default {
props:['machineId','height'],
components:{Delete,Plus,Top,Bottom,Sync},
setup(props) {
const {t} = useI18n();
const globalData = injectGlobalData();
const state = reactive({
list:[],
height:computed(()=>props.height),
bufferSize:globalData.value.bufferSize,
machineid:props.machineId || globalData.value.config.Client.Id,
isSelf:computed(()=>{
return state.machineid === globalData.value.config.Client.Id;
})
});
const getData = ()=>{
getTunnelTransports(state.machineid).then((res)=>{
state.list = res.sort((a,b)=>a.Order - b.Order);
});
}
const handleOrderChange = ()=>{
handleSave(state.list);
}
const handleSave = ()=>{
state.list = state.list.slice().sort((a,b)=>a.Order - b.Order);
setTunnelTransports({
machineid:state.machineid,
data:state.list
}).then(()=>{
ElMessage.success(t('common.oper'));
}).catch((err)=>{
console.log(err);
ElMessage.error(t('common.operFail'));
});
}
onMounted(()=>{
getData();
});
return {state,handleOrderChange,handleSave}
}
}
</script>
<style lang="stylus" scoped>
</style>

View File

@@ -1,5 +1,5 @@
v1.8.3
2025-06-14 20:50:30
2025-06-16 17:35:00
1. 一些累计更新
2. 修复socks5解决CPU爆满问题增加本地域名解析支持HTTP代理
3. 优化唤醒模块