Compare commits
3 Commits
c215145123
...
a697770187
| Author | SHA1 | Date | |
|---|---|---|---|
| a697770187 | |||
| c8b2218841 | |||
| b01e1ba039 |
@ -22,6 +22,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||||||
let msg = shared?.string(forKey: "test_msg")
|
let msg = shared?.string(forKey: "test_msg")
|
||||||
SDLLogger.shared.log("NE read message: \(msg ?? "failed")")
|
SDLLogger.shared.log("NE read message: \(msg ?? "failed")")
|
||||||
|
|
||||||
|
DarwinNotificationCenter.shared.post(.vpnStatusChanged)
|
||||||
|
|
||||||
// host: "192.168.0.101", port: 1265
|
// host: "192.168.0.101", port: 1265
|
||||||
guard let options, let config = SDLConfiguration.parse(options: options) else {
|
guard let options, let config = SDLConfiguration.parse(options: options) else {
|
||||||
completionHandler(TunnelError.invalidConfiguration)
|
completionHandler(TunnelError.invalidConfiguration)
|
||||||
@ -58,8 +60,29 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||||||
|
|
||||||
override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) {
|
override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) {
|
||||||
// Add code here to handle the message.
|
// Add code here to handle the message.
|
||||||
if let handler = completionHandler {
|
|
||||||
handler(messageData)
|
Task {
|
||||||
|
if let message = try? NEMessage(serializedBytes: messageData) {
|
||||||
|
switch message.message {
|
||||||
|
case .exitNodeIpChanged(let exitNodeIpChanged):
|
||||||
|
let exitNodeIp = exitNodeIpChanged.ip
|
||||||
|
do {
|
||||||
|
try await self.contextActor?.updateExitNode(exitNodeIp: exitNodeIp)
|
||||||
|
var reply = NEReply()
|
||||||
|
reply.code = 0
|
||||||
|
reply.message = "操作成功"
|
||||||
|
completionHandler?(try reply.serializedData())
|
||||||
|
|
||||||
|
} catch let err {
|
||||||
|
var reply = NEReply()
|
||||||
|
reply.code = 1
|
||||||
|
reply.message = err.localizedDescription
|
||||||
|
completionHandler?(try reply.serializedData())
|
||||||
|
}
|
||||||
|
case .none:
|
||||||
|
()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -21,7 +21,7 @@ actor SDLContextActor {
|
|||||||
|
|
||||||
private var state: State = .unregistered
|
private var state: State = .unregistered
|
||||||
|
|
||||||
nonisolated let config: SDLConfiguration
|
var config: SDLConfiguration
|
||||||
// nat的网络类型
|
// nat的网络类型
|
||||||
var natType: SDLNATProberActor.NatType = .blocked
|
var natType: SDLNATProberActor.NatType = .blocked
|
||||||
|
|
||||||
@ -134,6 +134,16 @@ actor SDLContextActor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 取消出口节点的时候,ip地址为: 0.0.0.0
|
||||||
|
public func updateExitNode(exitNodeIp: String) async throws {
|
||||||
|
if let ip = SDLUtil.ipv4StrToInt32(exitNodeIp), ip > 0 {
|
||||||
|
self.config.exitNode = .init(exitNodeIp: ip)
|
||||||
|
} else {
|
||||||
|
self.config.exitNode = nil
|
||||||
|
}
|
||||||
|
try await self.setNetworkSettings(config: config, dnsServer: SDLDNSClient.Helper.dnsServer)
|
||||||
|
}
|
||||||
|
|
||||||
private func startQUICClient() async throws -> SDLQUICClient {
|
private func startQUICClient() async throws -> SDLQUICClient {
|
||||||
self.quicWorker?.cancel()
|
self.quicWorker?.cancel()
|
||||||
self.quicClient?.stop()
|
self.quicClient?.stop()
|
||||||
@ -220,9 +230,8 @@ actor SDLContextActor {
|
|||||||
self.dnsWorker = nil
|
self.dnsWorker = nil
|
||||||
|
|
||||||
// 启动dns服务
|
// 启动dns服务
|
||||||
let dnsSocketAddress = try SocketAddress.makeAddressResolvingHost(self.config.serverHost, port: 15353)
|
let dnsClient = SDLDNSClient(host: self.config.serverHost, port: 15353, logger: SDLLogger.shared)
|
||||||
let dnsClient = try await SDLDNSClient(dnsServerAddress: dnsSocketAddress, logger: SDLLogger.shared)
|
dnsClient.start()
|
||||||
try dnsClient.start()
|
|
||||||
SDLLogger.shared.log("[SDLContext] dnsClient started")
|
SDLLogger.shared.log("[SDLContext] dnsClient started")
|
||||||
self.dnsClient = dnsClient
|
self.dnsClient = dnsClient
|
||||||
self.dnsWorker = Task.detached {
|
self.dnsWorker = Task.detached {
|
||||||
@ -415,7 +424,7 @@ actor SDLContextActor {
|
|||||||
SDLLogger.shared.log("[SDLContext] get registerSuperAck, aes_key len: \(key.count)", level: .info)
|
SDLLogger.shared.log("[SDLContext] get registerSuperAck, aes_key len: \(key.count)", level: .info)
|
||||||
// 服务器分配的tun网卡信息
|
// 服务器分配的tun网卡信息
|
||||||
do {
|
do {
|
||||||
try await self.setNetworkSettings(networkAddress: self.config.networkAddress, dnsServer: SDLDNSClient.Helper.dnsServer)
|
try await self.setNetworkSettings(config: self.config, dnsServer: SDLDNSClient.Helper.dnsServer)
|
||||||
SDLLogger.shared.log("[SDLContext] setNetworkSettings successed")
|
SDLLogger.shared.log("[SDLContext] setNetworkSettings successed")
|
||||||
self.state = .registered
|
self.state = .registered
|
||||||
self.startReader()
|
self.startReader()
|
||||||
@ -657,7 +666,7 @@ actor SDLContextActor {
|
|||||||
let networkAddr = self.config.networkAddress
|
let networkAddr = self.config.networkAddress
|
||||||
|
|
||||||
if SDLDNSClient.Helper.isDnsRequestPacket(ipPacket: packet) {
|
if SDLDNSClient.Helper.isDnsRequestPacket(ipPacket: packet) {
|
||||||
self.dnsClient?.forward(ipPacket: packet)
|
self.dnsClient?.forward(ipPacketData: packet.data)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -695,8 +704,8 @@ actor SDLContextActor {
|
|||||||
}
|
}
|
||||||
// 不在同一个网段的数据,看到是否配置了网络出口
|
// 不在同一个网段的数据,看到是否配置了网络出口
|
||||||
// 如果配置了,转发数据个网络出口,否则丢弃
|
// 如果配置了,转发数据个网络出口,否则丢弃
|
||||||
else {
|
else if let exitNode = config.exitNode {
|
||||||
let exitNodeIp: UInt32 = 1234
|
let exitNodeIp: UInt32 = exitNode.exitNodeIp
|
||||||
SDLLogger.shared.log("[SDLContext] global dstIp: \(packet.header.destination.asIpAddress())", level: .debug)
|
SDLLogger.shared.log("[SDLContext] global dstIp: \(packet.header.destination.asIpAddress())", level: .debug)
|
||||||
// 查找arp缓存中是否有目标mac地址
|
// 查找arp缓存中是否有目标mac地址
|
||||||
if let dstMac = await self.arpServer.query(ip: exitNodeIp) {
|
if let dstMac = await self.arpServer.query(ip: exitNodeIp) {
|
||||||
@ -752,16 +761,21 @@ actor SDLContextActor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 网络改变时需要重新配置网络信息
|
// MARK: 网络改变时需要重新配置网络信息
|
||||||
private func setNetworkSettings(networkAddress: SDLConfiguration.NetworkAddress, dnsServer: String) async throws {
|
private func setNetworkSettings(config: SDLConfiguration, dnsServer: String) async throws {
|
||||||
|
let networkAddress = config.networkAddress
|
||||||
|
|
||||||
// 配置路由规则
|
// 配置路由规则
|
||||||
let routes: [NEIPv4Route] = [
|
var routes: [NEIPv4Route] = [
|
||||||
NEIPv4Route(destinationAddress: networkAddress.netAddress, subnetMask: networkAddress.maskAddress),
|
NEIPv4Route(destinationAddress: networkAddress.netAddress, subnetMask: networkAddress.maskAddress),
|
||||||
NEIPv4Route(destinationAddress: dnsServer, subnetMask: "255.255.255.255"),
|
NEIPv4Route(destinationAddress: dnsServer, subnetMask: "255.255.255.255"),
|
||||||
|
|
||||||
NEIPv4Route(destinationAddress: "172.16.1.0", subnetMask: "255.255.255.0"),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// 如果存在出口节点配置,则接管系统默认留有
|
||||||
|
if config.exitNode != nil {
|
||||||
|
routes.append(.default())
|
||||||
|
}
|
||||||
|
|
||||||
// Add code here to start the process of connecting the tunnel.
|
// Add code here to start the process of connecting the tunnel.
|
||||||
let networkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "8.8.8.8")
|
let networkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "8.8.8.8")
|
||||||
networkSettings.mtu = 1250
|
networkSettings.mtu = 1250
|
||||||
@ -769,15 +783,22 @@ actor SDLContextActor {
|
|||||||
// 设置网卡的DNS解析
|
// 设置网卡的DNS解析
|
||||||
let networkDomain = networkAddress.networkDomain
|
let networkDomain = networkAddress.networkDomain
|
||||||
let dnsSettings = NEDNSSettings(servers: [dnsServer])
|
let dnsSettings = NEDNSSettings(servers: [dnsServer])
|
||||||
|
|
||||||
dnsSettings.searchDomains = [networkDomain]
|
dnsSettings.searchDomains = [networkDomain]
|
||||||
dnsSettings.matchDomains = [networkDomain]
|
dnsSettings.matchDomains = [networkDomain]
|
||||||
|
// 必须设置为 false,否则它会尝试接管全局解析
|
||||||
dnsSettings.matchDomainsNoSearch = false
|
dnsSettings.matchDomainsNoSearch = false
|
||||||
|
|
||||||
networkSettings.dnsSettings = dnsSettings
|
networkSettings.dnsSettings = dnsSettings
|
||||||
|
|
||||||
let ipv4Settings = NEIPv4Settings(addresses: [networkAddress.ipAddress], subnetMasks: [networkAddress.maskAddress])
|
let ipv4Settings = NEIPv4Settings(addresses: [networkAddress.ipAddress], subnetMasks: [networkAddress.maskAddress])
|
||||||
// 设置路由表
|
// 设置路由表
|
||||||
//NEIPv4Route.default()
|
|
||||||
ipv4Settings.includedRoutes = routes
|
ipv4Settings.includedRoutes = routes
|
||||||
|
// TODO 要排除的路由表
|
||||||
|
ipv4Settings.excludedRoutes = [
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
networkSettings.ipv4Settings = ipv4Settings
|
networkSettings.ipv4Settings = ipv4Settings
|
||||||
// 网卡配置设置必须成功
|
// 网卡配置设置必须成功
|
||||||
try await self.provider.setTunnelNetworkSettings(networkSettings)
|
try await self.provider.setTunnelNetworkSettings(networkSettings)
|
||||||
|
|||||||
@ -239,6 +239,7 @@ final class SDLQUICClient {
|
|||||||
case .event:
|
case .event:
|
||||||
guard let bytes = buffer.readBytes(length: buffer.readableBytes),
|
guard let bytes = buffer.readBytes(length: buffer.readableBytes),
|
||||||
let event = try? SDLEvent(serializedBytes: bytes) else {
|
let event = try? SDLEvent(serializedBytes: bytes) else {
|
||||||
|
SDLLogger.shared.log("SDLQUICClient decode Event Error")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return .event(event)
|
return .event(event)
|
||||||
|
|||||||
@ -9,6 +9,8 @@ import NIOCore
|
|||||||
|
|
||||||
// 配置项目
|
// 配置项目
|
||||||
public class SDLConfiguration {
|
public class SDLConfiguration {
|
||||||
|
|
||||||
|
// 网络地址信息
|
||||||
public struct NetworkAddress {
|
public struct NetworkAddress {
|
||||||
public let networkId: UInt32
|
public let networkId: UInt32
|
||||||
public let ip: UInt32
|
public let ip: UInt32
|
||||||
@ -38,6 +40,11 @@ public class SDLConfiguration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 网络出口
|
||||||
|
public struct ExitNode {
|
||||||
|
let exitNodeIp: UInt32
|
||||||
|
}
|
||||||
|
|
||||||
// 当前的客户端版本
|
// 当前的客户端版本
|
||||||
let version: Int
|
let version: Int
|
||||||
|
|
||||||
@ -67,6 +74,8 @@ public class SDLConfiguration {
|
|||||||
let accessToken: String
|
let accessToken: String
|
||||||
let identityId: UInt32
|
let identityId: UInt32
|
||||||
|
|
||||||
|
var exitNode: ExitNode?
|
||||||
|
|
||||||
public init(version: Int,
|
public init(version: Int,
|
||||||
serverHost: String,
|
serverHost: String,
|
||||||
stunServers: [String],
|
stunServers: [String],
|
||||||
@ -75,7 +84,8 @@ public class SDLConfiguration {
|
|||||||
hostname: String,
|
hostname: String,
|
||||||
noticePort: Int,
|
noticePort: Int,
|
||||||
accessToken: String,
|
accessToken: String,
|
||||||
identityId: UInt32) {
|
identityId: UInt32,
|
||||||
|
exitNode: ExitNode?) {
|
||||||
|
|
||||||
self.version = version
|
self.version = version
|
||||||
self.serverHost = serverHost
|
self.serverHost = serverHost
|
||||||
@ -86,7 +96,9 @@ public class SDLConfiguration {
|
|||||||
self.accessToken = accessToken
|
self.accessToken = accessToken
|
||||||
self.identityId = identityId
|
self.identityId = identityId
|
||||||
self.hostname = hostname
|
self.hostname = hostname
|
||||||
|
self.exitNode = exitNode
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析配置文件
|
// 解析配置文件
|
||||||
@ -109,6 +121,12 @@ extension SDLConfiguration {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 网络出口配置是可选的
|
||||||
|
var exitNode: ExitNode? = nil
|
||||||
|
if let exitNodeIpStr = options["exit_node_ip"] as? String, let exitNodeIp = SDLUtil.ipv4StrToInt32(exitNodeIpStr) {
|
||||||
|
exitNode = .init(exitNodeIp: exitNodeIp)
|
||||||
|
}
|
||||||
|
|
||||||
return SDLConfiguration(version: version,
|
return SDLConfiguration(version: version,
|
||||||
serverHost: serverHost,
|
serverHost: serverHost,
|
||||||
stunServers: [serverHost, stunAssistHost],
|
stunServers: [serverHost, stunAssistHost],
|
||||||
@ -117,7 +135,8 @@ extension SDLConfiguration {
|
|||||||
hostname: hostname,
|
hostname: hostname,
|
||||||
noticePort: noticePort,
|
noticePort: noticePort,
|
||||||
accessToken: accessToken,
|
accessToken: accessToken,
|
||||||
identityId: identityId)
|
identityId: identityId,
|
||||||
|
exitNode: exitNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func parseNetworkAddress(_ config: [String: NSObject]) -> SDLConfiguration.NetworkAddress? {
|
private static func parseNetworkAddress(_ config: [String: NSObject]) -> SDLConfiguration.NetworkAddress? {
|
||||||
|
|||||||
@ -1,84 +1,106 @@
|
|||||||
//
|
//
|
||||||
// DNSClient.swift
|
// SDLDNSClient 2.swift
|
||||||
// Tun
|
// punchnet
|
||||||
//
|
//
|
||||||
// Created by 安礼成 on 2025/12/10.
|
// Created by 安礼成 on 2026/4/9.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import NIOCore
|
import Network
|
||||||
import NIOPosix
|
|
||||||
|
|
||||||
// 处理和sn-server服务器之间的通讯
|
final class SDLDNSClient {
|
||||||
final class SDLDNSClient: ChannelInboundHandler {
|
private var connection: NWConnection?
|
||||||
typealias InboundIn = AddressedEnvelope<ByteBuffer>
|
|
||||||
|
|
||||||
private let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
|
||||||
|
|
||||||
private var channel: Channel?
|
|
||||||
private let logger: SDLLogger
|
private let logger: SDLLogger
|
||||||
private let dnsServerAddress: SocketAddress
|
private let dnsServerAddress: NWEndpoint
|
||||||
|
|
||||||
|
// 用于对外输出收到的 DNS 响应包
|
||||||
public let packetFlow: AsyncStream<Data>
|
public let packetFlow: AsyncStream<Data>
|
||||||
private let packetContinuation: AsyncStream<Data>.Continuation
|
private let packetContinuation: AsyncStream<Data>.Continuation
|
||||||
|
|
||||||
// 启动函数
|
// 用来处理关闭事件
|
||||||
init(dnsServerAddress: SocketAddress, logger: SDLLogger) async throws {
|
private let (closeStream, closeContinuation) = AsyncStream.makeStream(of: Void.self)
|
||||||
self.dnsServerAddress = dnsServerAddress
|
|
||||||
|
/// - Parameter host: 你的 sn-server 地址 (如 "8.8.8.8")
|
||||||
|
/// - Parameter port: 端口 (如 53)
|
||||||
|
init(host: String, port: UInt16, logger: SDLLogger) {
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
(self.packetFlow, self.packetContinuation) = AsyncStream.makeStream(of: Data.self, bufferingPolicy: .unbounded)
|
self.dnsServerAddress = .hostPort(host: NWEndpoint.Host(host), port: NWEndpoint.Port(integerLiteral: port))
|
||||||
|
|
||||||
|
let (stream, continuation) = AsyncStream.makeStream(of: Data.self, bufferingPolicy: .unbounded)
|
||||||
|
self.packetFlow = stream
|
||||||
|
self.packetContinuation = continuation
|
||||||
}
|
}
|
||||||
|
|
||||||
func start() throws {
|
func start() {
|
||||||
let bootstrap = DatagramBootstrap(group: group)
|
// 1. 配置参数:这是解决环路的关键
|
||||||
.channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
|
let parameters = NWParameters.udp
|
||||||
.channelInitializer { channel in
|
|
||||||
channel.pipeline.addHandler(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
let channel = try bootstrap.bind(host: "0.0.0.0", port: 0).wait()
|
// 禁止此连接走 TUN 网卡(在 NE 中 TUN 通常被归类为 .other)
|
||||||
self.logger.log("[DNSClient] started", level: .debug)
|
parameters.prohibitedInterfaceTypes = [.other]
|
||||||
self.channel = channel
|
// 2. 增强健壮性:启用多路径切换(替代 pathSelectionOptions 的意图)
|
||||||
}
|
parameters.multipathServiceType = .handover
|
||||||
|
|
||||||
func waitClose() async throws {
|
// 2. 创建连接
|
||||||
try await self.channel?.closeFuture.get()
|
let connection = NWConnection(to: self.dnsServerAddress, using: parameters)
|
||||||
}
|
self.connection = connection
|
||||||
|
|
||||||
// --MARK: ChannelInboundHandler delegate
|
connection.stateUpdateHandler = { [weak self] state in
|
||||||
|
switch state {
|
||||||
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
|
case .ready:
|
||||||
let envelope = unwrapInboundIn(data)
|
self?.logger.log("[DNSClient] Connection ready", level: .debug)
|
||||||
|
self?.receiveLoop() // 开始循环接收数据
|
||||||
var buffer = envelope.data
|
case .failed(let error):
|
||||||
let remoteAddress = envelope.remoteAddress
|
self?.logger.log("[DNSClient] Connection failed: \(error)", level: .error)
|
||||||
self.logger.log("[DNSClient] read data: \(buffer), from: \(remoteAddress)", level: .debug)
|
self?.stop()
|
||||||
|
case .cancelled:
|
||||||
let len = buffer.readableBytes
|
self?.packetContinuation.finish()
|
||||||
if let bytes = buffer.readBytes(length: len) {
|
self?.closeContinuation.finish()
|
||||||
self.packetContinuation.yield(Data(bytes))
|
default:
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func channelInactive(context: ChannelHandlerContext) {
|
// 启动连接队列
|
||||||
self.packetContinuation.finish()
|
connection.start(queue: .global())
|
||||||
}
|
}
|
||||||
|
|
||||||
func forward(ipPacket: IPPacket) {
|
public func waitClose() async {
|
||||||
guard let channel = self.channel else {
|
for await _ in closeStream { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 接收数据的递归循环
|
||||||
|
private func receiveLoop() {
|
||||||
|
connection?.receiveMessage { [weak self] content, _, isComplete, error in
|
||||||
|
if let data = content, !data.isEmpty {
|
||||||
|
// 将收到的 DNS 响应写回 AsyncStream
|
||||||
|
self?.packetContinuation.yield(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if error == nil && self?.connection?.state == .ready {
|
||||||
|
self?.receiveLoop() // 继续监听下一个包
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 发送 DNS 查询包(由 TUN 拦截到的原始 IP 包数据)
|
||||||
|
func forward(ipPacketData: Data) {
|
||||||
|
guard let connection = self.connection, connection.state == .ready else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let buffer = channel.allocator.buffer(bytes: ipPacket.data)
|
connection.send(content: ipPacketData, completion: .contentProcessed { [weak self] error in
|
||||||
let envelope = AddressedEnvelope<ByteBuffer>(remoteAddress: self.dnsServerAddress, data: buffer)
|
if let error = error {
|
||||||
channel.pipeline.eventLoop.execute {
|
self?.logger.log("[DNSClient] Send error: \(error)", level: .error)
|
||||||
channel.writeAndFlush(envelope, promise: nil)
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func stop() {
|
||||||
|
connection?.cancel()
|
||||||
|
connection = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
try? self.group.syncShutdownGracefully()
|
stop()
|
||||||
self.packetContinuation.finish()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
193
Tun/Punchnet/TunMessage.pb.swift
Normal file
193
Tun/Punchnet/TunMessage.pb.swift
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
// DO NOT EDIT.
|
||||||
|
// swift-format-ignore-file
|
||||||
|
// swiftlint:disable all
|
||||||
|
//
|
||||||
|
// Generated by the Swift generator plugin for the protocol buffer compiler.
|
||||||
|
// Source: tun_pb.proto
|
||||||
|
//
|
||||||
|
// For information on using the generated types, please see the documentation:
|
||||||
|
// https://github.com/apple/swift-protobuf/
|
||||||
|
|
||||||
|
import SwiftProtobuf
|
||||||
|
|
||||||
|
// If the compiler emits an error on this type, it is because this file
|
||||||
|
// was generated by a version of the `protoc` Swift plug-in that is
|
||||||
|
// incompatible with the version of SwiftProtobuf to which you are linking.
|
||||||
|
// Please ensure that you are building against the same version of the API
|
||||||
|
// that was used to generate this file.
|
||||||
|
fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck {
|
||||||
|
struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {}
|
||||||
|
typealias Version = _2
|
||||||
|
}
|
||||||
|
|
||||||
|
struct NEMessage: Sendable {
|
||||||
|
// SwiftProtobuf.Message conformance is added in an extension below. See the
|
||||||
|
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
|
||||||
|
// methods supported on all messages.
|
||||||
|
|
||||||
|
var message: NEMessage.OneOf_Message? = nil
|
||||||
|
|
||||||
|
var exitNodeIpChanged: NEMessage.ExitNodeIpChanged {
|
||||||
|
get {
|
||||||
|
if case .exitNodeIpChanged(let v)? = message {return v}
|
||||||
|
return NEMessage.ExitNodeIpChanged()
|
||||||
|
}
|
||||||
|
set {message = .exitNodeIpChanged(newValue)}
|
||||||
|
}
|
||||||
|
|
||||||
|
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||||
|
|
||||||
|
enum OneOf_Message: Equatable, Sendable {
|
||||||
|
case exitNodeIpChanged(NEMessage.ExitNodeIpChanged)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 网络出口ip改变映射变化, 空字符串表示关闭
|
||||||
|
struct ExitNodeIpChanged: Sendable {
|
||||||
|
// SwiftProtobuf.Message conformance is added in an extension below. See the
|
||||||
|
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
|
||||||
|
// methods supported on all messages.
|
||||||
|
|
||||||
|
var ip: String = String()
|
||||||
|
|
||||||
|
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||||
|
|
||||||
|
init() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct NEReply: Sendable {
|
||||||
|
// SwiftProtobuf.Message conformance is added in an extension below. See the
|
||||||
|
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
|
||||||
|
// methods supported on all messages.
|
||||||
|
|
||||||
|
var code: Int32 = 0
|
||||||
|
|
||||||
|
var message: String = String()
|
||||||
|
|
||||||
|
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||||
|
|
||||||
|
init() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Code below here is support for the SwiftProtobuf runtime.
|
||||||
|
|
||||||
|
extension NEMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||||
|
static let protoMessageName: String = "NEMessage"
|
||||||
|
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
|
||||||
|
1: .standard(proto: "exit_node_ip_changed"),
|
||||||
|
]
|
||||||
|
|
||||||
|
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||||
|
while let fieldNumber = try decoder.nextFieldNumber() {
|
||||||
|
// The use of inline closures is to circumvent an issue where the compiler
|
||||||
|
// allocates stack space for every case branch when no optimizations are
|
||||||
|
// enabled. https://github.com/apple/swift-protobuf/issues/1034
|
||||||
|
switch fieldNumber {
|
||||||
|
case 1: try {
|
||||||
|
var v: NEMessage.ExitNodeIpChanged?
|
||||||
|
var hadOneofValue = false
|
||||||
|
if let current = self.message {
|
||||||
|
hadOneofValue = true
|
||||||
|
if case .exitNodeIpChanged(let m) = current {v = m}
|
||||||
|
}
|
||||||
|
try decoder.decodeSingularMessageField(value: &v)
|
||||||
|
if let v = v {
|
||||||
|
if hadOneofValue {try decoder.handleConflictingOneOf()}
|
||||||
|
self.message = .exitNodeIpChanged(v)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
default: break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||||
|
// The use of inline closures is to circumvent an issue where the compiler
|
||||||
|
// allocates stack space for every if/case branch local when no optimizations
|
||||||
|
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
||||||
|
// https://github.com/apple/swift-protobuf/issues/1182
|
||||||
|
try { if case .exitNodeIpChanged(let v)? = self.message {
|
||||||
|
try visitor.visitSingularMessageField(value: v, fieldNumber: 1)
|
||||||
|
} }()
|
||||||
|
try unknownFields.traverse(visitor: &visitor)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: NEMessage, rhs: NEMessage) -> Bool {
|
||||||
|
if lhs.message != rhs.message {return false}
|
||||||
|
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NEMessage.ExitNodeIpChanged: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||||
|
static let protoMessageName: String = NEMessage.protoMessageName + ".ExitNodeIpChanged"
|
||||||
|
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
|
||||||
|
1: .same(proto: "ip"),
|
||||||
|
]
|
||||||
|
|
||||||
|
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||||
|
while let fieldNumber = try decoder.nextFieldNumber() {
|
||||||
|
// The use of inline closures is to circumvent an issue where the compiler
|
||||||
|
// allocates stack space for every case branch when no optimizations are
|
||||||
|
// enabled. https://github.com/apple/swift-protobuf/issues/1034
|
||||||
|
switch fieldNumber {
|
||||||
|
case 1: try { try decoder.decodeSingularStringField(value: &self.ip) }()
|
||||||
|
default: break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||||
|
if !self.ip.isEmpty {
|
||||||
|
try visitor.visitSingularStringField(value: self.ip, fieldNumber: 1)
|
||||||
|
}
|
||||||
|
try unknownFields.traverse(visitor: &visitor)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: NEMessage.ExitNodeIpChanged, rhs: NEMessage.ExitNodeIpChanged) -> Bool {
|
||||||
|
if lhs.ip != rhs.ip {return false}
|
||||||
|
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NEReply: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||||
|
static let protoMessageName: String = "NEReply"
|
||||||
|
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
|
||||||
|
1: .same(proto: "code"),
|
||||||
|
2: .same(proto: "message"),
|
||||||
|
]
|
||||||
|
|
||||||
|
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||||
|
while let fieldNumber = try decoder.nextFieldNumber() {
|
||||||
|
// The use of inline closures is to circumvent an issue where the compiler
|
||||||
|
// allocates stack space for every case branch when no optimizations are
|
||||||
|
// enabled. https://github.com/apple/swift-protobuf/issues/1034
|
||||||
|
switch fieldNumber {
|
||||||
|
case 1: try { try decoder.decodeSingularInt32Field(value: &self.code) }()
|
||||||
|
case 2: try { try decoder.decodeSingularStringField(value: &self.message) }()
|
||||||
|
default: break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||||
|
if self.code != 0 {
|
||||||
|
try visitor.visitSingularInt32Field(value: self.code, fieldNumber: 1)
|
||||||
|
}
|
||||||
|
if !self.message.isEmpty {
|
||||||
|
try visitor.visitSingularStringField(value: self.message, fieldNumber: 2)
|
||||||
|
}
|
||||||
|
try unknownFields.traverse(visitor: &visitor)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: NEReply, rhs: NEReply) -> Bool {
|
||||||
|
if lhs.code != rhs.code {return false}
|
||||||
|
if lhs.message != rhs.message {return false}
|
||||||
|
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -29,7 +29,15 @@ struct SystemConfig {
|
|||||||
return "macOS \(version.majorVersion).\(version.minorVersion)"
|
return "macOS \(version.majorVersion).\(version.minorVersion)"
|
||||||
}()
|
}()
|
||||||
|
|
||||||
static func getOptions(networkId: UInt32, networkDomain: String, ip: String, maskLen: UInt8, accessToken: String, identityId: UInt32, hostname: String, noticePort: Int) -> [String: NSObject] {
|
static func getOptions(networkId: UInt32,
|
||||||
|
networkDomain: String,
|
||||||
|
ip: String,
|
||||||
|
maskLen: UInt8,
|
||||||
|
accessToken: String,
|
||||||
|
identityId: UInt32,
|
||||||
|
hostname: String,
|
||||||
|
noticePort: Int,
|
||||||
|
exitNodeIp: String?) -> [String: NSObject] {
|
||||||
// guard let serverIp = DNSResolver.resolveAddrInfos(serverHost).first,
|
// guard let serverIp = DNSResolver.resolveAddrInfos(serverHost).first,
|
||||||
// let stunAssistIp = DNSResolver.resolveAddrInfos(stunAssistHost).first else {
|
// let stunAssistIp = DNSResolver.resolveAddrInfos(stunAssistHost).first else {
|
||||||
// return nil
|
// return nil
|
||||||
@ -38,7 +46,7 @@ struct SystemConfig {
|
|||||||
let clientId = getClientId()
|
let clientId = getClientId()
|
||||||
let mac = getMacAddress()
|
let mac = getMacAddress()
|
||||||
|
|
||||||
return [
|
var options = [
|
||||||
"version": version as NSObject,
|
"version": version as NSObject,
|
||||||
"client_id": clientId as NSObject,
|
"client_id": clientId as NSObject,
|
||||||
"access_token": accessToken as NSObject,
|
"access_token": accessToken as NSObject,
|
||||||
@ -55,6 +63,12 @@ struct SystemConfig {
|
|||||||
"network_domain": networkDomain as NSObject
|
"network_domain": networkDomain as NSObject
|
||||||
] as NSObject
|
] as NSObject
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if let exitNodeIp {
|
||||||
|
options["exit_node_ip"] = exitNodeIp as NSObject
|
||||||
|
}
|
||||||
|
|
||||||
|
return options
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func getClientId() -> String {
|
public static func getClientId() -> String {
|
||||||
|
|||||||
@ -87,10 +87,6 @@ extension SDLAPIClient {
|
|||||||
case nodeList = "node_list"
|
case nodeList = "node_list"
|
||||||
}
|
}
|
||||||
|
|
||||||
static func `default`() -> Self {
|
|
||||||
return .init(ip: "0.0.0.0", maskLen: 24, hostname: "", identityId: 0, resourceList: [], nodeList: [])
|
|
||||||
}
|
|
||||||
|
|
||||||
func getNode(id: Int?) -> Node? {
|
func getNode(id: Int?) -> Node? {
|
||||||
return nodeList.first(where: { $0.id == id })
|
return nodeList.first(where: { $0.id == id })
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,7 +16,7 @@ extension SDLAPIClient {
|
|||||||
let nodeName: String
|
let nodeName: String
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case nnid
|
case nnid = "node_id"
|
||||||
case nodeName = "node_name"
|
case nodeName = "node_name"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,9 +19,9 @@ class AppContext {
|
|||||||
var noticePort: Int
|
var noticePort: Int
|
||||||
|
|
||||||
// 调用 "/connect" 之后的网络信息
|
// 调用 "/connect" 之后的网络信息
|
||||||
var networkContext: SDLAPIClient.NetworkContext = .default()
|
var networkContext: SDLAPIClient.NetworkContext? = nil
|
||||||
|
|
||||||
// 保存当前登陆vpn使用的配置项
|
// 在menu里面需要使用
|
||||||
var vpnOptions: [String: NSObject]? = nil
|
var vpnOptions: [String: NSObject]? = nil
|
||||||
|
|
||||||
// 当前app所处的场景
|
// 当前app所处的场景
|
||||||
@ -95,7 +95,32 @@ class AppContext {
|
|||||||
throw AppContextError(message: "网络已经连接")
|
throw AppContextError(message: "网络已经连接")
|
||||||
}
|
}
|
||||||
|
|
||||||
let context = try await SDLAPIClient.connectNetwork(accesToken: session.accessToken)
|
self.networkContext = try await SDLAPIClient.connectNetwork(accesToken: session.accessToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
func changeExitNodeIp(exitNodeIp: String) async throws -> Data {
|
||||||
|
// 避免重复连接
|
||||||
|
guard vpnManager.isConnected else {
|
||||||
|
throw AppContextError(message: "网络未连接")
|
||||||
|
}
|
||||||
|
|
||||||
|
var exitNodeIpChanged = NEMessage.ExitNodeIpChanged()
|
||||||
|
exitNodeIpChanged.ip = exitNodeIp
|
||||||
|
|
||||||
|
var neMessage = NEMessage()
|
||||||
|
neMessage.message = .exitNodeIpChanged(exitNodeIpChanged)
|
||||||
|
|
||||||
|
let message = try neMessage.serializedData()
|
||||||
|
|
||||||
|
return try await self.vpnManager.sendMessage(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动tun
|
||||||
|
func startTun() async throws {
|
||||||
|
guard let session = self.networkSession, let context = self.networkContext else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let options = SystemConfig.getOptions(
|
let options = SystemConfig.getOptions(
|
||||||
networkId: UInt32(session.networkId),
|
networkId: UInt32(session.networkId),
|
||||||
networkDomain: session.networkDomain,
|
networkDomain: session.networkDomain,
|
||||||
@ -104,23 +129,21 @@ class AppContext {
|
|||||||
accessToken: session.accessToken,
|
accessToken: session.accessToken,
|
||||||
identityId: context.identityId,
|
identityId: context.identityId,
|
||||||
hostname: context.hostname,
|
hostname: context.hostname,
|
||||||
noticePort: noticePort
|
noticePort: noticePort,
|
||||||
|
exitNodeIp: self.loadExitNodeIp()
|
||||||
)
|
)
|
||||||
|
|
||||||
try await self.vpnManager.enableVpn(options: options)
|
try await self.vpnManager.enableVpn(options: options)
|
||||||
self.networkContext = context
|
|
||||||
self.vpnOptions = options
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 断开网络连接
|
// 断开网络连接
|
||||||
func disconnectNetwork() async throws {
|
func stopTun() async throws {
|
||||||
try await self.vpnManager.disableVpn()
|
try await self.vpnManager.disableVpn()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 退出登陆
|
// 退出登陆
|
||||||
func logout() async throws {
|
func logout() async throws {
|
||||||
try await self.vpnManager.disableVpn()
|
try await self.vpnManager.disableVpn()
|
||||||
self.networkContext = .default()
|
self.networkContext = nil
|
||||||
self.loginCredit = nil
|
self.loginCredit = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,4 +164,24 @@ class AppContext {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理网络出口数据
|
||||||
|
extension AppContext {
|
||||||
|
|
||||||
|
func loadExitNodeIp() -> String? {
|
||||||
|
if let data = try? KeychainStore.shared.load(account: "exitNodeIp") {
|
||||||
|
return String(data: data, encoding: .utf8)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveExitNodeIp(exitNodeIp: String) async throws {
|
||||||
|
// 将数据缓存到keychain
|
||||||
|
if let data = exitNodeIp.data(using: .utf8) {
|
||||||
|
try KeychainStore.shared.save(data, account: "exitNodeIp")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -104,17 +104,22 @@ struct NetworkStatusBar: View {
|
|||||||
@Environment(AppContext.self) private var appContext
|
@Environment(AppContext.self) private var appContext
|
||||||
@State private var vpnManger = VPNManager.shared
|
@State private var vpnManger = VPNManager.shared
|
||||||
|
|
||||||
|
@State private var exitNodeIp: String = ""
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
let isOnBinding = Binding(
|
let isOnBinding = Binding(
|
||||||
get: { vpnManger.isConnected },
|
get: { vpnManger.isConnected },
|
||||||
set: { newValue in
|
set: { newValue in
|
||||||
if newValue {
|
if newValue {
|
||||||
Task {
|
Task {
|
||||||
|
if self.appContext.networkContext == nil {
|
||||||
try? await self.appContext.connectNetwork()
|
try? await self.appContext.connectNetwork()
|
||||||
}
|
}
|
||||||
|
try? await self.appContext.startTun()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Task {
|
Task {
|
||||||
try? await self.appContext.disconnectNetwork()
|
try? await self.appContext.stopTun()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -139,7 +144,7 @@ struct NetworkStatusBar: View {
|
|||||||
Text(networkSession.networkName)
|
Text(networkSession.networkName)
|
||||||
.font(.system(size: 12, weight: .semibold))
|
.font(.system(size: 12, weight: .semibold))
|
||||||
|
|
||||||
Text("局域网IP: \(appContext.networkContext.ip)")
|
Text("局域网IP: \(appContext.networkContext?.ip ?? "0.0.0.0")")
|
||||||
.font(.system(size: 10, design: .monospaced))
|
.font(.system(size: 10, design: .monospaced))
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
@ -151,6 +156,20 @@ struct NetworkStatusBar: View {
|
|||||||
Toggle("", isOn: isOnBinding)
|
Toggle("", isOn: isOnBinding)
|
||||||
.toggleStyle(.switch)
|
.toggleStyle(.switch)
|
||||||
.controlSize(.small) // macOS 顶部栏或面板推荐使用 small 尺寸
|
.controlSize(.small) // macOS 顶部栏或面板推荐使用 small 尺寸
|
||||||
|
|
||||||
|
|
||||||
|
TextField("出口节点:", text: $exitNodeIp)
|
||||||
|
|
||||||
|
Button {
|
||||||
|
Task {
|
||||||
|
let result = try await self.appContext.changeExitNodeIp(exitNodeIp: self.exitNodeIp)
|
||||||
|
let reply = try NEReply(serializedBytes: result)
|
||||||
|
NSLog("change exit node ip: \(reply)")
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Text("启动出口节点")
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
.padding(.vertical, 5)
|
.padding(.vertical, 5)
|
||||||
}
|
}
|
||||||
@ -170,7 +189,7 @@ struct NetworkConnectedView: View {
|
|||||||
GridItem(.flexible(), spacing: 8),
|
GridItem(.flexible(), spacing: 8),
|
||||||
GridItem(.flexible(), spacing: 8)
|
GridItem(.flexible(), spacing: 8)
|
||||||
], spacing: 10) {
|
], spacing: 10) {
|
||||||
ForEach(appContext.networkContext.resourceList, id: \.uuid) { res in
|
ForEach(appContext.networkContext?.resourceList ?? [], id: \.uuid) { res in
|
||||||
ResourceItemCard(resource: res)
|
ResourceItemCard(resource: res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -236,6 +255,7 @@ struct NetworkDisconnectedView: View {
|
|||||||
|
|
||||||
do {
|
do {
|
||||||
try await self.appContext.connectNetwork()
|
try await self.appContext.connectNetwork()
|
||||||
|
try await self.appContext.startTun()
|
||||||
} catch let err as SDLAPIError {
|
} catch let err as SDLAPIError {
|
||||||
self.showAlert = true
|
self.showAlert = true
|
||||||
self.errorMessage = err.message
|
self.errorMessage = err.message
|
||||||
@ -266,7 +286,7 @@ struct NetworkDeviceGroupView: View {
|
|||||||
// 如果你的 WindowStyle 是 .hiddenTitleBar,这个 Padding 非常重要
|
// 如果你的 WindowStyle 是 .hiddenTitleBar,这个 Padding 非常重要
|
||||||
Color.clear.frame(height: 28)
|
Color.clear.frame(height: 28)
|
||||||
|
|
||||||
List(appContext.networkContext.nodeList, id: \.id, selection: $selectedId) { node in
|
List(appContext.networkContext?.nodeList ?? [], id: \.id, selection: $selectedId) { node in
|
||||||
NetworkNodeHeadView(node: node)
|
NetworkNodeHeadView(node: node)
|
||||||
// 技巧:在 HStack 方案中,tag 配合 List 的 selection 依然有效
|
// 技巧:在 HStack 方案中,tag 配合 List 的 selection 依然有效
|
||||||
.tag(node.id)
|
.tag(node.id)
|
||||||
@ -281,7 +301,7 @@ struct NetworkDeviceGroupView: View {
|
|||||||
|
|
||||||
// --- 2. 详情区域 (Detail) ---
|
// --- 2. 详情区域 (Detail) ---
|
||||||
ZStack {
|
ZStack {
|
||||||
if let selectedNode = appContext.networkContext.getNode(id: selectedId) {
|
if let selectedNode = appContext.networkContext?.getNode(id: selectedId) {
|
||||||
NetworkNodeDetailView(node: selectedNode)
|
NetworkNodeDetailView(node: selectedNode)
|
||||||
.transition(.opacity.animation(.easeInOut(duration: 0.2)))
|
.transition(.opacity.animation(.easeInOut(duration: 0.2)))
|
||||||
} else {
|
} else {
|
||||||
@ -298,7 +318,7 @@ struct NetworkDeviceGroupView: View {
|
|||||||
.ignoresSafeArea() // 真正顶到最上方
|
.ignoresSafeArea() // 真正顶到最上方
|
||||||
.onAppear {
|
.onAppear {
|
||||||
if selectedId == nil {
|
if selectedId == nil {
|
||||||
selectedId = appContext.networkContext.firstNodeId()
|
selectedId = appContext.networkContext?.firstNodeId()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,10 +23,14 @@ struct SettingsDeviceView: View {
|
|||||||
.background(Color.blue.opacity(0.1))
|
.background(Color.blue.opacity(0.1))
|
||||||
.cornerRadius(12)
|
.cornerRadius(12)
|
||||||
|
|
||||||
// TODO
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text(self.appContext.networkContext.hostname)
|
if let networkContext = self.appContext.networkContext {
|
||||||
|
Text(networkContext.hostname)
|
||||||
.font(.title3.bold())
|
.font(.title3.bold())
|
||||||
|
} else {
|
||||||
|
Text("未知")
|
||||||
|
.font(.title3.bold())
|
||||||
|
}
|
||||||
|
|
||||||
Text(SystemConfig.systemInfo)
|
Text(SystemConfig.systemInfo)
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
@ -38,7 +42,7 @@ struct SettingsDeviceView: View {
|
|||||||
// MARK: - 详细参数卡片
|
// MARK: - 详细参数卡片
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
// 设备名称行
|
// 设备名称行
|
||||||
DevicePropertyRow(title: "设备名称", value: self.appContext.networkContext.hostname) {
|
DevicePropertyRow(title: "设备名称", value: self.appContext.networkContext?.hostname ?? "未知") {
|
||||||
Button {
|
Button {
|
||||||
// 修改逻辑
|
// 修改逻辑
|
||||||
} label: {
|
} label: {
|
||||||
@ -55,7 +59,7 @@ struct SettingsDeviceView: View {
|
|||||||
Divider().padding(.leading, 16)
|
Divider().padding(.leading, 16)
|
||||||
|
|
||||||
// IPv4 行
|
// IPv4 行
|
||||||
DevicePropertyRow(title: "虚拟 IPv4", value: self.appContext.networkContext.ip) {
|
DevicePropertyRow(title: "虚拟 IPv4", value: self.appContext.networkContext?.ip ?? "0.0.0.0") {
|
||||||
Image(systemName: "info.circle")
|
Image(systemName: "info.circle")
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -89,11 +89,13 @@ struct punchnetApp: App {
|
|||||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
|
|
||||||
func applicationWillFinishLaunching(_ notification: Notification) {
|
func applicationWillFinishLaunching(_ notification: Notification) {
|
||||||
|
|
||||||
let shared = UserDefaults(suiteName: "group.com.jihe.punchnetmac")
|
let shared = UserDefaults(suiteName: "group.com.jihe.punchnetmac")
|
||||||
shared?.set("App says hello", forKey: "test_msg")
|
shared?.set("App says hello", forKey: "test_msg")
|
||||||
shared?.synchronize()
|
shared?.synchronize()
|
||||||
|
|
||||||
|
DarwinNotificationCenter.shared.addObserver(for: .vpnStatusChanged) { name in
|
||||||
|
NSLog("DarwinNotificationCenter get message: \(name)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
|
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user