@@ -33,6 +33,7 @@ import (
33
33
"github.com/coder/coder/v2/coderd/httpapi"
34
34
"github.com/coder/coder/v2/coderd/httpmw"
35
35
"github.com/coder/coder/v2/coderd/jwtutils"
36
+ "github.com/coder/coder/v2/coderd/rbac"
36
37
"github.com/coder/coder/v2/coderd/rbac/policy"
37
38
"github.com/coder/coder/v2/coderd/wspubsub"
38
39
"github.com/coder/coder/v2/codersdk"
@@ -844,31 +845,10 @@ func (api *API) workspaceAgentClientCoordinate(rw http.ResponseWriter, r *http.R
844
845
return
845
846
}
846
847
847
- // Accept a resume_token query parameter to use the same peer ID.
848
- var (
849
- peerID = uuid .New ()
850
- resumeToken = r .URL .Query ().Get ("resume_token" )
851
- )
852
- if resumeToken != "" {
853
- var err error
854
- peerID , err = api .Options .CoordinatorResumeTokenProvider .VerifyResumeToken (ctx , resumeToken )
855
- // If the token is missing the key ID, it's probably an old token in which
856
- // case we just want to generate a new peer ID.
857
- if xerrors .Is (err , jwtutils .ErrMissingKeyID ) {
858
- peerID = uuid .New ()
859
- } else if err != nil {
860
- httpapi .Write (ctx , rw , http .StatusUnauthorized , codersdk.Response {
861
- Message : workspacesdk .CoordinateAPIInvalidResumeToken ,
862
- Detail : err .Error (),
863
- Validations : []codersdk.ValidationError {
864
- {Field : "resume_token" , Detail : workspacesdk .CoordinateAPIInvalidResumeToken },
865
- },
866
- })
867
- return
868
- } else {
869
- api .Logger .Debug (ctx , "accepted coordinate resume token for peer" ,
870
- slog .F ("peer_id" , peerID .String ()))
871
- }
848
+ peerID , err := api .handleResumeToken (ctx , rw , r )
849
+ if err != nil {
850
+ // handleResumeToken has already written the response.
851
+ return
872
852
}
873
853
874
854
api .WebsocketWaitMutex .Lock ()
@@ -891,13 +871,47 @@ func (api *API) workspaceAgentClientCoordinate(rw http.ResponseWriter, r *http.R
891
871
go httpapi .Heartbeat (ctx , conn )
892
872
893
873
defer conn .Close (websocket .StatusNormalClosure , "" )
894
- err = api .TailnetClientService .ServeClient (ctx , version , wsNetConn , peerID , workspaceAgent .ID )
874
+ err = api .TailnetClientService .ServeClient (ctx , version , wsNetConn , tailnet.StreamID {
875
+ Name : "client" ,
876
+ ID : peerID ,
877
+ Auth : tailnet.ClientCoordinateeAuth {
878
+ AgentID : workspaceAgent .ID ,
879
+ },
880
+ })
895
881
if err != nil && ! xerrors .Is (err , io .EOF ) && ! xerrors .Is (err , context .Canceled ) {
896
882
_ = conn .Close (websocket .StatusInternalError , err .Error ())
897
883
return
898
884
}
899
885
}
900
886
887
+ // handleResumeToken accepts a resume_token query parameter to use the same peer ID
888
+ func (api * API ) handleResumeToken (ctx context.Context , rw http.ResponseWriter , r * http.Request ) (peerID uuid.UUID , err error ) {
889
+ peerID = uuid .New ()
890
+ resumeToken := r .URL .Query ().Get ("resume_token" )
891
+ if resumeToken != "" {
892
+ peerID , err = api .Options .CoordinatorResumeTokenProvider .VerifyResumeToken (ctx , resumeToken )
893
+ // If the token is missing the key ID, it's probably an old token in which
894
+ // case we just want to generate a new peer ID.
895
+ if xerrors .Is (err , jwtutils .ErrMissingKeyID ) {
896
+ peerID = uuid .New ()
897
+ err = nil
898
+ } else if err != nil {
899
+ httpapi .Write (ctx , rw , http .StatusUnauthorized , codersdk.Response {
900
+ Message : workspacesdk .CoordinateAPIInvalidResumeToken ,
901
+ Detail : err .Error (),
902
+ Validations : []codersdk.ValidationError {
903
+ {Field : "resume_token" , Detail : workspacesdk .CoordinateAPIInvalidResumeToken },
904
+ },
905
+ })
906
+ return peerID , err
907
+ } else {
908
+ api .Logger .Debug (ctx , "accepted coordinate resume token for peer" ,
909
+ slog .F ("peer_id" , peerID .String ()))
910
+ }
911
+ }
912
+ return peerID , err
913
+ }
914
+
901
915
// @Summary Post workspace agent log source
902
916
// @ID post-workspace-agent-log-source
903
917
// @Security CoderSessionToken
@@ -1469,6 +1483,80 @@ func (api *API) workspaceAgentsExternalAuthListen(ctx context.Context, rw http.R
1469
1483
}
1470
1484
}
1471
1485
1486
+ // @Summary User-scoped tailnet RPC connection
1487
+ // @ID user-scoped-tailnet-rpc-connection
1488
+ // @Security CoderSessionToken
1489
+ // @Tags Agents
1490
+ // @Success 101
1491
+ // @Router /tailnet [get]
1492
+ func (api * API ) tailnetRPCConn (rw http.ResponseWriter , r * http.Request ) {
1493
+ ctx := r .Context ()
1494
+
1495
+ version := "2.0"
1496
+ qv := r .URL .Query ().Get ("version" )
1497
+ if qv != "" {
1498
+ version = qv
1499
+ }
1500
+ if err := proto .CurrentVersion .Validate (version ); err != nil {
1501
+ httpapi .Write (ctx , rw , http .StatusBadRequest , codersdk.Response {
1502
+ Message : "Unknown or unsupported API version" ,
1503
+ Validations : []codersdk.ValidationError {
1504
+ {Field : "version" , Detail : err .Error ()},
1505
+ },
1506
+ })
1507
+ return
1508
+ }
1509
+
1510
+ peerID , err := api .handleResumeToken (ctx , rw , r )
1511
+ if err != nil {
1512
+ // handleResumeToken has already written the response.
1513
+ return
1514
+ }
1515
+
1516
+ // Used to authorize tunnel request
1517
+ sshPrep , err := api .HTTPAuth .AuthorizeSQLFilter (r , policy .ActionSSH , rbac .ResourceWorkspace .Type )
1518
+ if err != nil {
1519
+ httpapi .Write (ctx , rw , http .StatusInternalServerError , codersdk.Response {
1520
+ Message : "Internal error preparing sql filter." ,
1521
+ Detail : err .Error (),
1522
+ })
1523
+ return
1524
+ }
1525
+
1526
+ api .WebsocketWaitMutex .Lock ()
1527
+ api .WebsocketWaitGroup .Add (1 )
1528
+ api .WebsocketWaitMutex .Unlock ()
1529
+ defer api .WebsocketWaitGroup .Done ()
1530
+
1531
+ conn , err := websocket .Accept (rw , r , nil )
1532
+ if err != nil {
1533
+ httpapi .Write (ctx , rw , http .StatusBadRequest , codersdk.Response {
1534
+ Message : "Failed to accept websocket." ,
1535
+ Detail : err .Error (),
1536
+ })
1537
+ return
1538
+ }
1539
+ ctx , wsNetConn := codersdk .WebsocketNetConn (ctx , conn , websocket .MessageBinary )
1540
+ defer wsNetConn .Close ()
1541
+ defer conn .Close (websocket .StatusNormalClosure , "" )
1542
+
1543
+ go httpapi .Heartbeat (ctx , conn )
1544
+ err = api .TailnetClientService .ServeClient (ctx , version , wsNetConn , tailnet.StreamID {
1545
+ Name : "client" ,
1546
+ ID : peerID ,
1547
+ Auth : tailnet.ClientUserCoordinateeAuth {
1548
+ Auth : & rbacAuthorizer {
1549
+ sshPrep : sshPrep ,
1550
+ db : api .Database ,
1551
+ },
1552
+ },
1553
+ })
1554
+ if err != nil && ! xerrors .Is (err , io .EOF ) && ! xerrors .Is (err , context .Canceled ) {
1555
+ _ = conn .Close (websocket .StatusInternalError , err .Error ())
1556
+ return
1557
+ }
1558
+ }
1559
+
1472
1560
// createExternalAuthResponse creates an ExternalAuthResponse based on the
1473
1561
// provider type. This is to support legacy `/workspaceagents/me/gitauth`
1474
1562
// which uses `Username` and `Password`.
0 commit comments