Merge pull request #11 from keeshux/add-ncp-support
Add initial NCP support
This commit is contained in:
commit
7df229c115
|
@ -15,10 +15,12 @@ The client is known to work with [OpenVPN®][openvpn] 2.3+ servers. Key renegoti
|
|||
- [x] Handshake and tunneling over UDP or TCP
|
||||
- [x] Ciphers
|
||||
- AES-CBC (128 and 256 bit)
|
||||
- AES-GCM (128 and 256 bit)
|
||||
- AES-GCM (128 and 256 bit, 2.4)
|
||||
- [x] HMAC digests
|
||||
- SHA-1
|
||||
- SHA-256
|
||||
- [x] NCP (Negotiable Crypto Parameters, 2.4)
|
||||
- Server-side
|
||||
- [x] TLS handshake
|
||||
- CA validation
|
||||
- Client certificate
|
||||
|
|
|
@ -59,8 +59,9 @@ struct CoreConfiguration {
|
|||
// MARK: Authentication
|
||||
|
||||
static let peerInfo = [
|
||||
"IV_VER=2.3.99",
|
||||
"IV_VER=2.4",
|
||||
"IV_PROTO=2",
|
||||
"IV_NCP=2",
|
||||
""
|
||||
].joined(separator: "\n")
|
||||
|
||||
|
|
|
@ -47,12 +47,11 @@
|
|||
|
||||
- (nonnull instancetype)initWithEncrypter:(nonnull id<DataPathEncrypter>)encrypter
|
||||
decrypter:(nonnull id<DataPathDecrypter>)decrypter
|
||||
peerId:(uint32_t)peerId // 24-bit, discard most significant byte
|
||||
compressionFraming:(CompressionFramingNative)compressionFraming
|
||||
maxPackets:(NSInteger)maxPackets
|
||||
usesReplayProtection:(BOOL)usesReplayProtection;
|
||||
|
||||
- (void)setPeerId:(uint32_t)peerId; // 24-bit, discard most significant byte
|
||||
- (void)setCompressionFraming:(CompressionFramingNative)compressionFraming;
|
||||
|
||||
- (NSArray<NSData *> *)encryptPackets:(nonnull NSArray<NSData *> *)packets key:(uint8_t)key error:(NSError **)error;
|
||||
- (NSArray<NSData *> *)decryptPackets:(nonnull NSArray<NSData *> *)packets keepAlive:(nullable bool *)keepAlive error:(NSError **)error;
|
||||
|
||||
|
|
|
@ -80,7 +80,7 @@
|
|||
return (uint8_t *)addr;
|
||||
}
|
||||
|
||||
- (instancetype)initWithEncrypter:(id<DataPathEncrypter>)encrypter decrypter:(id<DataPathDecrypter>)decrypter maxPackets:(NSInteger)maxPackets usesReplayProtection:(BOOL)usesReplayProtection
|
||||
- (instancetype)initWithEncrypter:(id<DataPathEncrypter>)encrypter decrypter:(id<DataPathDecrypter>)decrypter peerId:(uint32_t)peerId compressionFraming:(CompressionFramingNative)compressionFraming maxPackets:(NSInteger)maxPackets usesReplayProtection:(BOOL)usesReplayProtection
|
||||
{
|
||||
NSParameterAssert(encrypter);
|
||||
NSParameterAssert(decrypter);
|
||||
|
@ -103,7 +103,9 @@
|
|||
self.inReplay = [[ReplayProtector alloc] init];
|
||||
}
|
||||
|
||||
self.compressionFraming = CompressionFramingNativeDisabled;
|
||||
[self.encrypter setPeerId:peerId];
|
||||
[self.decrypter setPeerId:peerId];
|
||||
[self setCompressionFraming:compressionFraming];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
@ -150,15 +152,6 @@
|
|||
return [[self class] alignedPointer:self.decBuffer];
|
||||
}
|
||||
|
||||
- (void)setPeerId:(uint32_t)peerId
|
||||
{
|
||||
NSAssert(self.encrypter, @"Setting peer-id to nil encrypter");
|
||||
NSAssert(self.decrypter, @"Setting peer-id to nil decrypter");
|
||||
|
||||
[self.encrypter setPeerId:peerId];
|
||||
[self.decrypter setPeerId:peerId];
|
||||
}
|
||||
|
||||
- (void)setCompressionFraming:(CompressionFramingNative)compressionFraming
|
||||
{
|
||||
switch (compressionFraming) {
|
||||
|
|
|
@ -146,6 +146,15 @@ public protocol SessionReply {
|
|||
|
||||
/// The DNS servers set up for this session.
|
||||
var dnsServers: [String] { get }
|
||||
|
||||
/// The optional authentication token.
|
||||
var authToken: String? { get }
|
||||
|
||||
/// The optional 24-bit peer-id.
|
||||
var peerId: UInt32? { get }
|
||||
|
||||
/// The negotiated cipher if any (NCP).
|
||||
var cipher: SessionProxy.Cipher? { get }
|
||||
}
|
||||
|
||||
extension SessionProxy {
|
||||
|
@ -179,6 +188,8 @@ extension SessionProxy {
|
|||
|
||||
private static let peerIdRegexp = try! NSRegularExpression(pattern: "peer-id [0-9]+", options: [])
|
||||
|
||||
private static let cipherRegexp = try! NSRegularExpression(pattern: "cipher [^\\s]+", options: [])
|
||||
|
||||
let ipv4: IPv4Settings?
|
||||
|
||||
let ipv6: IPv6Settings?
|
||||
|
@ -189,6 +200,8 @@ extension SessionProxy {
|
|||
|
||||
let peerId: UInt32?
|
||||
|
||||
let cipher: SessionProxy.Cipher?
|
||||
|
||||
init?(message: String) throws {
|
||||
guard message.hasPrefix("PUSH_REPLY") else {
|
||||
return nil
|
||||
|
@ -207,6 +220,7 @@ extension SessionProxy {
|
|||
var dnsServers: [String] = []
|
||||
var authToken: String?
|
||||
var peerId: UInt32?
|
||||
var cipher: SessionProxy.Cipher?
|
||||
|
||||
// MARK: Routing (IPv4)
|
||||
|
||||
|
@ -354,10 +368,17 @@ extension SessionProxy {
|
|||
PushReply.peerIdRegexp.enumerateArguments(in: message) {
|
||||
peerId = UInt32($0[0])
|
||||
}
|
||||
|
||||
// MARK: NCP
|
||||
|
||||
PushReply.cipherRegexp.enumerateArguments(in: message) {
|
||||
cipher = SessionProxy.Cipher(rawValue: $0[0].uppercased())
|
||||
}
|
||||
|
||||
self.dnsServers = dnsServers
|
||||
self.authToken = authToken
|
||||
self.peerId = peerId
|
||||
self.cipher = cipher
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -74,8 +74,6 @@ extension SessionProxy {
|
|||
|
||||
private var isTLSConnected: Bool
|
||||
|
||||
private var canHandlePackets: Bool
|
||||
|
||||
init(id: UInt8) {
|
||||
self.id = id
|
||||
|
||||
|
@ -83,7 +81,6 @@ extension SessionProxy {
|
|||
state = .invalid
|
||||
softReset = false
|
||||
isTLSConnected = false
|
||||
canHandlePackets = false
|
||||
}
|
||||
|
||||
// Ruby: Key.hard_reset_timeout
|
||||
|
@ -109,21 +106,11 @@ extension SessionProxy {
|
|||
return isTLSConnected
|
||||
}
|
||||
|
||||
func startHandlingPackets(withPeerId peerId: UInt32? = nil, compressionFraming: CompressionFraming = .disabled) {
|
||||
dataPath?.setPeerId(peerId ?? PacketPeerIdDisabled)
|
||||
dataPath?.setCompressionFraming(compressionFraming.native)
|
||||
canHandlePackets = true
|
||||
}
|
||||
|
||||
func encrypt(packets: [Data]) throws -> [Data]? {
|
||||
guard let dataPath = dataPath else {
|
||||
log.warning("Data: Set dataPath first")
|
||||
return nil
|
||||
}
|
||||
guard canHandlePackets else {
|
||||
log.warning("Data: Invoke startHandlingPackets() before encrypting")
|
||||
return nil
|
||||
}
|
||||
return try dataPath.encryptPackets(packets, key: id)
|
||||
}
|
||||
|
||||
|
@ -132,10 +119,6 @@ extension SessionProxy {
|
|||
log.warning("Data: Set dataPath first")
|
||||
return nil
|
||||
}
|
||||
guard canHandlePackets else {
|
||||
log.warning("Data: Invoke startHandlingPackets() before decrypting")
|
||||
return nil
|
||||
}
|
||||
var keepAlive = false
|
||||
let decrypted = try dataPath.decryptPackets(packets, keepAlive: &keepAlive)
|
||||
if keepAlive {
|
||||
|
|
|
@ -123,9 +123,7 @@ public class SessionProxy {
|
|||
|
||||
private var remoteSessionId: Data?
|
||||
|
||||
private var authToken: String?
|
||||
|
||||
private var peerId: UInt32?
|
||||
private var pushReply: SessionReply?
|
||||
|
||||
private var nextPushRequestDate: Date?
|
||||
|
||||
|
@ -229,7 +227,7 @@ public class SessionProxy {
|
|||
- Returns: `true` if supports link rebinding.
|
||||
*/
|
||||
public func canRebindLink() -> Bool {
|
||||
return (peerId != nil)
|
||||
return (pushReply?.peerId != nil)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -241,7 +239,7 @@ public class SessionProxy {
|
|||
- Seealso: `canRebindLink()`.
|
||||
*/
|
||||
public func rebindLink(_ link: LinkInterface) {
|
||||
guard let _ = peerId else {
|
||||
guard let _ = pushReply?.peerId else {
|
||||
log.warning("Session doesn't support link rebinding!")
|
||||
return
|
||||
}
|
||||
|
@ -316,11 +314,10 @@ public class SessionProxy {
|
|||
|
||||
sessionId = nil
|
||||
remoteSessionId = nil
|
||||
authToken = nil
|
||||
nextPushRequestDate = nil
|
||||
connectedDate = nil
|
||||
authenticator = nil
|
||||
peerId = nil
|
||||
pushReply = nil
|
||||
link = nil
|
||||
if !(tunnel?.isPersistent ?? false) {
|
||||
tunnel = nil
|
||||
|
@ -614,7 +611,6 @@ public class SessionProxy {
|
|||
controlPacketIdOut = 0
|
||||
controlPacketIdIn = 0
|
||||
authenticator = nil
|
||||
peerId = nil
|
||||
bytesIn = 0
|
||||
bytesOut = 0
|
||||
}
|
||||
|
@ -624,6 +620,7 @@ public class SessionProxy {
|
|||
log.debug("Send hard reset")
|
||||
|
||||
resetControlChannel()
|
||||
pushReply = nil
|
||||
do {
|
||||
try sessionId = SecureRandom.data(length: ProtocolMacros.sessionIdLength)
|
||||
} catch let e {
|
||||
|
@ -662,7 +659,7 @@ public class SessionProxy {
|
|||
negotiationKey.controlState = .preAuth
|
||||
|
||||
do {
|
||||
authenticator = try Authenticator(configuration.username, authToken ?? configuration.password)
|
||||
authenticator = try Authenticator(configuration.username, pushReply?.authToken ?? configuration.password)
|
||||
try authenticator?.putAuth(into: negotiationKey.tls)
|
||||
} catch let e {
|
||||
deferStop(.shutdown, e)
|
||||
|
@ -701,11 +698,7 @@ public class SessionProxy {
|
|||
enqueueControlPackets(code: .controlV1, key: negotiationKey.id, payload: cipherTextOut)
|
||||
|
||||
if negotiationKey.softReset {
|
||||
authenticator = nil
|
||||
negotiationKey.startHandlingPackets(withPeerId: peerId)
|
||||
negotiationKey.controlState = .connected
|
||||
connectedDate = Date()
|
||||
transitionKeys()
|
||||
completeConnection()
|
||||
}
|
||||
nextPushRequestDate = Date().addingTimeInterval(CoreConfiguration.retransmissionLimit)
|
||||
}
|
||||
|
@ -725,6 +718,14 @@ public class SessionProxy {
|
|||
}
|
||||
}
|
||||
|
||||
private func completeConnection() {
|
||||
setupEncryption()
|
||||
authenticator = nil
|
||||
negotiationKey.controlState = .connected
|
||||
connectedDate = Date()
|
||||
transitionKeys()
|
||||
}
|
||||
|
||||
// MARK: Control
|
||||
|
||||
// Ruby: handle_ctrl_pkt
|
||||
|
@ -848,8 +849,6 @@ public class SessionProxy {
|
|||
return
|
||||
}
|
||||
|
||||
setupKeys()
|
||||
|
||||
negotiationKey.controlState = .preIfConfig
|
||||
nextPushRequestDate = Date().addingTimeInterval(negotiationKey.softReset ? CoreConfiguration.softResetDelay : CoreConfiguration.retransmissionLimit)
|
||||
pushRequest()
|
||||
|
@ -884,21 +883,13 @@ public class SessionProxy {
|
|||
return
|
||||
}
|
||||
reply = optionalReply
|
||||
authToken = reply.authToken
|
||||
peerId = reply.peerId
|
||||
} catch let e {
|
||||
deferStop(.shutdown, e)
|
||||
return
|
||||
}
|
||||
|
||||
authenticator = nil
|
||||
negotiationKey.startHandlingPackets(
|
||||
withPeerId: peerId,
|
||||
compressionFraming: configuration.compressionFraming
|
||||
)
|
||||
negotiationKey.controlState = .connected
|
||||
connectedDate = Date()
|
||||
transitionKeys()
|
||||
pushReply = reply
|
||||
completeConnection()
|
||||
|
||||
guard let remoteAddress = link?.remoteAddress else {
|
||||
fatalError("Could not resolve link remote address")
|
||||
|
@ -1007,22 +998,25 @@ public class SessionProxy {
|
|||
}
|
||||
|
||||
// Ruby: setup_keys
|
||||
private func setupKeys() {
|
||||
private func setupEncryption() {
|
||||
guard let auth = authenticator else {
|
||||
fatalError("Setting up keys without having authenticated")
|
||||
fatalError("Setting up encryption without having authenticated")
|
||||
}
|
||||
guard let sessionId = sessionId else {
|
||||
fatalError("Setting up keys without a local sessionId")
|
||||
fatalError("Setting up encryption without a local sessionId")
|
||||
}
|
||||
guard let remoteSessionId = remoteSessionId else {
|
||||
fatalError("Setting up keys without a remote sessionId")
|
||||
fatalError("Setting up encryption without a remote sessionId")
|
||||
}
|
||||
guard let serverRandom1 = auth.serverRandom1, let serverRandom2 = auth.serverRandom2 else {
|
||||
fatalError("Setting up keys without server randoms")
|
||||
fatalError("Setting up encryption without server randoms")
|
||||
}
|
||||
|
||||
guard let pushReply = pushReply else {
|
||||
fatalError("Setting up encryption without a former PUSH_REPLY")
|
||||
}
|
||||
|
||||
if CoreConfiguration.logsSensitiveData {
|
||||
log.debug("Setup keys from the following components:")
|
||||
log.debug("Set up encryption from the following components:")
|
||||
log.debug("\tpreMaster: \(auth.preMaster.toHex())")
|
||||
log.debug("\trandom1: \(auth.random1.toHex())")
|
||||
log.debug("\trandom2: \(auth.random2.toHex())")
|
||||
|
@ -1031,13 +1025,18 @@ public class SessionProxy {
|
|||
log.debug("\tsessionId: \(sessionId.toHex())")
|
||||
log.debug("\tremoteSessionId: \(remoteSessionId.toHex())")
|
||||
} else {
|
||||
log.debug("Setup keys")
|
||||
log.debug("Set up encryption")
|
||||
}
|
||||
|
||||
let pushedCipher = pushReply.cipher
|
||||
if let negCipher = pushedCipher {
|
||||
log.debug("Negotiated cipher: \(negCipher.rawValue)")
|
||||
}
|
||||
|
||||
let bridge: EncryptionBridge
|
||||
do {
|
||||
bridge = try EncryptionBridge(
|
||||
configuration.cipher,
|
||||
pushedCipher ?? configuration.cipher,
|
||||
configuration.digest,
|
||||
auth,
|
||||
sessionId,
|
||||
|
@ -1051,6 +1050,8 @@ public class SessionProxy {
|
|||
negotiationKey.dataPath = DataPath(
|
||||
encrypter: bridge.encrypter(),
|
||||
decrypter: bridge.decrypter(),
|
||||
peerId: pushReply.peerId ?? PacketPeerIdDisabled,
|
||||
compressionFraming: configuration.compressionFraming.native,
|
||||
maxPackets: link?.packetBufferSize ?? 200,
|
||||
usesReplayProtection: CoreConfiguration.usesReplayProtection
|
||||
)
|
||||
|
|
|
@ -87,8 +87,14 @@ class DataPathEncryptionTests: XCTestCase {
|
|||
}
|
||||
|
||||
func privateTestDataPathHigh(peerId: UInt32?) {
|
||||
let path = DataPath(encrypter: enc, decrypter: dec, maxPackets: 1000, usesReplayProtection: false)
|
||||
path.setCompressionFraming(.disabled)
|
||||
let path = DataPath(
|
||||
encrypter: enc,
|
||||
decrypter: dec,
|
||||
peerId: peerId ?? PacketPeerIdDisabled,
|
||||
compressionFraming: .disabled,
|
||||
maxPackets: 1000,
|
||||
usesReplayProtection: false
|
||||
)
|
||||
|
||||
if let peerId = peerId {
|
||||
enc.setPeerId(peerId)
|
||||
|
|
|
@ -54,7 +54,14 @@ class DataPathPerformanceTests: XCTestCase {
|
|||
encrypter = crypto.encrypter()
|
||||
decrypter = crypto.decrypter()
|
||||
|
||||
dataPath = DataPath(encrypter: encrypter, decrypter: decrypter, maxPackets: 200, usesReplayProtection: false)
|
||||
dataPath = DataPath(
|
||||
encrypter: encrypter,
|
||||
decrypter: decrypter,
|
||||
peerId: PacketPeerIdDisabled,
|
||||
compressionFraming: .disabled,
|
||||
maxPackets: 200,
|
||||
usesReplayProtection: false
|
||||
)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
|
|
|
@ -91,4 +91,12 @@ class PushTests: XCTestCase {
|
|||
XCTAssertEqual(reply.ipv6?.defaultGateway, "fe80::601:30ff:feb7:dc02")
|
||||
XCTAssertEqual(reply.dnsServers, ["2001:4860:4860::8888", "2001:4860:4860::8844"])
|
||||
}
|
||||
|
||||
func testNCP() {
|
||||
let msg = "PUSH_REPLY,dhcp-option DNS 8.8.8.8,dhcp-option DNS 4.4.4.4,comp-lzo no,route 10.8.0.1,topology net30,ping 10,ping-restart 120,ifconfig 10.8.0.6 10.8.0.5,peer-id 0,cipher AES-256-CBC"
|
||||
let reply = try! SessionProxy.PushReply(message: msg)!
|
||||
reply.debug()
|
||||
|
||||
XCTAssertEqual(reply.cipher, .aes256cbc)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue