Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -213,11 +213,17 @@ class NebulaVpnService : VpnService() {
try {
vpnInterface = builder.establish()
nebula = mobileNebula.MobileNebula.newNebula(site!!.config, site!!.getKey(this), site!!.logFile, vpnInterface!!.detachFd().toLong())
nebula!!.start(exitCallbackFor(nebula!!))

} catch (e: Exception) {
Log.e(TAG, "Got an error $e")
// Go owns the detached tun fd from the moment newNebula is called and closes
// it on failure, never close it here, a second close can hit an unrelated
// recycled fd. The network callback and reload receiver below were never
// registered, so there is nothing else for stopVpn to clean up.
nebula = null
vpnInterface?.close()
announceExit(site!!.id, e.message)
announceExit(site!!.id, e.message ?: e.toString())
return stopSelf()
}

Expand All @@ -226,7 +232,6 @@ class NebulaVpnService : VpnService() {
//TODO: There is an open discussion around sleep killing tunnels or just changing mobile to tear down stale tunnels
//registerSleep()

nebula!!.start()
running = true
sendSimple(MSG_IS_RUNNING, 1)
}
Expand Down Expand Up @@ -301,29 +306,44 @@ class NebulaVpnService : VpnService() {
nebula?.reload(site!!.config, site!!.getKey(this))
}

private fun stopVpn() {
private fun stopVpn(error: String? = null) {
if (nebula == null) {
return stopSelf()
}

unregisterNetworkCallback()
unregisterReloadReceiver()
// stop() blocks until the packet readers have drained and nebula has fully stopped
nebula?.stop()
nebula = null
running = false
announceExit(site?.id, null)
announceExit(site?.id, error)
stopSelf()
}

// Called from a Go thread when nebula dies on its own, e.g. a fatal packet
// reader error, so the tunnel comes down instead of blackholing traffic.
// Bound to its session's nebula instance, a stale callback posted from a
// dying session must not tear down a new session on this same service.
private fun exitCallbackFor(sessionNebula: mobileNebula.Nebula): mobileNebula.ExitCallback {
return object : mobileNebula.ExitCallback {
override fun onExit(message: String?) {
Handler(Looper.getMainLooper()).post {
if (running && nebula === sessionNebula) {
stopVpn(message ?: "Nebula exited unexpectedly")
}
}
}
}
}

override fun onRevoke() {
stopVpn()
//TODO: wait for the thread to exit
super.onRevoke()
}

override fun onDestroy() {
stopVpn()
//TODO: wait for the thread to exit
super.onDestroy()
}

Expand Down
81 changes: 77 additions & 4 deletions ios/NebulaNetworkExtension/PacketTunnelProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,41 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
private var didSleep = false
private var cachedRouteDescription: String?

// A stopTunnel can race an in-flight start() before self.nebula exists, in
// which case its nebula?.stop() is a silent no-op. Latch the stop here so
// start() can apply it to the instance once it has been built. Never reset,
// a provider instance serves one session, and if the system ever reused one
// a stale latch refuses to start, which beats two nebulas on one tun fd.
private var stopped = false
private let stoppedLock = NSLock()

private func isStopped() -> Bool {
stoppedLock.lock()
defer { stoppedLock.unlock() }
return stopped
}

// Latch the stop and halt the network monitor in one critical section so a
// stop can't slip between start()'s latch check and its monitor startup
private func markStoppedAndHaltMonitor() {
stoppedLock.lock()
defer { stoppedLock.unlock() }
stopped = true
stopNetworkMonitor()
}

// Start the post startup work only if a stop has not raced us, atomic with
// the stopped latch for the same reason as markStoppedAndHaltMonitor
private func startPostStartWork() {
stoppedLock.lock()
defer { stoppedLock.unlock() }
if stopped {
return
}
startNetworkMonitor()
dnUpdater.updateSingleLoop(site: site!, onUpdate: handleDNUpdate)
}

override func startTunnel(options: [String: NSObject]? = nil) async throws {
// There is currently no way to get initialization errors back to the UI via completionHandler here
// `expectStart` is sent only via the UI which means we should wait for the real start command which has another completion handler the UI can intercept
Expand Down Expand Up @@ -112,15 +147,23 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
var nebulaErr: NSError?
self.nebula = MobileNebulaNewNebula(
String(data: config, encoding: .utf8), key, self.site!.logFile, tunFD, &nebulaErr)
self.startNetworkMonitor()

if nebulaErr != nil {
self.log.error("We had an error starting up: \(nebulaErr, privacy: .public)")
throw nebulaErr!
}

self.nebula!.start()
self.dnUpdater.updateSingleLoop(site: self.site!, onUpdate: self.handleDNUpdate)
// A stopTunnel that raced us before self.nebula existed had nothing to
// stop, apply it to this instance so start(self) tears it back down
if isStopped() {
self.nebula?.stop()
}

try self.nebula!.start(self)

// Skips the monitor and DN updater if a stopTunnel raced us, the tunnel
// is coming down and nothing would ever cancel them
startPostStartWork()
}

private func getNetworkAddressesAndRoutes(networks: [String], unsafeRoutes: [UnsafeRoute]) throws
Expand Down Expand Up @@ -250,8 +293,10 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
override func stopTunnel(
with reason: NEProviderStopReason, completionHandler: @escaping () -> Void
) {
markStoppedAndHaltMonitor()

// stop() blocks until the packet readers have drained and nebula has fully stopped
nebula?.stop()
stopNetworkMonitor()
completionHandler()
}

Expand Down Expand Up @@ -299,6 +344,15 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
// No response data, this is expected on a clean start
return try? JSONEncoder().encode(IPCResponse.init(type: .success, message: nil))
} catch {
// A stop that raced this start already tore the tunnel down, either
// cleanly or by yanking the session out from under one of the startup
// steps. Report success so the user's own disconnect doesn't pop an
// error in the UI, the site status will settle to disconnected via the
// status stream. Log it in case the failure predated the stop.
if isStopped() {
log.error("Start failed while stopping: \(error.localizedDescription, privacy: .public)")
return try? JSONEncoder().encode(IPCResponse.init(type: .success, message: nil))
}
defer {
self.cancelTunnelWithError(error)
}
Expand Down Expand Up @@ -398,3 +452,22 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
return nil
}
}

extension PacketTunnelProvider: MobileNebulaExitCallbackProtocol {
// Called from a Go thread when nebula dies on its own, e.g. a fatal packet
// reader error, so the tunnel comes down instead of blackholing traffic
func onExit(_ message: String?) {
// A reader can die from the tun being torn out from under it during a
// requested stop, don't report that as a tunnel failure
if isStopped() {
return
}

let error = message ?? "Nebula exited unexpectedly"
log.error("\(error, privacy: .public)")
cancelTunnelWithError(
NSError(
domain: "net.defined.mobileNebula", code: 1,
userInfo: [NSLocalizedDescriptionKey: error]))
}
}
Loading
Loading