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

Commit2bfe5bd

Browse files
fix: unquarantine dylib after download (#38)
1 parentdf3d755 commit2bfe5bd

14 files changed

+145
-54
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/AuthButton.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ struct AuthButton<VPN: VPNService, S: Session>: View {
1717
}
1818
} label:{
1919
ButtonRowView{
20-
Text(session.hasSession?"SignOut":"SignIn")
20+
Text(session.hasSession?"Signout":"Signin")
2121
}
2222
}.buttonStyle(.plain)
2323
}

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

Lines changed: 20 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("Approve in System Settings")}
54+
}.buttonStyle(.plain)
55+
}else{
56+
AuthButton<VPN,S>()
57+
}
5658
Button{
5759
openSettings()
5860
appActivate()
@@ -84,10 +86,19 @@ 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+
// Sourced from:
96+
// https://gist.github.com/rmcdongit/f66ff91e0dad78d4d6346a75ded4b751?permalink_comment_id=5261757
97+
// We'll need to ensure this continues to work in future macOS versions
98+
// swiftlint:disable:next line_length
99+
NSWorkspace.shared.open(URL(string:"x-apple.systempreferences:com.apple.ExtensionsPreferences?extensionPointIdentifier=com.apple.system_extension.network_extension.extension-point")!)
100+
}
101+
91102
#Preview{
92103
VPNMenu<PreviewVPN,PreviewSession>().frame(width:256)
93104
.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/VPNMenuTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ struct VPNMenuTests {
2727
lettoggle=try view.find(ViewType.Toggle.self)
2828
#expect(toggle.isDisabled())
2929
#expect(throws:Never.self){try view.find(text:"Sign in to use CoderVPN")}
30-
#expect(throws:Never.self){try view.find(button:"SignIn")}
30+
#expect(throws:Never.self){try view.find(button:"Signin")}
3131
}
3232
}
3333
}

‎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: 37 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,9 @@ 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+
makeNSError(suffix:"Manager", desc:"Tunnel read loop failed:\(error.localizedDescription)")
95+
)
8996
return
9097
}
9198
logger.info("tunnel read loop exited")
@@ -227,6 +234,9 @@ enum ManagerError: Error {
227234
case serverInfo(String)
228235
case errorResponse(msg:String)
229236
case noTunnelFileDescriptor
237+
case noApp
238+
case permissionDenied
239+
case tunnelFail(anyError)
230240

231241
vardescription:String{
232242
switchself{
@@ -248,6 +258,12 @@ enum ManagerError: Error {
248258
msg
249259
case.noTunnelFileDescriptor:
250260
"Could not find a tunnel file descriptor"
261+
case.noApp:
262+
"The VPN must be started with the app open during first-time setup."
263+
case.permissionDenied:
264+
"Permission was not granted to execute the CoderVPN dylib"
265+
caselet.tunnelFail(err):
266+
"Failed to communicate with dylib over tunnel:\(err)"
251267
}
252268
}
253269
}
@@ -272,3 +288,23 @@ func writeVpnLog(_ log: Vpn_Log) {
272288
letfields= log.fields.map{"\($0.name):\($0.value)"}.joined(separator:",")
273289
logger.log(level: level,"\(log.message, privacy:.public):\(fields, privacy:.public)")
274290
}
291+
292+
privatefunc removeQuarantine(_ dest:URL)asyncthrows(ManagerError){
293+
varflag:AnyObject?
294+
letfile=NSURL(fileURLWithPath: dest.path)
295+
try? file.getResourceValue(&flag, forKey: kCFURLQuarantinePropertiesKeyasURLResourceKey)
296+
if flag!=nil{
297+
guardlet conn= globalXPCListenerDelegate.connelse{
298+
throw.noApp
299+
}
300+
// Wait for unsandboxed app to accept our file
301+
letsuccess=awaitwithCheckedContinuation{[dest] continuationin
302+
conn.removeQuarantine(path: dest.path){ successin
303+
continuation.resume(returning: success)
304+
}
305+
}
306+
if !success{
307+
throw.permissionDenied
308+
}
309+
}
310+
}

‎Coder Desktop/VPN/PacketTunnelProvider.swift

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -49,41 +49,44 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
4949
logger.info("startTunnel called")
5050
guard manager==nilelse{
5151
logger.error("startTunnel called with non-nil Manager")
52-
completionHandler(PTPError.alreadyRunning)
52+
completionHandler(makeNSError(suffix:"PTP", desc:"Already running"))
5353
return
5454
}
5555
guardlet proto= protocolConfigurationas?NETunnelProviderProtocol,
5656
let baseAccessURL= proto.serverAddress
5757
else{
5858
logger.error("startTunnel called with nil protocolConfiguration")
59-
completionHandler(PTPError.missingConfiguration)
59+
completionHandler(makeNSError(suffix:"PTP", desc:"Missing Configuration"))
6060
return
6161
}
6262
// HACK: We can't write to the system keychain, and the NE can't read the user keychain.
6363
guardlet token= proto.providerConfiguration?["token"]as?Stringelse{
6464
logger.error("startTunnel called with nil token")
65-
completionHandler(PTPError.missingToken)
65+
completionHandler(makeNSError(suffix:"PTP", desc:"Missing Token"))
6666
return
6767
}
6868
logger.debug("retrieved token & access URL")
6969
letcompletionHandler=CallbackWrapper(completionHandler)
7070
Task{
7171
dothrows(ManagerError){
7272
logger.debug("creating manager")
73-
manager=tryawaitManager(
73+
letmanager=tryawaitManager(
7474
with:self,
7575
cfg:.init(
7676
apiToken: token, serverUrl:.init(string: baseAccessURL)!
7777
)
7878
)
7979
globalXPCListenerDelegate.vpnXPCInterface.manager= manager
8080
logger.debug("starting vpn")
81-
tryawait manager!.startVPN()
81+
tryawait manager.startVPN()
8282
logger.info("vpn started")
83+
self.manager= manager
8384
completionHandler(nil)
8485
} catch{
8586
logger.error("error starting manager:\(error.description, privacy:.public)")
86-
completionHandler(errorasNSError)
87+
completionHandler(
88+
makeNSError(suffix:"Manager", desc: error.description)
89+
)
8790
}
8891
}
8992
}
@@ -152,9 +155,3 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
152155
tryawaitsetTunnelNetworkSettings(currentSettings)
153156
}
154157
}
155-
156-
enumPTPError:Error{
157-
case alreadyRunning
158-
case missingConfiguration
159-
case missingToken
160-
}

‎Coder Desktop/VPNLib/Util.swift

Lines changed: 11 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
}
@@ -21,3 +21,11 @@ public struct CompletionWrapper<T>: @unchecked Sendable {
2121
block()
2222
}
2323
}
24+
25+
publicfunc makeNSError(suffix:String, code:Int=-1, desc:String)->NSError{
26+
NSError(
27+
domain:"\(Bundle.main.bundleIdentifier!).\(suffix)",
28+
code: code,
29+
userInfo:[NSLocalizedDescriptionKey: desc]
30+
)
31+
}

‎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