Compare commits

..

No commits in common. "master" and "v1.0-stable" have entirely different histories.

96 changed files with 1429 additions and 8183 deletions

View File

@ -1,80 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ "master" ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ "master" ]
schedule:
- cron: '22 22 * * 3'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'go' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Install pam-devel
run: sudo apt-get -y install libpam-dev
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Go
uses: actions/setup-go@v4
with:
go-version-file: go.mod
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# 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)
- name: Autobuild
uses: github/codeql-action/autobuild@v3
# 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
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3

View File

@ -1,48 +0,0 @@
name: Docker Image CI
on:
push:
branches: [ "master" ]
tags: [ "v*" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }}
- 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: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
uses: peter-evans/dockerhub-description@v3
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }}
repository: ${{ github.repository_owner }}/rdpgw
readme-filepath: ./dev/docker/docker-readme.md

View File

@ -16,23 +16,17 @@ jobs:
- name: Set up Go 1.x
uses: actions/setup-go@v2
with:
go-version: ^1.22
go-version: ^1.14
id: go
- name: Install pam-devel
run: sudo apt-get -y install libpam-dev
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: Install golint
run: go get -u golang.org/x/lint/golint
- name: Update go.sum
run: make mod
- name: Build
run: make build
run: go build -v .
- name: Test
run: make test
run: go test -cover -v ./...

3
.gitignore vendored
View File

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

View File

@ -1,78 +0,0 @@
BINDIR := $(CURDIR)/bin
INSTALL_PATH ?= /usr/local/bin
BINNAME ?= rdpgw
BINNAME2 ?= rdpgw-auth
# Rebuild the binary if any of these files change
SRC := $(shell find . -type f -name '*.go' -print) go.mod go.sum
# Required for globs to work correctly
SHELL = /usr/bin/env bash
GIT_COMMIT = $(shell git rev-parse HEAD)
GIT_SHA = $(shell git rev-parse --short HEAD)
GIT_TAG = $(shell git describe --tags --abbrev=0 --exact-match 2>/dev/null)
GIT_DIRTY = $(shell test -n "`git status --porcelain`" && echo "dirty" || echo "clean")
ifdef VERSION
BINARY_VERSION = $(VERSION)
endif
BINARY_VERSION ?= ${GIT_TAG}
VERSION_METADATA = unreleased
# Clear the "unreleased" string in BuildMetadata
ifneq ($(GIT_TAG),)
VERSION_METADATA =
endif
.PHONY: all
all: mod build deb
# ------------------------------------------------------------------------------
# build
.PHONY: build
build: $(BINDIR)/$(BINNAME)
$(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)'/$(BINNAME2) ./cmd/auth
.PHONY: deb
deb:clean mod build
dpkg-buildpackage -b -us -uc
# ------------------------------------------------------------------------------
# install
.PHONY: install
install: build
@install "$(BINDIR)/$(BINNAME)" "$(INSTALL_PATH)/$(BINNAME)"
# ------------------------------------------------------------------------------
# mod
.PHONY: mod
mod:
go mod tidy -compat=1.23
# ------------------------------------------------------------------------------
# test
.PHONY: test
test:
go test -cover -v ./...
# ------------------------------------------------------------------------------
# clean
.PHONY: clean
clean:
@rm -rf '$(BINDIR)' ./_dist
dh_clean
.PHONY: info
info:
@echo "Version: ${VERSION}"
@echo "Git Tag: ${GIT_TAG}"
@echo "Git Commit: ${GIT_COMMIT}"
@echo "Git Tree State: ${GIT_DIRTY}"

538
README.md
View File

