This commit is contained in:
snltty
2025-04-24 17:28:28 +08:00
parent e20b01a4b7
commit 16c0e3cfef
26 changed files with 769 additions and 141 deletions

View File

@@ -1,4 +1,6 @@
namespace linker.app
using linker.libs.extends;
namespace linker.app
{
public partial class App : Application
{
@@ -10,6 +12,8 @@
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
MainPage = new MainPage();
}

View File

@@ -14,9 +14,6 @@ namespace linker.app
{
webview.Source = new Uri($"http://127.0.0.1:1804?t={DateTime.Now.Ticks}");
};
}
}
}

View File

@@ -88,7 +88,9 @@ namespace linker.app
}
/// <summary>
/// VPN服务先启动服务才能创建VPN连接
/// </summary>
[Service(Label = "VpnServiceLinker", Name = "com.snltty.linker.app.VpnServiceLinker", Enabled = true, Permission = "android.permission.BIND_VPN_SERVICE")]
public class VpnServiceLinker : VpnService, ILinkerTunDeviceCallback, ITuntapProxyCallback
{
@@ -142,6 +144,9 @@ namespace linker.app
}
/// <summary>
/// 前台服务运行Linker
/// </summary>
[Service(Label = "ForegroundService", Name = "com.snltty.linker.app.ForegroundService", Exported = true)]
[IntentFilter(new string[] { "com.snltty.linker.app.ForegroundService" })]
public sealed class ForegroundService : Service
@@ -214,6 +219,7 @@ namespace linker.app
LinkerMessengerEntry.Initialize();
LinkerMessengerEntry.AddService<IWebServerFileReader, WebServerFileReader>();
LinkerMessengerEntry.AddService<ISystemInformation, SystemInformation>();
LinkerMessengerEntry.Build();
LinkerMessengerEntry.Setup(ExcludeModule.Logger, config);
IPlatformApplication.Current.Services.GetService<InitializeService>().SendOnInitialized();
@@ -284,6 +290,9 @@ namespace linker.app
}
}
/// <summary>
/// VPN设备
/// </summary>
public sealed class LinkerVpnDevice : ILinkerTunDevice
{
private string name = string.Empty;
@@ -487,6 +496,9 @@ namespace linker.app
}
}
/// <summary>
/// 管理页面的文件读取跟PC的不太一样
/// </summary>
public sealed class WebServerFileReader : IWebServerFileReader
{
ReceiveDataBuffer receiveDataBuffer = new ReceiveDataBuffer();
@@ -503,5 +515,18 @@ namespace linker.app
}
}
/// <summary>
/// 获取系统信息
/// </summary>
public sealed class SystemInformation: ISystemInformation
{
public string Get()
{
var deviceInfo = DeviceInfo.Current;
return $"{deviceInfo.Manufacturer} {deviceInfo.Name} {deviceInfo.VersionString} {deviceInfo.Platform} {deviceInfo.Idiom.ToString()}";
}
}
}

View File

@@ -2,14 +2,18 @@
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<IsFirstTimeProjectOpen>False</IsFirstTimeProjectOpen>
<ActiveDebugFramework>net8.0-android</ActiveDebugFramework>
<ActiveDebugProfile>Pixel 5 - API 34 (Android 14.0 - API 34)</ActiveDebugProfile>
<SelectedPlatformGroup>Emulator</SelectedPlatformGroup>
<ActiveDebugFramework>net8.0-ios</ActiveDebugFramework>
<ActiveDebugProfile>模拟器</ActiveDebugProfile>
<SelectedPlatformGroup>Simulator</SelectedPlatformGroup>
<DefaultDevice>pixel_5_-_api_34</DefaultDevice>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-android|AnyCPU'">
<DebuggerFlavor>ProjectDebugger</DebuggerFlavor>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetPlatformIdentifier)'=='iOS'">
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
<ItemGroup>
<None Update="App.xaml">
<SubType>Designer</SubType>

View File

