Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit87ec698

Browse files
committed
fix: unquarantine dylib after download
1 parentdf3d755 commit87ec698

File tree

12 files changed

+161
-52
lines changed

12 files changed

+161
-52
lines changed

‎Coder Desktop/Coder Desktop/Coder_Desktop.entitlements

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,9 @@
88
</array>
99
<key>com.apple.developer.system-extension.install</key>
1010
<true/>
11-
<key>com.apple.security.app-sandbox</key>
12-
<true/>
1311
<key>com.apple.security.application-groups</key>
1412
<array>
1513
<string>$(TeamIdentifierPrefix)com.coder.Coder-Desktop</string>
1614
</array>
17-
<key>com.apple.security.files.user-selected.read-only</key>
18-
<true/>
19-
<key>com.apple.security.network.client</key>
20-
<true/>
2115
</dict>
2216
</plist>

‎Coder Desktop/Coder Desktop/NetworkExtension.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,13 @@ enum NetworkExtensionState: Equatable {
2424
/// An actor that handles configuring, enabling, and disabling the VPN tunnel via the
2525
/// NetworkExtension APIs.
2626
extensionCoderVPNService{
27-
funchasNetworkExtensionConfig()async->Bool{
27+
funcloadNetworkExtensionConfig()async{
2828
do{
29-
_=tryawaitgetTunnelManager()
30-
returntrue
29+
lettm=tryawaitgetTunnelManager()
30+
neState=.disabled
31+
serverAddress= tm.protocolConfiguration?.serverAddress
3132
}catch{
32-
returnfalse
33+
neState=.unconfigured
3334
}
3435
}
3536

‎Coder Desktop/Coder Desktop/VPNService.swift

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,13 @@ final class CoderVPNService: NSObject, VPNService {
6363
// only stores a weak reference to the delegate.
6464
varsystemExtnDelegate:SystemExtensionDelegate<CoderVPNService>?
6565

66+
varserverAddress:String?
67+
6668
overrideinit(){
6769
super.init()
6870
installSystemExtension()
6971
Task{
70-
neState=ifawaithasNetworkExtensionConfig(){
71-
.disabled
72-
}else{
73-
.unconfigured
74-
}
72+
awaitloadNetworkExtensionConfig()
7573
}
7674
xpc.connect()
7775
xpc.getPeerState()
@@ -115,6 +113,7 @@ final class CoderVPNService: NSObject, VPNService {
115113
func configureTunnelProviderProtocol(proto:NETunnelProviderProtocol?){
116114
Task{
117115
iflet proto{
116+
serverAddress= proto.serverAddress
118117
awaitconfigureNetworkExtension(proto: proto)
119118
// this just configures the VPN, it doesn't enable it
120119
tunnelState=.disabled

‎Coder Desktop/Coder Desktop/Views/VPNMenu.swift

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,7 @@ struct VPNMenu<VPN: VPNService, S: Session>: View {
3131
Text("Workspace Agents")
3232
.font(.headline)
3333
.foregroundColor(.gray)
34-
if session.hasSession{
35-
VPNState<VPN>()
36-
}else{
37-
Text("Sign in to use CoderVPN")
38-
.font(.body)
39-
.foregroundColor(.gray)
40-
}
34+
VPNState<VPN,S>()
4135
}.padding([.horizontal,.top],Theme.Size.trayInset)
4236
Agents<VPN,S>()
4337
// Trailing stack
@@ -52,7 +46,15 @@ struct VPNMenu<VPN: VPNService, S: Session>: View {
5246
}.buttonStyle(.plain)
5347
TrayDivider()
5448
}
55-
AuthButton<VPN,S>()
49+
if vpn.state==.failed(.systemExtensionError(.needsUserApproval)){
50+
Button{
51+
openSystemExtensionSettings()
52+
} label:{
53+
ButtonRowView{Text("Open System Preferences")}
54+
}.buttonStyle(.plain)
55+
}else{
56+
AuthButton<VPN,S>()
57+
}
5658
Button{
5759
openSettings()
5860
appActivate()
@@ -84,10 +86,18 @@ struct VPNMenu<VPN: VPNService, S: Session>: View {
8486
privatevarvpnDisabled:Bool{
8587
!session.hasSession ||
8688
vpn.state==.connecting ||
87-
vpn.state==.disconnecting
89+
vpn.state==.disconnecting ||
90+
vpn.state==.failed(.systemExtensionError(.needsUserApproval))
8891
}
8992
}
9093

94+
func openSystemExtensionSettings(){
95+
// TODO: Check this still works in a new macOS version
96+
// https://gist.github.com/rmcdongit/f66ff91e0dad78d4d6346a75ded4b751?permalink_comment_id=5261757
97+
// swiftlint:disable:next line_length
98+
NSWorkspace.shared.open(URL(string:"x-apple.systempreferences:com.apple.ExtensionsPreferences?extensionPointIdentifier=com.apple.system_extension.network_extension.extension-point")!)
99+
}
100+
91101
#Preview{
92102
VPNMenu<PreviewVPN,PreviewSession>().frame(width:256)
93103
.environmentObject(PreviewVPN())

‎Coder Desktop/Coder Desktop/Views/VPNState.swift

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,35 @@
11
import SwiftUI
22

3-
structVPNState<VPN:VPNService>:View{
3+
structVPNState<VPN:VPNService, S:Session>:View{
44
@EnvironmentObjectvarvpn:VPN
5+
@EnvironmentObjectvarsession:S
56

67
letinspection=Inspection<Self>()
78

89
varbody:someView{
910
Group{
10-
switch vpn.state{
11-
case.disabled:
12-
Text("Enable CoderVPN to see agents")
11+
switch(vpn.state, session.hasSession){
12+
case(.failed(.systemExtensionError(.needsUserApproval)), _):
13+
Text("Awaiting System Extension Approval")
14+
.font(.body)
15+
.foregroundStyle(.gray)
16+
case(_,false):
17+
Text("Sign in to use CoderVPN")
1318
.font(.body)
1419
.foregroundColor(.gray)
15-
case.connecting,.disconnecting:
20+
case(.disabled, _):
21+
Text("Enable CoderVPN to see agents")
22+
.font(.body)
23+
.foregroundStyle(.gray)
24+
case(.connecting, _),(.disconnecting, _):
1625
HStack{
1726
Spacer()
1827
ProgressView(
1928
vpn.state==.connecting?"Starting CoderVPN...":"Stopping CoderVPN..."
2029
).padding()
2130
Spacer()
2231
}
23-
caselet.failed(vpnErr):
32+
caselet(.failed(vpnErr), _):
2433
Text("\(vpnErr.description)")
2534
.font(.headline)
2635
.foregroundColor(.red)

‎Coder Desktop/Coder Desktop/XPCInterface.swift

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,39 @@ import VPNLib
6464
svc.onExtensionPeerUpdate(data)
6565
}
6666
}
67+
68+
// The NE has verified the dylib and knows better than Gatekeeper
69+
func removeQuarantine(path:String, reply:@escaping(Bool)->Void){
70+
letreply=CallbackWrapper(reply)
71+
Task{@MainActorin
72+
letprompt="""
73+
Coder Desktop wants to execute code downloaded from\
74+
\(svc.serverAddress??"the Coder deployment"). The code has been\
75+
verified to be signed by Coder.
76+
"""
77+
letsource="""
78+
do shell script"xattr -d com.apple.quarantine\(path)"\
79+
with prompt"\(prompt)"\
80+
with administrator privileges
81+
"""
82+
letsuccess=awaitwithCheckedContinuation{ continuationin
83+
guardlet script=NSAppleScript(source: source)else{
84+
continuation.resume(returning:false)
85+
return
86+
}
87+
// Run on a background thread
88+
Task.detached{
89+
varerror:NSDictionary?
90+
script.executeAndReturnError(&error)
91+
iflet error{
92+
self.logger.error("AppleScript error:\(error)")
93+
continuation.resume(returning:false)
94+
}else{
95+
continuation.resume(returning:true)
96+
}
97+
}
98+
}
99+
reply(success)
100+
}
101+
}
67102
}

‎Coder Desktop/Coder DesktopTests/VPNStateTests.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@ import ViewInspector
77
@Suite(.timeLimit(.minutes(1)))
88
structVPNStateTests{
99
letvpn:MockVPNService
10-
letsut:VPNState<MockVPNService>
10+
letsession:MockSession
11+
letsut:VPNState<MockVPNService,MockSession>
1112
letview:anyView
1213

1314
init(){
1415
vpn=MockVPNService()
15-
sut=VPNState<MockVPNService>()
16-
view= sut.environmentObject(vpn)
16+
sut=VPNState<MockVPNService,MockSession>()
17+
session=MockSession()
18+
session.hasSession=true
19+
view= sut.environmentObject(vpn).environmentObject(session)
1720
}
1821

1922
@Test

‎Coder Desktop/VPN/Manager.swift

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ actor Manager {
4646
}catch{
4747
throw.validation(error)
4848
}
49+
50+
// HACK: The downloaded dylib may be quarantined, but we've validated it's signature
51+
// so it's safe to execute. However, this SE must be sandboxed, so we defer to the app.
52+
tryawaitremoveQuarantine(dest)
53+
4954
do{
5055
try tunnelHandle=TunnelHandle(dylibPath: dest)
5156
}catch{
@@ -85,7 +90,13 @@ actor Manager {
8590
}catch{
8691
logger.error("tunnel read loop failed:\(error.localizedDescription, privacy:.public)")
8792
tryawait tunnelHandle.close()
88-
ptp.cancelTunnelWithError(error)
93+
ptp.cancelTunnelWithError(
94+
NSError(
95+
domain:"\(Bundle.main.bundleIdentifier!).Manager",
96+
code:-1,
97+
userInfo:[NSLocalizedDescriptionKey:"Tunnel read loop failed:\(error.localizedDescription)"]
98+
)
99+
)
89100
return
90101
}
91102
logger.info("tunnel read loop exited")
@@ -227,6 +238,9 @@ enum ManagerError: Error {
227238
case serverInfo(String)
228239
case errorResponse(msg:String)
229240
case noTunnelFileDescriptor
241+
case noApp
242+
case permissionDenied
243+
case tunnelFail(anyError)
230244

231245
vardescription:String{
232246
switchself{
@@ -248,6 +262,12 @@ enum ManagerError: Error {
248262
msg
249263
case.noTunnelFileDescriptor:
250264
"Could not find a tunnel file descriptor"
265+
case.noApp:
266+
"The VPN must be started with the app open during first-time setup."
267+
case.permissionDenied:
268+
"Permission was not granted to execute the CoderVPN dylib"
269+
caselet.tunnelFail(err):
270+
"Failed to communicate with dylib over tunnel:\(err)"
251271
}
252272
}
253273
}
@@ -272,3 +292,23 @@ func writeVpnLog(_ log: Vpn_Log) {
272292
letfields= log.fields.map{"\($0.name):\($0.value)"}.joined(separator:",")
273293
logger.log(level: level,"\(log.message, privacy:.public):\(fields, privacy:.public)")
274294
}
295+
296+
privatefunc removeQuarantine(_ dest:URL)asyncthrows(ManagerError){
297+
varflag:AnyObject?
298+
letfile=NSURL(fileURLWithPath: dest.path)
299+
try? file.getResourceValue(&flag, forKey: kCFURLQuarantinePropertiesKeyasURLResourceKey)
300+
if flag!=nil{
301+
guardlet conn= globalXPCListenerDelegate.connelse{
302+
throw.noApp
303+
}
304+
// Wait for unsandboxed app to accept our file
305+
letsuccess=awaitwithCheckedContinuation{[dest] continuationin
306+
conn.removeQuarantine(path: dest.path){ successin
307+
continuation.resume(returning: success)
308+
}
309+
}
310+
if !success{
311+
throw.permissionDenied
312+
}
313+
}
314+
}

‎Coder Desktop/VPN/PacketTunnelProvider.swift

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,47 +43,73 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
4343
returnnil
4444
}
4545

46+
// swiftlint:disable:next function_body_length
4647
overridefunc startTunnel(
4748
options _:[String:NSObject]?, completionHandler:@escaping(Error?)->Void
4849
){
4950
logger.info("startTunnel called")
5051
guard manager==nilelse{
5152
logger.error("startTunnel called with non-nil Manager")
52-
completionHandler(PTPError.alreadyRunning)
53+
completionHandler(
54+
NSError(
55+
domain:"\(Bundle.main.bundleIdentifier!).PTP",
56+
code:-1,
57+
userInfo:[NSLocalizedDescriptionKey:"Already running"]
58+
)
59+
)
5360
return
5461
}
5562
guardlet proto= protocolConfigurationas?NETunnelProviderProtocol,
5663
let baseAccessURL= proto.serverAddress
5764
else{
5865
logger.error("startTunnel called with nil protocolConfiguration")
59-
completionHandler(PTPError.missingConfiguration)
66+
completionHandler(
67+
NSError(
68+
domain:"\(Bundle.main.bundleIdentifier!).PTP",
69+
code:-1,
70+
userInfo:[NSLocalizedDescriptionKey:"Missing Configuration"]
71+
)
72+
)
6073
return
6174
}
6275
// HACK: We can't write to the system keychain, and the NE can't read the user keychain.
6376
guardlet token= proto.providerConfiguration?["token"]as?Stringelse{
6477
logger.error("startTunnel called with nil token")
65-
completionHandler(PTPError.missingToken)
78+
completionHandler(
79+
NSError(
80+
domain:"\(Bundle.main.bundleIdentifier!).PTP",
81+
code:-1,
82+
userInfo:[NSLocalizedDescriptionKey:"Missing Token"]
83+
)
84+
)
6685
return
6786
}
6887
logger.debug("retrieved token & access URL")
6988
letcompletionHandler=CallbackWrapper(completionHandler)
7089
Task{
7190
dothrows(ManagerError){
7291
logger.debug("creating manager")
73-
manager=tryawaitManager(
92+
letmanager=tryawaitManager(
7493
with:self,
7594
cfg:.init(
7695
apiToken: token, serverUrl:.init(string: baseAccessURL)!
7796
)
7897
)
7998
globalXPCListenerDelegate.vpnXPCInterface.manager= manager
8099
logger.debug("starting vpn")
81-
tryawait manager!.startVPN()
100+
tryawait manager.startVPN()
82101
logger.info("vpn started")
102+
self.manager= manager
83103
completionHandler(nil)
84104
} catch{
85105
logger.error("error starting manager:\(error.description, privacy:.public)")
86-
completionHandler(errorasNSError)
106+
completionHandler(
107+
NSError(
108+
domain:"\(Bundle.main.bundleIdentifier!).Manager",
109+
code:-1,
110+
userInfo:[NSLocalizedDescriptionKey: error.description]
111+
)
112+
)
87113
}
88114
}
89115
}
@@ -152,9 +178,3 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
152178
tryawaitsetTunnelNetworkSettings(currentSettings)
153179
}
154180
}
155-
156-
enumPTPError:Error{
157-
case alreadyRunning
158-
case missingConfiguration
159-
case missingToken
160-
}

‎Coder Desktop/VPNLib/Util.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
publicstructCallbackWrapper<T, U>:@uncheckedSendable{
2-
privateletblock:(T?)->U
2+
privateletblock:(T)->U
33

4-
publicinit(_ block:@escaping(T?)->U){
4+
publicinit(_ block:@escaping(T)->U){
55
self.block= block
66
}
77

8-
publicfunc callAsFunction(_ error:T?)->U{
8+
publicfunc callAsFunction(_ error:T)->U{
99
block(error)
1010
}
1111
}

‎Coder Desktop/VPNLib/XPC.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ import Foundation
1010
@objcpublicprotocolVPNXPCClientCallbackProtocol{
1111
// data is a serialized `Vpn_PeerUpdate`
1212
func onPeerUpdate(_ data:Data)
13+
func removeQuarantine(path:String, reply:@escaping(Bool)->Void)
1314
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp