Compare commits

...

3 Commits

Author SHA1 Message Date
a697770187 解决dns的回路问题 2026-04-09 17:31:32 +08:00
c8b2218841 fix 2026-04-09 10:57:49 +08:00
b01e1ba039 增加出口ip的支持 2026-04-08 16:55:07 +08:00
13 changed files with 458 additions and 100 deletions

View File

@ -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:
()
}
}
} }
} }

View File

@ -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)
// arpmac // arpmac
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)

View File

@ -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)

View File

@ -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? {

View File

@ -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()
} }
} }

View 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
}
}

View File

@ -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 {

View File

@ -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 })
} }

View File

@ -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"
} }
} }

View File

@ -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")
}
}
} }

View File

@ -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()
} }
} }
} }

View File

@ -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)
} }

View File

@ -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 {