傻瓜共享版

This commit is contained in:
snltty
2025-04-28 16:09:26 +08:00
parent f212cbc9ee
commit 5af63ed003
39 changed files with 507 additions and 353 deletions

1
.gitignore vendored
View File

@@ -5,5 +5,6 @@ obj
node_modules
/public/*
/x64/*
linker.share.win
TestResults

View File

@@ -26,6 +26,7 @@ namespace linker.ics
{
try
{
publicCon.EnableSharing(tagSHARINGCONNECTIONTYPE.ICSSHARINGTYPE_PUBLIC);
}
catch (Exception ex)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

View File

@@ -1,199 +0,0 @@
import ctypes
import os
import zipfile
import requests
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
from PIL import Image, ImageTk
import winshell
from win32com.client import Dispatch
import threading
import webbrowser
import pythoncom
from io import BytesIO
import sys
class InstallerApp:
def __init__(self, root):
self.root = root
self.root.title("Linker 安装程序")
self.root.geometry("400x400")
self.root.resizable(False, False) # 禁止调整窗口大小
# 检查管理员权限
if not self.is_admin():
messagebox.showerror("权限不足", "当前用户没有管理员权限,请以管理员身份运行程序。")
self.root.quit()
# 默认安装路径
self.default_install_dir = r"C:\Program Files (x86)\linker"
self.install_dir = self.default_install_dir
# 设置背景色
self.root.config(bg="#f4f4f4")
# 加载 Logo
self.load_logo()
# 快速安装按钮
self.btn_install = tk.Button(self.root, text="快速安装", command=self.start_installation,
bg="#007BFF", fg="white", font=("Arial", 14), width=20, height=2)
self.btn_install.pack(pady=10)
# 安装目录选择按钮
self.btn_select_path = tk.Button(self.root, text="选择安装目录", command=self.select_install_path,
bg="#007BFF", fg="white", font=("Arial", 10), width=20)
self.btn_select_path.pack(pady=10)
# 协议复选框
self.agree_var = tk.IntVar()
self.agree_checkbox = tk.Checkbutton(self.root, text="我已阅读并同意", variable=self.agree_var,
bg="#f4f4f4", font=("Arial", 10))
self.agree_checkbox.pack(pady=10)
# 用户协议链接
self.terms_label = tk.Label(self.root, text="《用户许可协议》", fg="blue", cursor="hand2", bg="#f4f4f4")
self.terms_label.pack(pady=5)
self.terms_label.bind("<Button-1>", self.open_terms) # 点击跳转到协议网页
# 安装进度条
self.progress = ttk.Progressbar(self.root, orient="horizontal", length=300, mode="determinate")
self.progress.pack(pady=10)
# 状态标签
self.label_status = tk.Label(self.root, text="", bg="#f4f4f4", font=("Arial", 10))
self.label_status.pack(pady=5)
def is_admin(self):
"""检查当前程序是否以管理员身份运行"""
try:
return ctypes.windll.shell32.IsUserAnAdmin() != 0
except:
return False
def load_logo(self):
"""加载 logo本地优先失败后从网络加载"""
if getattr(sys, 'frozen', False):
base_path = sys._MEIPASS # PyInstaller 运行环境
else:
base_path = os.path.dirname(__file__) # 普通 Python 运行环境
local_logo_path = os.path.join(base_path, 'img', 'logo.png')
remote_logo_url = "https://linker-doc.snltty.com/img/logo.png"
try:
if os.path.exists(local_logo_path):
self.logo = Image.open(local_logo_path)
else:
response = requests.get(remote_logo_url, timeout=10)
response.raise_for_status()
img_data = BytesIO(response.content)
self.logo = Image.open(img_data)
# 调整大小
self.logo = self.logo.resize((100, 100), Image.Resampling.LANCZOS)
self.logo = ImageTk.PhotoImage(self.logo)
self.logo_label = tk.Label(self.root, image=self.logo, bg="#f4f4f4")
self.logo_label.pack(pady=20) # 确保 Logo 正确显示
except Exception as e:
print(f"加载 logo 失败: {e}")
self.logo = None
def select_install_path(self):
"""选择安装路径"""
self.install_dir = filedialog.askdirectory(initialdir=self.default_install_dir, title="选择安装目录")
if not self.install_dir:
self.install_dir = self.default_install_dir
def open_terms(self, event):
"""打开用户许可协议"""
webbrowser.open("https://linker-doc.snltty.com/docs/1%E3%80%81%E9%A6%96%E9%A1%B5")
def start_installation(self):
"""开始安装"""
if not self.agree_var.get():
messagebox.showerror("错误", "请先勾选‘我已阅读并同意《用户许可协议》’")
return
self.progress["value"] = 0
self.btn_install.config(state=tk.DISABLED)
# 启动安装
threading.Thread(target=self.install_process, daemon=True).start()
def install_process(self):
"""执行安装流程"""
zip_path = os.path.join(self.install_dir, "linker.zip")
if not os.path.exists(self.install_dir):
os.makedirs(self.install_dir)
self.update_status("正在下载 ZIP 文件...")
self.download_zip("https://static.qbcode.cn/downloads/linker/v1.6.9/linker-win-x64.zip", zip_path)
self.progress["value"] = 30
self.update_status("正在解压文件...")
extract_folder = self.extract_zip(zip_path, self.install_dir)
self.progress["value"] = 70
os.remove(zip_path)
linker_exe = self.find_linker_exe(extract_folder)
if linker_exe:
self.update_status("创建快捷方式...")
pythoncom.CoInitialize()
self.create_shortcut(linker_exe, "linker")
self.progress["value"] = 100
messagebox.showinfo("完成", "安装完成!")
else:
messagebox.showerror("错误", "linker.tray.win.exe 未找到,安装失败!")
self.btn_install.config(state=tk.NORMAL)
def download_zip(self, url, save_path):
"""下载 ZIP 文件"""
try:
response = requests.get(url, stream=True)
response.raise_for_status()
with open(save_path, 'wb') as file:
for chunk in response.iter_content(1024):
file.write(chunk)
except requests.exceptions.RequestException as e:
messagebox.showerror("下载错误", f"下载失败: {e}")
self.btn_install.config(state=tk.NORMAL)
def extract_zip(self, zip_path, extract_to):
"""解压 ZIP 文件"""
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
zip_ref.extractall(extract_to)
return extract_to
def find_linker_exe(self, extract_folder):
"""寻找 linker.tray.win.exe"""
for root, dirs, files in os.walk(extract_folder):
if "linker.tray.win.exe" in files:
return os.path.join(root, "linker.tray.win.exe")
return None
def create_shortcut(self, target_path, shortcut_name):
"""创建快捷方式"""
desktop = winshell.desktop()
shortcut_path = os.path.join(desktop, f"{shortcut_name}.lnk")
shell = Dispatch('WScript.Shell')
shortcut = shell.CreateShortcut(shortcut_path)
shortcut.TargetPath = target_path
shortcut.WorkingDirectory = os.path.dirname(target_path)
shortcut.Save()
def update_status(self, message):
"""更新状态标签"""
self.label_status.config(text=message)
self.root.update_idletasks()
if __name__ == "__main__":
root = tk.Tk()
app = InstallerApp(root)
root.mainloop()

View File

@@ -1,46 +0,0 @@
added_files = [
('img/logo.png','img/')
]
# 配置打包参数
a = Analysis(
['main1.0.py'],
pathex=['D:/py/linker/.venv/Lib/site-packages'],
binaries=[],
datas=added_files ,
hiddenimports=['requests', 'pywin32', 'winshell', 'tkinterweb'],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='linker简易安装程序',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon='D:\py\linker\src\linker.install.win\img\linker.ico'
)

View File

@@ -1,4 +0,0 @@
加载所需要的模块
pip install -r requirements.txt
打包
pyinstaller .\main1.0.spec

View File

@@ -189,7 +189,7 @@ namespace linker.messenger.entry
if ((modules & ExcludeModule.Socks5) != ExcludeModule.Socks5)
serviceProvider.UseSocks5Client();
if ((modules & ExcludeModule.Tuntap) != ExcludeModule.Tuntap)
serviceProvider.UseTuntapClient();
serviceProvider.UseTuntapClient(configDic);
if ((modules & ExcludeModule.Updater) != ExcludeModule.Updater)
serviceProvider.UseUpdaterClient();
serviceProvider.UseExRoute().UseAccessClient().UseDecenterClient().UsePcpClient().UseRelayClient().UseSyncClient().UseTunnelClient().UseFlowClient();

View File

@@ -73,7 +73,7 @@ namespace linker.messenger.flow.messenger
sForwardFlow.Update();
SForwardFlowRequestInfo info = serializer.Deserialize<SForwardFlowRequestInfo>(connection.ReceiveRequestWrap.Payload.Span);
if (sForwardServerStore.SecretKey == info.SecretKey)
if (sForwardServerStore.ValidateSecretKey(info.SecretKey))
{
info.GroupId = string.Empty;
}
@@ -97,7 +97,7 @@ namespace linker.messenger.flow.messenger
{
relayFlow.Update();
RelayFlowRequestInfo info = serializer.Deserialize<RelayFlowRequestInfo>(connection.ReceiveRequestWrap.Payload.Span);
if (relayServerStore.SecretKey == info.SecretKey)
if (relayServerStore.ValidateSecretKey(info.SecretKey))
{
info.GroupId = string.Empty;
}
@@ -152,7 +152,7 @@ namespace linker.messenger.flow.messenger
private DateTime start = DateTime.Now;
public FlowClientMessenger( MessengerFlow messengerFlow,ISerializer serializer)
public FlowClientMessenger(MessengerFlow messengerFlow, ISerializer serializer)
{
this.messengerFlow = messengerFlow;
this.serializer = serializer;

View File

@@ -292,7 +292,7 @@ namespace linker.messenger.relay.messenger
public async Task UpdateNodeForward(IConnection connection)
{
RelayServerNodeUpdateWrapInfo info = serializer.Deserialize<RelayServerNodeUpdateWrapInfo>(connection.ReceiveRequestWrap.Payload.Span);
if (info.SecretKey == relayServerStore.SecretKey)
if (relayServerStore.ValidateSecretKey(info.SecretKey))
{
await relayServerTransfer.UpdateNodeReport(info.Info).ConfigureAwait(false);
connection.Write(Helper.TrueArray);
@@ -337,7 +337,7 @@ namespace linker.messenger.relay.messenger
public void AccessCdkey(IConnection connection)
{
string secretKey = serializer.Deserialize<string>(connection.ReceiveRequestWrap.Payload.Span);
connection.Write(relayServerStore.SecretKey == secretKey ? Helper.TrueArray : Helper.FalseArray);
connection.Write(relayServerStore.ValidateSecretKey(secretKey) ? Helper.TrueArray : Helper.FalseArray);
}
/// <summary>
/// 添加CDKEY
@@ -352,7 +352,7 @@ namespace linker.messenger.relay.messenger
connection.Write(Helper.FalseArray);
return;
}
if (relayServerStore.SecretKey != info.SecretKey)
if (relayServerStore.ValidateSecretKey(info.SecretKey))
{
connection.Write(Helper.FalseArray);
return;
@@ -376,7 +376,7 @@ namespace linker.messenger.relay.messenger
connection.Write(Helper.FalseArray);
return;
}
if (relayServerStore.SecretKey == info.SecretKey)
if (relayServerStore.ValidateSecretKey(info.SecretKey))
{
await relayServerCdkeyStore.Del(info.Id).ConfigureAwait(false);
}
@@ -401,7 +401,7 @@ namespace linker.messenger.relay.messenger
connection.Write(serializer.Serialize(new RelayServerCdkeyPageResultInfo { }));
return;
}
if (relayServerStore.SecretKey != info.SecretKey && string.IsNullOrWhiteSpace(info.UserId))
if (relayServerStore.ValidateSecretKey(info.SecretKey) && string.IsNullOrWhiteSpace(info.UserId))
{
connection.Write(serializer.Serialize(new RelayServerCdkeyPageResultInfo { }));
return;
@@ -427,7 +427,7 @@ namespace linker.messenger.relay.messenger
connection.Write(serializer.Serialize(new RelayServerCdkeyTestResultInfo { }));
return;
}
if (relayServerStore.SecretKey != info.SecretKey)
if (relayServerStore.ValidateSecretKey(info.SecretKey))
{
connection.Write(serializer.Serialize(new RelayServerCdkeyTestResultInfo { }));
return;

View File

@@ -2,10 +2,7 @@
{
public interface IRelayServerStore
{
/// <summary>
/// 中继密钥
/// </summary>
public string SecretKey { get; }
public bool ValidateSecretKey(string secretKey);
/// <summary>
/// 设置中继密钥

View File

@@ -15,7 +15,7 @@ namespace linker.messenger.relay.server.validator
public async Task<string> Validate(linker.messenger.relay.client.transport.RelayInfo relayInfo, SignCacheInfo fromMachine, SignCacheInfo toMachine)
{
if (relayInfo.SecretKey != relayServerStore.SecretKey)
if (relayServerStore.ValidateSecretKey(relayInfo.SecretKey) == false)
{
return $"SecretKey validate fail";
}

View File

@@ -2,10 +2,6 @@
{
public interface ISForwardServerStore
{
/// <summary>
/// 穿透密钥
/// </summary>
public string SecretKey { get; }
/// <summary>
/// 缓冲区大小
/// </summary>
@@ -19,6 +15,7 @@
/// </summary>
public int[] TunnelPortRange { get; }
public bool ValidateSecretKey(string key);
/// <summary>
/// 穿透密钥
/// </summary>

View File

@@ -17,7 +17,7 @@ namespace linker.messenger.sforward.server.validator
public async Task<string> Validate(SignCacheInfo signCacheInfo, SForwardAddInfo sForwardAddInfo)
{
if (sForwardServerStore.SecretKey != sForwardAddInfo.SecretKey)
if (sForwardServerStore.ValidateSecretKey(sForwardAddInfo.SecretKey))
{
return $"sforward secretKey 【{sForwardAddInfo.SecretKey}】 valid fail";
}

View File

@@ -7,12 +7,9 @@ namespace linker.messenger.signin
/// </summary>
public interface ISignInServerStore : IStore<SignCacheInfo>
{
/// <summary>
/// 信标密钥
/// </summary>
public string SecretKey { get; }
public int CleanDays { get; }
public bool ValidateSecretKey(string key);
/// <summary>
/// 设置信标密钥
/// </summary>

View File

@@ -51,12 +51,10 @@
/// <returns></returns>
public async Task<string> Validate(SignInfo signInfo, SignCacheInfo cache)
{
if (string.IsNullOrWhiteSpace(signInServerStore.SecretKey) == false)
signInfo.Args.TryGetValue("signin-secretkey", out string secretkey);
if (signInServerStore.ValidateSecretKey(secretkey) == false)
{
if (signInfo.Args.TryGetValue("signin-secretkey", out string secretkey) == false || secretkey != signInServerStore.SecretKey)
{
return $"server secretkey validate fail";
}
return $"server secretkey validate fail";
}
await Task.CompletedTask.ConfigureAwait(false);
return string.Empty;

View File

@@ -4,13 +4,17 @@ namespace linker.messenger.store.file.relay
{
public sealed class RelayServerStore : IRelayServerStore
{
public string SecretKey => config.Data.Server.Relay.SecretKey;
private readonly FileConfig config;
public RelayServerStore(FileConfig config)
{
this.config = config;
}
public bool ValidateSecretKey(string secretKey)
{
return string.IsNullOrWhiteSpace(config.Data.Server.Relay.SecretKey) || config.Data.Server.Relay.SecretKey == secretKey;
}
public void SetSecretKey(string secretKey)
{
config.Data.Server.Relay.SecretKey = secretKey;
@@ -21,5 +25,7 @@ namespace linker.messenger.store.file.relay
config.Data.Update();
return true;
}
}
}

View File

@@ -4,8 +4,6 @@ namespace linker.messenger.store.file.sforward
{
public sealed class SForwardServerStore : ISForwardServerStore
{
public string SecretKey => fileConfig.Data.Server.SForward.SecretKey;
public byte BufferSize => fileConfig.Data.Server.SForward.BufferSize;
public int WebPort => fileConfig.Data.Server.SForward.WebPort;
@@ -18,6 +16,10 @@ namespace linker.messenger.store.file.sforward
this.fileConfig = fileConfig;
}
public bool ValidateSecretKey(string key)
{
return string.IsNullOrWhiteSpace(fileConfig.Data.Server.SForward.SecretKey) || fileConfig.Data.Server.SForward.SecretKey == key;
}
public bool SetSecretKey(string key)
{
fileConfig.Data.Server.SForward.SecretKey = key;

View File

@@ -5,7 +5,6 @@ namespace linker.messenger.store.file.signIn
{
public sealed class SignInServerStore : ISignInServerStore
{
public string SecretKey => fileConfig.Data.Server.SignIn.SecretKey;
public int CleanDays => fileConfig.Data.Server.SignIn.CleanDays;
private readonly Storefactory dBfactory;
@@ -17,6 +16,11 @@ namespace linker.messenger.store.file.signIn
liteCollection = dBfactory.GetCollection<SignCacheInfo>("signs");
this.fileConfig = fileConfig;
}
public bool ValidateSecretKey(string key)
{
return string.IsNullOrWhiteSpace(fileConfig.Data.Server.SignIn.SecretKey) || fileConfig.Data.Server.SignIn.SecretKey == key;
}
public void SetSecretKey(string secretKey)
{
fileConfig.Data.Server.SignIn.SecretKey = secretKey;

View File

@@ -5,14 +5,16 @@ namespace linker.messenger.store.file.updater
{
public sealed class UpdaterServerStore : IUpdaterServerStore
{
public string SecretKey => fileConfig.Data.Server.Updater.SecretKey;
private readonly FileConfig fileConfig;
public UpdaterServerStore(FileConfig fileConfig)
{
this.fileConfig = fileConfig;
}
public bool ValidateSecretKey(string key)
{
return string.IsNullOrWhiteSpace(fileConfig.Data.Server.Updater.SecretKey) || fileConfig.Data.Server.Updater.SecretKey == key;
}
public void SetSecretKey(string key)
{
fileConfig.Data.Server.Updater.SecretKey = key;

View File

@@ -1,11 +1,17 @@
using linker.messenger.api;
using linker.libs;
using linker.libs.extends;
using linker.messenger.api;
using linker.messenger.decenter;
using linker.messenger.exroute;
using linker.messenger.signin;
using linker.messenger.tunnel;
using linker.messenger.tuntap.lease;
using linker.messenger.tuntap.messenger;
using linker.tun;
using Microsoft.Extensions.DependencyInjection;
using System.Net;
using System.Text;
using System.Text.Json;
namespace linker.messenger.tuntap
{
public static class Entry
@@ -35,7 +41,7 @@ namespace linker.messenger.tuntap
return serviceCollection;
}
public static ServiceProvider UseTuntapClient(this ServiceProvider serviceProvider)
public static ServiceProvider UseTuntapClient(this ServiceProvider serviceProvider, Dictionary<string, string> configDic)
{
TuntapProxy tuntapProxy = serviceProvider.GetService<TuntapProxy>();
TuntapTransfer tuntapTransfer = serviceProvider.GetService<TuntapTransfer>();
@@ -54,15 +60,63 @@ namespace linker.messenger.tuntap
IApiServer apiServer = serviceProvider.GetService<IApiServer>();
apiServer.AddPlugins(new List<libs.api.IApiController> { serviceProvider.GetService<TuntapApiController>() });
ExRouteTransfer exRouteTransfer= serviceProvider.GetService<ExRouteTransfer>();
ExRouteTransfer exRouteTransfer = serviceProvider.GetService<ExRouteTransfer>();
exRouteTransfer.AddExRoutes(new List<IExRoute> { serviceProvider.GetService<TuntapExRoute>() });
TunnelClientExcludeIPTransfer tunnelClientExcludeIPTransfer = serviceProvider.GetService<TunnelClientExcludeIPTransfer>();
tunnelClientExcludeIPTransfer.AddTunnelExcludeIPs(new List<ITunnelClientExcludeIP> { serviceProvider.GetService<TuntapTunnelExcludeIP>() });
DecenterClientTransfer decenterClientTransfer= serviceProvider.GetService<DecenterClientTransfer>();
DecenterClientTransfer decenterClientTransfer = serviceProvider.GetService<DecenterClientTransfer>();
decenterClientTransfer.AddDecenters(new List<IDecenter> { serviceProvider.GetService<TuntapDecenter>() });
if (configDic.TryGetValue("Tuntap", out string base64))
{
ITuntapClientStore tuntapClientStore = serviceProvider.GetService<ITuntapClientStore>();
ILeaseClientStore leaseClientStore = serviceProvider.GetService<ILeaseClientStore>();
ISignInClientStore signInClientStore = serviceProvider.GetService<ISignInClientStore>();
try
{
JsonElement doc = JsonDocument.Parse(Encoding.UTF8.GetString(Convert.FromBase64String(base64))).RootElement;
if (doc.TryGetProperty("IP", out JsonElement ip))
{
tuntapClientStore.Info.IP = IPAddress.Parse(ip.GetString());
}
if (doc.TryGetProperty("PrefixLength", out JsonElement prefixLength))
{
tuntapClientStore.Info.PrefixLength = prefixLength.GetByte();
}
if (doc.TryGetProperty("Lans", out JsonElement lans))
{
tuntapClientStore.Info.Lans = lans.GetString().DeJson<List<TuntapLanInfo>>();
}
if (doc.TryGetProperty("Name", out JsonElement name))
{
tuntapClientStore.Info.Name = name.GetString();
}
if (doc.TryGetProperty("Running", out JsonElement running))
{
tuntapClientStore.Info.Running = running.GetBoolean();
}
if (doc.TryGetProperty("Switch", out JsonElement _switch))
{
tuntapClientStore.Info.Switch = (TuntapSwitch)_switch.GetInt32();
}
if (doc.TryGetProperty("Forwards", out JsonElement forwards))
{
tuntapClientStore.Info.Forwards = forwards.GetString().DeJson<List<TuntapForwardInfo>>();
}
if (doc.TryGetProperty("Lease", out JsonElement lease))
{
leaseClientStore.Set(signInClientStore.Group.Id, lease.GetString().DeJson<LeaseInfo>());
}
}
catch (Exception ex)
{
LoggerHelper.Instance.Error(ex);
}
tuntapClientStore.Confirm();
}
return serviceProvider;
}

View File

@@ -2,10 +2,8 @@
{
public interface IUpdaterServerStore
{
/// <summary>
/// 更新密钥
/// </summary>
public string SecretKey { get; }
public bool ValidateSecretKey(string key);
/// <summary>
/// 设置更新密钥
/// </summary>

View File

@@ -132,7 +132,7 @@ namespace linker.messenger.updater
MachineId = string.Empty,
Current = info.Current,
Length = info.Length,
Status = info.Status,
Status = info.Status,
Version = info.Version
};
connection.Write(serializer.Serialize(result));
@@ -151,7 +151,7 @@ namespace linker.messenger.updater
public void ConfirmServer(IConnection connection)
{
UpdaterConfirmServerInfo confirm = serializer.Deserialize<UpdaterConfirmServerInfo>(connection.ReceiveRequestWrap.Payload.Span);
if (updaterServerStore.SecretKey == confirm.SecretKey)
if (updaterServerStore.ValidateSecretKey(confirm.SecretKey))
{
if (string.IsNullOrWhiteSpace(confirm.Version))
{
@@ -172,7 +172,7 @@ namespace linker.messenger.updater
public void ExitServer(IConnection connection)
{
UpdaterConfirmServerInfo confirm = serializer.Deserialize<UpdaterConfirmServerInfo>(connection.ReceiveRequestWrap.Payload.Span);
if (updaterServerStore.SecretKey == confirm.SecretKey)
if (updaterServerStore.ValidateSecretKey(confirm.SecretKey))
{
Environment.Exit(1);
}
@@ -195,7 +195,7 @@ namespace linker.messenger.updater
}
//需要密钥
if (confirm.All && updaterServerStore.SecretKey != confirm.SecretKey)
if (confirm.All && updaterServerStore.ValidateSecretKey(confirm.SecretKey) == false)
{
connection.Write(Helper.FalseArray);
return;

View File

@@ -1,9 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<project ver="10" name="linker.share.win" libEmbed="true" icon="res\favicon.ico" ui="win" output="linker.share.win.exe" CompanyName="snltty" FileDescription="linker.share.win" LegalCopyright="Copyright (C) snltty 2025" ProductName="linker.share.win" InternalName="linker.share.win" FileVersion="0.0.0.1" ProductVersion="0.0.0.1" publishDir="/dist/" dstrip="false">
<project ver="10" name="linker.share.win" libEmbed="true" icon="res\favicon.ico" ui="win" output="linker.share.win.exe" CompanyName="snltty" FileDescription="linker.share.win" LegalCopyright="Copyright (C) snltty 2025" ProductName="linker.share.win" InternalName="linker.share.win" FileVersion="0.0.0.3" ProductVersion="0.0.0.3" publishDir="/dist/" dstrip="false">
<file name="main.aardio" path="main.aardio" comment="main.aardio"/>
<folder name="资源文件" path="res" embed="true" local="false" ignored="false">
<file name="connect.aardio" path="res\connect.aardio" comment="res\connect.aardio"/>
<file name="share.aardio" path="res\share.aardio" comment="res\share.aardio"/>
<file name="info.aardio" path="res\info.aardio" comment="res\info.aardio"/>
</folder>
<folder name="窗体文件" path="dlg" comment="目录" embed="true"/>
</project>

View File

@@ -1,9 +1,9 @@
//RUNAS//
import win.ui;
/*DSG{{*/
mainForm = win.form(text="linker.share.win";right=250;bottom=291;border="thin";max=false;topmost=1)
mainForm = win.form(text="linker局域网共享";right=352;bottom=312;border="thin";max=false;topmost=1)
mainForm.add(
mainTab={cls="tab";left=1;top=-2;right=250;bottom=254;aw=1;edge=1;z=1}
mainTab={cls="tab";left=8;top=1;right=346;bottom=306;z=1}
)
/*}}*/
@@ -16,6 +16,7 @@ if(!atom){
mainForm.mainTab.loadForm("\res\connect.aardio");
mainForm.mainTab.loadForm("\res\share.aardio");
mainForm.mainTab.loadForm("\res\info.aardio");
mainForm.show();
return win.loopMessage();

View File

@@ -1,7 +1,16 @@
import win.ui;
/*DSG{{*/
var winform = win.form(text="连接共享";right=759;bottom=469)
winform.add()
var winform = win.form(text="连接共享";right=330;bottom=276)
winform.add(
button={cls="button";text="删除";left=223;top=10;right=269;bottom=31;z=3};
button2={cls="button";text="新增";left=275;top=10;right=321;bottom=31;z=4};
button3={cls="button";text="开启连接";left=120;top=234;right=208;bottom=269;z=8};
checkbox={cls="checkbox";text="自动开启连接";left=10;top=242;right=105;bottom=261;z=7};
combobox={cls="combobox";left=67;top=11;right=217;bottom=32;edge=1;items={};mode="dropdown";z=2};
groupbox={cls="groupbox";text="在线的";left=10;top=37;right=321;bottom=228;edge=1;z=5};
listview2={cls="listview";left=17;top=56;right=314;bottom=221;edge=1;z=6};
static={cls="static";text="选择共享";left=10;top=12;right=67;bottom=29;transparent=1;z=1}
)
/*}}*/
winform.show();

View File

@@ -1,13 +1,286 @@
import win.ui;
/*DSG{{*/
var winform = win.form(text="发出共享";right=243;bottom=254)
var winform = win.form(text="发出共享";right=330;bottom=283)
winform.add(
button={cls="button";text="生成";left=195;top=12;right=234;bottom=35;z=3};
edit={cls="edit";text="Edit";left=44;top=14;right=189;bottom=35;edge=1;z=1};
static={cls="static";text="密钥";left=11;top=16;right=48;bottom=35;transparent=1;z=2}
btnCopy={cls="button";text="复制共享密钥";left=229;top=240;right=321;bottom=267;z=16};
btnNewSecretkey={cls="button";text="新ID";left=276;top=41;right=321;bottom=64;z=14};
btnSave={cls="button";text="保存";left=276;top=12;right=321;bottom=35;z=19};
btnStart={cls="button";text="开启共享";left=122;top=237;right=210;bottom=272;z=15};
cbLan={cls="combobox";left=80;top=102;right=205;bottom=123;edge=1;items={};mode="dropdown";z=4};
cbLanPrefixlength={cls="combobox";left=248;top=102;right=314;bottom=123;edge=1;items={};mode="dropdown";z=7};
cbRoutePrefixlength={cls="combobox";left=248;top=131;right=314;bottom=152;edge=1;items={};mode="dropdown";z=9};
ckAuto={cls="checkbox";text="自动开启共享";left=11;top=246;right=115;bottom=265;z=17};
groupbox={cls="groupbox";text="共享网段";left=11;top=75;right=321;bottom=228;edge=1;z=3};
static={cls="static";text="服务器";left=11;top=16;right=54;bottom=33;transparent=1;z=2};
static3={cls="static";text="内网网段";left=19;top=105;right=72;bottom=122;transparent=1;z=5};
static4={cls="static";text="掩码";left=216;top=105;right=247;bottom=119;transparent=1;z=6};
static5={cls="static";text="掩码";left=216;top=135;right=247;bottom=152;transparent=1;z=8};
static6={cls="static";text="映射路由";left=19;top=134;right=72;bottom=154;transparent=1;z=11};
static9={cls="static";text="唯一ID";left=11;top=46;right=52;bottom=63;transparent=1;z=13};
textMsg={cls="edit";left=19;top=163;right=314;bottom=219;autohscroll=false;edge=1;multiline=1;readonly=1;z=18};
textRoute={cls="ipaddress";left=80;top=132;right=205;bottom=153;edge=1;z=10};
textSecretkey={cls="edit";left=58;top=42;right=273;bottom=63;edge=1;z=12};
textServer={cls="edit";left=58;top=13;right=273;bottom=34;edge=1;z=1}
)
/*}}*/
import dotNet;
compiler = dotNet.createCompiler("C#");
compiler.Source = /******
using System;
namespace LinkerLibsSpace
{
public class LinkerLibs
{
public string GetGuid()
{
return Guid.NewGuid().ToString();
}
}
}
******/
assembly = compiler.CompileOrFail();
assembly.import("LinkerLibsSpace");
winform.linkerLibs = LinkerLibsSpace.LinkerLibs();
if(mainForm != null)
{
winform.mainForm = mainForm;
winform.mainForm.shareForm =winform;
}
import crypt.aes;
import crypt.bin;
import web.json;
winform.aes = crypt.aes();
winform.aes.setPassword("BC7F0C7C-6A1E-4792-A17F-0008D300C076");
winform.shareSettingTab = {
"server"="",
"secretkey" = winform.linkerLibs.GetGuid(),
"lan"="",
"lanPrefixlength"="24",
"route"="192.168.188.0",
"routePrefixlength"="24",
"auto"=false
}
winform.loadShareSetting = function()
{
configStr = string.load("~/configs/share.json");
if(configStr)
{
json = web.json.parse(winform.aes.decrypt(configStr));
winform.shareSettingTab.server = json.server;
winform.shareSettingTab.secretkey = json.secretkey;
winform.shareSettingTab.lan = json.lan;
winform.shareSettingTab.lanPrefixlength = json.lanPrefixlength;
winform.shareSettingTab.route = json.route;
winform.shareSettingTab.routePrefixlength = json.routePrefixlength;
winform.shareSettingTab.auto = json.auto;
}
winform.btnSave.oncommand = function()
{
winform.saveShareSetting();
}
}
winform.saveShareSetting = function()
{
import console;
winform.shareSettingTab.server = winform.textServer.text;
winform.shareSettingTab.secretkey = winform.textSecretkey.text;
winform.shareSettingTab.lan = winform.cbLan.selText;
winform.shareSettingTab.lanPrefixlength = winform.cbLanPrefixlength.selText;
winform.shareSettingTab.route = winform.textRoute.text;
winform.shareSettingTab.routePrefixlength = winform.cbRoutePrefixlength.selText;
winform.shareSettingTab.auto = winform.ckAuto.checked;
config = winform.aes.encrypt(web.json.stringify(winform.shareSettingTab));
import fsys;
try{fsys.delete("~/configs/share.json");}catch(e){}
string.save("~/configs/share.json",config ,true);
win.msgbox("已保存配置",,0x01);
}
winform.initLans = function()
{
import com.wmi;
winform.prefixlengthDefault = "24";
winform.prefixlengthTab = {32,31,30,29,28,27,26,25,24,23,22,21,20,19,18,17,16};
winform.cbLanPrefixlength.items = winform.prefixlengthTab;
winform.cbLanPrefixlength.selText = winform.shareSettingTab.lanPrefixlength;
winform.cbLanPrefixlength.onEditChange = function(){
winform.cbLanPrefixlength.selText = winform.prefixlengthDefault;
}
winform.cbRoutePrefixlength.items = winform.prefixlengthTab;
winform.cbRoutePrefixlength.selText = winform.shareSettingTab.routePrefixlength;
winform.cbRoutePrefixlength.onEditChange = function(){
winform.cbRoutePrefixlength.selText = winform.prefixlengthDefault;
}
lansTab = {};
for item in com.wmi.eachProperties("SELECT * FROM Win32_NetworkAdapterConfiguration WHERE IPEnabled=true") {
table.push(lansTab,item.IPAddress[1]);
}
winform.cbLan.items = lansTab;
if(#winform.shareSettingTab.lan)winform.cbLan.selText = winform.shareSettingTab.lan
else winform.cbLan.selText = lansTab[1];
winform.cbLan.onEditChange = function(){
winform.cbLan.selText = lansTab[1];
}
winform.textRoute.text = winform.shareSettingTab.route;
function setText()
{
winform.textMsg.text = "对方使用【"+winform.textRoute.text+"/"+winform.cbRoutePrefixlength.selText+"】访问你的【"+winform.cbLan.selText+"/"+winform.cbLanPrefixlength.selText+"】,这能有效的解决网段冲突,和隐藏你的真实内网网段";
}
winform.cbLan.onListChange = function(){
setText();
}
winform.cbLanPrefixlength.onListChange = function(){
setText();
}
winform.cbRoutePrefixlength.onListChange = function(){
setText();
}
winform.textRoute.onChange = function(){
if(owner.onModified)owner.onModified(true);
setText();
}
setText();
}
winform.initSecretkey = function()
{
winform.textSecretkey.text = winform.shareSettingTab.secretkey;
winform.btnNewSecretkey.oncommand = function(){
result = win.msgbox("旧的共享密钥将不可用确认使用一个新ID吗",,0x01/*MB_YESNOCANCEL*/ );
if(result == 1)
{
winform.textSecretkey.text = winform.linkerLibs.GetGuid();
}
}
}
winform.initAuto = function()
{
winform.ckAuto.checked = winform.shareSettingTab.auto;
}
winform.initServer = function()
{
winform.textServer.text = winform.shareSettingTab.server;
}
winform.getShareKey = function()
{
json = {
"Client"={
CApi = {ApiPort:18003,ApiPassword="snltty",WebPort:0},
Servers={{Name="linker",Host=winform.textServer.text}},
Groups={{Name="Linker",Id="Linker",Password=winform.textSecretkey.text}}
},
"Common"= { Modes={"client"},Install=true },
"Tuntap" = {
Running=true,
Lans={
{
IP= winform.cbLan.selText,
PrefixLength = winform.cbLanPrefixlength.selText,
MapIP = winform.textRoute.text,
MapPrefixLength = winform.cbRoutePrefixlength.selText
}
},
Lease={
IP="10.18.0.0",
PrefixLength=16,
Name="linkers"
}
}
}
if(!#json.Client.Servers[1].Host)
{
winform.textServer.showErrorTip("必填","请填写服务器")
return null;
}
if(!#json.Client.Groups[1].Password)
{
winform.textSecretkey.showErrorTip("必选","请填写唯一id")
return null;
}
if(!#json.Tuntap.Lans[1].IP)
{
winform.cbLan.showErrorTip("必选","请选择一个局域网IP")
return null;
}
return json;
}
winform.initSHareKey = function()
{
winform.btnCopy.oncommand = function()
{
import win.clip;
json = winform.getShareKey();
if(json)
{
encodeText = winform.aes.encrypt(web.json.stringify(json));
base64Text = crypt.bin.encodeBase64(encodeText);
win.clip.write(base64Text);
win.msgbox("已复制到剪贴板",,0x01);
}
}
}
winform.startProc = null;
winform.runStart = function()
{
import process.popen;
import console;
winform.btnStart.text = "操作中..";
if(winform.startProc)
{
winform.startProc.terminate();
winform.startProc = null;
}
else
{
json = winform.getShareKey();
if(json)
{
params = {
"--config-client",
crypt.bin.encodeBase64(web.json.stringify(json.Client))
"--config-common",
crypt.bin.encodeBase64(web.json.stringify(json.Common))
"--config-tuntap",
crypt.bin.encodeBase64(web.json.stringify(json.Tuntap))
}
winform.startProc = process.popen("~/linker.exe",process.joinArguments(params));
}
}
winform.btnStart.text = winform.startProc ? "关闭共享" : "开启共享";
}
winform.initStart = function()
{
if(winform.ckAuto.checked)
{
winform.runStart();
}
winform.btnStart.oncommand = function()
{
winform.runStart();
}
}
winform.loadShareSetting();
winform.initLans();
winform.initSecretkey();
winform.initAuto();
winform.initServer();
winform.initSHareKey();
winform.initStart();
winform.show();
win.loopMessage();
return winform;

View File

@@ -2,19 +2,23 @@
using linker.libs.extends;
using Microsoft.Win32.SafeHandles;
using System.Net;
using System.Threading;
using System.Runtime.InteropServices;
namespace linker.tun
{
/// <summary>
/// osx网卡实现未测试
/// </summary>
internal sealed class LinkerOsxTunDevice : ILinkerTunDevice
{
private string name = string.Empty;
public string Name => name;
public bool Running => fs != null;
public bool Running => safeFileHandle != null;
public bool AppNat => false;
private FileStream fs = null;
private FileStream fsRead = null;
private FileStream fsWrite = null;
private IPAddress address;
private byte prefixLength = 24;
private SafeFileHandle safeFileHandle;
@@ -28,42 +32,79 @@ namespace linker.tun
public bool Setup(string name, IPAddress address, byte prefixLength, out string error)
{
this.name = name;
this.name = "utun0";
error = string.Empty;
interfaceOsx = GetOsxInterfaceNum();
this.address = address;
this.prefixLength = prefixLength;
safeFileHandle = File.OpenHandle($"/dev/{Name}", FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite, FileOptions.Asynchronous);
fs = new FileStream(safeFileHandle, FileAccess.ReadWrite, 1500);
IntPtr arg = Marshal.AllocHGlobal(4);
Marshal.WriteInt32(arg, 0);
try
{
interfaceOsx = GetOsxInterfaceNum();
safeFileHandle = File.OpenHandle($"/dev/utun", FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite, FileOptions.Asynchronous);
IPAddress network = NetworkHelper.ToNetworkIP(address, NetworkHelper.ToPrefixValue(prefixLength));
CommandHelper.Osx(string.Empty, new string[] {
$"route delete -net {network}/{prefixLength} {address}",
$"ifconfig {Name} {address} {address} up",
$"route add -net {network}/{prefixLength} {address}",
});
return true;
int ret = OsxAPI.Ioctl(safeFileHandle, arg);
if (ret < 0)
{
Shutdown();
error = $"open utun failed: {Marshal.GetLastWin32Error()}";
return false;
}
this.name = $"utun{Marshal.ReadInt32(arg)}";
fsRead = new FileStream(safeFileHandle, FileAccess.Read, 65 * 1024, true);
fsWrite = new FileStream(safeFileHandle, FileAccess.Write, 65 * 1024, true);
IPAddress network = NetworkHelper.ToNetworkIP(address, NetworkHelper.ToPrefixValue(prefixLength));
CommandHelper.Osx(string.Empty, new string[] {
$"route delete -net {network}/{prefixLength} {address}",
$"ifconfig {Name} {address} {address} up",
$"route add -net {network}/{prefixLength} {address}",
});
return true;
}
catch (Exception ex)
{
Shutdown();
error = $"open utun failed: {ex.Message}";
}
finally
{
Marshal.FreeHGlobal(arg);
}
return false;
}
public void Shutdown()
{
if (fs != null)
try
{
safeFileHandle?.Dispose();
interfaceOsx = string.Empty;
fs.Close();
fs.Dispose();
fs = null;
safeFileHandle?.Dispose();
safeFileHandle = null;
try { fsRead?.Flush(); } catch (Exception) { }
try { fsRead?.Close(); fsRead?.Dispose(); } catch (Exception) { }
fsRead = null;
try { fsWrite?.Flush(); } catch (Exception) { }
try { fsWrite?.Close(); fsWrite?.Dispose(); } catch (Exception) { }
fsWrite = null;
}
catch (Exception)
{
}
IPAddress network = NetworkHelper.ToNetworkIP(address, NetworkHelper.ToPrefixValue(this.prefixLength));
CommandHelper.Osx(string.Empty, new string[] { $"route delete -net {network}/{prefixLength} {address}" });
}
public void Refresh()
{
}
public void AddRoute(LinkerTunDeviceRouteItem[] ips)
@@ -93,7 +134,7 @@ namespace linker.tun
public void SetMtu(int value)
{
CommandHelper.Osx(string.Empty, new string[] { $"ifconfig {Name} mtu {value}" });
CommandHelper.Osx(string.Empty, new string[] { $"ifconfig {Name} mtu {value} up" });
}
public void SetSystemNat(out string error)
@@ -134,34 +175,28 @@ namespace linker.tun
}
private byte[] buffer = new byte[2 * 1024];
private byte[] buffer = new byte[65 * 1024];
private readonly object writeLockObj = new object();
public byte[] Read(out int length)
{
length = 0;
try
{
length = fs.Read(buffer.AsSpan(4));
length.ToBytes(buffer);
length += 4;
return buffer;
}
catch (Exception)
{
}
return Helper.EmptyArray;
if (safeFileHandle == null) return Helper.EmptyArray;
length = fsRead.Read(buffer.AsSpan(4));
length.ToBytes(buffer);
length += 4;
return buffer;
}
public bool Write(ReadOnlyMemory<byte> buffer)
{
try
if (safeFileHandle == null) return true;
lock (writeLockObj)
{
fs.Write(buffer.Span);
fsWrite.Write(buffer.Span);
fsWrite.Flush();
return true;
}
catch (Exception)
{
}
return false;
}
private string GetOsxInterfaceNum()
@@ -187,7 +222,6 @@ namespace linker.tun
return string.Empty;
}
public async Task<bool> CheckAvailable(bool order = false)
{
return await Task.FromResult(true).ConfigureAwait(false);

23
src/linker.tun/OsxTun.cs Normal file
View File

@@ -0,0 +1,23 @@
using Microsoft.Win32.SafeHandles;
using System.Runtime.InteropServices;
using System.Text;
namespace linker.tun
{
internal static class OsxAPI
{
// 定义 macOS 的 ioctl 命令(来自 <net/if_utun.h>
private const uint UTUN_CONTROL = 0x80000000; // 'u' << 24
private const uint UTUN_CTRL_SET_IFNAME = (UTUN_CONTROL | 1);
// P/Invoke 声明
[DllImport("libc", EntryPoint = "ioctl", SetLastError = true)]
private static extern int Ioctl(SafeFileHandle fd, uint request, IntPtr arg);
public static int Ioctl(SafeFileHandle fd, IntPtr arg)
{
return Ioctl(fd, UTUN_CTRL_SET_IFNAME, arg);
}
}
}

View File

@@ -34,7 +34,7 @@ namespace linker.tunnel.transport
public bool DisableSSL => false;
public byte Order => 3;
public byte Order => 5;
/// <summary>
/// 连接成功

View File

@@ -32,7 +32,7 @@ namespace linker.tunnel.transport
public bool DisableSSL => false;
public byte Order => 2;
public byte Order => 4;
/// <summary>
/// 连接成功

View File

@@ -35,7 +35,7 @@ namespace linker.tunnel.transport
public bool DisableSSL => false;
public byte Order => 5;
public byte Order => 2;
public Action<ITunnelConnection> OnConnected { get; set; } = (state) => { };

View File

@@ -38,7 +38,7 @@ namespace linker.tunnel.transport
public bool DisableSSL => false;
public byte Order => 1;
public byte Order => 3;
public Action<ITunnelConnection> OnConnected { get; set; } = (state) => { };

View File

@@ -33,7 +33,7 @@ namespace linker.tunnel.transport
public bool DisableSSL => false;
public byte Order => 4;
public byte Order => 1;
public Action<ITunnelConnection> OnConnected { get; set; } = (state) => { };

View File

@@ -88,6 +88,11 @@ namespace linker
configDic.Add("Common", args[i + 1]);
i++;
}
else if (args[i] == "--config-tuntap")
{
configDic.Add("Tuntap", args[i + 1]);
i++;
}
}
return configDic;
}

View File

@@ -1,5 +1,5 @@
v1.7.6
2025-04-27 11:23:46
2025-04-28 16:09:25
1. 一些优化
2. 安卓APP勉强能用支持分身下拉刷新在线升级
5. 如果你设备很多,请尝试升级其中一个成功重启后再升级其它