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