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