Sorry, writing a response during the night in a foreign language was a bad idea...
I mean, I have a "container" bundle and a "nested" bundle which I put in "container".app/Contents/MacOS.
The container application launches the nested one as an agent.
I tested launching via launchctl and Service Management framework.
It turns out that I have to put "Apple Events" entitlements for both the container and the nested app and update their Info.plists accordingly (put there Privacy - AppleEvents Sending Usage Description), even though only the nested app deals with Scripting Bridge.
When a nested app reaches the execution point where it invokes Scripting Bridge API a corresponding popup from the system appears to get the user verdict (allow/deny access). However, the message in this popup states that Automation permission is requested by a container app that doesn't deal with Apple Events and acts as a simple launcher for the nested one.
It turns out that I didn't need AssociatedBundleIdentifiers in the nested app's Info.plist (so far, I don't know whether it could a problem for notarization).
Post
Replies
Boosts
Views
Activity
@DTS Engineer
So, here is what I have investigated. If I put the application bundle inside another bundle I should set entitlements and Info.plist accordingly for both container and nested app. In that case despite the way you are launching a nested application (either via launchctl utility or via Service Management framework). With this setup, I could finally see the message about the process asking for Automation permission. However, the application name in the message wasn't the container's not the nested app's.
@DTS Engineer checked with your suggestion to add AssociatedBundleIdentifiers. Whilst tccd now doesn't report this particular error GUI app still doesn't request automation permission (which happens when I run it as a standalone process) on SBApplication call.
Probably you are right, I am going to check with your suggestions, thank you!
So far I have modified the UI element app so I can run it as a standalone GUI-less app (via Xcode without launchctl invocation or by double-clicking on the bundle). By GUI-less I mean with "Application is agent" key set to YES in Info.plist and without "Main storyboard" key. In this configuration, the app can acquire Automation permissions and interact with Finder via Scripting Bridge.
Hi, eskimo!
Finally, I was able to check that. This termination mystery was solved: an exit() statement caused app termination. The statement was in the program itself and it had nothing in common with tccd.
However, the issue with tccd that caused the creation of this thread persists. It continues to report the absence of entitlements for Apple Events despite the fact, that the bundle has it.
If we return to our naming convention: "UI element app" is a user agent that has the corresponding plist and gets launched via launchctl command from "container app". UI element app bundle is a part of the container app bundle.
Considering these conditions is it possible that the issue may be with the container app not having the required entitlements?
Tested with your suggestion the issue persists. The error message from tccd persists, the UI element app app gets closed on calling SBApplication(bundleIdentifier:). No crash reports in Console.
Sorry, responded in the comment section. Duplicate as a reply:
Ok, considering only the real product:
The UI element app is an app, right? Not something else, like an app extension.
Exactly, that is an app not an app extension or anything else
Are either of these sandboxed?
They aren't sandboxed
Where did you app the Apple event capability? On the UI element app?
Yes, on the UI element app via Xcode "Signing & Capabilities"
In that TCC error, which bundle ID and path are shown? The container app? Or the UI element app?
UI element app
Ah, exactly. Thank you!
Seems like the code snippet from my last reply (with calls of inbound- and outbound- copiers inside open() completion handler) works, I am successful in controlling the traffic. On the other hand, is it supposed to work like that?
I have read Handling flow copying article. So, it doesn't clear to me how I can switch between reading and writing on both sides of the connection. Here is the example of outboundCopier() from the article
let flow: NEAppProxyTCPFlow
let connection: NWConnection
// Reads from the flow and writes to the remote connection.
func outboundCopier() {
flow.readData { (data, error) in
if error == nil, let readData = data, !readData.isEmpty {
connection.send(content: readData,
completion: .contentProcessed( { connectionError in
// Handle completion success or error.
// Set up another read if there is no error.
if connectionError == nil {
self.outboundCopier()
}
}))
} else {
// Handle error case or the read that contains empty data.
}
}
}
I see that the method is called recursively until there is data that can be sent from the flow to the remote endpoint. However, I don't get where we should call inboundCopier() to read the response. Should it be called in the else branch from the outboundCopier(), when we detect that there is no flow data left unread?
Or I can call inbound and outbound copiers synchronously inside the flow open() completion handler, when the state of the NWConnection transfers to .ready? For instance:
case .ready:
flow.open(withLocalEndpoint: nil) { error in
if let error = error {
os_log(.debug, log: self.log, "TCP flow opening failed %@", error.localizedDescription)
return
}
self.outboundCopier()
self.inboundCopier()
}
Let's assume that inboundCopier() and outboundCopier() have the same implementations as in the article
Thank you in advance, for explanation
Thank you for reply, Eskimo!
So, I try to handle TCP flow like this
override func handleNewFlow(_ flow: NEAppProxyFlow) -> Bool {
os_log(.debug, log: self.log, "Handle new flow")
if let tcpFlow = flow as? NEAppProxyTCPFlow {
guard let targetEndpoint = tcpFlow.remoteEndpoint as? NWHostEndpoint else {
os_log(.debug, log: self.log, "On outboundCopierTCP. Unable to cast remote endpoint to NWHostEndpoint")
return false
}
let tcpConnectionManager = TCPConnectionManager(flow: tcpFlow, endpoint: targetEndpoint)
tcpConnectionManager.startExchangingData()
return true
}
return false
}
Am I heading to the right direction?
You can review a TCPConnectionManagerClass implementation in my reply above. It manages NWConnection with intended target of the flow and read\write operation between connection and flow
Finally, TCPConnectionManager
class TCPConnectionManager {
public let flow: NEAppProxyTCPFlow
private let connection: NWConnection
private let tcpConnection = NWTCPConnection()
private let log = OSLog(subsystem: "com.ybelikov.transparent.proxy.network.extension", category: "provider")
private let connectionQueue = DispatchQueue(label: "com.ybelikov.transparent.proxy.network.extension.TCPQueue")
init(flow: NEAppProxyTCPFlow, endpoint: NWHostEndpoint) {
self.flow = flow
let host = Network.NWEndpoint.Host(endpoint.hostname)
let port = Network.NWEndpoint.Port(endpoint.port) ?? Network.NWEndpoint.Port.any
os_log(.debug, log: self.log, "On TCP init. host: %@, port: %@", String(describing: host), String(describing: port))
self.connection = NWConnection(host: host, port: port, using: .tcp)
}
public func startExchangingData() {
connection.stateUpdateHandler = self.stateChangedCallback(to:)
connection.start(queue: connectionQueue)
}
private func stateChangedCallback(to state: NWConnection.State) {
switch state {
case .ready:
os_log(.debug, log: self.log, "TCPConnectionManager::stateChangedCallback. TCP connection is ready")
flow.open(withLocalEndpoint: tcpConnection.localAddress as? NWHostEndpoint) { error in
if let error = error {
os_log(.debug, log: self.log, "TCP flow opening failed %@", error.localizedDescription)
return
}
self.handleOutgoingTCPData()
}
break
case .failed(let error):
os_log(.debug, log: self.log, "TCPConnectionManager::stateChangedCallback. TCP connection is failed %@", error.localizedDescription)
self.connection.cancel()
break
case .cancelled:
os_log(.debug, log: self.log, "TCPConnectionManager::stateChangedCallback. TCP connection is cancelled")
break
case .preparing:
os_log(.debug, log: self.log, "TCPConnectionManager::stateChangedCallback. TCP connection is preparing")
break
case .setup:
os_log(.debug, log: self.log, "TCPConnectionManager::stateChangedCallback. TCP connection is in setup state")
break
case .waiting(let error):
os_log(.debug, log: self.log, "TCPConnectionManager::stateChangedCallback. TCP connection is in waiting state, %@", error.localizedDescription)
self.connection.cancel()
break
default:
os_log(.debug, log: self.log, "TCPConnectionManager::stateChangedCallback. State is unknown")
break
}
}
private func handleOutgoingTCPData() {
os_log(.debug, log: self.log, "handleOutgoingTCPData. handling TCP flow data")
flow.readData { data, error in
if let error = error {
os_log(.debug, log: self.log, "handleOutgoingTCPData. Error on handling TCP flow data: %@", error.localizedDescription)
return
}
if let data = data, !data.isEmpty {
self.connection.send(content: data, completion: .contentProcessed({ error in
if let error = error {
os_log(.debug, log: self.log, "TCPConnectionManager::sendDataToEndpoint. TCP error: %@", error.localizedDescription)
return
}
os_log(.debug, log: self.log, "TCP data sent successfully")
self.handleOutgoingTCPData()
}))
} else {
self.handleIncomingTCPData()
}
}
}
private func handleIncomingTCPData() {
os_log(.debug, log: self.log, "On handleIncomingTCPData")
connection.receive(minimumIncompleteLength: 0,
maximumLength: 2048) { (data, _, isComplete, error) in
switch (data, isComplete, error) {
case (let data?, _, _):
os_log(.debug, log: self.log, "On handleIncomingTCPData. Received data, writing it to the flow")
self.flow.write(data) { writeError in
if writeError == nil {
os_log(.debug, log: self.log, "On handleIncomingTCPData. Write error is nil")
self.handleIncomingTCPData()
}
}
case (_, true, _):
os_log(.debug, log: self.log, "On handleIncomingTCPData. Connection is completed")
self.connection.stateUpdateHandler = nil
self.connection.cancel()
self.flow.closeReadWithError(error)
self.flow.closeWriteWithError(error)
case (_, _, let error?):
os_log(.debug, log: self.log, "On handleIncomingTCPData. Error: %@", error.localizedDescription)
self.connection.cancel()
self.flow.closeReadWithError(error)
self.flow.closeWriteWithError(error)
default:
os_log(.debug, log: self.log, "On handleIncomingTCPData. Unknown case")
}
}
}
}
Ah, exactly! Thank you for pointing this out. It works fine now!
Hi, Matt and Oskar!
Can you explain how the traffic can be redirected from one extension to another? For instance, I would like to set up a pipeline with NETransparentProxyProvider and NEPacketTunnelProvider. As I understand from Matt's reply, if I process traffic inside the transparent proxy and redirect it somewhere, the tunnel provider will consider this traffic as originating from the proxy instead original application. However, for me, it is not clear how the proxy would redirect traffic to the packet tunnel. Should the extension with NEPacketTunnelProvider runs a sort of a server to sniff all traffic from the proxy?
So, I have managed to see the desired log about proxy start. However I don't see logs inside handleNewFlow. The method contains nothing complicated. All that I want to achieve for now is to "see" all outgoing traffic and allow it.
override func handleNewFlow(_ flow: NEAppProxyFlow) -> Bool {
os_log(.debug, log: self.log, "Handle new flow")
return false
}
inside startProxy I configured NETransparentProxySettings like this, to sniff all outgoing traffic:
override func startProxy(options: [String : Any]?, completionHandler: @escaping (Error?) -> Void) {
os_log(.debug, log: self.log, "Proxy started")
let proxyNetworkSettings = NETransparentProxyNetworkSettings(tunnelRemoteAddress: "127.0.0.1")
let allOutgoingTCPTrafficRule = NENetworkRule(remoteNetwork: nil, remotePrefix: 0, localNetwork: nil, localPrefix: 0, protocol: .TCP, direction: .outbound)
let allOutgoingUDPTrafficRule = NENetworkRule(remoteNetwork: nil, remotePrefix: 0, localNetwork: nil, localPrefix: 0, protocol: .UDP, direction: .outbound)
proxyNetworkSettings.includedNetworkRules = [allOutgoingTCPTrafficRule, allOutgoingUDPTrafficRule]
setTunnelNetworkSettings(proxyNetworkSettings) { error in
if let error = error {
os_log(.debug, log: self.log, "Error on saving tunnel network settings")
return
}
os_log(.debug, log: self.log, "Tunnel network settings saved correctly")
}
}
Also, I see in the Network Settings panel that the proxy's network profile hangs in "connecting" state forever.
Is there a problem with tunnelRemoteAddress? What I have to configure to be able to track outgoing flows?