区块链中文技术社区

OpenVPN 管理器:面向企业 VPN 管理的完整开源解决方案

管理整个组织内的 VPN 用户不应该再像 2005 年那样了。不再需要使用包含凭据的电子表格、手动配置文件,也不再需要费心去了解“现在谁在线?”。

OpenVPN Manager——一个开源的 Web 应用程序,它为您的 VPN 基础架构带来现代化的用户管理;同时发布的还有OpenVPN Client——一个配套工具,它可以将所有内容无缝集成。

问题

如果你曾经为团队管理过 OpenVPN 服务器,你就会了解这些痛点:

  • 用户管理一片混乱——添加用户意味着生成证书、编辑配置,还要祈祷别搞砸了什么。
  • 完全看不到任何信息——谁在线?带宽使用情况如何?连接历史记录?祝你好运,希望能找到答案。
  • 访问控制难题——工程部门和人力资源部门需要不同的网络访问权限,但 OpenVPN 并没有简化这一过程。
  • 审计要求——当安全部门询问“谁在何时访问了什么?”时,您可能需要花费数小时翻阅日志。
  • 承包商访问权限——本应过期但从未过期的临时用户。

解决方案:两个项目,一个完整系统

OpenVPN 管理器(Web 应用程序)

整个系统的核心。一个基于网页的管理面板,您可以在这里管理所有事项:

基于角色的访问控制的用户管理

三个角色确保一切井然有序:

  • 用户——可以查看和编辑自己的个人资料
  • 管理员——可以创建和管理团队中的用户
  • 管理员——拥有所有权限

非常适合团队负责人需要管理团队的 VPN 访问权限,而又不想麻烦 IT 部门的组织。


基于组的网络访问

定义用户组(例如工程组、人力资源组、财务组),并为每个用户分配网络段。用户继承其所属用户的访问权限。简单、可扩展且可审计。


VPN IP 自动分配

只需配置一次 VPN 网络范围,系统就会自动为新用户分配下一个可用的 IP 地址。从此告别 IP 地址冲突和手动追踪。

会话跟踪与审核

最后,了解您的 VPN 信息:

  • 实时连接状态
  • 用户带宽使用情况
  • 完整的连接历史记录
  • 所有变更的完整审计跟踪


限时访问

需要授予承包商两周的访问权限?设置日期valid_from即可valid_to。访问权限会自动过期。再也不用担心忘记登录了。

OpenVPN客户端(集成脚本)

将你的 OpenVPN 服务器连接到管理器的 Go 二进制文件:

┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ 
│ OpenVPN 服务器 │─────────────▶│ OpenVPN 客户端 │────▶│ OpenVPN 管理器
│ │ │ │ (Go 二进制文件)                           │ │ (Web 应用) │ 
└─────────────────┘ └───────────────────┘ └───────────────────┘ 
                               │ 
                               ▼ 
                        ┌───────────────────┐ 
                        │ nftables/iptables 
                        │ │ (防火墙) │ 
                        └─────────────────┘

四个二进制文件处理所有事情:

BinaryPurpose 功能openvpn-login包括:根据 API 验证用户凭据openvpn-connect;分配 IP 地址;根据组成员身份推送路由openvpn-disconnect;记录会话结束和流量统计信息;openvpn-firewall为每个用户生成 nftables/iptables 规则。

完整安装指南

步骤 1:安装 OpenVPN 管理器

选项 A:DEB 软件包(Debian/Ubuntu)

VERSION="1.0.1" 
wget https://github.com/tldr-it-stepankutaj/openvpn-mng/releases/download/v${VERSION}/openvpn-mng_${VERSION}_linux_amd64.deb 
sudo dpkg -i openvpn-mng_${VERSION}_linux_amd64.deb

选项 B:RPM 软件包(RHEL/AlmaLinux/Rocky)

VERSION="1.0.1" 
wget https://github.com/tldr-it-stepankutaj/openvpn-mng/releases/download/v${VERSION}/openvpn-mng_${VERSION}_linux_amd64.rpm 
sudo dnf install ./openvpn-mng_${VERSION}_linux_amd64.rpm

选项 C:Docker

docker pull tldrit/openvpn-mng:latest

步骤 2:配置数据库

# PostgreSQL 
sudo -u postgres psql 
CREATE USER openvpn WITH PASSWORD 'your-secure-password'; 
CREATE DATABASE openvpn_mng OWNER openvpn; 
\q

步骤 3:配置 OpenVPN 管理器

# 生成密钥
JWT_SECRET=$(openssl rand -hex 32) 
VPN_TOKEN=$(openssl rand -hex 32)
echo "JWT_SECRET: $JWT_SECRET" 
echo "VPN_TOKEN: $VPN_TOKEN"
# 编辑配置文件
sudo nano /etc/openvpn-mng/config.yaml

关键设置:

database:
  type: "postgres"
  host: "localhost"
  password: "your-db-password"
  database: "openvpn_mng"
auth:
  jwt_secret: "your-generated-jwt-secret"
api:
  vpn_token: "your-generated-vpn-token"
vpn:
  network: "10.90.90.0/24"
  server_ip: "10.90.90.1"

步骤 4:启动 OpenVPN 管理器

sudo systemctl enable --now openvpn-mng.service
# 验证
curl http://localhost:8080/

默认登录名:admin/ admin123— 请立即更改!

步骤 5:安装 OpenVPN 客户端

选项 A:DEB 软件包(Debian/Ubuntu)

VERSION="1.0.0" 
wget https://github.com/tldr-it-stepankutaj/openvpn-client/releases/download/v${VERSION}/openvpn-client_${VERSION}_linux_amd64.deb 
sudo dpkg -i openvpn-client_${VERSION}_linux_amd64.deb

选项 B:RPM 软件包(RHEL/AlmaLinux/Rocky)

VERSION="1.0.0" 
wget https://github.com/tldr-it-stepankutaj/openvpn-client/releases/download/v${VERSION}/openvpn-client_${VERSION}_linux_amd64.rpm 
sudo dnf install ./openvpn-client_${VERSION}_linux_amd64.rpm

选项 C:从源代码构建

git clone https://github.com/tldr-it-stepankutaj/openvpn-client.git 
cd openvpn-client 
make build-linux 
sudo make install

步骤 6:配置 OpenVPN 客户端

sudo mkdir -p /etc/openvpn/client 
sudo nano /etc/openvpn/client/config.yaml
api:
  base_url: "http://127.0.0.1:8080"
  token: "your-vpn-token-from-step-3"
  timeout: 10s
openvpn:
  session_dir: "/var/run/openvpn"
firewall:
  type: "nftables"
  nftables:
    rules_file: "/etc/nftables.d/vpn-users.nft"
    reload_command: "/usr/sbin/nft -f /etc/sysconfig/nftables.conf"

步骤 7:配置 OpenVPN 服务器

添加到您的 OpenVPN 服务器配置中:

# VPN network (must match OpenVPN Manager config)
server 10.90.90.0 255.255.255.0
topology subnet
# Authentication via OpenVPN Manager
username-as-common-name
auth-user-pass-verify /usr/bin/openvpn-login via-file
client-connect /usr/bin/openvpn-connect
client-disconnect /usr/bin/openvpn-disconnect
script-security 2

步骤 8:设置防火墙规则定时任务

# 创建初始规则
sudo mkdir -p /etc/nftables.d 
sudo touch /etc/nftables.d/vpn-users.nft 
sudo /usr/bin/openvpn-firewall
# 添加 cron 任务
echo "*/5 * * * * root /usr/bin/openvpn-firewall >> /var/log/openvpn-firewall.log 2>&1" | sudo tee /etc/cron.d/openvpn-firewall

步骤 9:重启 OpenVPN

sudo systemctl restart openvpn@server

这一切是如何协同运作的

  1. 用户使用用户名和密码连接
  2. openvpn-login使用 Manager API 验证凭据
  3. openvpn-connect获取用户的组和网络,然后推送路由
  4. 防火墙规则根据用户的组成员身份限制流量
  5. 会话在管理器数据库中进行跟踪。
  6. 管理员可在网页界面查看实时连接。
  7. openvpn-disconnect 会在用户断开连接时记录流量统计信息。

技术栈

OpenVPN 管理器:

  • Go + Gin 框架
  • GORM(PostgreSQL/MySQL)
  • JWT 身份验证
  • Bootstrap 5 Web UI

OpenVPN客户端:

  • Go
  • nftables/iptables 支持
  • 结构化日志记录(slog)

两者都编译成单个二进制文件,可以部署到任何地方。

接下来是什么?

计划功能:

  • LDAP/Active Directory 集成
  • 双因素身份验证
  • WireGuard 支持
  • 普罗米修斯指标
  • Terraform 提供程序

开始使用

