diff --git a/.github/workflows/dependency-review.yaml b/.github/workflows/dependency-review.yaml index b266c34d2e960..9eaef33cec519 100644 --- a/.github/workflows/dependency-review.yaml +++ b/.github/workflows/dependency-review.yaml @@ -18,13 +18,21 @@ jobs: allow-ghsas: 'GHSA-6xf3-5hp7-xqqg' # IronRDP uses MIT/Apache-2.0 but slashes are not recognized by dependency review action allow-dependencies-licenses: >- + pkg:cargo/ironrdp-cliprdr, pkg:cargo/ironrdp-core, pkg:cargo/ironrdp-async, pkg:cargo/ironrdp-connector, + pkg:cargo/ironrdp-displaycontrol, + pkg:cargo/ironrdp-dvc, + pkg:cargo/ironrdp-error, + pkg:cargo/ironrdp-graphics, pkg:cargo/ironrdp-pdu, + pkg:cargo/ironrdp-rdpdr, + pkg:cargo/ironrdp-rdpsnd, pkg:cargo/ironrdp-session, pkg:cargo/ironrdp-svc, pkg:cargo/ironrdp-tokio, + pkg:cargo/ironrdp-tls, pkg:cargo/asn1-rs, pkg:cargo/asn1-rs-derive, pkg:cargo/asn1-rs-impl, diff --git a/Cargo.lock b/Cargo.lock index d9fa0c291965b..718a7919054d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1434,8 +1434,8 @@ dependencies = [ [[package]] name = "ironrdp-async" -version = "0.2.0" -source = "git+https://github.com/Devolutions/IronRDP?rev=2f57fd2de320f58fe240d88a83519255ba94cb73#2f57fd2de320f58fe240d88a83519255ba94cb73" +version = "0.2.1" +source = "git+https://github.com/Devolutions/IronRDP?rev=dd221bf22401c4635798ec012724cba7e6d503b2#dd221bf22401c4635798ec012724cba7e6d503b2" dependencies = [ "bytes", "ironrdp-connector", @@ -1446,8 +1446,8 @@ dependencies = [ [[package]] name = "ironrdp-cliprdr" -version = "0.1.0" -source = "git+https://github.com/Devolutions/IronRDP?rev=2f57fd2de320f58fe240d88a83519255ba94cb73#2f57fd2de320f58fe240d88a83519255ba94cb73" +version = "0.1.1" +source = "git+https://github.com/Devolutions/IronRDP?rev=dd221bf22401c4635798ec012724cba7e6d503b2#dd221bf22401c4635798ec012724cba7e6d503b2" dependencies = [ "bitflags 2.6.0", "ironrdp-core", @@ -1459,8 +1459,8 @@ dependencies = [ [[package]] name = "ironrdp-connector" -version = "0.2.1" -source = "git+https://github.com/Devolutions/IronRDP?rev=2f57fd2de320f58fe240d88a83519255ba94cb73#2f57fd2de320f58fe240d88a83519255ba94cb73" +version = "0.2.2" +source = "git+https://github.com/Devolutions/IronRDP?rev=dd221bf22401c4635798ec012724cba7e6d503b2#dd221bf22401c4635798ec012724cba7e6d503b2" dependencies = [ "ironrdp-core", "ironrdp-error", @@ -1477,16 +1477,16 @@ dependencies = [ [[package]] name = "ironrdp-core" -version = "0.1.1" -source = "git+https://github.com/Devolutions/IronRDP?rev=2f57fd2de320f58fe240d88a83519255ba94cb73#2f57fd2de320f58fe240d88a83519255ba94cb73" +version = "0.1.2" +source = "git+https://github.com/Devolutions/IronRDP?rev=dd221bf22401c4635798ec012724cba7e6d503b2#dd221bf22401c4635798ec012724cba7e6d503b2" dependencies = [ "ironrdp-error", ] [[package]] name = "ironrdp-displaycontrol" -version = "0.1.0" -source = "git+https://github.com/Devolutions/IronRDP?rev=2f57fd2de320f58fe240d88a83519255ba94cb73#2f57fd2de320f58fe240d88a83519255ba94cb73" +version = "0.1.1" +source = "git+https://github.com/Devolutions/IronRDP?rev=dd221bf22401c4635798ec012724cba7e6d503b2#dd221bf22401c4635798ec012724cba7e6d503b2" dependencies = [ "ironrdp-core", "ironrdp-dvc", @@ -1497,8 +1497,8 @@ dependencies = [ [[package]] name = "ironrdp-dvc" -version = "0.1.0" -source = "git+https://github.com/Devolutions/IronRDP?rev=2f57fd2de320f58fe240d88a83519255ba94cb73#2f57fd2de320f58fe240d88a83519255ba94cb73" +version = "0.1.1" +source = "git+https://github.com/Devolutions/IronRDP?rev=dd221bf22401c4635798ec012724cba7e6d503b2#dd221bf22401c4635798ec012724cba7e6d503b2" dependencies = [ "ironrdp-core", "ironrdp-pdu", @@ -1509,13 +1509,13 @@ dependencies = [ [[package]] name = "ironrdp-error" -version = "0.1.0" -source = "git+https://github.com/Devolutions/IronRDP?rev=2f57fd2de320f58fe240d88a83519255ba94cb73#2f57fd2de320f58fe240d88a83519255ba94cb73" +version = "0.1.1" +source = "git+https://github.com/Devolutions/IronRDP?rev=dd221bf22401c4635798ec012724cba7e6d503b2#dd221bf22401c4635798ec012724cba7e6d503b2" [[package]] name = "ironrdp-graphics" -version = "0.1.0" -source = "git+https://github.com/Devolutions/IronRDP?rev=2f57fd2de320f58fe240d88a83519255ba94cb73#2f57fd2de320f58fe240d88a83519255ba94cb73" +version = "0.1.1" +source = "git+https://github.com/Devolutions/IronRDP?rev=dd221bf22401c4635798ec012724cba7e6d503b2#dd221bf22401c4635798ec012724cba7e6d503b2" dependencies = [ "bit_field", "bitflags 2.6.0", @@ -1531,8 +1531,8 @@ dependencies = [ [[package]] name = "ironrdp-pdu" -version = "0.1.1" -source = "git+https://github.com/Devolutions/IronRDP?rev=2f57fd2de320f58fe240d88a83519255ba94cb73#2f57fd2de320f58fe240d88a83519255ba94cb73" +version = "0.1.2" +source = "git+https://github.com/Devolutions/IronRDP?rev=dd221bf22401c4635798ec012724cba7e6d503b2#dd221bf22401c4635798ec012724cba7e6d503b2" dependencies = [ "bit_field", "bitflags 2.6.0", @@ -1554,8 +1554,8 @@ dependencies = [ [[package]] name = "ironrdp-rdpdr" -version = "0.1.0" -source = "git+https://github.com/Devolutions/IronRDP?rev=2f57fd2de320f58fe240d88a83519255ba94cb73#2f57fd2de320f58fe240d88a83519255ba94cb73" +version = "0.1.1" +source = "git+https://github.com/Devolutions/IronRDP?rev=dd221bf22401c4635798ec012724cba7e6d503b2#dd221bf22401c4635798ec012724cba7e6d503b2" dependencies = [ "bitflags 2.6.0", "ironrdp-core", @@ -1567,8 +1567,8 @@ dependencies = [ [[package]] name = "ironrdp-rdpsnd" -version = "0.1.0" -source = "git+https://github.com/Devolutions/IronRDP?rev=2f57fd2de320f58fe240d88a83519255ba94cb73#2f57fd2de320f58fe240d88a83519255ba94cb73" +version = "0.1.1" +source = "git+https://github.com/Devolutions/IronRDP?rev=dd221bf22401c4635798ec012724cba7e6d503b2#dd221bf22401c4635798ec012724cba7e6d503b2" dependencies = [ "bitflags 2.6.0", "ironrdp-core", @@ -1579,8 +1579,8 @@ dependencies = [ [[package]] name = "ironrdp-session" -version = "0.2.0" -source = "git+https://github.com/Devolutions/IronRDP?rev=2f57fd2de320f58fe240d88a83519255ba94cb73#2f57fd2de320f58fe240d88a83519255ba94cb73" +version = "0.2.1" +source = "git+https://github.com/Devolutions/IronRDP?rev=dd221bf22401c4635798ec012724cba7e6d503b2#dd221bf22401c4635798ec012724cba7e6d503b2" dependencies = [ "ironrdp-connector", "ironrdp-core", @@ -1595,8 +1595,8 @@ dependencies = [ [[package]] name = "ironrdp-svc" -version = "0.1.1" -source = "git+https://github.com/Devolutions/IronRDP?rev=2f57fd2de320f58fe240d88a83519255ba94cb73#2f57fd2de320f58fe240d88a83519255ba94cb73" +version = "0.1.2" +source = "git+https://github.com/Devolutions/IronRDP?rev=dd221bf22401c4635798ec012724cba7e6d503b2#dd221bf22401c4635798ec012724cba7e6d503b2" dependencies = [ "bitflags 2.6.0", "ironrdp-core", @@ -1605,8 +1605,8 @@ dependencies = [ [[package]] name = "ironrdp-tls" -version = "0.1.0" -source = "git+https://github.com/Devolutions/IronRDP?rev=2f57fd2de320f58fe240d88a83519255ba94cb73#2f57fd2de320f58fe240d88a83519255ba94cb73" +version = "0.1.1" +source = "git+https://github.com/Devolutions/IronRDP?rev=dd221bf22401c4635798ec012724cba7e6d503b2#dd221bf22401c4635798ec012724cba7e6d503b2" dependencies = [ "tokio", "tokio-rustls", @@ -1615,8 +1615,8 @@ dependencies = [ [[package]] name = "ironrdp-tokio" -version = "0.2.0" -source = "git+https://github.com/Devolutions/IronRDP?rev=2f57fd2de320f58fe240d88a83519255ba94cb73#2f57fd2de320f58fe240d88a83519255ba94cb73" +version = "0.2.1" +source = "git+https://github.com/Devolutions/IronRDP?rev=dd221bf22401c4635798ec012724cba7e6d503b2#dd221bf22401c4635798ec012724cba7e6d503b2" dependencies = [ "bytes", "ironrdp-async", diff --git a/Cargo.toml b/Cargo.toml index cf40926486bbf..d0d0aa4f71a26 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,18 +21,18 @@ lto = "thin" [workspace.dependencies] # Note: To use a local IronRDP repository as a crate (for example, ironrdp-cliprdr), define the dependency as follows: # ironrdp-cliprdr = { path = "/path/to/local/IronRDP/crates/ironrdp-cliprdr" } -ironrdp-cliprdr = { git = "https://github.com/Devolutions/IronRDP", rev = "2f57fd2de320f58fe240d88a83519255ba94cb73" } -ironrdp-connector = { git = "https://github.com/Devolutions/IronRDP", rev = "2f57fd2de320f58fe240d88a83519255ba94cb73" } -ironrdp-core = { git = "https://github.com/Devolutions/IronRDP", rev = "2f57fd2de320f58fe240d88a83519255ba94cb73" } -ironrdp-displaycontrol = { git = "https://github.com/Devolutions/IronRDP", rev = "2f57fd2de320f58fe240d88a83519255ba94cb73" } -ironrdp-dvc = { git = "https://github.com/Devolutions/IronRDP", rev = "2f57fd2de320f58fe240d88a83519255ba94cb73" } -ironrdp-graphics = { git = "https://github.com/Devolutions/IronRDP", rev = "2f57fd2de320f58fe240d88a83519255ba94cb73" } -ironrdp-pdu = { git = "https://github.com/Devolutions/IronRDP", rev = "2f57fd2de320f58fe240d88a83519255ba94cb73" } -ironrdp-rdpdr = { git = "https://github.com/Devolutions/IronRDP", rev = "2f57fd2de320f58fe240d88a83519255ba94cb73" } -ironrdp-rdpsnd = { git = "https://github.com/Devolutions/IronRDP", rev = "2f57fd2de320f58fe240d88a83519255ba94cb73" } -ironrdp-session = { git = "https://github.com/Devolutions/IronRDP", rev = "2f57fd2de320f58fe240d88a83519255ba94cb73" } -ironrdp-svc = { git = "https://github.com/Devolutions/IronRDP", rev = "2f57fd2de320f58fe240d88a83519255ba94cb73" } -ironrdp-tls = { git = "https://github.com/Devolutions/IronRDP", rev = "2f57fd2de320f58fe240d88a83519255ba94cb73", features = [ +ironrdp-cliprdr = { git = "https://github.com/Devolutions/IronRDP", rev = "dd221bf22401c4635798ec012724cba7e6d503b2" } +ironrdp-connector = { git = "https://github.com/Devolutions/IronRDP", rev = "dd221bf22401c4635798ec012724cba7e6d503b2" } +ironrdp-core = { git = "https://github.com/Devolutions/IronRDP", rev = "dd221bf22401c4635798ec012724cba7e6d503b2" } +ironrdp-displaycontrol = { git = "https://github.com/Devolutions/IronRDP", rev = "dd221bf22401c4635798ec012724cba7e6d503b2" } +ironrdp-dvc = { git = "https://github.com/Devolutions/IronRDP", rev = "dd221bf22401c4635798ec012724cba7e6d503b2" } +ironrdp-graphics = { git = "https://github.com/Devolutions/IronRDP", rev = "dd221bf22401c4635798ec012724cba7e6d503b2" } +ironrdp-pdu = { git = "https://github.com/Devolutions/IronRDP", rev = "dd221bf22401c4635798ec012724cba7e6d503b2" } +ironrdp-rdpdr = { git = "https://github.com/Devolutions/IronRDP", rev = "dd221bf22401c4635798ec012724cba7e6d503b2" } +ironrdp-rdpsnd = { git = "https://github.com/Devolutions/IronRDP", rev = "dd221bf22401c4635798ec012724cba7e6d503b2" } +ironrdp-session = { git = "https://github.com/Devolutions/IronRDP", rev = "dd221bf22401c4635798ec012724cba7e6d503b2" } +ironrdp-svc = { git = "https://github.com/Devolutions/IronRDP", rev = "dd221bf22401c4635798ec012724cba7e6d503b2" } +ironrdp-tls = { git = "https://github.com/Devolutions/IronRDP", rev = "dd221bf22401c4635798ec012724cba7e6d503b2", features = [ "rustls", ] } -ironrdp-tokio = { git = "https://github.com/Devolutions/IronRDP", rev = "2f57fd2de320f58fe240d88a83519255ba94cb73" } +ironrdp-tokio = { git = "https://github.com/Devolutions/IronRDP", rev = "dd221bf22401c4635798ec012724cba7e6d503b2" } diff --git a/api/types/desktop.go b/api/types/desktop.go index a6455484e2daa..d3fb9a2de3b57 100644 --- a/api/types/desktop.go +++ b/api/types/desktop.go @@ -543,3 +543,11 @@ func checkNameAndScreenSize(name string, screenSize *Resolution) error { } return nil } + +// RDPLicenseKey is struct for retrieving licenses from backend cache, used only internally +type RDPLicenseKey struct { + Version uint32 // e.g. 0x000a0002 + Issuer string // e.g. example.com + Company string // e.g. Example Corporation + ProductID string // e.g. A02 +} diff --git a/lib/auth/storage/storage.go b/lib/auth/storage/storage.go index 76db71182e982..625cc393f8698 100644 --- a/lib/auth/storage/storage.go +++ b/lib/auth/storage/storage.go @@ -27,7 +27,9 @@ package storage import ( "context" "encoding/json" + "strconv" "strings" + "time" "github.com/coreos/go-semver/semver" "github.com/gravitational/trace" @@ -233,6 +235,42 @@ func (p *ProcessStorage) WriteTeleportVersion(ctx context.Context, version *semv return nil } +func rdpLicenseKey(key *types.RDPLicenseKey) backend.Key { + return backend.NewKey("rdplicense", key.Issuer, strconv.Itoa(int(key.Version)), key.Company, key.ProductID) +} + +type rdpLicense struct { + Data []byte `json:"data"` +} + +// WriteRDPLicense writes an RDP license to local storage. +func (p *ProcessStorage) WriteRDPLicense(ctx context.Context, key *types.RDPLicenseKey, license []byte) error { + value, err := json.Marshal(rdpLicense{Data: license}) + if err != nil { + return trace.Wrap(err) + } + item := backend.Item{ + Key: rdpLicenseKey(key), + Value: value, + Expires: p.BackendStorage.Clock().Now().Add(28 * 24 * time.Hour), + } + _, err = p.stateStorage.Put(ctx, item) + return trace.Wrap(err) +} + +// ReadRDPLicense reads a previously obtained license from storage. +func (p *ProcessStorage) ReadRDPLicense(ctx context.Context, key *types.RDPLicenseKey) ([]byte, error) { + item, err := p.stateStorage.Get(ctx, rdpLicenseKey(key)) + if err != nil { + return nil, trace.Wrap(err) + } + license := rdpLicense{} + if err := json.Unmarshal(item.Value, &license); err != nil { + return nil, trace.Wrap(err) + } + return license.Data, nil +} + // ReadLocalIdentity reads, parses and returns the given pub/pri key + cert from the // key storage (dataDir). func ReadLocalIdentity(dataDir string, id state.IdentityID) (*state.Identity, error) { diff --git a/lib/auth/storage/storage_test.go b/lib/auth/storage/storage_test.go new file mode 100644 index 0000000000000..42302101c7036 --- /dev/null +++ b/lib/auth/storage/storage_test.go @@ -0,0 +1,72 @@ +// Teleport +// Copyright (C) 2025 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package storage + +import ( + "context" + "testing" + + "github.com/gravitational/trace" + "github.com/stretchr/testify/require" + + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/backend/memory" +) + +func TestRDPLicense(t *testing.T) { + ctx := context.Background() + mem, err := memory.New(memory.Config{}) + require.NoError(t, err) + storage := ProcessStorage{ + BackendStorage: mem, + stateStorage: mem, + } + + _, err = storage.ReadRDPLicense(ctx, &types.RDPLicenseKey{ + Version: 1, + Issuer: "issuer", + Company: "company", + ProductID: "productID", + }) + require.True(t, trace.IsNotFound(err)) + + licenseData := []byte{1, 2, 3} + err = storage.WriteRDPLicense(ctx, &types.RDPLicenseKey{ + Version: 1, + Issuer: "issuer", + Company: "company", + ProductID: "productID", + }, licenseData) + require.NoError(t, err) + + _, err = storage.ReadRDPLicense(ctx, &types.RDPLicenseKey{ + Version: 2, + Issuer: "issuer", + Company: "company", + ProductID: "productID", + }) + require.True(t, trace.IsNotFound(err)) + + license, err := storage.ReadRDPLicense(ctx, &types.RDPLicenseKey{ + Version: 1, + Issuer: "issuer", + Company: "company", + ProductID: "productID", + }) + require.NoError(t, err) + require.Equal(t, licenseData, license) +} diff --git a/lib/service/desktop.go b/lib/service/desktop.go index c4950a882a8ba..943bedad32f12 100644 --- a/lib/service/desktop.go +++ b/lib/service/desktop.go @@ -210,6 +210,7 @@ func (process *TeleportProcess) initWindowsDesktopServiceRegistered(logger *slog srv, err := desktop.NewWindowsService(desktop.WindowsServiceConfig{ DataDir: process.Config.DataDir, + LicenseStore: process.storage, Logger: process.logger.With(teleport.ComponentKey, teleport.Component(teleport.ComponentWindowsDesktop, process.id)), Clock: process.Clock, Authorizer: authorizer, diff --git a/lib/srv/desktop/rdp/rdpclient/client.go b/lib/srv/desktop/rdp/rdpclient/client.go index c7be6dd702016..b086dda472ce9 100644 --- a/lib/srv/desktop/rdp/rdpclient/client.go +++ b/lib/srv/desktop/rdp/rdpclient/client.go @@ -72,6 +72,7 @@ import "C" import ( "context" + "encoding/binary" "fmt" "os" "runtime/cgo" @@ -80,6 +81,7 @@ import ( "time" "unsafe" + "github.com/google/uuid" "github.com/gravitational/trace" "github.com/sirupsen/logrus" @@ -329,6 +331,19 @@ func (c *Client) startRustRDP(ctx context.Context) error { return trace.BadParameter("user key was nil") } + hostID, err := uuid.Parse(c.cfg.HostID) + if err != nil { + return trace.Wrap(err) + } + + nextHostID := hostID[:] + cHostID := [4]C.uint32_t{} + for i := 0; i < len(cHostID); i++ { + const uint32Len = 4 + cHostID[i] = (C.uint32_t)(binary.LittleEndian.Uint32(nextHostID[:uint32Len])) + nextHostID = nextHostID[uint32Len:] + } + res := C.client_run( C.uintptr_t(c.handle), C.CGOConnectParams{ @@ -349,6 +364,7 @@ func (c *Client) startRustRDP(ctx context.Context) error { allow_clipboard: C.bool(c.cfg.AllowClipboard), allow_directory_sharing: C.bool(c.cfg.AllowDirectorySharing), show_desktop_wallpaper: C.bool(c.cfg.ShowDesktopWallpaper), + client_id: cHostID, }, ) @@ -750,6 +766,106 @@ func toClient(handle C.uintptr_t) (value *Client, err error) { return cgo.Handle(handle).Value().(*Client), nil } +//export cgo_read_rdp_license +func cgo_read_rdp_license(handle C.uintptr_t, req *C.CGOLicenseRequest, data_out **C.uint8_t, len_out *C.size_t) C.CGOErrCode { + *data_out = nil + *len_out = 0 + + client, err := toClient(handle) + if err != nil { + return C.ErrCodeFailure + } + + issuer := C.GoString(req.issuer) + company := C.GoString(req.company) + productID := C.GoString(req.product_id) + + license, err := client.readRDPLicense(context.Background(), types.RDPLicenseKey{ + Version: uint32(req.version), + Issuer: issuer, + Company: company, + ProductID: productID, + }) + if trace.IsNotFound(err) { + return C.ErrCodeNotFound + } else if err != nil { + return C.ErrCodeFailure + } + + // in this case, we expect the caller to use cgo_free_rdp_license + // when the data is no longer needed + *data_out = (*C.uint8_t)(C.CBytes(license)) + *len_out = C.size_t(len(license)) + return C.ErrCodeSuccess +} + +//export cgo_free_rdp_license +func cgo_free_rdp_license(p *C.uint8_t) { + C.free(unsafe.Pointer(p)) +} + +//export cgo_write_rdp_license +func cgo_write_rdp_license(handle C.uintptr_t, req *C.CGOLicenseRequest, data *C.uint8_t, length C.size_t) C.CGOErrCode { + client, err := toClient(handle) + if err != nil { + return C.ErrCodeFailure + } + + issuer := C.GoString(req.issuer) + company := C.GoString(req.company) + productID := C.GoString(req.product_id) + + licenseData := C.GoBytes(unsafe.Pointer(data), C.int(length)) + + err = client.writeRDPLicense(context.Background(), types.RDPLicenseKey{ + Version: uint32(req.version), + Issuer: issuer, + Company: company, + ProductID: productID, + }, licenseData) + if err != nil { + return C.ErrCodeFailure + } + + return C.ErrCodeSuccess +} + +func (c *Client) readRDPLicense(ctx context.Context, key types.RDPLicenseKey) ([]byte, error) { + log := c.cfg.Logger.With( + "issuer", key.Issuer, + "company", key.Company, + "version", key.Version, + "product", key.ProductID, + ) + + license, err := c.cfg.LicenseStore.ReadRDPLicense(ctx, &key) + switch { + case trace.IsNotFound(err): + log.InfoContext(ctx, "existing RDP license not found") + case err != nil: + log.ErrorContext(ctx, "could not look up existing RDP license", "error", err) + case len(license) > 0: + log.InfoContext(ctx, "found existing RDP license") + } + + return license, trace.Wrap(err) +} + +func (c *Client) writeRDPLicense(ctx context.Context, key types.RDPLicenseKey, license []byte) error { + log := c.cfg.Logger.With( + "issuer", key.Issuer, + "company", key.Company, + "version", key.Version, + "product", key.ProductID, + ) + log.InfoContext(ctx, "writing RDP license to storage") + err := c.cfg.LicenseStore.WriteRDPLicense(ctx, &key, license) + if err != nil { + log.ErrorContext(ctx, "could not write RDP license", "error", err) + } + return trace.Wrap(err) +} + //export cgo_handle_fastpath_pdu func cgo_handle_fastpath_pdu(handle C.uintptr_t, data *C.uint8_t, length C.uint32_t) C.CGOErrCode { goData := asRustBackedSlice(data, int(length)) diff --git a/lib/srv/desktop/rdp/rdpclient/client_common.go b/lib/srv/desktop/rdp/rdpclient/client_common.go index 80e192e37427c..51a54308685c5 100644 --- a/lib/srv/desktop/rdp/rdpclient/client_common.go +++ b/lib/srv/desktop/rdp/rdpclient/client_common.go @@ -27,14 +27,25 @@ import ( "github.com/gravitational/trace" + "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/srv/desktop/tdp" ) +// LicenseStore implements client-side license storage for Microsoft +// Remote Desktop Services (RDS) licenses. +type LicenseStore interface { + WriteRDPLicense(ctx context.Context, key *types.RDPLicenseKey, license []byte) error + ReadRDPLicense(ctx context.Context, key *types.RDPLicenseKey) ([]byte, error) +} + // Config for creating a new Client. type Config struct { // Addr is the network address of the RDP server, in the form host:port. Addr string + LicenseStore LicenseStore + HostID string + // UserCertGenerator generates user certificates for RDP authentication. GenerateUserCert GenerateUserCertFn CertTTL time.Duration diff --git a/lib/srv/desktop/rdp/rdpclient/src/client.rs b/lib/srv/desktop/rdp/rdpclient/src/client.rs index 3dae0fc453b59..3f6d6423dfa59 100644 --- a/lib/srv/desktop/rdp/rdpclient/src/client.rs +++ b/lib/srv/desktop/rdp/rdpclient/src/client.rs @@ -74,6 +74,7 @@ use tokio::sync::mpsc::{channel, error::SendError, Receiver, Sender}; use tokio::task::JoinError; // Export this for crate level use. use crate::cliprdr::{ClipboardFn, TeleportCliprdrBackend}; +use crate::license::GoLicenseCache; use crate::rdpdr::scard::SCARD_DEVICE_ID; use crate::rdpdr::TeleportRdpdrBackend; use crate::ssl::TlsStream; @@ -150,7 +151,7 @@ impl Client { let mut rng = rand_chacha::ChaCha20Rng::from_entropy(); let pin = format!("{:08}", rng.gen_range(0i32..=99999999i32)); - let connector_config = create_config(¶ms, pin.clone()); + let connector_config = create_config(¶ms, pin.clone(), cgo_handle); // Create a channel for sending/receiving function calls to/from the Client. let (client_handle, function_receiver) = ClientHandle::new(100); @@ -1401,7 +1402,7 @@ impl FunctionReceiver { type RdpReadStream = Framed>>>; type RdpWriteStream = Framed>>>; -fn create_config(params: &ConnectParams, pin: String) -> Config { +fn create_config(params: &ConnectParams, pin: String, cgo_handle: CgoHandle) -> Config { Config { desktop_size: DesktopSize { width: params.screen_width, @@ -1456,6 +1457,8 @@ fn create_config(params: &ConnectParams, pin: String) -> Config { PerformanceFlags::empty() }, desktop_scale_factor: 0, + license_cache: Some(Arc::new(GoLicenseCache { cgo_handle })), + hardware_id: Some(params.client_id), } } @@ -1474,6 +1477,7 @@ pub struct ConnectParams { pub show_desktop_wallpaper: bool, pub ad: bool, pub nla: bool, + pub client_id: [u32; 4], } #[derive(Debug)] diff --git a/lib/srv/desktop/rdp/rdpclient/src/lib.rs b/lib/srv/desktop/rdp/rdpclient/src/lib.rs index c82663f704c73..55ac5f72a77d9 100644 --- a/lib/srv/desktop/rdp/rdpclient/src/lib.rs +++ b/lib/srv/desktop/rdp/rdpclient/src/lib.rs @@ -45,6 +45,7 @@ use std::ptr; use util::{from_c_string, from_go_array}; pub mod client; mod cliprdr; +mod license; mod network_client; mod piv; mod rdpdr; @@ -124,6 +125,7 @@ pub unsafe extern "C" fn client_run(cgo_handle: CgoHandle, params: CGOConnectPar allow_clipboard: params.allow_clipboard, allow_directory_sharing: params.allow_directory_sharing, show_desktop_wallpaper: params.show_desktop_wallpaper, + client_id: params.client_id, }, ) { Ok(res) => CGOResult { @@ -509,6 +511,7 @@ pub struct CGOConnectParams { allow_clipboard: bool, allow_directory_sharing: bool, show_desktop_wallpaper: bool, + client_id: [u32; 4], } /// CGOKeyboardEvent is a CGO-compatible version of KeyboardEvent that we pass back to Go. @@ -579,6 +582,7 @@ pub enum CGOErrCode { ErrCodeSuccess = 0, ErrCodeFailure = 1, ErrCodeClientPtr = 2, + ErrCodeNotFound = 3, } #[repr(C)] @@ -733,6 +737,19 @@ pub type CGOSharedDirectoryTruncateResponse = SharedDirectoryTruncateResponse; // These functions are defined on the Go side. // Look for functions with '//export funcname' comments. extern "C" { + fn cgo_free_rdp_license(data: *mut u8); + fn cgo_read_rdp_license( + cgo_handle: CgoHandle, + req: *mut CGOLicenseRequest, + data_out: *mut *mut u8, + len_out: *mut usize, + ) -> CGOErrCode; + fn cgo_write_rdp_license( + cgo_handle: CgoHandle, + req: *mut CGOLicenseRequest, + data: *mut u8, + length: usize, + ) -> CGOErrCode; fn cgo_handle_remote_copy(cgo_handle: CgoHandle, data: *mut u8, len: u32) -> CGOErrCode; fn cgo_handle_fastpath_pdu(cgo_handle: CgoHandle, data: *mut u8, len: u32) -> CGOErrCode; fn cgo_handle_rdp_connection_activated( @@ -784,3 +801,11 @@ extern "C" { /// /// [cgo.Handle]: https://pkg.go.dev/runtime/cgo#Handle type CgoHandle = usize; + +#[repr(C)] +pub struct CGOLicenseRequest { + version: u32, + issuer: *const c_char, + company: *const c_char, + product_id: *const c_char, +} diff --git a/lib/srv/desktop/rdp/rdpclient/src/license.rs b/lib/srv/desktop/rdp/rdpclient/src/license.rs new file mode 100644 index 0000000000000..3636d2d4c6eb3 --- /dev/null +++ b/lib/srv/desktop/rdp/rdpclient/src/license.rs @@ -0,0 +1,85 @@ +// Teleport +// Copyright (C) 2025 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +use crate::{ + cgo_free_rdp_license, cgo_read_rdp_license, cgo_write_rdp_license, CGOErrCode, + CGOLicenseRequest, CgoHandle, +}; +use ironrdp_connector::{custom_err, general_err, ConnectorError, ConnectorResult, LicenseCache}; +use ironrdp_pdu::rdp::server_license::LicenseInformation; +use picky_krb::negoex::NegoexDataType; +use std::ffi::{CString, NulError}; +use std::{ptr, slice}; + +#[derive(Debug)] +pub(crate) struct GoLicenseCache { + pub(crate) cgo_handle: CgoHandle, +} + +fn conversion_error(e: NulError) -> ConnectorError { + custom_err!("conversion error", e) +} + +impl LicenseCache for GoLicenseCache { + fn get_license(&self, license_info: LicenseInformation) -> ConnectorResult>> { + let issuer = CString::new(license_info.scope).map_err(conversion_error)?; + let company = CString::new(license_info.company_name).map_err(conversion_error)?; + let product_id = CString::new(license_info.product_id).map_err(conversion_error)?; + let mut req = CGOLicenseRequest { + version: license_info.version, + issuer: issuer.as_ptr(), + company: company.as_ptr(), + product_id: product_id.as_ptr(), + }; + let mut data: *mut u8 = ptr::null_mut(); + let mut size = 0usize; + unsafe { + match cgo_read_rdp_license(self.cgo_handle, &mut req, &mut data, &mut size) { + CGOErrCode::ErrCodeSuccess => { + let license = slice::from_raw_parts_mut(data, size).to_vec(); + cgo_free_rdp_license(data); + Ok(Some(license)) + } + CGOErrCode::ErrCodeFailure => Err(general_err!("error retrieving license")), + CGOErrCode::ErrCodeClientPtr => Err(general_err!("invalid client pointer")), + CGOErrCode::ErrCodeNotFound => Ok(None), + } + } + } + + fn store_license(&self, mut license_info: LicenseInformation) -> ConnectorResult<()> { + let issuer = CString::new(license_info.scope).map_err(conversion_error)?; + let company = CString::new(license_info.company_name).map_err(conversion_error)?; + let product_id = CString::new(license_info.product_id).map_err(conversion_error)?; + let mut req = CGOLicenseRequest { + version: license_info.version, + issuer: issuer.as_ptr(), + company: company.as_ptr(), + product_id: product_id.as_ptr(), + }; + unsafe { + match cgo_write_rdp_license( + self.cgo_handle, + &mut req, + license_info.license_info.as_mut_ptr(), + license_info.license_info.size(), + ) { + CGOErrCode::ErrCodeSuccess => Ok(()), + _ => Err(general_err!("error storing license")), + } + } + } +} diff --git a/lib/srv/desktop/windows_server.go b/lib/srv/desktop/windows_server.go index 5568d2b20d5a1..670bfb248e06e 100644 --- a/lib/srv/desktop/windows_server.go +++ b/lib/srv/desktop/windows_server.go @@ -160,8 +160,9 @@ type WindowsServiceConfig struct { // Logger is the logger for the service. Logger *slog.Logger // Clock provides current time. - Clock clockwork.Clock - DataDir string + Clock clockwork.Clock + DataDir string + LicenseStore rdpclient.LicenseStore // Authorizer is used to authorize requests. Authorizer authz.Authorizer // LockWatcher is used to monitor for new locks. @@ -958,7 +959,9 @@ func (s *WindowsService) connectRDP(ctx context.Context, log *slog.Logger, tdpCo //nolint:staticcheck // SA4023. False positive, depends on build tags. rdpc, err := rdpclient.New(rdpclient.Config{ - Logger: log, + LicenseStore: s.cfg.LicenseStore, + HostID: s.cfg.Heartbeat.HostUUID, + Logger: log, GenerateUserCert: func(ctx context.Context, username string, ttl time.Duration) (certDER, keyDER []byte, err error) { return s.generateUserCert(ctx, username, ttl, desktop, createUsers, groups) },