@ -2,10 +2,6 @@ GO Remote Desktop Gateway
=========================
![Go](https://github.com/bolkedebruin/rdpgw/workflows/Go/badge.svg)
[![Docker Pulls](https://badgen.net/docker/pulls/bolkedebruin/rdpgw?icon=docker&label=pulls)](https://hub.docker.com/r/bolkedebruin/rdpgw/)
[![Docker Stars](https://badgen.net/docker/stars/bolkedebruin/rdpgw?icon=docker&label=stars)](https://hub.docker.com/r/bolkedebruin/rdpgw/)
[![Docker Image Size](https://badgen.net/docker/size/bolkedebruin/rdpgw?icon=docker&label=image%20size)](https://hub.docker.com/r/bolkedebruin/rdpgw/)
:star: Star us on GitHub — it helps!
@ -14,329 +10,90 @@ 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
on Kubernetes.
# AIM
## AIM
RDPGW aims to provide a full open source replacement for MS Remote Desktop Gateway,
including access policies.
# Security requirements
## 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
if you want.
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
## Security
RDPGW wants to be secure when you set it up from the beginning. It does this by having OpenID
Connect integration enabled by default. 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
if you want.
### Mixing authentication mechanisms
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.
### Open ID Connect
![OpenID Connect](docs/images/flow-openid.svg)
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.
To enable OpenID Connect make sure to set the following variables in the configuration file.
```yaml
Server:
Authentication:
- openid
OpenId:
ProviderUrl: http://<provider_url>
ClientId: <your client id>
ClientSecret: <your-secret>
Caps:
TokenAuth: true
```
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
https://your-gateway/connect. If 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.
### 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.
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
```bash
# ./rdpgw-auth -n rdpgw -s /tmp/rdpgw-auth.sock
cd rdpgw
go build -o rdpgw .
```
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
## Configuration
By default the configuration is read from `rdpgw.yaml`. Below is a
template.
```yaml
# web server configuration.
Server:
# 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
# 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
# used a keytab and krb5conf need to be supplied. local can be stacked with
# kerberos or ntlm authentication, so that the clients selects what it wants.
Authentication:
# - kerberos
# - local
- openid
# - ntlm
# The socket to connect to if using local auth. Ensure rdpgw auth is configured to
# use the same socket.
# 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
# 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)
# set this option to 'disable'. This is mutually exclusive with 'authentication: local'
# Note: rdp connections over a gateway require TLS
Tls: auto
# gateway address advertised in the rdp files and browser
GatewayAddress: localhost
# port to listen on (change to 80 or equivalent if not using TLS)
Port: 443
server:
# TLS certificate files (required)
certFile: server.pem
keyFile: key.pem
# gateway address advertised in the rdp files
gatewayAddress: localhost
# port to listen on
port: 443
# list of acceptable desktop hosts to connect to
Hosts:
hosts:
- localhost:3389
- my-{{ preferred_username }}-host:3389
# Allow the user to connect to any host (insecure)
- any
# if true the server randomly selects a host to connect to
# valid options are:
# - roundrobin, which selects a random host from the list (default)
# - signed, a listed host specified in the signed query parameter
# - unsigned, a listed host specified in the query parameter
# - any, insecurely allow any host specified in the query parameter
HostSelection: roundrobin
roundRobin: false
# a random strings of at least 32 characters to secure cookies on the client
# make sure to share this across the different pods
SessionKey: thisisasessionkeyreplacethisjetzt
SessionEncryptionKey: thisisasessionkeyreplacethisnunu!
# where to store session details. This can be either file or cookie (default: cookie)
# if a file store is chosen, it is required to have clients 'keep state' to the rdpgw
# instance they are connected to.
SessionStore: cookie
# tries to set the receive / send buffer of the connections to the client
# in case of high latency high bandwidth the defaults set by the OS might
# be to low for a good experience
# ReceiveBuf: 12582912
# SendBuf: 12582912
sessionKey: thisisasessionkeyreplacethisjetzt
sessionEncryptionKey: thisisasessionkeyreplacethisnunu!
# Open ID Connect specific settings
OpenId:
ProviderUrl: http://keycloak/auth/realms/test
ClientId: rdpgw
ClientSecret: your-secret
# Kerberos:
# Keytab: /etc/keytabs/rdpgw.keytab
# Krb5conf: /etc/krb5.conf
# enabled / disabled capabilities
Caps:
SmartCardAuth: false
# required for openid connect
TokenAuth: true
openId:
providerUrl: http://keycloak/auth/realms/test
clientId: rdpgw
clientSecret: your-secret
# enabled / disabled capabilities
caps:
smartCardAuth: false
tokenAuth: true
# connection timeout in minutes, 0 is limitless
IdleTimeout: 10
EnablePrinter: true
EnablePort: true
EnablePnp: true
EnableDrive: true
EnableClipboard: true
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
idleTimeout: 10
enablePrinter: true
enablePort: true
enablePnp: true
enableDrive: true
enableClipboard: true
client:
# this is a go string templated with {{ username }} and {{ token }}
# the example below uses the ASCII field separator to distinguish
# 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
# domain gets set in the rdp file and the domain name is stripped from the username
SplitUserDomain: false
# If true, removes "username" (and "domain" if SplitUserDomain is true) from RDP file.
# NoUsername: true
Security:
# a random string of 32 characters to secure cookies on the client
security:
# a random string of at least 32 characters to secure cookies on the client
# make sure to share this amongst different pods
PAATokenSigningKey: thisisasessionkeyreplacethisjetzt
# PAATokenEncryptionKey: thisisasessionkeyreplacethisjetzt
# a random string of 32 characters to secure cookies on the client
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 true the username will be set to a jwt with the username embedded into it
EnableUserToken: true
@ -344,49 +101,17 @@ Security:
# connection is opened.
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
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 9443. You will need to allow your browser
and [xrdp](http://www.xrdp.org) and exposes it services on port 443. 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`.
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
# with open id
cd dev/docker
docker-compose -f docker-compose.yml 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
docker-compose build
docker-compose 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
Point your browser to `https://your-gateway/connect`. After authentication
@ -402,154 +127,11 @@ 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).
## 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
* Integrate Open Policy Agent
* Integrate GOKRB5
* Integrate uber-go/zap
* Research: TLS defragmentation
* 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 访问限制和请求频率限制

View File

@ -1,15 +0,0 @@
# 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

@ -1,15 +1,15 @@
package web
package api
import (
"context"
"encoding/json"
"fmt"
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/security"
"github.com/bolkedebruin/rdpgw/security"
"log"
"net/http"
)
func TokenInfo(w http.ResponseWriter, r *http.Request) {
func (c *Config) TokenInfo(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Invalid request", http.StatusMethodNotAllowed)
return
@ -37,4 +37,4 @@ func TokenInfo(w http.ResponseWriter, r *http.Request) {
http.Error(w, "cannot encode json", http.StatusInternalServerError)
return
}
}
}

219
api/web.go Normal file
View File

@ -0,0 +1,219 @@
package api
import (
"context"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/gorilla/sessions"
"github.com/patrickmn/go-cache"
"golang.org/x/oauth2"
"log"
"math/rand"
"net/http"
"strconv"
"strings"
"time"
)
const (
RdpGwSession = "RDPGWSESSION"
MaxAge = 120
)
type TokenGeneratorFunc func(context.Context, string, string) (string, error)
type UserTokenGeneratorFunc func(context.Context, string) (string, error)
type Config struct {
SessionKey []byte
SessionEncryptionKey []byte
PAATokenGenerator TokenGeneratorFunc
UserTokenGenerator UserTokenGeneratorFunc
EnableUserToken bool
OAuth2Config *oauth2.Config
store *sessions.CookieStore
OIDCTokenVerifier *oidc.IDTokenVerifier
stateStore *cache.Cache
Hosts []string
GatewayAddress string
UsernameTemplate string
NetworkAutoDetect int
BandwidthAutoDetect int
ConnectionType int
SplitUserDomain bool
DefaultDomain string
}
func (c *Config) NewApi() {
if len(c.SessionKey) < 32 {
log.Fatal("Session key too small")
}
if len(c.Hosts) < 1 {
log.Fatal("Not enough hosts to connect to specified")
}
c.store = sessions.NewCookieStore(c.SessionKey, c.SessionEncryptionKey)
c.stateStore = cache.New(time.Minute*2, 5*time.Minute)
}
func (c *Config) HandleCallback(w http.ResponseWriter, r *http.Request) {
state := r.URL.Query().Get("state")
s, found := c.stateStore.Get(state)
if !found {
http.Error(w, "unknown state", http.StatusBadRequest)
return
}
url := s.(string)
ctx := context.Background()
oauth2Token, err := c.OAuth2Config.Exchange(ctx, r.URL.Query().Get("code"))
if err != nil {
http.Error(w, "Failed to exchange token: "+err.Error(), http.StatusInternalServerError)
return
}
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
if !ok {
http.Error(w, "No id_token field in oauth2 token.", http.StatusInternalServerError)
return
}
idToken, err := c.OIDCTokenVerifier.Verify(ctx, rawIDToken)
if err != nil {
http.Error(w, "Failed to verify ID Token: "+err.Error(), http.StatusInternalServerError)
return
}
resp := struct {
OAuth2Token *oauth2.Token
IDTokenClaims *json.RawMessage // ID Token payload is just JSON.
}{oauth2Token, new(json.RawMessage)}
if err := idToken.Claims(&resp.IDTokenClaims); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var data map[string]interface{}
if err := json.Unmarshal(*resp.IDTokenClaims, &data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
session, err := c.store.Get(r, RdpGwSession)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
session.Options.MaxAge = MaxAge
session.Values["preferred_username"] = data["preferred_username"]
session.Values["authenticated"] = true
session.Values["access_token"] = oauth2Token.AccessToken
if err = session.Save(r, w); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
http.Redirect(w, r, url, http.StatusFound)
}
func (c *Config) Authenticated(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
session, err := c.store.Get(r, RdpGwSession)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
found := session.Values["authenticated"]
if found == nil || !found.(bool) {
seed := make([]byte, 16)
rand.Read(seed)
state := hex.EncodeToString(seed)
c.stateStore.Set(state, r.RequestURI, cache.DefaultExpiration)
http.Redirect(w, r, c.OAuth2Config.AuthCodeURL(state), http.StatusFound)
return
}
ctx := context.WithValue(r.Context(), "preferred_username", session.Values["preferred_username"])
ctx = context.WithValue(ctx, "access_token", session.Values["access_token"])
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func (c *Config) HandleDownload(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
userName, ok := ctx.Value("preferred_username").(string)
if !ok {
log.Printf("preferred_username not found in context")
http.Error(w, errors.New("cannot find session or user").Error(), http.StatusInternalServerError)
return
}
// do a round robin selection for now
rand.Seed(time.Now().Unix())
host := c.Hosts[rand.Intn(len(c.Hosts))]
host = strings.Replace(host, "{{ preferred_username }}", userName, 1)
// split the username into user and domain
var user = userName
var domain = c.DefaultDomain
if c.SplitUserDomain {
creds := strings.SplitN(userName, "@", 2)
user = creds[0]
if len(creds) > 1 {
domain = creds[1]
}
}
render := user
if c.UsernameTemplate != "" {
render = fmt.Sprintf(c.UsernameTemplate)
render = strings.Replace(render, "{{ username }}", user, 1)
if c.UsernameTemplate == render {
log.Printf("Invalid username template. %s == %s", c.UsernameTemplate, user)
http.Error(w, errors.New("invalid server configuration").Error(), http.StatusInternalServerError)
return
}
}
token, err := c.PAATokenGenerator(ctx, user, host)
if err != nil {
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)
}
if c.EnableUserToken {
userToken, err := c.UserTokenGenerator(ctx, user)
if err != nil {
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)
}
render = strings.Replace(render, "{{ token }}", userToken, 1)
}
// authenticated
seed := make([]byte, 16)
rand.Read(seed)
fn := hex.EncodeToString(seed) + ".rdp"
w.Header().Set("Content-Disposition", "attachment; filename="+fn)
w.Header().Set("Content-Type", "application/x-rdp")
data := "full address:s:"+host+"\r\n"+
"gatewayhostname:s:"+c.GatewayAddress+"\r\n"+
"gatewaycredentialssource:i:5\r\n"+
"gatewayusagemethod:i:1\r\n"+
"gatewayprofileusagemethod:i:1\r\n"+
"gatewayaccesstoken:s:"+token+"\r\n"+
"networkautodetect:i:"+strconv.Itoa(c.NetworkAutoDetect)+"\r\n"+
"bandwidthautodetect:i:"+strconv.Itoa(c.BandwidthAutoDetect)+"\r\n"+
"connection type:i:"+strconv.Itoa(c.ConnectionType)+"\r\n"+
"username:s:"+render+"\r\n"+
"domain:s:"+domain+"\r\n"+
"bitmapcachesize:i:32000\r\n"
http.ServeContent(w, r, fn, time.Now(), strings.NewReader(data))
}

View File

@ -1,152 +0,0 @@
package main
import (
"context"
"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/msteinert/pam/v2"
"github.com/thought-machine/go-flags"
"google.golang.org/grpc"
"log"
"net"
"os"
"syscall"
)
const (
protocol = "unix"
)
var opts struct {
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"`
ConfigFile string `short:"c" long:"conf" default:"rdpgw-auth.yaml" description:"users config file for NTLM (yaml)"`
}
type AuthServiceImpl struct {
auth.UnimplementedAuthenticateServer
serviceName string
ntlm *ntlm.NTLMAuth
}
var conf config.Configuration
var _ auth.AuthenticateServer = (*AuthServiceImpl)(nil)
func NewAuthService(serviceName string, database database.Database) auth.AuthenticateServer {
s := &AuthServiceImpl{
serviceName: serviceName,
ntlm: ntlm.NewNTLMAuth(database),
}
return s
}
func (s *AuthServiceImpl) Authenticate(ctx context.Context, message *auth.UserPass) (*auth.AuthResponse, error) {
t, err := pam.StartFunc(s.serviceName, message.Username, func(s pam.Style, msg string) (string, error) {
switch s {
case pam.PromptEchoOff:
return message.Password, nil
case pam.PromptEchoOn, pam.ErrorMsg, pam.TextInfo:
return "", nil
}
return "", errors.New("unrecognized PAM message style")
})
r := &auth.AuthResponse{}
r.Authenticated = false
if err != nil {
log.Printf("Error authenticating user: %s due to: %s", message.Username, err)
r.Error = err.Error()
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 {
log.Printf("Authentication for user: %s failed due to: %s", message.Username, err)
r.Error = err.Error()
return r, nil
}
if err = t.AcctMgmt(0); err != nil {
log.Printf("Account authorization for user: %s failed due to %s", message.Username, err)
r.Error = err.Error()
return r, nil
}
log.Printf("User: %s authenticated", message.Username)
r.Authenticated = true
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() {
_, err := flags.Parse(&opts)
if err != nil {
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)
cleanup := func() {
if _, err := os.Stat(opts.SocketAddr); err == nil {
if err := os.RemoveAll(opts.SocketAddr); err != nil {
log.Fatal(err)
}
}
}
cleanup()
oldUmask := syscall.Umask(0)
listener, err := net.Listen(protocol, opts.SocketAddr)
syscall.Umask(oldUmask)
if err != nil {
log.Fatal(err)
}
server := grpc.NewServer()
// 根据配置选择使用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)
server.Serve(listener)
}

View File

@ -1,50 +0,0 @@
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
}

View File

@ -1,181 +0,0 @@
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
}

View File

@ -1,25 +0,0 @@
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

@ -1,43 +0,0 @@
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")
}
}

View File

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

View File

@ -1,164 +0,0 @@
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
}

View File

@ -1,168 +0,0 @@
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

@ -1,280 +0,0 @@
package config
import (
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/security"
"github.com/knadh/koanf/parsers/yaml"
"github.com/knadh/koanf/providers/confmap"
"github.com/knadh/koanf/providers/env"
"github.com/knadh/koanf/providers/file"
"github.com/knadh/koanf/v2"
"log"
"os"
"strings"
)
const (
TlsDisable = "disable"
TlsAuto = "auto"
HostSelectionSigned = "signed"
HostSelectionRoundRobin = "roundrobin"
SessionStoreCookie = "cookie"
SessionStoreFile = "file"
AuthenticationOpenId = "openid"
AuthenticationBasic = "local"
AuthenticationKerberos = "kerberos"
)
type Configuration struct {
Server ServerConfig `koanf:"server"`
OpenId OpenIDConfig `koanf:"openid"`
Kerberos KerberosConfig `koanf:"kerberos"`
Caps RDGCapsConfig `koanf:"caps"`
Security SecurityConfig `koanf:"security"`
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 {
GatewayAddress string `koanf:"gatewayaddress"`
Port int `koanf:"port"`
CertFile string `koanf:"certfile"`
KeyFile string `koanf:"keyfile"`
Hosts []string `koanf:"hosts"`
HostSelection string `koanf:"hostselection"`
SessionKey string `koanf:"sessionkey"`
SessionEncryptionKey string `koanf:"sessionencryptionkey"`
SessionStore string `koanf:"sessionstore"`
MaxSessionLength int `koanf:"maxsessionlength"`
SendBuf int `koanf:"sendbuf"`
ReceiveBuf int `koanf:"receivebuf"`
Tls string `koanf:"tls"`
Authentication []string `koanf:"authentication"`
AuthSocket string `koanf:"authsocket"`
BasicAuthTimeout int `koanf:"basicauthtimeout"`
}
type KerberosConfig struct {
Keytab string `koanf:"keytab"`
Krb5Conf string `koanf:"krb5conf"`
}
type OpenIDConfig struct {
ProviderUrl string `koanf:"providerurl"`
ClientId string `koanf:"clientid"`
ClientSecret string `koanf:"clientsecret"`
}
type RDGCapsConfig struct {
SmartCardAuth bool `koanf:"smartcardauth"`
TokenAuth bool `koanf:"tokenauth"`
IdleTimeout int `koanf:"idletimeout"`
RedirectAll bool `koanf:"redirectall"`
DisableRedirect bool `koanf:"disableredirect"`
EnableClipboard bool `koanf:"enableclipboard"`
EnablePrinter bool `koanf:"enableprinter"`
EnablePort bool `koanf:"enableport"`
EnablePnp bool `koanf:"enablepnp"`
EnableDrive bool `koanf:"enabledrive"`
}
type SecurityConfig struct {
PAATokenEncryptionKey string `koanf:"paatokenencryptionkey"`
PAATokenSigningKey string `koanf:"paatokensigningkey"`
UserTokenEncryptionKey string `koanf:"usertokenencryptionkey"`
UserTokenSigningKey string `koanf:"usertokensigningkey"`
QueryTokenSigningKey string `koanf:"querytokensigningkey"`
QueryTokenIssuer string `koanf:"querytokenissuer"`
VerifyClientIp bool `koanf:"verifyclientip"`
EnableUserToken bool `koanf:"enableusertoken"`
}
type ClientConfig struct {
Defaults string `koanf:"defaults"`
// kept for backwards compatibility
UsernameTemplate string `koanf:"usernametemplate"`
SplitUserDomain bool `koanf:"splituserdomain"`
NoUsername bool `koanf:"nousername"`
}
func ToCamel(s string) string {
s = strings.TrimSpace(s)
n := strings.Builder{}
n.Grow(len(s))
var capNext bool = true
for i, v := range []byte(s) {
vIsCap := v >= 'A' && v <= 'Z'
vIsLow := v >= 'a' && v <= 'z'
if capNext {
if vIsLow {
v += 'A'
v -= 'a'
}
} else if i == 0 {
if vIsCap {
v += 'a'
v -= 'A'
}
}
if vIsCap || vIsLow {
n.WriteByte(v)
capNext = false
} else if vIsNum := v >= '0' && v <= '9'; vIsNum {
n.WriteByte(v)
capNext = true
} else {
capNext = v == '_' || v == ' ' || v == '-' || v == '.'
if v == '.' {
n.WriteByte(v)
}
}
}
return n.String()
}
var Conf Configuration
func Load(configFile string) Configuration {
var k = koanf.New(".")
k.Load(confmap.Provider(map[string]interface{}{
"Server.Tls": "auto",
"Server.Port": 443,
"Server.SessionStore": "cookie",
"Server.HostSelection": "roundrobin",
"Server.Authentication": "openid",
"Server.AuthSocket": "/tmp/rdpgw-auth.sock",
"Server.BasicAuthTimeout": 5,
"Client.NetworkAutoDetect": 1,
"Client.BandwidthAutoDetect": 1,
"Security.VerifyClientIp": true,
"Caps.TokenAuth": true,
}, "."), 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 {
log.Fatalf("Error loading config from file: %v", err)
}
}
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 = ToCamel(key)
v = strings.Trim(v, " ")
// handle lists
if strings.Contains(v, " ") {
return key, strings.Split(v, " ")
}
return key, v
}), nil); err != nil {
log.Fatalf("Error loading config from environment: %v", err)
}
koanfTag := koanf.UnmarshalConf{Tag: "koanf"}
k.UnmarshalWithConf("Server", &Conf.Server, koanfTag)
k.UnmarshalWithConf("OpenId", &Conf.OpenId, koanfTag)
k.UnmarshalWithConf("Caps", &Conf.Caps, koanfTag)
k.UnmarshalWithConf("Security", &Conf.Security, koanfTag)
k.UnmarshalWithConf("Client", &Conf.Client, koanfTag)
k.UnmarshalWithConf("Kerberos", &Conf.Kerberos, koanfTag)
k.UnmarshalWithConf("PXVDI", &Conf.PXVDI, koanfTag)
if len(Conf.Security.PAATokenEncryptionKey) != 32 {
Conf.Security.PAATokenEncryptionKey, _ = security.GenerateRandomString(32)
log.Printf("No valid `security.paatokenencryptionkey` specified (empty or not 32 characters). Setting to random")
}
if len(Conf.Security.PAATokenSigningKey) != 32 {
Conf.Security.PAATokenSigningKey, _ = security.GenerateRandomString(32)
log.Printf("No valid `security.paatokensigningkey` specified (empty or not 32 characters). Setting to random")
}
if Conf.Security.EnableUserToken {
if len(Conf.Security.UserTokenEncryptionKey) != 32 {
Conf.Security.UserTokenEncryptionKey, _ = security.GenerateRandomString(32)
log.Printf("No valid `security.usertokenencryptionkey` specified (empty or not 32 characters). Setting to random")
}
if len(Conf.Security.UserTokenSigningKey) != 32 {
Conf.Security.UserTokenSigningKey, _ = security.GenerateRandomString(32)
log.Printf("No valid `security.usertokensigningkey` specified (empty or not 32 characters). Setting to random")
}
}
if len(Conf.Server.SessionKey) != 32 {
Conf.Server.SessionKey, _ = security.GenerateRandomString(32)
log.Printf("No valid `server.sessionkey` specified (empty or not 32 characters). Setting to random")
}
if len(Conf.Server.SessionEncryptionKey) != 32 {
Conf.Server.SessionEncryptionKey, _ = security.GenerateRandomString(32)
log.Printf("No valid `server.sessionencryptionkey` specified (empty or not 32 characters). Setting to random")
}
if Conf.Server.HostSelection == "signed" && len(Conf.Security.QueryTokenSigningKey) == 0 {
log.Fatalf("host selection is set to `signed` but `querytokensigningkey` is not set")
}
if Conf.Server.BasicAuthEnabled() && Conf.Server.Tls == "disable" {
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() {
log.Fatalf("openid is configured but tokenauth disabled")
}
if Conf.Server.KerberosEnabled() && Conf.Kerberos.Keytab == "" {
log.Fatalf("kerberos is configured but no keytab was specified")
}
// prepend '//' if required for URL parsing
if !strings.Contains(Conf.Server.GatewayAddress, "//") {
Conf.Server.GatewayAddress = "//" + Conf.Server.GatewayAddress
}
return Conf
}
func (s *ServerConfig) OpenIDEnabled() bool {
return s.matchAuth("openid")
}
func (s *ServerConfig) KerberosEnabled() bool {
return s.matchAuth("kerberos")
}
func (s *ServerConfig) BasicAuthEnabled() bool {
return s.matchAuth("local") || s.matchAuth("basic")
}
func (s *ServerConfig) NtlmEnabled() bool {
return s.matchAuth("ntlm")
}
func (s *ServerConfig) matchAuth(needle string) bool {
for _, q := range s.Authentication {
if q == needle {
return true
}
}
return false
}

View File

@ -1,57 +0,0 @@
package identity
import (
"context"
"net/http"
"time"
)
const (
CTXKey = "github.com/bolkedebruin/rdpgw/common/identity"
AttrRemoteAddr = "remoteAddr"
AttrClientIp = "clientIp"
AttrProxies = "proxyAddresses"
AttrAccessToken = "accessToken" // todo remove for security reasons
)
type Identity interface {
UserName() string
SetUserName(string)
DisplayName() string
SetDisplayName(string)
Domain() string
SetDomain(string)
Authenticated() bool
SetAuthenticated(bool)
AuthTime() time.Time
SetAuthTime(time2 time.Time)
SessionId() string
SetAttribute(string, interface{})
GetAttribute(string) interface{}
Attributes() map[string]interface{}
DelAttribute(string)
Email() string
SetEmail(string)
Expiry() time.Time
SetExpiry(time.Time)
Marshal() ([]byte, error)
Unmarshal([]byte) error
}
func AddToRequestCtx(id Identity, r *http.Request) *http.Request {
ctx := r.Context()
ctx = context.WithValue(ctx, CTXKey, id)
return r.WithContext(ctx)
}
func FromRequestCtx(r *http.Request) Identity {
return FromCtx(r.Context())
}
func FromCtx(ctx context.Context) Identity {
if id, ok := ctx.Value(CTXKey).(Identity); ok {
return id
}
return nil
}

View File

@ -1,28 +0,0 @@
package identity
import (
"log"
"testing"
)
func TestMarshalling(t *testing.T) {
u := NewUser()
u.SetUserName("ANAME")
u.SetAuthenticated(true)
u.SetDomain("DOMAIN")
c := NewUser()
data, err := u.Marshal()
if err != nil {
log.Fatalf("Cannot marshal %s", err)
}
err = c.Unmarshal(data)
if err != nil {
t.Fatalf("Error while unmarshalling: %s", err)
}
if u.UserName() != c.UserName() || u.Authenticated() != c.Authenticated() || u.Domain() != c.Domain() {
t.Fatalf("identities not equal: %+v != %+v", u, c)
}
}

View File

@ -1,170 +0,0 @@
package identity
import (
"bytes"
"encoding/gob"
"github.com/google/uuid"
"time"
)
type User struct {
authenticated bool
domain string
userName string
displayName string
email string
authTime time.Time
sessionId string
expiry time.Time
attributes map[string]interface{}
groupMembership map[string]bool
}
type user struct {
Authenticated bool
UserName string
Domain string
DisplayName string
Email string
AuthTime time.Time
SessionId string
Expiry time.Time
Attributes map[string]interface{}
GroupMembership map[string]bool
}
func NewUser() *User {
uuid := uuid.New().String()
return &User{
attributes: make(map[string]interface{}),
groupMembership: make(map[string]bool),
sessionId: uuid,
}
}
func (u *User) UserName() string {
return u.userName
}
func (u *User) SetUserName(s string) {
u.userName = s
}
func (u *User) DisplayName() string {
if u.displayName == "" {
return u.userName
}
return u.displayName
}
func (u *User) SetDisplayName(s string) {
u.displayName = s
}
func (u *User) Domain() string {
return u.domain
}
func (u *User) SetDomain(s string) {
u.domain = s
}
func (u *User) Authenticated() bool {
return u.authenticated
}
func (u *User) SetAuthenticated(b bool) {
u.authenticated = b
}
func (u *User) AuthTime() time.Time {
return u.authTime
}
func (u *User) SetAuthTime(t time.Time) {
u.authTime = t
}
func (u *User) SessionId() string {
return u.sessionId
}
func (u *User) SetAttribute(s string, i interface{}) {
u.attributes[s] = i
}
func (u *User) GetAttribute(s string) interface{} {
if found, ok := u.attributes[s]; ok {
return found
}
return nil
}
func (u *User) Attributes() map[string]interface{} {
return u.attributes
}
func (u *User) DelAttribute(s string) {
delete(u.attributes, s)
}
func (u *User) Email() string {
return u.email
}
func (u *User) SetEmail(s string) {
u.email = s
}
func (u *User) Expiry() time.Time {
return u.expiry
}
func (u *User) SetExpiry(t time.Time) {
u.expiry = t
}
func (u *User) Marshal() ([]byte, error) {
buf := new(bytes.Buffer)
enc := gob.NewEncoder(buf)
uu := user{
Authenticated: u.authenticated,
UserName: u.userName,
Domain: u.domain,
DisplayName: u.displayName,
Email: u.email,
AuthTime: u.authTime,
SessionId: u.sessionId,
Expiry: u.expiry,
Attributes: u.attributes,
GroupMembership: u.groupMembership,
}
err := enc.Encode(uu)
if err != nil {
return []byte{}, err
}
return buf.Bytes(), nil
}
func (u *User) Unmarshal(b []byte) error {
buf := bytes.NewBuffer(b)
dec := gob.NewDecoder(buf)
var uu user
err := dec.Decode(&uu)
if err != nil {
return err
}
u.sessionId = uu.SessionId
u.userName = uu.UserName
u.domain = uu.Domain
u.displayName = uu.DisplayName
u.email = uu.Email
u.authenticated = uu.Authenticated
u.authTime = uu.AuthTime
u.expiry = uu.Expiry
u.attributes = uu.Attributes
u.groupMembership = uu.GroupMembership
return nil
}

View File

@ -1,210 +0,0 @@
package kdcproxy
import (
"fmt"
krbconfig "github.com/bolkedebruin/gokrb5/v8/config"
"github.com/jcmturner/gofork/encoding/asn1"
"io"
"log"
"net"
"net/http"
"time"
)
const (
maxLength = 128 * 1024
systemConfigPath = "/etc/krb5.conf"
timeout = 5 * time.Second
)
type KdcProxyMsg struct {
Message []byte `asn1:"tag:0,explicit"`
Realm string `asn1:"tag:1,optional"`
Flags int `asn1:"tag:2,optional"`
}
type Kdc struct {
Realm string
Host string
Proto string
Conn net.Conn
}
type KerberosProxy struct {
krb5Config *krbconfig.Config
}
func InitKdcProxy(krb5Conf string) KerberosProxy {
path := systemConfigPath
if krb5Conf != "" {
path = krb5Conf
}
cfg, err := krbconfig.Load(path)
if err != nil {
log.Fatalf("Cannot load krb5 config %s due to %s", path, err)
}
return KerberosProxy{
krb5Config: cfg,
}
}
func (k KerberosProxy) Handler(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
length := r.ContentLength
if length == -1 {
http.Error(w, "Content length required", http.StatusLengthRequired)
return
}
if length > maxLength {
http.Error(w, "Request entity too large", http.StatusRequestEntityTooLarge)
return
}
data := make([]byte, length)
_, err := io.ReadFull(r.Body, data)
if err != nil {
log.Printf("Error reading from stream: %s", err)
http.Error(w, "Error reading from stream", http.StatusInternalServerError)
return
}
msg, err := decode(data)
if err != nil {
log.Printf("Cannot unmarshal: %s", err)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
krb5resp, err := k.forward(msg.Realm, msg.Message)
if err != nil {
log.Printf("cannot forward to kdc due to %s", err)
http.Error(w, "Service unavailable", http.StatusServiceUnavailable)
return
}
reply, err := encode(krb5resp)
if err != nil {
log.Printf("unable to encode krb5 message due to %s", err)
http.Error(w, "encoding error", http.StatusInternalServerError)
}
w.Header().Set("Content-Type", "application/kerberos")
w.Write(reply)
}
func (k *KerberosProxy) forward(realm string, data []byte) (resp []byte, err error) {
if realm == "" {
realm = k.krb5Config.LibDefaults.DefaultRealm
}
// load udp first as is the default for kerberos
udpCnt, udpKdcs, err := k.krb5Config.GetKDCs(realm, false)
if err != nil {
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 {
conn, err := net.Dial(kdcs[i].Proto, kdcs[i].Host)
if err != nil {
log.Printf("error connecting to %s due to %s, trying next if available", kdcs[i], err)
continue
}
conn.SetDeadline(time.Now().Add(timeout))
// if we proxy over UDP remove the length prefix
if kdcs[i].Proto == "tcp" {
_, err = conn.Write(data)
} else {
_, err = conn.Write(data[4:])
}
if err != nil {
log.Printf("cannot write packet data to %s due to %s, trying next if available", kdcs[i], err)
conn.Close()
continue
}
kdcs[i].Conn = conn
go awaitReply(conn, kdcs[i].Proto == "udp", replies)
}
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) {
var m KdcProxyMsg
rest, err := asn1.Unmarshal(data, &m)
if err != nil {
return nil, err
}
if len(rest) > 0 {
return nil, fmt.Errorf("trailing data in request")
}
return &m, nil
}
func encode(krb5data []byte) (r []byte, err error) {
m := KdcProxyMsg{Message: krb5data}
enc, err := asn1.Marshal(m)
if err != nil {
log.Printf("cannot marshal due to %s", err)
return nil, err
}
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

@ -1,312 +0,0 @@
package main
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"github.com/bolkedebruin/gokrb5/v8/keytab"
"github.com/bolkedebruin/gokrb5/v8/service"
"github.com/bolkedebruin/gokrb5/v8/spnego"
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/config"
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/kdcproxy"
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/protocol"
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/security"
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/web"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/gorilla/mux"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/thought-machine/go-flags"
"golang.org/x/crypto/acme/autocert"
"golang.org/x/oauth2"
"log"
"net/http"
"net/url"
"os"
"strconv"
"time"
)
const (
gatewayEndPoint = "/remoteDesktopGateway/"
kdcProxyEndPoint = "/KdcProxy"
)
var opts struct {
ConfigFile string `short:"c" long:"conf" default:"rdpgw.yaml" description:"config file (yaml)"`
}
var conf config.Configuration
func initOIDC(callbackUrl *url.URL) *web.OIDC {
// set oidc config
provider, err := oidc.NewProvider(context.Background(), conf.OpenId.ProviderUrl)
if err != nil {
log.Fatalf("Cannot get oidc provider: %s", err)
}
oidcConfig := &oidc.Config{
ClientID: conf.OpenId.ClientId,
}
verifier := provider.Verifier(oidcConfig)
oauthConfig := oauth2.Config{
ClientID: conf.OpenId.ClientId,
ClientSecret: conf.OpenId.ClientSecret,
RedirectURL: callbackUrl.String(),
Endpoint: provider.Endpoint(),
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
}
security.OIDCProvider = provider
security.Oauth2Config = oauthConfig
o := web.OIDCConfig{
OAuth2Config: &oauthConfig,
OIDCTokenVerifier: verifier,
}
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() {
// load config
_, err := flags.Parse(&opts)
if err != nil {
panic(err)
}
conf = config.Load(opts.ConfigFile)
// set callback url and external advertised gateway address
url, err := url.Parse(conf.Server.GatewayAddress)
if err != nil {
log.Printf("Cannot parse server gateway address %s due to %s", url, err)
}
if url.Scheme == "" {
url.Scheme = "https"
}
url.Path = "callback"
// set security options
security.VerifyClientIP = conf.Security.VerifyClientIp
security.SigningKey = []byte(conf.Security.PAATokenSigningKey)
security.EncryptionKey = []byte(conf.Security.PAATokenEncryptionKey)
security.UserEncryptionKey = []byte(conf.Security.UserTokenEncryptionKey)
security.UserSigningKey = []byte(conf.Security.UserTokenSigningKey)
security.QuerySigningKey = []byte(conf.Security.QueryTokenSigningKey)
security.HostSelection = conf.Server.HostSelection
security.Hosts = conf.Server.Hosts
// init session store
web.InitStore([]byte(conf.Server.SessionKey),
[]byte(conf.Server.SessionEncryptionKey),
conf.Server.SessionStore,
conf.Server.MaxSessionLength,
)
// configure web backend
w := &web.Config{
QueryInfo: security.QueryInfo,
QueryTokenIssuer: conf.Security.QueryTokenIssuer,
EnableUserToken: conf.Security.EnableUserToken,
Hosts: conf.Server.Hosts,
HostSelection: conf.Server.HostSelection,
RdpOpts: web.RdpOpts{
UsernameTemplate: conf.Client.UsernameTemplate,
SplitUserDomain: conf.Client.SplitUserDomain,
NoUsername: conf.Client.NoUsername,
},
GatewayAddress: url,
TemplateFile: conf.Client.Defaults,
}
if conf.Caps.TokenAuth {
w.PAATokenGenerator = security.GeneratePAAToken
}
if conf.Security.EnableUserToken {
w.UserTokenGenerator = security.GenerateUserToken
}
h := w.NewHandler()
log.Printf("Starting remote desktop gateway server")
// 启动连接信息记录器
startConnectionLogger(10 * time.Second)
cfg := &tls.Config{}
// configure tls security
if conf.Server.Tls == config.TlsDisable {
log.Printf("TLS disabled - rdp gw connections require tls, make sure to have a terminator")
} else {
// auto config
tlsConfigured := false
tlsDebug := os.Getenv("SSLKEYLOGFILE")
if tlsDebug != "" {
w, err := os.OpenFile(tlsDebug, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
log.Fatalf("Cannot open key log file %s for writing %s", tlsDebug, err)
}
log.Printf("Key log file set to: %s", tlsDebug)
cfg.KeyLogWriter = w
}
if conf.Server.KeyFile != "" && conf.Server.CertFile != "" {
cert, err := tls.LoadX509KeyPair(conf.Server.CertFile, conf.Server.KeyFile)
if err != nil {
log.Printf("Cannot load certfile or keyfile (%s) falling back to acme", err)
}
cfg.Certificates = append(cfg.Certificates, cert)
tlsConfigured = true
}
if !tlsConfigured {
log.Printf("Using acme / letsencrypt for tls configuration. Enabling http (port 80) for verification")
// setup a simple handler which sends a HTHS header for six months (!)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Strict-Transport-Security", "max-age=15768000 ; includeSubDomains")
fmt.Fprintf(w, "Hello from RDPGW")
})
certMgr := autocert.Manager{
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist(url.Host),
Cache: autocert.DirCache("/tmp/rdpgw"),
}
cfg.GetCertificate = certMgr.GetCertificate
go func() {
http.ListenAndServe(":80", certMgr.HTTPHandler(nil))
}()
}
}
// gateway confg
gw := protocol.Gateway{
RedirectFlags: protocol.RedirectFlags{
Clipboard: conf.Caps.EnableClipboard,
Drive: conf.Caps.EnableDrive,
Printer: conf.Caps.EnablePrinter,
Port: conf.Caps.EnablePort,
Pnp: conf.Caps.EnablePnp,
DisableAll: conf.Caps.DisableRedirect,
EnableAll: conf.Caps.RedirectAll,
},
IdleTimeout: conf.Caps.IdleTimeout,
SmartCardAuth: conf.Caps.SmartCardAuth,
TokenAuth: conf.Caps.TokenAuth,
ReceiveBuf: conf.Server.ReceiveBuf,
SendBuf: conf.Server.SendBuf,
}
if conf.Caps.TokenAuth {
gw.CheckPAACookie = security.CheckPAACookie
gw.CheckHost = security.CheckSession(security.CheckHost)
} else {
gw.CheckHost = security.CheckHost
}
r := mux.NewRouter()
// ensure identity is set in context and get some extra info
r.Use(web.EnrichContext)
// prometheus metrics
r.Handle("/metrics", promhttp.Handler())
// for sso callbacks
r.HandleFunc("/tokeninfo", web.TokenInfo)
// gateway endpoint
rdp := r.PathPrefix(gatewayEndPoint).Subrouter()
// openid
if conf.Server.OpenIDEnabled() {
log.Printf("enabling openid extended authentication")
o := initOIDC(url)
r.Handle("/connect", o.Authenticated(http.HandlerFunc(h.HandleDownload)))
r.HandleFunc("/callback", o.HandleCallback)
// only enable un-auth endpoint for openid only config
if !conf.Server.KerberosEnabled() && !conf.Server.BasicAuthEnabled() && !conf.Server.NtlmEnabled() {
rdp.Name("gw").HandlerFunc(gw.HandleGatewayProtocol)
}
}
// for stacking of authentication
auth := web.NewAuthMux()
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
if conf.Server.BasicAuthEnabled() {
log.Printf("enabling basic authentication")
q := web.BasicAuthHandler{SocketAddress: conf.Server.AuthSocket, Timeout: conf.Server.BasicAuthTimeout}
rdp.NewRoute().HeadersRegexp("Authorization", "Basic").HandlerFunc(q.BasicAuth(gw.HandleGatewayProtocol))
auth.Register(`Basic realm="restricted", charset="UTF-8"`)
}
// spnego / kerberos
if conf.Server.KerberosEnabled() {
log.Printf("enabling kerberos authentication")
keytab, err := keytab.Load(conf.Kerberos.Keytab)
if err != nil {
log.Fatalf("Cannot load keytab: %s", err)
}
rdp.NewRoute().HeadersRegexp("Authorization", "Negotiate").Handler(
spnego.SPNEGOKRB5Authenticate(web.TransposeSPNEGOContext(http.HandlerFunc(gw.HandleGatewayProtocol)),
keytab,
service.Logger(log.Default())))
// kdcproxy
k := kdcproxy.InitKdcProxy(conf.Kerberos.Krb5Conf)
r.HandleFunc(kdcProxyEndPoint, k.Handler).Methods("POST")
auth.Register("Negotiate")
}
// setup server
server := http.Server{
Addr: ":" + strconv.Itoa(conf.Server.Port),
Handler: r,
TLSConfig: cfg,
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)), // disable http2
}
if conf.Server.Tls == config.TlsDisable {
err = server.ListenAndServe()
} else {
err = server.ListenAndServeTLS("", "")
}
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}

View File

@ -1,155 +0,0 @@
package protocol
/*
const (
ERROR_NO = 0x0000000
ERROR_CLIENT_DISCONNECT = 0x0000001
ERROR_CLIENT_LOGOFF = 0x0000002
ERROR_NETWORK_DISCONNECT = 0x0000003
ERROR_NOT_FOUND = 0x0000104
ERROR_NO_MEM = 0x0000106
ERROR_CONNECT_TIMEOUT = 0x0000108
ERROR_SMARTCARD_SERVICE = 0x000010A
ERROR_UNAVAILABLE = 0x0000204
ERROR_SMARTCARD_READER = 0x000020A
ERROR_NETWORK = 0x0000304
ERROR_SMARTCART_NOCARD = 0x000030A
ERROR_SECURITY = 0x0000406
ERROR_INVALID_NAME = 0x0000408
ERROR_SMARTCARD_SUBSYSTEM = 0x000040A
ERROR_GENERIC = 0x0000704
ERROR_CONSOLE_EXIST = 0x0000708
ERROR_LICENSING_PROTOCOL = 0x0000808
ERROR_NETWORK_GENERIC = 0x0000904
ERROR_SECURITY_UNEXPECTED_CERTIFICATE = 0x0000907
ERROR_LICENSING_TIMEOUT = 0x0000908
ERROR_SECURITY_USER = 0x0000A07
ERROR_GENERIC_UNAVAIL = 0x0000B04
ERROR_ENCRYPTION = 0x0000B06
ERROR_SECURITY_USER_DISABLED = 0x0000B07
ERROR_SECURITY_NLA_REQUIRED = 0x0000B09
ERROR_SECURITY_USER_RESTRICTION = 0x0000C07
ERROR_DECOMPRESSION = 0x0000C08
ERROR_SECURITY_USER_LOCKED_OUT = 0x0000D07
ERROR_SECURITY_USER_DIALOG_REQUIRED = 0x0000D09
ERROR_SECURITY_FIPS_REQUIRED = 0x0000E06
ERROR_SECURITY_USER_EXPIRED = 0x0000E07
ERROR_GENERIC_FAILED = 0x0000E08
ERROR_SERVER_RA_UNAVAILABLE = 0x0000E09
ERROR_SECURITY_USER_PASSWORD_EXPIRED = 0x0000F07
ERROR_SECURITY_USER_CREDENTIALS_NOT_SENT = 0x0000F08
ERROR_SECURITY_USER_TIME_RESTRICTION = 0x0001007
ERROR_LOW_VIDEO = 0x0001008
ERROR_SECURITY_USER_COMPUTER_RANGE = 0x0001107
ERROR_SECURITY_USER_CHANGE_PASSWORD = 0x0001207
ERROR_SECURITY_USER_LOGON_TYPE = 0x0001307
ERROR_KRB_SUB_REQUIRED = 0x0001407
ERROR_SECURITY_SERVER_INVALID_CERTIFICATE = 0x0001B07
ERROR_SECURITY_SERVER_TIMESKEW = 0x0001D07
ERROR_SECURITY_SMARTCARD_LOCKEDOUT = 0x0002207
ERROR_RELAUNCH_APP = 0x0002507
ERROR_UPGRADE_CLIENT = 0x0002604
ERROR_RELAUNCH_REMOTE = 0x2000001
ERROR_REMOTEAPP_UNSUPPORTED = 0x2000002
ERROR_SECURITY_USER_PASSWORD_INVALID = 0x3000001
ERROR_SECURITY_CERTIFICATE_REVOKE_LIST_UNAVAIL = 0x3000002
ERROR_SECURITY_CERTIFICATE_INVALID = 0x3000003
ERROR_SECURITY_CERTIFICATE_REVOKED = 0x3000004
ERROR_SECURITY_GATEWAY_IDENTITY = 0x3000005
ERROR_SECURITY_GATEWAY_SUBJECT = 0x3000006
ERROR_SECURITY_GATEWAY_EXPIRED = 0x3000007
ERROR_SECURITY_REMOTE_ERROR = 0x3000008
ERROR_GATEWAY_NETWORK_SEND = 0x3000009
ERROR_GATEWAY_NETWORK_RECEIVE = 0x300000A
ERROR_SECURITY_ALTERNATE = 0x300000B
ERROR_GATEWAY_INVALID_ADDRESS = 0x300000C
ERROR_GATEWAY_TEMP_UNAVAIL = 0x300000D
ERROR_REMOTE_CLIENT_MISSING = 0x300000E
ERROR_GATEWAY_LOW_RESOURCES = 0x300000F
ERROR_GATEWAY_CLIENT_DLL = 0x3000010
ERROR_SMARTCART_NOSERVICE = 0x3000011
ERROR_SECURITY_SMARTCARD_REMOVED = 0x3000012
ERROR_SECURITY_SMARTCARD_REQUIRED = 0x3000013
ERROR_SECURITY_SMARTCARD_REMOVED2 = 0x3000014
ERROR_SECURITY_USER_PASSWORD_INVALID2 = 0x3000015
ERROR_SECURITY_TRANSPORT = 0x3000017
ERROR_GATEWAY_TERMINATE = 0x3000018
ERROR_GATEWAY_ADMIN_TERMINATE = 0x3000019
ERROR_SECURITY_USER_CREDENTIALS = 0x300001A
ERROR_SECURITY_GATEWAY_NOT_PERMITTED = 0x300001B
ERROR_SECURITY_GATEWAY_UNAUTHORIZED = 0x300001C
ERROR_SECURITY_GATEWAY_RESTRICTED = 0x300001F
ERROR_SECURITY_PROXY_AUTH = 0x3000020
ERROR_SECURITY_USER_PASSWORD_MUST_CHANGE = 0x3000021
ERROR_GATEWAY_MAX_REACHED = 0x3000022
ERROR_GATEWAY_UNSUPPORTED_REQUEST = 0x3000023
ERROR_GATEWAY_UNSUPPORTED_CAP = 0x3000024
ERROR_GATEWAY_INCOMPAT = 0x3000025
ERROR_SECURITY_SMARTCARD_INVALID_CREDENTIALS = 0x3000026
ERROR_SECURITY_NLA_INVALID = 0x3000027
ERROR_GATEWAY_NO_CERTIFICATE = 0x3000028
ERROR_GATEWAY_NOT_ALLOWED = 0x3000029
ERROR_GATEWAY_INVALID_CERTIFICATE = 0x300002A
ERROR_SECURITY_GATEWAY_USER_PASSWORD_REQUIRED = 0x300002B
ERROR_SECURITY_GATEWAY_SMARTCARD_REQUIRED = 0x300002C
ERROR_SECURITY_SMARTCARD_UNAVAIL = 0x300002D
ERROR_SECURITY_FIREWALL_NOAUTH = 0x300002F
ERROR_SECURITY_FIREWALL_AUTH = 0x3000030
ERROR_NO_INPUT = 0x3000032
ERROR_TIMEOUT = 0x3000033
ERROR_SECURITY_GATEWAY_COOKIE_INVALID = 0x3000034
ERROR_SECURITY_GATEWAY_COOKIE_REJECTED = 0x3000035
ERROR_SECURITY_GATEWAY_AUTH_METHOD = 0x3000037
ERROR_SECURITY_USER_PERIOD_AUTH = 0x3000038
ERROR_SECURITY_USER_PERIOD_AUTHZ = 0x3000039
ERROR_SECURITY_GATEWAY_POLICY = 0x300003B
ERROR_SECURITY_SMARTCARD_CERTIFICATE = 0x300003C
ERROR_LOGON_FIRST = 0x300003D
ERROR_AUTH_LOGON_FIRST = 0x300003E
ERROR_SESSION_ENDED = 0x300003F
ERROR_SESSION_ENDED_AUTH = 0x3000040
ERROR_SECURITY_GATEWAY_NAP = 0x3000041
ERROR_COOKIE_SIZE = 0x3000042
ERROR_PROXY_CONFIG = 0x3000044
ERROR_NO_PERMISSION = 0x3000045
ERROR_NO_RESOURCES = 0x3000046
ERROR_RESOURCE_ACCESS = 0x3000047
ERROR_UPGRADE_CLIENT2 = 0x3000049
ERROR_SECURITY_NETWORK_HTTPS = 0x300004A
ERROR_TEMP_FAIL = 0x300004B
ERROR_SECURITY_USER_MISMATCH = 0x300004C
ERROR_AZURE_TOO_MANY = 0x300004D
ERROR_MAX_USER = 0x300004E
ERROR_AZURE_TRIAL = 0x300004F
ERROR_AZURE_EXPIRED = 0x3000050
)
*/
/* Common Error Code */
const (
ERROR_SUCCESS = 0x00000000
ERROR_ACCESS_DENIED = 0x00000005
E_PROXY_INTERNALERROR = 0x800759D8
E_PROXY_RAP_ACCESSDENIED = 0x800759DA
E_PROXY_NAP_ACCESSDENIED = 0x800759DB
E_PROXY_ALREADYDISCONNECTED = 0x800759DF
E_PROXY_QUARANTINE_ACCESSDENIED = 0x800759ED
E_PROXY_NOCERTAVAILABLE = 0x800759EE
E_PROXY_COOKIE_BADPACKET = 0x800759F7
E_PROXY_COOKIE_AUTHENTICATION_ACCESS_DENIED = 0x800759F8
E_PROXY_UNSUPPORTED_AUTHENTICATION_METHOD = 0x800759F9
E_PROXY_CAPABILITYMISMATCH = 0x800759E9
E_PROXY_TS_CONNECTFAILED = 0x000059DD
E_PROXY_MAXCONNECTIONSREACHED = 0x000059E6
// E_PROXY_INTERNALERROR = 0x000059D8
ERROR_GRACEFUL_DISCONNECT = 0x000004CA
E_PROXY_NOTSUPPORTED = 0x000059E8
SEC_E_LOGON_DENIED = 0x8009030C
E_PROXY_SESSIONTIMEOUT = 0x000059F6
E_PROXY_REAUTH_AUTHN_FAILED = 0x000059FA
E_PROXY_REAUTH_CAP_FAILED = 0x000059FB
E_PROXY_REAUTH_RAP_FAILED = 0x000059FC
E_PROXY_SDR_NOT_SUPPORTED_BY_TS = 0x000059FD
E_PROXY_REAUTH_NAP_FAILED = 0x00005A00
E_PROXY_CONNECTIONABORTED = 0x000004D4
)

View File

@ -1,228 +0,0 @@
package protocol
import (
"context"
"errors"
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/identity"
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/transport"
"github.com/google/uuid"
"github.com/gorilla/websocket"
"github.com/patrickmn/go-cache"
"log"
"net"
"net/http"
"reflect"
"syscall"
"time"
)
const (
rdgConnectionIdKey = "Rdg-Connection-Id"
MethodRDGIN = "RDG_IN_DATA"
MethodRDGOUT = "RDG_OUT_DATA"
)
type CheckPAACookieFunc func(context.Context, string) (bool, error)
type CheckClientNameFunc func(context.Context, string) (bool, error)
type CheckHostFunc func(context.Context, string) (bool, error)
type Gateway struct {
// CheckPAACookie verifies if the PAA cookie sent by the client is valid
CheckPAACookie CheckPAACookieFunc
// CheckClientName verifies if the client name is allowed to connect
CheckClientName CheckClientNameFunc
// CheckHost verifies if the client is allowed to connect to the remote host
CheckHost CheckHostFunc
// RedirectFlags sets what devices the client is allowed to redirect to the remote host
RedirectFlags RedirectFlags
// IdleTimeOut is used to determine when to disconnect clients that have been idle
IdleTimeout int
// SmartCardAuth sets whether to use smart card based authentication
SmartCardAuth bool
// TokenAuth sets whether to use token/cookie based authentication
TokenAuth bool
ReceiveBuf int
SendBuf int
}
var upgrader = websocket.Upgrader{}
var c = cache.New(5*time.Minute, 10*time.Minute)
func (g *Gateway) HandleGatewayProtocol(w http.ResponseWriter, r *http.Request) {
connectionCache.Set(float64(c.ItemCount()))
var t *Tunnel
ctx := r.Context()
id := identity.FromRequestCtx(r)
connId := r.Header.Get(rdgConnectionIdKey)
x, found := c.Get(connId)
if !found {
t = &Tunnel{
RDGId: connId,
RemoteAddr: id.GetAttribute(identity.AttrRemoteAddr).(string),
User: id,
}
} else {
t = x.(*Tunnel)
}
ctx = context.WithValue(ctx, CtxTunnel, t)
if r.Method == MethodRDGOUT {
if r.Header.Get("Connection") != "upgrade" && r.Header.Get("Upgrade") != "websocket" {
g.handleLegacyProtocol(w, r.WithContext(ctx), t)
return
}
r.Method = "GET" // force
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("Cannot upgrade falling back to old protocol: %t", err)
return
}
defer conn.Close()
err = g.setSendReceiveBuffers(conn.UnderlyingConn())
if err != nil {
log.Printf("Cannot set send/receive buffers: %t", err)
}
g.handleWebsocketProtocol(ctx, conn, t)
} else if r.Method == MethodRDGIN {
g.handleLegacyProtocol(w, r.WithContext(ctx), t)
}
}
func (g *Gateway) setSendReceiveBuffers(conn net.Conn) error {
if g.SendBuf < 1 && g.ReceiveBuf < 1 {
return nil
}
// conn == tls.Tunnel
ptr := reflect.ValueOf(conn)
val := reflect.Indirect(ptr)
if val.Kind() != reflect.Struct {
return errors.New("didn't get a struct from conn")
}
// this gets net.Tunnel -> *net.TCPConn -> net.TCPConn
ptrConn := val.FieldByName("conn")
valConn := reflect.Indirect(ptrConn)
if !valConn.IsValid() {
return errors.New("cannot find conn field")
}
valConn = valConn.Elem().Elem()
// net.FD
ptrNetFd := valConn.FieldByName("fd")
valNetFd := reflect.Indirect(ptrNetFd)
if !valNetFd.IsValid() {
return errors.New("cannot find fd field")
}
// pfd member
ptrPfd := valNetFd.FieldByName("pfd")
valPfd := reflect.Indirect(ptrPfd)
if !valPfd.IsValid() {
return errors.New("cannot find pfd field")
}
// finally the exported Sysfd
ptrSysFd := valPfd.FieldByName("Sysfd")
if !ptrSysFd.IsValid() {
return errors.New("cannot find Sysfd field")
}
fd := int(ptrSysFd.Int())
if g.ReceiveBuf > 0 {
err := syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_RCVBUF, g.ReceiveBuf)
if err != nil {
return wrapSyscallError("setsockopt", err)
}
}
if g.SendBuf > 0 {
err := syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_SNDBUF, g.SendBuf)
if err != nil {
return wrapSyscallError("setsockopt", err)
}
}
return nil
}
func (g *Gateway) handleWebsocketProtocol(ctx context.Context, c *websocket.Conn, t *Tunnel) {
websocketConnections.Inc()
defer websocketConnections.Dec()
inout, _ := transport.NewWS(c)
defer inout.Close()
t.Id = uuid.New().String()
t.transportOut = inout
t.transportIn = inout
t.ConnectedOn = time.Now()
handler := NewProcessor(g, t)
RegisterTunnel(t, handler)
defer RemoveTunnel(t)
handler.Process(ctx)
}
// The legacy protocol (no websockets) uses an RDG_IN_DATA for client -> server
// and RDG_OUT_DATA for server -> client data. The handshakeRequest procedure is a bit different
// to ensure the connections do not get cached or terminated by a proxy prematurely.
func (g *Gateway) handleLegacyProtocol(w http.ResponseWriter, r *http.Request, t *Tunnel) {
log.Printf("Session %s, %t, %t", t.RDGId, t.transportOut != nil, t.transportIn != nil)
id := identity.FromRequestCtx(r)
if r.Method == MethodRDGOUT {
out, err := transport.NewLegacy(w)
if err != nil {
log.Printf("cannot hijack connection to support RDG OUT data channel: %s", err)
return
}
log.Printf("Opening RDGOUT for client %s", id.GetAttribute(identity.AttrClientIp))
t.transportOut = out
out.SendAccept(true)
c.Set(t.RDGId, t, cache.DefaultExpiration)
} else if r.Method == MethodRDGIN {
legacyConnections.Inc()
defer legacyConnections.Dec()
in, err := transport.NewLegacy(w)
if err != nil {
log.Printf("cannot hijack connection to support RDG IN data channel: %s", err)
return
}
defer in.Close()
if t.transportIn == nil {
t.Id = uuid.New().String()
t.transportIn = in
c.Set(t.RDGId, t, cache.DefaultExpiration)
log.Printf("Opening RDGIN for client %s", id.GetAttribute(identity.AttrClientIp))
in.SendAccept(false)
// read some initial data
in.Drain()
log.Printf("Legacy handshakeRequest done for client %s", id.GetAttribute(identity.AttrClientIp))
handler := NewProcessor(g, t)
RegisterTunnel(t, handler)
defer RemoveTunnel(t)
handler.Process(r.Context())
}
}
}

View File

@ -1,32 +0,0 @@
package protocol
import "github.com/prometheus/client_golang/prometheus"
var (
connectionCache = prometheus.NewGauge(
prometheus.GaugeOpts{
Namespace: "rdpgw",
Name: "connection_cache",
Help: "The amount of connections in the cache",
})
websocketConnections = prometheus.NewGauge(
prometheus.GaugeOpts{
Namespace: "rdpgw",
Name: "websocket_connections",
Help: "The count of websocket connections",
})
legacyConnections = prometheus.NewGauge(
prometheus.GaugeOpts{
Namespace: "rdpgw",
Name: "legacy_connections",
Help: "The count of legacy https connections",
})
)
func init() {
prometheus.MustRegister(connectionCache)
prometheus.MustRegister(legacyConnections)
prometheus.MustRegister(websocketConnections)
}

View File

@ -1,376 +0,0 @@
package protocol
import (
"bytes"
"context"
"encoding/binary"
"errors"
"fmt"
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/identity"
"io"
"log"
"net"
"strconv"
"time"
)
type Processor struct {
// gw is the gateway instance on which the connection arrived
// Immutable; never nil.
gw *Gateway
// state is the internal state of the processor
state int
// tunnel is the underlying connection with the client
tunnel *Tunnel
// ctl is a channel to control the processor in case of events
ctl chan int
}
func NewProcessor(gw *Gateway, tunnel *Tunnel) *Processor {
h := &Processor{
gw: gw,
state: SERVER_STATE_INITIALIZED,
tunnel: tunnel,
ctl: make(chan int),
}
return h
}
const tunnelId = 10
func (p *Processor) Process(ctx context.Context) error {
for {
pt, sz, pkt, err := p.tunnel.Read()
if err != nil {
log.Printf("Cannot read message from stream %p", err)
return err
}
switch pt {
case PKT_TYPE_HANDSHAKE_REQUEST:
log.Printf("Client handshakeRequest from %s", p.tunnel.User.GetAttribute(identity.AttrClientIp))
if p.state != SERVER_STATE_INITIALIZED {
log.Printf("Handshake attempted while in wrong state %d != %d", p.state, SERVER_STATE_INITIALIZED)
msg := p.handshakeResponse(0x0, 0x0, 0, E_PROXY_INTERNALERROR)
p.tunnel.Write(msg)
return fmt.Errorf("%x: wrong state", E_PROXY_INTERNALERROR)
}
major, minor, _, reqAuth := p.handshakeRequest(pkt)
caps, err := p.matchAuth(reqAuth)
if err != nil {
log.Println(err)
msg := p.handshakeResponse(0x0, 0x0, 0, E_PROXY_CAPABILITYMISMATCH)
p.tunnel.Write(msg)
return err
}
msg := p.handshakeResponse(major, minor, caps, ERROR_SUCCESS)
p.tunnel.Write(msg)
p.state = SERVER_STATE_HANDSHAKE
case PKT_TYPE_TUNNEL_CREATE:
log.Printf("Tunnel create")
if p.state != SERVER_STATE_HANDSHAKE {
log.Printf("Tunnel create attempted while in wrong state %d != %d",
p.state, SERVER_STATE_HANDSHAKE)
msg := p.tunnelResponse(E_PROXY_INTERNALERROR)
p.tunnel.Write(msg)
return fmt.Errorf("%x: PAA cookie rejected, wrong state", E_PROXY_INTERNALERROR)
}
_, cookie := p.tunnelRequest(pkt)
if p.gw.CheckPAACookie != nil {
if ok, _ := p.gw.CheckPAACookie(ctx, cookie); !ok {
log.Printf("Invalid PAA cookie received from client %s", p.tunnel.User.GetAttribute(identity.AttrClientIp))
msg := p.tunnelResponse(E_PROXY_COOKIE_AUTHENTICATION_ACCESS_DENIED)
p.tunnel.Write(msg)
return fmt.Errorf("%x: invalid PAA cookie", E_PROXY_COOKIE_AUTHENTICATION_ACCESS_DENIED)
}
}
msg := p.tunnelResponse(ERROR_SUCCESS)
p.tunnel.Write(msg)
p.state = SERVER_STATE_TUNNEL_CREATE
case PKT_TYPE_TUNNEL_AUTH:
log.Printf("Tunnel auth")
if p.state != SERVER_STATE_TUNNEL_CREATE {
log.Printf("Tunnel auth attempted while in wrong state %d != %d",
p.state, SERVER_STATE_TUNNEL_CREATE)
msg := p.tunnelAuthResponse(E_PROXY_INTERNALERROR)
p.tunnel.Write(msg)
return fmt.Errorf("%x: Tunnel auth rejected, wrong state", E_PROXY_INTERNALERROR)
}
client := p.tunnelAuthRequest(pkt)
if p.gw.CheckClientName != nil {
if ok, _ := p.gw.CheckClientName(ctx, client); !ok {
log.Printf("Invalid client name: %s", client)
msg := p.tunnelAuthResponse(ERROR_ACCESS_DENIED)
p.tunnel.Write(msg)
return fmt.Errorf("%x: Tunnel auth rejected, invalid client name", ERROR_ACCESS_DENIED)
}
}
msg := p.tunnelAuthResponse(ERROR_SUCCESS)
p.tunnel.Write(msg)
p.state = SERVER_STATE_TUNNEL_AUTHORIZE
case PKT_TYPE_CHANNEL_CREATE:
log.Printf("Channel create")
if p.state != SERVER_STATE_TUNNEL_AUTHORIZE {
log.Printf("Channel create attempted while in wrong state %d != %d",
p.state, SERVER_STATE_TUNNEL_AUTHORIZE)
msg := p.channelResponse(E_PROXY_INTERNALERROR)
p.tunnel.Write(msg)
return fmt.Errorf("%x: Channel create rejected, wrong state", E_PROXY_INTERNALERROR)
}
server, port := p.channelRequest(pkt)
host := net.JoinHostPort(server, strconv.Itoa(int(port)))
if p.gw.CheckHost != nil {
log.Printf("Verifying %s host connection", host)
if ok, _ := p.gw.CheckHost(ctx, host); !ok {
log.Printf("Not allowed to connect to %s by policy handler", host)
msg := p.channelResponse(E_PROXY_RAP_ACCESSDENIED)
p.tunnel.Write(msg)
return fmt.Errorf("%x: denied by security policy", E_PROXY_RAP_ACCESSDENIED)
}
}
log.Printf("Establishing connection to RDP server: %s", host)
p.tunnel.rwc, err = net.DialTimeout("tcp", host, time.Second*15)
if err != nil {
log.Printf("Error connecting to %s, %s", host, err)
msg := p.channelResponse(E_PROXY_INTERNALERROR)
p.tunnel.Write(msg)
return err
}
p.tunnel.TargetServer = host
log.Printf("Connection established")
msg := p.channelResponse(ERROR_SUCCESS)
p.tunnel.Write(msg)
// Make sure to start the flow from the RDP server first otherwise connections
// might hang eventually
go forward(p.tunnel.rwc, p.tunnel)
p.state = SERVER_STATE_CHANNEL_CREATE
case PKT_TYPE_DATA:
if p.state < SERVER_STATE_CHANNEL_CREATE {
log.Printf("Data received while in wrong state %d != %d", p.state, SERVER_STATE_CHANNEL_CREATE)
return errors.New("wrong state")
}
p.state = SERVER_STATE_OPENED
receive(pkt, p.tunnel.rwc)
case PKT_TYPE_KEEPALIVE:
// keepalives can be received while the channel is not open yet
if p.state < SERVER_STATE_CHANNEL_CREATE {
log.Printf("Keepalive received while in wrong state %d != %d", p.state, SERVER_STATE_CHANNEL_CREATE)
return errors.New("wrong state")
}
// avoid concurrency issues
// p.transportIn.Write(createPacket(PKT_TYPE_KEEPALIVE, []byte{}))
case PKT_TYPE_CLOSE_CHANNEL:
log.Printf("Close channel")
if p.state != SERVER_STATE_OPENED {
log.Printf("Channel closed while in wrong state %d != %d", p.state, SERVER_STATE_OPENED)
return errors.New("wrong state")
}
msg := p.channelCloseResponse(ERROR_SUCCESS)
p.tunnel.Write(msg)
p.state = SERVER_STATE_CLOSED
return nil
default:
log.Printf("Unknown packet (size %d): %x", sz, pkt)
}
}
}
// Creates a packet and is a response to a handshakeRequest request
// HTTP_EXTENDED_AUTH_SSPI_NTLM is not supported in Linux
// but could be in Windows. However, the NTLM protocol is insecure
func (p *Processor) handshakeResponse(major byte, minor byte, caps uint16, errorCode int) []byte {
buf := new(bytes.Buffer)
binary.Write(buf, binary.LittleEndian, uint32(errorCode)) // error_code
buf.Write([]byte{major, minor})
binary.Write(buf, binary.LittleEndian, uint16(0)) // server version
binary.Write(buf, binary.LittleEndian, uint16(caps)) // extended auth
return createPacket(PKT_TYPE_HANDSHAKE_RESPONSE, buf.Bytes())
}
func (p *Processor) handshakeRequest(data []byte) (major byte, minor byte, version uint16, extAuth uint16) {
r := bytes.NewReader(data)
binary.Read(r, binary.LittleEndian, &major)
binary.Read(r, binary.LittleEndian, &minor)
binary.Read(r, binary.LittleEndian, &version)
binary.Read(r, binary.LittleEndian, &extAuth)
log.Printf("major: %d, minor: %d, version: %d, ext auth: %d", major, minor, version, extAuth)
return
}
func (p *Processor) matchAuth(clientAuthCaps uint16) (caps uint16, err error) {
if p.gw.SmartCardAuth {
caps = caps | HTTP_EXTENDED_AUTH_SC
}
if p.gw.TokenAuth {
caps = caps | HTTP_EXTENDED_AUTH_PAA
}
if caps&clientAuthCaps == 0 && clientAuthCaps > 0 {
return 0, fmt.Errorf("%x has no matching capability configured (%x). Did you configure caps? ", clientAuthCaps, caps)
}
if caps > 0 && clientAuthCaps == 0 {
return 0, fmt.Errorf("%d caps are required by the server, but the client does not support them", caps)
}
return caps, nil
}
func (p *Processor) tunnelRequest(data []byte) (caps uint32, cookie string) {
var fields uint16
r := bytes.NewReader(data)
binary.Read(r, binary.LittleEndian, &caps)
binary.Read(r, binary.LittleEndian, &fields)
r.Seek(2, io.SeekCurrent)
if fields == HTTP_TUNNEL_PACKET_FIELD_PAA_COOKIE {
var size uint16
binary.Read(r, binary.LittleEndian, &size)
cookieB := make([]byte, size)
r.Read(cookieB)
cookie, _ = DecodeUTF16(cookieB)
}
return
}
func (p *Processor) tunnelResponse(errorCode int) []byte {
buf := new(bytes.Buffer)
binary.Write(buf, binary.LittleEndian, uint16(0)) // server version
binary.Write(buf, binary.LittleEndian, uint32(errorCode)) // error code
binary.Write(buf, binary.LittleEndian, uint16(HTTP_TUNNEL_RESPONSE_FIELD_TUNNEL_ID|HTTP_TUNNEL_RESPONSE_FIELD_CAPS)) // fields present
binary.Write(buf, binary.LittleEndian, uint16(0)) // reserved
// tunnel id (when is it used?)
binary.Write(buf, binary.LittleEndian, uint32(tunnelId))
binary.Write(buf, binary.LittleEndian, uint32(HTTP_CAPABILITY_IDLE_TIMEOUT))
return createPacket(PKT_TYPE_TUNNEL_RESPONSE, buf.Bytes())
}
func (p *Processor) tunnelAuthRequest(data []byte) string {
buf := bytes.NewReader(data)
var size uint16
binary.Read(buf, binary.LittleEndian, &size)
clData := make([]byte, size)
binary.Read(buf, binary.LittleEndian, &clData)
clientName, _ := DecodeUTF16(clData)
return clientName
}
func (p *Processor) tunnelAuthResponse(errorCode int) []byte {
buf := new(bytes.Buffer)
binary.Write(buf, binary.LittleEndian, uint32(errorCode)) // error code
binary.Write(buf, binary.LittleEndian, uint16(HTTP_TUNNEL_AUTH_RESPONSE_FIELD_REDIR_FLAGS|HTTP_TUNNEL_AUTH_RESPONSE_FIELD_IDLE_TIMEOUT)) // fields present
binary.Write(buf, binary.LittleEndian, uint16(0)) // reserved
// idle timeout
if p.gw.IdleTimeout < 0 {
p.gw.IdleTimeout = 0
}
binary.Write(buf, binary.LittleEndian, uint32(makeRedirectFlags(p.gw.RedirectFlags))) // redir flags
binary.Write(buf, binary.LittleEndian, uint32(p.gw.IdleTimeout)) // timeout in minutes
return createPacket(PKT_TYPE_TUNNEL_AUTH_RESPONSE, buf.Bytes())
}
func (p *Processor) channelRequest(data []byte) (server string, port uint16) {
buf := bytes.NewReader(data)
var resourcesSize byte
var alternative byte
var protocol uint16
var nameSize uint16
binary.Read(buf, binary.LittleEndian, &resourcesSize)
binary.Read(buf, binary.LittleEndian, &alternative)
binary.Read(buf, binary.LittleEndian, &port)
binary.Read(buf, binary.LittleEndian, &protocol)
binary.Read(buf, binary.LittleEndian, &nameSize)
nameData := make([]byte, nameSize)
binary.Read(buf, binary.LittleEndian, &nameData)
server, _ = DecodeUTF16(nameData)
return
}
func (p *Processor) channelResponse(errorCode int) []byte {
buf := new(bytes.Buffer)
binary.Write(buf, binary.LittleEndian, uint32(errorCode)) // error code
binary.Write(buf, binary.LittleEndian, uint16(HTTP_CHANNEL_RESPONSE_FIELD_CHANNELID)) // fields present
binary.Write(buf, binary.LittleEndian, uint16(0)) // reserved
// channel id is required for Windows clients
binary.Write(buf, binary.LittleEndian, uint32(1)) // channel id
// optional fields
// channel id uint32 (4)
// udp port uint16 (2)
// udp auth cookie 1 byte for side channel
// length uint16
return createPacket(PKT_TYPE_CHANNEL_RESPONSE, buf.Bytes())
}
func (p *Processor) channelCloseResponse(errorCode int) []byte {
buf := new(bytes.Buffer)
binary.Write(buf, binary.LittleEndian, uint32(errorCode)) // error code
binary.Write(buf, binary.LittleEndian, uint16(HTTP_CHANNEL_RESPONSE_FIELD_CHANNELID)) // fields present
binary.Write(buf, binary.LittleEndian, uint16(0)) // reserved
// channel id is required for Windows clients
binary.Write(buf, binary.LittleEndian, uint32(1)) // channel id
// optional fields
// channel id uint32 (4)
// udp port uint16 (2)
// udp auth cookie 1 byte for side channel
// length uint16
return createPacket(PKT_TYPE_CLOSE_CHANNEL_RESPONSE, buf.Bytes())
}
func makeRedirectFlags(flags RedirectFlags) int {
var redir = 0
if flags.DisableAll {
return HTTP_TUNNEL_REDIR_DISABLE_ALL
}
if flags.EnableAll {
return HTTP_TUNNEL_REDIR_ENABLE_ALL
}
if !flags.Port {
redir = redir | HTTP_TUNNEL_REDIR_DISABLE_PORT
}
if !flags.Clipboard {
redir = redir | HTTP_TUNNEL_REDIR_DISABLE_CLIPBOARD
}
if !flags.Drive {
redir = redir | HTTP_TUNNEL_REDIR_DISABLE_DRIVE
}
if !flags.Pnp {
redir = redir | HTTP_TUNNEL_REDIR_DISABLE_PNP
}
if !flags.Printer {
redir = redir | HTTP_TUNNEL_REDIR_DISABLE_PRINTER
}
return redir
}

View File

@ -1,101 +0,0 @@
package protocol
import (
"fmt"
"time"
)
var Connections map[string]*Monitor
type Monitor struct {
Processor *Processor
Tunnel *Tunnel
}
const (
ctlDisconnect = -1
)
func RegisterTunnel(t *Tunnel, p *Processor) {
if Connections == nil {
Connections = make(map[string]*Monitor)
}
Connections[t.Id] = &Monitor{
Processor: p,
Tunnel: t,
}
}
func RemoveTunnel(t *Tunnel) {
delete(Connections, t.Id)
}
func Disconnect(id string) error {
if Connections == nil {
return fmt.Errorf("%s connection does not exist", id)
}
if m, ok := Connections[id]; !ok {
m.Processor.ctl <- ctlDisconnect
return nil
}
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.
/*
func CalculateSpeedPerSecond(connId string) (in int, out int) {
now := time.Now().UnixMilli()
c := Connections[connId]
total := int64(0)
for _, v := range c.Tunnel.BytesReceived {
total += v
}
in = int(total / (now - c.TimeStamp) * 1000)
total = int64(0)
for _, v := range c.BytesSent {
total += v
}
out = int(total / (now - c.TimeStamp))
return in, out
}
*/

View File

@ -1,64 +0,0 @@
package protocol
import (
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/identity"
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/transport"
"net"
"time"
)
const (
CtxTunnel = "github.com/bolkedebruin/rdpgw/tunnel"
)
type Tunnel struct {
// Id identifies the connection in the server
Id string
// The connection-id (RDG-ConnID) as reported by the client
RDGId string
// The underlying incoming transport being either websocket or legacy http
// in case of websocket transportOut will equal transportIn
transportIn transport.Transport
// The underlying outgoing transport being either websocket or legacy http
// in case of websocket transportOut will equal transportOut
transportOut transport.Transport
// The remote desktop server (rdp, vnc etc) the clients intends to connect to
TargetServer string
// The obtained client ip address
RemoteAddr string
// User
User identity.Identity
// rwc is the underlying connection to the remote desktop server.
// It is of the type *net.TCPConn
rwc net.Conn
// BytesSent is the total amount of bytes sent by the server to the client minus tunnel overhead
BytesSent int64
// BytesReceived is the total amount of bytes received by the server from the client minus tunnel overhad
BytesReceived int64
// ConnectedOn is when the client connected to the server
ConnectedOn time.Time
// LastSeen is when the server received the last packet from the client
LastSeen time.Time
}
// Write puts the packet on the transport and updates the statistics for bytes sent
func (t *Tunnel) Write(pkt []byte) {
n, _ := t.transportOut.WritePacket(pkt)
t.BytesSent += int64(n)
}
// Read picks up a packet from the transport and returns the packet type
// packet, with the header removed, and the packet size. It updates the
// statistics for bytes received
func (t *Tunnel) Read() (pt int, size int, pkt []byte, err error) {
pt, size, pkt, err = readMessage(t.transportIn)
t.BytesReceived += int64(size)
t.LastSeen = time.Now()
return pt, size, pkt, err
}

View File

@ -1,85 +0,0 @@
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

@ -1,85 +0,0 @@
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)
})
}
}

View File

@ -1,253 +0,0 @@
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")
}
}

View File

@ -1,47 +0,0 @@
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

@ -1,37 +0,0 @@
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

@ -1,40 +0,0 @@
package security
import (
"context"
"errors"
"fmt"
"log"
"strings"
)
var (
Hosts []string
HostSelection string
)
func CheckHost(ctx context.Context, host string) (bool, error) {
switch HostSelection {
case "any":
return true, nil
case "signed":
// todo get from context?
return false, errors.New("cannot verify host in 'signed' mode as token data is missing")
case "roundrobin", "unsigned":
s := getTunnel(ctx)
if s.User.UserName() == "" {
return false, errors.New("no valid session info or username found in context")
}
log.Printf("Checking host for user %s", s.User.UserName())
for _, h := range Hosts {
h = strings.Replace(h, "{{ preferred_username }}", s.User.UserName(), 1)
if h == host {
return true, nil
}
}
return false, fmt.Errorf("invalid host %s", host)
}
return false, errors.New("unrecognized host selection criteria")
}

View File

@ -1,50 +0,0 @@
package security
import (
"context"
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/identity"
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/protocol"
"testing"
)
var (
info = protocol.Tunnel{
RDGId: "myid",
TargetServer: "my.remote.server",
RemoteAddr: "10.0.0.1",
}
hosts = []string{"localhost:3389", "my-{{ preferred_username }}-host:3389"}
)
func TestCheckHost(t *testing.T) {
info.User = identity.NewUser()
info.User.SetUserName("MYNAME")
ctx := context.WithValue(context.Background(), protocol.CtxTunnel, &info)
Hosts = hosts
// check any
HostSelection = "any"
host := "try.my.server:3389"
if ok, err := CheckHost(ctx, host); !ok || err != nil {
t.Fatalf("%s should be allowed with host selection %s (err: %s)", host, HostSelection, err)
}
HostSelection = "signed"
if ok, err := CheckHost(ctx, host); ok || err == nil {
t.Fatalf("signed host selection isnt supported at the moment")
}
HostSelection = "roundrobin"
if ok, err := CheckHost(ctx, host); ok {
t.Fatalf("%s should NOT be allowed with host selection %s (err: %s)", host, HostSelection, err)
}
host = "my-MYNAME-host:3389"
if ok, err := CheckHost(ctx, host); !ok {
t.Fatalf("%s should be allowed with host selection %s (err: %s)", host, HostSelection, err)
}
}

View File

@ -1,294 +0,0 @@
package security
import (
"context"
"errors"
"fmt"
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/identity"
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/protocol"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/go-jose/go-jose/v4"
"github.com/go-jose/go-jose/v4/jwt"
"golang.org/x/oauth2"
"log"
"time"
)
var (
SigningKey []byte
EncryptionKey []byte
UserSigningKey []byte
UserEncryptionKey []byte
QuerySigningKey []byte
OIDCProvider *oidc.Provider
Oauth2Config oauth2.Config
)
var ExpiryTime time.Duration = 5
var VerifyClientIP bool = true
type customClaims struct {
RemoteServer string `json:"remoteServer"`
ClientIP string `json:"clientIp"`
AccessToken string `json:"accessToken"`
}
func CheckSession(next protocol.CheckHostFunc) protocol.CheckHostFunc {
return func(ctx context.Context, host string) (bool, error) {
tunnel := getTunnel(ctx)
if tunnel == nil {
return false, errors.New("no valid session info found in context")
}
if tunnel.TargetServer != host {
log.Printf("Client specified host %s does not match token host %s", host, tunnel.TargetServer)
return false, nil
}
// use identity from context rather then set by tunnel
id := identity.FromCtx(ctx)
if VerifyClientIP && tunnel.RemoteAddr != id.GetAttribute(identity.AttrClientIp) {
log.Printf("Current client ip address %s does not match token client ip %s",
id.GetAttribute(identity.AttrClientIp), tunnel.RemoteAddr)
return false, nil
}
return next(ctx, host)
}
}
func CheckPAACookie(ctx context.Context, tokenString string) (bool, error) {
if tokenString == "" {
log.Printf("no token to parse")
return false, errors.New("no token to parse")
}
token, err := jwt.ParseSigned(tokenString, []jose.SignatureAlgorithm{jose.HS256})
if err != nil {
log.Printf("cannot parse token due to: %t", err)
return false, err
}
// check if the signing algo matches what we expect
for _, header := range token.Headers {
if header.Algorithm != string(jose.HS256) {
return false, fmt.Errorf("unexpected signing method: %v", header.Algorithm)
}
}
standard := jwt.Claims{}
custom := customClaims{}
// Claims automagically checks the signature...
err = token.Claims(SigningKey, &standard, &custom)
if err != nil {
log.Printf("token signature validation failed due to %tunnel", err)
return false, err
}
// ...but doesn't check the expiry claim :/
err = standard.Validate(jwt.Expected{
Issuer: "rdpgw",
Time: time.Now(),
})
if err != nil {
log.Printf("token validation failed due to %tunnel", err)
return false, err
}
// validate the access token
tokenSource := Oauth2Config.TokenSource(ctx, &oauth2.Token{AccessToken: custom.AccessToken})
user, err := OIDCProvider.UserInfo(ctx, tokenSource)
if err != nil {
log.Printf("Cannot get user info for access token: %tunnel", err)
return false, err
}
tunnel := getTunnel(ctx)
tunnel.TargetServer = custom.RemoteServer
tunnel.RemoteAddr = custom.ClientIP
tunnel.User.SetUserName(user.Subject)
return true, nil
}
func GeneratePAAToken(ctx context.Context, username string, server string) (string, error) {
if len(SigningKey) < 32 {
return "", errors.New("token signing key not long enough or not specified")
}
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.HS256, Key: SigningKey}, nil)
if err != nil {
log.Printf("Cannot obtain signer %s", err)
return "", err
}
standard := jwt.Claims{
Issuer: "rdpgw",
Expiry: jwt.NewNumericDate(time.Now().Add(time.Minute * 5)),
Subject: username,
}
id := identity.FromCtx(ctx)
private := customClaims{
RemoteServer: server,
ClientIP: id.GetAttribute(identity.AttrClientIp).(string),
AccessToken: id.GetAttribute(identity.AttrAccessToken).(string),
}
if token, err := jwt.Signed(sig).Claims(standard).Claims(private).Serialize(); err != nil {
log.Printf("Cannot sign PAA token %s", err)
return "", err
} else {
return token, nil
}
}
func GenerateUserToken(ctx context.Context, userName string) (string, error) {
if len(UserEncryptionKey) < 32 {
return "", errors.New("user token encryption key not long enough or not specified")
}
claims := jwt.Claims{
Subject: userName,
Expiry: jwt.NewNumericDate(time.Now().Add(time.Minute * 5)),
Issuer: "rdpgw",
}
enc, err := jose.NewEncrypter(
jose.A128CBC_HS256,
jose.Recipient{
Algorithm: jose.DIRECT,
Key: UserEncryptionKey,
},
(&jose.EncrypterOptions{Compression: jose.DEFLATE}).WithContentType("JWT"),
)
if err != nil {
log.Printf("Cannot encrypt user token due to %s", err)
return "", err
}
// this makes the token bigger and we deal with a limited space of 511 characters
if len(UserSigningKey) > 0 {
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.HS256, Key: UserSigningKey}, nil)
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
}
func UserInfo(ctx context.Context, token string) (jwt.Claims, error) {
standard := jwt.Claims{}
if len(UserEncryptionKey) > 0 && len(UserSigningKey) > 0 {
enc, err := jwt.ParseSignedAndEncrypted(
token,
[]jose.KeyAlgorithm{jose.DIRECT},
[]jose.ContentEncryption{jose.A128CBC_HS256},
[]jose.SignatureAlgorithm{jose.HS256},
)
if err != nil {
log.Printf("Cannot get token %s", err)
return standard, errors.New("cannot get token")
}
token, err := enc.Decrypt(UserEncryptionKey)
if err != nil {
log.Printf("Cannot decrypt token %s", err)
return standard, errors.New("cannot decrypt token")
}
if err = token.Claims(UserSigningKey, &standard); err != nil {
log.Printf("cannot verify signature %s", err)
return standard, errors.New("cannot verify signature")
}
} else if len(UserSigningKey) == 0 {
token, err := jwt.ParseEncrypted(token, []jose.KeyAlgorithm{jose.DIRECT}, []jose.ContentEncryption{jose.A128CBC_HS256})
if err != nil {
log.Printf("Cannot get token %s", err)
return standard, errors.New("cannot get token")
}
err = token.Claims(UserEncryptionKey, &standard)
if err != nil {
log.Printf("Cannot decrypt token %s", err)
return standard, errors.New("cannot decrypt token")
}
}
// go-jose doesnt verify the expiry
err := standard.Validate(jwt.Expected{
Issuer: "rdpgw",
Time: time.Now(),
})
if err != nil {
log.Printf("token validation failed due to %s", err)
return standard, fmt.Errorf("token validation failed due to %s", err)
}
return standard, nil
}
func QueryInfo(ctx context.Context, tokenString string, issuer string) (string, error) {
standard := jwt.Claims{}
token, err := jwt.ParseSigned(tokenString, []jose.SignatureAlgorithm{jose.HS256})
if err != nil {
log.Printf("Cannot get token %s", err)
return "", errors.New("cannot get token")
}
err = token.Claims(QuerySigningKey, &standard)
if err = token.Claims(QuerySigningKey, &standard); err != nil {
log.Printf("cannot verify signature %s", err)
return "", errors.New("cannot verify signature")
}
// go-jose doesnt verify the expiry
err = standard.Validate(jwt.Expected{
Issuer: issuer,
Time: time.Now(),
})
if err != nil {
log.Printf("token validation failed due to %s", err)
return "", fmt.Errorf("token validation failed due to %s", err)
}
return standard.Subject, nil
}
// GenerateQueryToken this is a helper function for testing
func GenerateQueryToken(ctx context.Context, query string, issuer string) (string, error) {
if len(QuerySigningKey) < 32 {
return "", errors.New("query token encryption key not long enough or not specified")
}
claims := jwt.Claims{
Subject: query,
Expiry: jwt.NewNumericDate(time.Now().Add(time.Minute * 5)),
Issuer: issuer,
}
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.HS256, Key: QuerySigningKey},
(&jose.SignerOptions{}).WithBase64(true))
if err != nil {
log.Printf("Cannot encrypt user token due to %s", err)
return "", err
}
token, err := jwt.Signed(sig).Claims(claims).Serialize()
return token, err
}
func getTunnel(ctx context.Context) *protocol.Tunnel {
s, ok := ctx.Value(protocol.CtxTunnel).(*protocol.Tunnel)
if !ok {
log.Printf("cannot get session info from context")
return nil
}
return s
}

View File

@ -1,76 +0,0 @@
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

@ -1,39 +0,0 @@
package security
import (
"crypto/rand"
"math/big"
)
// GenerateRandomBytes returns securely generated random bytes.
// It will return an error if the system's secure random
// number generator fails to function correctly, in which
// case the caller should not continue.
func GenerateRandomBytes(n int) ([]byte, error) {
b := make([]byte, n)
_, err := rand.Read(b)
// Note that err == nil only if we read len(b) bytes.
if err != nil {
return nil, err
}
return b, nil
}
// GenerateRandomString returns a securely generated random string.
// It will return an error if the system's secure random
// number generator fails to function correctly, in which
// case the caller should not continue.
func GenerateRandomString(n int) (string, error) {
const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-"
ret := make([]byte, n)
for i := 0; i < n; i++ {
num, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
if err != nil {
return "", err
}
ret[i] = letters[num.Int64()]
}
return string(ret), nil
}

View File

@ -1,82 +0,0 @@
package web
import (
"context"
"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"
)
const (
protocolGrpc = "unix"
)
type BasicAuthHandler struct {
SocketAddress string
Timeout int
}
func (h *BasicAuthHandler) BasicAuth(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
username, password, ok := r.BasicAuth()
if ok {
authenticated := h.authenticate(w, r, username, password)
if !authenticated {
log.Printf("User %s is not authenticated for this service", username)
} else {
log.Printf("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))
return
}
}
// If the Authentication header is not present, is invalid, or the
// username or password is wrong, then set a WWW-Authenticate
// header to inform the client that we expect them to use basic
// authentication and send a 401 Unauthorized response.
w.Header().Add("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
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
}

View File

@ -1,68 +0,0 @@
package web
import (
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/identity"
"github.com/jcmturner/goidentity/v6"
"log"
"net"
"net/http"
"strings"
)
func EnrichContext(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id, err := GetSessionIdentity(r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if id == nil {
id = identity.NewUser()
if err := SaveSessionIdentity(r, w, id); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
log.Printf("Identity SessionId: %s, UserName: %s: Authenticated: %t",
id.SessionId(), id.UserName(), id.Authenticated())
h := r.Header.Get("X-Forwarded-For")
if h != "" {
var proxies []string
ips := strings.Split(h, ",")
for i := range ips {
ips[i] = strings.TrimSpace(ips[i])
}
clientIp := ips[0]
if len(ips) > 1 {
proxies = ips[1:]
}
id.SetAttribute(identity.AttrClientIp, clientIp)
id.SetAttribute(identity.AttrProxies, proxies)
}
id.SetAttribute(identity.AttrRemoteAddr, r.RemoteAddr)
if h == "" {
clientIp, _, _ := net.SplitHostPort(r.RemoteAddr)
id.SetAttribute(identity.AttrClientIp, clientIp)
}
next.ServeHTTP(w, identity.AddToRequestCtx(id, r))
})
}
func TransposeSPNEGOContext(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gid := goidentity.FromHTTPRequestContext(r)
if gid != nil {
id := identity.FromRequestCtx(r)
id.SetUserName(gid.UserName())
id.SetAuthenticated(gid.Authenticated())
id.SetDomain(gid.Domain())
id.SetAuthTime(gid.AuthTime())
r = identity.AddToRequestCtx(id, r)
}
next.ServeHTTP(w, r)
})
}

View File

@ -1,29 +0,0 @@
package web
import (
"github.com/gorilla/mux"
"net/http"
)
type AuthMux struct {
headers []string
}
func NewAuthMux() *AuthMux {
return &AuthMux{}
}
func (a *AuthMux) Register(s string) {
a.headers = append(a.headers, s)
}
func (a *AuthMux) SetAuthenticate(w http.ResponseWriter, r *http.Request) {
for _, s := range a.headers {
w.Header().Add("WWW-Authenticate", s)
}
http.Error(w, "Unauthorized", http.StatusUnauthorized)
}
func NoAuthz(r *http.Request, rm *mux.RouteMatch) bool {
return r.Header.Get("Authorization") == ""
}

View File

@ -1,120 +0,0 @@
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,133 +0,0 @@
package web
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/identity"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/patrickmn/go-cache"
"golang.org/x/oauth2"
"net/http"
"time"
)
const (
CacheExpiration = time.Minute * 2
CleanupInterval = time.Minute * 5
)
type OIDC struct {
oAuth2Config *oauth2.Config
oidcTokenVerifier *oidc.IDTokenVerifier
stateStore *cache.Cache
}
type OIDCConfig struct {
OAuth2Config *oauth2.Config
OIDCTokenVerifier *oidc.IDTokenVerifier
}
func (c *OIDCConfig) New() *OIDC {
return &OIDC{
oAuth2Config: c.OAuth2Config,
oidcTokenVerifier: c.OIDCTokenVerifier,
stateStore: cache.New(CacheExpiration, CleanupInterval),
}
}
func (h *OIDC) HandleCallback(w http.ResponseWriter, r *http.Request) {
state := r.URL.Query().Get("state")
s, found := h.stateStore.Get(state)
if !found {
http.Error(w, "unknown state", http.StatusBadRequest)
return
}
url := s.(string)
ctx := r.Context()
oauth2Token, err := h.oAuth2Config.Exchange(ctx, r.URL.Query().Get("code"))
if err != nil {
http.Error(w, "Failed to exchange token: "+err.Error(), http.StatusInternalServerError)
return
}
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
if !ok {
http.Error(w, "No id_token field in oauth2 token.", http.StatusInternalServerError)
return
}
idToken, err := h.oidcTokenVerifier.Verify(ctx, rawIDToken)
if err != nil {
http.Error(w, "Failed to verify ID Token: "+err.Error(), http.StatusInternalServerError)
return
}
resp := struct {
OAuth2Token *oauth2.Token
IDTokenClaims *json.RawMessage // ID Token payload is just JSON.
}{oauth2Token, new(json.RawMessage)}
if err := idToken.Claims(&resp.IDTokenClaims); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var data map[string]interface{}
if err := json.Unmarshal(*resp.IDTokenClaims, &data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
id := identity.FromRequestCtx(r)
userName := findUsernameInClaims(data)
if userName == "" {
http.Error(w, "no oidc claim for username found", http.StatusInternalServerError)
}
id.SetUserName(userName)
id.SetAuthenticated(true)
id.SetAuthTime(time.Now())
id.SetAttribute(identity.AttrAccessToken, oauth2Token.AccessToken)
if err = SaveSessionIdentity(r, w, id); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
http.Redirect(w, r, url, http.StatusFound)
}
func findUsernameInClaims(data map[string]interface{}) string {
candidates := []string{"preferred_username", "unique_name", "upn", "username"}
for _, claim := range candidates {
userName, found := data[claim].(string)
if found {
return userName
}
}
return ""
}
func (h *OIDC) Authenticated(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := identity.FromRequestCtx(r)
if !id.Authenticated() {
seed := make([]byte, 16)
_, err := rand.Read(seed)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
state := hex.EncodeToString(seed)
h.stateStore.Set(state, r.RequestURI, cache.DefaultExpiration)
http.Redirect(w, r, h.oAuth2Config.AuthCodeURL(state), http.StatusFound)
return
}
// replace the identity with the one from the sessions
next.ServeHTTP(w, r)
})
}

View File

@ -1,49 +0,0 @@
package web
import "testing"
func TestFindUserNameInClaims(t *testing.T) {
cases := []struct {
data map[string]interface{}
ret string
name string
}{
{
data: map[string]interface{}{
"preferred_username": "exists",
},
ret: "exists",
name: "preferred_username",
},
{
data: map[string]interface{}{
"upn": "exists",
},
ret: "exists",
name: "upn",
},
{
data: map[string]interface{}{
"unique_name": "exists",
},
ret: "exists",
name: "unique_name",
},
{
data: map[string]interface{}{
"fail": "exists",
},
ret: "",
name: "fail",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
s := findUsernameInClaims(tc.data)
if s != tc.ret {
t.Fatalf("expected return: %v, got: %v", tc.ret, s)
}
})
}
}

View File

@ -1,85 +0,0 @@
package web
import (
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/identity"
"github.com/gorilla/sessions"
"log"
"net/http"
"os"
)
const (
rdpGwSession = "RDPGWSESSION"
MaxAge = 120
identityKey = "RDPGWID"
maxSessionLength = 8192
)
var sessionStore sessions.Store
func InitStore(sessionKey []byte, encryptionKey []byte, storeType string, maxLength int) {
if len(sessionKey) < 32 {
log.Fatal("Session key too small")
}
if len(encryptionKey) < 32 {
log.Fatal("Session key too small")
}
if storeType == "file" {
log.Println("Filesystem is used as session storage")
fs := sessions.NewFilesystemStore(os.TempDir(), sessionKey, encryptionKey)
// set max length
if maxLength == 0 {
maxLength = maxSessionLength
}
log.Printf("Setting maximum session storage to %d bytes", maxLength)
fs.MaxLength(maxLength)
sessionStore = fs
} else {
log.Println("Cookies are used as session storage")
sessionStore = sessions.NewCookieStore(sessionKey, encryptionKey)
}
}
func GetSession(r *http.Request) (*sessions.Session, error) {
session, err := sessionStore.Get(r, rdpGwSession)
if err != nil {
return nil, err
}
return session, nil
}
func GetSessionIdentity(r *http.Request) (identity.Identity, error) {
s, err := GetSession(r)
if err != nil {
return nil, err
}
idData := s.Values[identityKey]
if idData == nil {
return nil, nil
}
id := identity.NewUser()
id.Unmarshal(idData.([]byte))
return id, nil
}
func SaveSessionIdentity(r *http.Request, w http.ResponseWriter, id identity.Identity) error {
session, err := GetSession(r)
if err != nil {
return err
}
session.Options.MaxAge = MaxAge
idData, err := id.Marshal()
if err != nil {
return err
}
session.Values[identityKey] = idData
return sessionStore.Save(r, w, session)
}

View File

@ -1,228 +0,0 @@
package web
import (
"context"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/identity"
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/rdp"
"hash/maphash"
"log"
rnd "math/rand"
"net/http"
"net/url"
"strings"
"time"
)
type TokenGeneratorFunc func(context.Context, string, string) (string, error)
type UserTokenGeneratorFunc func(context.Context, string) (string, error)
type QueryInfoFunc func(context.Context, string, string) (string, error)
type Config struct {
PAATokenGenerator TokenGeneratorFunc
UserTokenGenerator UserTokenGeneratorFunc
QueryInfo QueryInfoFunc
QueryTokenIssuer string
EnableUserToken bool
Hosts []string
HostSelection string
GatewayAddress *url.URL
RdpOpts RdpOpts
TemplateFile string
}
type RdpOpts struct {
UsernameTemplate string
SplitUserDomain bool
NoUsername bool
}
type Handler struct {
paaTokenGenerator TokenGeneratorFunc
enableUserToken bool
userTokenGenerator UserTokenGeneratorFunc
queryInfo QueryInfoFunc
queryTokenIssuer string
gatewayAddress *url.URL
hosts []string
hostSelection string
rdpOpts RdpOpts
rdpDefaults string
}
func (c *Config) NewHandler() *Handler {
if len(c.Hosts) < 1 {
log.Fatal("Not enough hosts to connect to specified")
}
return &Handler{
paaTokenGenerator: c.PAATokenGenerator,
enableUserToken: c.EnableUserToken,
userTokenGenerator: c.UserTokenGenerator,
queryInfo: c.QueryInfo,
queryTokenIssuer: c.QueryTokenIssuer,
gatewayAddress: c.GatewayAddress,
hosts: c.Hosts,
hostSelection: c.HostSelection,
rdpOpts: c.RdpOpts,
rdpDefaults: c.TemplateFile,
}
}
func (h *Handler) selectRandomHost() string {
r := rnd.New(rnd.NewSource(int64(new(maphash.Hash).Sum64())))
host := h.hosts[r.Intn(len(h.hosts))]
return host
}
func (h *Handler) getHost(ctx context.Context, u *url.URL) (string, error) {
switch h.hostSelection {
case "roundrobin":
return h.selectRandomHost(), nil
case "signed":
hosts, ok := u.Query()["host"]
if !ok {
return "", errors.New("invalid query parameter")
}
host, err := h.queryInfo(ctx, hosts[0], h.queryTokenIssuer)
if err != nil {
return "", err
}
found := false
for _, check := range h.hosts {
if check == host {
found = true
break
}
}
if !found {
log.Printf("Invalid host %s specified in token", hosts[0])
return "", errors.New("invalid host specified in query token")
}
return host, nil
case "unsigned":
hosts, ok := u.Query()["host"]
if !ok {
return "", errors.New("invalid query parameter")
}
for _, check := range h.hosts {
if check == hosts[0] {
return hosts[0], nil
}
}
// not found
log.Printf("Invalid host %s specified in client request", hosts[0])
return "", errors.New("invalid host specified in query parameter")
case "any":
hosts, ok := u.Query()["host"]
if !ok {
return "", errors.New("invalid query parameter")
}
return hosts[0], nil
default:
return h.selectRandomHost(), nil
}
}
func (h *Handler) HandleDownload(w http.ResponseWriter, r *http.Request) {
id := identity.FromRequestCtx(r)
ctx := r.Context()
opts := h.rdpOpts
if !id.Authenticated() {
log.Printf("unauthenticated user %s", id.UserName())
http.Error(w, errors.New("cannot find session or user").Error(), http.StatusInternalServerError)
return
}
// determine host to connect to
host, err := h.getHost(ctx, r.URL)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
host = strings.Replace(host, "{{ preferred_username }}", id.UserName(), 1)
// split the username into user and domain
var user = id.UserName()
var domain = ""
if opts.SplitUserDomain {
creds := strings.SplitN(id.UserName(), "@", 2)
user = creds[0]
if len(creds) > 1 {
domain = creds[1]
}
}
render := user
if opts.UsernameTemplate != "" {
render = fmt.Sprintf(h.rdpOpts.UsernameTemplate)
render = strings.Replace(render, "{{ username }}", user, 1)
if h.rdpOpts.UsernameTemplate == render {
log.Printf("Invalid username template. %s == %s", h.rdpOpts.UsernameTemplate, user)
http.Error(w, errors.New("invalid server configuration").Error(), http.StatusInternalServerError)
return
}
}
token, err := h.paaTokenGenerator(ctx, user, host)
if err != nil {
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)
return
}
if h.enableUserToken {
userToken, err := h.userTokenGenerator(ctx, user)
if err != nil {
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)
return
}
render = strings.Replace(render, "{{ token }}", userToken, 1)
}
// authenticated
seed := make([]byte, 16)
_, 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"
w.Header().Set("Content-Disposition", "attachment; filename="+fn)
w.Header().Set("Content-Type", "application/x-rdp")
var d *rdp.Builder
if h.rdpDefaults == "" {
d = rdp.NewBuilder()
} else {
d, err = rdp.NewBuilderFromFile(h.rdpDefaults)
if err != nil {
log.Printf("Cannot load RDP template file %s due to %s", h.rdpDefaults, err)
http.Error(w, errors.New("unable to load RDP template").Error(), http.StatusInternalServerError)
return
}
}
if !h.rdpOpts.NoUsername {
d.Settings.Username = render
if domain != "" {
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

@ -1,235 +0,0 @@
package web
import (
"context"
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/identity"
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/rdp"
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/security"
"net/http"
"net/http/httptest"
"net/url"
"os"
"strings"
"testing"
)
const (
testuser = "test_user"
gateway = "https://my.gateway.com:993"
)
var (
hosts = []string{"10.0.0.1:3389", "10.1.1.1:3000", "32.32.11.1", "remote.host.com"}
key = []byte("thisisasessionkeyreplacethisjetzt")
)
func contains(needle string, haystack []string) bool {
for _, val := range haystack {
if val == needle {
return true
}
}
return false
}
func TestGetHost(t *testing.T) {
ctx := context.Background()
c := Config{
HostSelection: "roundrobin",
Hosts: hosts,
}
h := c.NewHandler()
u := &url.URL{
Host: "example.com",
}
vals := u.Query()
host, err := h.getHost(ctx, u)
if err != nil {
t.Fatalf("#{err}")
}
if !contains(host, hosts) {
t.Fatalf("host %s is not in hosts list", host)
}
// check unsigned
c.HostSelection = "unsigned"
vals.Set("host", "in.valid.host")
u.RawQuery = vals.Encode()
h = c.NewHandler()
host, err = h.getHost(ctx, u)
if err == nil {
t.Fatalf("Accepted host %s is not in hosts list", host)
}
vals.Set("host", hosts[0])
u.RawQuery = vals.Encode()
h = c.NewHandler()
host, err = h.getHost(ctx, u)
if err != nil {
t.Fatalf("Not accepted host %s is in hosts list (err: %s)", hosts[0], err)
}
if host != hosts[0] {
t.Fatalf("host %s is not equal to input %s", host, hosts[0])
}
// check any
c.HostSelection = "any"
test := "bla.bla.com"
vals.Set("host", test)
u.RawQuery = vals.Encode()
h = c.NewHandler()
host, err = h.getHost(ctx, u)
if err != nil {
t.Fatalf("%s is not accepted", host)
}
if test != host {
t.Fatalf("Returned host %s is not equal to input host %s", host, test)
}
// check signed
c.HostSelection = "signed"
c.QueryInfo = security.QueryInfo
issuer := "rdpgwtest"
security.QuerySigningKey = key
queryToken, err := security.GenerateQueryToken(ctx, hosts[0], issuer)
if err != nil {
t.Fatalf("cannot generate token")
}
vals.Set("host", queryToken)
u.RawQuery = vals.Encode()
h = c.NewHandler()
host, err = h.getHost(ctx, u)
if err != nil {
t.Fatalf("Not accepted host %s is in hosts list (err: %s)", hosts[0], err)
}
if host != hosts[0] {
t.Fatalf("%s does not equal %s", host, hosts[0])
}
}
func TestHandler_HandleDownload(t *testing.T) {
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)
ctx := req.Context()
u, _ := url.Parse(gateway)
c := Config{
HostSelection: "roundrobin",
Hosts: hosts,
PAATokenGenerator: paaTokenMock,
GatewayAddress: u,
RdpOpts: RdpOpts{SplitUserDomain: true},
}
h := c.NewHandler()
hh := http.HandlerFunc(h.HandleDownload)
hh.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
}
if ctype := rr.Header().Get("Content-Type"); ctype != "application/x-rdp" {
t.Errorf("content type header does not match: got %v want %v",
ctype, "application/json")
}
if cdisp := rr.Header().Get("Content-Disposition"); cdisp == "" {
t.Errorf("content disposition is nil")
}
data := rdpToMap(strings.Split(rr.Body.String(), rdp.CRLF))
if data["username"] != testuser {
t.Errorf("username key in rdp does not match: got %v want %v", data["username"], testuser)
}
if data["gatewayhostname"] != u.Host {
t.Errorf("gatewayhostname key in rdp does not match: got %v want %v", data["gatewayhostname"], u.Host)
}
if token, _ := paaTokenMock(ctx, testuser, data["full address"]); token != data["gatewayaccesstoken"] {
t.Errorf("gatewayaccesstoken key in rdp does not match username_full address: got %v want %v",
data["gatewayaccesstoken"], token)
}
if !contains(data["full address"], hosts) {
t.Errorf("full address key in rdp is not in allowed hosts list: go %v want in %v",
data["full address"], hosts)
}
}
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) {
return username + "_" + host, nil
}
func rdpToMap(rdp []string) map[string]string {
ret := make(map[string]string)
for s := range rdp {
d := strings.SplitN(rdp[s], ":", 3)
if len(d) >= 2 {
ret[d[0]] = d[2]
}
}
return ret
}

60
common/remote.go Normal file
View File