OpenVPN 管理器:

OpenVPN客户端:

这两个项目均采用 Apache 2.0 许可协议。欢迎贡献代码!

如果您厌倦了传统的 VPN 用户管理方式,不妨试试这个方法。如果您觉得它有用,请在 GitHub 上点个星标,非常感谢!

原文:https://medium.com/@stepan_95301/openvpn-manager-a-complete-open-source-solution-for-enterprise-vpn-management-fe214b8f4627

MPCium 实测

部署

  1. 编译部署

git clone https://github.com/fystack/mpcium.git
cd mpcium
source /etc/profile
go env -w GOPRIVATE=github.com/bcskill/*
go mod tidy
make

可用命令

  • mpcium:启动 MPCium 节点
  • mpcium-cli:用于对等方、身份和发起方配置的 CLI 实用程序
go build ./cmd/mpcium
go build ./cmd/mpcium-cli
  1. 启动NATS 和 Consul

cd mpcium
docker compose up -d

NATS:高性能、轻量级的 消息系统(消息队列 / 消息总线),主要用于服务之间的异步通信

Service A ───► NATS ───► Service B

​ ▲

​ └─► Service C(也可同时收到消息)

Consul:HashiCorp 开源的一个用于服务网络的工具,主要用于 服务发现、配置管理、健康检查,也支持 分布式 Key-Value 存储服务网格功能

[Service A] <---> [Consul Agent] <---> [Consul Server] <---> [Service B info]

  1. 部署节点

3.1 创建节点配置

cd ../
mkdir test-node
cd test-node
mpcium-cli generate-peers -n 3
cp ../mpcium/config.yaml.template ./config.yaml

编辑badger_password 为16,24,32 位密码

3.2 注册链节点

mpcium-cli register-peers

3.3 生成启动器

mpcium-cli generate-initiator

从生成的event_initiator.identity.json 中复制public_key, 更新到 config.yaml中的event_initiator_pubkey

3.4 创建目录

mkdir node{0..2}
for dir in node{0..2}; do cp config.yaml peers.json "$dir/"; done

3.5 创建身份

cd node0
mpcium-cli generate-identity --node node0
cd ../node1
mpcium-cli generate-identity --node node1
cd ../node2
mpcium-cli generate-identity --node node2
cd ../

cp node0/identity/node0_identity.json node1/identity/node0_identity.json
cp node0/identity/node0_identity.json node2/identity/node0_identity.json
cp node1/identity/node1_identity.json node0/identity/node1_identity.json
cp node1/identity/node1_identity.json node2/identity/node1_identity.json
cp node2/identity/node2_identity.json node0/identity/node2_identity.json
cp node2/identity/node2_identity.json node1/identity/node2_identity.json

3.6 启动节点

cd node0
mpcium start -n node0
cd node1
mpcium start -n node1
cd node2
mpcium start -n node2

实际测试

https://github.com/fystack/mpcium-client-ts

event_initiator.key

f81851f905abc51de2618ddb7f75dde2a422dd0f6f3abb0d51c850287ecac900

创建钱包

import { connect } from "nats";
import { KeygenSuccessEvent, MpciumClient } from "../src";
import { computeAddress, hexlify } from "ethers";
import base58 from "bs58";
import * as fs from "fs";
import * as path from "path";
import { v4 } from "uuid";

async function main() {
  const args = process.argv.slice(2);
  const nIndex = args.indexOf("-n");
  const walletCount = nIndex !== -1 ? parseInt(args[nIndex + 1]) || 1 : 1;

  const nc = await connect({ servers: "nats://127.0.0.1:4222" }).catch(
    (err) => {
      console.error(`Failed to connect to NATS: ${err.message}`);
      process.exit(1);
    }
  );
  console.log(`Connected to NATS at ${nc.getServer()}`);

  const mpcClient = await MpciumClient.create({
    nc: nc,
    keyPath: "./event_initiator.key",
    // password: "your-password-here",
  });

  const walletsPath = path.resolve("./wallets.json");
  let wallets: Record<string, KeygenSuccessEvent> = {};
  if (fs.existsSync(walletsPath)) {
    try {
      wallets = JSON.parse(fs.readFileSync(walletsPath, "utf8"));
    } catch (error) {
      console.warn(`Could not read wallets file: ${error.message}`);
    }
  }

  let remaining = walletCount;

  mpcClient.onWalletCreationResult((event: KeygenSuccessEvent) => {
    const timestamp = new Date().toISOString();
    console.log(`${timestamp} Received wallet creation result:`, event);

    if (event.eddsa_pub_key) {
      const pubKeyBytes = Buffer.from(event.eddsa_pub_key, "base64");
      const solanaAddress = base58.encode(pubKeyBytes);
      console.log(`Solana wallet address: ${solanaAddress}`);
    }

    if (event.ecdsa_pub_key) {
      const pubKeyBytes = Buffer.from(event.ecdsa_pub_key, "base64");
      const uncompressedKey =
        pubKeyBytes.length === 65
          ? pubKeyBytes
          : Buffer.concat([Buffer.from([0x04]), pubKeyBytes]);
      const ethAddress = computeAddress(hexlify(uncompressedKey));
      console.log(`Ethereum wallet address: ${ethAddress}`);
    }

    wallets[event.wallet_id] = event;
    fs.writeFileSync(walletsPath, JSON.stringify(wallets, null, 2));
    console.log(`Wallet saved to wallets.json with ID: ${event.wallet_id}`);

    remaining -= 1;
    if (remaining === 0) {
      console.log("All wallets generated.");
    }
  });

  try {
    for (let i = 0; i < walletCount; i++) {
      const timestamp = new Date().toISOString();
      const walletID = await mpcClient.createWallet(`${v4()}:${i}`);
      console.log(
        `${timestamp} CreateWallet sent #${
          i + 1
        }, awaiting result... walletID: ${walletID}`
      );
    }

    const shutdown = async () => {
      console.log("Cleaning up...");
      await mpcClient.cleanup();
      await nc.drain();
      process.exit(0);
    };

    process.on("SIGINT", shutdown);
  } catch (error) {
    console.error("Error:", error);
    await mpcClient.cleanup();
    await nc.drain();
    process.exit(1);
  }
}

main().catch(console.error);
/mpcium-client-ts# ts-node ./examples/generate.ts 
Connected to NATS at 127.0.0.1:4222
Subscribed to wallet creation results (consume mode)
  result_type: 'success',
  error_reason: '',
  error_code: ''
}
Solana wallet address: Cmroym1zhiv6Q3DaHSRQv3CJBFnBqm4dtLkYnsAqbQ2c
Ethereum wallet address: 0xef049bd27aCD860D7A157e492378bf54eE9671B9
Wallet saved to wallets.json with ID: b7b3cb5e-fbea-4785-86c7-f67c3562ddfc:0

签名发送交易

测试地址,忽略手续费不足问题

import { connect } from "nats";
import { MpciumClient, KeyType, SigningResultEvent } from "../src";
import { ethers } from "ethers";
import { SigningResultType } from "../src/types";
import * as fs from "fs";
import * as path from "path";

// The wallet ID should be provided via command line argument
const walletId = "b7b3cb5e-fbea-4785-86c7-f67c3562ddfc:0";

// Destination wallet to send ETH to
const DESTINATION_WALLET = "0x742d35Cc6634C0532925a3b844Bc454e4438f44e";

// Amount to send in ETH
const AMOUNT_TO_SEND = "0.0001"; // 0.001 ETH

// Function to load wallet from wallets.json
function loadWallet(walletId: string) {
  const walletsPath = path.resolve("./wallets.json");
  try {
    if (fs.existsSync(walletsPath)) {
      const wallets = JSON.parse(fs.readFileSync(walletsPath, "utf8"));
      if (wallets[walletId]) {
        return wallets[walletId];
      }
      throw new Error(`Wallet with ID ${walletId} not found in wallets.json`);
    } else {
      throw new Error("wallets.json file not found");
    }
  } catch (error) {
    console.error(`Failed to load wallet: ${error.message}`);
    process.exit(1);
  }
}

async function main() {
  console.log(`Using wallet ID: ${walletId}`);

  // First, establish NATS connection separately
  const nc = await connect({ servers: "nats://127.0.0.1:4222" }).catch(
    (err) => {
      console.error(`Failed to connect to NATS: ${err.message}`);
      process.exit(1);
    }
  );
  console.log(`Connected to NATS at ${nc.getServer()}`);

  // Create client with key path
  const mpcClient = await MpciumClient.create({
    nc: nc,
    keyPath: "./event_initiator.key",
    // password: "your-password-here", // Required for .age encrypted keys
  });

  try {
    // Connect to Ethereum testnet (Sepolia)
    const provider = new ethers.JsonRpcProvider(
      "https://eth-sepolia.public.blastapi.io"
    );
    console.log("Connected to Ethereum Sepolia testnet");

    // Get the wallet's public key/address
    const ethAddress = await getEthAddressForWallet(walletId);
    const fromAddress = ethAddress;

    console.log(`Sender account: ${fromAddress}`);
    console.log(`Destination account: ${DESTINATION_WALLET}`);
    console.log(`Amount to send: ${AMOUNT_TO_SEND} ETH`);

    // Get the current nonce for the sender address
    const nonce = await provider.getTransactionCount(fromAddress);

    // Get the current gas price
    const feeData = await provider.getFeeData();

    console.log("feeData:", feeData);

    // Create an Ethereum transaction
    const transaction = {
      to: DESTINATION_WALLET,
      value: ethers.parseEther(AMOUNT_TO_SEND),
      gasLimit: 21000, // Standard gas limit for ETH transfers
      maxFeePerGas: feeData.maxFeePerGas,
      maxPriorityFeePerGas: feeData.maxPriorityFeePerGas,
      nonce: nonce,
      type: 2, // EIP-1559 transaction
      chainId: 11155111, // Sepolia chain ID
    };

    // Calculate the transaction hash (this is what needs to be signed)
    const unsignedTx = ethers.Transaction.from(transaction);
    const txHash = unsignedTx.unsignedHash;
    const txHashHex = txHash.substring(2); // Remove '0x' prefix

    console.log(`Transaction hash: ${txHash}`);

    // Subscribe to signing results
    let signatureReceived = false;
    mpcClient.onSignResult((event: SigningResultEvent) => {
      console.log("Received signing result:", event);
      signatureReceived = true;

      if (event.result_type === SigningResultType.Success) {
        processSuccessfulSignature(event);
      } else {
        console.error(`Signing failed: ${event.error_message}`);
      }
    });

    // Process a successful signature
    function processSuccessfulSignature(event: SigningResultEvent) {
      try {
        // For ECDSA Ethereum signatures, we need the r, s, and v (recovery) values
        if (!event.r || !event.s || event.signature_recovery === null) {
          console.error("Missing signature components in result:", event);
          return;
        }

        // Convert from base64 to hex strings
        const r = "0x" + Buffer.from(event.r, "base64").toString("hex");
        const s = "0x" + Buffer.from(event.s, "base64").toString("hex");

        // Decode signature_recovery from base64 to a number
        const recoveryBuffer = Buffer.from(event.signature_recovery, "base64");
        const v = recoveryBuffer[0]; // Get the first byte as the recovery value

        console.log(`Signature components - r: ${r}, s: ${s}, v: ${v}`);

        // Create a signed transaction
        const signedTx = ethers.Transaction.from({
          ...transaction,
          signature: { r, s, v },
        });

        // Verify signature
        const recoveredAddress = signedTx.from;
        console.log(`Recovered signer: ${recoveredAddress}`);
        if (!recoveredAddress) {
          console.error("Signature verification failed!");
          return;
        }

        if (recoveredAddress.toLowerCase() !== fromAddress.toLowerCase()) {
          console.error(
            "Signature verification failed! Addresses don't match."
          );
          return;
        }

        console.log("Signature verification successful!");
        broadcastTransaction(signedTx.serialized);
      } catch (error) {
        console.error("Error processing signature:", error);
        if (error instanceof Error) {
          console.error(error.stack);
        }
      }
    }

    // Broadcast the transaction to the network
    function broadcastTransaction(signedTxHex: string) {
      provider
        .broadcastTransaction(signedTxHex)
        .then((tx) => {
          console.log(`Transaction sent! Transaction hash: ${tx.hash}`);
          console.log(
            `View transaction: https://sepolia.etherscan.io/tx/${tx.hash}`
          );
        })
        .catch((err) => {
          console.error("Error broadcasting transaction:", err);
        });
    }

    // Send the transaction hash for signing, not the serialized transaction
    const txId = await mpcClient.signTransaction({
      walletId: walletId,
      keyType: KeyType.Secp256k1,
      networkInternalCode: "ethereum:sepolia",
      tx: Buffer.from(txHashHex, "hex").toString("base64"), // Convert hex to base64
    });

    console.log(`Signing request sent with txID: ${txId}`);

    // Wait for the result
    await new Promise((resolve) => {
      const checkInterval = setInterval(() => {
        if (signatureReceived) {
          clearInterval(checkInterval);
          resolve(null);
        }
      }, 1000);
    });

    // Keep the process running to allow time for transaction confirmation
    await new Promise((resolve) => setTimeout(resolve, 5000));

    console.log("Cleaning up...");
    await mpcClient.cleanup();
    await nc.drain();
  } catch (error) {
    console.error("Error:", error);
    await mpcClient.cleanup();
    await nc.drain();
    process.exit(1);
  }
}

// Helper function to get a wallet's Ethereum address
async function getEthAddressForWallet(walletId: string): Promise<string> {
  // Load wallet from wallets.json
  const wallet = loadWallet(walletId);

  if (wallet && wallet.ecdsa_pub_key) {
    // Convert base64 public key to Ethereum address
    const pubKeyBuffer = Buffer.from(wallet.ecdsa_pub_key, "base64");
    // Convert the buffer to hex string with "0x" prefix
    const pubKeyHex = "0x" + pubKeyBuffer.toString("hex");
    // Ethereum addresses are derived from the keccak256 hash of the uncompressed public key
    const address = ethers.computeAddress(pubKeyHex);
    return address;
  }

  throw new Error(`Wallet with ID ${walletId} has no ECDSA public key`);
}

// Run the example
main().catch(console.error);
/mpcium-client-ts# ts-node ./examples/sign-eth.ts
Using wallet ID: b7b3cb5e-fbea-4785-86c7-f67c3562ddfc:0
Connected to NATS at 127.0.0.1:4222
Connected to Ethereum Sepolia testnet
Sender account: 0xef049bd27aCD860D7A157e492378bf54eE9671B9
Destination account: 0x742d35Cc6634C0532925a3b844Bc454e4438f44e
Amount to send: 0.0001 ETH
feeData: FeeData {
  gasPrice: 14533302n,
  maxFeePerGas: 28066604n,
  maxPriorityFeePerGas: 1000000n
}
Transaction hash: 0x9c7835d42ef9643c110d5ddf038fba76a580c93a650242a1d66f5d74333bf3c7
Subscribed to signing results (consume mode)
SignTransaction request sent via JetStream for txID: c8354d11-aba5-46e8-88c2-c846b1fdac51
Signing request sent with txID: c8354d11-aba5-46e8-88c2-c846b1fdac51
Received signing result: {
  result_type: 'success',
  error_code: '',
  error_reason: '',
  is_timeout: false,
  network_internal_code: 'ethereum:sepolia',
  wallet_id: 'b7b3cb5e-fbea-4785-86c7-f67c3562ddfc:0',
  tx_id: 'c8354d11-aba5-46e8-88c2-c846b1fdac51',
  r: 'pylQvdMr+Z7N+9ADAdruVN7MTqysS0LNvVERgEx4Ktw=',
  s: 'ZA4LMVBf3Di9E35C3vph3ZWUvH4GHNnRTBb7aKYHFuc=',
  signature_recovery: 'AA==',
  signature: null
}
Signature components - r: 0xa72950bdd32bf99ecdfbd00301daee54decc4eacac4b42cdbd5111804c782adc, s: 0x640e0b31505fdc38bd137e42defa61dd9594bc7e061cd9d14c16fb68a60716e7, v: 0
Recovered signer: 0xef049bd27aCD860D7A157e492378bf54eE9671B9
Signature verification successful!
Error broadcasting transaction: Error: insufficient funds for intrinsic transaction cost (transaction="0x02f87283aa36a780830f42408401ac432c82520894742d35cc6634c0532925a3b844bc454e4438f44e865af3107a400080c080a0a72950bdd32bf99ecdfbd00
301daee54decc4eacac4b42cdbd5111804c782adca0640e0b31505fdc38bd137e42defa61dd9594bc7e061cd9d14c16fb68a60716e7", info={ "error": { "code": -32003, "message": "insufficient funds for gas * price + value: have 0 want 100589398684000" } }, code=INSUFFICIENT_FUNDS, version=6.13.7)
    at makeError (/mnt/d/github/bcskill/code/MPC/mpcium-client-ts/node_modules/ethers/src.ts/utils/errors.ts:694:21)
    at JsonRpcProvider.getRpcError (/mnt/d/github/bcskill/code/MPC/mpcium-client-ts/node_modules/ethers/src.ts/providers/provider-jsonrpc.ts:1025:33)
    at /mnt/d/github/bcskill/code/MPC/mpcium-client-ts/node_modules/ethers/src.ts/providers/provider-jsonrpc.ts:563:45
    at processTicksAndRejections (node:internal/process/task_queues:95:5) {
  code: 'INSUFFICIENT_FUNDS',
  transaction: '0x02f87283aa36a780830f42408401ac432c82520894742d35cc6634c0532925a3b844bc454e4438f44e865af3107a400080c080a0a72950bdd32bf99ecdfbd00301daee54decc4eacac4b42cdbd5111804c782adca0640e0b31505fdc38bd137e42defa61dd9594bc7e061cd9d14c16fb68a60716e7',
  info: {
    error: {
      code: -32003,
      message: 'insufficient funds for gas * price + value: have 0 want 100589398684000'
    }
  },
  shortMessage: 'insufficient funds for intrinsic transaction cost'
}
Cleaning up...
Cleaned up all subscriptions

多钱包测试

Subscribed to wallet creation results (consume mode)
CreateWallet request sent via JetStream for wallet: bbcd5019-2e7d-46aa-a1e8-5bc14371bc57:0
2025-07-14T12:57:13.424Z CreateWallet sent #1, awaiting result... walletID: bbcd5019-2e7d-46aa-a1e8-5bc14371bc57:0
2025-07-14T12:57:17.181Z Received wallet creation result: {
  wallet_id: 'bbcd5019-2e7d-46aa-a1e8-5bc14371bc57:0',
  ecdsa_pub_key: '2+ZZj1XCFR/OGWk9g1EMYZFyXfDNaNHCf+egn1oTo6DvR6kQ6tOBHK0oZkCL6QNsjmfi14tQN71W5n7OOXLSBg==',
  eddsa_pub_key: 's8rOVjh6dMwmpXqRDRYSyOBenY/uaHBOUvumU2QFuBE=',
  result_type: 'success',
  error_reason: '',
  error_code: ''
}
Solana wallet address: D6qLkdfZLXjHhSLAJ38UHjkYmBHU4Ac8S1oQKUDJZ2Gx
Ethereum wallet address: 0xCfcec2Beed19a40A663494396fB73042f2049dB1
Wallet saved to wallets.json with ID: bbcd5019-2e7d-46aa-a1e8-5bc14371bc57:0

测试通过

异常测试

使用相同walletID

WRN Keygen session error error="Key already exists: 81418adb-fb32-4e17-9b85-8ae207aa9cb8:0" context="Failed to create ECDSA key generation session" errorCode=ERROR_KEY_ALREADY_EXISTS walletID=81418adb-fb32-4e17-9b85-8ae207aa9cb8:0

https://github.com/fystack/mpcium/issues/56

常见问题

创建地址无返回

问题排查

nats consumer info
> Select a Stream mpc
> Select a Consumer mpc_keygen_result

查看 Waiting Pulls: 3 of maximum 512 当前有几个客户端连接

移除所有监听列表

nats consumer rm mpc mpc_keygen_result

Ubuntu 20.04 中使用OpenVPN搭建VPN服务器

和其它的文章不同,以下是很简单的搭建方法
1、升级系统打上补丁可以使用以下命令行

sudo apt update
sudo apt upgrade -y

2、下载openvpn的安装脚本,使用脚本安装省去安装证书等过程。我们可以通过以下命令下载脚本并运行脚本

wget https://git.io/vpn -O openvpn-ubuntu-install.sh
chmod -v +x openvpn-ubuntu-install.sh
sudo ./openvpn-ubuntu-install.sh

在安装过程选择如下图:
主要选项是协议一般选择UDP,DNS可以任意选择,Client名称可以自己名字,运行完脚本最终会在 /root目录下生成一个 .ovpn文件,这个正是我们上网的凭证文件。同时vpn服务也会自动安装好了
3、我们可以通过以下命令来管理vpn服务

sudo systemctl status openvpn-server@server.service
sudo systemctl start openvpn-server@server.service
sudo systemctl stop openvpn-server@server.service
sudo systemctl restart openvpn-server@server.service

4、如果我们还要添加用户可以继续运行以下脚本

 sudo ./openvpn-ubuntu-install.sh

端口测试

nc -vuz 159.135.192.235 1194
// Connection to 159.135.192.235 1194 port [udp/openvpn] succeeded!

https://gist.github.com/ebta/301f286fa9056d67e61bfc7cedd4ad56
相关链接:
https://linuxconfig.org/basic-ubuntu-22-04-openvpn-client-server-connection-setup
https://kifarunix.com/install-and-setup-openvpn-server-on-ubuntu-22-04/
https://www.cyberciti.biz/faq/ubuntu-20-04-lts-set-up-openvpn-server-in-5-minutes/

Trustivon Validator:下一代区块链验证者管理平台

产品简介

官网:https://dashboard.trustivon.com/


Trustivon Validator 是一个功能强大的区块链验证者管理平台,专为 Trustivon 网络设计,提供直观、安全、高效的验证者节点管理和委托服务。该平台采用现代化的技术栈构建,支持多语言、响应式设计,适配各种设备,为用户提供最佳的使用体验。

核心功能

1. 验证者列表管理

  • 实时展示网络中的所有验证者节点
  • 显示验证者的关键信息:地址、佣金率、总质押量、个人质押量、状态
  • 支持按不同状态筛选验证者(活跃、挖矿)
  • 响应式设计,适配桌面和移动设备

2. 委托管理

  • 一键委托 TC 代币给验证者
  • 支持取消委托操作
  • 提供重新委托功能,方便用户在不同验证者之间转移质押
  • 实时显示可委托余额和可用奖励

3. 奖励领取

  • 支持领取委托奖励
  • 显示待处理的解除绑定记录
  • 提供解除绑定周期倒计时
  • 一键发起奖励领取

4. 验证者详情

  • 查看验证者的详细信息
  • 显示验证者的投票地址
  • 支持复制验证者地址
  • 提供验证者的状态变化历史

5. MetaMask 集成

  • 无缝连接 MetaMask 钱包
  • 支持钱包地址显示和管理
  • 安全的交易签名机制
  • 实时监听钱包状态变化

6. 多语言支持

  • 支持英文、繁体中文、日文三种语言
  • 动态语言切换
  • 本地化的用户界面

使用指南

1. 连接钱包

  1. 点击右上角的"连接钱包"按钮
  2. 在弹出的MetaMask窗口中确认连接
  3. 连接成功后,右上角会显示您的钱包地址

2. 委托操作

  1. 在验证者列表中选择一个验证者
  2. 点击"委托"按钮
  3. 输入委托金额
  4. 确认委托设置
  5. 在MetaMask中确认交易

3. 领取奖励

  1. 点击"领取奖励"按钮
  2. 查看可领取的奖励金额
  3. 确认领取操作
  4. 在MetaMask中确认交易

4. 管理委托

  1. 在验证者列表中找到您已委托的验证者
  2. 可以选择"取消委托"或"重新委托"
  3. 按照提示完成操作

优势和亮点

1. 安全性

  • 基于MetaMask SDK的安全连接
  • 所有交易都在用户钱包中签名
  • 不存储用户的私钥或助记词
  • 采用HTTPS协议传输数据

2. 用户体验

  • 直观的界面设计,易于使用
  • 响应式布局,适配各种设备
  • 实时数据更新,提供最新信息
  • 多语言支持,服务全球用户

3. 功能完整性

  • 涵盖验证者管理的全生命周期
  • 支持委托、取消委托、重新委托
  • 提供奖励领取功能
  • 详细的验证者信息展示

4. 技术先进性

  • 采用现代化的技术栈
  • 组件化架构,便于扩展和维护
  • 类型安全的代码设计
  • 优秀的性能表现

未来规划

  1. 支持更多网络 :计划支持主网和更多测试网络
  2. 增强数据分析 :提供更详细的验证者性能分析和统计
  3. 移动应用开发 :推出iOS和Android移动应用
  4. 增强安全性 :添加更多安全验证和提示
  5. 社区功能 :支持验证者和委托者之间的交流
  6. 更多语言支持 :添加更多语言选项

如何开始使用

  1. 访问 Trustivon Validator 平台
  2. 连接您的MetaMask钱包
  3. 浏览验证者列表,选择合适的验证者
  4. 开始您的委托之旅

系统要求

  • 现代浏览器(Chrome、Firefox、Safari、Edge)
  • 已安装MetaMask扩展
  • 稳定的网络连接
  • 少量TC代币用于支付交易手续费

结语

Trustivon Validator 平台为用户提供了一个安全、高效、易用的验证者管理工具,帮助用户更好地参与Trustivon网络的治理和质押。无论您是经验丰富的区块链用户还是初学者,都能轻松上手使用该平台。

加入 Trustivon Validator 生态,共同构建更安全、更高效的区块链网络!