Compare commits

...

69 Commits

Author SHA1 Message Date
Lierfang Support Team
564ee12988 update version
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
Docker Image CI / build (push) Has been cancelled
Go / Build (push) Has been cancelled
2025-06-13 15:57:04 +08:00
14cbe3b9f2 同步动态rdp网关功能
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
Docker Image CI / build (push) Has been cancelled
Go / Build (push) Has been cancelled
2025-05-29 15:27:17 +08:00
fe3f8885c6 增加apiurl字段 2025-04-02 11:23:30 +08:00
760ae5cf8d 增加git忽略 2025-04-02 11:13:15 +08:00
a5be141bac 增加连接信息 2025-04-02 11:08:17 +08:00
7ac73b9489 增加服务端验证
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
Docker Image CI / build (push) Has been cancelled
Go / Build (push) Has been cancelled
2025-04-01 22:12:24 +08:00
f72a4416e3 Add debian support
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
Docker Image CI / build (push) Has been cancelled
Go / Build (push) Has been cancelled
2025-02-14 21:49:34 +08:00
Firman Alamsyah
80604075d0
docs: Add link to docker hub (#128)
Added link to docker hub
2025-01-15 23:24:19 +01:00
dependabot[bot]
c8312348fd
Bump golang.org/x/crypto from 0.21.0 to 0.31.0 (#130)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.21.0 to 0.31.0.
- [Commits](https://github.com/golang/crypto/compare/v0.21.0...v0.31.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-15 23:23:58 +01:00
m7913d
372dc43ef2
Support for NTLM authentication added (#109)
* Support for NTLM authentication added

To support NTLM authentication, a database is added as an authentication source.
Currently, only the configuration file is supported as a database.
Database authentication supports Basic and NTLM authentication protcols.

ServerConfig.BasicAuthEnabled renamed to LocalEnabled as Basic auth can be used with NTLM or Local.
2024-04-24 14:12:41 +02:00
dependabot[bot]
7472c7b2c1
Bump golang.org/x/net from 0.22.0 to 0.23.0 (#111)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.22.0 to 0.23.0.
- [Commits](https://github.com/golang/net/compare/v0.22.0...v0.23.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-19 22:16:43 +02:00
Bolke de Bruin
d76ccf324a Let's not leak 2024-04-12 12:44:07 +02:00
Bolke de Bruin
9c6d056d69 Use jose v4 and make clearer and fix signing/encryption 2024-04-12 12:33:46 +02:00
Bolke de Bruin
bc36b2b0cb Fix b parsing 2024-03-30 12:12:55 +01:00
Bolke de Bruin
a963ca0d00 Fix parsing of bool to int 2024-03-30 12:07:28 +01:00
Bolke de Bruin
531af7d2d5 Fix run script to forward command line options 2024-03-30 11:30:28 +01:00
Bolke de Bruin
2e7080e674 Make sure right go is pulled 2024-03-30 11:14:55 +01:00
Bolke de Bruin
d99316d947 Update workflows 2024-03-30 11:07:47 +01:00
Bolke de Bruin
c5ecf67f29 Fix docker release naming 2024-03-30 10:58:42 +01:00
Bolke de Bruin
2d9a368fc4 More details about the clients 2024-03-21 16:36:47 +01:00
Bolke de Bruin
5d30deb48c Add untested explicit settings in rdp file 2024-03-21 16:22:14 +01:00
Bolke de Bruin
95a8623cb6 Change remoteapplicationmode to default to false as that seems to be the case 2024-03-21 15:35:45 +01:00
Bolke de Bruin
f95ff94a76 Add note about mstsc 2024-03-20 11:23:34 +01:00
Bolke de Bruin
447599b92a Add request uri for better debugging 2024-03-20 10:56:58 +01:00
Bolke de Bruin
37c14c4615 Add local docker compose 2024-03-19 13:49:41 +01:00
Bolke de Bruin
a7ea3121d9 Only split when required 2024-03-19 10:23:57 +01:00
Bolke de Bruin
7bf2a59838 Testing 2024-03-19 10:20:14 +01:00
Bolke de Bruin
ec63346c8a Handle arrays in env variables 2024-03-19 09:42:19 +01:00
Bolke de Bruin
dc60652b83 Add docker compose options 2024-03-19 09:30:03 +01:00
Bolke de Bruin
79f8d8f545 Add local PAM docker-compose 2024-03-19 09:25:15 +01:00
Bolke de Bruin
46620c87b7 upgrades 2024-03-18 15:27:30 +01:00
Bolke de Bruin
f70348c18a Upgrade 2024-03-18 14:27:46 +01:00
Bolke de Bruin
e477717716 Update 2024-03-18 14:17:36 +01:00
Bolke de Bruin
c45d57f0c1 Fix readme 2024-03-18 14:15:55 +01:00
Bolke de Bruin
a21b266e0d Add run 2024-03-18 14:11:40 +01:00
Bolke de Bruin
841c688df0 Fix docker 2024-03-18 14:10:06 +01:00
Bolke de Bruin
e939275a8a Make dynamic 2024-03-18 14:09:22 +01:00
Bolke de Bruin
d532838a25 Update dep 2024-03-18 14:05:13 +01:00
Bolke de Bruin
1b1d54b572 Debug 2024-03-18 14:03:18 +01:00
Bolke de Bruin
91e382c586 Move to more flexibility in image 2024-03-18 13:36:41 +01:00
Bolke de Bruin
f75321f6b7 Base on alpine and run both rdpgw-auth and gateway 2024-03-18 13:26:24 +01:00
Bolke de Bruin
cb7f09debb Reduce intervals 2024-03-18 12:19:24 +01:00
Bolke de Bruin
1006f2bac9 Further details 2024-03-18 12:08:58 +01:00
Bolke de Bruin
32693f4197 Update images 2024-03-18 12:07:56 +01:00
Bolke de Bruin
43ac78bf57 Fixes 2024-03-18 11:36:55 +01:00
Bolke de Bruin
918f1b9e93 Clarify usage 2024-03-18 11:34:43 +01:00
Bolke de Bruin
da70e5967b Fix background 2024-03-18 11:22:30 +01:00
Bolke de Bruin
43493e9548 Add more docs 2024-03-18 11:18:44 +01:00
Bolke de Bruin
5d92dcff8d Improve security docs 2024-03-16 14:59:04 +01:00
Bolke de Bruin
ecbe63f175 Use list of kdcs and ensure length is removed / added when necessary 2024-03-16 13:10:30 +01:00
Bolke de Bruin
a67962b02d Fix no username issues 2024-03-16 11:32:02 +01:00
Jonathan Giroux (Koltes)
8e117ad083
Can omit username from rendered RDP (#83) 2024-03-15 12:30:22 +01:00
fliaping
6325c0c4b7
add "username" as claim key (#98) 2024-03-15 12:29:00 +01:00
dependabot[bot]
db00db131b
Bump github.com/go-jose/go-jose/v3 from 3.0.1 to 3.0.3 (#101)
Bumps [github.com/go-jose/go-jose/v3](https://github.com/go-jose/go-jose) from 3.0.1 to 3.0.3.
- [Release notes](https://github.com/go-jose/go-jose/releases)
- [Changelog](https://github.com/go-jose/go-jose/blob/v3.0.3/CHANGELOG.md)
- [Commits](https://github.com/go-jose/go-jose/compare/v3.0.1...v3.0.3)

---
updated-dependencies:
- dependency-name: github.com/go-jose/go-jose/v3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-15 12:28:41 +01:00
dependabot[bot]
71e3668943
Bump google.golang.org/protobuf from 1.30.0 to 1.33.0 (#102)
Bumps google.golang.org/protobuf from 1.30.0 to 1.33.0.

---
updated-dependencies:
- dependency-name: google.golang.org/protobuf
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-15 12:28:30 +01:00
Aaron Burchfield
4cb8216c49
docker compose command must be a list (#93) 2023-12-19 13:19:45 +01:00
dependabot[bot]
bce7ed0164
Bump golang.org/x/crypto from 0.14.0 to 0.17.0 (#92)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.14.0 to 0.17.0.
- [Commits](https://github.com/golang/crypto/compare/v0.14.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-19 13:19:07 +01:00
ryanblenis
f72613c2ba
Add BasicAuthTimeout setting versus static 5 seconds (#90) 2023-12-16 21:07:37 +01:00
dependabot[bot]
017f338d86
Bump golang.org/x/net from 0.9.0 to 0.17.0 (#85)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.9.0 to 0.17.0.
- [Commits](https://github.com/golang/net/compare/v0.9.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-15 12:58:12 +01:00
dependabot[bot]
93d77f297e
Bump google.golang.org/grpc from 1.54.0 to 1.56.3 (#86)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.54.0 to 1.56.3.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.54.0...v1.56.3)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-15 12:57:50 +01:00
dependabot[bot]
cd58eeef81
Bump github.com/go-jose/go-jose/v3 from 3.0.0 to 3.0.1 (#87)
Bumps [github.com/go-jose/go-jose/v3](https://github.com/go-jose/go-jose) from 3.0.0 to 3.0.1.
- [Release notes](https://github.com/go-jose/go-jose/releases)
- [Changelog](https://github.com/go-jose/go-jose/blob/v3/CHANGELOG.md)
- [Commits](https://github.com/go-jose/go-jose/compare/v3.0.0...v3.0.1)

---
updated-dependencies:
- dependency-name: github.com/go-jose/go-jose/v3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-15 12:57:33 +01:00
Bolke de Bruin
e9e592b43a Add missing rdp options
Some options were missing so they could not be set
in the rdp template.

Closes: #78
2023-09-13 11:27:19 +02:00
Bolke de Bruin
6b32631434 Finalize rdp templating 2023-05-15 10:43:38 +02:00
Bolke de Bruin
cdc497f365 Add templating option for RDP files 2023-05-15 10:43:38 +02:00
bolkedebruin
769abae3ba
Update README.md
Kerberos is integrated
2023-04-17 07:25:43 +00:00
Bolke de Bruin
303ed64744 bump koanf 2023-04-16 10:42:16 +02:00
Bolke de Bruin
0665f92879 bump dependencies except koanf 2023-04-16 10:18:22 +02:00
Bolke de Bruin
acd98367db Merge branch 'multiple_oidc' 2023-04-16 10:03:46 +02:00
dependabot[bot]
31c472feaf
Bump golang.org/x/net from 0.0.0-20220725212005-46097bf591d3 to 0.7.0 (#67)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.0.0-20220725212005-46097bf591d3 to 0.7.0.
- [Release notes](https://github.com/golang/net/releases)
- [Commits](https://github.com/golang/net/commits/v0.7.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-15 10:40:21 +02:00
65 changed files with 4042 additions and 553 deletions

View File

@ -41,11 +41,16 @@ jobs:
run: sudo apt-get -y install libpam-dev run: sudo apt-get -y install libpam-dev
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Install Go
uses: actions/setup-go@v4
with:
go-version-file: go.mod
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v2 uses: github/codeql-action/init@v3
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
@ -59,7 +64,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v2 uses: github/codeql-action/autobuild@v3
# Command-line programs to run using the OS shell. # Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@ -72,4 +77,4 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh # ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2 uses: github/codeql-action/analyze@v3

View File

@ -4,8 +4,6 @@ on:
push: push:
branches: [ "master" ] branches: [ "master" ]
tags: [ "v*" ] tags: [ "v*" ]
pull_request:
branches: [ "master" ]
jobs: jobs:
@ -15,17 +13,17 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v2 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKER_USER }} username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push - name: Build and push - latest
uses: docker/build-push-action@v3 uses: docker/build-push-action@v3
with: with:
context: ./dev/docker context: ./dev/docker
@ -33,6 +31,14 @@ jobs:
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
push: true push: true
tags: ${{ github.repository_owner }}/rdpgw:latest tags: ${{ github.repository_owner }}/rdpgw:latest
- name: Build and push - latest
uses: docker/build-push-action@v3
with:
context: ./dev/docker
file: ./dev/docker/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ github.repository_owner }}/rdpgw:${{ github.ref_name }}
- name: Update Docker Hub Description - name: Update Docker Hub Description
uses: peter-evans/dockerhub-description@v3 uses: peter-evans/dockerhub-description@v3
with: with:

View File

@ -16,7 +16,7 @@ jobs:
- name: Set up Go 1.x - name: Set up Go 1.x
uses: actions/setup-go@v2 uses: actions/setup-go@v2
with: with:
go-version: ^1.19 go-version: ^1.22
id: go id: go
- name: Install pam-devel - name: Install pam-devel

2
.gitignore vendored
View File

@ -1 +1,3 @@
go.sum go.sum
node_modules/
package-lock.json

View File

@ -26,7 +26,7 @@ ifneq ($(GIT_TAG),)
endif endif
.PHONY: all .PHONY: all
all: mod build all: mod build deb
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# build # build
@ -38,6 +38,10 @@ $(BINDIR)/$(BINNAME): $(SRC)
go build $(GOFLAGS) -trimpath -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o '$(BINDIR)'/$(BINNAME) ./cmd/rdpgw go build $(GOFLAGS) -trimpath -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o '$(BINDIR)'/$(BINNAME) ./cmd/rdpgw
go build $(GOFLAGS) -trimpath -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o '$(BINDIR)'/$(BINNAME2) ./cmd/auth go build $(GOFLAGS) -trimpath -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o '$(BINDIR)'/$(BINNAME2) ./cmd/auth
.PHONY: deb
deb:clean mod build
dpkg-buildpackage -b -us -uc
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# install # install
@ -50,7 +54,7 @@ install: build
.PHONY: mod .PHONY: mod
mod: mod:
go mod tidy -compat=1.19 go mod tidy -compat=1.23
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# test # test
@ -64,6 +68,7 @@ test:
.PHONY: clean .PHONY: clean
clean: clean:
@rm -rf '$(BINDIR)' ./_dist @rm -rf '$(BINDIR)' ./_dist
dh_clean
.PHONY: info .PHONY: info
info: info:

462
README.md
View File

@ -14,67 +14,258 @@ This allows you to connect with the official Microsoft clients to remote desktop
These desktops could be, for example, [XRDP](http://www.xrdp.org) desktops running in containers These desktops could be, for example, [XRDP](http://www.xrdp.org) desktops running in containers
on Kubernetes. on Kubernetes.
## AIM # AIM
RDPGW aims to provide a full open source replacement for MS Remote Desktop Gateway, RDPGW aims to provide a full open source replacement for MS Remote Desktop Gateway,
including access policies. including access policies.
## Multi Factor Authentication (MFA) # Security requirements
RDPGW provides multi factor authentication out of the box with OpenID Connect integration. Thus
Several security requirements are stipulated by the client that is connecting to it and some are
enforced by the gateway. The client requires that the server's TLS certificate is valid and that
it is signed by a trusted authority. In addition, the common name in the certificate needs to
match the DNS hostname of the gateway. If these requirements are not met the client will refuse
to connect.
The gateway has several security phases. In the authentication phase the client's credentials are
verified. Depending the authentication mechanism used, the client's credentials are verified against
an OpenID Connect provider, Kerberos, a local PAM service or a local database.
If OpenID Connect is used the user will
need to connect to a webpage provided by the gateway to authenticate, which in turn will redirect
the user to the OpenID Connect provider. If the authentication is successful the browser will download
a RDP file with temporary credentials that allow the user to connect to the gateway by using a remote
desktop client.
If Kerberos is used the client will need to have a valid ticket granting ticket (TGT). The gateway
will proxy the TGT request to the KDC. Therefore, the gateway needs to be able to connect to the KDC
and a krb5.conf file needs to be provided. The proxy works without the need for an RDP file and thus
the client can connect directly to the gateway.
If local authentication is used the client will need to provide a username and password that is verified
against PAM. This requires, to ensure privilege separation, that ```rdpgw-auth``` is also running and a
valid PAM configuration is provided per typical configuration.
If NTLM authentication is used, the allowed user credentials for the gateway should be configured in the
configuration file of `rdpgw-auth`.
Finally, RDP hosts that the client wants to connect to are verified against what was provided by / allowed by
the server. Next to that the client's ip address needs to match the one it obtained the gateway token with if
using OpenID Connect. Due to proxies and NAT this is not always possible and thus can be disabled. However, this
is a security risk.
# Configuration
The configuration is done through a YAML file. The configuration file is read from `rdpgw.yaml` by default.
At the bottom of this README is an example configuration file. In these sections you will find the most important
settings.
## Authentication
RDPGW wants to be secure when you set it up from the start. It supports several authentication
mechanisms such as OpenID Connect, Kerberos, PAM or NTLM.
Technically, cookies are encrypted and signed on the client side relying
on [Gorilla Sessions](https://www.gorillatoolkit.org/pkg/sessions). PAA tokens (gateway access tokens)
are generated and signed according to the JWT spec by using [jwt-go](https://github.com/dgrijalva/jwt-go)
signed with a 256 bit HMAC.
### Multi Factor Authentication (MFA)
RDPGW provides multi-factor authentication out of the box with OpenID Connect integration. Thus
you can integrate your remote desktops with Keycloak, Okta, Google, Azure, Apple or Facebook you can integrate your remote desktops with Keycloak, Okta, Google, Azure, Apple or Facebook
if you want. if you want.
## Security ### Mixing authentication mechanisms
__NOTE__: rdogw now supports PAM authentication as well if you configure it to use 'local' authentication. Further documentation pending. It is technically possible to mix authentication mechanisms. Currently, you can mix local with Kerberos or NTLM. If you enable
OpenID Connect it is not possible to mix it with local or Kerberos at the moment.
RDPGW wants to be secure when you set it up from the beginning. It does this by having OpenID ### Open ID Connect
Connect integration enabled by default. Cookies are encrypted and signed on the client side relying ![OpenID Connect](docs/images/flow-openid.svg)
on [Gorilla Sessions](https://www.gorillatoolkit.org/pkg/sessions). PAA tokens (gateway access tokens)
are generated and signed according to the JWT spec by using [jwt-go](https://github.com/dgrijalva/jwt-go)
signed with a 256 bit HMAC. Hosts provided by the user are verified against what was provided by
the server. Finally, the client's ip address needs to match the one it obtained the token with.
## How to build & install To use OpenID Connect make sure you have properly configured your OpenID Connect provider, and you have a client id
and secret. The client id and secret are used to authenticate the gateway to the OpenID Connect provider. The provider
will then authenticate the user and provide the gateway with a token. The gateway will then use this token to generate
a PAA token that is used to connect to the RDP host.
__NOTE__: a docker image is available on docker hub, which removes the need for building and installing go. To enable OpenID Connect make sure to set the following variables in the configuration file.
Ensure that you have `make` (comes with standard build tools, like `build-essential` on Debian), `go` (version 1.19 or above), and development files for PAM (`libpam0g-dev` on Debian) installed. ```yaml
Server:
Then clone the repo and issues the following. Authentication:
- openid
```bash OpenId:
cd rdpgw ProviderUrl: http://<provider_url>
make ClientId: <your client id>
make install ClientSecret: <your-secret>
Caps:
TokenAuth: true
``` ```
## Configuration As you can see in the flow diagram when using OpenID Connect the user will use a browser to connect to the gateway first at
By default the configuration is read from `rdpgw.yaml`. Below is a https://your-gateway/connect. If authentication is successful the browser will download a RDP file with temporary credentials
template. that allow the user to connect to the gateway by using a remote desktop client.
### Kerberos
![Kerberos](docs/images/flow-kerberos.svg)
__NOTE__: Kerberos is heavily reliant on DNS (forward and reverse). Make sure that your DNS is properly configured.
Next to that, its errors are not always very descriptive. It is beyond the scope of this project to provide a full
Kerberos tutorial.
To use Kerberos make sure you have a keytab and krb5.conf file. The keytab is used to authenticate the gateway to the KDC
and the krb5.conf file is used to configure the KDC. The keytab needs to contain a valid principal for the gateway.
Use `ktutil` or a similar tool provided by your Kerberos server to create a keytab file for the newly created service principal.
Place this keytab file in a secure location on the server and make sure that the file is only readable by the user that runs
the gateway.
```plaintext
ktutil
addent -password -p HTTP/rdpgw.example.com@YOUR.REALM -k 1 -e aes256-cts-hmac-sha1-96
wkt rdpgw.keytab
```
Then set the following in the configuration file.
```yaml
Server:
Authentication:
- kerberos
Kerberos:
Keytab: /etc/keytabs/rdpgw.keytab
Krb5conf: /etc/krb5.conf
Caps:
TokenAuth: false
```
The client can then connect directly to the gateway without the need for a RDP file.
### PAM / Local (Basic Auth)
![PAM](docs/images/flow-pam.svg)
The gateway can also support authentication against PAM. Sometimes this is referred to as local or passwd authentication,
but it also supports LDAP authentication or even Active Directory if you have the correct modules installed. Typically
(for passwd), PAM requires that it is accessed as root. Therefore, the gateway comes with a small helper program called
`rdpgw-auth` that is used to authenticate the user. This program needs to be run as root or setuid.
__NOTE__: The default windows client ``mstsc`` does not support basic auth. You will need to use a different client or
switch to OpenID Connect, Kerberos or NTLM authentication.
__NOTE__: Using PAM for passwd (i.e. LDAP is fine) within a container is not recommended. It is better to use OpenID
Connect or Kerberos. If you do want to use it within a container you can choose to run the helper program outside the
container and have the socket available within. Alternatively, you can mount all what is needed into the container but
PAM is quite sensitive to the environment.
Ensure you have a PAM service file for the gateway, `/etc/pam.d/rdpgw`. For authentication against local accounts on the
host located in `/etc/passwd` and `/etc/shadow` you can use the following.
```plaintext
auth required pam_unix.so
account required pam_unix.so
```
Then set the following in the configuration file.
```yaml
Server:
Authentication:
- local
AuthSocket: /tmp/rdpgw-auth.sock
Caps:
TokenAuth: false
```
Make sure to run both the gateway and `rdpgw-auth`. The gateway will connect to the socket to authenticate the user.
```bash
# ./rdpgw-auth -n rdpgw -s /tmp/rdpgw-auth.sock
```
The client can then connect to the gateway directly by using a remote desktop client.
### NTLM
The gateway can also support NTLM authentication.
Currently, only the configuration file is supported as a database for credential lookup.
In the future, support for real databases (e.g. sqlite) may be added.
NTLM authentication has the advantage that it is easy to setup, especially in case the gateway is used for a limited number of users.
Unlike PAM / local, NTLM authentication supports the default windows client ``mstsc``.
__WARNING__: The password is currently saved in plain text. So, you should keep the config file as secure as possible and avoid
reusing the same password for other applications. The password is stored in plain text to support the NTLM authentication protocol.
To enable NTLM authentication make sure to set the following variables in the configuration file.
Configuration file for `rdpgw`:
```yaml
Server:
Authentication:
- ntlm
Caps:
TokenAuth: false
```
Configuration file for `rdpgw-auth`:
````yaml
Users:
- {Username: "my_username", Password: "my_secure_password"} # Modify this password!
````
The client can then connect to the gateway directly by using a remote desktop client using the gateway credentials
configured in the YAML configuration file.
## TLS
The gateway requires a valid TLS certificate. This means a certificate that is signed by a valid CA that is in the store
of your clients. If this is not the case particularly Windows clients will fail to connect. You can either provide a
certificate and key file or let the gateway obtain a certificate from letsencrypt. If you want to use letsencrypt make
sure that the host is reachable on port 80 from the letsencrypt servers.
For letsencrypt:
```yaml
Tls: auto
```
for your own certificate:
```yaml
Tls: enable
CertFile: server.pem
KeyFile: key.pem
```
__NOTE__: You can disable TLS on the gateway, but you will then need to make sure a proxy is run in front of it that does
TLS termination.
## Example configuration file for Open ID Connect
```yaml ```yaml
# web server configuration. # web server configuration.
Server: Server:
# can be set to openid, kerberos and local. If openid is used rdpgw expects # can be set to openid, kerberos, local and ntlm. If openid is used rdpgw expects
# a configured openid provider, make sure to set caps.tokenauth to true. If local # a configured openid provider, make sure to set caps.tokenauth to true. If local
# rdpgw connects to rdpgw-auth over a socket to verify users and password. Note: # rdpgw connects to rdpgw-auth over a socket to verify users and password. Note:
# rdpgw-auth needs to be run as root or setuid in order to work. If kerberos is # rdpgw-auth needs to be run as root or setuid in order to work. If kerberos is
# used a keytab and krb5conf need to be supplied. local and kerberos authentication # used a keytab and krb5conf need to be supplied. local can be stacked with
# can be stacked, so that the clients selects what it wants. # kerberos or ntlm authentication, so that the clients selects what it wants.
Authentication: Authentication:
# - kerberos
# - local
- openid - openid
# - ntlm
# The socket to connect to if using local auth. Ensure rdpgw auth is configured to # The socket to connect to if using local auth. Ensure rdpgw auth is configured to
# use the same socket. # use the same socket.
AuthSocket: /tmp/rdpgw-auth.sock # AuthSocket: /tmp/rdpgw-auth.sock
# Basic auth timeout (in seconds). Useful if you're planning on waiting for MFA
BasicAuthTimeout: 5
# The default option 'auto' uses a certificate file if provided and found otherwise # The default option 'auto' uses a certificate file if provided and found otherwise
# it uses letsencrypt to obtain a certificate, the latter requires that the host is reachable # it uses letsencrypt to obtain a certificate, the latter requires that the host is reachable
# from letsencrypt servers. If TLS termination happens somewhere else (e.g. a load balancer) # from letsencrypt servers. If TLS termination happens somewhere else (e.g. a load balancer)
# set this option to 'disable'. This is mutually exclusive with 'authentication: local' # set this option to 'disable'. This is mutually exclusive with 'authentication: local'
# Note: rdp connections over a gateway require TLS # Note: rdp connections over a gateway require TLS
Tls: auto Tls: auto
# TLS certificate files
CertFile: server.pem
KeyFile: key.pem
# gateway address advertised in the rdp files and browser # gateway address advertised in the rdp files and browser
GatewayAddress: localhost GatewayAddress: localhost
# port to listen on (change to 80 or equivalent if not using TLS) # port to listen on (change to 80 or equivalent if not using TLS)
@ -108,12 +299,13 @@ OpenId:
ProviderUrl: http://keycloak/auth/realms/test ProviderUrl: http://keycloak/auth/realms/test
ClientId: rdpgw ClientId: rdpgw
ClientSecret: your-secret ClientSecret: your-secret
Kerberos: # Kerberos:
Keytab: /etc/keytabs/rdpgw.keytab # Keytab: /etc/keytabs/rdpgw.keytab
Krb5conf: /etc/krb5.conf # Krb5conf: /etc/krb5.conf
# enabled / disabled capabilities # enabled / disabled capabilities
Caps: Caps:
SmartCardAuth: false SmartCardAuth: false
# required for openid connect
TokenAuth: true TokenAuth: true
# connection timeout in minutes, 0 is limitless # connection timeout in minutes, 0 is limitless
IdleTimeout: 10 IdleTimeout: 10
@ -123,18 +315,19 @@ Caps:
EnableDrive: true EnableDrive: true
EnableClipboard: true EnableClipboard: true
Client: Client:
# template rdp file to use for clients
# rdp file settings and their defaults see here:
# https://docs.microsoft.com/en-us/windows-server/remote/remote-desktop-services/clients/rdp-files
defaults: /etc/rdpgw/default.rdp
# this is a go string templated with {{ username }} and {{ token }} # this is a go string templated with {{ username }} and {{ token }}
# the example below uses the ASCII field separator to distinguish # the example below uses the ASCII field separator to distinguish
# between user and token # between user and token
UsernameTemplate: "{{ username }}@bla.com\x1f{{ token }}" UsernameTemplate: "{{ username }}@bla.com\x1f{{ token }}"
# rdp file settings see:
# https://docs.microsoft.com/en-us/windows-server/remote/remote-desktop-services/clients/rdp-files
NetworkAutoDetect: 0
BandwidthAutoDetect: 1
ConnectionType: 6
# If true puts splits "user@domain.com" into the user and domain component so that # If true puts splits "user@domain.com" into the user and domain component so that
# domain gets set in the rdp file and the domain name is stripped from the username # domain gets set in the rdp file and the domain name is stripped from the username
SplitUserDomain: false SplitUserDomain: false
# If true, removes "username" (and "domain" if SplitUserDomain is true) from RDP file.
# NoUsername: true
Security: Security:
# a random string of 32 characters to secure cookies on the client # a random string of 32 characters to secure cookies on the client
# make sure to share this amongst different pods # make sure to share this amongst different pods
@ -142,6 +335,8 @@ Security:
# PAATokenEncryptionKey: thisisasessionkeyreplacethisjetzt # PAATokenEncryptionKey: thisisasessionkeyreplacethisjetzt
# a random string of 32 characters to secure cookies on the client # a random string of 32 characters to secure cookies on the client
UserTokenEncryptionKey: thisisasessionkeyreplacethisjetzt UserTokenEncryptionKey: thisisasessionkeyreplacethisjetzt
# Signing makes the token bigger and we are limited to 511 characters
# UserTokenSigningKey: thisisasessionkeyreplacethisjetzt
# if you want to enable token generation for the user # if you want to enable token generation for the user
# if true the username will be set to a jwt with the username embedded into it # if true the username will be set to a jwt with the username embedded into it
EnableUserToken: true EnableUserToken: true
@ -149,18 +344,50 @@ Security:
# connection is opened. # connection is opened.
VerifyClientIp: true VerifyClientIp: true
``` ```
## How to build & install
__NOTE__: a [docker image](https://hub.docker.com/r/bolkedebruin/rdpgw/) is available on docker hub, which removes the need for building and installing go.
Ensure that you have `make` (comes with standard build tools, like `build-essential` on Debian), `go` (version 1.19 or above), and development files for PAM (`libpam0g-dev` on Debian) installed.
Then clone the repo and issues the following.
```bash
cd rdpgw
make
make install
```
## Testing locally ## Testing locally
A convenience docker-compose allows you to test the RDPGW locally. It uses [Keycloak](http://www.keycloak.org) A convenience docker-compose allows you to test the RDPGW locally. It uses [Keycloak](http://www.keycloak.org)
and [xrdp](http://www.xrdp.org) and exposes it services on port 443. You will need to allow your browser and [xrdp](http://www.xrdp.org) and exposes it services on port 9443. You will need to allow your browser
to connect to localhost with and self signed security certificate. For chrome set `chrome://flags/#allow-insecure-localhost`. to connect to localhost with and self signed security certificate. For chrome set `chrome://flags/#allow-insecure-localhost`.
The username to login to both Keycloak and xrdp is `admin` as is the password. The username to login to both Keycloak and xrdp is `admin` as is the password.
__NOTE__: The redirecting relies on DNS. Make sure to add ``127.0.0.1 keycloak`` to your `/etc/hosts` file to ensure
that the redirect works.
__NOTE__: The local testing environment uses a self signed certificate. This works for MAC clients, but not for Windows.
If you want to test it on Windows you will need to provide a valid certificate.
```bash ```bash
# with open id
cd dev/docker cd dev/docker
docker-compose build docker-compose -f docker-compose.yml up
docker-compose up
# or for arm64 with open id
docker-compose -f docker-compose-arm64.yml up
# or for local or pam
docker-compose -f docker-compose-local.yml up
``` ```
You can then connect to the gateway at `https://localhost:9443/connect` for the OpenID connect flavors which will start
the authentication flow. Or you can connect directly with the gateway set and the host set to ``xrdp`` if using the ``local``
flavor. You can login with 'admin/admin'. The RDP file will download and you can open it with a remote
desktop client. Also for logging in 'admin/admin' will work.
## Use ## Use
Point your browser to `https://your-gateway/connect`. After authentication Point your browser to `https://your-gateway/connect`. After authentication
and RDP file will download to your desktop. This file can be opened by one and RDP file will download to your desktop. This file can be opened by one
@ -175,11 +402,154 @@ It will return 200 OK with the decrypted token.
In this way you can integrate, for example, it with [pam-jwt](https://github.com/bolkedebruin/pam-jwt). In this way you can integrate, for example, it with [pam-jwt](https://github.com/bolkedebruin/pam-jwt).
## Client Caveats
The several clients that Microsoft provides come with their own caveats.
The most important one is that the default client on Windows ``mstsc`` does
not support basic authentication. This means you need to use either OpenID Connect,
Kerberos or ntlm authentication.
In addition to that, ``mstsc``, when configuring a gateway directly in the client requires
you to either:
* "save the credentials" for the gateway
* or specify a (random) domain name in the username field (e.g. ``.\username``) when prompted for the gateway credentials,
otherwise the client will not connect at all (it won't send any packages to the gateway) and it will keep on asking for new credentials.
Finally, ``mstsc`` requires a valid certificate on the gateway.
The Microsoft Remote Desktop Client from the Microsoft Store does not have these issues,
but it requires that the username and password used for authentication are the same for
both the gateway and the RDP host.
The Microsoft Remote Desktop Client for Mac does not have these issues and is the most flexible.
It supports basic authentication, OpenID Connect and Kerberos and can use different credentials
The official Microsoft IOS and Android clients seem also more flexible.
Third party clients like [FreeRDP](https://www.freerdp.com) might also provide more
flexibility.
## TODO ## TODO
* Integrate Open Policy Agent
* Integrate GOKRB5
* Integrate uber-go/zap
* Research: TLS defragmentation
* Improve Web Interface * Improve Web Interface
# Acknowledgements
* This product includes software developed by the Thomson Reuters Global Resources. ([go-ntlm](https://github.com/m7913d/go-ntlm) - BSD-4 License)
# RDPGW 认证 API 服务器
这是一个用于 RDPGW远程桌面网关的认证 API 服务器,提供用户验证和密码获取功能。
## 功能特性
- 支持用户名和密码验证verify 模式)
- 支持密码检索功能getpassword 模式),用于 NTLM 认证
- 支持 GET 和 POST 请求方式
- 可通过配置文件自定义设置
## 安装
确保已安装 Node.js v10 或更高版本,然后执行:
```bash
npm install express
```
## 配置
配置文件为 `config.json`,包含以下选项:
```json
{
"port": 3000,
"apiPath": "/api/checkperm",
"users": {
"testuser": "testpassword",
"admin": "adminpass"
},
"logger": {
"level": "info",
"logToFile": false,
"logFile": "server.log"
}
}
```
### 配置选项说明
- `port`: 服务器监听端口
- `apiPath`: API 路径
- `users`: 用户名和密码字典
- `logger`: 日志配置
## 使用方法
### 启动服务器
```bash
node index.js
```
### API 请求示例
#### 验证模式 (verify)
**GET 请求**:
```
http://localhost:3000/api/checkperm?username=testuser&password=testpassword&mode=verify
```
**POST 请求**:
```bash
curl -X POST http://localhost:3000/api/checkperm \
-H "Content-Type: application/json" \
-d '{"username":"testuser", "password":"testpassword", "mode":"verify"}'
```
**响应**:
```json
{
"status": "success",
"user": "testuser"
}
```
#### 密码获取模式 (getpassword)
**GET 请求**:
```
http://localhost:3000/api/checkperm?username=testuser&mode=getpassword
```
**POST 请求**:
```bash
curl -X POST http://localhost:3000/api/checkperm \
-H "Content-Type: application/json" \
-d '{"username":"testuser", "mode":"getpassword"}'
```
**响应**:
```json
{
"status": "success",
"password": "testpassword"
}
```
## RDPGW 集成
在 RDPGW 配置中添加以下内容:
```yaml
ntlm_api:
enable: true
server: http://localhost:3000
path: /api/checkperm
mode: getpassword
```
## 安全建议
- 在生产环境中,请使用 HTTPS 而非 HTTP
- 限制 API 服务器的访问权限
- 定期更换密码并审查访问日志
- 考虑实现 IP 访问限制和请求频率限制

15
UPGRADING.md Normal file
View File

@ -0,0 +1,15 @@
# Upgrading from 1.X to 2.0
In 2.0 the options for configuring client side RDP settings have been removed in favor of template file.
The template file is a RDP file that is used as a template for the connection. The template file is parsed
and a few settings are replaced to ensure the client can connect to the server and the correct domain is used.
The format of the template file is as follows:
```
# <setting>:<type i or s>:<value>
domain:s:testdomain
connection type:i:2
```
The filename is set under `client > defaults`.

View File

@ -3,8 +3,12 @@ package main
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"github.com/bolkedebruin/rdpgw/cmd/auth/config"
"github.com/bolkedebruin/rdpgw/cmd/auth/database"
"github.com/bolkedebruin/rdpgw/cmd/auth/ntlm"
"github.com/bolkedebruin/rdpgw/shared/auth" "github.com/bolkedebruin/rdpgw/shared/auth"
"github.com/msteinert/pam" "github.com/msteinert/pam/v2"
"github.com/thought-machine/go-flags" "github.com/thought-machine/go-flags"
"google.golang.org/grpc" "google.golang.org/grpc"
"log" "log"
@ -20,16 +24,24 @@ const (
var opts struct { var opts struct {
ServiceName string `short:"n" long:"name" default:"rdpgw" description:"the PAM service name to use"` ServiceName string `short:"n" long:"name" default:"rdpgw" description:"the PAM service name to use"`
SocketAddr string `short:"s" long:"socket" default:"/tmp/rdpgw-auth.sock" description:"the location of the socket"` SocketAddr string `short:"s" long:"socket" default:"/tmp/rdpgw-auth.sock" description:"the location of the socket"`
ConfigFile string `short:"c" long:"conf" default:"rdpgw-auth.yaml" description:"users config file for NTLM (yaml)"`
} }
type AuthServiceImpl struct { type AuthServiceImpl struct {
auth.UnimplementedAuthenticateServer
serviceName string serviceName string
ntlm *ntlm.NTLMAuth
} }
var conf config.Configuration
var _ auth.AuthenticateServer = (*AuthServiceImpl)(nil) var _ auth.AuthenticateServer = (*AuthServiceImpl)(nil)
func NewAuthService(serviceName string) auth.AuthenticateServer { func NewAuthService(serviceName string, database database.Database) auth.AuthenticateServer {
s := &AuthServiceImpl{serviceName: serviceName} s := &AuthServiceImpl{
serviceName: serviceName,
ntlm: ntlm.NewNTLMAuth(database),
}
return s return s
} }
@ -52,7 +64,13 @@ func (s *AuthServiceImpl) Authenticate(ctx context.Context, message *auth.UserPa
r.Error = err.Error() r.Error = err.Error()
return r, err return r, err
} }
defer func() {
err := t.End()
if err != nil {
fmt.Fprintf(os.Stderr, "end: %v\n", err)
os.Exit(1)
}
}()
if err = t.Authenticate(0); err != nil { if err = t.Authenticate(0); err != nil {
log.Printf("Authentication for user: %s failed due to: %s", message.Username, err) log.Printf("Authentication for user: %s failed due to: %s", message.Username, err)
r.Error = err.Error() r.Error = err.Error()
@ -70,11 +88,34 @@ func (s *AuthServiceImpl) Authenticate(ctx context.Context, message *auth.UserPa
return r, nil return r, nil
} }
func (s *AuthServiceImpl) NTLM(ctx context.Context, message *auth.NtlmRequest) (*auth.NtlmResponse, error) {
r, err := s.ntlm.Authenticate(message)
if err != nil {
log.Printf("[%s] NTLM failed: %s", message.Session, err)
} else if r.Authenticated {
log.Printf("[%s] User: %s authenticated using NTLM", message.Session, r.Username)
} else if r.NtlmMessage != "" {
log.Printf("[%s] Sending NTLM challenge", message.Session)
}
return r, err
}
func main() { func main() {
_, err := flags.Parse(&opts) _, err := flags.Parse(&opts)
if err != nil { if err != nil {
panic(err) var fErr *flags.Error
if errors.As(err, &fErr) {
if fErr.Type == flags.ErrHelp {
fmt.Printf("Acknowledgements:\n")
fmt.Printf(" - This product includes software developed by the Thomson Reuters Global Resources. (go-ntlm - https://github.com/m7913d/go-ntlm - BSD-4 License)\n")
} }
}
return
}
conf = config.Load(opts.ConfigFile)
log.Printf("Starting auth server on %s", opts.SocketAddr) log.Printf("Starting auth server on %s", opts.SocketAddr)
cleanup := func() { cleanup := func() {
@ -92,8 +133,20 @@ func main() {
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
server := grpc.NewServer() server := grpc.NewServer()
service := NewAuthService(opts.ServiceName)
// 根据配置选择使用API认证或本地配置认证
var db database.Database
if conf.PXVDI.Enabled && conf.PXVDI.ApiUrl != "" {
log.Printf("Using API authentication, API URL: %s", conf.PXVDI.ApiUrl)
db = database.NewApiDb(conf.PXVDI.ApiUrl, conf.PXVDI.ApiKey)
} else {
log.Printf("Using local configuration file authentication")
db = database.NewConfig(conf.Users)
}
service := NewAuthService(opts.ServiceName, db)
auth.RegisterAuthenticateServer(server, service) auth.RegisterAuthenticateServer(server, service)
server.Serve(listener) server.Serve(listener)
} }

View File

@ -0,0 +1,50 @@
package config
import (
"github.com/knadh/koanf/parsers/yaml"
"github.com/knadh/koanf/providers/confmap"
"github.com/knadh/koanf/providers/file"
"github.com/knadh/koanf/v2"
"log"
"os"
)
type Configuration struct {
Users []UserConfig `koanf:"users"`
PXVDI PXVDIConfig `koanf:"pxvdi"`
}
type PXVDIConfig struct {
Enabled bool `koanf:"enabled"`
ApiUrl string `koanf:"apiurl"`
ApiKey string `koanf:"apikey"`
}
type UserConfig struct {
Username string `koanf:"username"`
Password string `koanf:"password"`
}
var Conf Configuration
func Load(configFile string) Configuration {
var k = koanf.New(".")
k.Load(confmap.Provider(map[string]interface{}{}, "."), nil)
if _, err := os.Stat(configFile); os.IsNotExist(err) {
log.Printf("Config file %s not found, skipping config file", configFile)
} else {
if err := k.Load(file.Provider(configFile), yaml.Parser()); err != nil {
log.Fatalf("Error loading config from file: %v", err)
}
}
koanfTag := koanf.UnmarshalConf{Tag: "koanf"}
k.UnmarshalWithConf("Users", &Conf.Users, koanfTag)
k.UnmarshalWithConf("PXVDI", &Conf.PXVDI, koanfTag)
return Conf
}

181
cmd/auth/database/apidb.go Normal file
View File

@ -0,0 +1,181 @@
package database
import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/url"
)
// ApiDb 结构实现Database接口通过API验证用户凭据
type ApiDb struct {
ApiUrl string // API URL模板使用{username}和{password}作为占位符
ApiKey string // API密钥
}
// NewApiDb 创建一个新的ApiDb实例
func NewApiDb(apiUrl string, apiKey string) *ApiDb {
return &ApiDb{
ApiUrl: apiUrl,
ApiKey: apiKey,
}
}
// GetPassword 从API获取用户密码
// 这个方法会调用API来获取用户的实际密码用于NTLM认证
func (a *ApiDb) GetPassword(username string) string {
log.Printf("Getpassword: %s", username)
// 如果用户名为空,直接返回失败
if username == "" {
log.Printf("API password retrieval failed: empty username")
return ""
}
// 构建API URL
fullUrl := fmt.Sprintf("%s/api/custom/public/ntlmcheck", a.ApiUrl)
// 构建POST请求体
requestData := map[string]string{
"user": username,
"apikey": a.ApiKey,
}
jsonData, err := json.Marshal(requestData)
if err != nil {
log.Printf("Failed to marshal request data: %v", err)
return ""
}
log.Printf("Sending API password POST request to: %s", fullUrl)
// 创建自定义HTTP客户端跳过SSL证书验证
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client := &http.Client{Transport: tr}
// 发送POST请求到API使用不验证SSL证书的客户端
resp, err := client.Post(fullUrl, "application/json", bytes.NewBuffer(jsonData))
if err != nil {
log.Printf("API password retrieval error: %v", err)
return ""
}
defer resp.Body.Close()
// 检查HTTP状态码
if resp.StatusCode != http.StatusOK {
log.Printf("API password retrieval failed with status: %d", resp.StatusCode)
return ""
}
// 读取响应内容
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("Failed to read API response: %v", err)
return ""
}
log.Printf("API password response received")
// 解析响应 - 适应新的响应格式
var result struct {
Success bool `json:"success"`
Data struct {
Pass string `json:"pass"`
} `json:"data"`
}
err = json.NewDecoder(bytes.NewReader(body)).Decode(&result)
if err != nil {
log.Printf("Failed to parse API response: %v", err)
return ""
}
// 检查响应内容
if !result.Success || result.Data.Pass == "" {
log.Printf("API did not return a valid password for user: %s", username)
return ""
}
log.Printf("API password retrieval successful for user: %s", username)
return result.Data.Pass
}
// VerifyCredentials 验证用户凭据
func (a *ApiDb) VerifyCredentials(username, password string) bool {
// 如果用户名为空,直接返回失败
if username == "" {
log.Printf("API verification failed: empty username")
return false
}
// 构建API URL替换占位符
apiUrl := a.ApiUrl
// 构建完整的URL包括查询参数
var fullUrl string
if password == "" {
// NTLM场景下直接将用户名传递给API
// 这种情况下后端API应当能够独立验证用户
fullUrl = fmt.Sprintf("%s?username=%s&mode=verify",
apiUrl, url.QueryEscape(username))
log.Printf("Verifying NTLM user via API: %s", username)
} else {
// 常规场景
fullUrl = fmt.Sprintf("%s?username=%s&password=%s&mode=verify",
apiUrl, url.QueryEscape(username), url.QueryEscape(password))
}
log.Printf("Sending API verification request to: %s", fullUrl)
// 创建自定义HTTP客户端跳过SSL证书验证
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client := &http.Client{Transport: tr}
// 发送请求到API使用不验证SSL证书的客户端
resp, err := client.Get(fullUrl)
if err != nil {
log.Printf("API verification error: %v", err)
return false
}
defer resp.Body.Close()
// 检查HTTP状态码
if resp.StatusCode != http.StatusOK {
log.Printf("API verification failed with status: %d", resp.StatusCode)
return false
}
// 读取响应内容
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("Failed to read API response: %v", err)
return false
}
log.Printf("API response: %s", string(body))
// 解析响应
var result map[string]interface{}
err = json.NewDecoder(bytes.NewReader(body)).Decode(&result)
if err != nil {
log.Printf("Failed to parse API response: %v", err)
return false
}
// 检查响应内容
// 如果响应包含"success",则验证成功
if status, ok := result["status"].(string); ok && status == "success" {
log.Printf("API verification successful for user: %s", username)
return true
}
// 如果没有找到预期的成功标识,则验证失败
log.Printf("API verification failed for user: %s", username)
return false
}

25
cmd/auth/database/config.go Executable file
View File

@ -0,0 +1,25 @@
package database
import (
"github.com/bolkedebruin/rdpgw/cmd/auth/config"
)
type Config struct {
users map[string]config.UserConfig
}
func NewConfig(users []config.UserConfig) *Config {
usersMap := map[string]config.UserConfig{}
for _, user := range users {
usersMap[user.Username] = user
}
return &Config{
users: usersMap,
}
}
func (c *Config) GetPassword (username string) string {
return c.users[username].Password
}

View File

@ -0,0 +1,43 @@
package database
import (
"github.com/bolkedebruin/rdpgw/cmd/auth/config"
"testing"
)
func createTestDatabase () (Database) {
var users = []config.UserConfig{}
user1 := config.UserConfig{}
user1.Username = "my_username"
user1.Password = "my_password"
users = append(users, user1)
user2 := config.UserConfig{}
user2.Username = "my_username2"
user2.Password = "my_password2"
users = append(users, user2)
config := NewConfig(users)
return config
}
func TestDatabaseConfigValidUsername(t *testing.T) {
database := createTestDatabase()
if database.GetPassword("my_username") != "my_password" {
t.Fatalf("Wrong password returned")
}
if database.GetPassword("my_username2") != "my_password2" {
t.Fatalf("Wrong password returned")
}
}
func TestDatabaseInvalidUsername(t *testing.T) {
database := createTestDatabase()
if database.GetPassword("my_invalid_username") != "" {
t.Fatalf("Non empty password returned for invalid username")
}
}

5
cmd/auth/database/database.go Executable file
View File

@ -0,0 +1,5 @@
package database
type Database interface {
GetPassword (username string) string
}

164
cmd/auth/ntlm/ntlm.go Normal file
View File

@ -0,0 +1,164 @@
package ntlm
import (
"encoding/base64"
"errors"
"github.com/bolkedebruin/rdpgw/cmd/auth/database"
"github.com/bolkedebruin/rdpgw/shared/auth"
"github.com/patrickmn/go-cache"
"github.com/m7913d/go-ntlm/ntlm"
"fmt"
"log"
"time"
)
const (
cacheExpiration = time.Minute
cleanupInterval = time.Minute * 5
)
type NTLMAuth struct {
contextCache *cache.Cache
// Information about the server, returned to the client during authentication
ServerName string // e.g. EXAMPLE1
DomainName string // e.g. EXAMPLE
DnsServerName string // e.g. example1.example.com
DnsDomainName string // e.g. example.com
DnsTreeName string // e.g. example.com
Database database.Database
}
func NewNTLMAuth (database database.Database) (*NTLMAuth) {
return &NTLMAuth{
contextCache: cache.New(cacheExpiration, cleanupInterval),
Database: database,
}
}
func (h *NTLMAuth) Authenticate(message *auth.NtlmRequest) (*auth.NtlmResponse, error) {
r := &auth.NtlmResponse{}
r.Authenticated = false
if message.Session == "" {
return r, errors.New("Invalid (empty) session specified")
}
if message.NtlmMessage == "" {
return r, errors.New("Empty NTLM message specified")
}
c := h.getContext(message.Session)
err := c.Authenticate(message.NtlmMessage, r)
if err != nil || r.Authenticated {
h.removeContext(message.Session)
}
return r, err
}
func (h *NTLMAuth) getContext (session string) (*ntlmContext) {
if c_, found := h.contextCache.Get(session); found {
if c, ok := c_.(*ntlmContext); ok {
return c
}
}
c := new(ntlmContext)
c.h = h
h.contextCache.Set(session, c, cache.DefaultExpiration)
return c
}
func (h *NTLMAuth) removeContext (session string) {
h.contextCache.Delete(session)
}
type ntlmContext struct {
session ntlm.ServerSession
h *NTLMAuth
}
func (c *ntlmContext) Authenticate(authorisationEncoded string, r *auth.NtlmResponse) (error) {
authorisation, err := base64.StdEncoding.DecodeString(authorisationEncoded)
if err != nil {
return errors.New(fmt.Sprintf("Failed to decode NTLM Authorisation header: %s", err))
}
nm, err := ntlm.ParseNegotiateMessage(authorisation)
if err == nil {
return c.negotiate(nm, r)
}
if (nm != nil && nm.MessageType == 1) {
return errors.New(fmt.Sprintf("Failed to parse NTLM Authorisation header: %s", err))
} else if c.session == nil {
return errors.New(fmt.Sprintf("New NTLM auth sequence should start with negotioate request"))
}
am, err := ntlm.ParseAuthenticateMessage(authorisation, 2)
if err == nil {
return c.authenticate(am, r)
}
return errors.New(fmt.Sprintf("Failed to parse NTLM Authorisation header: %s", err))
}
func (c *ntlmContext) negotiate(nm *ntlm.NegotiateMessage, r *auth.NtlmResponse) (error) {
session, err := ntlm.CreateServerSession(ntlm.Version2, ntlm.ConnectionOrientedMode)
if err != nil {
c.session = nil;
return errors.New(fmt.Sprintf("Failed to create NTLM server session: %s", err))
}
c.session = session
c.session.SetRequireNtHash(true)
c.session.SetDomainName(c.h.DomainName)
c.session.SetComputerName(c.h.ServerName)
c.session.SetDnsDomainName(c.h.DnsDomainName)
c.session.SetDnsComputerName(c.h.DnsServerName)
c.session.SetDnsTreeName(c.h.DnsTreeName)
err = c.session.ProcessNegotiateMessage(nm)
if err != nil {
return errors.New(fmt.Sprintf("Failed to process NTLM negotiate message: %s", err))
}
cm, err := c.session.GenerateChallengeMessage()
if err != nil {
return errors.New(fmt.Sprintf("Failed to generate NTLM challenge message: %s", err))
}
r.NtlmMessage = base64.StdEncoding.EncodeToString(cm.Bytes())
return nil
}
func (c *ntlmContext) authenticate(am *ntlm.AuthenticateMessage, r *auth.NtlmResponse) (error) {
if c.session == nil {
return errors.New(fmt.Sprintf("NTLM Authenticate requires active session: first call negotioate"))
}
username := am.UserName.String()
log.Printf("NTLM: Trying to validate user: %s", username)
password := c.h.Database.GetPassword(username)
if password == "" {
log.Printf("NTLM: unknown username specified: %s", username)
return nil
}
log.Printf("NTLM: Successfully retrieved password for user: %s", username)
c.session.SetUserInfo(username, password, "")
err := c.session.ProcessAuthenticateMessage(am)
if err != nil {
log.Printf("Failed to process NTLM authenticate message: %s", err)
return nil
}
r.Authenticated = true
r.Username = username
log.Printf("NTLM: User %s authenticated successfully", username)
return nil
}

168
cmd/auth/ntlm/ntlm_test.go Normal file
View File

@ -0,0 +1,168 @@
package ntlm
import (
"encoding/base64"
"github.com/bolkedebruin/rdpgw/cmd/auth/config"
"github.com/bolkedebruin/rdpgw/cmd/auth/database"
"github.com/bolkedebruin/rdpgw/shared/auth"
"github.com/m7913d/go-ntlm/ntlm"
"testing"
"log"
)
func createTestDatabase () (database.Database) {
user := config.UserConfig{}
user.Username = "my_username"
user.Password = "my_password"
var users = []config.UserConfig{}
users = append(users, user)
config := database.NewConfig(users)
return config
}
func TestNtlmValidCredentials(t *testing.T) {
client := ntlm.V2ClientSession{}
client.SetUserInfo("my_username", "my_password", "")
authenticateResponse := authenticate(t, &client)
if !authenticateResponse.Authenticated {
t.Errorf("Failed to authenticate")
return
}
if authenticateResponse.Username != "my_username" {
t.Errorf("Wrong username returned")
return
}
}
func TestNtlmInvalidPassword(t *testing.T) {
client := ntlm.V2ClientSession{}
client.SetUserInfo("my_username", "my_invalid_password", "")
authenticateResponse := authenticate(t, &client)
if authenticateResponse.Authenticated {
t.Errorf("Authenticated with wrong password")
return
}
if authenticateResponse.Username != "" {
t.Errorf("If authentication failed, no username should be returned")
return
}
}
func TestNtlmInvalidUsername(t *testing.T) {
client := ntlm.V2ClientSession{}
client.SetUserInfo("my_invalid_username", "my_password", "")
authenticateResponse := authenticate(t, &client)
if authenticateResponse.Authenticated {
t.Errorf("Authenticated with wrong password")
return
}
if authenticateResponse.Username != "" {
t.Errorf("If authentication failed, no username should be returned")
return
}
}
func authenticate(t *testing.T, client *ntlm.V2ClientSession) (*auth.NtlmResponse) {
session := "X"
database := createTestDatabase()
server := NewNTLMAuth(database)
negotiate, err := client.GenerateNegotiateMessage()
if err != nil {
t.Errorf("Could not generate negotiate message: %s", err)
return nil
}
negotiateRequest := &auth.NtlmRequest{}
negotiateRequest.Session = session
negotiateRequest.NtlmMessage = base64.StdEncoding.EncodeToString(negotiate.Bytes())
negotiateResponse, err := server.Authenticate(negotiateRequest)
if err != nil {
t.Errorf("Could not generate challenge message: %s", err)
return nil
}
if negotiateResponse.Authenticated {
t.Errorf("User should not be authenticated by after negotiate message")
return nil
}
if negotiateResponse.NtlmMessage == "" {
t.Errorf("Could not generate challenge message")
return nil
}
decodedChallenge, err := base64.StdEncoding.DecodeString(negotiateResponse.NtlmMessage)
if err != nil {
t.Errorf("Challenge should be base64 encoded: %s", err)
return nil
}
challenge, err := ntlm.ParseChallengeMessage(decodedChallenge)
if err != nil {
t.Errorf("Invalid challenge message generated: %s", err)
return nil
}
client.ProcessChallengeMessage(challenge)
authenticate, err := client.GenerateAuthenticateMessage()
if err != nil {
t.Errorf("Could not generate authenticate message: %s", err)
return nil
}
authenticateRequest := &auth.NtlmRequest{}
authenticateRequest.Session = session
authenticateRequest.NtlmMessage = base64.StdEncoding.EncodeToString(authenticate.Bytes())
authenticateResponse, err := server.Authenticate(authenticateRequest)
if err != nil {
t.Errorf("Could not parse authenticate message: %s", err)
return authenticateResponse
}
if authenticateResponse.NtlmMessage != "" {
t.Errorf("Authenticate request should not generate a new NTLM message")
return authenticateResponse
}
return authenticateResponse
}
func TestInvalidBase64 (t *testing.T) {
testInvalidDataBase(t, "X", "X") // not valid base64
}
func TestInvalidData (t *testing.T) {
testInvalidDataBase(t, "X", "XXXX") // valid base64
}
func TestInvalidDataEmptyMessage (t *testing.T) {
testInvalidDataBase(t, "X", "")
}
func TestEmptySession (t *testing.T) {
testInvalidDataBase(t, "", "XXXX")
}
func testInvalidDataBase (t *testing.T, session string, data string) {
database := createTestDatabase()
server := NewNTLMAuth(database)
request := &auth.NtlmRequest{}
request.Session = session
request.NtlmMessage = data
response, err := server.Authenticate(request)
log.Printf("%s",err)
if err == nil {
t.Errorf("Invalid request should return an error")
}
if response.Authenticated {
t.Errorf("User should not be authenticated using invalid data")
}
if response.NtlmMessage != "" {
t.Errorf("No NTLM message should be generated for invalid data")
}
}

View File

@ -2,12 +2,13 @@ package config
import ( import (
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/security" "github.com/bolkedebruin/rdpgw/cmd/rdpgw/security"
"github.com/knadh/koanf"
"github.com/knadh/koanf/parsers/yaml" "github.com/knadh/koanf/parsers/yaml"
"github.com/knadh/koanf/providers/confmap" "github.com/knadh/koanf/providers/confmap"
"github.com/knadh/koanf/providers/env" "github.com/knadh/koanf/providers/env"
"github.com/knadh/koanf/providers/file" "github.com/knadh/koanf/providers/file"
"github.com/knadh/koanf/v2"
"log" "log"
"os"
"strings" "strings"
) )
@ -33,6 +34,13 @@ type Configuration struct {
Caps RDGCapsConfig `koanf:"caps"` Caps RDGCapsConfig `koanf:"caps"`
Security SecurityConfig `koanf:"security"` Security SecurityConfig `koanf:"security"`
Client ClientConfig `koanf:"client"` Client ClientConfig `koanf:"client"`
PXVDI PXVDIConfig `koanf:"pxvdi"`
}
type PXVDIConfig struct {
Enabled bool `koanf:"enabled"`
ApiUrl string `koanf:"apiurl"`
ApiKey string `koanf:"apikey"`
} }
type ServerConfig struct { type ServerConfig struct {
@ -51,6 +59,7 @@ type ServerConfig struct {
Tls string `koanf:"tls"` Tls string `koanf:"tls"`
Authentication []string `koanf:"authentication"` Authentication []string `koanf:"authentication"`
AuthSocket string `koanf:"authsocket"` AuthSocket string `koanf:"authsocket"`
BasicAuthTimeout int `koanf:"basicauthtimeout"`
} }
type KerberosConfig struct { type KerberosConfig struct {
@ -89,12 +98,11 @@ type SecurityConfig struct {
} }
type ClientConfig struct { type ClientConfig struct {
NetworkAutoDetect int `koanf:"networkautodetect"` Defaults string `koanf:"defaults"`
BandwidthAutoDetect int `koanf:"bandwidthautodetect"` // kept for backwards compatibility
ConnectionType int `koanf:"connectiontype"`
UsernameTemplate string `koanf:"usernametemplate"` UsernameTemplate string `koanf:"usernametemplate"`
SplitUserDomain bool `koanf:"splituserdomain"` SplitUserDomain bool `koanf:"splituserdomain"`
DefaultDomain string `koanf:"defaultdomain"` NoUsername bool `koanf:"nousername"`
} }
func ToCamel(s string) string { func ToCamel(s string) string {
@ -145,22 +153,35 @@ func Load(configFile string) Configuration {
"Server.HostSelection": "roundrobin", "Server.HostSelection": "roundrobin",
"Server.Authentication": "openid", "Server.Authentication": "openid",
"Server.AuthSocket": "/tmp/rdpgw-auth.sock", "Server.AuthSocket": "/tmp/rdpgw-auth.sock",
"Server.BasicAuthTimeout": 5,
"Client.NetworkAutoDetect": 1, "Client.NetworkAutoDetect": 1,
"Client.BandwidthAutoDetect": 1, "Client.BandwidthAutoDetect": 1,
"Security.VerifyClientIp": true, "Security.VerifyClientIp": true,
"Caps.TokenAuth": true, "Caps.TokenAuth": true,
}, "."), nil) }, "."), nil)
if _, err := os.Stat(configFile); os.IsNotExist(err) {
log.Printf("Config file %s not found, using defaults and environment", configFile)
} else {
if err := k.Load(file.Provider(configFile), yaml.Parser()); err != nil { if err := k.Load(file.Provider(configFile), yaml.Parser()); err != nil {
log.Fatalf("Error loading config from file: %v", err) log.Fatalf("Error loading config from file: %v", err)
} }
}
if err := k.Load(env.ProviderWithValue("RDPGW_", ".", func(s string, v string) (string, interface{}) { if err := k.Load(env.ProviderWithValue("RDPGW_", ".", func(s string, v string) (string, interface{}) {
key := strings.Replace(strings.ToLower(strings.TrimPrefix(s, "RDPGW_")), "__", ".", -1) key := strings.Replace(strings.ToLower(strings.TrimPrefix(s, "RDPGW_")), "__", ".", -1)
key = ToCamel(key) key = ToCamel(key)
v = strings.Trim(v, " ")
// handle lists
if strings.Contains(v, " ") {
return key, strings.Split(v, " ")
}
return key, v return key, v
}), nil); err != nil { }), nil); err != nil {
log.Fatalf("Error loading config from file: %v", err) log.Fatalf("Error loading config from environment: %v", err)
} }
koanfTag := koanf.UnmarshalConf{Tag: "koanf"} koanfTag := koanf.UnmarshalConf{Tag: "koanf"}
@ -170,6 +191,7 @@ func Load(configFile string) Configuration {
k.UnmarshalWithConf("Security", &Conf.Security, koanfTag) k.UnmarshalWithConf("Security", &Conf.Security, koanfTag)
k.UnmarshalWithConf("Client", &Conf.Client, koanfTag) k.UnmarshalWithConf("Client", &Conf.Client, koanfTag)
k.UnmarshalWithConf("Kerberos", &Conf.Kerberos, koanfTag) k.UnmarshalWithConf("Kerberos", &Conf.Kerberos, koanfTag)
k.UnmarshalWithConf("PXVDI", &Conf.PXVDI, koanfTag)
if len(Conf.Security.PAATokenEncryptionKey) != 32 { if len(Conf.Security.PAATokenEncryptionKey) != 32 {
Conf.Security.PAATokenEncryptionKey, _ = security.GenerateRandomString(32) Conf.Security.PAATokenEncryptionKey, _ = security.GenerateRandomString(32)
@ -211,6 +233,10 @@ func Load(configFile string) Configuration {
log.Fatalf("basicauth=local and tls=disable are mutually exclusive") log.Fatalf("basicauth=local and tls=disable are mutually exclusive")
} }
if Conf.Server.NtlmEnabled() && Conf.Server.KerberosEnabled() {
log.Fatalf("ntlm and kerberos authentication are not stackable")
}
if !Conf.Caps.TokenAuth && Conf.Server.OpenIDEnabled() { if !Conf.Caps.TokenAuth && Conf.Server.OpenIDEnabled() {
log.Fatalf("openid is configured but tokenauth disabled") log.Fatalf("openid is configured but tokenauth disabled")
} }
@ -240,6 +266,10 @@ func (s *ServerConfig) BasicAuthEnabled() bool {
return s.matchAuth("local") || s.matchAuth("basic") return s.matchAuth("local") || s.matchAuth("basic")
} }
func (s *ServerConfig) NtlmEnabled() bool {
return s.matchAuth("ntlm")
}
func (s *ServerConfig) matchAuth(needle string) bool { func (s *ServerConfig) matchAuth(needle string) bool {
for _, q := range s.Authentication { for _, q := range s.Authentication {
if q == needle { if q == needle {

View File

@ -23,6 +23,13 @@ type KdcProxyMsg struct {
Flags int `asn1:"tag:2,optional"` Flags int `asn1:"tag:2,optional"`
} }
type Kdc struct {
Realm string
Host string
Proto string
Conn net.Conn
}
type KerberosProxy struct { type KerberosProxy struct {
krb5Config *krbconfig.Config krb5Config *krbconfig.Config
} }
@ -97,39 +104,71 @@ func (k *KerberosProxy) forward(realm string, data []byte) (resp []byte, err err
} }
// load udp first as is the default for kerberos // load udp first as is the default for kerberos
c, kdcs, err := k.krb5Config.GetKDCs(realm, false) udpCnt, udpKdcs, err := k.krb5Config.GetKDCs(realm, false)
if err != nil || c < 1 { if err != nil {
return nil, fmt.Errorf("cannot get kdc for realm %s due to %s", realm, err) return nil, fmt.Errorf("cannot get udp kdc for realm %s due to %s", realm, err)
} }
// load tcp
tcpCnt, tcpKdcs, err := k.krb5Config.GetKDCs(realm, true)
if err != nil {
return nil, fmt.Errorf("cannot get tcp kdc for realm %s due to %s", realm, err)
}
if tcpCnt+udpCnt == 0 {
return nil, fmt.Errorf("cannot get any kdcs (tcp or udp) for realm %s", realm)
}
// merge the kdcs
kdcs := make([]Kdc, tcpCnt+udpCnt)
for i := range udpKdcs {
kdcs[i] = Kdc{Realm: realm, Host: udpKdcs[i], Proto: "udp"}
}
for i := range tcpKdcs {
kdcs[i+udpCnt] = Kdc{Realm: realm, Host: tcpKdcs[i], Proto: "tcp"}
}
replies := make(chan []byte, len(kdcs))
for i := range kdcs { for i := range kdcs {
conn, err := net.Dial("tcp", kdcs[i]) conn, err := net.Dial(kdcs[i].Proto, kdcs[i].Host)
if err != nil { if err != nil {
log.Printf("error connecting to %s due to %s, trying next if available", kdcs[i], err) log.Printf("error connecting to %s due to %s, trying next if available", kdcs[i], err)
continue continue
} }
conn.SetDeadline(time.Now().Add(timeout)) conn.SetDeadline(time.Now().Add(timeout))
// if we proxy over UDP remove the length prefix
if kdcs[i].Proto == "tcp" {
_, err = conn.Write(data) _, err = conn.Write(data)
} else {
_, err = conn.Write(data[4:])
}
if err != nil { if err != nil {
log.Printf("cannot write packet data to %s due to %s, trying next if available", kdcs[i], err) log.Printf("cannot write packet data to %s due to %s, trying next if available", kdcs[i], err)
conn.Close() conn.Close()
continue continue
} }
// todo check header kdcs[i].Conn = conn
resp, err = io.ReadAll(conn) go awaitReply(conn, kdcs[i].Proto == "udp", replies)
if err != nil {
log.Printf("error reading from kdc %s due to %s, trying next if available", kdcs[i], err)
conn.Close()
continue
}
conn.Close()
return resp, nil
} }
return nil, fmt.Errorf("no kdcs found for realm %s", realm) reply := <-replies
// close all the connections and return the first reply
for kdc := range kdcs {
if kdcs[kdc].Conn != nil {
kdcs[kdc].Conn.Close()
}
<-replies
}
if reply != nil {
return reply, nil
}
return nil, fmt.Errorf("no replies received from kdcs for realm %s", realm)
} }
func decode(data []byte) (msg *KdcProxyMsg, err error) { func decode(data []byte) (msg *KdcProxyMsg, err error) {
@ -155,3 +194,17 @@ func encode(krb5data []byte) (r []byte, err error) {
} }
return enc, nil return enc, nil
} }
func awaitReply(conn net.Conn, isUdp bool, reply chan<- []byte) {
resp, err := io.ReadAll(conn)
if err != nil {
log.Printf("error reading from kdc due to %s", err)
reply <- nil
return
}
if isUdp {
// udp will be missing the length prefix so add it
resp = append([]byte{byte(len(resp))}, resp...)
}
reply <- resp
}

View File

@ -3,6 +3,7 @@ package main
import ( import (
"context" "context"
"crypto/tls" "crypto/tls"
"encoding/json"
"fmt" "fmt"
"github.com/bolkedebruin/gokrb5/v8/keytab" "github.com/bolkedebruin/gokrb5/v8/keytab"
"github.com/bolkedebruin/gokrb5/v8/service" "github.com/bolkedebruin/gokrb5/v8/service"
@ -23,6 +24,7 @@ import (
"net/url" "net/url"
"os" "os"
"strconv" "strconv"
"time"
) )
const ( const (
@ -65,6 +67,27 @@ func initOIDC(callbackUrl *url.URL) *web.OIDC {
return o.New() return o.New()
} }
// 定时记录连接用户信息
func startConnectionLogger(interval time.Duration) {
ticker := time.NewTicker(interval)
go func() {
for range ticker.C {
connections := protocol.GetActiveConnections()
if len(connections) > 0 {
connData, err := json.Marshal(connections)
if err != nil {
log.Printf("连接信息序列化失败: %v", err)
continue
}
log.Printf("当前活跃连接数: %d, 连接详情: %s", len(connections), string(connData))
} else {
log.Printf("当前无活跃连接")
}
}
}()
log.Printf("启动连接信息记录器,间隔时间: %v", interval)
}
func main() { func main() {
// load config // load config
_, err := flags.Parse(&opts) _, err := flags.Parse(&opts)
@ -110,12 +133,10 @@ func main() {
RdpOpts: web.RdpOpts{ RdpOpts: web.RdpOpts{
UsernameTemplate: conf.Client.UsernameTemplate, UsernameTemplate: conf.Client.UsernameTemplate,
SplitUserDomain: conf.Client.SplitUserDomain, SplitUserDomain: conf.Client.SplitUserDomain,
DefaultDomain: conf.Client.DefaultDomain, NoUsername: conf.Client.NoUsername,
NetworkAutoDetect: conf.Client.NetworkAutoDetect,
BandwidthAutoDetect: conf.Client.BandwidthAutoDetect,
ConnectionType: conf.Client.ConnectionType,
}, },
GatewayAddress: url, GatewayAddress: url,
TemplateFile: conf.Client.Defaults,
} }
if conf.Caps.TokenAuth { if conf.Caps.TokenAuth {
@ -127,6 +148,10 @@ func main() {
h := w.NewHandler() h := w.NewHandler()
log.Printf("Starting remote desktop gateway server") log.Printf("Starting remote desktop gateway server")
// 启动连接信息记录器
startConnectionLogger(10 * time.Second)
cfg := &tls.Config{} cfg := &tls.Config{}
// configure tls security // configure tls security
@ -223,7 +248,7 @@ func main() {
r.HandleFunc("/callback", o.HandleCallback) r.HandleFunc("/callback", o.HandleCallback)
// only enable un-auth endpoint for openid only config // only enable un-auth endpoint for openid only config
if !conf.Server.KerberosEnabled() || !conf.Server.BasicAuthEnabled() { if !conf.Server.KerberosEnabled() && !conf.Server.BasicAuthEnabled() && !conf.Server.NtlmEnabled() {
rdp.Name("gw").HandlerFunc(gw.HandleGatewayProtocol) rdp.Name("gw").HandlerFunc(gw.HandleGatewayProtocol)
} }
} }
@ -232,10 +257,20 @@ func main() {
auth := web.NewAuthMux() auth := web.NewAuthMux()
rdp.MatcherFunc(web.NoAuthz).HandlerFunc(auth.SetAuthenticate) rdp.MatcherFunc(web.NoAuthz).HandlerFunc(auth.SetAuthenticate)
// ntlm
if conf.Server.NtlmEnabled() {
log.Printf("enabling NTLM authentication")
ntlm := web.NTLMAuthHandler{SocketAddress: conf.Server.AuthSocket, Timeout: conf.Server.BasicAuthTimeout}
rdp.NewRoute().HeadersRegexp("Authorization", "NTLM").HandlerFunc(ntlm.NTLMAuth(gw.HandleGatewayProtocol))
rdp.NewRoute().HeadersRegexp("Authorization", "Negotiate").HandlerFunc(ntlm.NTLMAuth(gw.HandleGatewayProtocol))
auth.Register(`NTLM`)
auth.Register(`Negotiate`)
}
// basic auth // basic auth
if conf.Server.BasicAuthEnabled() { if conf.Server.BasicAuthEnabled() {
log.Printf("enabling basic authentication") log.Printf("enabling basic authentication")
q := web.BasicAuthHandler{SocketAddress: conf.Server.AuthSocket} q := web.BasicAuthHandler{SocketAddress: conf.Server.AuthSocket, Timeout: conf.Server.BasicAuthTimeout}
rdp.NewRoute().HeadersRegexp("Authorization", "Basic").HandlerFunc(q.BasicAuth(gw.HandleGatewayProtocol)) rdp.NewRoute().HeadersRegexp("Authorization", "Basic").HandlerFunc(q.BasicAuth(gw.HandleGatewayProtocol))
auth.Register(`Basic realm="restricted", charset="UTF-8"`) auth.Register(`Basic realm="restricted", charset="UTF-8"`)
} }

View File

@ -1,6 +1,9 @@
package protocol package protocol
import "fmt" import (
"fmt"
"time"
)
var Connections map[string]*Monitor var Connections map[string]*Monitor
@ -41,6 +44,40 @@ func Disconnect(id string) error {
return fmt.Errorf("%s connection does not exist", id) return fmt.Errorf("%s connection does not exist", id)
} }
// GetActiveConnections 返回所有当前活跃的连接信息
func GetActiveConnections() []map[string]interface{} {
connections := []map[string]interface{}{}
for id, monitor := range Connections {
tunnel := monitor.Tunnel
if tunnel == nil {
continue
}
// 计算连接持续时间
duration := time.Since(tunnel.ConnectedOn)
// 收集每个连接的关键信息
connInfo := map[string]interface{}{
"id": id,
"rdgId": tunnel.RDGId,
"targetServer": tunnel.TargetServer,
"remoteAddr": tunnel.RemoteAddr,
"userName": tunnel.User.UserName(),
"domain": tunnel.User.Domain(),
"connectedOn": tunnel.ConnectedOn,
"lastSeen": tunnel.LastSeen,
"bytesSent": tunnel.BytesSent,
"bytesReceived": tunnel.BytesReceived,
"durationSecs": int(duration.Seconds()),
}
connections = append(connections, connInfo)
}
return connections
}
// CalculateSpeedPerSecond calculate moving average. // CalculateSpeedPerSecond calculate moving average.
/* /*
func CalculateSpeedPerSecond(connId string) (in int, out int) { func CalculateSpeedPerSecond(connId string) (in int, out int) {

View File

@ -0,0 +1,85 @@
package rdp
import (
"bufio"
"bytes"
"fmt"
"sort"
"strconv"
"strings"
)
type RDP struct{}
func Parser() *RDP {
return &RDP{}
}
func (p *RDP) Unmarshal(b []byte) (map[string]interface{}, error) {
r := bytes.NewReader(b)
scanner := bufio.NewScanner(r)
mp := make(map[string]interface{})
c := 0
for scanner.Scan() {
c++
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
fields := strings.SplitN(line, ":", 3)
if len(fields) != 3 {
return nil, fmt.Errorf("malformed line %d: %q", c, line)
}
key := strings.TrimSpace(fields[0])
t := strings.TrimSpace(fields[1])
val := strings.TrimSpace(fields[2])
switch t {
case "i":
intValue, err := strconv.Atoi(val)
if err != nil {
return nil, fmt.Errorf("cannot parse integer at line %d: %s", c, line)
}
mp[key] = intValue
case "s":
mp[key] = val
case "b":
mp[key] = val
default:
return nil, fmt.Errorf("malformed line %d: %s", c, line)
}
}
return mp, nil
}
func (p *RDP) Marshal(o map[string]interface{}) ([]byte, error) {
var b bytes.Buffer
keys := make([]string, 0, len(o))
for k := range o {
keys = append(keys, k)
}
sort.Strings(keys)
for _, key := range keys {
v := o[key]
switch v.(type) {
case bool:
if v == true {
fmt.Fprintf(&b, "%s:i:1", key)
} else {
fmt.Fprintf(&b, "%s:i:0", key)
}
case int:
fmt.Fprintf(&b, "%s:i:%d", key, v)
case string:
fmt.Fprintf(&b, "%s:s:%s", key, v)
default:
return nil, fmt.Errorf("error marshalling")
}
fmt.Fprint(&b, "\r\n")
}
return b.Bytes(), nil
}

View File

@ -0,0 +1,85 @@
package rdp
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestUnmarshalRDPFile(t *testing.T) {
rdp := Parser()
testCases := []struct {
name string
cfg []byte
expOutput map[string]interface{}
err error
}{
{
name: "empty",
expOutput: map[string]interface{}{},
},
{
name: "string",
cfg: []byte(`username:s:user1`),
expOutput: map[string]interface{}{
"username": "user1",
},
},
{
name: "integer",
cfg: []byte(`session bpp:i:32`),
expOutput: map[string]interface{}{
"session bpp": 32,
},
},
{
name: "multi",
cfg: []byte("compression:i:1\r\nusername:s:user2\r\n"),
expOutput: map[string]interface{}{
"compression": 1,
"username": "user2",
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
outMap, err := rdp.Unmarshal(tc.cfg)
assert.Equal(t, tc.err, err)
assert.Equal(t, tc.expOutput, outMap)
})
}
}
func TestRDP_Marshal(t *testing.T) {
testCases := []struct {
name string
input map[string]interface{}
output []byte
err error
}{
{
name: "Empty RDP",
input: map[string]interface{}{},
output: []byte(nil),
},
{
name: "Valid RDP all types",
input: map[string]interface{}{
"compression": 1,
"session bpp": 32,
"username": "user1",
},
output: []byte("compression:i:1\r\nsession bpp:i:32\r\nusername:s:user1\r\n"),
},
}
rdp := Parser()
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
out, err := rdp.Marshal(tc.input)
assert.Equal(t, tc.output, out)
assert.Equal(t, tc.err, err)
})
}
}

253
cmd/rdpgw/rdp/rdp.go Normal file
View File

@ -0,0 +1,253 @@
package rdp
import (
"errors"
"fmt"
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/rdp/koanf/parsers/rdp"
"github.com/fatih/structs"
"github.com/go-viper/mapstructure/v2"
"github.com/knadh/koanf/providers/file"
"github.com/knadh/koanf/v2"
"log"
"reflect"
"strconv"
"strings"
)
const (
CRLF = "\r\n"
)
const (
SourceNTLM int = iota
SourceSmartCard
SourceCurrent
SourceBasic
SourceUserSelect
SourceCookie
)
type RdpSettings struct {
AllowFontSmoothing bool `rdp:"allow font smoothing" default:"0"`
AllowDesktopComposition bool `rdp:"allow desktop composition" default:"0"`
DisableFullWindowDrag bool `rdp:"disable full window drag" default:"0"`
DisableMenuAnims bool `rdp:"disable menu anims" default:"0"`
DisableThemes bool `rdp:"disable themes" default:"0"`
DisableCursorSetting bool `rdp:"disable cursor setting" default:"0"`
GatewayHostname string `rdp:"gatewayhostname"`
FullAddress string `rdp:"full address"`
AlternateFullAddress string `rdp:"alternate full address"`
Username string `rdp:"username"`
Domain string `rdp:"domain"`
GatewayCredentialsSource int `rdp:"gatewaycredentialssource" default:"0"`
GatewayCredentialMethod int `rdp:"gatewayprofileusagemethod" default:"0"`
GatewayUsageMethod int `rdp:"gatewayusagemethod" default:"0"`
GatewayAccessToken string `rdp:"gatewayaccesstoken"`
PromptCredentialsOnce bool `rdp:"promptcredentialonce" default:"true"`
AuthenticationLevel int `rdp:"authentication level" default:"3"`
EnableCredSSPSupport bool `rdp:"enablecredsspsupport" default:"true"`
EnableRdsAasAuth bool `rdp:"enablerdsaadauth" default:"false"`
DisableConnectionSharing bool `rdp:"disableconnectionsharing" default:"false"`
AlternateShell string `rdp:"alternate shell"`
AutoReconnectionEnabled bool `rdp:"autoreconnectionenabled" default:"true"`
BandwidthAutodetect bool `rdp:"bandwidthautodetect" default:"true"`
NetworkAutodetect bool `rdp:"networkautodetect" default:"true"`
Compression bool `rdp:"compression" default:"true"`
VideoPlaybackMode bool `rdp:"videoplaybackmode" default:"true"`
ConnectionType int `rdp:"connection type" default:"2"`
AudioCaptureMode bool `rdp:"audiocapturemode" default:"false"`
EncodeRedirectedVideoCapture bool `rdp:"encode redirected video capture" default:"true"`
RedirectedVideoCaptureEncodingQuality int `rdp:"redirected video capture encoding quality" default:"0"`
AudioMode int `rdp:"audiomode" default:"0"`
CameraStoreRedirect string `rdp:"camerastoredirect" default:"false"`
DeviceStoreRedirect string `rdp:"devicestoredirect" default:"false"`
DriveStoreRedirect string `rdp:"drivestoredirect" default:"false"`
KeyboardHook int `rdp:"keyboardhook" default:"2"`
RedirectClipboard bool `rdp:"redirectclipboard" default:"true"`
RedirectComPorts bool `rdp:"redirectcomports" default:"false"`
RedirectLocation bool `rdp:"redirectlocation" default:"false"`
RedirectPrinters bool `rdp:"redirectprinters" default:"true"`
RedirectSmartcards bool `rdp:"redirectsmartcards" default:"true"`
RedirectWebAuthn bool `rdp:"redirectwebauthn" default:"true"`
UsbDeviceStoRedirect string `rdp:"usbdevicestoredirect"`
UseMultimon bool `rdp:"use multimon" default:"false"`
SelectedMonitors string `rdp:"selectedmonitors"`
MaximizeToCurrentDisplays bool `rdp:"maximizetocurrentdisplays" default:"false"`
SingleMonInWindowedMode bool `rdp:"singlemoninwindowedmode" default:"0"`
ScreenModeId int `rdp:"screen mode id" default:"2"`
SmartSizing bool `rdp:"smart sizing" default:"false"`
DynamicResolution bool `rdp:"dynamic resolution" default:"true"`
DesktopSizeId int `rdp:"desktop size id"`
DesktopHeight int `rdp:"desktopheight"`
DesktopWidth int `rdp:"desktopwidth"`
DesktopScaleFactor int `rdp:"desktopscalefactor"`
BitmapCacheSize int `rdp:"bitmapcachesize" default:"1500"`
BitmapCachePersistEnable bool `rdp:"bitmapcachepersistenable" default:"true"`
RemoteApplicationCmdLine string `rdp:"remoteapplicationcmdline"`
RemoteAppExpandWorkingDir bool `rdp:"remoteapplicationexpandworkingdir" default:"true"`
RemoteApplicationFile string `rdp:"remoteapplicationfile" default:"true"`
RemoteApplicationIcon string `rdp:"remoteapplicationicon"`
RemoteApplicationMode bool `rdp:"remoteapplicationmode" default:"false"`
RemoteApplicationName string `rdp:"remoteapplicationname"`
RemoteApplicationProgram string `rdp:"remoteapplicationprogram"`
}
type Builder struct {
Settings RdpSettings
Metadata mapstructure.Metadata
}
func NewBuilder() *Builder {
c := RdpSettings{}
initStruct(&c)
return &Builder{
Settings: c,
Metadata: mapstructure.Metadata{},
}
}
func NewBuilderFromFile(filename string) (*Builder, error) {
c := RdpSettings{}
initStruct(&c)
metadata := mapstructure.Metadata{}
decoderConfig := &mapstructure.DecoderConfig{
Result: &c,
Metadata: &metadata,
WeaklyTypedInput: true,
}
var k = koanf.New(".")
if err := k.Load(file.Provider(filename), rdp.Parser()); err != nil {
return nil, err
}
t := koanf.UnmarshalConf{Tag: "rdp", DecoderConfig: decoderConfig}
if err := k.UnmarshalWithConf("", &c, t); err != nil {
return nil, err
}
return &Builder{
Settings: c,
Metadata: metadata,
}, nil
}
func (rb *Builder) String() string {
var sb strings.Builder
addStructToString(rb.Settings, rb.Metadata, &sb)
return sb.String()
}
func addStructToString(st interface{}, metadata mapstructure.Metadata, sb *strings.Builder) {
s := structs.New(st)
for _, f := range s.Fields() {
if isZero(f) && !isSet(f, metadata) {
continue
}
sb.WriteString(f.Tag("rdp"))
sb.WriteString(":")
switch f.Kind() {
case reflect.String:
sb.WriteString("s:")
sb.WriteString(f.Value().(string))
case reflect.Int:
sb.WriteString("i:")
fmt.Fprintf(sb, "%d", f.Value())
case reflect.Bool:
sb.WriteString("i:")
if f.Value().(bool) {
sb.WriteString("1")
} else {
sb.WriteString("0")
}
}
sb.WriteString(CRLF)
}
}
func isZero(f *structs.Field) bool {
t := f.Tag("default")
if t == "" {
return f.IsZero()
}
switch f.Kind() {
case reflect.String:
if f.Value().(string) != t {
return false
}
return true
case reflect.Int:
i, err := strconv.Atoi(t)
if err != nil {
log.Fatalf("runtime error: default %s is not an integer", t)
}
if f.Value().(int) != i {
return false
}
return true
case reflect.Bool:
b := false
if t == "true" || t == "1" {
b = true
}
if f.Value().(bool) != b {
return false
}
return true
}
return f.IsZero()
}
func isSet(f *structs.Field, metadata mapstructure.Metadata) bool {
for _, v := range metadata.Unset {
if v == f.Name() {
log.Printf("field %s is unset", f.Name())
return true
}
}
return false
}
func initStruct(st interface{}) {
s := structs.New(st)
for _, f := range s.Fields() {
t := f.Tag("default")
if t == "" {
continue
}
err := setVariable(f, t)
if err != nil {
log.Fatalf("cannot init rdp struct: %s", err)
}
}
}
func setVariable(f *structs.Field, v string) error {
switch f.Kind() {
case reflect.String:
return f.Set(v)
case reflect.Int:
i, err := strconv.Atoi(v)
if err != nil {
return err
}
return f.Set(i)
case reflect.Bool:
b := false
if v == "true" || v == "1" {
b = true
}
return f.Set(b)
default:
return errors.New("invalid field type")
}
}

47
cmd/rdpgw/rdp/rdp_test.go Normal file
View File

@ -0,0 +1,47 @@
package rdp
import (
"log"
"strings"
"testing"
)
const (
GatewayHostName = "my.yahoo.com"
)
func TestRdpBuilder(t *testing.T) {
builder := NewBuilder()
builder.Settings.GatewayHostname = "my.yahoo.com"
builder.Settings.AutoReconnectionEnabled = true
builder.Settings.SmartSizing = true
s := builder.String()
if !strings.Contains(s, "gatewayhostname:s:"+GatewayHostName+CRLF) {
t.Fatalf("%s does not contain `gatewayhostname:s:%s", s, GatewayHostName)
}
if strings.Contains(s, "autoreconnectionenabled") {
t.Fatalf("autoreconnectionenabled is in %s, but it's default value", s)
}
if !strings.Contains(s, "smart sizing:i:1"+CRLF) {
t.Fatalf("%s does not contain smart sizing:i:1", s)
}
log.Printf(builder.String())
}
func TestInitStruct(t *testing.T) {
conn := RdpSettings{}
initStruct(&conn)
if conn.PromptCredentialsOnce != true {
t.Fatalf("conn.PromptCredentialsOnce != true")
}
}
func TestLoadFile(t *testing.T) {
_, err := NewBuilderFromFile("rdp_test_file.rdp")
if err != nil {
t.Fatalf("LoadFile failed: %v", err)
}
}

View File

@ -0,0 +1,37 @@
Password:b:0200000000000000000000000000000000000000000000000800000072006400700000000E660000100000001000000031A2D4A21767565E3A268420A9397C4400000000048000001000000010000000A56C359BBBA13EC284391427E6A107BD20000000333E6F6DA024E1B6B4CC7DDF57BFC1783ED02F212B8FBD39997C888F9D4B438914000000A80D19234BA4CC5CE2695A34EF0B9B92D5D777A6
ColorDepthID:i:1
ScreenStyle:i:0
DesktopWidth:i:640
DesktopHeight:i:480
UserName:s:rdesktop
SavePassword:i:1
Keyboard Layout:s:00000409
BitmapPersistCacheSize:i:1
BitmapCacheSize:i:21
KeyboardFunctionKey:i:12
KeyboardSubType:i:0
KeyboardType:i:4
KeyboardLayoutString:s:0xE0010409
Disable Themes:i:0
Disable Menu Anims:i:1
Disable Full Window Drag:i:1
Disable Wallpaper:i:1
MaxReconnectAttempts:i:20
KeyboardHookMode:i:0
Compress:i:1
BBarShowPinBtn:i:0
BitmapPersistenceEnabled:i:0
AudioRedirectionMode:i:2
EnablePortRedirection:i:0
EnableDriveRedirection:i:0
AutoReconnectEnabled:i:1
EnableSCardRedirection:i:1
EnablePrinterRedirection:i:0
BBarEnabled:i:0
DisableFileAccess:i:0
MinutesToIdleTimeout:i:5
GrabFocusOnConnect:i:0
StartFullScreen:i:1
Domain:s:GE3SDT8KLRL4J
enablecredsspsupport:i:0
use multimon:i:1

View File

@ -7,8 +7,8 @@ import (
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/identity" "github.com/bolkedebruin/rdpgw/cmd/rdpgw/identity"
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/protocol" "github.com/bolkedebruin/rdpgw/cmd/rdpgw/protocol"
"github.com/coreos/go-oidc/v3/oidc" "github.com/coreos/go-oidc/v3/oidc"
"github.com/go-jose/go-jose/v3" "github.com/go-jose/go-jose/v4"
"github.com/go-jose/go-jose/v3/jwt" "github.com/go-jose/go-jose/v4/jwt"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"log" "log"
"time" "time"
@ -62,9 +62,9 @@ func CheckPAACookie(ctx context.Context, tokenString string) (bool, error) {
return false, errors.New("no token to parse") return false, errors.New("no token to parse")
} }
token, err := jwt.ParseSigned(tokenString) token, err := jwt.ParseSigned(tokenString, []jose.SignatureAlgorithm{jose.HS256})
if err != nil { if err != nil {
log.Printf("cannot parse token due to: %tunnel", err) log.Printf("cannot parse token due to: %t", err)
return false, err return false, err
} }
@ -136,7 +136,7 @@ func GeneratePAAToken(ctx context.Context, username string, server string) (stri
AccessToken: id.GetAttribute(identity.AttrAccessToken).(string), AccessToken: id.GetAttribute(identity.AttrAccessToken).(string),
} }
if token, err := jwt.Signed(sig).Claims(standard).Claims(private).CompactSerialize(); err != nil { if token, err := jwt.Signed(sig).Claims(standard).Claims(private).Serialize(); err != nil {
log.Printf("Cannot sign PAA token %s", err) log.Printf("Cannot sign PAA token %s", err)
return "", err return "", err
} else { } else {
@ -157,7 +157,10 @@ func GenerateUserToken(ctx context.Context, userName string) (string, error) {
enc, err := jose.NewEncrypter( enc, err := jose.NewEncrypter(
jose.A128CBC_HS256, jose.A128CBC_HS256,
jose.Recipient{Algorithm: jose.DIRECT, Key: UserEncryptionKey}, jose.Recipient{
Algorithm: jose.DIRECT,
Key: UserEncryptionKey,
},
(&jose.EncrypterOptions{Compression: jose.DEFLATE}).WithContentType("JWT"), (&jose.EncrypterOptions{Compression: jose.DEFLATE}).WithContentType("JWT"),
) )
@ -167,16 +170,29 @@ func GenerateUserToken(ctx context.Context, userName string) (string, error) {
} }
// this makes the token bigger and we deal with a limited space of 511 characters // this makes the token bigger and we deal with a limited space of 511 characters
// sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.HS256, Key: SigningKey}, nil) if len(UserSigningKey) > 0 {
// token, err := jwt.SignedAndEncrypted(sig, enc).Claims(claims).CompactSerialize() sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.HS256, Key: UserSigningKey}, nil)
token, err := jwt.Encrypted(enc).Claims(claims).CompactSerialize() token, err := jwt.SignedAndEncrypted(sig, enc).Claims(claims).Serialize()
if len(token) > 511 {
log.Printf("WARNING: token too long: len %d > 511", len(token))
}
return token, err
}
// no signature
token, err := jwt.Encrypted(enc).Claims(claims).Serialize()
return token, err return token, err
} }
func UserInfo(ctx context.Context, token string) (jwt.Claims, error) { func UserInfo(ctx context.Context, token string) (jwt.Claims, error) {
standard := jwt.Claims{} standard := jwt.Claims{}
if len(UserEncryptionKey) > 0 && len(UserSigningKey) > 0 { if len(UserEncryptionKey) > 0 && len(UserSigningKey) > 0 {
enc, err := jwt.ParseSignedAndEncrypted(token) enc, err := jwt.ParseSignedAndEncrypted(
token,
[]jose.KeyAlgorithm{jose.DIRECT},
[]jose.ContentEncryption{jose.A128CBC_HS256},
[]jose.SignatureAlgorithm{jose.HS256},
)
if err != nil { if err != nil {
log.Printf("Cannot get token %s", err) log.Printf("Cannot get token %s", err)
return standard, errors.New("cannot get token") return standard, errors.New("cannot get token")
@ -186,16 +202,12 @@ func UserInfo(ctx context.Context, token string) (jwt.Claims, error) {
log.Printf("Cannot decrypt token %s", err) log.Printf("Cannot decrypt token %s", err)
return standard, errors.New("cannot decrypt token") return standard, errors.New("cannot decrypt token")
} }
if _, err := verifyAlg(token.Headers, string(jose.HS256)); err != nil {
log.Printf("signature validation failure: %s", err)
return standard, errors.New("signature validation failure")
}
if err = token.Claims(UserSigningKey, &standard); err != nil { if err = token.Claims(UserSigningKey, &standard); err != nil {
log.Printf("cannot verify signature %s", err) log.Printf("cannot verify signature %s", err)
return standard, errors.New("cannot verify signature") return standard, errors.New("cannot verify signature")
} }
} else if len(UserSigningKey) == 0 { } else if len(UserSigningKey) == 0 {
token, err := jwt.ParseEncrypted(token) token, err := jwt.ParseEncrypted(token, []jose.KeyAlgorithm{jose.DIRECT}, []jose.ContentEncryption{jose.A128CBC_HS256})
if err != nil { if err != nil {
log.Printf("Cannot get token %s", err) log.Printf("Cannot get token %s", err)
return standard, errors.New("cannot get token") return standard, errors.New("cannot get token")
@ -205,21 +217,6 @@ func UserInfo(ctx context.Context, token string) (jwt.Claims, error) {
log.Printf("Cannot decrypt token %s", err) log.Printf("Cannot decrypt token %s", err)
return standard, errors.New("cannot decrypt token") return standard, errors.New("cannot decrypt token")
} }
} else {
token, err := jwt.ParseSigned(token)
if err != nil {
log.Printf("Cannot get token %s", err)
return standard, errors.New("cannot get token")
}
if _, err := verifyAlg(token.Headers, string(jose.HS256)); err != nil {
log.Printf("signature validation failure: %s", err)
return standard, errors.New("signature validation failure")
}
err = token.Claims(UserSigningKey, &standard)
if err = token.Claims(UserSigningKey, &standard); err != nil {
log.Printf("cannot verify signature %s", err)
return standard, errors.New("cannot verify signature")
}
} }
// go-jose doesnt verify the expiry // go-jose doesnt verify the expiry
@ -238,15 +235,11 @@ func UserInfo(ctx context.Context, token string) (jwt.Claims, error) {
func QueryInfo(ctx context.Context, tokenString string, issuer string) (string, error) { func QueryInfo(ctx context.Context, tokenString string, issuer string) (string, error) {
standard := jwt.Claims{} standard := jwt.Claims{}
token, err := jwt.ParseSigned(tokenString) token, err := jwt.ParseSigned(tokenString, []jose.SignatureAlgorithm{jose.HS256})
if err != nil { if err != nil {
log.Printf("Cannot get token %s", err) log.Printf("Cannot get token %s", err)
return "", errors.New("cannot get token") return "", errors.New("cannot get token")
} }
if _, err := verifyAlg(token.Headers, string(jose.HS256)); err != nil {
log.Printf("signature validation failure: %s", err)
return "", errors.New("signature validation failure")
}
err = token.Claims(QuerySigningKey, &standard) err = token.Claims(QuerySigningKey, &standard)
if err = token.Claims(QuerySigningKey, &standard); err != nil { if err = token.Claims(QuerySigningKey, &standard); err != nil {
log.Printf("cannot verify signature %s", err) log.Printf("cannot verify signature %s", err)
@ -287,7 +280,7 @@ func GenerateQueryToken(ctx context.Context, query string, issuer string) (strin
return "", err return "", err
} }
token, err := jwt.Signed(sig).Claims(claims).CompactSerialize() token, err := jwt.Signed(sig).Claims(claims).Serialize()
return token, err return token, err
} }
@ -299,12 +292,3 @@ func getTunnel(ctx context.Context) *protocol.Tunnel {
} }
return s return s
} }
func verifyAlg(headers []jose.Header, alg string) (bool, error) {
for _, header := range headers {
if header.Algorithm != alg {
return false, fmt.Errorf("invalid signing method %s", header.Algorithm)
}
}
return true, nil
}

View File

@ -0,0 +1,76 @@
package security
import (
"context"
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/identity"
"testing"
)
func TestGenerateUserToken(t *testing.T) {
cases := []struct {
SigningKey []byte
EncryptionKey []byte
name string
username string
}{
{
SigningKey: []byte("5aa3a1568fe8421cd7e127d5ace28d2d"),
EncryptionKey: []byte("d3ecd7e565e56e37e2f2e95b584d8c0c"),
name: "sign_and_encrypt",
username: "test_sign_and_encrypt",
},
{
SigningKey: nil,
EncryptionKey: []byte("d3ecd7e565e56e37e2f2e95b584d8c0c"),
name: "encrypt_only",
username: "test_encrypt_only",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
SigningKey = tc.SigningKey
UserEncryptionKey = tc.EncryptionKey
token, err := GenerateUserToken(context.Background(), tc.username)
if err != nil {
t.Fatalf("GenerateUserToken failed: %s", err)
}
claims, err := UserInfo(context.Background(), token)
if err != nil {
t.Fatalf("UserInfo failed: %s", err)
}
if claims.Subject != tc.username {
t.Fatalf("Expected %s, got %s", tc.username, claims.Subject)
}
})
}
}
func TestPAACookie(t *testing.T) {
SigningKey = []byte("5aa3a1568fe8421cd7e127d5ace28d2d")
EncryptionKey = []byte("d3ecd7e565e56e37e2f2e95b584d8c0c")
username := "test_paa_cookie"
attr_client_ip := "127.0.0.1"
attr_access_token := "aabbcc"
id := identity.NewUser()
id.SetUserName(username)
id.SetAttribute(identity.AttrClientIp, attr_client_ip)
id.SetAttribute(identity.AttrAccessToken, attr_access_token)
ctx := context.Background()
ctx = context.WithValue(ctx, identity.CTXKey, id)
_, err := GeneratePAAToken(ctx, "test_paa_cookie", "host.does.not.exist")
if err != nil {
t.Fatalf("GeneratePAAToken failed: %s", err)
}
/*ok, err := CheckPAACookie(ctx, token)
if err != nil {
t.Fatalf("CheckPAACookie failed: %s", err)
}
if !ok {
t.Fatalf("CheckPAACookie failed")
}*/
}

View File

@ -2,9 +2,9 @@ package transport
import ( import (
"bufio" "bufio"
"crypto/rand"
"errors" "errors"
"io" "io"
"math/rand"
"net" "net"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"

View File

@ -18,38 +18,16 @@ const (
type BasicAuthHandler struct { type BasicAuthHandler struct {
SocketAddress string SocketAddress string
Timeout int
} }
func (h *BasicAuthHandler) BasicAuth(next http.HandlerFunc) http.HandlerFunc { func (h *BasicAuthHandler) BasicAuth(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
username, password, ok := r.BasicAuth() username, password, ok := r.BasicAuth()
if ok { if ok {
ctx := r.Context() authenticated := h.authenticate(w, r, username, password)
conn, err := grpc.Dial(h.SocketAddress, grpc.WithTransportCredentials(insecure.NewCredentials()), if !authenticated {
grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
return net.Dial(protocolGrpc, addr)
}))
if err != nil {
log.Printf("Cannot reach authentication provider: %s", err)
http.Error(w, "Server error", http.StatusInternalServerError)
return
}
defer conn.Close()
c := auth.NewAuthenticateClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
req := &auth.UserPass{Username: username, Password: password}
res, err := c.Authenticate(ctx, req)
if err != nil {
log.Printf("Error talking to authentication provider: %s", err)
http.Error(w, "Server error", http.StatusInternalServerError)
return
}
if !res.Authenticated {
log.Printf("User %s is not authenticated for this service", username) log.Printf("User %s is not authenticated for this service", username)
} else { } else {
log.Printf("User %s authenticated", username) log.Printf("User %s authenticated", username)
@ -60,7 +38,6 @@ func (h *BasicAuthHandler) BasicAuth(next http.HandlerFunc) http.HandlerFunc {
next.ServeHTTP(w, identity.AddToRequestCtx(id, r)) next.ServeHTTP(w, identity.AddToRequestCtx(id, r))
return return
} }
} }
// If the Authentication header is not present, is invalid, or the // If the Authentication header is not present, is invalid, or the
// username or password is wrong, then set a WWW-Authenticate // username or password is wrong, then set a WWW-Authenticate
@ -70,3 +47,36 @@ func (h *BasicAuthHandler) BasicAuth(next http.HandlerFunc) http.HandlerFunc {
http.Error(w, "Unauthorized", http.StatusUnauthorized) http.Error(w, "Unauthorized", http.StatusUnauthorized)
} }
} }
func (h *BasicAuthHandler) authenticate(w http.ResponseWriter, r *http.Request, username string, password string) (authenticated bool) {
if h.SocketAddress == "" {
return false
}
ctx := r.Context()
conn, err := grpc.Dial(h.SocketAddress, grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
return net.Dial(protocolGrpc, addr)
}))
if err != nil {
log.Printf("Cannot reach authentication provider: %s", err)
http.Error(w, "Server error", http.StatusInternalServerError)
return false
}
defer conn.Close()
c := auth.NewAuthenticateClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(h.Timeout))
defer cancel()
req := &auth.UserPass{Username: username, Password: password}
res, err := c.Authenticate(ctx, req)
if err != nil {
log.Printf("Error talking to authentication provider: %s", err)
http.Error(w, "Server error", http.StatusInternalServerError)
return false
}
return res.Authenticated
}

120
cmd/rdpgw/web/ntlm.go Normal file
View File

@ -0,0 +1,120 @@
package web
import (
"context"
"errors"
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/identity"
"github.com/bolkedebruin/rdpgw/shared/auth"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"log"
"net"
"net/http"
"time"
)
type ntlmAuthMode uint32
const (
authNone ntlmAuthMode = iota
authNTLM
authNegotiate
)
type NTLMAuthHandler struct {
SocketAddress string
Timeout int
}
func (h *NTLMAuthHandler) NTLMAuth(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
authPayload, authMode, err := h.getAuthPayload(r)
if err != nil {
log.Printf("Failed parsing auth header: %s", err)
h.requestAuthenticate(w)
return
}
authenticated, username := h.authenticate(w, r, authPayload, authMode)
if authenticated {
log.Printf("NTLM: User %s authenticated", username)
id := identity.FromRequestCtx(r)
id.SetUserName(username)
id.SetAuthenticated(true)
id.SetAuthTime(time.Now())
next.ServeHTTP(w, identity.AddToRequestCtx(id, r))
}
}
}
func (h *NTLMAuthHandler) getAuthPayload (r *http.Request) (payload string, authMode ntlmAuthMode, err error) {
authorisationEncoded := r.Header.Get("Authorization")
if authorisationEncoded[0:5] == "NTLM " {
return authorisationEncoded[5:], authNTLM, nil
}
if authorisationEncoded[0:10] == "Negotiate " {
return authorisationEncoded[10:], authNegotiate, nil
}
return "", authNone, errors.New("Invalid NTLM Authorisation header")
}
func (h *NTLMAuthHandler) requestAuthenticate (w http.ResponseWriter) {
w.Header().Add("WWW-Authenticate", `NTLM`)
w.Header().Add("WWW-Authenticate", `Negotiate`)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
}
func (h *NTLMAuthHandler) getAuthPrefix (authMode ntlmAuthMode) (prefix string) {
if authMode == authNTLM {
return "NTLM "
}
if authMode == authNegotiate {
return "Negotiate "
}
return ""
}
func (h *NTLMAuthHandler) authenticate(w http.ResponseWriter, r *http.Request, authorisationEncoded string, authMode ntlmAuthMode) (authenticated bool, username string) {
if h.SocketAddress == "" {
return false, ""
}
ctx := r.Context()
conn, err := grpc.Dial(h.SocketAddress, grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
return net.Dial(protocolGrpc, addr)
}))
if err != nil {
log.Printf("Cannot reach authentication provider: %s", err)
http.Error(w, "Server error", http.StatusInternalServerError)
return false, ""
}
defer conn.Close()
c := auth.NewAuthenticateClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(h.Timeout))
defer cancel()
req := &auth.NtlmRequest{Session: r.RemoteAddr, NtlmMessage: authorisationEncoded}
res, err := c.NTLM(ctx, req)
if err != nil {
log.Printf("Error talking to authentication provider: %s", err)
http.Error(w, "Server error", http.StatusInternalServerError)
return false, ""
}
if res.NtlmMessage != "" {
log.Printf("Sending NTLM challenge")
w.Header().Add("WWW-Authenticate", h.getAuthPrefix(authMode)+res.NtlmMessage)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return false, ""
}
if !res.Authenticated {
h.requestAuthenticate(w)
return false, ""
}
return res.Authenticated, res.Username
}

View File

@ -1,13 +1,13 @@
package web package web
import ( import (
"crypto/rand"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/identity" "github.com/bolkedebruin/rdpgw/cmd/rdpgw/identity"
"github.com/coreos/go-oidc/v3/oidc" "github.com/coreos/go-oidc/v3/oidc"
"github.com/patrickmn/go-cache" "github.com/patrickmn/go-cache"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"math/rand"
"net/http" "net/http"
"time" "time"
) )
@ -99,7 +99,7 @@ func (h *OIDC) HandleCallback(w http.ResponseWriter, r *http.Request) {
} }
func findUsernameInClaims(data map[string]interface{}) string { func findUsernameInClaims(data map[string]interface{}) string {
candidates := []string{"preferred_username", "unique_name", "upn"} candidates := []string{"preferred_username", "unique_name", "upn", "username"}
for _, claim := range candidates { for _, claim := range candidates {
userName, found := data[claim].(string) userName, found := data[claim].(string)
if found { if found {
@ -116,7 +116,11 @@ func (h *OIDC) Authenticated(next http.Handler) http.Handler {
if !id.Authenticated() { if !id.Authenticated() {
seed := make([]byte, 16) seed := make([]byte, 16)
rand.Read(seed) _, err := rand.Read(seed)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
state := hex.EncodeToString(seed) state := hex.EncodeToString(seed)
h.stateStore.Set(state, r.RequestURI, cache.DefaultExpiration) h.stateStore.Set(state, r.RequestURI, cache.DefaultExpiration)
http.Redirect(w, r, h.oAuth2Config.AuthCodeURL(state), http.StatusFound) http.Redirect(w, r, h.oAuth2Config.AuthCodeURL(state), http.StatusFound)

View File

@ -1,229 +0,0 @@
package web
import (
"fmt"
"github.com/fatih/structs"
"log"
"reflect"
"strconv"
"strings"
)
const (
crlf = "\r\n"
)
const (
SourceNTLM int = iota
SourceSmartCard
SourceCurrent
SourceBasic
SourceUserSelect
SourceCookie
)
type RdpConnection struct {
GatewayHostname string `rdp:"gatewayhostname"`
FullAddress string `rdp:"full address"`
AlternateFullAddress string `rdp:"alternate full address"`
Username string `rdp:"username"`
Domain string `rdp:"domain"`
GatewayCredentialsSource int `rdp:"gatewaycredentialssource" default:"0"`
GatewayCredentialMethod int `rdp:"gatewayprofileusagemethod" default:"0"`
GatewayUsageMethod int `rdp:"gatewayusagemethod" default:"0"`
GatewayAccessToken string `rdp:"gatewayaccesstoken"`
PromptCredentialsOnce bool `rdp:"promptcredentialonce" default:"true"`
AuthenticationLevel int `rdp:"authentication level" default:"3"`
EnableCredSSPSupport bool `rdp:"enablecredsspsupport" default:"true"`
EnableRdsAasAuth bool `rdp:"enablerdsaadauth" default:"false"`
DisableConnectionSharing bool `rdp:"disableconnectionsharing" default:"false"`
AlternateShell string `rdp:"alternate shell"`
}
type RdpSession struct {
AutoReconnectionEnabled bool `rdp:"autoreconnectionenabled" default:"true"`
BandwidthAutodetect bool `rdp:"bandwidthautodetect" default:"true"`
NetworkAutodetect bool `rdp:"networkautodetect" default:"true"`
Compression bool `rdp:"compression" default:"true"`
VideoPlaybackMode bool `rdp:"videoplaybackmode" default:"true"`
ConnectionType int `rdp:"connection type" default:"2"`
}
type RdpDeviceRedirect struct {
AudioCaptureMode bool `rdp:"audiocapturemode" default:"false"`
EncodeRedirectedVideoCapture bool `rdp:"encode redirected video capture" default:"true"`
RedirectedVideoCaptureEncodingQuality int `rdp:"redirected video capture encoding quality" default:"0"`
AudioMode int `rdp:"audiomode" default:"0"`
CameraStoreRedirect string `rdp:"camerastoredirect" default:"false"`
DeviceStoreRedirect string `rdp:"devicestoredirect" default:"false"`
DriveStoreRedirect string `rdp:"drivestoredirect" default:"false"`
KeyboardHook int `rdp:"keyboardhook" default:"2"`
RedirectClipboard bool `rdp:"redirectclipboard" default:"true"`
RedirectComPorts bool `rdp:"redirectcomports" default:"false"`
RedirectLocation bool `rdp:"redirectlocation" default:"false"`
RedirectPrinters bool `rdp:"redirectprinters" default:"true"`
RedirectSmartcards bool `rdp:"redirectsmartcards" default:"true"`
RedirectWebAuthn bool `rdp:"redirectwebauthn" default:"true"`
UsbDeviceStoRedirect string `rdp:"usbdevicestoredirect"`
}
type RdpDisplay struct {
UseMultimon bool `rdp:"use multimon" default:"false"`
SelectedMonitors string `rdp:"selectedmonitors"`
MaximizeToCurrentDisplays bool `rdp:"maximizetocurrentdisplays" default:"false"`
SingleMonInWindowedMode bool `rdp:"singlemoninwindowedmode" default:"0"`
ScreenModeId int `rdp:"screen mode id" default:"2"`
SmartSizing bool `rdp:"smart sizing" default:"false"`
DynamicResolution bool `rdp:"dynamic resolution" default:"true"`
DesktopSizeId int `rdp:"desktop size id"`
DesktopHeight int `rdp:"desktopheight"`
DesktopWidth int `rdp:"desktopwidth"`
DesktopScaleFactor int `rdp:"desktopscalefactor"`
BitmapCacheSize int `rdp:"bitmapcachesize" default:"1500"`
}
type RdpRemoteApp struct {
RemoteApplicationCmdLine string `rdp:"remoteapplicationcmdline"`
RemoteAppExpandWorkingDir bool `rdp:"remoteapplicationexpandworkingdir" default:"true"`
RemoteApplicationFile string `rdp:"remoteapplicationfile" default:"true"`
RemoteApplicationIcon string `rdp:"remoteapplicationicon"`
RemoteApplicationMode bool `rdp:"remoteapplicationmode" default:"true"`
RemoteApplicationName string `rdp:"remoteapplicationname"`
RemoteApplicationProgram string `rdp:"remoteapplicationprogram"`
}
type RdpBuilder struct {
Connection RdpConnection
Session RdpSession
DeviceRedirect RdpDeviceRedirect
Display RdpDisplay
RemoteApp RdpRemoteApp
}
func NewRdp() *RdpBuilder {
c := RdpConnection{}
s := RdpSession{}
dr := RdpDeviceRedirect{}
disp := RdpDisplay{}
ra := RdpRemoteApp{}
initStruct(&c)
initStruct(&s)
initStruct(&dr)
initStruct(&disp)
initStruct(&ra)
return &RdpBuilder{
Connection: c,
Session: s,
DeviceRedirect: dr,
Display: disp,
RemoteApp: ra,
}
}
func (rb *RdpBuilder) String() string {
var sb strings.Builder
addStructToString(rb.Connection, &sb)
addStructToString(rb.Session, &sb)
addStructToString(rb.DeviceRedirect, &sb)
addStructToString(rb.Display, &sb)
addStructToString(rb.RemoteApp, &sb)
return sb.String()
}
func addStructToString(st interface{}, sb *strings.Builder) {
s := structs.New(st)
for _, f := range s.Fields() {
if isZero(f) {
continue
}
sb.WriteString(f.Tag("rdp"))
sb.WriteString(":")
switch f.Kind() {
case reflect.String:
sb.WriteString("s:")
sb.WriteString(f.Value().(string))
case reflect.Int:
sb.WriteString("i:")
fmt.Fprintf(sb, "%d", f.Value())
case reflect.Bool:
sb.WriteString("i:")
if f.Value().(bool) {
sb.WriteString("1")
} else {
sb.WriteString("0")
}
}
sb.WriteString(crlf)
}
}
func isZero(f *structs.Field) bool {
t := f.Tag("default")
if t == "" {
return f.IsZero()
}
switch f.Kind() {
case reflect.String:
if f.Value().(string) != t {
return false
}
return true
case reflect.Int:
i, err := strconv.Atoi(t)
if err != nil {
log.Fatalf("runtime error: default %s is not an integer", t)
}
if f.Value().(int) != i {
return false
}
return true
case reflect.Bool:
b := false
if t == "true" || t == "1" {
b = true
}
if f.Value().(bool) != b {
return false
}
return true
}
return f.IsZero()
}
func initStruct(st interface{}) {
s := structs.New(st)
for _, f := range s.Fields() {
t := f.Tag("default")
if t == "" {
continue
}
switch f.Kind() {
case reflect.String:
f.Set(t)
case reflect.Int:
i, err := strconv.Atoi(t)
if err != nil {
log.Fatalf("runtime error: default %s is not an integer", t)
}
f.Set(i)
case reflect.Bool:
b := false
if t == "true" || t == "1" {
b = true
}
err := f.Set(b)
if err != nil {
log.Fatalf("Cannot set bool field")
}
}
}
}

View File

@ -1,40 +0,0 @@
package web
import (
"log"
"strings"
"testing"
)
const (
GatewayHostName = "my.yahoo.com"
)
func TestRdpBuilder(t *testing.T) {
builder := NewRdp()
builder.Connection.GatewayHostname = "my.yahoo.com"
builder.Session.AutoReconnectionEnabled = true
builder.Display.SmartSizing = true
s := builder.String()
if !strings.Contains(s, "gatewayhostname:s:"+GatewayHostName+crlf) {
t.Fatalf("%s does not contain `gatewayhostname:s:%s", s, GatewayHostName)
}
if strings.Contains(s, "autoreconnectionenabled") {
t.Fatalf("autoreconnectionenabled is in %s, but is default value", s)
}
if !strings.Contains(s, "smart sizing:i:1"+crlf) {
t.Fatalf("%s does not contain smart sizing:i:1", s)
}
log.Printf(builder.String())
}
func TestInitStruct(t *testing.T) {
conn := RdpConnection{}
initStruct(&conn)
if conn.PromptCredentialsOnce != true {
t.Fatalf("conn.PromptCredentialsOnce != true")
}
}

View File

@ -2,12 +2,15 @@ package web
import ( import (
"context" "context"
"crypto/rand"
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt" "fmt"
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/identity" "github.com/bolkedebruin/rdpgw/cmd/rdpgw/identity"
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/rdp"
"hash/maphash"
"log" "log"
"math/rand" rnd "math/rand"
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
@ -28,15 +31,13 @@ type Config struct {
HostSelection string HostSelection string
GatewayAddress *url.URL GatewayAddress *url.URL
RdpOpts RdpOpts RdpOpts RdpOpts
TemplateFile string
} }
type RdpOpts struct { type RdpOpts struct {
UsernameTemplate string UsernameTemplate string
SplitUserDomain bool SplitUserDomain bool
DefaultDomain string NoUsername bool
NetworkAutoDetect int
BandwidthAutoDetect int
ConnectionType int
} }
type Handler struct { type Handler struct {
@ -49,12 +50,14 @@ type Handler struct {
hosts []string hosts []string
hostSelection string hostSelection string
rdpOpts RdpOpts rdpOpts RdpOpts
rdpDefaults string
} }
func (c *Config) NewHandler() *Handler { func (c *Config) NewHandler() *Handler {
if len(c.Hosts) < 1 { if len(c.Hosts) < 1 {
log.Fatal("Not enough hosts to connect to specified") log.Fatal("Not enough hosts to connect to specified")
} }
return &Handler{ return &Handler{
paaTokenGenerator: c.PAATokenGenerator, paaTokenGenerator: c.PAATokenGenerator,
enableUserToken: c.EnableUserToken, enableUserToken: c.EnableUserToken,
@ -65,12 +68,13 @@ func (c *Config) NewHandler() *Handler {
hosts: c.Hosts, hosts: c.Hosts,
hostSelection: c.HostSelection, hostSelection: c.HostSelection,
rdpOpts: c.RdpOpts, rdpOpts: c.RdpOpts,
rdpDefaults: c.TemplateFile,
} }
} }
func (h *Handler) selectRandomHost() string { func (h *Handler) selectRandomHost() string {
rand.Seed(time.Now().Unix()) r := rnd.New(rnd.NewSource(int64(new(maphash.Hash).Sum64())))
host := h.hosts[rand.Intn(len(h.hosts))] host := h.hosts[r.Intn(len(h.hosts))]
return host return host
} }
@ -145,7 +149,7 @@ func (h *Handler) HandleDownload(w http.ResponseWriter, r *http.Request) {
// split the username into user and domain // split the username into user and domain
var user = id.UserName() var user = id.UserName()
var domain = opts.DefaultDomain var domain = ""
if opts.SplitUserDomain { if opts.SplitUserDomain {
creds := strings.SplitN(id.UserName(), "@", 2) creds := strings.SplitN(id.UserName(), "@", 2)
user = creds[0] user = creds[0]
@ -169,6 +173,7 @@ func (h *Handler) HandleDownload(w http.ResponseWriter, r *http.Request) {
if err != nil { if err != nil {
log.Printf("Cannot generate PAA token for user %s due to %s", user, err) log.Printf("Cannot generate PAA token for user %s due to %s", user, err)
http.Error(w, errors.New("unable to generate gateway credentials").Error(), http.StatusInternalServerError) http.Error(w, errors.New("unable to generate gateway credentials").Error(), http.StatusInternalServerError)
return
} }
if h.enableUserToken { if h.enableUserToken {
@ -176,32 +181,48 @@ func (h *Handler) HandleDownload(w http.ResponseWriter, r *http.Request) {
if err != nil { if err != nil {
log.Printf("Cannot generate token for user %s due to %s", user, err) log.Printf("Cannot generate token for user %s due to %s", user, err)
http.Error(w, errors.New("unable to generate gateway credentials").Error(), http.StatusInternalServerError) http.Error(w, errors.New("unable to generate gateway credentials").Error(), http.StatusInternalServerError)
return
} }
render = strings.Replace(render, "{{ token }}", userToken, 1) render = strings.Replace(render, "{{ token }}", userToken, 1)
} }
// authenticated // authenticated
seed := make([]byte, 16) seed := make([]byte, 16)
rand.Read(seed) _, err = rand.Read(seed)
if err != nil {
log.Printf("Cannot generate random seed due to %s", err)
http.Error(w, errors.New("unable to generate random sequence").Error(), http.StatusInternalServerError)
return
}
fn := hex.EncodeToString(seed) + ".rdp" fn := hex.EncodeToString(seed) + ".rdp"
w.Header().Set("Content-Disposition", "attachment; filename="+fn) w.Header().Set("Content-Disposition", "attachment; filename="+fn)
w.Header().Set("Content-Type", "application/x-rdp") w.Header().Set("Content-Type", "application/x-rdp")
rdp := NewRdp() var d *rdp.Builder
rdp.Connection.Username = render if h.rdpDefaults == "" {
rdp.Connection.Domain = domain d = rdp.NewBuilder()
rdp.Connection.FullAddress = host } else {
rdp.Connection.GatewayHostname = h.gatewayAddress.Host d, err = rdp.NewBuilderFromFile(h.rdpDefaults)
rdp.Connection.GatewayCredentialsSource = SourceCookie if err != nil {
rdp.Connection.GatewayAccessToken = token log.Printf("Cannot load RDP template file %s due to %s", h.rdpDefaults, err)
rdp.Connection.GatewayCredentialMethod = 1 http.Error(w, errors.New("unable to load RDP template").Error(), http.StatusInternalServerError)
rdp.Connection.GatewayUsageMethod = 1 return
rdp.Session.NetworkAutodetect = opts.NetworkAutoDetect != 0 }
rdp.Session.BandwidthAutodetect = opts.BandwidthAutoDetect != 0 }
rdp.Session.ConnectionType = opts.ConnectionType
rdp.Display.SmartSizing = true if !h.rdpOpts.NoUsername {
rdp.Display.BitmapCacheSize = 32000 d.Settings.Username = render
if domain != "" {
http.ServeContent(w, r, fn, time.Now(), strings.NewReader(rdp.String())) d.Settings.Domain = domain
}
}
d.Settings.FullAddress = host
d.Settings.GatewayHostname = h.gatewayAddress.Host
d.Settings.GatewayCredentialsSource = rdp.SourceCookie
d.Settings.GatewayAccessToken = token
d.Settings.GatewayCredentialMethod = 1
d.Settings.GatewayUsageMethod = 1
http.ServeContent(w, r, fn, time.Now(), strings.NewReader(d.String()))
} }

View File

@ -3,10 +3,12 @@ package web
import ( import (
"context" "context"
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/identity" "github.com/bolkedebruin/rdpgw/cmd/rdpgw/identity"
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/rdp"
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/security" "github.com/bolkedebruin/rdpgw/cmd/rdpgw/security"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url" "net/url"
"os"
"strings" "strings"
"testing" "testing"
) )
@ -149,7 +151,7 @@ func TestHandler_HandleDownload(t *testing.T) {
t.Errorf("content disposition is nil") t.Errorf("content disposition is nil")
} }
data := rdpToMap(strings.Split(rr.Body.String(), crlf)) data := rdpToMap(strings.Split(rr.Body.String(), rdp.CRLF))
if data["username"] != testuser { if data["username"] != testuser {
t.Errorf("username key in rdp does not match: got %v want %v", data["username"], testuser) t.Errorf("username key in rdp does not match: got %v want %v", data["username"], testuser)
} }
@ -170,6 +172,51 @@ func TestHandler_HandleDownload(t *testing.T) {
} }
func TestHandler_HandleDownloadWithRdpTemplate(t *testing.T) {
f, err := os.CreateTemp("", "rdp")
if err != nil {
t.Fatal(err)
}
defer os.Remove(f.Name())
err = os.WriteFile(f.Name(), []byte("domain:s:testdomain\r\n"), 0644)
if err != nil {
t.Fatal(err)
}
req, err := http.NewRequest("GET", "/connect", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
id := identity.NewUser()
id.SetUserName(testuser)
id.SetAuthenticated(true)
req = identity.AddToRequestCtx(id, req)
u, _ := url.Parse(gateway)
c := Config{
HostSelection: "roundrobin",
Hosts: hosts,
PAATokenGenerator: paaTokenMock,
GatewayAddress: u,
RdpOpts: RdpOpts{SplitUserDomain: true},
TemplateFile: f.Name(),
}
h := c.NewHandler()
hh := http.HandlerFunc(h.HandleDownload)
hh.ServeHTTP(rr, req)
data := rdpToMap(strings.Split(rr.Body.String(), rdp.CRLF))
if data["domain"] != "testdomain" {
t.Errorf("domain key in rdp does not match: got %v want %v", data["domain"], "testdomain")
}
}
func paaTokenMock(ctx context.Context, username string, host string) (string, error) { func paaTokenMock(ctx context.Context, username string, host string) (string, error) {
return username + "_" + host, nil return username + "_" + host, nil
} }

11
debian/changelog vendored Normal file
View File

@ -0,0 +1,11 @@
rdpgw (2.0.3) UNRELEASED; urgency=medium
* add Dynamic Gateway
-- Lierfang Support Team <itsupport@lierfang.com> Fri, 13 Jun 2025 15:56:06 +0800
rdpgw (2.0.2) UNSTABLE; urgency=medium
* init
-- Jiangcuo <Jiangcuo@lierfang.com> Mon, 03 Feb 2025 13:56:49 +0800

21
debian/control vendored Normal file
View File

@ -0,0 +1,21 @@
Source: rdpgw
Section: admin
Priority: optional
Maintainer: Lierfang <it_support@lierfang.com>
Homepage: https://github.com/bolkedebruin/rdpgw
Build-Depends: debhelper-compat (= 12),
golang ( >= 1.23.5-1~bpo12+1 ),
build-essential,
libpam0g-dev,
dh-golang
Package: rdpgw
Architecture: any
Depends: dbus,
openssl,
${misc:Depends},
${shlibs:Depends},
Description: rdpgw
RDPGW is an implementation of the Remote Desktop Gateway protocol.
This allows you to connect with the official Microsoft clients to remote desktops over HTTPS.
These desktops could be, for example, XRDP desktops running in containers on Kubernetes.

11
debian/copyright vendored Normal file
View File

@ -0,0 +1,11 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Source: https://github.com/bolkedebruin/rdpgw
Files: *
Copyright: bolkedebruin
License: Apache-2.0
Files: debian/*
Copyright: Lierfang <service@lierfang.com>
License: Apache-2.0

5
debian/install vendored Normal file
View File

@ -0,0 +1,5 @@
bin/rdpgw usr/bin/
bin/rdpgw-auth usr/sbin/
debian/rdpgw.yaml etc/rdpgw/
debian/rdpgw-auth.yaml etc/rdpgw/
debian/rdpgw-auth.service lib/systemd/system/

21
debian/postinst vendored Normal file
View File

@ -0,0 +1,21 @@
#!/bin/sh
set -e
#DEBHELPER#
case "$1" in
configure)
if [ ! -f "/etc/rdpgw/server.pem" ]; then
random=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1)
openssl genrsa -des3 -passout pass:$random -out /tmp/server.pass.key 2048
openssl rsa -passin pass:$random -in /tmp/server.pass.key -out /etc/rdpgw/key.pem
rm /tmp/server.pass.key
openssl req -new -sha256 -key /etc/rdpgw/key.pem -out /etc/rdpgw/server.csr -subj "/C=US/ST=VA/L=SomeCity/O=MyCompany/OU=MyDivision/CN=rdpgw"
openssl x509 -req -days 365 -in /etc/rdpgw/server.csr -signkey /etc/rdpgw/key.pem -out /etc/rdpgw/server.pem
fi
deb-systemd-invoke reload-or-try-restart rdpgw-auth.service || true
deb-systemd-invoke reload-or-try-restart rdpgw.service || true
;;
esac

16
debian/rdpgw-auth.service vendored Normal file
View File

@ -0,0 +1,16 @@
[Unit]
Description=RDP Gateway Auth Service
After=network.target
StartLimitBurst=5
StartLimitInterval=10s
[Service]
Type=simple
User=root
ExecStart=/usr/sbin/rdpgw-auth -c /etc/rdpgw/rdpgw-auth.yaml -s /run/rdpgw-auth.sock
Restart=on-failure
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target

6
debian/rdpgw-auth.yaml vendored Normal file
View File

@ -0,0 +1,6 @@
PXVDI:
Enabled: true
apiUrl: "https://10.13.16.164:3002"
apiKey: "dasdasdasdas"
Users:
- {Username: "debian-rdpgw-start", Password: "debian-rdpgw-password"}

16
debian/rdpgw.service vendored Normal file
View File

@ -0,0 +1,16 @@
[Unit]
Description=RDP Gateway Service
After=network.target
StartLimitBurst=5
StartLimitInterval=10s
[Service]
Type=simple
User=root
ExecStart=/usr/bin/rdpgw -c /etc/rdpgw/rdpgw.yaml
Restart=on-failure
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target

30
debian/rdpgw.yaml vendored Normal file
View File

@ -0,0 +1,30 @@
Server:
Authentication:
- ntlm
BasicAuthTimeout: 5
AuthSocket: /run/rdpgw-auth.sock
GatewayAddress: localhost
Port: 443
Hosts:
- localhost:3389
HostSelection: any
Tls: enable
CertFile: /etc/rdpgw/server.pem
KeyFile: /etc/rdpgw/key.pem
Caps:
SmartCardAuth: false
TokenAuth: false
IdleTimeout: 10
EnablePrinter: true
EnablePort: true
EnablePnp: true
EnableDrive: true
EnableClipboard: true
Client:
UsernameTemplate: "{{ username }}"
SplitUserDomain: false
Security:
PAATokenSigningKey: thisisasessionkeyreplacethisjetzt
UserTokenEncryptionKey: thisisasessionkeyreplacethisjetzt
EnableUserToken: false
VerifyClientIp: trufalsee

19
debian/rules vendored Executable file
View File

@ -0,0 +1,19 @@
#!/usr/bin/make -f
# Uncomment this to turn on verbose mode.
#export DH_VERBOSE=1
include /usr/share/dpkg/pkg-info.mk
%:
dh $@
override_dh_auto_build:
override_dh_auto_test:
override_dh_auto_install:
override_dh_auto_clean:
override_dh_dwz:

View File

@ -1,7 +1,8 @@
# builder stage # builder stage
FROM golang as builder FROM golang:1.22-alpine as builder
RUN apt-get update && apt-get install -y libpam-dev #RUN apt-get update && apt-get install -y libpam-dev
RUN apk --no-cache add git gcc musl-dev linux-pam-dev openssl
# add user # add user
RUN adduser --disabled-password --gecos "" --home /opt/rdpgw --uid 1001 rdpgw RUN adduser --disabled-password --gecos "" --home /opt/rdpgw --uid 1001 rdpgw
@ -27,18 +28,22 @@ RUN git clone https://github.com/bolkedebruin/rdpgw.git /app && \
chmod +x /opt/rdpgw/rdpgw-auth && \ chmod +x /opt/rdpgw/rdpgw-auth && \
chmod u+s /opt/rdpgw/rdpgw-auth chmod u+s /opt/rdpgw/rdpgw-auth
FROM scratch FROM alpine:latest
RUN apk --no-cache add linux-pam musl
# make tempdir in case filestore is used # make tempdir in case filestore is used
ADD tmp.tar / ADD tmp.tar /
USER 1001 COPY --chown=0 rdpgw-pam /etc/pam.d/rdpgw
USER 1001
COPY --chown=1001 run.sh run.sh
COPY --chown=1001 --from=builder /opt/rdpgw /opt/rdpgw COPY --chown=1001 --from=builder /opt/rdpgw /opt/rdpgw
COPY --chown=1001 --from=builder /etc/passwd /etc/passwd COPY --chown=1001 --from=builder /etc/passwd /etc/passwd
COPY --chown=1001 --from=builder /etc/ssl/certs /etc/ssl/certs COPY --chown=1001 --from=builder /etc/ssl/certs /etc/ssl/certs
COPY --chown=1001 rdpgw.yaml /opt/rdpgw/rdpgw.yaml USER 0
WORKDIR /opt/rdpgw WORKDIR /opt/rdpgw
ENTRYPOINT ["/opt/rdpgw/rdpgw"] ENTRYPOINT ["/bin/sh", "/run.sh"]

View File

@ -1,4 +1,4 @@
FROM rattydave/docker-ubuntu-xrdp-mate-custom:20.04 FROM rattydave/docker-ubuntu-xrdp-mate-custom:latest
RUN cd /etc/xrdp/ && \ RUN cd /etc/xrdp/ && \
openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem -days 3650 \ openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem -days 3650 \

View File

@ -21,14 +21,14 @@ services:
restart: on-failure restart: on-failure
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/auth"] test: ["CMD", "curl", "-f", "http://localhost:8080/auth"]
interval: 30s interval: 10s
timeout: 3s timeout: 3s
retries: 10 retries: 10
start_period: 5s start_period: 5s
xrdp: xrdp:
container_name: xrdp container_name: xrdp
hostname: xrdp hostname: xrdp
image: bolkedebruin/docker-ubuntu-xrdp-mate-rdpgw:20.04 image: bolkedebruin/docker-ubuntu-xrdp-mate-rdpgw:latest
ports: ports:
- 3389:3389 - 3389:3389
restart: on-failure restart: on-failure
@ -38,16 +38,30 @@ services:
TZ: "Europe/Amsterdam" TZ: "Europe/Amsterdam"
rdpgw: rdpgw:
container_name: rdpgw container_name: rdpgw
hostname: rdpgw
image: bolkedebruin/rdpgw:latest
build: . build: .
ports: ports:
- 9443:9443 - 9443:9443
restart: on-failure restart: on-failure
depends_on: depends_on:
- keycloak keycloak:
condition: service_healthy
environment: environment:
RDPGW_SERVER__SESSION_STORE: file RDPGW_SERVER__SESSION_STORE: file
RDPGW_SERVER__CERT_FILE: /opt/rdpgw/server.pem
RDPGW_SERVER__KEY_FILE: /opt/rdpgw/key.pem
RDPGW_SERVER__GATEWAY_ADDRESS: localhost:9443
RDPGW_SERVER__PORT: 9443
RDPGW_SERVER__HOSTS: xrdp:3389
RDPGW_SERVER__ROUND_ROBIN: "false"
RDPGW_OPEN_ID__PROVIDER_URL: "http://keycloak:8080/auth/realms/rdpgw"
RDPGW_OPEN_ID__CLIENT_ID: rdpgw
RDPGW_OPEN_ID__CLIENT_SECRET: 01cd304c-6f43-4480-9479-618eb6fd578f
RDPGW_CLIENT__USERNAME_TEMPLATE: "{{ username }}"
RDPGW_CAPS__TOKEN_AUTH: "true"
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://keycloak:8080"] test: ["CMD", "curl", "-f", "http://keycloak:8080"]
interval: 30s interval: 10s
timeout: 10s timeout: 10s
retries: 10 retries: 10

View File

@ -0,0 +1,39 @@
version: '3.4'
services:
xrdp:
container_name: xrdp
hostname: xrdp
image: bolkedebruin/docker-ubuntu-xrdp-mate-rdpgw:latest
ports:
- 3389:3389
restart: on-failure
volumes:
- ${PWD}/xrdp_users.txt:/root/createusers.txt
environment:
TZ: "Europe/Amsterdam"
rdpgw:
container_name: rdpgw
hostname: rdpgw
image: bolkedebruin/rdpgw:latest
build: .
ports:
- 9443:9443
restart: on-failure
volumes:
- ${PWD}/xrdp_users.txt:/root/createusers.txt
environment:
RDPGW_SERVER__SESSION_STORE: file
RDPGW_SERVER__CERT_FILE: /opt/rdpgw/server.pem
RDPGW_SERVER__KEY_FILE: /opt/rdpgw/key.pem
RDPGW_SERVER__GATEWAY_ADDRESS: localhost:9443
RDPGW_SERVER__PORT: 9443
RDPGW_SERVER__HOSTS: xrdp:3389
RDPGW_SERVER__ROUND_ROBIN: "false"
RDPGW_SERVER__AUTHENTICATION: local
RDPGW_CAPS__TOKEN_AUTH: "false"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9443/"]
interval: 10s
timeout: 10s
retries: 10

View File

@ -21,17 +21,19 @@ services:
- 8080:8080 - 8080:8080
restart: on-failure restart: on-failure
command: command:
- start-dev --import-realm --http-relative-path=/auth - start-dev
- --import-realm
- --http-relative-path=/auth
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/auth"] test: ["CMD", "curl", "-f", "http://localhost:8080/auth"]
interval: 30s interval: 10s
timeout: 3s timeout: 3s
retries: 10 retries: 10
start_period: 5s start_period: 5s
xrdp: xrdp:
container_name: xrdp container_name: xrdp
hostname: xrdp hostname: xrdp
image: bolkedebruin/docker-ubuntu-xrdp-mate-rdpgw:20.04 image: bolkedebruin/docker-ubuntu-xrdp-mate-rdpgw:latest
ports: ports:
- 3389:3389 - 3389:3389
restart: on-failure restart: on-failure
@ -45,9 +47,23 @@ services:
- 9443:9443 - 9443:9443
restart: on-failure restart: on-failure
depends_on: depends_on:
- keycloak keycloak:
condition: service_healthy
environment:
RDPGW_SERVER__SESSION_STORE: file
RDPGW_SERVER__CERT_FILE: /opt/rdpgw/server.pem
RDPGW_SERVER__KEY_FILE: /opt/rdpgw/key.pem
RDPGW_SERVER__GATEWAY_ADDRESS: localhost:9443
RDPGW_SERVER__PORT: 9443
RDPGW_SERVER__HOSTS: xrdp:3389
RDPGW_SERVER__ROUND_ROBIN: "false"
RDPGW_OPEN_ID__PROVIDER_URL: "http://keycloak:8080/auth/realms/rdpgw"
RDPGW_OPEN_ID__CLIENT_ID: rdpgw
RDPGW_OPEN_ID__CLIENT_SECRET: 01cd304c-6f43-4480-9479-618eb6fd578f
RDPGW_CLIENT__USERNAME_TEMPLATE: "{{ username }}"
RDPGW_CAPS__TOKEN_AUTH: "true"
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://keycloak:8080"] test: ["CMD", "curl", "-f", "http://keycloak:8080"]
interval: 30s interval: 10s
timeout: 10s timeout: 10s
retries: 10 retries: 10

View File

@ -22,14 +22,14 @@ across the different instances if this is not what you want.
## Configuration through environment variables ## Configuration through environment variables
```bash ```bash
docker --run name rdpgw bolkedebruin/rdpgw:latest \ docker --run name rdpgw bolkedebruin/rdpgw:latest \
-e RDPGW_SERVER__SSL_CERT_FILE=/etc/rdpgw/cert.pem -e RDPGW_SERVER__CERT_FILE=/etc/rdpgw/cert.pem
-e RDPGW_SERVER__SSL_KEY_FILE=/etc/rdpgw.cert.pem -e RDPGW_SERVER__KEY_FILE=/etc/rdpgw.cert.pem
-e RDPGW_SERVER__GATEWAY_ADDRESS=https://localhost:443 -e RDPGW_SERVER__GATEWAY_ADDRESS=https://localhost:443
-e RDPGW_SERVER__SESSION_KEY=thisisasessionkeyreplacethisjetz # 32 characters -e RDPGW_SERVER__SESSION_KEY=thisisasessionkeyreplacethisjetz # 32 characters
-e RDPGW_SERVER__SESSION_ENCRYPTION_KEY=thisisasessionkeyreplacethisnunu # 32 characters -e RDPGW_SERVER__SESSION_ENCRYPTION_KEY=thisisasessionkeyreplacethisnunu # 32 characters
-e RDPGW_OPENID__PROVIDER_URL=http://keycloak:8080/auth/realms/rdpgw -e RDPGW_OPEN_ID__PROVIDER_URL=http://keycloak:8080/auth/realms/rdpgw
-e RDPGW_OPENID__CLIENT_ID=rdpgw -e RDPGW_OPEN_ID__CLIENT_ID=rdpgw
-e RDPGW_OPENID__CLIENT_SECRET=01cd304c-6f43-4480-9479-618eb6fd578f -e RDPGW_OPEN_ID__CLIENT_SECRET=01cd304c-6f43-4480-9479-618eb6fd578f
-e RDPGW_SECURITY__SECURITY_PAA_TOKEN_SIGNING_KEY=prettypleasereplacemeinproductio # 32 characters -e RDPGW_SECURITY__SECURITY_PAA_TOKEN_SIGNING_KEY=prettypleasereplacemeinproductio # 32 characters
-v conf:/etc/rdpgw -v conf:/etc/rdpgw
``` ```

3
dev/docker/rdpgw-pam Normal file
View File

@ -0,0 +1,3 @@
# basic PAM configuration for rdpgw on Alpine
auth include base-auth
auth include base-account

View File

@ -14,9 +14,6 @@ OpenId:
ClientSecret: 01cd304c-6f43-4480-9479-618eb6fd578f ClientSecret: 01cd304c-6f43-4480-9479-618eb6fd578f
Client: Client:
UsernameTemplate: "{{ username }}" UsernameTemplate: "{{ username }}"
NetworkAutoDetect: 0
BandwidthAutoDetect: 1
ConnectionType: 6
Security: Security:
PAATokenSigningKey: prettypleasereplacemeinproductio PAATokenSigningKey: prettypleasereplacemeinproductio
Caps: Caps:

34
dev/docker/run.sh Executable file
View File

@ -0,0 +1,34 @@
#!/bin/sh
USER=rdpgw
file="/root/createusers.txt"
if [ -f $file ]
then
while IFS=: read -r username password is_sudo
do
echo "Username: $username, Password: **** , Sudo: $is_sudo"
if getent passwd "$username" > /dev/null 2>&1
then
echo "User Exists"
else
adduser -s /sbin/nologin "$username"
echo "$username:$password" | chpasswd
fi
done <"$file"
fi
cd /opt/rdpgw || exit 1
if [ -n "${RDPGW_SERVER__AUTHENTICATION}" ]; then
if [ "${RDPGW_SERVER__AUTHENTICATION}" = "local" ]; then
echo "Starting rdpgw-auth"
/opt/rdpgw/rdpgw-auth &
fi
fi
# drop privileges and run the application
su -c /opt/rdpgw/rdpgw "${USER}" -- "$@" &
wait
exit $?

84
docs/api-auth.md Normal file
View File

@ -0,0 +1,84 @@
# NTLM认证API集成
RDPGW支持通过API集成NTLM认证允许使用您自己的用户管理系统进行认证。
## API模式
API支持两种模式
1. **验证模式verify**:验证用户凭据是否有效
2. **密码获取模式getpassword**获取用户的明文密码用于NTLM挑战-响应计算
## API要求
### 1. 验证模式
用于验证用户凭据是否有效。
**请求格式**
```
GET https://your-api-server/api/checkperm/?username=<用户名>&password=<密码>&mode=verify
```
**成功响应**
```json
{
"status": "success"
}
```
### 2. 密码获取模式
用于获取用户的明文密码这是NTLM认证所必需的。
**请求格式**
```
GET https://your-api-server/api/checkperm/?username=<用户名>&mode=getpassword
```
**成功响应**
```json
{
"status": "success",
"password": "用户的明文密码"
}
```
## 配置
在`rdpgw-auth.yaml`中配置API认证
```yaml
apiauth:
enabled: true
apiurl: "https://your-api-server/api/checkperm/"
```
## 安全考虑
1. **密码安全**API必须通过HTTPS提供并且在内部网络中运行以确保安全性
2. **密码存储**您的API服务必须能够以某种形式获取或计算用户密码这需要安全的密码存储机制
3. **替代方案**如果不想暴露明文密码请考虑使用其他认证方式如OpenID Connect或基本认证
## NTLM认证流程
1. 客户端如FreeRDP发送NTLM协商消息到RDPGW
2. RDPGW生成挑战并发送回客户端
3. 客户端计算响应并发送认证消息
4. RDPGW调用API以`getpassword`模式获取用户密码
5. RDPGW使用获取的密码计算期望的响应并与客户端响应比较
6. 如果匹配,认证成功;否则,认证失败
## 优势
- 保持标准NTLM认证流程客户端无需修改
- 与您的用户管理系统集成
- 支持所有NTLM客户端包括标准Windows远程桌面客户端
## 注意事项
- 您的API必须能够安全地存储和提供用户密码
- 明文密码传输存在固有的安全风险,即使在加密通道中也是如此
- 确保API服务器仅对RDPGW服务器可访问并考虑实施IP限制或类似措施

271
docs/images/flow-auth.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 60 KiB

View File

@ -0,0 +1,232 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="500"
height="350"
version="1.1"
id="svg12"
sodipodi:docname="flow-kerberos.svg"
xml:space="preserve"
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs12"><rect
x="170.78426"
y="222.01954"
width="83.594403"
height="14.381833"
id="rect25" /><rect
x="151.90811"
y="181.57064"
width="73.706893"
height="19.77502"
id="rect24" /><rect
x="161.79562"
y="151.00924"
width="124.94217"
height="19.77502"
id="rect23" /><rect
x="62.920519"
y="159.99789"
width="170.78426"
height="27.864801"
id="rect22" /><rect
x="154.6047"
y="70.111435"
width="114.1558"
height="14.381833"
id="rect20" /><rect
x="133.93082"
y="257.97412"
width="213.0309"
height="26.067072"
id="rect18" /><rect
x="346.96173"
y="155.50357"
width="102.47056"
height="28.763666"
id="rect17" /><rect
x="200.44679"
y="197.7502"
width="212.13203"
height="20.673885"
id="rect16" /><rect
x="81.796677"
y="164.4922"
width="157.3013"
height="16.179562"
id="rect15" /><rect
x="200.44679"
y="108.76261"
width="95.27964"
height="19.775021"
id="rect14" /><rect
x="200.44679"
y="197.7502"
width="212.13203"
height="20.673885"
id="rect16-2" /><rect
x="81.796677"
y="164.4922"
width="157.3013"
height="16.179562"
id="rect15-6" /></defs><sodipodi:namedview
id="namedview12"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.1125147"
inkscape:cx="521.34143"
inkscape:cy="166.73937"
inkscape:window-width="2400"
inkscape:window-height="1274"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="0"
inkscape:current-layer="svg12" /><!-- Rectangles --><!-- Text --><text
x="61.016945"
y="43.05085"
font-family="Arial"
font-size="16px"
fill="#000000"
id="text5"><tspan
sodipodi:role="line"
id="tspan20"
x="61.016945"
y="43.05085">Kerberos</tspan><tspan
sodipodi:role="line"
x="61.016945"
y="63.05085"
id="tspan22" /></text><!-- Lines --><line
x1="134.4955"
y1="87.286629"
x2="288.42737"
y2="87.286629"
stroke="#000000"
stroke-width="2.48139"
id="line9" /><line
x1="132.88857"
y1="146.08278"
x2="286.8204"
y2="146.08278"
stroke="#000000"
stroke-width="2.48139"
id="line9-49" /><line
x1="132.17955"
y1="216.35213"
x2="286.11139"
y2="216.35213"
stroke="#000000"
stroke-width="2.48139"
id="line9-49-5" /><line
x1="134.74902"
y1="144.4328"
x2="287.49759"
y2="98.587257"
stroke="#000000"
stroke-width="2.37506"
id="line9-4" /><line
x1="134.23099"
y1="215.71254"
x2="286.97955"
y2="169.86702"
stroke="#000000"
stroke-width="2.37506"
id="line9-4-7" /><g
style="overflow:hidden;fill:currentColor"
id="g12"
transform="matrix(0.06600417,0,0,0.05178799,19.223463,60.951852)"><path
d="M 843.28296,870.11556 C 834.84444,729.6 738.98667,612.69333 609.37481,572.96593 687.88148,536.27259 742.4,456.53333 742.4,364.08889 c 0,-127.24148 -103.15852,-230.4 -230.4,-230.4 -127.24148,0 -230.4,103.15852 -230.4,230.4 0,92.44444 54.51852,172.1837 133.12,208.87704 C 285.10815,612.69333 189.25037,729.6 180.81185,870.11556 c -0.6637,10.9037 7.96445,20.19555 18.96297,20.19555 v 0 c 9.95555,0 18.29925,-7.77481 18.96296,-17.73037 C 227.74518,718.50667 355.65037,596.38518 512,596.38518 c 156.34963,0 284.25481,122.12149 293.35704,276.19556 0.56889,9.95556 8.91259,17.73037 18.96296,17.73037 10.99852,0 19.62667,-9.29185 18.96296,-20.19555 z M 319.52593,364.08889 c 0,-106.28741 86.18666,-192.47408 192.47407,-192.47408 106.28741,0 192.47407,86.18667 192.47407,192.47408 0,106.28741 -86.18666,192.47407 -192.47407,192.47407 -106.28741,0 -192.47407,-86.18666 -192.47407,-192.47407 z"
id="path1" /></g><g
style="overflow:hidden;fill:currentColor"
id="g14"
transform="matrix(0.04275091,0,0,0.04222869,292.71414,66.391967)"><path
d="M 665.6,509.952 H 347.648 c -12.8,0 -21.504,8.704 -21.504,21.504 v 204.8 c 0,12.8 8.704,21.504 21.504,21.504 h 315.904 c 12.8,0 21.504,-8.704 21.504,-21.504 v -204.8 c 2.048,-12.8 -8.704,-21.504 -19.456,-21.504 z M 533.504,661.504 c 0,0 0,2.048 0,0 0,12.8 -8.704,23.552 -21.504,23.552 -12.8,0 -21.504,-8.704 -21.504,-21.504 v -2.048 c -12.8,-6.144 -21.504,-21.504 -21.504,-36.352 0,-23.552 19.456,-42.496 42.496,-42.496 23.04,0 42.496,19.456 42.496,42.496 0.512,14.848 -7.68,29.696 -20.48,36.352 z"
fill="#ff6a00"
id="path1-3" /><path
d="M 981.504,492.544 C 970.752,243.2 763.904,44.544 512,44.544 c -251.904,0 -458.752,198.656 -469.504,448 v 31.744 C 48.64,778.24 256,983.04 512,983.04 c 256,0 462.848,-204.8 469.504,-458.752 z M 810.496,272.896 c -42.496,34.304 -91.648,51.2 -130.048,61.952 -23.552,-87.552 -64,-159.744 -108.544,-198.144 95.744,14.848 179.2,64 238.592,136.192 z M 452.096,136.704 C 409.6,175.104 369.152,247.296 345.6,332.8 307.2,322.048 260.096,305.152 217.6,270.848 275.456,198.656 358.4,151.552 452.096,136.704 Z M 825.344,733.696 C 808.448,718.848 786.944,706.048 765.44,693.248 735.744,678.4 720.384,708.096 748.544,720.896 768,729.6 786.944,742.4 805.888,757.248 743.936,832 656.384,881.152 556.032,891.904 c 21.504,-14.848 45.056,-40.448 64,-72.704 6.656,-16.896 -21.504,-27.648 -36.352,-2.048 -25.6,36.352 -51.2,57.344 -74.752,57.344 -21.504,0 -49.152,-21.504 -72.704,-55.296 -21.504,-31.744 -42.496,-14.848 -38.4,0 19.456,29.696 40.448,53.248 61.952,70.656 -98.304,-8.704 -183.296,-57.344 -243.2,-130.048 19.456,-14.848 38.4,-27.648 57.344,-36.352 27.648,-14.848 10.752,-42.496 -16.896,-27.648 -19.456,10.752 -40.448,23.552 -59.904,38.4 C 154.624,674.304 131.072,602.112 129.024,525.312 H 261.12 c 8.704,0 16.896,-6.656 16.896,-16.896 0,-10.24 -6.144,-16.896 -16.896,-16.896 H 130.048 c 4.096,-72.704 27.648,-140.8 68.096,-198.144 38.4,34.304 91.648,55.296 138.752,68.096 -4.096,21.504 -8.704,42.496 -10.752,66.048 0,19.456 32.256,19.456 32.256,0 16.896,-149.504 96.256,-283.648 153.6,-283.648 57.344,0 136.704,136.704 155.648,288.256 2.048,19.456 34.304,16.896 31.744,0 -2.048,-21.504 -6.656,-42.496 -10.752,-64 49.152,-12.8 102.4,-34.304 140.8,-68.096 38.4,55.296 61.952,123.904 66.048,194.048 H 763.392 c -8.704,0 -16.896,6.144 -16.896,16.896 0,10.752 6.144,16.896 16.896,16.896 H 896 c -2.048,75.776 -27.648,146.432 -70.656,205.824 z"
fill="#ff6a00"
id="path2-2" /><path
d="m 512,317.952 c -59.904,0 -106.496,47.104 -106.496,106.496 v 31.744 H 448 v -31.744 c 0,-34.304 27.648,-64 64,-64 36.352,0 64,27.648 64,64 v 149.504 h 42.496 V 424.448 C 618.496,364.544 571.904,317.952 512,317.952 Z"
fill="#ff6a00"
id="path3-6" /></g><g
style="overflow:hidden;fill:currentColor"
id="g14-8"
transform="matrix(0.04275091,0,0,0.04222869,291.65593,116.90534)"><path
d="M 665.6,509.952 H 347.648 c -12.8,0 -21.504,8.704 -21.504,21.504 v 204.8 c 0,12.8 8.704,21.504 21.504,21.504 h 315.904 c 12.8,0 21.504,-8.704 21.504,-21.504 v -204.8 c 2.048,-12.8 -8.704,-21.504 -19.456,-21.504 z M 533.504,661.504 c 0,0 0,2.048 0,0 0,12.8 -8.704,23.552 -21.504,23.552 -12.8,0 -21.504,-8.704 -21.504,-21.504 v -2.048 c -12.8,-6.144 -21.504,-21.504 -21.504,-36.352 0,-23.552 19.456,-42.496 42.496,-42.496 23.04,0 42.496,19.456 42.496,42.496 0.512,14.848 -7.68,29.696 -20.48,36.352 z"
fill="#ff6a00"
id="path1-3-7" /><path
d="M 981.504,492.544 C 970.752,243.2 763.904,44.544 512,44.544 c -251.904,0 -458.752,198.656 -469.504,448 v 31.744 C 48.64,778.24 256,983.04 512,983.04 c 256,0 462.848,-204.8 469.504,-458.752 z M 810.496,272.896 c -42.496,34.304 -91.648,51.2 -130.048,61.952 -23.552,-87.552 -64,-159.744 -108.544,-198.144 95.744,14.848 179.2,64 238.592,136.192 z M 452.096,136.704 C 409.6,175.104 369.152,247.296 345.6,332.8 307.2,322.048 260.096,305.152 217.6,270.848 275.456,198.656 358.4,151.552 452.096,136.704 Z M 825.344,733.696 C 808.448,718.848 786.944,706.048 765.44,693.248 735.744,678.4 720.384,708.096 748.544,720.896 768,729.6 786.944,742.4 805.888,757.248 743.936,832 656.384,881.152 556.032,891.904 c 21.504,-14.848 45.056,-40.448 64,-72.704 6.656,-16.896 -21.504,-27.648 -36.352,-2.048 -25.6,36.352 -51.2,57.344 -74.752,57.344 -21.504,0 -49.152,-21.504 -72.704,-55.296 -21.504,-31.744 -42.496,-14.848 -38.4,0 19.456,29.696 40.448,53.248 61.952,70.656 -98.304,-8.704 -183.296,-57.344 -243.2,-130.048 19.456,-14.848 38.4,-27.648 57.344,-36.352 27.648,-14.848 10.752,-42.496 -16.896,-27.648 -19.456,10.752 -40.448,23.552 -59.904,38.4 C 154.624,674.304 131.072,602.112 129.024,525.312 H 261.12 c 8.704,0 16.896,-6.656 16.896,-16.896 0,-10.24 -6.144,-16.896 -16.896,-16.896 H 130.048 c 4.096,-72.704 27.648,-140.8 68.096,-198.144 38.4,34.304 91.648,55.296 138.752,68.096 -4.096,21.504 -8.704,42.496 -10.752,66.048 0,19.456 32.256,19.456 32.256,0 16.896,-149.504 96.256,-283.648 153.6,-283.648 57.344,0 136.704,136.704 155.648,288.256 2.048,19.456 34.304,16.896 31.744,0 -2.048,-21.504 -6.656,-42.496 -10.752,-64 49.152,-12.8 102.4,-34.304 140.8,-68.096 38.4,55.296 61.952,123.904 66.048,194.048 H 763.392 c -8.704,0 -16.896,6.144 -16.896,16.896 0,10.752 6.144,16.896 16.896,16.896 H 896 c -2.048,75.776 -27.648,146.432 -70.656,205.824 z"
fill="#ff6a00"
id="path2-2-5" /><path
d="m 512,317.952 c -59.904,0 -106.496,47.104 -106.496,106.496 v 31.744 H 448 v -31.744 c 0,-34.304 27.648,-64 64,-64 36.352,0 64,27.648 64,64 v 149.504 h 42.496 V 424.448 C 618.496,364.544 571.904,317.952 512,317.952 Z"
fill="#ff6a00"
id="path3-6-9" /></g><g
style="overflow:hidden;fill:currentColor"
id="g14-0-7"
transform="matrix(0.04275091,0,0,0.04222869,290.08317,192.55758)"><path
d="M 665.6,509.952 H 347.648 c -12.8,0 -21.504,8.704 -21.504,21.504 v 204.8 c 0,12.8 8.704,21.504 21.504,21.504 h 315.904 c 12.8,0 21.504,-8.704 21.504,-21.504 v -204.8 c 2.048,-12.8 -8.704,-21.504 -19.456,-21.504 z M 533.504,661.504 c 0,0 0,2.048 0,0 0,12.8 -8.704,23.552 -21.504,23.552 -12.8,0 -21.504,-8.704 -21.504,-21.504 v -2.048 c -12.8,-6.144 -21.504,-21.504 -21.504,-36.352 0,-23.552 19.456,-42.496 42.496,-42.496 23.04,0 42.496,19.456 42.496,42.496 0.512,14.848 -7.68,29.696 -20.48,36.352 z"
fill="#ff6a00"
id="path1-3-0-1" /><path
d="M 981.504,492.544 C 970.752,243.2 763.904,44.544 512,44.544 c -251.904,0 -458.752,198.656 -469.504,448 v 31.744 C 48.64,778.24 256,983.04 512,983.04 c 256,0 462.848,-204.8 469.504,-458.752 z M 810.496,272.896 c -42.496,34.304 -91.648,51.2 -130.048,61.952 -23.552,-87.552 -64,-159.744 -108.544,-198.144 95.744,14.848 179.2,64 238.592,136.192 z M 452.096,136.704 C 409.6,175.104 369.152,247.296 345.6,332.8 307.2,322.048 260.096,305.152 217.6,270.848 275.456,198.656 358.4,151.552 452.096,136.704 Z M 825.344,733.696 C 808.448,718.848 786.944,706.048 765.44,693.248 735.744,678.4 720.384,708.096 748.544,720.896 768,729.6 786.944,742.4 805.888,757.248 743.936,832 656.384,881.152 556.032,891.904 c 21.504,-14.848 45.056,-40.448 64,-72.704 6.656,-16.896 -21.504,-27.648 -36.352,-2.048 -25.6,36.352 -51.2,57.344 -74.752,57.344 -21.504,0 -49.152,-21.504 -72.704,-55.296 -21.504,-31.744 -42.496,-14.848 -38.4,0 19.456,29.696 40.448,53.248 61.952,70.656 -98.304,-8.704 -183.296,-57.344 -243.2,-130.048 19.456,-14.848 38.4,-27.648 57.344,-36.352 27.648,-14.848 10.752,-42.496 -16.896,-27.648 -19.456,10.752 -40.448,23.552 -59.904,38.4 C 154.624,674.304 131.072,602.112 129.024,525.312 H 261.12 c 8.704,0 16.896,-6.656 16.896,-16.896 0,-10.24 -6.144,-16.896 -16.896,-16.896 H 130.048 c 4.096,-72.704 27.648,-140.8 68.096,-198.144 38.4,34.304 91.648,55.296 138.752,68.096 -4.096,21.504 -8.704,42.496 -10.752,66.048 0,19.456 32.256,19.456 32.256,0 16.896,-149.504 96.256,-283.648 153.6,-283.648 57.344,0 136.704,136.704 155.648,288.256 2.048,19.456 34.304,16.896 31.744,0 -2.048,-21.504 -6.656,-42.496 -10.752,-64 49.152,-12.8 102.4,-34.304 140.8,-68.096 38.4,55.296 61.952,123.904 66.048,194.048 H 763.392 c -8.704,0 -16.896,6.144 -16.896,16.896 0,10.752 6.144,16.896 16.896,16.896 H 896 c -2.048,75.776 -27.648,146.432 -70.656,205.824 z"
fill="#ff6a00"
id="path2-2-9-5" /><path
d="m 512,317.952 c -59.904,0 -106.496,47.104 -106.496,106.496 v 31.744 H 448 v -31.744 c 0,-34.304 27.648,-64 64,-64 36.352,0 64,27.648 64,64 v 149.504 h 42.496 V 424.448 C 618.496,364.544 571.904,317.952 512,317.952 Z"
fill="#ff6a00"
id="path3-6-5-5" /></g><g
style="overflow:hidden;fill:currentColor"
id="g18"
transform="matrix(0.02516607,0,0,0.02459152,94.079836,77.295599)"><path
d="m 128,85.333333 c -46.933333,0 -85.333333,38.399997 -85.333333,85.333337 v 512 A 85.333333,85.333333 0 0 0 128,768 h 298.66667 v 85.33333 h -85.33334 v 85.33334 H 682.66667 V 853.33333 H 597.33333 V 768 H 896 c 46.93333,0 85.33333,-38.4 85.33333,-85.33333 v -512 c 0,-46.93334 -38.4,-85.333336 -85.33333,-85.333337 M 128,170.66667 h 768 v 512 H 128 M 640,213.33333 490.66667,362.66667 640,512 l 59.73333,-59.73333 -89.6,-89.6 89.6,-89.6 M 384,341.33333 l -59.73333,59.73334 89.6,89.6 -89.6,89.6 L 384,640 533.33333,490.66667"
id="path1-8" /></g><g
style="overflow:hidden;fill:currentColor"
id="g19"
transform="matrix(0.03266725,0,0,0.03617844,341.02251,197.20412)"><path
d="M 0,139.392 409.42933,81.92 409.6,489.13067 0.384,491.52 Z M 409.30133,535.21067 409.6,942.08 0,884.18133 V 532.48 Z M 450.56,81.024 1024,0 V 487.12533 L 450.56,491.52 Z M 1024,533.33333 1023.872,1024 451.37067,944.72533 450.56,532.48 1024,533.376 Z"
fill="#0078d7"
id="path1-5" /></g><text
xml:space="preserve"
id="text20"
style="white-space:pre;shape-inside:url(#rect20);display:inline;fill:#000000"
transform="translate(17.078426,1.7977291)"><tspan
x="154.60547"
y="81.059292"
id="tspan6">Authentication</tspan></text><path
id="path5289"
style="fill:#000000;stroke-width:0.110144"
d="m 327.94236,132.02299 c 0.0606,0.0171 -3.71191,0.11032 -3.45946,5.43921 -1.81339,-0.4647 -4.67039,0.14458 -6.6981,2.42505 -2.05764,2.31413 -5.53002,3.06567 -7.03568,4.20321 0,0 0.82169,1.59402 1.5939,2.02394 0.20057,0.11165 0.4123,0.17508 0.62946,0.20238 l 0.67006,1.21071 0.0736,-1.17458 0.45687,0.8204 0.0634,-1.03363 c 0.0247,-0.009 0.0488,-0.0128 0.0736,-0.0216 l 0.39342,0.70113 0.0532,-0.88184 c 1.07968,-0.47773 2.17818,-1.2374 3.07114,-1.41671 1.59806,-0.32091 2.65787,0.33129 2.25636,4.24291 -3.06578,-2.0673 -5.34393,-1.5927 -7.31738,-0.40477 0,0 -0.50812,1.23953 0.3325,1.76369 3.50763,-1.83664 3.52516,0.94253 6.15491,1.1312 0.0221,-0.0241 0.0443,-0.0479 0.0661,-0.0723 1.97991,-2.22673 4.6186,-2.92005 6.54581,-2.62379 0.0456,-2.42769 0.95349,-3.87017 1.87063,-4.57175 0.75561,-0.56763 1.3864,-0.85491 2.0711,-0.78426 0.15219,0.041 0.2391,0.32129 0.32742,0.5132 0,1e-5 -0.0217,0.0573 -0.0279,0.0759 l 0.0279,0.003 c -0.0387,1.27274 -0.26338,2.51089 -0.27918,3.59232 -0.004,0.23655 0.002,0.4618 0.0178,0.67944 1.11291,-1.67183 2.29902,-3.14124 3.46712,-4.12725 -1.5352,-2.33582 -3.20478,-4.29403 -4.69809,-5.08499 -1.61033,-1.86095 -0.77262,-4.46043 -0.70051,-6.83055 z m 7.47734,4.76338 c 0.0721,2.37025 0.90985,4.96959 -0.70051,6.83054 -1.50705,0.79824 -3.19456,2.78545 -4.74119,5.15006 0.1149,0.47877 0.32471,0.92106 0.68528,1.3481 2.01177,1.11647 4.1666,4.00763 5.9824,7.15229 1.47575,-1.6174 3.49397,-2.45042 6.01534,-1.37701 4.15829,0.92235 3.54345,-3.12836 7.59909,-1.00471 0.84062,-0.52413 0.33503,-1.76369 0.33503,-1.76369 -1.97355,-1.18787 -4.25159,-1.6625 -7.31737,0.40476 -0.40149,-3.91156 0.65833,-4.56018 2.25636,-4.23935 0.89295,0.17931 1.99137,0.93536 3.07113,1.41316 l 0.0534,0.88183 0.39339,-0.70113 c 0.0248,0.009 0.0489,0.0132 0.0736,0.0217 l 0.0634,1.03362 0.45687,-0.81677 0.0736,1.17457 0.67006,-1.21071 c 0.21716,-0.0273 0.42889,-0.0944 0.62946,-0.20601 0.77224,-0.42989 1.5939,-2.02393 1.5939,-2.02393 -1.50566,-1.13745 -4.98052,-1.8854 -7.03815,-4.19953 -2.02773,-2.28049 -4.88223,-2.89336 -6.69553,-2.42861 0.25243,-5.32881 -3.5201,-5.42212 -3.45946,-5.43921 z m -12.94929,2.50812 0.0787,0.51681 c 0,0 -0.78651,0.48492 -1.35538,0.72281 0.0624,0.13017 0.0982,0.28584 0.0965,0.45538 -0.005,0.43746 -0.25624,0.78708 -0.56345,0.78064 -0.30723,-0.006 -0.55277,-0.36487 -0.54824,-0.80232 8.2e-4,-0.0748 0.0111,-0.1485 0.0253,-0.21683 -0.48125,0.0481 -0.94926,0.0542 -0.94926,0.0542 l -0.0787,-0.51681 c 0,0 1.14374,-0.0181 1.69544,-0.18432 0.5517,-0.16625 1.59898,-0.80955 1.59898,-0.80955 z m 7.26154,4.47422 c 0.0606,0.0171 -3.71441,0.11033 -3.46205,5.43921 -1.81339,-0.46469 -4.6678,0.14821 -6.69552,2.42861 -2.05765,2.31413 -5.53251,3.06212 -7.03817,4.19954 0,0 0.82169,1.59401 1.5939,2.02393 0.20058,0.11165 0.41229,0.17869 0.62947,0.206 l 0.67005,1.2107 0.0761,-1.17457 0.45687,0.81677 0.0634,-1.03362 c 0.0247,-0.009 0.0488,-0.0128 0.0736,-0.0216 l 0.39087,0.70112 0.0532,-0.88183 c 1.07967,-0.47773 2.17819,-1.23378 3.07113,-1.41316 1.59806,-0.3209 2.65789,0.33129 2.25637,4.2429 -3.06577,-2.0673 -5.34392,-1.59625 -7.31738,-0.40838 0,0 -0.50558,1.23953 0.33503,1.76369 4.05565,-2.12369 3.4409,1.92706 7.59909,1.00471 6.11014,-2.60131 9.27192,5.99647 8.89356,9.81945 l 8.83772,-4.79229 c 0.69424,-2.21832 -5.33784,-14.94256 -9.78973,-17.30046 -1.61033,-1.86096 -0.7701,-4.46042 -0.69799,-6.83054 z m 11.16043,0.28913 c 0,0 1.04729,0.64692 1.59899,0.81316 0.55171,0.16625 1.69294,0.18433 1.69294,0.18433 l -0.0762,0.51318 c 0,0 -0.46801,-0.006 -0.94925,-0.0542 0.0143,0.0683 0.0221,0.142 0.0229,0.21684 0.004,0.43746 -0.24102,0.79586 -0.54823,0.80231 -0.30722,0.007 -0.55638,-0.34317 -0.56093,-0.78063 -0.002,-0.16954 0.0341,-0.32521 0.0965,-0.45538 -0.56883,-0.23789 -1.35539,-0.72281 -1.35539,-0.72281 l 0.0787,-0.51681 z m -16.63497,6.98236 0.0787,0.5168 c 0,0 -0.784,0.48492 -1.35281,0.72281 0.0624,0.13018 0.0982,0.28584 0.0965,0.45538 -0.005,0.43746 -0.25623,0.78708 -0.56345,0.78063 -0.30722,-0.006 -0.55276,-0.36485 -0.54823,-0.80232 8.2e-4,-0.0748 0.009,-0.14849 0.0228,-0.21684 -0.48124,0.0481 -0.94677,0.0542 -0.94677,0.0542 l -0.0787,-0.5132 c 0,0 1.14373,-0.0181 1.69542,-0.18432 0.55171,-0.16625 1.59649,-0.81315 1.59649,-0.81315 z" /><text
xml:space="preserve"
id="text22"
style="white-space:pre;shape-inside:url(#rect22);fill:#000000"
transform="rotate(-14.400077,-58.773649,-265.80494)"><tspan
x="62.919922"
y="170.94601"
id="tspan7">Auth: Negotiate</tspan></text><text
xml:space="preserve"
id="text23"
style="white-space:pre;shape-inside:url(#rect23);fill:#000000"
transform="translate(-1.7977291,-1.7977291)"><tspan
x="161.79492"
y="161.95773"
id="tspan8">Get TGT over proxy</tspan></text><text
xml:space="preserve"
id="text24"
style="white-space:pre;shape-inside:url(#rect24);fill:#000000"
transform="rotate(-15.585876,175.24729,55.905131)"
inkscape:transform-center-x="33.257988"
inkscape:transform-center-y="3.5954582"><tspan
x="151.9082"
y="192.51828"
id="tspan9">TGT</tspan></text><text
xml:space="preserve"
id="text25"
style="white-space:pre;shape-inside:url(#rect25);fill:#000000"
transform="translate(22.471614,-2.6965937)"><tspan
x="170.78516"
y="232.9675"
id="tspan10">Connect</tspan></text></svg>

After

Width:  |  Height:  |  Size: 20 KiB

271
docs/images/flow-openid.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 60 KiB

218
docs/images/flow-pam.svg Normal file
View File

@ -0,0 +1,218 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="500"
height="250"
version="1.1"
id="svg12"
sodipodi:docname="flow-pam.svg"
xml:space="preserve"
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs12"><rect
x="168.98654"
y="172.58199"
width="111.4592"
height="26.067072"
id="rect29" /><rect
x="341.56854"
y="66.515976"
width="62.021652"
height="17.078426"
id="rect28" /><rect
x="376.62424"
y="32.359123"
width="96.178505"
height="19.775021"
id="rect27" /><rect
x="170.78426"
y="222.01955"
width="83.594406"
height="14.381833"
id="rect25" /><rect
x="151.90811"
y="181.57063"
width="73.706894"
height="19.775021"
id="rect24" /><rect
x="161.79562"
y="151.00925"
width="124.94217"
height="19.775021"
id="rect23" /><rect
x="62.920521"
y="159.99789"
width="170.78426"
height="27.864801"
id="rect22" /><rect
x="154.60471"
y="70.111435"
width="114.1558"
height="14.381833"
id="rect20" /><rect
x="133.93082"
y="257.97412"
width="213.0309"
height="26.067072"
id="rect18" /><rect
x="346.96173"
y="155.50357"
width="102.47056"
height="28.763666"
id="rect17" /><rect
x="200.44679"
y="197.7502"
width="212.13203"
height="20.673885"
id="rect16" /><rect
x="81.796677"
y="164.4922"
width="157.3013"
height="16.179562"
id="rect15" /><rect
x="200.44679"
y="108.76261"
width="95.27964"
height="19.775021"
id="rect14" /><rect
x="200.44679"
y="197.7502"
width="212.13203"
height="20.673885"
id="rect16-2" /><rect
x="81.796677"
y="164.4922"
width="157.3013"
height="16.179562"
id="rect15-6" /></defs><sodipodi:namedview
id="namedview12"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.1125147"
inkscape:cx="521.34143"
inkscape:cy="166.73937"
inkscape:window-width="2400"
inkscape:window-height="1274"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="0"
inkscape:current-layer="svg12" /><!-- Rectangles --><!-- Text --><text
x="61.016945"
y="43.05085"
font-family="Arial"
font-size="16px"
fill="#000000"
id="text5"><tspan
sodipodi:role="line"
x="61.016945"
y="43.05085"
id="tspan22">PAM(Basic/Local)</tspan></text><!-- Lines --><line
x1="134.4955"
y1="103.28663"
x2="288.42737"
y2="103.28663"
stroke="#000000"
stroke-width="2.48139"
id="line9" /><line
x1="342.65012"
y1="104.55545"
x2="403.99893"
y2="104.55545"
stroke="#000000"
stroke-width="1.56651"
id="line9-1" /><line
x1="132.88857"
y1="162.08278"
x2="286.8204"
y2="162.08278"
stroke="#000000"
stroke-width="2.48139"
id="line9-49" /><line
x1="134.74902"
y1="160.4328"
x2="287.49759"
y2="114.58726"
stroke="#000000"
stroke-width="2.37506"
id="line9-4" /><g
style="overflow:hidden;fill:currentColor"
id="g12"
transform="matrix(0.06600417,0,0,0.05178799,19.223463,76.951852)"><path
d="M 843.28296,870.11556 C 834.84444,729.6 738.98667,612.69333 609.37481,572.96593 687.88148,536.27259 742.4,456.53333 742.4,364.08889 c 0,-127.24148 -103.15852,-230.4 -230.4,-230.4 -127.24148,0 -230.4,103.15852 -230.4,230.4 0,92.44444 54.51852,172.1837 133.12,208.87704 C 285.10815,612.69333 189.25037,729.6 180.81185,870.11556 c -0.6637,10.9037 7.96445,20.19555 18.96297,20.19555 v 0 c 9.95555,0 18.29925,-7.77481 18.96296,-17.73037 C 227.74518,718.50667 355.65037,596.38518 512,596.38518 c 156.34963,0 284.25481,122.12149 293.35704,276.19556 0.56889,9.95556 8.91259,17.73037 18.96296,17.73037 10.99852,0 19.62667,-9.29185 18.96296,-20.19555 z M 319.52593,364.08889 c 0,-106.28741 86.18666,-192.47408 192.47407,-192.47408 106.28741,0 192.47407,86.18667 192.47407,192.47408 0,106.28741 -86.18666,192.47407 -192.47407,192.47407 -106.28741,0 -192.47407,-86.18666 -192.47407,-192.47407 z"
id="path1" /></g><g
style="overflow:hidden;fill:currentColor"
id="g14"
transform="matrix(0.04275091,0,0,0.04222869,292.71414,82.391967)"><path
d="M 665.6,509.952 H 347.648 c -12.8,0 -21.504,8.704 -21.504,21.504 v 204.8 c 0,12.8 8.704,21.504 21.504,21.504 h 315.904 c 12.8,0 21.504,-8.704 21.504,-21.504 v -204.8 c 2.048,-12.8 -8.704,-21.504 -19.456,-21.504 z M 533.504,661.504 c 0,0 0,2.048 0,0 0,12.8 -8.704,23.552 -21.504,23.552 -12.8,0 -21.504,-8.704 -21.504,-21.504 v -2.048 c -12.8,-6.144 -21.504,-21.504 -21.504,-36.352 0,-23.552 19.456,-42.496 42.496,-42.496 23.04,0 42.496,19.456 42.496,42.496 0.512,14.848 -7.68,29.696 -20.48,36.352 z"
fill="#ff6a00"
id="path1-3" /><path
d="M 981.504,492.544 C 970.752,243.2 763.904,44.544 512,44.544 c -251.904,0 -458.752,198.656 -469.504,448 v 31.744 C 48.64,778.24 256,983.04 512,983.04 c 256,0 462.848,-204.8 469.504,-458.752 z M 810.496,272.896 c -42.496,34.304 -91.648,51.2 -130.048,61.952 -23.552,-87.552 -64,-159.744 -108.544,-198.144 95.744,14.848 179.2,64 238.592,136.192 z M 452.096,136.704 C 409.6,175.104 369.152,247.296 345.6,332.8 307.2,322.048 260.096,305.152 217.6,270.848 275.456,198.656 358.4,151.552 452.096,136.704 Z M 825.344,733.696 C 808.448,718.848 786.944,706.048 765.44,693.248 735.744,678.4 720.384,708.096 748.544,720.896 768,729.6 786.944,742.4 805.888,757.248 743.936,832 656.384,881.152 556.032,891.904 c 21.504,-14.848 45.056,-40.448 64,-72.704 6.656,-16.896 -21.504,-27.648 -36.352,-2.048 -25.6,36.352 -51.2,57.344 -74.752,57.344 -21.504,0 -49.152,-21.504 -72.704,-55.296 -21.504,-31.744 -42.496,-14.848 -38.4,0 19.456,29.696 40.448,53.248 61.952,70.656 -98.304,-8.704 -183.296,-57.344 -243.2,-130.048 19.456,-14.848 38.4,-27.648 57.344,-36.352 27.648,-14.848 10.752,-42.496 -16.896,-27.648 -19.456,10.752 -40.448,23.552 -59.904,38.4 C 154.624,674.304 131.072,602.112 129.024,525.312 H 261.12 c 8.704,0 16.896,-6.656 16.896,-16.896 0,-10.24 -6.144,-16.896 -16.896,-16.896 H 130.048 c 4.096,-72.704 27.648,-140.8 68.096,-198.144 38.4,34.304 91.648,55.296 138.752,68.096 -4.096,21.504 -8.704,42.496 -10.752,66.048 0,19.456 32.256,19.456 32.256,0 16.896,-149.504 96.256,-283.648 153.6,-283.648 57.344,0 136.704,136.704 155.648,288.256 2.048,19.456 34.304,16.896 31.744,0 -2.048,-21.504 -6.656,-42.496 -10.752,-64 49.152,-12.8 102.4,-34.304 140.8,-68.096 38.4,55.296 61.952,123.904 66.048,194.048 H 763.392 c -8.704,0 -16.896,6.144 -16.896,16.896 0,10.752 6.144,16.896 16.896,16.896 H 896 c -2.048,75.776 -27.648,146.432 -70.656,205.824 z"
fill="#ff6a00"
id="path2-2" /><path
d="m 512,317.952 c -59.904,0 -106.496,47.104 -106.496,106.496 v 31.744 H 448 v -31.744 c 0,-34.304 27.648,-64 64,-64 36.352,0 64,27.648 64,64 v 149.504 h 42.496 V 424.448 C 618.496,364.544 571.904,317.952 512,317.952 Z"
fill="#ff6a00"
id="path3-6" /></g><g
style="overflow:hidden;fill:currentColor"
id="g14-8"
transform="matrix(0.04275091,0,0,0.04222869,292.55479,139.19739)"><path
d="M 665.6,509.952 H 347.648 c -12.8,0 -21.504,8.704 -21.504,21.504 v 204.8 c 0,12.8 8.704,21.504 21.504,21.504 h 315.904 c 12.8,0 21.504,-8.704 21.504,-21.504 v -204.8 c 2.048,-12.8 -8.704,-21.504 -19.456,-21.504 z M 533.504,661.504 c 0,0 0,2.048 0,0 0,12.8 -8.704,23.552 -21.504,23.552 -12.8,0 -21.504,-8.704 -21.504,-21.504 v -2.048 c -12.8,-6.144 -21.504,-21.504 -21.504,-36.352 0,-23.552 19.456,-42.496 42.496,-42.496 23.04,0 42.496,19.456 42.496,42.496 0.512,14.848 -7.68,29.696 -20.48,36.352 z"
fill="#ff6a00"
id="path1-3-7" /><path
d="M 981.504,492.544 C 970.752,243.2 763.904,44.544 512,44.544 c -251.904,0 -458.752,198.656 -469.504,448 v 31.744 C 48.64,778.24 256,983.04 512,983.04 c 256,0 462.848,-204.8 469.504,-458.752 z M 810.496,272.896 c -42.496,34.304 -91.648,51.2 -130.048,61.952 -23.552,-87.552 -64,-159.744 -108.544,-198.144 95.744,14.848 179.2,64 238.592,136.192 z M 452.096,136.704 C 409.6,175.104 369.152,247.296 345.6,332.8 307.2,322.048 260.096,305.152 217.6,270.848 275.456,198.656 358.4,151.552 452.096,136.704 Z M 825.344,733.696 C 808.448,718.848 786.944,706.048 765.44,693.248 735.744,678.4 720.384,708.096 748.544,720.896 768,729.6 786.944,742.4 805.888,757.248 743.936,832 656.384,881.152 556.032,891.904 c 21.504,-14.848 45.056,-40.448 64,-72.704 6.656,-16.896 -21.504,-27.648 -36.352,-2.048 -25.6,36.352 -51.2,57.344 -74.752,57.344 -21.504,0 -49.152,-21.504 -72.704,-55.296 -21.504,-31.744 -42.496,-14.848 -38.4,0 19.456,29.696 40.448,53.248 61.952,70.656 -98.304,-8.704 -183.296,-57.344 -243.2,-130.048 19.456,-14.848 38.4,-27.648 57.344,-36.352 27.648,-14.848 10.752,-42.496 -16.896,-27.648 -19.456,10.752 -40.448,23.552 -59.904,38.4 C 154.624,674.304 131.072,602.112 129.024,525.312 H 261.12 c 8.704,0 16.896,-6.656 16.896,-16.896 0,-10.24 -6.144,-16.896 -16.896,-16.896 H 130.048 c 4.096,-72.704 27.648,-140.8 68.096,-198.144 38.4,34.304 91.648,55.296 138.752,68.096 -4.096,21.504 -8.704,42.496 -10.752,66.048 0,19.456 32.256,19.456 32.256,0 16.896,-149.504 96.256,-283.648 153.6,-283.648 57.344,0 136.704,136.704 155.648,288.256 2.048,19.456 34.304,16.896 31.744,0 -2.048,-21.504 -6.656,-42.496 -10.752,-64 49.152,-12.8 102.4,-34.304 140.8,-68.096 38.4,55.296 61.952,123.904 66.048,194.048 H 763.392 c -8.704,0 -16.896,6.144 -16.896,16.896 0,10.752 6.144,16.896 16.896,16.896 H 896 c -2.048,75.776 -27.648,146.432 -70.656,205.824 z"
fill="#ff6a00"
id="path2-2-5" /><path
d="m 512,317.952 c -59.904,0 -106.496,47.104 -106.496,106.496 v 31.744 H 448 v -31.744 c 0,-34.304 27.648,-64 64,-64 36.352,0 64,27.648 64,64 v 149.504 h 42.496 V 424.448 C 618.496,364.544 571.904,317.952 512,317.952 Z"
fill="#ff6a00"
id="path3-6-9" /></g><g
style="overflow:hidden;fill:currentColor"
id="g18"
transform="matrix(0.02516607,0,0,0.02459152,94.079836,93.295599)"><path
d="m 128,85.333333 c -46.933333,0 -85.333333,38.399997 -85.333333,85.333337 v 512 A 85.333333,85.333333 0 0 0 128,768 h 298.66667 v 85.33333 h -85.33334 v 85.33334 H 682.66667 V 853.33333 H 597.33333 V 768 H 896 c 46.93333,0 85.33333,-38.4 85.33333,-85.33333 v -512 c 0,-46.93334 -38.4,-85.333336 -85.33333,-85.333337 M 128,170.66667 h 768 v 512 H 128 M 640,213.33333 490.66667,362.66667 640,512 l 59.73333,-59.73333 -89.6,-89.6 89.6,-89.6 M 384,341.33333 l -59.73333,59.73334 89.6,89.6 -89.6,89.6 L 384,640 533.33333,490.66667"
id="path1-8" /></g><g
style="overflow:hidden;fill:currentColor"
id="g19"
transform="matrix(0.03266725,0,0,0.03617844,345.51683,142.19382)"><path
d="M 0,139.392 409.42933,81.92 409.6,489.13067 0.384,491.52 Z M 409.30133,535.21067 409.6,942.08 0,884.18133 V 532.48 Z M 450.56,81.024 1024,0 V 487.12533 L 450.56,491.52 Z M 1024,533.33333 1023.872,1024 451.37067,944.72533 450.56,532.48 1024,533.376 Z"
fill="#0078d7"
id="path1-5" /></g><text
xml:space="preserve"
id="text20"
style="white-space:pre;shape-inside:url(#rect20);display:inline;fill:#000000"
transform="translate(17.078426,17.797729)"><tspan
x="154.60547"
y="81.059292"
id="tspan5">Authentication</tspan></text><g
style="overflow:hidden;fill:currentColor"
id="g27"
transform="matrix(0.04222519,0,0,0.03933851,410.28976,84.846267)"><path
d="M 916.48,242.88 907.52,172.16 512,32 116.48,172.16 l -8.96,70.4 c -3.2,23.68 -68.48,578.88 365.12,736 L 512,992 551.04,977.92 C 983.36,822.4 919.68,266.56 916.48,242.88 Z m -154.88,147.2 -211.52,339.2 -2.88,4.48 A 86.4,86.4 0 0 1 428.48,761.28 87.68,87.68 0 0 1 405.12,739.84 L 268.48,557.76 A 64.03559,64.03559 0 0 1 359.04,467.2 L 459.2,544 659.2,315.2 a 64,64 0 0 1 102.72,76.16 z"
fill="#231f20"
id="path1-4" /></g><text
xml:space="preserve"
id="text27"
style="white-space:pre;shape-inside:url(#rect27);display:inline;fill:#000000"
transform="translate(21.572749,35.77502)"><tspan
x="376.625"
y="43.307339"
id="tspan6">rdpgw-auth</tspan></text><text
xml:space="preserve"
id="text28"
style="white-space:pre;shape-inside:url(#rect28);display:inline;fill:#000000"
transform="translate(11.685239,21.393187)"><tspan
x="341.56836"
y="77.463589"
id="tspan7">socket</tspan></text><text
xml:space="preserve"
id="text29"
style="white-space:pre;shape-inside:url(#rect29);display:inline;fill:#000000"
transform="translate(23.370478,-8.089781)"><tspan
x="168.98633"
y="183.53"
id="tspan8">connect</tspan></text></svg>

After

Width:  |  Height:  |  Size: 13 KiB

22
docs/images/flow.svg Normal file
View File

@ -0,0 +1,22 @@
<svg width="500" height="300" xmlns="http://www.w3.org/2000/svg">
<!-- Rectangles -->
<rect x="50" y="50" width="150" height="50" fill="lightblue" stroke="black" stroke-width="2"/>
<rect x="200" y="50" width="150" height="50" fill="lightblue" stroke="black" stroke-width="2"/>
<rect x="50" y="150" width="150" height="50" fill="lightblue" stroke="black" stroke-width="2"/>
<rect x="200" y="150" width="150" height="50" fill="lightblue" stroke="black" stroke-width="2"/>
<rect x="350" y="150" width="150" height="50" fill="lightblue" stroke="black" stroke-width="2"/>
<!-- Text -->
<text x="75" y="85" font-family="Arial" font-size="16" fill="black">Client</text>
<text x="235" y="85" font-family="Arial" font-size="16" fill="black">RDP Gateway</text>
<text x="65" y="185" font-family="Arial" font-size="16" fill="black">RDP GW Auth</text>
<text x="215" y="185" font-family="Arial" font-size="16" fill="black">PAM</text>
<text x="365" y="185" font-family="Arial" font-size="16" fill="black">Passwd or LDAP</text>
<!-- Lines -->
<line x1="100" y1="75" x2="200" y2="75" stroke="black" stroke-width="2"/>
<line x1="200" y1="100" x2="100" y2="175" stroke="black" stroke-width="2"/>
<line x1="100" y1="175" x2="200" y2="175" stroke="black" stroke-width="2"/>
<line x1="200" y1="200" x2="350" y2="200" stroke="black" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,23 @@
# RDPGW认证配置示例
# 将此文件保存为rdpgw-auth.yaml并根据您的环境调整设置
# API认证配置
ApiAuth:
# 启用API认证
enabled: true
# API URL - 验证用户凭据的端点
# NTLM认证需要API提供明文密码
# 此API需要支持以下两种模式
# 1. ?username=user&mode=getpassword - 返回用户的明文密码
# 2. ?username=user&password=pass&mode=verify - 验证凭据是否正确
apiurl: "https://your-api-server/api/checkperm/"
# 用户配置
# 当使用API认证时此部分可以为空
# 如果为特定用户提供密码将绕过API并使用本地密码
Users: []
# 或者您也可以同时支持API认证和本地用户认证
# 在这种情况下,请提供本地用户凭据
# users:
# - {Username: "local_user", Password: "local_password"}

70
go.mod
View File

@ -1,52 +1,60 @@
module github.com/bolkedebruin/rdpgw module github.com/bolkedebruin/rdpgw
go 1.19 go 1.22
require ( require (
github.com/bolkedebruin/gokrb5/v8 v8.5.0 github.com/bolkedebruin/gokrb5/v8 v8.5.0
github.com/coreos/go-oidc/v3 v3.2.0 github.com/coreos/go-oidc/v3 v3.9.0
github.com/fatih/structs v1.1.0 github.com/fatih/structs v1.1.0
github.com/go-jose/go-jose/v3 v3.0.0 github.com/go-jose/go-jose/v4 v4.0.1
github.com/google/uuid v1.1.2 github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1
github.com/gorilla/mux v1.8.0 github.com/google/uuid v1.6.0
github.com/gorilla/sessions v1.2.1 github.com/gorilla/mux v1.8.1
github.com/gorilla/websocket v1.5.0 github.com/gorilla/sessions v1.2.2
github.com/gorilla/websocket v1.5.1
github.com/jcmturner/gofork v1.7.6 github.com/jcmturner/gofork v1.7.6
github.com/jcmturner/goidentity/v6 v6.0.1 github.com/jcmturner/goidentity/v6 v6.0.1
github.com/knadh/koanf v1.4.2 github.com/knadh/koanf/parsers/yaml v0.1.0
github.com/msteinert/pam v1.0.0 github.com/knadh/koanf/providers/confmap v0.1.0
github.com/knadh/koanf/providers/env v0.1.0
github.com/knadh/koanf/providers/file v0.1.0
github.com/knadh/koanf/v2 v2.1.0
github.com/m7913d/go-ntlm v0.0.1
github.com/msteinert/pam/v2 v2.0.0
github.com/patrickmn/go-cache v2.1.0+incompatible github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/prometheus/client_golang v1.12.1 github.com/prometheus/client_golang v1.19.0
github.com/thought-machine/go-flags v1.6.1 github.com/stretchr/testify v1.9.0
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa github.com/thought-machine/go-flags v1.6.3
golang.org/x/oauth2 v0.0.0-20220722155238-128564f6959c golang.org/x/crypto v0.31.0
google.golang.org/grpc v1.49.0 golang.org/x/oauth2 v0.18.0
google.golang.org/protobuf v1.28.1 google.golang.org/grpc v1.62.1
google.golang.org/protobuf v1.33.0
) )
require ( require (
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/golang/protobuf v1.5.2 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect github.com/go-jose/go-jose/v3 v3.0.1 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/jcmturner/aescts/v2 v2.0.0 // indirect github.com/jcmturner/aescts/v2 v2.0.0 // indirect
github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
github.com/jcmturner/rpc/v2 v2.0.3 // indirect github.com/jcmturner/rpc/v2 v2.0.3 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/knadh/koanf/maps v0.1.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/client_model v0.6.0 // indirect
github.com/prometheus/common v0.32.1 // indirect github.com/prometheus/common v0.50.0 // indirect
github.com/prometheus/procfs v0.7.3 // indirect github.com/prometheus/procfs v0.13.0 // indirect
golang.org/x/net v0.0.0-20220725212005-46097bf591d3 // indirect golang.org/x/net v0.23.0 // indirect
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.3.7 // indirect golang.org/x/text v0.21.0 // indirect
google.golang.org/appengine v1.6.7 // indirect google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

132
index.js Normal file
View File

@ -0,0 +1,132 @@
const express = require('express');
const fs = require('fs');
const path = require('path');
// 配置信息
const config = {
port: 3000,
// 用户信息,可以通过配置文件覆盖
users: {
'testuser': 'testpassword',
'admin': 'adminpass',
'user1': 'password1'
},
// API路径
apiPath: '/api/checkperm'
};
// 尝试加载配置文件
try {
const configPath = path.join(__dirname, 'config.json');
if (fs.existsSync(configPath)) {
const fileConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
// 合并配置
Object.assign(config, fileConfig);
console.log('已加载配置文件');
}
} catch (error) {
console.log('加载配置文件失败,使用默认配置:', error.message);
}
const app = express();
// 添加中间件解析JSON和表单数据
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// 处理认证逻辑的函数
function handleAuth(username, password, mode) {
// 验证参数是否存在
if (!username) {
console.log('缺少必要参数: username');
return { status: 400, response: { status: 'error', message: '缺少必要参数: username' } };
}
// 检查用户是否存在
if (!config.users[username]) {
console.log(`用户不存在: ${username}`);
return { status: 401, response: { status: 'error', message: '用户不存在' } };
}
// 根据模式处理请求
if (mode === 'getpassword') {
// 密码获取模式 - 用于NTLM认证
console.log(`返回用户 ${username} 的密码`);
return {
status: 200,
response: {
status: 'success',
password: config.users[username]
}
};
} else {
// 默认为verify模式 - 验证用户名和密码
if (!password) {
console.log('缺少必要参数: password');
return { status: 400, response: { status: 'error', message: '缺少必要参数: password' } };
}
if (config.users[username] === password) {
console.log('认证成功');
return {
status: 200,
response: {
status: 'success',
user: username
}
};
} else {
console.log('认证失败: 密码不正确');
return { status: 401, response: { status: 'error', message: '认证失败' } };
}
}
}
// 认证API端点 - GET
app.get(config.apiPath, (req, res) => {
const { username, password, mode } = req.query;
console.log('收到GET认证请求:');
console.log(`username: ${username}`);
console.log(`mode: ${mode || 'verify'}`);
if (password) {
console.log(`password: ${'*'.repeat(password ? password.length : 0)}`); // 为安全起见不打印实际密码
}
const result = handleAuth(username, password, mode);
return res.status(result.status).json(result.response);
});
// 认证API端点 - POST
app.post(config.apiPath, (req, res) => {
const { username, password, mode } = req.body;
console.log('收到POST认证请求:');
console.log(`username: ${username}`);
console.log(`mode: ${mode || 'verify'}`);
if (password) {
console.log(`password: ${'*'.repeat(password ? password.length : 0)}`); // 为安全起见不打印实际密码
}
const result = handleAuth(username, password, mode);
return res.status(result.status).json(result.response);
});
// 根路径返回服务信息
app.get('/', (req, res) => {
res.send('RDPGW远程认证测试服务已启动');
});
// 启动服务器
app.listen(config.port, () => {
console.log(`认证服务器已启动,监听端口: ${config.port}`);
console.log('当前配置:');
console.log(`- 端口: ${config.port}`);
console.log(`- API路径: ${config.apiPath}`);
console.log(`- 已配置用户数: ${Object.keys(config.users).length}`);
console.log('\n支持的模式:');
console.log(`1. 验证模式 (GET): http://localhost:${config.port}${config.apiPath}?username=testuser&password=testpassword&mode=verify`);
console.log(`2. 密码获取模式 (GET): http://localhost:${config.port}${config.apiPath}?username=testuser&mode=getpassword`);
console.log('---');
console.log('POST请求也支持可以通过请求体发送参数');
});

View File

@ -14,6 +14,18 @@ message AuthResponse {
string error = 2; string error = 2;
} }
message NtlmRequest {
string session = 1;
string ntlmMessage = 2;
}
message NtlmResponse {
bool authenticated = 1;
string username = 2;
string ntlmMessage = 3;
}
service Authenticate { service Authenticate {
rpc Authenticate (UserPass) returns (AuthResponse) {} rpc Authenticate (UserPass) returns (AuthResponse) {}
rpc NTLM (NtlmRequest) returns (NtlmResponse) {}
} }

View File

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.28.1 // protoc-gen-go v1.25.0-devel
// protoc v3.21.5 // protoc v3.14.0
// source: auth.proto // source: auth.proto
package auth package auth
@ -130,6 +130,132 @@ func (x *AuthResponse) GetError() string {
return "" return ""
} }
type NtlmRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Session string `protobuf:"bytes,1,opt,name=session,proto3" json:"session,omitempty"`
NtlmMessage string `protobuf:"bytes,2,opt,name=ntlmMessage,proto3" json:"ntlmMessage,omitempty"`
}
func (x *NtlmRequest) Reset() {
*x = NtlmRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_auth_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *NtlmRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*NtlmRequest) ProtoMessage() {}
func (x *NtlmRequest) ProtoReflect() protoreflect.Message {
mi := &file_auth_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use NtlmRequest.ProtoReflect.Descriptor instead.
func (*NtlmRequest) Descriptor() ([]byte, []int) {
return file_auth_proto_rawDescGZIP(), []int{2}
}
func (x *NtlmRequest) GetSession() string {
if x != nil {
return x.Session
}
return ""
}
func (x *NtlmRequest) GetNtlmMessage() string {
if x != nil {
return x.NtlmMessage
}
return ""
}
type NtlmResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Authenticated bool `protobuf:"varint,1,opt,name=authenticated,proto3" json:"authenticated,omitempty"`
Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"`
NtlmMessage string `protobuf:"bytes,3,opt,name=ntlmMessage,proto3" json:"ntlmMessage,omitempty"`
Error string `protobuf:"bytes,4,opt,name=error,proto3" json:"error,omitempty"`
}
func (x *NtlmResponse) Reset() {
*x = NtlmResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_auth_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *NtlmResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*NtlmResponse) ProtoMessage() {}
func (x *NtlmResponse) ProtoReflect() protoreflect.Message {
mi := &file_auth_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use NtlmResponse.ProtoReflect.Descriptor instead.
func (*NtlmResponse) Descriptor() ([]byte, []int) {
return file_auth_proto_rawDescGZIP(), []int{3}
}
func (x *NtlmResponse) GetAuthenticated() bool {
if x != nil {
return x.Authenticated
}
return false
}
func (x *NtlmResponse) GetUsername() string {
if x != nil {
return x.Username
}
return ""
}
func (x *NtlmResponse) GetNtlmMessage() string {
if x != nil {
return x.NtlmMessage
}
return ""
}
func (x *NtlmResponse) GetError() string {
if x != nil {
return x.Error
}
return ""
}
var File_auth_proto protoreflect.FileDescriptor var File_auth_proto protoreflect.FileDescriptor
var file_auth_proto_rawDesc = []byte{ var file_auth_proto_rawDesc = []byte{
@ -143,12 +269,29 @@ var file_auth_proto_rawDesc = []byte{
0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x61, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x61,
0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05,
0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72,
0x6f, 0x72, 0x32, 0x44, 0x0a, 0x0c, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x6f, 0x72, 0x22, 0x49, 0x0a, 0x0b, 0x4e, 0x74, 0x6c, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x65, 0x12, 0x34, 0x0a, 0x0c, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01,
0x74, 0x65, 0x12, 0x0e, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x50, 0x61, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x6e,
0x73, 0x73, 0x1a, 0x12, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x52, 0x65, 0x74, 0x6c, 0x6d, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2e, 0x2f, 0x61, 0x75, 0x52, 0x0b, 0x6e, 0x74, 0x6c, 0x6d, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x88, 0x01,
0x74, 0x68, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 0x0a, 0x0c, 0x4e, 0x74, 0x6c, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24,
0x0a, 0x0d, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x18,
0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63,
0x61, 0x74, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65,
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65,
0x12, 0x20, 0x0a, 0x0b, 0x6e, 0x74, 0x6c, 0x6d, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18,
0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6e, 0x74, 0x6c, 0x6d, 0x4d, 0x65, 0x73, 0x73, 0x61,
0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28,
0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x32, 0x75, 0x0a, 0x0c, 0x41, 0x75, 0x74, 0x68,
0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x34, 0x0a, 0x0c, 0x41, 0x75, 0x74, 0x68,
0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e,
0x55, 0x73, 0x65, 0x72, 0x50, 0x61, 0x73, 0x73, 0x1a, 0x12, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e,
0x41, 0x75, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2f,
0x0a, 0x04, 0x4e, 0x54, 0x4c, 0x4d, 0x12, 0x11, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x4e, 0x74,
0x6c, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x61, 0x75, 0x74, 0x68,
0x2e, 0x4e, 0x74, 0x6c, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42,
0x08, 0x5a, 0x06, 0x2e, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x33,
} }
var ( var (
@ -163,16 +306,20 @@ func file_auth_proto_rawDescGZIP() []byte {
return file_auth_proto_rawDescData return file_auth_proto_rawDescData
} }
var file_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 2) var file_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
var file_auth_proto_goTypes = []interface{}{ var file_auth_proto_goTypes = []interface{}{
(*UserPass)(nil), // 0: auth.UserPass (*UserPass)(nil), // 0: auth.UserPass
(*AuthResponse)(nil), // 1: auth.AuthResponse (*AuthResponse)(nil), // 1: auth.AuthResponse
(*NtlmRequest)(nil), // 2: auth.NtlmRequest
(*NtlmResponse)(nil), // 3: auth.NtlmResponse
} }
var file_auth_proto_depIdxs = []int32{ var file_auth_proto_depIdxs = []int32{
0, // 0: auth.Authenticate.Authenticate:input_type -> auth.UserPass 0, // 0: auth.Authenticate.Authenticate:input_type -> auth.UserPass
1, // 1: auth.Authenticate.Authenticate:output_type -> auth.AuthResponse 2, // 1: auth.Authenticate.NTLM:input_type -> auth.NtlmRequest
1, // [1:2] is the sub-list for method output_type 1, // 2: auth.Authenticate.Authenticate:output_type -> auth.AuthResponse
0, // [0:1] is the sub-list for method input_type 3, // 3: auth.Authenticate.NTLM:output_type -> auth.NtlmResponse
2, // [2:4] is the sub-list for method output_type
0, // [0:2] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name 0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee 0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name 0, // [0:0] is the sub-list for field type_name
@ -208,6 +355,30 @@ func file_auth_proto_init() {
return nil return nil
} }
} }
file_auth_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*NtlmRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_auth_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*NtlmResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
} }
type x struct{} type x struct{}
out := protoimpl.TypeBuilder{ out := protoimpl.TypeBuilder{
@ -215,7 +386,7 @@ func file_auth_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_auth_proto_rawDesc, RawDescriptor: file_auth_proto_rawDesc,
NumEnums: 0, NumEnums: 0,
NumMessages: 2, NumMessages: 4,
NumExtensions: 0, NumExtensions: 0,
NumServices: 1, NumServices: 1,
}, },

View File

@ -1,8 +1,4 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT. // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.2.0
// - protoc v3.21.5
// source: auth.proto
package auth package auth
@ -23,6 +19,7 @@ const _ = grpc.SupportPackageIsVersion7
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type AuthenticateClient interface { type AuthenticateClient interface {
Authenticate(ctx context.Context, in *UserPass, opts ...grpc.CallOption) (*AuthResponse, error) Authenticate(ctx context.Context, in *UserPass, opts ...grpc.CallOption) (*AuthResponse, error)
NTLM(ctx context.Context, in *NtlmRequest, opts ...grpc.CallOption) (*NtlmResponse, error)
} }
type authenticateClient struct { type authenticateClient struct {
@ -42,20 +39,35 @@ func (c *authenticateClient) Authenticate(ctx context.Context, in *UserPass, opt
return out, nil return out, nil
} }
func (c *authenticateClient) NTLM(ctx context.Context, in *NtlmRequest, opts ...grpc.CallOption) (*NtlmResponse, error) {
out := new(NtlmResponse)
err := c.cc.Invoke(ctx, "/auth.Authenticate/NTLM", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// AuthenticateServer is the server API for Authenticate service. // AuthenticateServer is the server API for Authenticate service.
// All implementations should embed UnimplementedAuthenticateServer // All implementations must embed UnimplementedAuthenticateServer
// for forward compatibility // for forward compatibility
type AuthenticateServer interface { type AuthenticateServer interface {
Authenticate(context.Context, *UserPass) (*AuthResponse, error) Authenticate(context.Context, *UserPass) (*AuthResponse, error)
NTLM(context.Context, *NtlmRequest) (*NtlmResponse, error)
mustEmbedUnimplementedAuthenticateServer()
} }
// UnimplementedAuthenticateServer should be embedded to have forward compatible implementations. // UnimplementedAuthenticateServer must be embedded to have forward compatible implementations.
type UnimplementedAuthenticateServer struct { type UnimplementedAuthenticateServer struct {
} }
func (UnimplementedAuthenticateServer) Authenticate(context.Context, *UserPass) (*AuthResponse, error) { func (UnimplementedAuthenticateServer) Authenticate(context.Context, *UserPass) (*AuthResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Authenticate not implemented") return nil, status.Errorf(codes.Unimplemented, "method Authenticate not implemented")
} }
func (UnimplementedAuthenticateServer) NTLM(context.Context, *NtlmRequest) (*NtlmResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method NTLM not implemented")
}
func (UnimplementedAuthenticateServer) mustEmbedUnimplementedAuthenticateServer() {}
// UnsafeAuthenticateServer may be embedded to opt out of forward compatibility for this service. // UnsafeAuthenticateServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to AuthenticateServer will // Use of this interface is not recommended, as added methods to AuthenticateServer will
@ -86,6 +98,24 @@ func _Authenticate_Authenticate_Handler(srv interface{}, ctx context.Context, de
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _Authenticate_NTLM_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(NtlmRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AuthenticateServer).NTLM(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/auth.Authenticate/NTLM",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthenticateServer).NTLM(ctx, req.(*NtlmRequest))
}
return interceptor(ctx, in, info, handler)
}
// Authenticate_ServiceDesc is the grpc.ServiceDesc for Authenticate service. // Authenticate_ServiceDesc is the grpc.ServiceDesc for Authenticate service.
// It's only intended for direct use with grpc.RegisterService, // It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy) // and not to be introspected or modified (even as a copy)
@ -97,6 +127,10 @@ var Authenticate_ServiceDesc = grpc.ServiceDesc{
MethodName: "Authenticate", MethodName: "Authenticate",
Handler: _Authenticate_Authenticate_Handler, Handler: _Authenticate_Authenticate_Handler,
}, },
{
MethodName: "NTLM",
Handler: _Authenticate_NTLM_Handler,
},
}, },
Streams: []grpc.StreamDesc{}, Streams: []grpc.StreamDesc{},
Metadata: "auth.proto", Metadata: "auth.proto",