@ -0,0 +1,60 @@
package common
import (
"context"
"log"
"net"
"net/http"
"strings"
)
const (
ClientIPCtx = "ClientIP"
ProxyAddressesCtx = "ProxyAddresses"
RemoteAddressCtx = "RemoteAddress"
)
func EnrichContext(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
h := r.Header.Get("X-Forwarded-For")
if h != "" {
var proxies []string
ips := strings.Split(h, ",")
for i := range ips {
ips[i] = strings.TrimSpace(ips[i])
}
clientIp := ips[0]
if len(ips) > 1 {
proxies = ips[1:]
}
ctx = context.WithValue(ctx, ClientIPCtx, clientIp)
ctx = context.WithValue(ctx, ProxyAddressesCtx, proxies)
}
ctx = context.WithValue(ctx, RemoteAddressCtx, r.RemoteAddr)
if h == "" {
clientIp, _, _ := net.SplitHostPort(r.RemoteAddr)
ctx = context.WithValue(ctx, ClientIPCtx, clientIp)
}
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func GetClientIp(ctx context.Context) string {
s, ok := ctx.Value(ClientIPCtx).(string)
if !ok {
return ""
}
return s
}
func GetAccessToken(ctx context.Context) string {
token, ok := ctx.Value("access_token").(string)
if !ok {
log.Printf("cannot get access token from context")
return ""
}
return token
}

95
config/configuration.go Normal file
View File

@ -0,0 +1,95 @@
package config
import (
"github.com/spf13/viper"
"log"
)
type Configuration struct {
Server ServerConfig
OpenId OpenIDConfig
Caps RDGCapsConfig
Security SecurityConfig
Client ClientConfig
}
type ServerConfig struct {
GatewayAddress string
Port int
CertFile string
KeyFile string
Hosts []string
RoundRobin bool
SessionKey string
SessionEncryptionKey string
}
type OpenIDConfig struct {
ProviderUrl string
ClientId string
ClientSecret string
}
type RDGCapsConfig struct {
SmartCardAuth bool
TokenAuth bool
IdleTimeout int
RedirectAll bool
DisableRedirect bool
EnableClipboard bool
EnablePrinter bool
EnablePort bool
EnablePnp bool
EnableDrive bool
}
type SecurityConfig struct {
PAATokenEncryptionKey string
PAATokenSigningKey string
UserTokenEncryptionKey string
UserTokenSigningKey string
VerifyClientIp bool
EnableUserToken bool
}
type ClientConfig struct {
NetworkAutoDetect int
BandwidthAutoDetect int
ConnectionType int
UsernameTemplate string
SplitUserDomain bool
DefaultDomain string
}
func init() {
viper.SetDefault("server.certFile", "server.pem")
viper.SetDefault("server.keyFile", "key.pem")
viper.SetDefault("server.port", 443)
viper.SetDefault("client.networkAutoDetect", 1)
viper.SetDefault("client.bandwidthAutoDetect", 1)
viper.SetDefault("security.verifyClientIp", true)
}
func Load(configFile string) Configuration {
var conf Configuration
viper.SetConfigName("rdpgw")
viper.SetConfigFile(configFile)
viper.AddConfigPath(".")
viper.SetEnvPrefix("RDPGW")
viper.AutomaticEnv()
if err := viper.ReadInConfig(); err != nil {
log.Fatalf("No config file found (%s)", err)
}
if err := viper.Unmarshal(&conf); err != nil {
log.Fatalf("Cannot unmarshal the config file; %s", err)
}
if len(conf.Security.PAATokenSigningKey) < 32 {
log.Fatalf("Token signing key not long enough")
}
return conf
}

11
debian/changelog vendored
View File

@ -1,11 +0,0 @@
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
View File

@ -1,21 +0,0 @@
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
View File

@ -1,11 +0,0 @@
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
View File

@ -1,5 +0,0 @@
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
View File

@ -1,21 +0,0 @@
#!/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

View File

@ -1,16 +0,0 @@
[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

View File

@ -1,6 +0,0 @@
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
View File

@ -1,16 +0,0 @@
[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
View File

@ -1,30 +0,0 @@
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
View File

@ -1,19 +0,0 @@
#!/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,12 +0,0 @@
FROM golang:1
WORKDIR /src
ENV CGO_ENABLED 0
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build github.com/bolkedebruin/rdpgw/cmd/rdpgw
FROM gcr.io/distroless/static-debian11:nonroot
WORKDIR /config
COPY --from=0 /src/rdpgw /rdpgw
CMD ["/rdpgw"]

View File

@ -1,49 +1,36 @@
# builder stage
FROM golang:1.22-alpine as builder
FROM debian:buster-slim
#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
RUN adduser --disabled-password --gecos "" --home /opt/rdpgw --uid 1001 rdpgw
# certificate
RUN mkdir -p /opt/rdpgw && cd /opt/rdpgw && \
RUN apt-get update && \
apt-get install -y git golang openssl curl && \
random=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1) && \
openssl genrsa -des3 -passout pass:$random -out server.pass.key 2048 && \
openssl rsa -passin pass:$random -in server.pass.key -out key.pem && \
rm server.pass.key && \
openssl req -new -sha256 -key key.pem -out server.csr \
-subj "/C=US/ST=VA/L=SomeCity/O=MyCompany/OU=MyDivision/CN=rdpgw" && \
-subj "/C=US/ST=VA/L=SomeCity/O=MyCompany/OU=MyDivision/CN=localhost" && \
openssl x509 -req -days 365 -in server.csr -signkey key.pem -out server.pem
# build rdpgw and set rights
ARG CACHEBUST
RUN git clone https://github.com/bolkedebruin/rdpgw.git /app && \
cd /app && \
go mod tidy -compat=1.19 && \
CGO_ENABLED=0 GOOS=linux go build -trimpath -tags '' -ldflags '' -o '/opt/rdpgw/rdpgw' ./cmd/rdpgw && \
CGO_ENABLED=1 GOOS=linux go build -trimpath -tags '' -ldflags '' -o '/opt/rdpgw/rdpgw-auth' ./cmd/auth && \
chmod +x /opt/rdpgw/rdpgw && \
chmod +x /opt/rdpgw/rdpgw-auth && \
chmod u+s /opt/rdpgw/rdpgw-auth
RUN curl -L https://dl.google.com/go/go1.14.7.linux-amd64.tar.gz -o golang.tgz && \
tar zxvf golang.tgz && rm golang.tgz
FROM alpine:latest
RUN git clone https://github.com/bolkedebruin/rdpgw.git && \
cd rdpgw && \
env GOOS=linux GOARCH=amd64 GOROOT=/go /go/bin/go build && \
mkdir -p /opt/rdpgw && \
mv rdpgw /opt/rdpgw/rdpgw && \
rm -rf /root/go && \
rm -rf /rdpgw
RUN apk --no-cache add linux-pam musl
RUN rm -rf /go
# make tempdir in case filestore is used
ADD tmp.tar /
COPY rdpgw.yaml /opt/rdpgw/rdpgw.yaml
COPY --chown=0 rdpgw-pam /etc/pam.d/rdpgw
RUN useradd -m -d /opt/rdpgw -u 1001 -c "rdgw" rdgw && \
mv server.pem /opt/rdpgw/server.pem && \
mv key.pem /opt/rdpgw/key.pem && \
chown -R 1001 /opt/rdpgw && \
chmod +x /opt/rdpgw/rdpgw
USER 1001
COPY --chown=1001 run.sh run.sh
COPY --chown=1001 --from=builder /opt/rdpgw /opt/rdpgw
COPY --chown=1001 --from=builder /etc/passwd /etc/passwd
COPY --chown=1001 --from=builder /etc/ssl/certs /etc/ssl/certs
USER 0
WORKDIR /opt/rdpgw
ENTRYPOINT ["/bin/sh", "/run.sh"]
ENTRYPOINT /opt/rdpgw/rdpgw

View File

@ -1,7 +0,0 @@
FROM rattydave/docker-ubuntu-xrdp-mate-custom:latest
RUN cd /etc/xrdp/ && \
openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem -days 3650 \
-subj "/C=US/ST=VA/L=SomeCity/O=MyCompany/OU=MyDivision/CN=xrdp"
COPY xrdp.ini /etc/xrdp/xrdp.ini

View File

@ -1,67 +0,0 @@
version: '3.4'
volumes:
mysql_data:
driver: local
realm-export.json:
services:
keycloak:
container_name: keycloak
image: richardjkendall/keycloak-arm:latest
hostname: keycloak
volumes:
- ${PWD}/realm-export.json:/export/realm-export.json
environment:
KEYCLOAK_USER: admin
KEYCLOAK_PASSWORD: admin
KEYCLOAK_IMPORT: /export/realm-export.json
ports:
- 8080:8080
restart: on-failure
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/auth"]
interval: 10s
timeout: 3s
retries: 10
start_period: 5s
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
depends_on:
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:
test: ["CMD", "curl", "-f", "http://keycloak:8080"]
interval: 10s
timeout: 10s
retries: 10

View File

@ -1,39 +0,0 @@
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

@ -7,33 +7,26 @@ volumes:
services:
keycloak:
container_name: keycloak
image: quay.io/keycloak/keycloak:latest
image: quay.io/keycloak/keycloak:11.0.0
hostname: keycloak
volumes:
- ${PWD}/realm-export.json:/opt/keycloak/data/import/realm-export.json
- ${PWD}/realm-export.json:/export/realm-export.json
environment:
KEYCLOAK_USER: admin
KEYCLOAK_PASSWORD: admin
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin
KEYCLOAK_IMPORT: /export/realm-export.json
ports:
- 8080:8080
restart: on-failure
command:
- start-dev
- --import-realm
- --http-relative-path=/auth
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/auth"]
interval: 10s
interval: 30s
timeout: 3s
retries: 10
start_period: 5s
xrdp:
container_name: xrdp
hostname: xrdp
image: bolkedebruin/docker-ubuntu-xrdp-mate-rdpgw:latest
image: rattydave/docker-ubuntu-xrdp-mate-custom:20.04
ports:
- 3389:3389
restart: on-failure
@ -41,29 +34,3 @@ services:
- ${PWD}/xrdp_users.txt:/root/createusers.txt
environment:
TZ: "Europe/Amsterdam"
rdpgw:
build: .
ports:
- 9443:9443
restart: on-failure
depends_on:
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:
test: ["CMD", "curl", "-f", "http://keycloak:8080"]
interval: 10s
timeout: 10s
retries: 10

View File

@ -1,35 +0,0 @@
# RDPGW
## What is RDPGW?
Remote Desktop Gateway (RDPGW, RDG or RD Gateway) provides a secure encrypted connection
to user desktops via RDP. It enhances control by removing all remote user direct access to
your system and replaces it with a point-to-point remote desktop connection.
## How to use this image
The remote desktop gateway relies on an OpenID Connect authentication service, such as Keycloak,
Azure AD or Google, and a backend remote desktop service such as XRDP, gnome-remote-desktop, or
Windows VMs. Make sure that these services have been properly setup and can be reached from
where you will run this image.
This image works stateless, which means it does not store any state by default. In case you configure
the session store to be a `filestore` a little bit of session information is stored temporarily. This means
that a load balancer would need to maintain state for a while, which typically is the case.
Session and token encryption keys will be randomized on startup. As a consequence sessions will be
invalidated on restarts and if you are load balancing the different instances will not be able to share
user sessions. Make sure to set these encryption keys to something static, so they can be shared
across the different instances if this is not what you want.
## Configuration through environment variables
```bash
docker --run name rdpgw bolkedebruin/rdpgw:latest \
-e RDPGW_SERVER__CERT_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__SESSION_KEY=thisisasessionkeyreplacethisjetz # 32 characters
-e RDPGW_SERVER__SESSION_ENCRYPTION_KEY=thisisasessionkeyreplacethisnunu # 32 characters
-e RDPGW_OPEN_ID__PROVIDER_URL=http://keycloak:8080/auth/realms/rdpgw
-e RDPGW_OPEN_ID__CLIENT_ID=rdpgw
-e RDPGW_OPEN_ID__CLIENT_SECRET=01cd304c-6f43-4480-9479-618eb6fd578f
-e RDPGW_SECURITY__SECURITY_PAA_TOKEN_SIGNING_KEY=prettypleasereplacemeinproductio # 32 characters
-v conf:/etc/rdpgw
```

View File

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

View File

@ -1,20 +1,21 @@
Server:
CertFile: /opt/rdpgw/server.pem
KeyFile: /opt/rdpgw/key.pem
GatewayAddress: localhost:9443
Port: 9443
Hosts:
server:
certFile: /opt/rdpgw/server.pem
keyFile: /opt/rdpgw/key.pem
gatewayAddress: localhost:9443
port: 9443
hosts:
- xrdp:3389
RoundRobin: false
SessionKey: thisisasessionkeyreplacethisjetz
SessionEncryptionKey: thisisasessionkeyreplacethisnunu
OpenId:
ProviderUrl: http://keycloak:8080/auth/realms/rdpgw
ClientId: rdpgw
ClientSecret: 01cd304c-6f43-4480-9479-618eb6fd578f
Client:
UsernameTemplate: "{{ username }}"
Security:
roundRobin: false
sessionKey: thisisasessionkeyreplacethisjetz
sessionEncryptionKey: thisisasessionkeyreplacethisnunu
openId:
providerUrl: http://keycloak:8080/auth/realms/rdpgw
clientId: rdpgw
clientSecret: 01cd304c-6f43-4480-9479-618eb6fd578f
client:
usernameTemplate: "{{ username }}"
networkAutoDetect: 0
bandwidthAutoDetect: 1
ConnectionType: 6
security:
PAATokenSigningKey: prettypleasereplacemeinproductio
Caps:
TokenAuth: true

View File

@ -1,34 +0,0 @@
#!/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 $?

Binary file not shown.

View File

@ -1,208 +0,0 @@
[Globals]
; xrdp.ini file version number
ini_version=1
; fork a new process for each incoming connection
fork=true
; tcp port to listen
port=3389
; regulate if the listening socket use socket option tcp_nodelay
; no buffering will be performed in the TCP stack
tcp_nodelay=true
; regulate if the listening socket use socket option keepalive
; if the network connection disappear without close messages the connection will be closed
tcp_keepalive=true
#tcp_send_buffer_bytes=32768
#tcp_recv_buffer_bytes=32768
; security layer can be 'tls', 'rdp' or 'negotiate'
; for client compatible layer
security_layer=negotiate
; minimum security level allowed for client
; can be 'none', 'low', 'medium', 'high', 'fips'
crypt_level=high
; X.509 certificate and private key
; openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem -days 365
certificate=cert.pem
key_file=key.pem
; set SSL protocols
; can be comma separated list of 'SSLv3', 'TLSv1', 'TLSv1.1', 'TLSv1.2'
ssl_protocols=TLSv1, TLSv1.1, TLSv1.2
; set TLS cipher suites
#tls_ciphers=HIGH
; Section name to use for automatic login if the client sends username
; and password. If empty, the domain name sent by the client is used.
; If empty and no domain name is given, the first suitable section in
; this file will be used.
autorun=
allow_channels=true
allow_multimon=true
bitmap_cache=true
bitmap_compression=true
bulk_compression=true
#hidelogwindow=true
max_bpp=16
new_cursors=false
; fastpath - can be 'input', 'output', 'both', 'none'
use_fastpath=both
; when true, userid/password *must* be passed on cmd line
#require_credentials=true
; You can set the PAM error text in a gateway setup (MAX 256 chars)
#pamerrortxt=change your password according to policy at http://url
;
; colors used by windows in RGB format
;
blue=009cb5
grey=dedede
#black=000000
#dark_grey=808080
#blue=08246b
#dark_blue=08246b
#white=ffffff
#red=ff0000
#green=00ff00
#background=626c72
;
; configure login screen
;
; Login Screen Window Title
#ls_title=My Login Title
; top level window background color in RGB format
ls_top_window_bg_color=009cb5
; width and height of login screen
ls_width=350
ls_height=430
; login screen background color in RGB format
ls_bg_color=dedede
; optional background image filename (bmp format).
#ls_background_image=
; logo
; full path to bmp-file or file in shared folder
ls_logo_filename=
ls_logo_x_pos=55
ls_logo_y_pos=50
; for positioning labels such as username, password etc
ls_label_x_pos=30
ls_label_width=60
; for positioning text and combo boxes next to above labels
ls_input_x_pos=110
ls_input_width=210
; y pos for first label and combo box
ls_input_y_pos=220
; OK button
ls_btn_ok_x_pos=142
ls_btn_ok_y_pos=370
ls_btn_ok_width=85
ls_btn_ok_height=30
; Cancel button
ls_btn_cancel_x_pos=237
ls_btn_cancel_y_pos=370
ls_btn_cancel_width=85
ls_btn_cancel_height=30
[Logging]
LogFile=xrdp.log
LogLevel=debug
EnableSyslog=true
SyslogLevel=error
; LogLevel and SysLogLevel could by any of: core, error, warning, info or debug
[Channels]
; Channel names not listed here will be blocked by XRDP.
; You can block any channel by setting its value to false.
; IMPORTANT! All channels are not supported in all use
; cases even if you set all values to true.
; You can override these settings on each session type
; These settings are only used if allow_channels=true
rdpdr=true
rdpsnd=true
drdynvc=true
cliprdr=true
rail=true
xrdpvr=true
tcutils=true
; for debugging xrdp, in section xrdp1, change port=-1 to this:
#port=/tmp/.xrdp/xrdp_display_10
; for debugging xrdp, add following line to section xrdp1
#chansrvport=/tmp/.xrdp/xrdp_chansrv_socket_7210
;
; Session types
;
[Xorg]
name=Xorg - Resizing.
lib=libxup.so
username=ask
password=ask
ip=127.0.0.1
port=-1
code=20
#[X11rdp]
#name=X11rdp
#lib=libxup.so
#username=ask
#password=ask
#ip=127.0.0.1
#port=-1
#xserverbpp=24
#code=10
[Xvnc]
name=Xvnc - Screen Sharing.
lib=libvnc.so
username=ask
password=ask
ip=127.0.0.1
port=-1
xserverbpp=16
#delay_ms=2000
[Reconnect]
name=Reconnect
lib=libvnc.so
ip=127.0.0.1
port=ask5910
username=ask
password=ask
#delay_ms=2000
#[vnc-any]
#name=vnc-any
#lib=libvnc.so
#ip=ask
#port=ask5900
#username=na
#password=ask
#pamusername=asksame
#pampassword=asksame
#pamsessionmng=127.0.0.1
#delay_ms=2000
#[sesman-any]
#name=sesman-any
#lib=libvnc.so
#ip=ask
#port=-1
#username=ask
#password=ask
#delay_ms=20

View File

@ -1,84 +0,0 @@
# 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限制或类似措施

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 60 KiB

View File

@ -1,232 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 20 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 60 KiB

View File

@ -1,218 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@ -1,22 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,23 +0,0 @@
# 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"}

63
go.mod
View File

@ -1,60 +1,15 @@
module github.com/bolkedebruin/rdpgw
go 1.22
go 1.14
require (
github.com/bolkedebruin/gokrb5/v8 v8.5.0
github.com/coreos/go-oidc/v3 v3.9.0
github.com/fatih/structs v1.1.0
github.com/go-jose/go-jose/v4 v4.0.1
github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1
github.com/google/uuid v1.6.0
github.com/gorilla/mux v1.8.1
github.com/gorilla/sessions v1.2.2
github.com/gorilla/websocket v1.5.1
github.com/jcmturner/gofork v1.7.6
github.com/jcmturner/goidentity/v6 v6.0.1
github.com/knadh/koanf/parsers/yaml v0.1.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/coreos/go-oidc/v3 v3.0.0-alpha.1
github.com/gorilla/sessions v1.2.0
github.com/gorilla/websocket v1.4.2
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/prometheus/client_golang v1.19.0
github.com/stretchr/testify v1.9.0
github.com/thought-machine/go-flags v1.6.3
golang.org/x/crypto v0.31.0
golang.org/x/oauth2 v0.18.0
google.golang.org/grpc v1.62.1
google.golang.org/protobuf v1.33.0
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.7.0 // 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/jcmturner/aescts/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/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/reflectwalk v1.0.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.0 // indirect
github.com/prometheus/common v0.50.0 // indirect
github.com/prometheus/procfs v0.13.0 // indirect
golang.org/x/net v0.23.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
github.com/prometheus/client_golang v1.7.1
github.com/spf13/cobra v1.0.0
github.com/spf13/viper v1.7.0
github.com/square/go-jose/v3 v3.0.0-20200630053402-0a67ce9b0693
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
)

132
index.js
View File

@ -1,132 +0,0 @@
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请求也支持可以通过请求体发送参数');
});

146
main.go Normal file
View File

@ -0,0 +1,146 @@
package main
import (
"context"
"crypto/tls"
"github.com/bolkedebruin/rdpgw/api"
"github.com/bolkedebruin/rdpgw/common"
"github.com/bolkedebruin/rdpgw/config"
"github.com/bolkedebruin/rdpgw/protocol"
"github.com/bolkedebruin/rdpgw/security"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/spf13/cobra"
"golang.org/x/oauth2"
"log"
"net/http"
"os"
"strconv"
)
var cmd = &cobra.Command{
Use: "rdpgw",
Long: "Remote Desktop Gateway",
}
var (
configFile string
)
var conf config.Configuration
func main() {
// get config
cmd.PersistentFlags().StringVarP(&configFile, "conf", "c", "rdpgw.yaml", "config file (json, yaml, ini)")
conf = config.Load(configFile)
security.VerifyClientIP = conf.Security.VerifyClientIp
// set security keys
security.SigningKey = []byte(conf.Security.PAATokenSigningKey)
security.EncryptionKey = []byte(conf.Security.PAATokenEncryptionKey)
security.UserEncryptionKey = []byte(conf.Security.UserTokenEncryptionKey)
security.UserSigningKey = []byte(conf.Security.UserTokenSigningKey)
// set oidc config
provider, err := oidc.NewProvider(context.Background(), conf.OpenId.ProviderUrl)
if err != nil {
log.Fatalf("Cannot get oidc provider: %s", err)
}
oidcConfig := &oidc.Config{
ClientID: conf.OpenId.ClientId,
}
verifier := provider.Verifier(oidcConfig)
oauthConfig := oauth2.Config{
ClientID: conf.OpenId.ClientId,
ClientSecret: conf.OpenId.ClientSecret,
RedirectURL: "https://" + conf.Server.GatewayAddress + "/callback",
Endpoint: provider.Endpoint(),
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
}
security.OIDCProvider = provider
security.Oauth2Config = oauthConfig
api := &api.Config{
GatewayAddress: conf.Server.GatewayAddress,
OAuth2Config: &oauthConfig,
OIDCTokenVerifier: verifier,
PAATokenGenerator: security.GeneratePAAToken,
UserTokenGenerator: security.GenerateUserToken,
EnableUserToken: conf.Security.EnableUserToken,
SessionKey: []byte(conf.Server.SessionKey),
SessionEncryptionKey: []byte(conf.Server.SessionEncryptionKey),
Hosts: conf.Server.Hosts,
NetworkAutoDetect: conf.Client.NetworkAutoDetect,
UsernameTemplate: conf.Client.UsernameTemplate,
BandwidthAutoDetect: conf.Client.BandwidthAutoDetect,
ConnectionType: conf.Client.ConnectionType,
SplitUserDomain: conf.Client.SplitUserDomain,
DefaultDomain: conf.Client.DefaultDomain,
}
api.NewApi()
if conf.Server.CertFile == "" || conf.Server.KeyFile == "" {
log.Fatal("Both certfile and keyfile need to be specified")
}
//mux := http.NewServeMux()
//mux.HandleFunc("*", HelloServer)
log.Printf("Starting remote desktop gateway server")
cfg := &tls.Config{}
tlsDebug := os.Getenv("SSLKEYLOGFILE")
if tlsDebug != "" {
w, err := os.OpenFile(tlsDebug, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
log.Fatalf("Cannot open key log file %s for writing %s", tlsDebug, err)
}
log.Printf("Key log file set to: %s", tlsDebug)
cfg.KeyLogWriter = w
}
cert, err := tls.LoadX509KeyPair(conf.Server.CertFile, conf.Server.KeyFile)
if err != nil {
log.Fatal(err)
}
cfg.Certificates = append(cfg.Certificates, cert)
server := http.Server{
Addr: ":" + strconv.Itoa(conf.Server.Port),
TLSConfig: cfg,
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)), // disable http2
}
// create the gateway
handlerConfig := protocol.ServerConf{
IdleTimeout: conf.Caps.IdleTimeout,
TokenAuth: conf.Caps.TokenAuth,
SmartCardAuth: conf.Caps.SmartCardAuth,
RedirectFlags: protocol.RedirectFlags{
Clipboard: conf.Caps.EnableClipboard,
Drive: conf.Caps.EnableDrive,
Printer: conf.Caps.EnablePrinter,
Port: conf.Caps.EnablePort,
Pnp: conf.Caps.EnablePnp,
DisableAll: conf.Caps.DisableRedirect,
EnableAll: conf.Caps.RedirectAll,
},
VerifyTunnelCreate: security.VerifyPAAToken,
VerifyServerFunc: security.VerifyServerFunc,
}
gw := protocol.Gateway{
ServerConf: &handlerConfig,
}
http.Handle("/remoteDesktopGateway/", common.EnrichContext(http.HandlerFunc(gw.HandleGatewayProtocol)))
http.Handle("/connect", common.EnrichContext(api.Authenticated(http.HandlerFunc(api.HandleDownload))))
http.Handle("/metrics", promhttp.Handler())
http.HandleFunc("/tokeninfo", api.TokenInfo)
http.HandleFunc("/callback", api.HandleCallback)
err = server.ListenAndServeTLS("", "")
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}

