Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remote Desktop Protocol (RDP) matcher #219

Merged
merged 3 commits into from
Jul 22, 2024
Merged

Remote Desktop Protocol (RDP) matcher #219

merged 3 commits into from
Jul 22, 2024

Conversation

vnxme
Copy link
Collaborator

@vnxme vnxme commented Jul 20, 2024

Summary

The PR title seems to be self-explanatory. With this rdp matcher layer4 module allows Caddy to multiplex RDP connections. What is more, its cookie_hash option allows to match mstshash optional cookie value, and cookie_ips and cookie_ports options provide for msts optional cookie value matching (see important caveats in the comments to the matcher code). This way, if Microsoft Terminal Services Client (mstsc.exe) is used, Caddy may also route RDP connections to different servers based on the username, which is truncated to 9 characters to become mstshash. In addition, this matcher adds cookie_hash, cookie_ip, cookie_port and correlation_id to the replacer, if provided in the first packet. Last but not least, caddyfile support is included.

Config examples

Simple RDP multiplexing
{
  "apps": {
    "layer4": {
      "servers": {
        "example": {
          "listen": ["0.0.0.0:443"],
          "routes": [
            {
              "handle": [
                {
                  "handler": "proxy",
                  "upstreams": [
                    {"dial": ["single.machine.local:3389"]}
                  ]
                }
              ],
              "match": [
                {"rdp": {}}
              ]
            },
            {
              "handle": [
                {
                  "handler": "tls"
                },
                {
                  "handler": "proxy",
                  "upstreams": [
                    {"dial": ["localhost:8080"]}
                  ]
                }
              ],
              "match": [
                {"tls": {}}
              ]
            }
          ]
        }
      }
    }
  }
}
Same as above, but route RDP to 2 machines depending on cookie_hash, with a fallback mechanism
{
  "apps": {
    "layer4": {
      "servers": {
        "example": {
          "listen": ["0.0.0.0:443"],
          "routes": [
            {
              "handle": [
                {
                  "handler": "proxy",
                  "upstreams": [
                    {"dial": ["another.machine.local:3389"]}
                  ]
                }
              ],
              "match": [
                {"rdp": {"cookie_hash":"Administr"}}
              ]
            }
            {
              "handle": [
                {
                  "handler": "proxy",
                  "upstreams": [
                    {"dial": ["fallback.machine.local:3389"]}
                  ]
                }
              ],
              "match": [
                {"rdp": {}}
              ]
            },
            {
              "handle": [
                {
                  "handler": "tls"
                },
                {
                  "handler": "proxy",
                  "upstreams": [
                    {"dial": ["localhost:8080"]}
                  ]
                }
              ],
              "match": [
                {"tls": {}}
              ]
            }
          ]
        }
      }
    }
  }
}
Same as above, but route RDP to 2 machines depending on custom_info_regexp, with a fallback mechanism
{
  "apps": {
    "layer4": {
      "servers": {
        "example": {
          "listen": ["0.0.0.0:443"],
          "routes": [
            {
              "handle": [
                {
                  "handler": "proxy",
                  "upstreams": [
                    {"dial": ["another.machine.local:3389"]}
                  ]
                }
              ],
              "match": [
                {"rdp": {"custom_info_regexp":"^[a-z0-9]+$"}}
              ]
            }
            {
              "handle": [
                {
                  "handler": "proxy",
                  "upstreams": [
                    {"dial": ["fallback.machine.local:3389"]}
                  ]
                }
              ],
              "match": [
                {"rdp": {}}
              ]
            },
            {
              "handle": [
                {
                  "handler": "tls"
                },
                {
                  "handler": "proxy",
                  "upstreams": [
                    {"dial": ["localhost:8080"]}
                  ]
                }
              ],
              "match": [
                {"tls": {}}
              ]
            }
          ]
        }
      }
    }
  }
}

Testing

Tested on my machines having Windows 11 with a native server/client. Moreover, tested on MacOS Sonoma 14.5 with Jump Desktop 8.10.4 and Microsoft Remote Desktop 10.9.6 as clients, and iOS 17.5.1 with Microsoft Remote Desktop 10.5.9 as a client. For express testing you may use pre-compiled docker images vnxme/caddy:layer4-rdp. They have a minimum of the latest Caddy, and layer4 module with RDP matcher.

Note for testers leveraging mstsc.exe as a client and using cookie_hash option: this program seems to have a bug related to mstshash cookie update. When you open it and only fill host:port field (leave username and password empty), it sends a RDP Connection Request without a cookie - it's correct. Next, if you fill both the host:port field and username fields, it takes the first 9 characters of the username you provided as mstshash cookie value to include into the RDP Connection Request - it's correct again. However, if you cancel connection and change the username, it won't update mstshash cookie value, i.e. the RDP Connection Request will include 9 characters of the username you provided before the change - that's where the bug occurs which leads to Caddy misrouting RDP connections. Reopening the client resolves the problem.

Note for testers leveraging Apache Guacamole and using cookie_hash option: this program ignores any domain name, so it's only username value that's included into RDP CR packet as mstshash. What's great about this client is that it has a custom load balance info/cookie field, which takes precedence over standard cookie payload. Thus, you can effectively use custom_info or custom_info_regexp option of this matcher for RDP connections routing.

Note for testers using other RDP clients: it's unclear why some clients include usernames as cookie hashes while others don't. It seems this part of RDP is not fully standardised, so each group of developers decide what to do. For example, Microsoft Remote Desktop clients on MacOS and iOS don't send cookie hashes at all. If this matcher doesn't work for you as expected, the best way to debug is to capture the first packet with Wireshark.

Copy link
Owner

@mholt mholt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is awesome!

I don't have windows to test this but I see the tests and I'm happy about those.

If it works for you I'm happy to accept this and we can iterate on it later if needed.

Very cool!

@vnxme
Copy link
Collaborator Author

vnxme commented Jul 21, 2024

Rebased this PR onto master with #218 merged.

@mholt mholt merged commit 1507f4e into mholt:master Jul 22, 2024
6 checks passed
@mholt
Copy link
Owner

mholt commented Jul 22, 2024

Thanks for the great contribution 😃

@vnxme
Copy link
Collaborator Author

vnxme commented Jul 22, 2024

Wow, that was quick!

I added a few nice features, like custom info parsing and regexp matching, to better address RDP client diversity issues, and updated the opening post above.

@vnxme vnxme deleted the rdp branch July 22, 2024 13:48
@vnxme vnxme mentioned this pull request Jul 23, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants