github.com/mholt/caddy-l4@v0.0.0-20241104153248-ec8fae209322/modules/l4rdp/matcher.go (about)

     1  // Copyright 2024 VNXME
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package l4rdp
    16  
    17  import (
    18  	"bytes"
    19  	"encoding/base64"
    20  	"encoding/binary"
    21  	"io"
    22  	"net"
    23  	"net/netip"
    24  	"regexp"
    25  	"strconv"
    26  	"strings"
    27  
    28  	"github.com/caddyserver/caddy/v2"
    29  	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
    30  	"github.com/caddyserver/caddy/v2/modules/caddyhttp"
    31  	"github.com/mholt/caddy-l4/layer4"
    32  )
    33  
    34  func init() {
    35  	caddy.RegisterModule(&MatchRDP{})
    36  }
    37  
    38  // MatchRDP is able to match RDP connections.
    39  type MatchRDP struct {
    40  	CookieHash       string   `json:"cookie_hash,omitempty"`
    41  	CookieHashRegexp string   `json:"cookie_hash_regexp,omitempty"`
    42  	CookieIPs        []string `json:"cookie_ips,omitempty"`
    43  	CookiePorts      []uint16 `json:"cookie_ports,omitempty"`
    44  	CustomInfo       string   `json:"custom_info,omitempty"`
    45  	CustomInfoRegexp string   `json:"custom_info_regexp,omitempty"`
    46  
    47  	cookieIPs []netip.Prefix
    48  
    49  	cookieHashRegexp *regexp.Regexp
    50  	customInfoRegexp *regexp.Regexp
    51  }
    52  
    53  // CaddyModule returns the Caddy module information.
    54  func (m *MatchRDP) CaddyModule() caddy.ModuleInfo {
    55  	return caddy.ModuleInfo{
    56  		ID:  "layer4.matchers.rdp",
    57  		New: func() caddy.Module { return new(MatchRDP) },
    58  	}
    59  }
    60  
    61  // Match returns true if the connection looks like RDP.
    62  func (m *MatchRDP) Match(cx *layer4.Connection) (bool, error) {
    63  	// Replace placeholders in filters
    64  	repl := cx.Context.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
    65  	cookieHash := repl.ReplaceAll(m.CookieHash, "")
    66  	cookieHash = cookieHash[:min(RDPCookieHashBytesMax, uint16(len(cookieHash)))]
    67  	customInfo := repl.ReplaceAll(m.CustomInfo, "")
    68  	customInfo = customInfo[:min(RDPCustomInfoBytesMax, uint16(len(customInfo)))]
    69  
    70  	// Read a number of bytes to parse headers
    71  	headerBuf := make([]byte, RDPConnReqBytesMin)
    72  	n, err := io.ReadFull(cx, headerBuf)
    73  	if err != nil || n < int(RDPConnReqBytesMin) {
    74  		return false, err
    75  	}
    76  
    77  	// Parse TPKTHeader
    78  	h := &TPKTHeader{}
    79  	if err = h.FromBytes(headerBuf[TPKTHeaderBytesStart : TPKTHeaderBytesStart+TPKTHeaderBytesTotal]); err != nil {
    80  		return false, nil
    81  	}
    82  
    83  	// Validate TPKTHeader
    84  	if h.Version != TPKTHeaderVersion || h.Reserved != TPKTHeaderReserved ||
    85  		h.Length < RDPConnReqBytesMin || h.Length > RDPConnReqBytesMax {
    86  		return false, nil
    87  	}
    88  
    89  	// Parse X224Crq
    90  	x := &X224Crq{}
    91  	if err = x.FromBytes(headerBuf[X224CrqBytesStart : X224CrqBytesStart+X224CrqBytesTotal]); err != nil {
    92  		return false, nil
    93  	}
    94  
    95  	// Validate X224Crq
    96  	if x.TypeCredit != X224CrqTypeCredit || x.DstRef != X224CrqDstRef ||
    97  		x.SrcRef != X224CrqSrcRef || x.ClassOptions != X224CrqClassOptions ||
    98  		uint16(x.Length) != (h.Length-TPKTHeaderBytesTotal-1) {
    99  		return false, nil
   100  	}
   101  
   102  	// Calculate and validate payload length
   103  	// NOTE: at this stage we can't be absolutely sure that the protocol is RDP, though payloads are optional.
   104  	// This behaviour may be changed in the future if there are many false negative matches due to some RDP
   105  	// clients sending RDP connection requests containing TPKTHeader and X224Crq headers only.
   106  	payloadBytesTotal := uint16(x.Length) - (X224CrqBytesTotal - 1)
   107  	if payloadBytesTotal == 0 {
   108  		return false, nil
   109  	}
   110  
   111  	// Read a number of bytes to parse payload
   112  	payloadBuf := make([]byte, payloadBytesTotal)
   113  	n, err = io.ReadFull(cx, payloadBuf)
   114  	if err != nil || n < int(payloadBytesTotal) {
   115  		return false, err
   116  	}
   117  
   118  	// Validate the remaining connection buffer
   119  	// NOTE: if at least 1 byte remains, we can technically be sure, the protocol isn't RDP.
   120  	// This behaviour may be changed in the future if there are many false negative matches.
   121  	extraBuf := make([]byte, 1)
   122  	n, err = io.ReadFull(cx, extraBuf)
   123  	if err == nil && n == len(extraBuf) {
   124  		return false, err
   125  	}
   126  
   127  	// Find CRLF which divides token/cookie from RDPNegReq and RDPCorrInfo
   128  	var RDPNegReqBytesStart uint16 = 0
   129  	for index, b := range payloadBuf {
   130  		if b == ASCIIByteCR && payloadBuf[index+1] == ASCIIByteLF {
   131  			RDPNegReqBytesStart = uint16(index) + 2 // start after CR LF
   132  			break
   133  		}
   134  	}
   135  
   136  	// Process optional RDPCookie
   137  	var hasValidCookie bool
   138  	for RDPNegReqBytesStart >= RDPCookieBytesMin {
   139  		RDPCookieBytesTotal := RDPNegReqBytesStart // include CR LF
   140  
   141  		// Parse RDPCookie
   142  		c := string(payloadBuf[RDPCookieBytesStart : RDPCookieBytesStart+RDPCookieBytesTotal])
   143  
   144  		// Validate RDPCookie
   145  		if RDPCookieBytesTotal > RDPCookieBytesMax || !strings.HasPrefix(c, RDPCookiePrefix) {
   146  			break
   147  		}
   148  
   149  		// Extract hash (username truncated to max number of characters from the left)
   150  		// NOTE: according to mstsc.exe tests, if "domain" and "username" are provided, hash will be "domain/us"
   151  		hashBytesStart := uint16(len(RDPCookiePrefix))
   152  		hashBytesTotal := RDPCookieBytesTotal - hashBytesStart - 2 // exclude CR LF
   153  		hash := c[hashBytesStart : hashBytesStart+hashBytesTotal]
   154  
   155  		// Add hash to the replacer
   156  		repl.Set("l4.rdp.cookie_hash", hash)
   157  
   158  		// Full match
   159  		if len(cookieHash) > 0 && cookieHash != hash {
   160  			break
   161  		}
   162  
   163  		// Regexp match
   164  		if len(m.CookieHashRegexp) > 0 && !m.cookieHashRegexp.MatchString(hash) {
   165  			break
   166  		}
   167  
   168  		hasValidCookie = true
   169  		break
   170  	}
   171  
   172  	// NOTE: we can stop validation because hash hasn't matched
   173  	if !hasValidCookie && (len(cookieHash) > 0 || len(m.CookieHashRegexp) > 0) {
   174  		return false, nil
   175  	}
   176  
   177  	// Process optional RDPToken
   178  	var hasValidToken bool
   179  	for !hasValidCookie && RDPNegReqBytesStart >= RDPTokenBytesMin {
   180  		RDPTokenBytesTotal := RDPNegReqBytesStart // include CR LF
   181  
   182  		// Parse RDPToken
   183  		t := &RDPToken{}
   184  		if err = t.FromBytes(payloadBuf[RDPTokenBytesStart : RDPTokenBytesStart+RDPTokenBytesTotal]); err != nil {
   185  			break
   186  		}
   187  
   188  		// Validate RDPToken
   189  		if t.Version != RDPTokenVersion || t.Reserved != RDPTokenReserved ||
   190  			t.Length != RDPTokenBytesTotal || t.LengthIndicator != uint8(t.Length-5) ||
   191  			t.TypeCredit != x.TypeCredit || t.DstRef != x.DstRef || t.SrcRef != x.SrcRef ||
   192  			t.ClassOptions != x.ClassOptions {
   193  			break
   194  		}
   195  
   196  		// NOTE: RDPToken without a cookie value is technically correct
   197  		l := t.Length - RDPTokenBytesMin
   198  		if l == 0 {
   199  			hasValidToken = (len(m.cookieIPs) == 0) && (len(m.CookiePorts) == 0)
   200  			break
   201  		}
   202  
   203  		// Validate RDPToken.Optional (1/6)
   204  		// NOTE: maximum length has been calculated for a cookie having IPv4 address. If it supports IPv6 addresses,
   205  		// RDPTokenOptionalCookieBytesMax constant has to be adjusted accordingly. The IP parsing process
   206  		// would also need to be redesigned to provide for solutions relevant for both address families.
   207  		RDPTokenOptionalCookieBytesTotal := l - 2 // exclude CR LF
   208  		if RDPTokenOptionalCookieBytesTotal < RDPTokenOptionalCookieBytesMin ||
   209  			RDPTokenOptionalCookieBytesTotal > RDPTokenOptionalCookieBytesMax {
   210  			break
   211  		}
   212  
   213  		// Validate RDPToken.Optional (2/6)
   214  		c := string(t.Optional[RDPTokenOptionalCookieBytesStart:RDPTokenOptionalCookieBytesTotal])
   215  		if !strings.HasPrefix(c, RDPTokenOptionalCookiePrefix) {
   216  			break
   217  		}
   218  
   219  		// Validate RDPToken.Optional (3/6)
   220  		d := strings.Split(c[len(RDPTokenOptionalCookiePrefix):], string(RDPTokenOptionalCookieSeparator))
   221  		if len(d) != 3 {
   222  			break
   223  		}
   224  
   225  		// Validate RDPToken.Optional (4/6)
   226  		ipStr, portStr, reservedStr := d[0], d[1], d[2]
   227  		if reservedStr != RDPTokenOptionalCookieReserved {
   228  			break
   229  		}
   230  
   231  		// Validate RDPToken.Optional (5/6)
   232  		ipNum, err := strconv.ParseUint(ipStr, 10, 32)
   233  		if err != nil {
   234  			break
   235  		}
   236  		ipBuf := make([]byte, 4)
   237  		binary.LittleEndian.PutUint32(ipBuf, uint32(ipNum))
   238  		ipVal := make(net.IP, 4)
   239  		if err = binary.Read(bytes.NewBuffer(ipBuf), binary.BigEndian, &ipVal); err != nil {
   240  			break
   241  		}
   242  
   243  		// Validate RDPToken.Optional (6/6)
   244  		portNum, err := strconv.ParseUint(portStr, 10, 16)
   245  		if err != nil {
   246  			break
   247  		}
   248  		portBuf := make([]byte, 4)
   249  		binary.LittleEndian.PutUint16(portBuf, uint16(portNum))
   250  		portVal := uint16(0)
   251  		if err = binary.Read(bytes.NewBuffer(portBuf), binary.BigEndian, &portVal); err != nil {
   252  			break
   253  		}
   254  
   255  		// Add IP and port to the replacer
   256  		repl.Set("l4.rdp.cookie_ip", ipVal.String())
   257  		repl.Set("l4.rdp.cookie_port", strconv.Itoa(int(portVal)))
   258  
   259  		if len(m.cookieIPs) > 0 {
   260  			var found bool
   261  			for _, prefix := range m.cookieIPs {
   262  				if prefix.Contains(netip.AddrFrom4([4]byte(ipVal))) {
   263  					found = true
   264  					break
   265  				}
   266  			}
   267  			if !found {
   268  				break
   269  			}
   270  		}
   271  
   272  		if len(m.CookiePorts) > 0 {
   273  			var found bool
   274  			for _, port := range m.CookiePorts {
   275  				if port == portVal {
   276  					found = true
   277  					break
   278  				}
   279  			}
   280  			if !found {
   281  				break
   282  			}
   283  		}
   284  
   285  		hasValidToken = true
   286  		break
   287  	}
   288  
   289  	// NOTE: we can stop validation because IPs or ports haven't matched
   290  	if !hasValidToken && (len(m.cookieIPs) > 0 || len(m.CookiePorts) > 0) {
   291  		return false, nil
   292  	}
   293  
   294  	// Process RDPCustom
   295  	var hasValidCustom bool
   296  	for !(hasValidCookie || hasValidToken) && RDPNegReqBytesStart >= RDPCustomBytesMin {
   297  		RDPCustomBytesTotal := RDPNegReqBytesStart // include CR LF
   298  
   299  		// Parse RDPCustom
   300  		c := string(payloadBuf[RDPCustomBytesStart : RDPCustomBytesStart+RDPCustomBytesTotal])
   301  
   302  		// Validate RDPCustom
   303  		if RDPCustomBytesTotal > RDPCustomBytesMax {
   304  			break
   305  		}
   306  
   307  		// Extract info (everything before CR LF)
   308  		// NOTE: according to Apache Guacamole tests, if "load balance info/cookie" option is non-empty,
   309  		// its contents is included into the RDP Connection Request packet without any changes
   310  		infoBytesTotal := RDPCustomBytesTotal - RDPCustomInfoBytesStart - 2 // exclude CR LF
   311  		info := c[RDPCustomInfoBytesStart : RDPCustomInfoBytesStart+infoBytesTotal]
   312  
   313  		// Add info to the replacer
   314  		repl.Set("l4.rdp.custom_info", info)
   315  
   316  		// Full match
   317  		if len(customInfo) > 0 && customInfo != info {
   318  			break
   319  		}
   320  
   321  		// Regexp match
   322  		if len(m.CustomInfoRegexp) > 0 && !m.customInfoRegexp.MatchString(info) {
   323  			break
   324  		}
   325  
   326  		hasValidCustom = true
   327  		break
   328  	}
   329  
   330  	// NOTE: we can stop validation because info hasn't matched
   331  	if !hasValidCustom && (len(customInfo) > 0 || len(m.CustomInfoRegexp) > 0) {
   332  		return false, nil
   333  	}
   334  
   335  	// Validate RDPCookie, RDPToken and RDPCustom presence to match payload boundaries
   336  	// NOTE: if there is anything before CR LF, but RDPCookie and RDPToken parsing has failed,
   337  	// we can technically be sure, the protocol isn't RDP. However, given RDPCustom has no mandatory prefix
   338  	// by definition (it's an extension to the official documentation), this condition can barely be met.
   339  	if RDPNegReqBytesStart > 0 && (!hasValidCookie && !hasValidToken && !hasValidCustom) {
   340  		return false, nil
   341  	}
   342  
   343  	// NOTE: Given RDPNegReq and RDPCorrInfo are optional, we have found CR LF at the end of the payload,
   344  	// and all the validations above have passed, we can reasonably treat the protocol in question as RDP.
   345  	// This behaviour may be changed in the future if there are many false positive matches.
   346  	if RDPNegReqBytesStart == payloadBytesTotal {
   347  		return true, nil
   348  	}
   349  
   350  	// Validate RDPNegReq boundaries
   351  	if RDPNegReqBytesStart+RDPNegReqBytesTotal > payloadBytesTotal {
   352  		return false, nil
   353  	}
   354  
   355  	// Parse RDPNegReq
   356  	r := &RDPNegReq{}
   357  	if err = r.FromBytes(payloadBuf[RDPNegReqBytesStart : RDPNegReqBytesStart+RDPNegReqBytesTotal]); err != nil {
   358  		return false, nil
   359  	}
   360  
   361  	// Validate RDPNegReq
   362  	// NOTE: for simplicity, we treat a RDPNegReq with all flags and protocols set as valid.
   363  	// This behaviour may be changed in the future if there are many false positive matches.
   364  	if r.Type != RDPNegReqType || r.Length != RDPNegReqLength ||
   365  		r.Flags|RDPNegReqFlagsAll != RDPNegReqFlagsAll || r.Protocols|RDPNegReqProtocolsAll != RDPNegReqProtocolsAll ||
   366  		(r.Protocols&RDPNegReqProtoHybridEx == RDPNegReqProtoHybridEx && r.Protocols&RDPNegReqProtoHybrid == 0) ||
   367  		(r.Protocols&RDPNegReqProtoHybrid == RDPNegReqProtoHybrid && r.Protocols&RDPNegReqProtoSSL == 0) {
   368  		return false, nil
   369  	}
   370  
   371  	// Validate RDPCorrInfo presence to match payload boundaries
   372  	// NOTE: nothing must be present after RDPNegReq unless RDPNegReqFlagCorrInfo is set,
   373  	// otherwise we can reasonably treat the connection as RDP, given all the validations above have passed.
   374  	if r.Flags&RDPNegReqFlagCorrInfo == 0 {
   375  		if RDPNegReqBytesStart+RDPNegReqBytesTotal < payloadBytesTotal {
   376  			return false, nil
   377  		} else {
   378  			return true, nil
   379  		}
   380  	}
   381  
   382  	// Validate RDPCorrInfo boundaries
   383  	RDPCorrInfoBytesStart := RDPNegReqBytesStart + RDPNegReqBytesTotal
   384  	if RDPCorrInfoBytesStart+RDPCorrInfoBytesTotal > payloadBytesTotal {
   385  		return false, nil
   386  	}
   387  
   388  	// Parse RDPCorrInfo
   389  	i := &RDPCorrInfo{}
   390  	if err = i.FromBytes(payloadBuf[RDPCorrInfoBytesStart : RDPCorrInfoBytesStart+RDPCorrInfoBytesTotal]); err != nil {
   391  		return false, nil
   392  	}
   393  
   394  	// Validate RDPCorrInfo (1/3)
   395  	// NOTE: the first byte of RDPCorrInfo.Identity must not be equal 0x00 or 0xF4
   396  	if i.Type != RDPCorrInfoType || i.Flags != RDPCorrInfoFlags || i.Length != RDPCorrInfoLength ||
   397  		i.Identity[0] == RDPCorrInfoReserved || i.Identity[0] == RDPCorrInfoIdentityF4 {
   398  		return false, nil
   399  	}
   400  
   401  	// Validate RDPCorrInfo (2/3)
   402  	// NOTE: no byte of RDPCorrInfo.Identity must be equal 0x0D
   403  	for _, b := range i.Identity {
   404  		if b == ASCIIByteCR {
   405  			return false, nil
   406  		}
   407  	}
   408  
   409  	// Add base64 of identity bytes to the replacer
   410  	repl.Set("l4.rdp.correlation_id", base64.StdEncoding.EncodeToString(i.Identity[:]))
   411  
   412  	// Validate RDPCorrInfo (3/3)
   413  	// NOTE: any byte of RDPCorrInfo.Reserved must be equal 0x00
   414  	for _, b := range i.Reserved {
   415  		if b != RDPCorrInfoReserved {
   416  			return false, nil
   417  		}
   418  	}
   419  
   420  	return true, nil
   421  }
   422  
   423  // Provision parses m's IP ranges, either from IP or CIDR expressions, and regular expressions.
   424  func (m *MatchRDP) Provision(_ caddy.Context) (err error) {
   425  	repl := caddy.NewReplacer()
   426  	for _, cookieAddrOrCIDR := range m.CookieIPs {
   427  		cookieAddrOrCIDR = repl.ReplaceAll(cookieAddrOrCIDR, "")
   428  		prefix, err := caddyhttp.CIDRExpressionToPrefix(cookieAddrOrCIDR)
   429  		if err != nil {
   430  			return err
   431  		}
   432  		m.cookieIPs = append(m.cookieIPs, prefix)
   433  	}
   434  	m.cookieHashRegexp, err = regexp.Compile(repl.ReplaceAll(m.CookieHashRegexp, ""))
   435  	if err != nil {
   436  		return err
   437  	}
   438  	m.customInfoRegexp, err = regexp.Compile(repl.ReplaceAll(m.CustomInfoRegexp, ""))
   439  	if err != nil {
   440  		return err
   441  	}
   442  	return nil
   443  }
   444  
   445  // UnmarshalCaddyfile sets up the MatchRDP from Caddyfile tokens. Syntax:
   446  //
   447  //	rdp {
   448  //		cookie_hash <value>
   449  //	}
   450  //	rdp {
   451  //		cookie_hash_regexp <value>
   452  //	}
   453  //	rdp {
   454  //		cookie_ip <ranges...>
   455  //		cookie_port <ports...>
   456  //	}
   457  //	rdp {
   458  //		custom_info <value>
   459  //	}
   460  //	rdp {
   461  //		custom_info_regexp <value>
   462  //	}
   463  //	rdp
   464  //
   465  // Note: according to the protocol documentation, RDP cookies and tokens are optional, i.e. it depends on the client
   466  // whether they are included in the first packet (RDP Connection Request) or not. Besides, no valid RDP CR packet must
   467  // contain cookie_hash ("mstshash") and cookie_ip:cookie_port ("msts") at the same time, i.e. Match will always return
   468  // false if cookie_hash and any of cookie_ip and cookie_port are set simultaneously. If this matcher has cookie_hash
   469  // option, but a valid RDP CR packet doesn't have it, Match will return false. If this matcher has a set of cookie_ip
   470  // and cookie_port options, or any of them, but a valid RDP CR packet doesn't have them, Match will return false.
   471  //
   472  // There are some RDP clients (e.g. Apache Guacamole) that support any text to be included into an RDP CR packet
   473  // instead of "mstshash" and "msts" cookies for load balancing and/or routing purposes, parsed here as custom_info.
   474  // If this matcher has custom_info option, but a valid RDP CR packet doesn't have it, Match will return false.
   475  // If custom_info option is combined with cookie_hash, cookie_ip or cookie_port, Match will return false as well.
   476  func (m *MatchRDP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
   477  	_, wrapper := d.Next(), d.Val() // consume wrapper name
   478  
   479  	// No same-line arguments are supported
   480  	if d.CountRemainingArgs() > 0 {
   481  		return d.ArgErr()
   482  	}
   483  
   484  	var hasCookieHash, hasCookieIPOrPort, hasCustomInfo bool
   485  	for nesting := d.Nesting(); d.NextBlock(nesting); {
   486  		optionName := d.Val()
   487  		switch optionName {
   488  		case "cookie_hash":
   489  			if hasCookieIPOrPort || hasCustomInfo {
   490  				return d.Errf("%s option '%s' can't be combined with other options", wrapper, optionName)
   491  			}
   492  			if hasCookieHash {
   493  				return d.Errf("duplicate %s option '%s'", wrapper, optionName)
   494  			}
   495  			if d.CountRemainingArgs() != 1 {
   496  				return d.ArgErr()
   497  			}
   498  			_, val := d.NextArg(), d.Val()
   499  			m.CookieHash, hasCookieHash = val, true
   500  		case "cookie_hash_regexp":
   501  			if hasCookieIPOrPort || hasCustomInfo {
   502  				return d.Errf("%s option '%s' can't be combined with other options", wrapper, optionName)
   503  			}
   504  			if hasCookieHash {
   505  				return d.Errf("duplicate %s option '%s'", wrapper, optionName)
   506  			}
   507  			if d.CountRemainingArgs() != 1 {
   508  				return d.ArgErr()
   509  			}
   510  			_, val := d.NextArg(), d.Val()
   511  			m.CookieHashRegexp, hasCookieHash = val, true
   512  		case "cookie_ip":
   513  			if hasCookieHash || hasCustomInfo {
   514  				return d.Errf("%s option '%s' can only be combined with 'cookie_port' option", wrapper, optionName)
   515  			}
   516  			if d.CountRemainingArgs() == 0 {
   517  				return d.ArgErr()
   518  			}
   519  			for d.NextArg() {
   520  				val := d.Val()
   521  				if val == "private_ranges" {
   522  					m.CookieIPs = append(m.CookieIPs, caddyhttp.PrivateRangesCIDR()...)
   523  					continue
   524  				}
   525  				m.CookieIPs = append(m.CookieIPs, val)
   526  			}
   527  			hasCookieIPOrPort = true
   528  		case "cookie_port":
   529  			if hasCookieHash || hasCustomInfo {
   530  				return d.Errf("%s option '%s' can only be combined with 'cookie_ip' option", wrapper, optionName)
   531  			}
   532  			if d.CountRemainingArgs() == 0 {
   533  				return d.ArgErr()
   534  			}
   535  			for d.NextArg() {
   536  				val := d.Val()
   537  				num, err := strconv.ParseUint(val, 10, 16)
   538  				if err != nil {
   539  					return d.Errf("parsing %s option '%s': %v", wrapper, optionName, err)
   540  				}
   541  				m.CookiePorts = append(m.CookiePorts, uint16(num))
   542  			}
   543  			hasCookieIPOrPort = true
   544  		case "custom_info":
   545  			if hasCookieHash || hasCookieIPOrPort {
   546  				return d.Errf("%s option '%s' can't be combined with other options", wrapper, optionName)
   547  			}
   548  			if hasCustomInfo {
   549  				return d.Errf("duplicate %s option '%s'", wrapper, optionName)
   550  			}
   551  			if d.CountRemainingArgs() != 1 {
   552  				return d.ArgErr()
   553  			}
   554  			_, val := d.NextArg(), d.Val()
   555  			m.CustomInfo, hasCustomInfo = val, true
   556  		case "custom_info_regexp":
   557  			if hasCookieHash || hasCookieIPOrPort {
   558  				return d.Errf("%s option '%s' can't be combined with other options", wrapper, optionName)
   559  			}
   560  			if hasCustomInfo {
   561  				return d.Errf("duplicate %s option '%s'", wrapper, optionName)
   562  			}
   563  			if d.CountRemainingArgs() != 1 {
   564  				return d.ArgErr()
   565  			}
   566  			_, val := d.NextArg(), d.Val()
   567  			m.CustomInfoRegexp, hasCustomInfo = val, true
   568  		default:
   569  			return d.ArgErr()
   570  		}
   571  
   572  		// No nested blocks are supported
   573  		if d.NextBlock(nesting + 1) {
   574  			return d.Errf("malformed %s option '%s': blocks are not supported", wrapper, optionName)
   575  		}
   576  	}
   577  
   578  	return nil
   579  }
   580  
   581  type RDPCorrInfo struct {
   582  	Type     uint8
   583  	Flags    uint8
   584  	Length   uint16
   585  	Identity [16]uint8
   586  	Reserved [16]uint8
   587  }
   588  
   589  func (i *RDPCorrInfo) FromBytes(src []byte) error {
   590  	return binary.Read(bytes.NewBuffer(src), RDPCorrInfoBytesOrder, i)
   591  }
   592  
   593  func (i *RDPCorrInfo) ToBytes() ([]byte, error) {
   594  	dst := bytes.NewBuffer(make([]byte, 0, RDPCorrInfoBytesTotal))
   595  	err := binary.Write(dst, RDPCorrInfoBytesOrder, i)
   596  	return dst.Bytes(), err
   597  }
   598  
   599  type RDPNegReq struct {
   600  	Type      uint8
   601  	Flags     uint8
   602  	Length    uint16
   603  	Protocols uint32
   604  }
   605  
   606  func (r *RDPNegReq) FromBytes(src []byte) error {
   607  	return binary.Read(bytes.NewBuffer(src), RDPNegReqBytesOrder, r)
   608  }
   609  
   610  func (r *RDPNegReq) ToBytes() ([]byte, error) {
   611  	dst := bytes.NewBuffer(make([]byte, 0, RDPNegReqBytesTotal))
   612  	err := binary.Write(dst, RDPNegReqBytesOrder, r)
   613  	return dst.Bytes(), err
   614  }
   615  
   616  type RDPToken struct {
   617  	Version         uint8
   618  	Reserved        uint8
   619  	Length          uint16
   620  	LengthIndicator uint8
   621  	TypeCredit      uint8
   622  	DstRef          uint16
   623  	SrcRef          uint16
   624  	ClassOptions    uint8
   625  	Optional        []uint8
   626  }
   627  
   628  func (t *RDPToken) FromBytes(src []byte) error {
   629  	buf := bytes.NewBuffer(src)
   630  	if err := binary.Read(buf, RDPTokenBytesOrder, &t.Version); err != nil {
   631  		return err
   632  	}
   633  	if err := binary.Read(buf, RDPTokenBytesOrder, &t.Reserved); err != nil {
   634  		return err
   635  	}
   636  	if err := binary.Read(buf, RDPTokenBytesOrder, &t.Length); err != nil {
   637  		return err
   638  	}
   639  	if err := binary.Read(buf, RDPTokenBytesOrder, &t.LengthIndicator); err != nil {
   640  		return err
   641  	}
   642  	if err := binary.Read(buf, RDPTokenBytesOrder, &t.TypeCredit); err != nil {
   643  		return err
   644  	}
   645  	if err := binary.Read(buf, RDPTokenBytesOrder, &t.DstRef); err != nil {
   646  		return err
   647  	}
   648  	if err := binary.Read(buf, RDPTokenBytesOrder, &t.SrcRef); err != nil {
   649  		return err
   650  	}
   651  	if err := binary.Read(buf, RDPTokenBytesOrder, &t.ClassOptions); err != nil {
   652  		return err
   653  	}
   654  	if buf.Len() > 0 {
   655  		t.Optional = append(t.Optional, buf.Bytes()...)
   656  	}
   657  	return nil
   658  }
   659  
   660  func (t *RDPToken) ToBytes() ([]byte, error) {
   661  	dst := bytes.NewBuffer(make([]byte, 0, RDPTokenBytesMin+uint16(len(t.Optional))))
   662  	if err := binary.Write(dst, RDPTokenBytesOrder, &t.Version); err != nil {
   663  		return nil, err
   664  	}
   665  	if err := binary.Write(dst, RDPTokenBytesOrder, &t.Reserved); err != nil {
   666  		return nil, err
   667  	}
   668  	if err := binary.Write(dst, RDPTokenBytesOrder, &t.Length); err != nil {
   669  		return nil, err
   670  	}
   671  	if err := binary.Write(dst, RDPTokenBytesOrder, &t.LengthIndicator); err != nil {
   672  		return nil, err
   673  	}
   674  	if err := binary.Write(dst, RDPTokenBytesOrder, &t.TypeCredit); err != nil {
   675  		return nil, err
   676  	}
   677  	if err := binary.Write(dst, RDPTokenBytesOrder, &t.DstRef); err != nil {
   678  		return nil, err
   679  	}
   680  	if err := binary.Write(dst, RDPTokenBytesOrder, &t.SrcRef); err != nil {
   681  		return nil, err
   682  	}
   683  	if err := binary.Write(dst, RDPTokenBytesOrder, &t.ClassOptions); err != nil {
   684  		return nil, err
   685  	}
   686  	return append(dst.Bytes(), t.Optional...), nil
   687  }
   688  
   689  type TPKTHeader struct {
   690  	Version  byte
   691  	Reserved byte
   692  	Length   uint16
   693  }
   694  
   695  func (h *TPKTHeader) FromBytes(src []byte) error {
   696  	return binary.Read(bytes.NewBuffer(src), TPKTHeaderBytesOrder, h)
   697  }
   698  
   699  func (h *TPKTHeader) ToBytes() ([]byte, error) {
   700  	dst := bytes.NewBuffer(make([]byte, 0, TPKTHeaderBytesTotal))
   701  	err := binary.Write(dst, TPKTHeaderBytesOrder, h)
   702  	return dst.Bytes(), err
   703  }
   704  
   705  type X224Crq struct {
   706  	Length       uint8
   707  	TypeCredit   uint8
   708  	DstRef       uint16
   709  	SrcRef       uint16
   710  	ClassOptions uint8
   711  }
   712  
   713  func (x *X224Crq) FromBytes(src []byte) error {
   714  	return binary.Read(bytes.NewBuffer(src), X224CrqBytesOrder, x)
   715  }
   716  
   717  func (x *X224Crq) ToBytes() ([]byte, error) {
   718  	dst := bytes.NewBuffer(make([]byte, 0, X224CrqBytesTotal))
   719  	err := binary.Write(dst, X224CrqBytesOrder, x)
   720  	return dst.Bytes(), err
   721  }
   722  
   723  // Interface guards
   724  var (
   725  	_ caddy.Provisioner     = (*MatchRDP)(nil)
   726  	_ caddyfile.Unmarshaler = (*MatchRDP)(nil)
   727  	_ layer4.ConnMatcher    = (*MatchRDP)(nil)
   728  )
   729  
   730  // Constants specific to RDP Connection Request. Packet structure is described in the comments below.
   731  const (
   732  	ASCIIByteCR uint8 = 0x0D
   733  	ASCIIByteLF uint8 = 0x0A
   734  
   735  	RDPCookieBytesMax            = uint16(X224CrqLengthMax) - (X224CrqBytesTotal - 1)
   736  	RDPCookieBytesMin            = uint16(len(RDPCookiePrefix)) + 1 + 2 // 2 bytes for CR LF and at least 1 character
   737  	RDPCookieBytesStart   uint16 = 0
   738  	RDPCookieHashBytesMax        = RDPCookieBytesMax - (RDPCookieBytesMin - 1)
   739  	RDPCookiePrefix              = "Cookie: mstshash="
   740  
   741  	RDPCorrInfoBytesTotal uint16 = 36
   742  	RDPCorrInfoType       uint8  = 0x06
   743  	RDPCorrInfoFlags      uint8  = 0x00
   744  	RDPCorrInfoLength            = RDPCorrInfoBytesTotal
   745  	RDPCorrInfoIdentityF4 uint8  = 0xF4
   746  	RDPCorrInfoReserved   uint8  = 0x00
   747  
   748  	RDPCustomBytesMax              = uint16(X224CrqLengthMax) - (X224CrqBytesTotal - 1)
   749  	RDPCustomBytesMin       uint16 = 1 + 2 // 2 bytes for CR LF and at least 1 character
   750  	RDPCustomBytesStart     uint16 = 0
   751  	RDPCustomInfoBytesMax          = RDPCustomBytesMax - (RDPCustomBytesMin - 1)
   752  	RDPCustomInfoBytesStart uint16 = 0
   753  
   754  	RDPNegReqBytesTotal    uint16 = 8
   755  	RDPNegReqType          uint8  = 0x01
   756  	RDPNegReqFlagAdminMode uint8  = 0x01
   757  	RDPNegReqFlagAuthMode  uint8  = 0x02
   758  	RDPNegReqFlagCorrInfo  uint8  = 0x08
   759  	RDPNegReqFlagsAll             = RDPNegReqFlagAdminMode | RDPNegReqFlagAuthMode | RDPNegReqFlagCorrInfo
   760  	RDPNegReqLength               = RDPNegReqBytesTotal
   761  	RDPNegReqProtoStandard uint32 = 0x00000000
   762  	RDPNegReqProtoSSL      uint32 = 0x00000001
   763  	RDPNegReqProtoHybrid   uint32 = 0x00000002
   764  	RDPNegReqProtoRDSTLS   uint32 = 0x00000004
   765  	RDPNegReqProtoHybridEx uint32 = 0x00000008
   766  	RDPNegReqProtoRDSAAD   uint32 = 0x00000010
   767  	RDPNegReqProtocolsAll         = RDPNegReqProtoStandard | RDPNegReqProtoSSL | RDPNegReqProtoHybrid |
   768  		RDPNegReqProtoRDSTLS | RDPNegReqProtoHybridEx | RDPNegReqProtoRDSAAD
   769  
   770  	RDPTokenBytesMin               uint16 = 11
   771  	RDPTokenBytesStart             uint16 = 0
   772  	RDPTokenVersion                uint8  = 0x03
   773  	RDPTokenReserved               uint8  = 0x00
   774  	RDPTokenOptionalCookieBytesMax        = uint16(len(RDPTokenOptionalCookiePrefix)) +
   775  		10 + // decimal representation of 2^32 has 10 digits, so 10 bytes are required at most
   776  		2 + // 2 bytes for separators
   777  		5 + // decimal representation of 2^16 has 5 digits, so 5 bytes are required at most
   778  		4 + // 4 reserved bytes for trailing zeros
   779  		2 + // 2 bytes for CR LF
   780  		0
   781  	RDPTokenOptionalCookieBytesMin = uint16(len(RDPTokenOptionalCookiePrefix)) +
   782  		1 + // at least 1 byte (1 digit) for IP
   783  		2 + // 2 bytes for separators
   784  		1 + // at least 1 byte (1 digit) for port
   785  		4 + // 4 reserved bytes for trailing zeros
   786  		2 + // 2 bytes for CR LF
   787  		0
   788  	RDPTokenOptionalCookieBytesStart uint16 = 0
   789  	RDPTokenOptionalCookiePrefix            = "Cookie: msts="
   790  	RDPTokenOptionalCookieReserved          = "0000"
   791  	RDPTokenOptionalCookieSeparator  uint8  = 0x2E
   792  
   793  	TPKTHeaderBytesStart uint16 = 0
   794  	TPKTHeaderBytesTotal uint16 = 4
   795  	TPKTHeaderReserved   uint8  = 0x00
   796  	TPKTHeaderVersion    uint8  = 0x03
   797  
   798  	X224CrqBytesStart          = TPKTHeaderBytesStart + TPKTHeaderBytesTotal
   799  	X224CrqBytesTotal   uint16 = 7
   800  	X224CrqLengthMax    uint8  = 254  // 255 is reserved for possible extensions
   801  	X224CrqTypeCredit   uint8  = 0xE0 // also known as TPDU code
   802  	X224CrqDstRef       uint16 = 0x0000
   803  	X224CrqSrcRef       uint16 = 0x0000
   804  	X224CrqClassOptions uint8  = 0x00
   805  
   806  	RDPConnReqBytesMax = TPKTHeaderBytesTotal + uint16(X224CrqLengthMax) + 1 // 1 byte for X224Crq.Length
   807  	RDPConnReqBytesMin = TPKTHeaderBytesTotal + X224CrqBytesTotal
   808  )
   809  
   810  // Variables specific to RDP Connection Request. Packet structure is described in the comments below.
   811  var (
   812  	RDPCorrInfoBytesOrder = binary.LittleEndian
   813  	RDPNegReqBytesOrder   = binary.LittleEndian
   814  	RDPTokenBytesOrder    = binary.BigEndian
   815  	TPKTHeaderBytesOrder  = binary.BigEndian
   816  	X224CrqBytesOrder     = binary.BigEndian
   817  )
   818  
   819  // Remote Desktop Protocol (RDP)
   820  // ref: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/d2a48824-e362-4ed1-bda8-0eb7cbb28b8c
   821  // ref: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/18a27ef9-6f9a-4501-b000-94b1fe3c2c10
   822  // ref: https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-RDPBCGR/%5BMS-RDPBCGR%5D.pdf
   823  // X.224 CR PDU, a packet each RDP connections begins with, has at least 11 bytes and may contain 6 elements:
   824  //
   825  //	ref: https://go.microsoft.com/fwlink/?LinkId=90541
   826  //	1. MANDATORY tpktHeader (4 bytes):
   827  //		tpktHeader.version (1 byte) must equal
   828  //			0x03 = 0b00000011
   829  //		tpktHeader.reserved (1 byte) must equal
   830  //			0x00
   831  //		tpktHeader.length (2 bytes) must equal the total length of tpktHeader, including:
   832  //			tpktHeader, x224Crq, routingToken, cookie, rdpNegReq, rdpCorrelationInfo
   833  //
   834  //	ref: https://go.microsoft.com/fwlink/?LinkId=90588
   835  //	2. MANDATORY x224Crq (7 bytes):
   836  //		x224Crq.length (1 byte) must equal the total length of fixed and variable parts of x224Crq
   837  //			0x0E = 0b00001110 - 14 = tpktHeader.length - 4 - 1
   838  //		x224Crq.TypeCredit (1 byte) must equal
   839  //			0xE0 = 0x11100000
   840  //		x224Crq.dstRef (2 bytes) must equal
   841  //			0x00
   842  //		x224Crq.srcRef (2 bytes) must equal
   843  //			0x00
   844  //		x224Crq.classOptions (1 byte) must equal
   845  //			0x00
   846  //
   847  //	ref: https://go.microsoft.com/fwlink/?LinkId=90204
   848  //	3. OPTIONAL routingToken (variable length; must not be present if cookie is present):
   849  //		routingToken.version (1 byte) must equal
   850  //			0x03
   851  //		routingToken.reserved (1 byte) must equal
   852  //			0x00
   853  //		routingToken.length (2 bytes, big-endian) must equal the total length of routingToken, including:
   854  //			version, reserved, length, lengthIndicator, typeCredit, dstRef, srcRef, classOptions, optional
   855  //		routingToken.lengthIndicator (1 byte) must equal the total length of the following components:
   856  //			typeCredit, dstRef, srcRef, classOptions, optional; i.e. it must be 5 bytes less than length
   857  //		routingToken.typeCredit (1 byte) must equal
   858  //			[???; it must probably equal to x224Crq.typeCredit]
   859  //		routingToken.dstRef (2 bytes) must equal
   860  //			[???; it must probably equal to x224Crq.dstRef]
   861  //		routingToken.srcRef (2 bytes) must equal
   862  //			[???; it must probably equal to x224Crq.srcRef]
   863  //		routingToken.classOptions (1 byte) must equal
   864  //			[???; it must probably equal to x224Crq.classOptions]
   865  //		routingToken.optional (variable length) may contain a cookie (max 37 bytes) formatted as follows:
   866  //			0x436F6F6B69653A206D7374733D (Cookie: msts=)
   867  //			[IP']0x2E[PORT']0x2E[RESERVED] ([number].[number].[number])
   868  //			0x0D0A (CR LF);
   869  //			where decimal IP and PORT values are converted into hex, byte order is reversed,
   870  //			then resulting hex values are converted back into decimals to get IP' and PORT',
   871  //			and RESERVED must equal 0x30303030; see ref for additional guidance on cookie format
   872  //
   873  //	ref: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/cbe1ed0a-d320-4ea5-be5a-f2eb6e032853#Appendix_A_43
   874  //	4. OPTIONAL cookie (ANSI string of variable length, max 28 bytes; must not be present if routingToken is present;
   875  //	all Microsoft RDP clients >5.0 include cookie, if a username is specified before connecting):
   876  //		0x436F6F6B69653A206D737473686173683D (Cookie: mstshash=)
   877  //		[IDENTIFIER]
   878  //		0x0D0A (CR LF);
   879  //		where IDENTIFIER can be a "domain/username" string truncated to 9 symbols for a native client (mstsc.exe),
   880  //		and an intact "username" string for Apache Guacamole (unless a load balance token/info field is set)
   881  //
   882  // 	ref: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/902b090b-9cb3-4efc-92bf-ee13373371e3
   883  //	5. OPTIONAL rdpNegReq (8 bytes):
   884  //		rdpNegReq.type (1 byte) must equal
   885  //			0x01 (TYPE_RDP_NEG_REQ)
   886  //		rdpNegReq.flags (1 byte) contains the following flags:
   887  //			0x01 (RESTRICTED_ADMIN_MODE_REQUIRED)
   888  //			0x02 (REDIRECTED_AUTHENTICATION_MODE_REQUIRED)
   889  //			0x08 (CORRELATION_INFO_PRESENT)
   890  //		rdpNegReq.length (2 bytes) must equal
   891  //			0x0008 - 8 bytes in total
   892  //		rdpNegReq.requestedProtocols (4 bytes) contains the following flags:
   893  //			0x00000000 (PROTOCOL_RDP) - Standard RDP Security
   894  //			0x00000001 (PROTOCOL_SSL) - TLS 1.0, 1.1, or 1.2
   895  //			0x00000002 (PROTOCOL_HYBRID) - CredSSP, requires PROTOCOL_SSL flag
   896  //			0x00000004 (PROTOCOL_RDSTLS) - RDSTLS protocol
   897  //			0x00000008 (PROTOCOL_HYBRID_EX) - CredSSP with EUAR PDU, requires PROTOCOL_HYBRID flag
   898  //			0x00000010 (PROTOCOL_RDSAAD) - RDS-AAD-Auth Security
   899  //
   900  //	6. OPTIONAL rdpCorrelationInfo (36 bytes; must only be present if CORRELATION_INFO_PRESENT is set in rdpNegReq.flags):
   901  //		rdpCorrelationInfo.type (1 byte) must equal
   902  //			0x06 (TYPE_RDP_CORRELATION_INFO)
   903  //		rdpCorrelationInfo.flags (1 byte) must equal
   904  //			0x00
   905  //		rdpCorrelationInfo.length (2 bytes) must equal
   906  //			0x0024 - 36 bytes in total
   907  //		rdpCorrelationInfo.correlationId (16 bytes) - a unique identifier to associate with the connection;
   908  //		the first byte SHOULD NOT have a value of 0x00 or 0xF4 and the value 0x0D SHOULD NOT be present at all
   909  //		rdpCorrelationInfo.reserved (16 bytes) must equal
   910  //			16x[0x00] - all bytes are zeroed