View File

@ -1,31 +0,0 @@
syntax = "proto3";
package auth;
option go_package = "./auth";
message UserPass {
string username = 1;
string password = 2;
}
message AuthResponse {
bool authenticated = 1;
string error = 2;
}
message NtlmRequest {
string session = 1;
string ntlmMessage = 2;
}
message NtlmResponse {
bool authenticated = 1;
string username = 2;
string ntlmMessage = 3;
}
service Authenticate {
rpc Authenticate (UserPass) returns (AuthResponse) {}
rpc NTLM (NtlmRequest) returns (NtlmResponse) {}
}

View File

@ -19,7 +19,7 @@ type ClientConfig struct {
SmartCardAuth bool
PAAToken string
NTLMAuth bool
Session *Tunnel
Session *SessionInfo
LocalConn net.Conn
Server string
Port int
@ -27,10 +27,10 @@ type ClientConfig struct {
}
func (c *ClientConfig) ConnectAndForward() error {
c.Session.transportOut.WritePacket(c.handshakeRequest())
c.Session.TransportOut.WritePacket(c.handshakeRequest())
for {
pt, sz, pkt, err := readMessage(c.Session.transportIn)
pt, sz, pkt, err := readMessage(c.Session.TransportIn)
if err != nil {
log.Printf("Cannot read message from stream %s", err)
return err
@ -44,7 +44,7 @@ func (c *ClientConfig) ConnectAndForward() error {
return err
}
log.Printf("Handshake response received. Caps: %d", caps)
c.Session.transportOut.WritePacket(c.tunnelRequest())
c.Session.TransportOut.WritePacket(c.tunnelRequest())
case PKT_TYPE_TUNNEL_RESPONSE:
tid, caps, err := c.tunnelResponse(pkt)
if err != nil {
@ -52,7 +52,7 @@ func (c *ClientConfig) ConnectAndForward() error {
return err
}
log.Printf("Tunnel creation succesful. Tunnel id: %d and caps %d", tid, caps)
c.Session.transportOut.WritePacket(c.tunnelAuthRequest())
c.Session.TransportOut.WritePacket(c.tunnelAuthRequest())
case PKT_TYPE_TUNNEL_AUTH_RESPONSE:
flags, timeout, err := c.tunnelAuthResponse(pkt)
if err != nil {
@ -60,7 +60,7 @@ func (c *ClientConfig) ConnectAndForward() error {
return err
}
log.Printf("Tunnel auth succesful. Flags: %d and timeout %d", flags, timeout)
c.Session.transportOut.WritePacket(c.channelRequest())
c.Session.TransportOut.WritePacket(c.channelRequest())
case PKT_TYPE_CHANNEL_RESPONSE:
cid, err := c.channelResponse(pkt)
if err != nil {
@ -71,7 +71,7 @@ func (c *ClientConfig) ConnectAndForward() error {
log.Printf("Channel id (%d) is smaller than 1. This doesnt work for Windows clients", cid)
}
log.Printf("Channel creation succesful. Channel id: %d", cid)
//go forward(c.LocalConn, c.Session.transportOut)
go forward(c.LocalConn, c.Session.TransportOut)
case PKT_TYPE_DATA:
receive(pkt, c.LocalConn)
default:

View File

@ -4,12 +4,10 @@ import (
"bytes"
"encoding/binary"
"errors"
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/transport"
"github.com/bolkedebruin/rdpgw/transport"
"io"
"log"
"net"
"os"
"syscall"
)
type RedirectFlags struct {
@ -22,6 +20,21 @@ type RedirectFlags struct {
EnableAll bool
}
type SessionInfo struct {
// The connection-id (RDG-ConnID) as reported by the client
ConnId string
// The underlying incoming transport being either websocket or legacy http
// in case of websocket TransportOut will equal TransportIn
TransportIn transport.Transport
// The underlying outgoing transport being either websocket or legacy http
// in case of websocket TransportOut will equal TransportOut
TransportOut transport.Transport
// The remote desktop server (rdp, vnc etc) the clients intends to connect to
RemoteServer string
// The obtained client ip address
ClientIp string
}
// readMessage parses and defragments a packet from a Transport. It returns
// at most the bytes that have been reported by the packet
func readMessage(in transport.Transport) (pt int, n int, msg []byte, err error) {
@ -92,7 +105,7 @@ func readHeader(data []byte) (packetType uint16, size uint32, packet []byte, err
}
// forwards data from a Connection to Transport and wraps it in the rdpgw protocol
func forward(in net.Conn, tunnel *Tunnel) {
func forward(in net.Conn, out transport.Transport) {
defer in.Close()
b1 := new(bytes.Buffer)
@ -106,7 +119,7 @@ func forward(in net.Conn, tunnel *Tunnel) {
}
binary.Write(b1, binary.LittleEndian, uint16(n))
b1.Write(buf[:n])
tunnel.Write(createPacket(PKT_TYPE_DATA, b1.Bytes()))
out.WritePacket(createPacket(PKT_TYPE_DATA, b1.Bytes()))
b1.Reset()
}
}
@ -123,11 +136,3 @@ func receive(data []byte, out net.Conn) {
out.Write(pkt)
}
// wrapSyscallError takes an error and a syscall name. If the error is
// a syscall.Errno, it wraps it in a os.SyscallError using the syscall name.
func wrapSyscallError(name string, err error) error {
if _, ok := err.(syscall.Errno); ok {
err = os.NewSyscallError(name, err)
}
return err
}

145
protocol/gateway.go Normal file
View File

@ -0,0 +1,145 @@
package protocol
import (
"context"
"github.com/bolkedebruin/rdpgw/common"
"github.com/bolkedebruin/rdpgw/transport"
"github.com/gorilla/websocket"
"github.com/patrickmn/go-cache"
"github.com/prometheus/client_golang/prometheus"
"log"
"net/http"
"time"
)
const (
rdgConnectionIdKey = "Rdg-Connection-Id"
MethodRDGIN = "RDG_IN_DATA"
MethodRDGOUT = "RDG_OUT_DATA"
)
var (
connectionCache = prometheus.NewGauge(
prometheus.GaugeOpts{
Namespace: "rdpgw",
Name: "connection_cache",
Help: "The amount of connections in the cache",
})
websocketConnections = prometheus.NewGauge(
prometheus.GaugeOpts{
Namespace: "rdpgw",
Name: "websocket_connections",
Help: "The count of websocket connections",
})
legacyConnections = prometheus.NewGauge(
prometheus.GaugeOpts{
Namespace: "rdpgw",
Name: "legacy_connections",
Help: "The count of legacy https connections",
})
)
type Gateway struct {
ServerConf *ServerConf
}
var upgrader = websocket.Upgrader{}
var c = cache.New(5*time.Minute, 10*time.Minute)
func init() {
prometheus.MustRegister(connectionCache)
prometheus.MustRegister(legacyConnections)
prometheus.MustRegister(websocketConnections)
}
func (g *Gateway) HandleGatewayProtocol(w http.ResponseWriter, r *http.Request) {
connectionCache.Set(float64(c.ItemCount()))
var s *SessionInfo
connId := r.Header.Get(rdgConnectionIdKey)
x, found := c.Get(connId)
if !found {
s = &SessionInfo{ConnId: connId}
} else {
s = x.(*SessionInfo)
}
ctx := context.WithValue(r.Context(), "SessionInfo", s)
if r.Method == MethodRDGOUT {
if r.Header.Get("Connection") != "upgrade" && r.Header.Get("Upgrade") != "websocket" {
g.handleLegacyProtocol(w, r.WithContext(ctx), s)
return
}
r.Method = "GET" // force
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("Cannot upgrade falling back to old protocol: %s", err)
return
}
defer conn.Close()
g.handleWebsocketProtocol(ctx, conn, s)
} else if r.Method == MethodRDGIN {
g.handleLegacyProtocol(w, r.WithContext(ctx), s)
}
}
func (g *Gateway) handleWebsocketProtocol(ctx context.Context, c *websocket.Conn, s *SessionInfo) {
websocketConnections.Inc()
defer websocketConnections.Dec()
inout, _ := transport.NewWS(c)
s.TransportOut = inout
s.TransportIn = inout
handler := NewServer(s, g.ServerConf)
handler.Process(ctx)
}
// The legacy protocol (no websockets) uses an RDG_IN_DATA for client -> server
// and RDG_OUT_DATA for server -> client data. The handshakeRequest procedure is a bit different
// to ensure the connections do not get cached or terminated by a proxy prematurely.
func (g *Gateway) handleLegacyProtocol(w http.ResponseWriter, r *http.Request, s *SessionInfo) {
log.Printf("Session %s, %t, %t", s.ConnId, s.TransportOut != nil, s.TransportIn != nil)
if r.Method == MethodRDGOUT {
out, err := transport.NewLegacy(w)
if err != nil {
log.Printf("cannot hijack connection to support RDG OUT data channel: %s", err)
return
}
log.Printf("Opening RDGOUT for client %s", common.GetClientIp(r.Context()))
s.TransportOut = out
out.SendAccept(true)
c.Set(s.ConnId, s, cache.DefaultExpiration)
} else if r.Method == MethodRDGIN {
legacyConnections.Inc()
defer legacyConnections.Dec()
in, err := transport.NewLegacy(w)
if err != nil {
log.Printf("cannot hijack connection to support RDG IN data channel: %s", err)
return
}
defer in.Close()
if s.TransportIn == nil {
s.TransportIn = in
c.Set(s.ConnId, s, cache.DefaultExpiration)
log.Printf("Opening RDGIN for client %s", common.GetClientIp(r.Context()))
in.SendAccept(false)
// read some initial data
in.Drain()
log.Printf("Legacy handshakeRequest done for client %s", common.GetClientIp(r.Context()))
handler := NewServer(s, g.ServerConf)
handler.Process(r.Context())
}
}
}

View File

@ -14,8 +14,8 @@ const (
TunnelCreateResponseLen = HeaderLen + 18
TunnelAuthLen = HeaderLen + 2 // + dynamic
TunnelAuthResponseLen = HeaderLen + 16
ChannelCreateLen = HeaderLen + 8 // + dynamic
ChannelResponseLen = HeaderLen + 12
ChannelCreateLen = HeaderLen + 8 // + dynamic
ChannelResponseLen = HeaderLen + 12
)
func verifyPacketHeader(data []byte, expPt uint16, expSize uint32) (uint16, uint32, []byte, error) {
@ -40,10 +40,11 @@ func TestHandshake(t *testing.T) {
client := ClientConfig{
PAAToken: "abab",
}
gw := &Gateway{}
tunnel := &Tunnel{}
h := NewProcessor(gw, tunnel)
s := &SessionInfo{}
hc := &ServerConf{
TokenAuth: true,
}
h := NewServer(s, hc)
data := client.handshakeRequest()
@ -65,7 +66,7 @@ func TestHandshake(t *testing.T) {
t.Fatalf("handshakeRequest failed got ext auth %d, expected %d", extAuth, extAuth|HTTP_EXTENDED_AUTH_PAA)
}
data = h.handshakeResponse(0x0, 0x0, HTTP_EXTENDED_AUTH_PAA, ERROR_SUCCESS)
data = h.handshakeResponse(0x0, 0x0)
_, _, pkt, err = verifyPacketHeader(data, PKT_TYPE_HANDSHAKE_RESPONSE, HandshakeResponseLen)
if err != nil {
t.Fatalf("verifyHeader failed: %s", err)
@ -78,63 +79,15 @@ func TestHandshake(t *testing.T) {
}
}
func capsHelper(gw Gateway) uint16 {
var caps uint16
if gw.TokenAuth {
caps = caps | HTTP_EXTENDED_AUTH_PAA
}
if gw.SmartCardAuth {
caps = caps | HTTP_EXTENDED_AUTH_SC
}
return caps
}
func TestMatchAuth(t *testing.T) {
gw := &Gateway{}
tunnel := &Tunnel{}
h := NewProcessor(gw, tunnel)
in := uint16(0)
caps, err := h.matchAuth(in)
if err != nil {
t.Fatalf("in caps: %x <= server caps %x, but %s", in, capsHelper(*gw), err)
}
if caps > in {
t.Fatalf("returned server caps %x > client cpas %x", capsHelper(*gw), in)
}
in = HTTP_EXTENDED_AUTH_PAA
caps, err = h.matchAuth(in)
if err == nil {
t.Fatalf("server cannot satisfy client caps %x but error is nil (server caps %x)", in, caps)
} else {
t.Logf("(SUCCESS) server cannot satisfy client caps : %s", err)
}
gw.SmartCardAuth = true
caps, err = h.matchAuth(in)
if err == nil {
t.Fatalf("server cannot satisfy client caps %x but error is nil (server caps %x)", in, caps)
} else {
t.Logf("(SUCCESS) server cannot satisfy client caps : %s", err)
}
gw.TokenAuth = true
caps, err = h.matchAuth(in)
if err != nil {
t.Fatalf("server caps %x (orig: %x) should match client request %x, %s", caps, capsHelper(*gw), in, err)
}
}
func TestTunnelCreation(t *testing.T) {
client := ClientConfig{
PAAToken: "abab",
}
gw := &Gateway{TokenAuth: true}
tunnel := &Tunnel{}
h := NewProcessor(gw, tunnel)
s := &SessionInfo{}
hc := &ServerConf{
TokenAuth: true,
}
h := NewServer(s, hc)
data := client.tunnelRequest()
_, _, pkt, err := verifyPacketHeader(data, PKT_TYPE_TUNNEL_CREATE,
@ -151,7 +104,7 @@ func TestTunnelCreation(t *testing.T) {
t.Fatalf("tunnelRequest failed got token %s, expected %s", token, client.PAAToken)
}
data = h.tunnelResponse(ERROR_SUCCESS)
data = h.tunnelResponse()
_, _, pkt, err = verifyPacketHeader(data, PKT_TYPE_TUNNEL_RESPONSE, TunnelCreateResponseLen)
if err != nil {
t.Fatalf("verifyHeader failed: %s", err)
@ -174,13 +127,15 @@ func TestTunnelAuth(t *testing.T) {
client := ClientConfig{
Name: name,
}
gw := &Gateway{
TokenAuth: true,
IdleTimeout: 10,
RedirectFlags: RedirectFlags{Clipboard: true},
s := &SessionInfo{}
hc := &ServerConf{
TokenAuth: true,
IdleTimeout: 10,
RedirectFlags: RedirectFlags{
Clipboard: true,
},
}
tunnel := &Tunnel{}
h := NewProcessor(gw, tunnel)
h := NewServer(s, hc)
data := client.tunnelAuthRequest()
_, _, pkt, err := verifyPacketHeader(data, PKT_TYPE_TUNNEL_AUTH, uint32(TunnelAuthLen+len(name)*2))
@ -193,7 +148,7 @@ func TestTunnelAuth(t *testing.T) {
t.Fatalf("tunnelAuthRequest failed got name %s, expected %s", n, name)
}
data = h.tunnelAuthResponse(ERROR_SUCCESS)
data = h.tunnelAuthResponse()
_, _, pkt, err = verifyPacketHeader(data, PKT_TYPE_TUNNEL_AUTH_RESPONSE, TunnelAuthResponseLen)
if err != nil {
t.Fatalf("verifyHeader failed: %s", err)
@ -206,9 +161,9 @@ func TestTunnelAuth(t *testing.T) {
t.Fatalf("tunnelAuthResponse failed got flags %d, expected %d",
flags, flags|HTTP_TUNNEL_REDIR_DISABLE_CLIPBOARD)
}
if int(timeout) != gw.IdleTimeout {
if int(timeout) != hc.IdleTimeout {
t.Fatalf("tunnelAuthResponse failed got timeout %d, expected %d",
timeout, gw.IdleTimeout)
timeout, hc.IdleTimeout)
}
}
@ -216,17 +171,17 @@ func TestChannelCreation(t *testing.T) {
server := "test_server"
client := ClientConfig{
Server: server,
Port: 3389,
Port: 3389,
}
gw := &Gateway{
s := &SessionInfo{}
hc := &ServerConf{
TokenAuth: true,
IdleTimeout: 10,
RedirectFlags: RedirectFlags{
Clipboard: true,
},
}
tunnel := &Tunnel{}
h := NewProcessor(gw, tunnel)
h := NewServer(s, hc)
data := client.channelRequest()
_, _, pkt, err := verifyPacketHeader(data, PKT_TYPE_CHANNEL_CREATE, uint32(ChannelCreateLen+len(server)*2))
@ -241,7 +196,7 @@ func TestChannelCreation(t *testing.T) {
t.Fatalf("channelRequest failed got port %d, expected %d", hPort, client.Port)
}
data = h.channelResponse(ERROR_SUCCESS)
data = h.channelResponse()
_, _, pkt, err = verifyPacketHeader(data, PKT_TYPE_CHANNEL_RESPONSE, uint32(ChannelResponseLen))
if err != nil {
t.Fatalf("verifyHeader failed: %s", err)

338
protocol/server.go Normal file
View File

@ -0,0 +1,338 @@
package protocol
import (
"bytes"
"context"
"encoding/binary"
"errors"
"github.com/bolkedebruin/rdpgw/common"
"io"
"log"
"net"
"strconv"
"time"
)
type VerifyTunnelCreate func(context.Context, string) (bool, error)
type VerifyTunnelAuthFunc func(context.Context, string) (bool, error)
type VerifyServerFunc func(context.Context, string) (bool, error)
type Server struct {
Session *SessionInfo
VerifyTunnelCreate VerifyTunnelCreate
VerifyTunnelAuthFunc VerifyTunnelAuthFunc
VerifyServerFunc VerifyServerFunc
RedirectFlags int
IdleTimeout int
SmartCardAuth bool
TokenAuth bool
ClientName string
Remote net.Conn
State int
}
type ServerConf struct {
VerifyTunnelCreate VerifyTunnelCreate
VerifyTunnelAuthFunc VerifyTunnelAuthFunc
VerifyServerFunc VerifyServerFunc
RedirectFlags RedirectFlags
IdleTimeout int
SmartCardAuth bool
TokenAuth bool
}
func NewServer(s *SessionInfo, conf *ServerConf) *Server {
h := &Server{
State: SERVER_STATE_INITIAL,
Session: s,
RedirectFlags: makeRedirectFlags(conf.RedirectFlags),
IdleTimeout: conf.IdleTimeout,
SmartCardAuth: conf.SmartCardAuth,
TokenAuth: conf.TokenAuth,
VerifyTunnelCreate: conf.VerifyTunnelCreate,
VerifyServerFunc: conf.VerifyServerFunc,
VerifyTunnelAuthFunc: conf.VerifyTunnelAuthFunc,
}
return h
}
const tunnelId = 10
func (s *Server) Process(ctx context.Context) error {
for {
pt, sz, pkt, err := readMessage(s.Session.TransportIn)
if err != nil {
log.Printf("Cannot read message from stream %s", err)
return err
}
switch pt {
case PKT_TYPE_HANDSHAKE_REQUEST:
log.Printf("Client handshakeRequest from %s", common.GetClientIp(ctx))
if s.State != SERVER_STATE_INITIAL {
log.Printf("Handshake attempted while in wrong state %d != %d", s.State, SERVER_STATE_INITIAL)
return errors.New("wrong state")
}
major, minor, _, _ := s.handshakeRequest(pkt) // todo check if auth matches what the handler can do
msg := s.handshakeResponse(major, minor)
s.Session.TransportOut.WritePacket(msg)
s.State = SERVER_STATE_HANDSHAKE
case PKT_TYPE_TUNNEL_CREATE:
log.Printf("Tunnel create")
if s.State != SERVER_STATE_HANDSHAKE {
log.Printf("Tunnel create attempted while in wrong state %d != %d",
s.State, SERVER_STATE_HANDSHAKE)
return errors.New("wrong state")
}
_, cookie := s.tunnelRequest(pkt)
if s.VerifyTunnelCreate != nil {
if ok, _ := s.VerifyTunnelCreate(ctx, cookie); !ok {
log.Printf("Invalid PAA cookie received from client %s", common.GetClientIp(ctx))
return errors.New("invalid PAA cookie")
}
}
msg := s.tunnelResponse()
s.Session.TransportOut.WritePacket(msg)
s.State = SERVER_STATE_TUNNEL_CREATE
case PKT_TYPE_TUNNEL_AUTH:
log.Printf("Tunnel auth")
if s.State != SERVER_STATE_TUNNEL_CREATE {
log.Printf("Tunnel auth attempted while in wrong state %d != %d",
s.State, SERVER_STATE_TUNNEL_CREATE)
return errors.New("wrong state")
}
client := s.tunnelAuthRequest(pkt)
if s.VerifyTunnelAuthFunc != nil {
if ok, _ := s.VerifyTunnelAuthFunc(ctx, client); !ok {
log.Printf("Invalid client name: %s", client)
return errors.New("invalid client name")
}
}
msg := s.tunnelAuthResponse()
s.Session.TransportOut.WritePacket(msg)
s.State = SERVER_STATE_TUNNEL_AUTHORIZE
case PKT_TYPE_CHANNEL_CREATE:
log.Printf("Channel create")
if s.State != SERVER_STATE_TUNNEL_AUTHORIZE {
log.Printf("Channel create attempted while in wrong state %d != %d",
s.State, SERVER_STATE_TUNNEL_AUTHORIZE)
return errors.New("wrong state")
}
server, port := s.channelRequest(pkt)
host := net.JoinHostPort(server, strconv.Itoa(int(port)))
if s.VerifyServerFunc != nil {
if ok, _ := s.VerifyServerFunc(ctx, host); !ok {
log.Printf("Not allowed to connect to %s by policy handler", host)
return errors.New("denied by security policy")
}
}
log.Printf("Establishing connection to RDP server: %s", host)
s.Remote, err = net.DialTimeout("tcp", host, time.Second*15)
if err != nil {
log.Printf("Error connecting to %s, %s", host, err)
return err
}
log.Printf("Connection established")
msg := s.channelResponse()
s.Session.TransportOut.WritePacket(msg)
// Make sure to start the flow from the RDP server first otherwise connections
// might hang eventually
go forward(s.Remote, s.Session.TransportOut)
s.State = SERVER_STATE_CHANNEL_CREATE
case PKT_TYPE_DATA:
if s.State < SERVER_STATE_CHANNEL_CREATE {
log.Printf("Data received while in wrong state %d != %d", s.State, SERVER_STATE_CHANNEL_CREATE)
return errors.New("wrong state")
}
s.State = SERVER_STATE_OPENED
receive(pkt, s.Remote)
case PKT_TYPE_KEEPALIVE:
// keepalives can be received while the channel is not open yet
if s.State < SERVER_STATE_CHANNEL_CREATE {
log.Printf("Keepalive received while in wrong state %d != %d", s.State, SERVER_STATE_CHANNEL_CREATE)
return errors.New("wrong state")
}
// avoid concurrency issues
// p.TransportIn.Write(createPacket(PKT_TYPE_KEEPALIVE, []byte{}))
case PKT_TYPE_CLOSE_CHANNEL:
log.Printf("Close channel")
if s.State != SERVER_STATE_OPENED {
log.Printf("Channel closed while in wrong state %d != %d", s.State, SERVER_STATE_OPENED)
return errors.New("wrong state")
}
s.Session.TransportIn.Close()
s.Session.TransportOut.Close()
s.State = SERVER_STATE_CLOSED
default:
log.Printf("Unknown packet (size %d): %x", sz, pkt)
}
}
}
// Creates a packet the is a response to a handshakeRequest request
// HTTP_EXTENDED_AUTH_SSPI_NTLM is not supported in Linux
// but could be in Windows. However the NTLM protocol is insecure
func (s *Server) handshakeResponse(major byte, minor byte) []byte {
var caps uint16
if s.SmartCardAuth {
caps = caps | HTTP_EXTENDED_AUTH_SC
}
if s.TokenAuth {
caps = caps | HTTP_EXTENDED_AUTH_PAA
}
buf := new(bytes.Buffer)
binary.Write(buf, binary.LittleEndian, uint32(0)) // error_code
buf.Write([]byte{major, minor})
binary.Write(buf, binary.LittleEndian, uint16(0)) // server version
binary.Write(buf, binary.LittleEndian, uint16(caps)) // extended auth
return createPacket(PKT_TYPE_HANDSHAKE_RESPONSE, buf.Bytes())
}
func (s *Server) handshakeRequest(data []byte) (major byte, minor byte, version uint16, extAuth uint16) {
r := bytes.NewReader(data)
binary.Read(r, binary.LittleEndian, &major)
binary.Read(r, binary.LittleEndian, &minor)
binary.Read(r, binary.LittleEndian, &version)
binary.Read(r, binary.LittleEndian, &extAuth)
log.Printf("major: %d, minor: %d, version: %d, ext auth: %d", major, minor, version, extAuth)
return
}
func (s *Server) tunnelRequest(data []byte) (caps uint32, cookie string) {
var fields uint16
r := bytes.NewReader(data)
binary.Read(r, binary.LittleEndian, &caps)
binary.Read(r, binary.LittleEndian, &fields)
r.Seek(2, io.SeekCurrent)
if fields == HTTP_TUNNEL_PACKET_FIELD_PAA_COOKIE {
var size uint16
binary.Read(r, binary.LittleEndian, &size)
cookieB := make([]byte, size)
r.Read(cookieB)
cookie, _ = DecodeUTF16(cookieB)
}
return
}
func (s *Server) tunnelResponse() []byte {
buf := new(bytes.Buffer)
binary.Write(buf, binary.LittleEndian, uint16(0)) // server version
binary.Write(buf, binary.LittleEndian, uint32(0)) // error code
binary.Write(buf, binary.LittleEndian, uint16(HTTP_TUNNEL_RESPONSE_FIELD_TUNNEL_ID|HTTP_TUNNEL_RESPONSE_FIELD_CAPS)) // fields present
binary.Write(buf, binary.LittleEndian, uint16(0)) // reserved
// tunnel id (when is it used?)
binary.Write(buf, binary.LittleEndian, uint32(tunnelId))
binary.Write(buf, binary.LittleEndian, uint32(HTTP_CAPABILITY_IDLE_TIMEOUT))
return createPacket(PKT_TYPE_TUNNEL_RESPONSE, buf.Bytes())
}
func (s *Server) tunnelAuthRequest(data []byte) string {
buf := bytes.NewReader(data)
var size uint16
binary.Read(buf, binary.LittleEndian, &size)
clData := make([]byte, size)
binary.Read(buf, binary.LittleEndian, &clData)
clientName, _ := DecodeUTF16(clData)
return clientName
}
func (s *Server) tunnelAuthResponse() []byte {
buf := new(bytes.Buffer)
binary.Write(buf, binary.LittleEndian, uint32(0)) // error code
binary.Write(buf, binary.LittleEndian, uint16(HTTP_TUNNEL_AUTH_RESPONSE_FIELD_REDIR_FLAGS|HTTP_TUNNEL_AUTH_RESPONSE_FIELD_IDLE_TIMEOUT)) // fields present
binary.Write(buf, binary.LittleEndian, uint16(0)) // reserved
// idle timeout
if s.IdleTimeout < 0 {
s.IdleTimeout = 0
}
binary.Write(buf, binary.LittleEndian, uint32(s.RedirectFlags)) // redir flags
binary.Write(buf, binary.LittleEndian, uint32(s.IdleTimeout)) // timeout in minutes
return createPacket(PKT_TYPE_TUNNEL_AUTH_RESPONSE, buf.Bytes())
}
func (s *Server) channelRequest(data []byte) (server string, port uint16) {
buf := bytes.NewReader(data)
var resourcesSize byte
var alternative byte
var protocol uint16
var nameSize uint16
binary.Read(buf, binary.LittleEndian, &resourcesSize)
binary.Read(buf, binary.LittleEndian, &alternative)
binary.Read(buf, binary.LittleEndian, &port)
binary.Read(buf, binary.LittleEndian, &protocol)
binary.Read(buf, binary.LittleEndian, &nameSize)
nameData := make([]byte, nameSize)
binary.Read(buf, binary.LittleEndian, &nameData)
server, _ = DecodeUTF16(nameData)
return
}
func (s *Server) channelResponse() []byte {
buf := new(bytes.Buffer)
binary.Write(buf, binary.LittleEndian, uint32(0)) // error code
binary.Write(buf, binary.LittleEndian, uint16(HTTP_CHANNEL_RESPONSE_FIELD_CHANNELID)) // fields present
binary.Write(buf, binary.LittleEndian, uint16(0)) // reserved
// channel id is required for Windows clients
binary.Write(buf, binary.LittleEndian, uint32(1)) // channel id
// optional fields
// channel id uint32 (4)
// udp port uint16 (2)
// udp auth cookie 1 byte for side channel
// length uint16
return createPacket(PKT_TYPE_CHANNEL_RESPONSE, buf.Bytes())
}
func makeRedirectFlags(flags RedirectFlags) int {
var redir = 0
if flags.DisableAll {
return HTTP_TUNNEL_REDIR_DISABLE_ALL
}
if flags.EnableAll {
return HTTP_TUNNEL_REDIR_ENABLE_ALL
}
if !flags.Port {
redir = redir | HTTP_TUNNEL_REDIR_DISABLE_PORT
}
if !flags.Clipboard {
redir = redir | HTTP_TUNNEL_REDIR_DISABLE_CLIPBOARD
}
if !flags.Drive {
redir = redir | HTTP_TUNNEL_REDIR_DISABLE_DRIVE
}
if !flags.Pnp {
redir = redir | HTTP_TUNNEL_REDIR_DISABLE_PNP
}
if !flags.Printer {
redir = redir | HTTP_TUNNEL_REDIR_DISABLE_PRINTER
}
return redir
}

View File

@ -43,7 +43,7 @@ const (
HTTP_TUNNEL_REDIR_DISABLE_ALL = 0x40000000
HTTP_TUNNEL_REDIR_DISABLE_DRIVE = 0x01
HTTP_TUNNEL_REDIR_DISABLE_PRINTER = 0x02
HTTP_TUNNEL_REDIR_DISABLE_PORT = 0x04
HTTP_TUNNEL_REDIR_DISABLE_PORT = 0x03
HTTP_TUNNEL_REDIR_DISABLE_CLIPBOARD = 0x08
HTTP_TUNNEL_REDIR_DISABLE_PNP = 0x10
)
@ -59,8 +59,8 @@ const (
)
const (
SERVER_STATE_INITIALIZED = 0x0
SERVER_STATE_HANDSHAKE = 0x1
SERVER_STATE_INITIAL = 0x0
SERVER_STATE_HANDSHAKE = 0x1
SERVER_STATE_TUNNEL_CREATE = 0x2
SERVER_STATE_TUNNEL_AUTHORIZE = 0x3
SERVER_STATE_CHANNEL_CREATE = 0x4

240
security/jwt.go Normal file
View File

@ -0,0 +1,240 @@
package security
import (
"context"
"errors"
"fmt"
"github.com/bolkedebruin/rdpgw/common"
"github.com/bolkedebruin/rdpgw/protocol"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/square/go-jose/v3"
"github.com/square/go-jose/v3/jwt"
"golang.org/x/oauth2"
"log"
"time"
)
var (
SigningKey []byte
EncryptionKey []byte
UserSigningKey []byte
UserEncryptionKey []byte
OIDCProvider *oidc.Provider
Oauth2Config oauth2.Config
)
var ExpiryTime time.Duration = 5
var VerifyClientIP bool = true
type customClaims struct {
RemoteServer string `json:"remoteServer"`
ClientIP string `json:"clientIp"`
AccessToken string `json:"accessToken"`
}
func VerifyPAAToken(ctx context.Context, tokenString string) (bool, error) {
token, err := jwt.ParseSigned(tokenString)
// check if the signing algo matches what we expect
for _, header := range token.Headers {
if header.Algorithm != string(jose.HS256) {
return false, fmt.Errorf("unexpected signing method: %v", header.Algorithm)
}
}
standard := jwt.Claims{}
custom := customClaims{}
// Claims automagically checks the signature...
err = token.Claims(SigningKey, &standard, &custom)
if err != nil {
log.Printf("token signature validation failed due to %s", err)
return false, err
}
// ...but doesn't check the expiry claim :/
err = standard.Validate(jwt.Expected{
Issuer: "rdpgw",
Time: time.Now(),
})
if err != nil {
log.Printf("token validation failed due to %s", err)
return false, err
}
// validate the access token
tokenSource := Oauth2Config.TokenSource(ctx, &oauth2.Token{AccessToken: custom.AccessToken})
_, err = OIDCProvider.UserInfo(ctx, tokenSource)
if err != nil {
log.Printf("Cannot get user info for access token: %s", err)
return false, err
}
s := getSessionInfo(ctx)
s.RemoteServer = custom.RemoteServer
s.ClientIp = custom.ClientIP
return true, nil
}
func VerifyServerFunc(ctx context.Context, host string) (bool, error) {
s := getSessionInfo(ctx)
if s == nil {
return false, errors.New("no valid session info found in context")
}
if s.RemoteServer != host {
log.Printf("Client specified host %s does not match token host %s", host, s.RemoteServer)
return false, nil
}
if VerifyClientIP && s.ClientIp != common.GetClientIp(ctx) {
log.Printf("Current client ip address %s does not match token client ip %s",
common.GetClientIp(ctx), s.ClientIp)
return false, nil
}
return true, nil
}
func GeneratePAAToken(ctx context.Context, username string, server string) (string, error) {
if len(SigningKey) < 32 {
return "", errors.New("token signing key not long enough or not specified")
}
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.HS256, Key: SigningKey}, nil)
if err != nil {
log.Printf("Cannot obtain signer %s", err)
return "", err
}
standard := jwt.Claims{
Issuer: "rdpgw",
Expiry: jwt.NewNumericDate(time.Now().Add(time.Minute * 5)),
Subject: username,
}
private := customClaims{
RemoteServer: server,
ClientIP: common.GetClientIp(ctx),
AccessToken: common.GetAccessToken(ctx),
}
if token, err := jwt.Signed(sig).Claims(standard).Claims(private).CompactSerialize(); err != nil {
log.Printf("Cannot sign PAA token %s", err)
return "", err
} else {
return token, nil
}
}
func GenerateUserToken(ctx context.Context, userName string) (string, error) {
if len(UserEncryptionKey) < 32 {
return "", errors.New("user token encryption key not long enough or not specified")
}
claims := jwt.Claims{
Subject: userName,
Expiry: jwt.NewNumericDate(time.Now().Add(time.Minute * 5)),
Issuer: "rdpgw",
}
enc, err := jose.NewEncrypter(
jose.A128CBC_HS256,
jose.Recipient{Algorithm: jose.DIRECT, Key: UserEncryptionKey},
(&jose.EncrypterOptions{Compression: jose.DEFLATE}).WithContentType("JWT"),
)
if err != nil {
log.Printf("Cannot encrypt user token due to %s", err)
return "", err
}
// 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)
// token, err := jwt.SignedAndEncrypted(sig, enc).Claims(claims).CompactSerialize()
token, err := jwt.Encrypted(enc).Claims(claims).CompactSerialize()
return token, err
}
func UserInfo(ctx context.Context, token string) (jwt.Claims, error) {
standard := jwt.Claims{}
if len(UserEncryptionKey) > 0 && len(UserSigningKey) > 0 {
enc, err := jwt.ParseSignedAndEncrypted(token)
if err != nil {
log.Printf("Cannot get token %s", err)
return standard, errors.New("cannot get token")
}
token, err := enc.Decrypt(UserEncryptionKey)
if err != nil {
log.Printf("Cannot decrypt token %s", err)
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 {
log.Printf("cannot verify signature %s", err)
return standard, errors.New("cannot verify signature")
}
} else if len(UserSigningKey) == 0 {
token, err := jwt.ParseEncrypted(token)
if err != nil {
log.Printf("Cannot get token %s", err)
return standard, errors.New("cannot get token")
}
err = token.Claims(UserEncryptionKey, &standard)
if err != nil {
log.Printf("Cannot decrypt token %s", err)
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
err := standard.Validate(jwt.Expected{
Issuer: "rdpgw",
Time: time.Now(),
})
if err != nil {
log.Printf("token validation failed due to %s", err)
return standard, fmt.Errorf("token validation failed due to %s", err)
}
return standard, nil
}
func getSessionInfo(ctx context.Context) *protocol.SessionInfo {
s, ok := ctx.Value("SessionInfo").(*protocol.SessionInfo)
if !ok {
log.Printf("cannot get session info from context")
return nil
}
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

@ -1,401 +0,0 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.25.0-devel
// protoc v3.14.0
// source: auth.proto
package auth
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type UserPass struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"`
Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"`
}
func (x *UserPass) Reset() {
*x = UserPass{}
if protoimpl.UnsafeEnabled {
mi := &file_auth_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *UserPass) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UserPass) ProtoMessage() {}
func (x *UserPass) ProtoReflect() protoreflect.Message {
mi := &file_auth_proto_msgTypes[0]
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 UserPass.ProtoReflect.Descriptor instead.
func (*UserPass) Descriptor() ([]byte, []int) {
return file_auth_proto_rawDescGZIP(), []int{0}
}
func (x *UserPass) GetUsername() string {
if x != nil {
return x.Username
}
return ""
}
func (x *UserPass) GetPassword() string {
if x != nil {
return x.Password
}
return ""
}
type AuthResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Authenticated bool `protobuf:"varint,1,opt,name=authenticated,proto3" json:"authenticated,omitempty"`
Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"`
}
func (x *AuthResponse) Reset() {
*x = AuthResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_auth_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *AuthResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AuthResponse) ProtoMessage() {}
func (x *AuthResponse) ProtoReflect() protoreflect.Message {
mi := &file_auth_proto_msgTypes[1]
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 AuthResponse.ProtoReflect.Descriptor instead.
func (*AuthResponse) Descriptor() ([]byte, []int) {
return file_auth_proto_rawDescGZIP(), []int{1}
}
func (x *AuthResponse) GetAuthenticated() bool {
if x != nil {
return x.Authenticated
}
return false
}
func (x *AuthResponse) GetError() string {
if x != nil {
return x.Error
}
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_rawDesc = []byte{
0x0a, 0x0a, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x04, 0x61, 0x75,
0x74, 0x68, 0x22, 0x42, 0x0a, 0x08, 0x55, 0x73, 0x65, 0x72, 0x50, 0x61, 0x73, 0x73, 0x12, 0x1a,
0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61,
0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61,
0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x4a, 0x0a, 0x0c, 0x41, 0x75, 0x74, 0x68, 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, 0x14, 0x0a, 0x05,
0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72,
0x6f, 0x72, 0x22, 0x49, 0x0a, 0x0b, 0x4e, 0x74, 0x6c, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01,
0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x6e,
0x74, 0x6c, 0x6d, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
0x52, 0x0b, 0x6e, 0x74, 0x6c, 0x6d, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x88, 0x01,
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 (
file_auth_proto_rawDescOnce sync.Once
file_auth_proto_rawDescData = file_auth_proto_rawDesc
)
func file_auth_proto_rawDescGZIP() []byte {
file_auth_proto_rawDescOnce.Do(func() {
file_auth_proto_rawDescData = protoimpl.X.CompressGZIP(file_auth_proto_rawDescData)
})
return file_auth_proto_rawDescData
}
var file_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
var file_auth_proto_goTypes = []interface{}{
(*UserPass)(nil), // 0: auth.UserPass
(*AuthResponse)(nil), // 1: auth.AuthResponse
(*NtlmRequest)(nil), // 2: auth.NtlmRequest
(*NtlmResponse)(nil), // 3: auth.NtlmResponse
}
var file_auth_proto_depIdxs = []int32{
0, // 0: auth.Authenticate.Authenticate:input_type -> auth.UserPass
2, // 1: auth.Authenticate.NTLM:input_type -> auth.NtlmRequest
1, // 2: auth.Authenticate.Authenticate:output_type -> auth.AuthResponse
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 extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_auth_proto_init() }
func file_auth_proto_init() {
if File_auth_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_auth_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*UserPass); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_auth_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*AuthResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
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{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_auth_proto_rawDesc,
NumEnums: 0,
NumMessages: 4,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_auth_proto_goTypes,
DependencyIndexes: file_auth_proto_depIdxs,
MessageInfos: file_auth_proto_msgTypes,
}.Build()
File_auth_proto = out.File
file_auth_proto_rawDesc = nil
file_auth_proto_goTypes = nil
file_auth_proto_depIdxs = nil
}

View File

@ -1,137 +0,0 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
package auth
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7
// AuthenticateClient is the client API for Authenticate service.
//
// 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 {
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 {
cc grpc.ClientConnInterface
}
func NewAuthenticateClient(cc grpc.ClientConnInterface) AuthenticateClient {
return &authenticateClient{cc}
}
func (c *authenticateClient) Authenticate(ctx context.Context, in *UserPass, opts ...grpc.CallOption) (*AuthResponse, error) {
out := new(AuthResponse)
err := c.cc.Invoke(ctx, "/auth.Authenticate/Authenticate", in, out, opts...)
if err != nil {
return nil, err
}
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.
// All implementations must embed UnimplementedAuthenticateServer
// for forward compatibility
type AuthenticateServer interface {
Authenticate(context.Context, *UserPass) (*AuthResponse, error)
NTLM(context.Context, *NtlmRequest) (*NtlmResponse, error)
mustEmbedUnimplementedAuthenticateServer()
}
// UnimplementedAuthenticateServer must be embedded to have forward compatible implementations.
type UnimplementedAuthenticateServer struct {
}
func (UnimplementedAuthenticateServer) Authenticate(context.Context, *UserPass) (*AuthResponse, error) {
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.
// Use of this interface is not recommended, as added methods to AuthenticateServer will
// result in compilation errors.
type UnsafeAuthenticateServer interface {
mustEmbedUnimplementedAuthenticateServer()
}
func RegisterAuthenticateServer(s grpc.ServiceRegistrar, srv AuthenticateServer) {
s.RegisterService(&Authenticate_ServiceDesc, srv)
}
func _Authenticate_Authenticate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(UserPass)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AuthenticateServer).Authenticate(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/auth.Authenticate/Authenticate",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthenticateServer).Authenticate(ctx, req.(*UserPass))
}
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.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var Authenticate_ServiceDesc = grpc.ServiceDesc{
ServiceName: "auth.Authenticate",
HandlerType: (*AuthenticateServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Authenticate",
Handler: _Authenticate_Authenticate_Handler,
},
{
MethodName: "NTLM",
Handler: _Authenticate_NTLM_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "auth.proto",
}

View File

@ -2,9 +2,9 @@ package transport
import (
"bufio"
"crypto/rand"
"errors"
"io"
"math/rand"
"net"
"net/http"
"net/http/httputil"
@ -12,14 +12,14 @@ import (
)
const (
crlf = "\r\n"
crlf = "\r\n"
HttpOK = "HTTP/1.1 200 OK\r\n"
)
type LegacyPKT struct {
Conn net.Conn
Conn net.Conn
ChunkedReader io.Reader
Writer *bufio.Writer
Writer *bufio.Writer
}
func NewLegacy(w http.ResponseWriter) (*LegacyPKT, error) {
@ -27,9 +27,9 @@ func NewLegacy(w http.ResponseWriter) (*LegacyPKT, error) {
if ok {
conn, rw, err := hj.Hijack()
l := &LegacyPKT{
Conn: conn,
Conn: conn,
ChunkedReader: httputil.NewChunkedReader(rw.Reader),
Writer: rw.Writer,
Writer: rw.Writer,
}
return l, err
}
@ -37,7 +37,7 @@ func NewLegacy(w http.ResponseWriter) (*LegacyPKT, error) {
return nil, errors.New("cannot hijack connection")
}
func (t *LegacyPKT) ReadPacket() (n int, p []byte, err error) {
func (t *LegacyPKT) ReadPacket() (n int, p []byte, err error){
buf := make([]byte, 4096) // bufio.defaultBufSize
n, err = t.ChunkedReader.Read(buf)
p = make([]byte, n)