@@ -4,6 +4,8 @@ using System.IO.Compression;
using linker.libs;
using linker.messenger.signin;
using linker.messenger.api;
using System.Text;
using linker.messenger.relay.client.transport;
namespace linker.messenger.store.file
{
public sealed class ConfigApiController : IApiController
@@ -14,8 +16,9 @@ namespace linker.messenger.store.file
private readonly IMessengerSender sender;
private readonly SignInClientState signInClientState;
private readonly IApiStore apiStore;
private readonly ExportResolver exportResolver;
public ConfigApiController(RunningConfig runningConfig, FileConfig config, SignInClientTransfer signInClientTransfer, IMessengerSender sender, SignInClientState signInClientState, IApiStore apiStore)
public ConfigApiController(RunningConfig runningConfig, FileConfig config, SignInClientTransfer signInClientTransfer, IMessengerSender sender, SignInClientState signInClientState, IApiStore apiStore, ExportResolver exportResolver)
{
this.runningConfig = runningConfig;
this.config = config;
@@ -23,6 +26,7 @@ namespace linker.messenger.store.file
this.sender = sender;
this.signInClientState = signInClientState;
this.apiStore = apiStore;
this.exportResolver = exportResolver;
}
public object Get(ApiControllerParamsInfo param)
@@ -79,6 +83,80 @@ namespace linker.messenger.store.file
return true;
}
public bool InstallCopy(ApiControllerParamsInfo param)
{
try
{
Dictionary<string, string> dic = Encoding.UTF8.GetString(Convert.FromBase64String(param.Content)).DeJson<Dictionary<string, string>>();
config.Save(dic);
return true;
}
catch (Exception)
{
}
return false;
}
public async Task<bool> InstallSave(ApiControllerParamsInfo param)
{
try
{
InstallSaveInfo info = param.Content.DeJson<InstallSaveInfo>();
string value = await exportResolver.Get(info.Server, info.Value);
Dictionary<string, string> dic = Encoding.UTF8.GetString(Convert.FromBase64String(value)).DeJson<Dictionary<string, string>>();
config.Save(dic);
return true;
}
catch (Exception)
{
}
return false;
}
[Access(AccessValue.Export)]
public async Task<string> Copy(ApiControllerParamsInfo param)
{
try
{
ConfigExportInfo configExportInfo = param.Content.DeJson<ConfigExportInfo>();
var (client, clientObject, common, commonObject) = await GetConfig(configExportInfo).ConfigureAwait(false);
Dictionary<string, string> dic = new Dictionary<string, string>
{
{"Client",clientObject.ToJson()},
{"Common",commonObject.ToJson()},
};
return Convert.ToBase64String(Encoding.UTF8.GetBytes(dic.ToJson()));
}
catch (Exception ex)
{
LoggerHelper.Instance.Error(ex);
}
return string.Empty;
}
[Access(AccessValue.Export)]
public async Task<string> Save(ApiControllerParamsInfo param)
{
try
{
ConfigExportInfo configExportInfo = param.Content.DeJson<ConfigExportInfo>();
var (client, clientObject, common, commonObject) = await GetConfig(configExportInfo).ConfigureAwait(false);
Dictionary<string, object> dic = new Dictionary<string, object>
{
{"Client",clientObject},
{"Common",commonObject},
};
string value = Convert.ToBase64String(Encoding.UTF8.GetBytes(dic.ToJson()));
return await exportResolver.Save(signInClientState.Connection.Address, value);
}
catch (Exception ex)
{
LoggerHelper.Instance.Error(ex);
}
return string.Empty;
}
[Access(AccessValue.Export)]
public async Task<bool> Export(ApiControllerParamsInfo param)
{
@@ -105,31 +183,9 @@ namespace linker.messenger.store.file
string configPath = Path.Combine(rootPath, $"configs");
Directory.CreateDirectory(configPath);
ConfigClientInfo client = config.Data.Client.ToJson().DeJson<ConfigClientInfo>();
client.Id = string.Empty;
client.Name = string.Empty;
if (configExportInfo.Single || client.OnlyNode)
{
client.Id = await signInClientTransfer.GetNewId().ConfigureAwait(false);
client.Name = configExportInfo.Name;
}
if (client.OnlyNode == false)
{
client.CApi.ApiPassword = configExportInfo.ApiPassword;
}
client.Access = config.Data.Client.Access & (AccessValue)configExportInfo.Access;
client.OnlyNode = true;
client.Action.Args.Clear();
client.Action.Arg = string.Empty;
client.Groups = [config.Data.Client.Groups[0]];
File.WriteAllText(Path.Combine(configPath, $"client.json"), client.Serialize(client));
ConfigCommonInfo common = config.Data.Common.ToJson().DeJson<ConfigCommonInfo>();
common.Install = true;
common.Modes = ["client"];
File.WriteAllText(Path.Combine(configPath, $"common.json"), common.ToJsonFormat());
var (client, clientObject, common, commonObject) = await GetConfig(configExportInfo).ConfigureAwait(false);
File.WriteAllText(Path.Combine(configPath, $"client.json"), config.Data.Client.Serialize(clientObject));
File.WriteAllText(Path.Combine(configPath, $"common.json"), config.Data.Common.Serialize(commonObject));
ZipFile.CreateFromDirectory(rootPath, zipPath);
@@ -182,6 +238,68 @@ namespace linker.messenger.store.file
}
private async Task<(ConfigClientInfo, object, ConfigCommonInfo, object)> GetConfig(ConfigExportInfo configExportInfo)
{
ConfigClientInfo client = config.Data.Client.ToJson().DeJson<ConfigClientInfo>();
client.Id = string.Empty;
client.Name = string.Empty;
if (configExportInfo.Single || client.OnlyNode)
{
client.Id = await signInClientTransfer.GetNewId().ConfigureAwait(false);
client.Name = configExportInfo.Name;
}
if (client.OnlyNode == false)
{
client.CApi.ApiPassword = configExportInfo.ApiPassword;
}
client.Access = (AccessValue)((ulong)config.Data.Client.Access & configExportInfo.Access);
if (configExportInfo.Relay) client.Relay = new RelayClientInfo { Servers = new RelayServerInfo[] { client.Relay.Servers[0] } };
else client.Relay = new RelayClientInfo { Servers = new RelayServerInfo[] { new RelayServerInfo { } } };
if (configExportInfo.SForward) client.SForward = new linker.messenger.sforward.SForwardConfigClientInfo { SecretKey = client.SForward.SecretKey };
else client.SForward = new linker.messenger.sforward.SForwardConfigClientInfo { };
if (configExportInfo.Server) client.Servers = new SignInClientServerInfo[] { client.Servers[0] };
else client.Servers = new SignInClientServerInfo[] { };
if (configExportInfo.Group) client.Groups = new SignInClientGroupInfo[] { client.Groups[0] };
else client.Groups = new SignInClientGroupInfo[] { };
if (configExportInfo.Updater) client.Updater = new linker.messenger.updater.UpdaterConfigClientInfo { SecretKey = client.Updater.SecretKey };
else client.Updater = new linker.messenger.updater.UpdaterConfigClientInfo { };
if (configExportInfo.Tunnel) client.Tunnel = new TunnelConfigClientInfo { Transports = client.Tunnel.Transports };
else client.Tunnel = new TunnelConfigClientInfo { Transports = new List<linker.tunnel.transport.TunnelTransportItemInfo>() };
ConfigCommonInfo common = config.Data.Common.ToJson().DeJson<ConfigCommonInfo>();
common.Install = true;
common.Modes = ["client"];
return (client, new
{
client.Id,
client.Name,
client.CApi,
client.Access,
Groups = new SignInClientGroupInfo[] { config.Data.Client.Groups[0] },
Servers = new SignInClientServerInfo[] { config.Data.Client.Servers[0] },
client.SForward,
client.Updater,
Relay = new { Servers = new RelayServerInfo[] { client.Relay.Servers[0] } },
client.Tunnel,
}, common, new { Install = true, Modes = new string[] { "client" } });
}
}
public sealed class InstallSaveInfo
{
public string Server { get; set; }
public string Value { get; set; }
}
public sealed class ConfigInstallInfo
@@ -246,6 +364,14 @@ namespace linker.messenger.store.file
public string ApiPassword { get; set; }
public bool Single { get; set; }
public ulong Access { get; set; }
public bool Relay { get; set; }
public bool SForward { get; set; }
public bool Updater { get; set; }
public bool Server { get; set; }
public bool Group { get; set; }
public bool Tunnel { get; set; }
}
}

View File

@@ -99,6 +99,9 @@ namespace linker.messenger.store.file
serviceCollection.AddSingleton<IPlanStore, PlanStore>();
serviceCollection.AddSingleton<ExportResolver>();
return serviceCollection;
}
public static ServiceProvider UseStoreFile(this ServiceProvider serviceProvider,Dictionary<string,string> configDic)
@@ -121,6 +124,12 @@ namespace linker.messenger.store.file
});
ResolverTransfer resolverTransfer = serviceProvider.GetService<ResolverTransfer>();
resolverTransfer.AddResolvers(new List<IResolver>
{
serviceProvider.GetService<ExportResolver>(),
});
return serviceProvider;
}
}

View File

@@ -0,0 +1,147 @@

using linker.libs;
using linker.libs.extends;
using Microsoft.Extensions.Caching.Memory;
using System.Buffers;
using System.Net;
using System.Net.Sockets;
namespace linker.messenger.store.file
{
public sealed class ExportResolver : IResolver
{
public byte Type => 222;
private readonly IMemoryCache cache = new MemoryCache(new MemoryCacheOptions { });
private readonly ISerializer serializer;
public ExportResolver(ISerializer serializer)
{
this.serializer = serializer;
}
public async Task<string> Save(string server, string value)
{
return await Save(NetworkHelper.GetEndPoint(server, 1802), value).ConfigureAwait(false);
}
public async Task<string> Save(IPEndPoint server, string value)
{
byte[] buffer = ArrayPool<byte>.Shared.Rent(1024);
Socket socket = new Socket(server.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
try
{
await socket.ConnectAsync(server).WaitAsync(TimeSpan.FromMilliseconds(5000));
await socket.SendAsync(new byte[] { Type });
await socket.SendAsync(serializer.Serialize(new ExportSaveInfo { Type = ExportSaveType.Save, Value = value }.ToJson()));
int length = await socket.ReceiveAsync(buffer.AsMemory(), SocketFlags.None).AsTask().WaitAsync(TimeSpan.FromMilliseconds(5000)).ConfigureAwait(false);
return serializer.Deserialize<string>(buffer.AsSpan(0, length));
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
finally
{
socket.SafeClose();
ArrayPool<byte>.Shared.Return(buffer);
}
return string.Empty;
}
public async Task<string> Get(string server, string value)
{
return await Get(NetworkHelper.GetEndPoint(server, 1802), value).ConfigureAwait(false);
}
public async Task<string> Get(IPEndPoint server, string value)
{
byte[] buffer = ArrayPool<byte>.Shared.Rent(8*1024);
Socket socket = new Socket(server.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
try
{
await socket.ConnectAsync(server).WaitAsync(TimeSpan.FromMilliseconds(5000));
await socket.SendAsync(new byte[] { Type });
await socket.SendAsync(serializer.Serialize(new ExportSaveInfo { Type = ExportSaveType.Get, Value = value }.ToJson()));
int length = await socket.ReceiveAsync(buffer.AsMemory(), SocketFlags.None).AsTask().WaitAsync(TimeSpan.FromMilliseconds(5000)).ConfigureAwait(false);
return serializer.Deserialize<string>(buffer.AsSpan(0, length));
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
finally
{
socket.SafeClose();
ArrayPool<byte>.Shared.Return(buffer);
}
return string.Empty;
}
public async Task Resolve(Socket socket, Memory<byte> memory)
{
byte[] buffer = ArrayPool<byte>.Shared.Rent(8192);
try
{
int length = await socket.ReceiveAsync(buffer.AsMemory(), SocketFlags.None).ConfigureAwait(false);
ExportSaveInfo info = serializer.Deserialize<string>(buffer.AsMemory(0, length).Span).DeJson<ExportSaveInfo>();
if (string.IsNullOrWhiteSpace(info.Value))
{
socket.SafeClose();
return;
}
Console.WriteLine(info.Type);
switch (info.Type)
{
case ExportSaveType.Save:
{
string key = Guid.NewGuid().ToString();
cache.Set(key, info.Value, TimeSpan.FromMinutes(10));
await socket.SendAsync(serializer.Serialize(key));
}
break;
case ExportSaveType.Get:
{
if (cache.TryGetValue(info.Value, out string value))
{
cache.Remove(info.Value);
await socket.SendAsync(serializer.Serialize(value));
}
}
break;
}
}
catch (Exception ex)
{
socket.SafeClose();
if (LoggerHelper.Instance.LoggerLevel <= LoggerTypes.DEBUG)
LoggerHelper.Instance.Error(ex);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
public async Task Resolve(Socket socket, IPEndPoint ep, Memory<byte> memory)
{
await Task.CompletedTask;
}
}
public sealed class ExportSaveInfo
{
public ExportSaveType Type { get; set; } = ExportSaveType.Save;
public string Value { get; set; } = string.Empty;
}
public enum ExportSaveType : byte
{
Save = 0,
Get = 1,
}
}

View File

@@ -31,6 +31,8 @@ namespace linker.messenger.tuntap
serviceCollection.AddSingleton<TuntapExRoute>();
serviceCollection.AddSingleton<ISystemInformation, SystemInformation>();
return serviceCollection;
}
public static ServiceProvider UseTuntapClient(this ServiceProvider serviceProvider)
@@ -70,6 +72,8 @@ namespace linker.messenger.tuntap
serviceCollection.AddSingleton<TuntapServerMessenger>();
serviceCollection.AddSingleton<LeaseServerTreansfer>();
serviceCollection.AddSingleton<ISystemInformation, SystemInformation>();
return serviceCollection;
}
public static ServiceProvider UseTuntapServer(this ServiceProvider serviceProvider)

View File

@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace linker.messenger.tuntap
{
public interface ISystemInformation
{
public string Get()
{
return $"{System.Runtime.InteropServices.RuntimeInformation.OSDescription} {(string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("SNLTTY_LINKER_IS_DOCKER")) == false ? "Docker" : "")}";
}
}
public sealed class SystemInformation: ISystemInformation { }
}

View File

@@ -30,8 +30,9 @@ namespace linker.messenger.tuntap
private readonly TuntapTransfer tuntapTransfer;
private readonly ExRouteTransfer exRouteTransfer;
private readonly SignInClientState signInClientState;
private readonly ISystemInformation systemInformation;
public TuntapDecenter(ISignInClientStore signInClientStore, SignInClientState signInClientState, ISerializer serializer, TuntapProxy tuntapProxy, TuntapConfigTransfer tuntapConfigTransfer, TuntapTransfer tuntapTransfer, ExRouteTransfer exRouteTransfer)
public TuntapDecenter(ISignInClientStore signInClientStore, SignInClientState signInClientState, ISerializer serializer, TuntapProxy tuntapProxy, TuntapConfigTransfer tuntapConfigTransfer, TuntapTransfer tuntapTransfer, ExRouteTransfer exRouteTransfer, ISystemInformation systemInformation)
{
this.signInClientStore = signInClientStore;
this.serializer = serializer;
@@ -40,7 +41,7 @@ namespace linker.messenger.tuntap
this.tuntapTransfer = tuntapTransfer;
this.exRouteTransfer = exRouteTransfer;
this.signInClientState = signInClientState;
this.systemInformation = systemInformation;
}
public void Refresh()
@@ -70,7 +71,7 @@ namespace linker.messenger.tuntap
Status = tuntapTransfer.Status,
SetupError = tuntapTransfer.SetupError,
NatError = tuntapTransfer.NatError,
SystemInfo = $"{System.Runtime.InteropServices.RuntimeInformation.OSDescription} {(string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("SNLTTY_LINKER_IS_DOCKER")) == false ? "Docker" : "")}",
SystemInfo = systemInformation.Get(),
Forwards = tuntapConfigTransfer.Info.Forwards,
Switch = tuntapConfigTransfer.Info.Switch

View File

@@ -164,17 +164,17 @@ namespace linker.tun
string str = CommandHelper.Linux(string.Empty, new string[] {
$"sysctl -w net.ipv4.ip_forward=1",
$"iptables -t nat -A POSTROUTING -o {Name} -j MASQUERADE",
$"iptables -A FORWARD -i {interfaceLinux} -o {Name} -j ACCEPT",
$"iptables -A FORWARD -i {Name} -j ACCEPT",
$"iptables -t nat -A POSTROUTING -o {Name} -j MASQUERADE",
$"iptables -t nat -A POSTROUTING ! -o {Name} -s {network}/{prefixLength} -j MASQUERADE",
isSupport ? $"iptables -A FORWARD -i {Name} -o {interfaceLinux} -m state --state ESTABLISHED,RELATED -j ACCEPT"
: $"iptables -A FORWARD -i {Name} -o {interfaceLinux} -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT",
$"iptables -A FORWARD -i {Name} -j ACCEPT",
isSupport ? $"iptables -A FORWARD -o {Name} -m state --state ESTABLISHED,RELATED -j ACCEPT"
: $"iptables -A FORWARD -o {Name} -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT",
$"iptables -t nat -A POSTROUTING ! -o {Name} -s {network}/{prefixLength} -j MASQUERADE",
$"iptables -t nat -A POSTROUTING ! -o {Name} -s {network}/{prefixLength} -j MASQUERADE",
});
RestartFirewall();
}
@@ -351,6 +351,4 @@ namespace linker.tun
return await Task.FromResult(output.Contains("state UP")).ConfigureAwait(false);
}
}
}

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="1745458919168" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2606" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M214.101333 512c0-32.512 5.546667-63.701333 15.36-92.928L57.173333 290.218667A491.861333 491.861333 0 0 0 4.693333 512c0 79.701333 18.858667 154.88 52.394667 221.610667l172.202667-129.066667A290.56 290.56 0 0 1 214.101333 512" fill="#FBBC05" p-id="2607"></path><path d="M516.693333 216.192c72.106667 0 137.258667 25.002667 188.458667 65.962667L854.101333 136.533333C763.349333 59.178667 646.997333 11.392 516.693333 11.392c-202.325333 0-376.234667 113.28-459.52 278.826667l172.373334 128.853333c39.68-118.016 152.832-202.88 287.146666-202.88" fill="#EA4335" p-id="2608"></path><path d="M516.693333 807.808c-134.357333 0-247.509333-84.864-287.232-202.88l-172.288 128.853333c83.242667 165.546667 257.152 278.826667 459.52 278.826667 124.842667 0 244.053333-43.392 333.568-124.757333l-163.584-123.818667c-46.122667 28.458667-104.234667 43.776-170.026666 43.776" fill="#34A853" p-id="2609"></path><path d="M1005.397333 512c0-29.568-4.693333-61.44-11.648-91.008H516.650667V614.4h274.602666c-13.696 65.962667-51.072 116.650667-104.533333 149.632l163.541333 123.818667c93.994667-85.418667 155.136-212.650667 155.136-375.850667" fill="#4285F4" p-id="2610"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

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="1745458938525" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3743" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M0 0m184.32 0l655.36 0q184.32 0 184.32 184.32l0 655.36q0 184.32-184.32 184.32l-655.36 0q-184.32 0-184.32-184.32l0-655.36q0-184.32 184.32-184.32Z" fill="#FE7945" p-id="3744"></path><path d="M184.32 286.72h317.44a153.6 153.6 0 0 1 153.6 153.6v307.2H184.32v-460.8z" fill="#FFFFFF" p-id="3745"></path><path d="M737.28 286.72h102.4v460.8H737.28z" fill="#FFFFFF" p-id="3746"></path><path d="M286.72 389.12h215.04a51.2 51.2 0 0 1 51.2 51.2v307.2H286.72v-358.4z" fill="#FE7945" p-id="3747"></path><path d="M368.64 491.52h102.4v256H368.64z" fill="#FFFFFF" p-id="3748"></path></svg>

After

Width:  |  Height:  |  Size: 905 B

View File

@@ -7,6 +7,18 @@ export const getConfig = () => {
export const install = (data) => {
return sendWebsocketMsg('config/install', data);
}
export const installCopy = (data) => {
return sendWebsocketMsg('config/InstallCopy', data);
}
export const installSave = (data) => {
return sendWebsocketMsg('config/InstallSave', data);
}
export const exportConfig = (data) => {
return sendWebsocketMsg('config/export', data);
}
export const copyConfig = (data) => {
return sendWebsocketMsg('config/copy', data);
}
export const saveConfig = (data) => {
return sendWebsocketMsg('config/save', data);
}

View File

@@ -63,6 +63,16 @@ export default {
'status.exportNamePlease': 'Please input device name',
'status.exportApiPassword': 'Api Pwd',
'status.exportApiPasswordPlease': 'Please input api password',
'status.exportDownload': 'Download',
'status.exportCopy': 'Copy',
'status.exportSave': 'Save',
'status.exportRelay': 'Relay secretKey',
'status.exportSForward': 'SForward secretKey',
'status.exportUpdater': 'Update secretKey',
'status.exportServer': 'Messenger server',
'status.exportGroup': 'Group',
'status.exportTunnel': 'Tunnel config',
'status.api': 'Manager api',
'status.apiClear': 'Clear',
'status.apiAlter': 'Alter',

View File

@@ -57,12 +57,21 @@ export default {
'status.cdkey': 'CDKEY商城',
'status.export': '导出配置',
'status.exportText': '导出配置,作为子设备运行如果使用docker容器映射configs文件夹即可',
'status.exportText': '导出配置,客户端覆盖配置文件,或粘贴配置信息,或在线加载',
'status.exportSingle': '单设备',
'status.exportName': '设备名',
'status.exportNamePlease': '请输入设备名',
'status.exportApiPassword': '接口密码',
'status.exportApiPasswordPlease': '请输入接口密码',
'status.exportDownload': '下载',
'status.exportCopy': '复制',
'status.exportSave': '保存',
'status.exportRelay': '中继密钥',
'status.exportSForward': '内网穿透密钥',
'status.exportUpdater': '更新密钥',
'status.exportServer': '信标服务器',
'status.exportGroup': '当前分组',
'status.exportTunnel': '打洞协议',
'status.api': '管理接口',
'status.apiClear': '清除',

View File

@@ -105,7 +105,7 @@ export default {
const route = useRoute();
const globalData = injectGlobalData();
const hasConfig = computed(()=>globalData.value.hasAccess('Config') || globalData.value.hasAccess('Sync') || globalData.value.hasAccess('Group'));
const hasConfig = computed(()=>globalData.value.hasAccess('Config'));
const hasLogger = computed(()=>globalData.value.hasAccess('LoggerShow'));
const hasTransport = computed(()=>globalData.value.hasAccess('Transport'));
const hasAction = computed(()=>globalData.value.hasAccess('Action'));

View File

@@ -3,16 +3,21 @@
<el-col :span="8">
<el-checkbox v-model="state.checkAll" @change="handleCheckAllChange" label="全选" :indeterminate="state.isIndeterminate" />
</el-col>
<el-col :span="8">
<el-checkbox v-model="state.full" ><span class="red">满权限(顶级管理权)</span></el-checkbox>
</el-col>
</el-row>
<el-checkbox-group v-model="state.checkList" @change="handleCheckedChange">
<el-row>
<template v-for="(item,index) in access" :key="index">
<el-col :span="8">
<el-checkbox :value="item.Value" :label="item.Text" />
</el-col>
</template>
</el-row>
</el-checkbox-group>
<div class="access-wrap scrollbar">
<el-checkbox-group v-model="state.checkList" @change="handleCheckedChange">
<el-row>
<template v-for="(item,index) in access" :key="index">
<el-col :span="8">
<el-checkbox :value="item.Value" :label="item.Text" />
</el-col>
</template>
</el-row>
</el-checkbox-group>
</div>
</template>
<script>
import { computed, onMounted, reactive } from 'vue';
@@ -46,10 +51,12 @@ export default {
globalData.value.config.Client.Accesss.Group.Value,
],
checkAll:false,
full:false,
isIndeterminate:false
});
const getValue = ()=>{
if(state.full) return (+(BigInt(0xffffffffffffffff)>>BigInt(12)).toString())-1;
return +state.checkList.reduce((sum,item)=>{
return (sum | BigInt(item));
},BigInt(0)).toString();
@@ -84,4 +91,5 @@ export default {
</script>
<style lang="stylus" scoped>
.el-col {text-align:left;}
.access-wrap{height:40rem}
</style>

View File

@@ -15,7 +15,7 @@ export const provideTuntap = () => {
});
provide(tuntapSymbol, tuntap);
const reg = /ios|android|windows|ubuntu|openwrt|armbian|archlinux|fedora|centos|rocky|alpine|debian|linux|docker/g;
const reg = /google|huawei|xiaomi|ios|android|windows|ubuntu|openwrt|armbian|archlinux|fedora|centos|rocky|alpine|debian|linux|docker/g;
const _getTuntapInfo = () => {
clearTimeout(tuntap.value.timer);

View File

@@ -0,0 +1,42 @@
<template>
<div>
<div>
<el-input v-model="state.content" type="textarea" :rows="10" resize="none"></el-input>
</div>
<div class="t-c mgt-1">
<el-button type="primary" @click="handleSave">确定</el-button>
</div>
</div>
</template>
<script>
import { installCopy } from '@/apis/config';
import { ElMessage } from 'element-plus';
import { reactive } from 'vue';
export default {
setup () {
const state = reactive({ content:'' })
const handleSave = ()=>{
if(!state.content) return;
installCopy(state.content).then((res)=>{
if(!res){
ElMessage.error('保存失败,可能格式有误,无法解析');
return;
}
ElMessage.success('保存成功');
window.location.reload();
}).catch(()=>{
ElMessage.error('保存失败');
})
}
return {
state,handleSave
}
}
}
</script>
<style lang="stylus" scoped>
</style>

View File

@@ -2,93 +2,40 @@
<div>
<el-dialog v-model="state.show" title="初始化配置" width="600" top="2vh">
<div>
<div class="head">
<el-steps :active="step.step" finish-status="success">
<template v-for="(item,index) in state.steps">
<el-step :title="item" />
</template>
</el-steps>
</div>
<div class="body">
<el-card shadow="never" v-if="step.step == 1">
<Common ref="currentDom"></Common>
</el-card>
<el-card shadow="never" v-if="step.step == 2">
<Server ref="currentDom"></Server>
</el-card>
<el-card shadow="never" v-if="step.step == 3">
<Client ref="currentDom"></Client>
</el-card>
<el-card shadow="never" v-if="step.step == 4">
<div class="t-c">完成保存后请重启软件</div>
</el-card>
</div>
<div class="footer t-c">
<el-button :disabled="step.step <= 1" @click="handlePrev">上一步</el-button>
<el-button v-if="step.step < state.steps.length" type="primary" @click="handleNext">下一步</el-button>
<el-button v-else type="primary" @click="handleSave">完成</el-button>
</div>
<el-tabs type="border-card">
<el-tab-pane label="手动输入">
<Input></Input>
</el-tab-pane>
<el-tab-pane label="粘贴配置">
<Copy></Copy>
</el-tab-pane>
<el-tab-pane label="在线导入">
<Save></Save>
</el-tab-pane>
</el-tabs>
</div>
</el-dialog>
</div>
</template>
<script>
import { injectGlobalData } from '@/provide';
import { install } from '@/apis/config';
import { reactive, ref, provide, computed } from 'vue';
import { ElMessage } from 'element-plus';
import Common from './Common.vue'
import Client from './Client.vue'
import Server from './Server.vue'
import { reactive} from 'vue';
import Input from './Input.vue'
import Copy from './Copy.vue'
import Save from './Save.vue'
export default {
components: { Common,Client,Server },
components: { Input,Copy,Save},
setup(props) {
const globalData = injectGlobalData();
const globalData = injectGlobalData();
const state = reactive({
show: globalData.value.config.Common.Install == false,
steps:computed(()=>['选择模式',
globalData.value.isPc ? '服务端' : '',
'客户端',
'完成'])
});
show: globalData.value.config.Common.Install == false
});
const currentDom = ref(null);
const step = ref({
step:1,
increment:1,
json:{},
form:{server:{},client:{},common:{}}
});
provide('step',step);
const handlePrev = ()=>{
step.value.step --;
step.value.increment = -1;
}
const handleNext = ()=>{
step.value.increment = 1;
currentDom.value.handleValidate().then((json)=>{
step.value.json = Object.assign(step.value.json,json.json);
step.value.form = Object.assign(step.value.form,json.form);
step.value.step ++;
}).catch(()=>{
});
}
const handleSave = ()=>{
install(step.value.json).then(()=>{
ElMessage.success('保存成功');
}).catch(()=>{
ElMessage.error('保存失败');
})
}
return { state,globalData,currentDom,step,handlePrev,handleNext,handleSave};
return { state,globalData};
}
}
</script>
<style lang="stylus" scoped>
.body{margin-top:1rem;}
.footer{
margin-top:2rem
}
</style>

View File

@@ -0,0 +1,90 @@
<template>
<div>
<div class="head">
<el-steps :active="step.step" finish-status="success">
<template v-for="(item,index) in state.steps">
<el-step :title="item" />
</template>
</el-steps>
</div>
<div class="body">
<el-card shadow="never" v-if="step.step == 1">
<Common ref="currentDom"></Common>
</el-card>
<el-card shadow="never" v-if="step.step == 2">
<Server ref="currentDom"></Server>
</el-card>
<el-card shadow="never" v-if="step.step == 3">
<Client ref="currentDom"></Client>
</el-card>
<el-card shadow="never" v-if="step.step == 4">
<div class="t-c">完成保存后请重启软件</div>
</el-card>
</div>
<div class="footer t-c">
<el-button :disabled="step.step <= 1" @click="handlePrev">上一步</el-button>
<el-button v-if="step.step < state.steps.length" type="primary" @click="handleNext">下一步</el-button>
<el-button v-else type="primary" @click="handleSave">完成</el-button>
</div>
</div>
</template>
<script>
import { injectGlobalData } from '@/provide';
import { install } from '@/apis/config';
import { reactive, ref, provide, computed } from 'vue';
import { ElMessage } from 'element-plus';
import Common from './Common.vue'
import Client from './Client.vue'
import Server from './Server.vue'
export default {
components: { Common,Client,Server },
setup(props) {
const globalData = injectGlobalData();
const state = reactive({
steps:computed(()=>['选择模式',
globalData.value.isPc ? '服务端' : '',
'客户端',
'完成'])
});
const currentDom = ref(null);
const step = ref({
step:1,
increment:1,
json:{},
form:{server:{},client:{},common:{}}
});
provide('step',step);
const handlePrev = ()=>{
step.value.step --;
step.value.increment = -1;
}
const handleNext = ()=>{
step.value.increment = 1;
currentDom.value.handleValidate().then((json)=>{
step.value.json = Object.assign(step.value.json,json.json);
step.value.form = Object.assign(step.value.form,json.form);
step.value.step ++;
}).catch(()=>{
});
}
const handleSave = ()=>{
install(step.value.json).then(()=>{
ElMessage.success('保存成功');
window.location.reload();
}).catch(()=>{
ElMessage.error('保存失败');
})
}
return { state,globalData,currentDom,step,handlePrev,handleNext,handleSave};
}
}
</script>
<style lang="stylus" scoped>
.body{margin-top:1rem;}
.footer{
margin-top:2rem
}
</style>

View File

@@ -0,0 +1,61 @@
<template>
<div>
<el-form ref="formDom" :model="state.ruleForm" :rules="state.rules" label-width="auto">
<el-form-item label="服务器" prop="server">
<el-input v-model="state.ruleForm.server" />
</el-form-item>
<el-form-item label="密钥" prop="value">
<el-input v-model="state.ruleForm.value" />
</el-form-item>
<el-form-item label="" prop="Btns">
<div class="t-c w-100">
<el-button type="primary" @click="handleSave">确认</el-button>
</div>
</el-form-item>
</el-form>
</div>
</template>
<script>
import { installSave } from '@/apis/config';
import { ElMessage } from 'element-plus';
import { reactive, ref } from 'vue';
export default {
setup () {
const state = reactive({
ruleForm: {
server: '',
value:'',
},
rules: {
server: [{ required: true, message: "必填", trigger: "blur" }],
value: [{ required: true, message: "必填", trigger: "blur" }],
}
})
const formDom = ref(null);
const handleSave = ()=>{
formDom.value.validate((valid) => {
if (!valid) return;
installSave(state.ruleForm).then((res)=>{
if(!res){
ElMessage.error('保存失败,可能服务器或者密钥不正确,或者密钥已被使用');
return;
}
ElMessage.success('保存成功');
window.location.reload();
}).catch(()=>{
ElMessage.error('保存失败');
})
});
}
return {
state,formDom,handleSave
}
}
}
</script>
<style lang="stylus" scoped>
</style>

View File

@@ -24,25 +24,61 @@
<span>{{$t('status.exportApiPassword')}} : </span><el-input type="password" show-password :disabled="onlyNode" v-model="state.apipassword" maxlength="36" show-word-limit style="width:15rem"></el-input>
</div>
</div>
<div>
<el-row>
<el-col :span="8"><el-checkbox v-model="state.relay" :label="$t('status.exportRelay')" /></el-col>
<el-col :span="8"><el-checkbox v-model="state.sforward" :label="$t('status.exportSForward')" /></el-col>
<el-col :span="8"><el-checkbox v-model="state.updater" :label="$t('status.exportUpdater')" /></el-col>
</el-row>
</div>
<div>
<el-row>
<el-col :span="8"><el-checkbox v-model="state.server" :label="$t('status.exportServer')" /></el-col>
<el-col :span="8"><el-checkbox v-model="state.group" :label="$t('status.exportGroup')" /></el-col>
<el-col :span="8"><el-checkbox v-model="state.tunnel" :label="$t('status.exportTunnel')" /></el-col>
</el-row>
</div>
</div>
</template>
<Access ref="accessDom" :machineid="machineId"></Access>
</el-card>
</div>
</div>
<template #footer>
<el-button plain @click="state.show = false" :loading="state.loading">{{$t('common.cancel') }}</el-button>
<el-button type="success" plain @click="handleExport" :loading="state.loading">{{$t('common.confirm') }}</el-button>
<el-button type="default" plain @click="handleExport" :loading="state.loading">{{$t('status.exportDownload') }}</el-button>
<el-button type="info" plain @click="handleCopy" :loading="state.loading">{{$t('status.exportCopy') }}</el-button>
<el-button type="success" plain @click="handleSave" :loading="state.loading">{{$t('status.exportSave') }}</el-button>
</template>
</el-dialog>
<el-dialog class="options-center" :title="$t('status.export')" destroy-on-close v-model="state.showCopy" center width="580" top="1vh">
<div class="port-wrap">
<el-input v-model="state.copyContent" type="textarea" :rows="10" resize="none" readonly></el-input>
</div>
<template #footer>
<el-button plain @click="copyToClipboard">{{$t('status.exportCopy') }}</el-button>
</template>
</el-dialog>
<el-dialog class="options-center" :title="$t('status.export')" destroy-on-close v-model="state.showSave" center width="300" top="1vh">
<div class="port-wrap">
<div>
<el-input v-model="state.saveServer" readonly></el-input>
</div>
<div style="margin-top:1rem">
<el-input v-model="state.saveContent" readonly></el-input>
</div>
</div>
<template #footer>
<el-button plain @click="copySaveToClipboard">{{$t('status.exportCopy') }}</el-button>
</template>
</el-dialog>
</div>
</template>
<script>
import { computed, onMounted, reactive, ref } from 'vue';
import { computed, reactive, ref } from 'vue';
import {Share} from '@element-plus/icons-vue'
import { exportConfig } from '@/apis/config';
import { ElMessage } from 'element-plus';
import { exportConfig,copyConfig,saveConfig } from '@/apis/config';
import { ElMessage, ElMessageBox } from 'element-plus';
import { injectGlobalData } from '@/provide';
import Access from '@/views/full/devices/Access.vue'
import { useI18n } from 'vue-i18n'
@@ -62,6 +98,20 @@ export default {
single:true,
name:'',
apipassword:onlyNode.value? globalData.value.config.Client.CApi.ApiPassword :'',
relay:true,
sforward:true,
updater:true,
server:true,
group:true,
tunnel:true,
copyContent:'',
showCopy:false,
saveServer:globalData.value.config.Client.Server.Host,
saveContent:'',
showSave:false
});
const accessDom = ref(null);
@@ -74,7 +124,13 @@ export default {
access:accessDom.value.getValue(),
single:state.single,
name:state.name,
apipassword:state.apipassword
apipassword:state.apipassword,
relay:state.relay,
sforward:state.sforward,
updater:state.updater,
server:state.server,
group:state.group,
tunnel:state.tunnel,
}
if(json.single){
@@ -104,6 +160,37 @@ export default {
link.click();
document.body.removeChild(link);
}
const handleSave = ()=>{
const json = getJson();
if(!json){
return;
};
state.loading = true;
saveConfig(json).then((res)=>{
state.loading = false;
state.show = false;
ElMessage.success(t('common.oper'));
state.saveContent = res;
state.showSave = true;
}).catch(()=>{
ElMessage.error(t('common.operFail'));
state.loading = false;
});
}
const copySaveToClipboard = async ()=>{
try {
await navigator.clipboard.writeText(`在初始化linker客户端时填写服务器和密钥导入配置\n服务器: ${state.saveServer}\n密钥: ${state.saveContent}`);
ElMessage.success(t('common.oper'));
return true;
} catch (err) {
ElMessage.error(t('common.operFail'));
return false;
}
}
const handleExport = ()=>{
const json = getJson();
@@ -123,7 +210,35 @@ export default {
state.loading = false;
});
}
return {config:props.config,onlyNode,hasExport,machineId, state,accessDom,handleExport};
const handleCopy = ()=>{
const json = getJson();
if(!json){
return;
};
state.loading = true;
copyConfig(json).then((res)=>{
state.loading = false;
state.show = false;
ElMessage.success(t('common.oper'));
state.copyContent = res;
state.showCopy = true;
}).catch(()=>{
ElMessage.error(t('common.operFail'));
state.loading = false;
});
}
const copyToClipboard = async()=> {
try {
await navigator.clipboard.writeText(state.copyContent);
ElMessage.success(t('common.oper'));
return true;
} catch (err) {
ElMessage.error(t('common.operFail'));
return false;
}
}
return {config:props.config,onlyNode,hasExport,machineId, state,accessDom,handleSave,handleExport,handleCopy,copyToClipboard,copySaveToClipboard};
}
}
</script>

View File

@@ -1,8 +1,7 @@
<template>
<div class="head-wrap">
<div class="tools flex">
<span class="label">服务器 </span><el-input v-model="state.server" readonly style="width: 14rem;" size="small"></el-input>
<span class="label">分组 : {{state.group}}</span>
<span class="flex-1"></span>
<el-button size="small" @click="handleRefresh">
刷新(F5)<el-icon><Refresh /></el-icon>
@@ -25,6 +24,7 @@ export default {
const globalData = injectGlobalData();
const state = reactive({
server:computed(()=>globalData.value.config.Client.Server.Host),
group:computed(()=>globalData.value.config.Client.Group.Name),
});
const handleRefresh = ()=>{
window.location.reload();

View File

@@ -1,5 +1,5 @@
v1.7.4
2025-04-24 00:55:37
2025-04-24 17:28:28
1. 一些优化
2. 内置应用层SNAT用于无法使用系统NetNat的windows系统
3. 如果你设备很多,请尝试升级其中一个成功重启后再升级其它