1
0
mirror of https://github.com/SoftEtherVPN/SoftEtherVPN.git synced 2025-06-27 19:35:09 +03:00

Merge pull request #2125 from AhmadReza6610/master

Add iOS client implementation with SoftEther protocol handshake support
This commit is contained in:
Ilya Shipitsin 2025-05-08 18:22:14 +02:00 committed by GitHub
commit cb9ccf41a5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 761 additions and 0 deletions

View File

@ -0,0 +1,118 @@
import Foundation
import Network
import Security
/// SecureConnection handles the TLS connection with the SoftEther VPN server
class SecureConnection {
// MARK: - Properties
private var connection: NWConnection?
private let host: String
private let port: UInt16
private let queue = DispatchQueue(label: "com.softether.connection", qos: .userInitiated)
// MARK: - Initialization
/// Initialize a secure connection
/// - Parameters:
/// - host: Server hostname or IP address
/// - port: Server port number
init(host: String, port: UInt16) {
self.host = host
self.port = port
}
// MARK: - Public Methods
/// Connect to the server using TLS
/// - Parameter completion: Callback with connection result
func connect(completion: @escaping (Bool, Error?) -> Void) {
let hostEndpoint = NWEndpoint.Host(host)
let portEndpoint = NWEndpoint.Port(rawValue: port)!
// Create TLS parameters
let tlsOptions = NWProtocolTLS.Options()
// Configure TLS for maximum compatibility with SoftEther
let securityOptions = tlsOptions.securityProtocolOptions
sec_protocol_options_set_tls_min_version(securityOptions, .TLSv12)
sec_protocol_options_set_tls_max_version(securityOptions, .TLSv13)
// Allow all cipher suites for compatibility
sec_protocol_options_set_cipher_suites(securityOptions, nil, 0)
// Disable certificate validation for initial development (ENABLE IN PRODUCTION)
sec_protocol_options_set_verify_block(securityOptions, { (_, _, trustResult, _) in
return true // Accept all certificates for testing
}, queue)
// Create TCP options with TLS
let tcpOptions = NWProtocolTCP.Options()
tcpOptions.enableKeepalive = true
tcpOptions.keepaliveIdle = 30
// Create connection parameters
let parameters = NWParameters(tls: tlsOptions, tcp: tcpOptions)
// Create the connection
connection = NWConnection(host: hostEndpoint, port: portEndpoint, using: parameters)
// Set up state handling
connection?.stateUpdateHandler = { [weak self] state in
switch state {
case .ready:
completion(true, nil)
case .failed(let error):
self?.disconnect()
completion(false, error)
case .cancelled:
completion(false, NSError(domain: "SoftEtherError", code: 1000, userInfo: [NSLocalizedDescriptionKey: "Connection cancelled"]))
default:
break
}
}
// Start the connection
connection?.start(queue: queue)
}
/// Disconnect from the server
func disconnect() {
connection?.cancel()
connection = nil
}
/// Send data to the server
/// - Parameters:
/// - data: Data to send
/// - completion: Callback with error if any
func send(data: Data, completion: @escaping (Error?) -> Void) {
guard let connection = connection, connection.state == .ready else {
completion(NSError(domain: "SoftEtherError", code: 1001, userInfo: [NSLocalizedDescriptionKey: "Connection not ready"]))
return
}
connection.send(content: data, completion: .contentProcessed { error in
completion(error)
})
}
/// Receive data from the server
/// - Parameter completion: Callback with received data and error if any
func receive(completion: @escaping (Data?, Error?) -> Void) {
guard let connection = connection, connection.state == .ready else {
completion(nil, NSError(domain: "SoftEtherError", code: 1001, userInfo: [NSLocalizedDescriptionKey: "Connection not ready"]))
return
}
connection.receive(minimumIncompleteLength: 1, maximumLength: 65536) { data, _, isComplete, error in
completion(data, error)
if isComplete {
// Connection was closed by the peer
self.disconnect()
}
}
}
}

View File

@ -0,0 +1,90 @@
import Foundation
/// Handles the specific client signature format that SoftEther expects
class SoftEtherClientSignature {
// MARK: - Constants
private enum Constants {
static let clientBuildNumber: UInt32 = 5187
static let clientVersion: UInt32 = 5_02_0000 + clientBuildNumber
static let clientString = "SoftEther VPN Client"
static let softEtherMagic: [UInt8] = [0x5E, 0x68] // 'Se' in hex
// Protocol identification constants from SoftEther source
static let cedar = "CEDAR"
static let sessionKey = "sessionkey"
static let protocol1 = "PROTOCOL"
static let protocol2 = "PROTOCOL2"
}
// MARK: - Public Methods
/// Generate the client signature packet that identifies this client as a legitimate SoftEther VPN client
/// - Returns: Data containing the formatted client signature
static func generateSignature() -> Data {
var data = Data()
// 1. Add SoftEther magic bytes
data.append(contentsOf: Constants.softEtherMagic)
// 2. Add client version in network byte order (big endian)
data.appendUInt32(Constants.clientVersion)
// 3. Add client build number in network byte order
data.appendUInt32(Constants.clientBuildNumber)
// 4. Add cedar protocol identifier
if let cedarData = Constants.cedar.data(using: .ascii) {
data.append(cedarData)
data.append(0) // null terminator
}
// 5. Add client string with null terminator
if let clientString = (Constants.clientString + "\0").data(using: .ascii) {
data.append(clientString)
}
// 6. Add protocol identifiers
if let protocolData = (Constants.protocol1 + "\0").data(using: .ascii) {
data.append(protocolData)
}
if let protocol2Data = (Constants.protocol2 + "\0").data(using: .ascii) {
data.append(protocol2Data)
}
// 7. Add session key marker
if let sessionKeyData = (Constants.sessionKey + "\0").data(using: .ascii) {
data.append(sessionKeyData)
}
// 8. Add random data for session key (typically 20 bytes)
let randomSessionKey = SoftEtherCrypto.randomBytes(count: 20)
data.append(randomSessionKey)
// 9. Calculate and append SHA-1 hash of the entire data for integrity verification
let hash = SoftEtherCrypto.sha1(data)
data.append(hash)
return data
}
/// Verify a server response to the client signature
/// - Parameter data: Response data from server
/// - Returns: True if valid response, false otherwise
static func verifyServerResponse(_ data: Data) -> Bool {
// Basic validation - a real implementation would parse and validate the server response format
// This is a minimal check to see if we have enough data and it starts with the magic bytes
guard data.count >= 8 else {
return false
}
// Check if response starts with SoftEther magic bytes
if data[0] == Constants.softEtherMagic[0] && data[1] == Constants.softEtherMagic[1] {
return true
}
return false
}
}

View File

@ -0,0 +1,97 @@
import Foundation
import CryptoKit
/// Handles encryption operations for SoftEther protocol
class SoftEtherCrypto {
// MARK: - Constants
private enum Constants {
static let sha1Size = 20
static let md5Size = 16
}
// MARK: - Public Methods
/// Generate secure random bytes
/// - Parameter count: Number of random bytes to generate
/// - Returns: Data containing random bytes
static func randomBytes(count: Int) -> Data {
var data = Data(count: count)
_ = data.withUnsafeMutableBytes {
SecRandomCopyBytes(kSecRandomDefault, count, $0.baseAddress!)
}
return data
}
/// Calculate SHA-1 hash
/// - Parameter data: Input data
/// - Returns: SHA-1 hash of the input data
static func sha1(_ data: Data) -> Data {
let digest = SHA1.hash(data: data)
return Data(digest)
}
/// Calculate MD5 hash
/// - Parameter data: Input data
/// - Returns: MD5 hash of the input data
static func md5(_ data: Data) -> Data {
let digest = Insecure.MD5.hash(data: data)
return Data(digest)
}
/// Encrypt data using RC4 algorithm (for SoftEther compatibility)
/// - Parameters:
/// - data: Data to encrypt
/// - key: Encryption key
/// - Returns: Encrypted data
static func rc4Encrypt(data: Data, key: Data) -> Data {
let rc4 = RC4(key: key)
return rc4.process(data)
}
/// Decrypt data using RC4 algorithm (for SoftEther compatibility)
/// - Parameters:
/// - data: Data to decrypt
/// - key: Decryption key
/// - Returns: Decrypted data
static func rc4Decrypt(data: Data, key: Data) -> Data {
// RC4 is symmetric, so encryption and decryption are the same operation
return rc4Encrypt(data: data, key: key)
}
}
/// Simple RC4 implementation for SoftEther compatibility
/// Note: RC4 is considered insecure, but SoftEther uses it in parts of its protocol
private class RC4 {
private var state: [UInt8]
init(key: Data) {
state = Array(0...255)
var j: Int = 0
// Key scheduling algorithm
for i in 0..<256 {
let keyByte = key[i % key.count]
j = (j + Int(state[i]) + Int(keyByte)) & 0xFF
state.swapAt(i, j)
}
}
func process(_ data: Data) -> Data {
var result = Data(count: data.count)
var i: Int = 0
var j: Int = 0
// Generate keystream and XOR with plaintext
for k in 0..<data.count {
i = (i + 1) & 0xFF
j = (j + Int(state[i])) & 0xFF
state.swapAt(i, j)
let keyStreamByte = state[(Int(state[i]) + Int(state[j])) & 0xFF]
result[k] = data[k] ^ keyStreamByte
}
return result
}
}

View File

@ -0,0 +1,123 @@
import Foundation
/// Handles the SoftEther packet structure for communication
class SoftEtherPacket {
// MARK: - Constants
private enum PacketType: UInt32 {
case clientSignature = 0x01
case serverResponse = 0x02
case sessionRequest = 0x03
case sessionResponse = 0x04
case data = 0x05
case keepAlive = 0x06
}
private enum Constants {
static let headerSize: UInt32 = 16
static let maxPacketSize: UInt32 = 1024 * 1024 // 1MB
}
// MARK: - Properties
private var packetType: PacketType
private var packetId: UInt32
private var packetData: Data
// MARK: - Initialization
/// Initialize a packet with type, ID and data
/// - Parameters:
/// - type: Packet type
/// - id: Packet ID
/// - data: Packet payload
init(type: UInt32, id: UInt32, data: Data) {
self.packetType = PacketType(rawValue: type) ?? .data
self.packetId = id
self.packetData = data
}
/// Initialize a packet from raw data
/// - Parameter data: Raw packet data
init?(fromData data: Data) {
guard data.count >= Int(Constants.headerSize) else {
return nil
}
// Parse header
let typeValue = data.readUInt32(at: 0)
self.packetId = data.readUInt32(at: 4)
let dataSize = data.readUInt32(at: 8)
// Validate packet
guard let type = PacketType(rawValue: typeValue),
dataSize <= Constants.maxPacketSize,
data.count >= Int(Constants.headerSize + dataSize) else {
return nil
}
self.packetType = type
// Extract payload
let startIndex = Int(Constants.headerSize)
let endIndex = startIndex + Int(dataSize)
self.packetData = data.subdata(in: startIndex..<endIndex)
}
// MARK: - Public Methods
/// Serialize the packet to binary data format
/// - Returns: Serialized packet data
func serialize() -> Data {
var result = Data(capacity: Int(Constants.headerSize) + packetData.count)
// Write header
result.appendUInt32(packetType.rawValue)
result.appendUInt32(packetId)
result.appendUInt32(UInt32(packetData.count))
result.appendUInt32(0) // Reserved
// Write payload
result.append(packetData)
return result
}
/// Get the packet type
/// - Returns: Packet type
func getType() -> UInt32 {
return packetType.rawValue
}
/// Get the packet ID
/// - Returns: Packet ID
func getId() -> UInt32 {
return packetId
}
/// Get the packet payload
/// - Returns: Packet payload data
func getData() -> Data {
return packetData
}
}
// MARK: - Extensions
extension Data {
/// Read a UInt32 value from the data at specified offset
/// - Parameter offset: Offset to read from
/// - Returns: UInt32 value in big-endian order
func readUInt32(at offset: Int) -> UInt32 {
let slice = self.subdata(in: offset..<(offset + 4))
return slice.withUnsafeBytes { $0.load(as: UInt32.self).bigEndian }
}
/// Append a UInt32 value to the data in big-endian order
/// - Parameter value: UInt32 value to append
mutating func appendUInt32(_ value: UInt32) {
var bigEndian = value.bigEndian
append(UnsafeBufferPointer(start: &bigEndian, count: 1))
}
}

View File

@ -0,0 +1,184 @@
import Foundation
import Network
import Security
import CryptoKit
/// SoftEtherProtocol manages the communication between iOS client and SoftEther VPN server
class SoftEtherProtocol {
// MARK: - Properties
private var secureConnection: SecureConnection?
private var isConnected = false
private var host: String = ""
private var port: UInt16 = 443
private var nextPacketId: UInt32 = 1
// MARK: - Public Methods
/// Connect to a SoftEther VPN server
/// - Parameters:
/// - host: The server hostname or IP address
/// - port: The server port (default: 443)
/// - completion: Callback with connection result
public func connect(to host: String, port: UInt16 = 443, completion: @escaping (Bool, Error?) -> Void) {
self.host = host
self.port = port
// Create a secure connection
secureConnection = SecureConnection(host: host, port: port)
// Connect using TLS
secureConnection?.connect { [weak self] success, error in
guard let self = self, success else {
completion(false, error ?? NSError(domain: "SoftEtherError", code: 1, userInfo: [NSLocalizedDescriptionKey: "TLS connection failed"]))
return
}
// After successful TLS connection, send the client signature
self.sendClientSignature { success, error in
if success {
self.isConnected = true
}
completion(success, error)
}
}
}
/// Disconnect from the server
public func disconnect() {
secureConnection?.disconnect()
isConnected = false
}
// MARK: - Private Methods
/// Send the SoftEther client signature to identify as a legitimate client
/// - Parameter completion: Callback with result
private func sendClientSignature(completion: @escaping (Bool, Error?) -> Void) {
// Generate client signature using our specialized class
let signatureData = SoftEtherClientSignature.generateSignature()
// Create a packet with the signature data
let packetId = self.nextPacketId
self.nextPacketId += 1
let packet = SoftEtherPacket(type: 0x01, id: packetId, data: signatureData)
let packetData = packet.serialize()
print("Sending client signature packet: \(packetData.count) bytes")
// Send the packet
secureConnection?.send(data: packetData) { [weak self] error in
guard let self = self else { return }
if let error = error {
print("Error sending client signature: \(error)")
completion(false, error)
return
}
// After sending signature, wait for server response
self.receiveServerResponse { success, error in
completion(success, error)
}
}
}
/// Receive and process server response after sending signature
/// - Parameter completion: Callback with result
private func receiveServerResponse(completion: @escaping (Bool, Error?) -> Void) {
secureConnection?.receive { data, error in
if let error = error {
print("Error receiving server response: \(error)")
completion(false, error)
return
}
guard let data = data, data.count > 4 else {
let error = NSError(domain: "SoftEtherError", code: 2, userInfo: [NSLocalizedDescriptionKey: "Invalid server response"])
print("Invalid server response: insufficient data")
completion(false, error)
return
}
print("Received server response: \(data.count) bytes")
// Parse the response packet
guard let packet = SoftEtherPacket(fromData: data) else {
let error = NSError(domain: "SoftEtherError", code: 3, userInfo: [NSLocalizedDescriptionKey: "Invalid packet format"])
print("Could not parse server response packet")
completion(false, error)
return
}
// Verify the response
let packetData = packet.getData()
let isValid = SoftEtherClientSignature.verifyServerResponse(packetData)
if isValid {
print("Server accepted our client signature")
completion(true, nil)
} else {
print("Server rejected our client signature")
let error = NSError(domain: "SoftEtherError", code: 4, userInfo: [NSLocalizedDescriptionKey: "Server rejected client signature"])
completion(false, error)
}
}
}
/// Send a data packet to the server
/// - Parameters:
/// - data: Data to send
/// - completion: Callback with result
func sendData(data: Data, completion: @escaping (Bool, Error?) -> Void) {
guard isConnected else {
completion(false, NSError(domain: "SoftEtherError", code: 5, userInfo: [NSLocalizedDescriptionKey: "Not connected to server"]))
return
}
let packetId = self.nextPacketId
self.nextPacketId += 1
let packet = SoftEtherPacket(type: 0x05, id: packetId, data: data)
let packetData = packet.serialize()
secureConnection?.send(data: packetData) { error in
if let error = error {
completion(false, error)
return
}
completion(true, nil)
}
}
/// Receive data from the server
/// - Parameter completion: Callback with received data and result
func receiveData(completion: @escaping (Data?, Bool, Error?) -> Void) {
guard isConnected else {
completion(nil, false, NSError(domain: "SoftEtherError", code: 5, userInfo: [NSLocalizedDescriptionKey: "Not connected to server"]))
return
}
secureConnection?.receive { data, error in
if let error = error {
completion(nil, false, error)
return
}
guard let data = data, data.count > 4 else {
completion(nil, false, NSError(domain: "SoftEtherError", code: 2, userInfo: [NSLocalizedDescriptionKey: "Invalid server response"]))
return
}
// Parse the packet
guard let packet = SoftEtherPacket(fromData: data) else {
completion(nil, false, NSError(domain: "SoftEtherError", code: 3, userInfo: [NSLocalizedDescriptionKey: "Invalid packet format"]))
return
}
completion(packet.getData(), true, nil)
}
}
}

View File

@ -0,0 +1,149 @@
import Foundation
import UIKit
/// SoftEtherVPNClient provides a simple interface for connecting to SoftEther VPN servers
public class SoftEtherVPNClient {
// MARK: - Properties
private let protocol: SoftEtherProtocol
private var connectionState: ConnectionState = .disconnected
// MARK: - Public Types
/// Connection states for the VPN client
public enum ConnectionState {
case disconnected
case connecting
case connected
case disconnecting
case error(Error)
}
/// Connection delegate to receive state updates
public protocol ConnectionDelegate: AnyObject {
func connectionStateDidChange(_ state: ConnectionState)
}
/// Weak reference to the delegate
public weak var delegate: ConnectionDelegate?
// MARK: - Initialization
public init() {
self.protocol = SoftEtherProtocol()
}
// MARK: - Public Methods
/// Connect to a SoftEther VPN server
/// - Parameters:
/// - host: Server hostname or IP address
/// - port: Server port (default: 443)
/// - completion: Optional completion handler
public func connect(to host: String, port: UInt16 = 443, completion: ((Bool, Error?) -> Void)? = nil) {
// Update state
connectionState = .connecting
delegate?.connectionStateDidChange(connectionState)
// Connect using the protocol implementation
protocol.connect(to: host, port: port) { [weak self] success, error in
guard let self = self else { return }
if success {
self.connectionState = .connected
} else if let error = error {
self.connectionState = .error(error)
} else {
self.connectionState = .disconnected
}
self.delegate?.connectionStateDidChange(self.connectionState)
completion?(success, error)
}
}
/// Disconnect from the server
/// - Parameter completion: Optional completion handler
public func disconnect(completion: (() -> Void)? = nil) {
// Update state
connectionState = .disconnecting
delegate?.connectionStateDidChange(connectionState)
// Disconnect
protocol.disconnect()
// Update state again
connectionState = .disconnected
delegate?.connectionStateDidChange(connectionState)
completion?()
}
/// Get the current connection state
/// - Returns: Current ConnectionState
public func getConnectionState() -> ConnectionState {
return connectionState
}
/// Check if currently connected
/// - Returns: True if connected, false otherwise
public func isConnected() -> Bool {
if case .connected = connectionState {
return true
}
return false
}
// MARK: - Example Usage
/// Example showing how to use this class in a view controller
public static func exampleUsage() -> String {
return """
// In your view controller:
private let vpnClient = SoftEtherVPNClient()
override func viewDidLoad() {
super.viewDidLoad()
// Set delegate
vpnClient.delegate = self
}
@IBAction func connectButtonTapped(_ sender: UIButton) {
if vpnClient.isConnected() {
vpnClient.disconnect()
} else {
vpnClient.connect(to: "vpn.example.com") { success, error in
if !success {
print("Failed to connect: \\(error?.localizedDescription ?? "Unknown error")")
}
}
}
}
// MARK: - ConnectionDelegate
extension YourViewController: SoftEtherVPNClient.ConnectionDelegate {
func connectionStateDidChange(_ state: SoftEtherVPNClient.ConnectionState) {
switch state {
case .connected:
connectButton.setTitle("Disconnect", for: .normal)
statusLabel.text = "Connected"
case .connecting:
statusLabel.text = "Connecting..."
case .disconnecting:
statusLabel.text = "Disconnecting..."
case .disconnected:
connectButton.setTitle("Connect", for: .normal)
statusLabel.text = "Disconnected"
case .error(let error):
statusLabel.text = "Error: \\(error.localizedDescription)"
connectButton.setTitle("Connect", for: .normal)
}
}
}
"""
}
}