├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE.txt ├── README.md ├── extra ├── README.md ├── tarssh ├── tarssh.1 ├── tarssh.service └── tarssh_log_stats.rb └── src ├── elapsed.rs ├── main.rs ├── peer_addr.rs └── retain_unordered.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "0.7.18" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "ansi_term" 16 | version = "0.12.1" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" 19 | dependencies = [ 20 | "winapi", 21 | ] 22 | 23 | [[package]] 24 | name = "async-stream" 25 | version = "0.3.3" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "dad5c83079eae9969be7fadefe640a1c566901f05ff91ab221de4b6f68d9507e" 28 | dependencies = [ 29 | "async-stream-impl", 30 | "futures-core", 31 | ] 32 | 33 | [[package]] 34 | name = "async-stream-impl" 35 | version = "0.3.3" 36 | source = "registry+https://github.com/rust-lang/crates.io-index" 37 | checksum = "10f203db73a71dfa2fb6dd22763990fa26f3d2625a6da2da900d23b87d26be27" 38 | dependencies = [ 39 | "proc-macro2", 40 | "quote", 41 | "syn", 42 | ] 43 | 44 | [[package]] 45 | name = "atty" 46 | version = "0.2.14" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 49 | dependencies = [ 50 | "hermit-abi", 51 | "libc", 52 | "winapi", 53 | ] 54 | 55 | [[package]] 56 | name = "autocfg" 57 | version = "1.1.0" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 60 | 61 | [[package]] 62 | name = "bitflags" 63 | version = "1.3.2" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 66 | 67 | [[package]] 68 | name = "bytes" 69 | version = "1.1.0" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" 72 | 73 | [[package]] 74 | name = "cfg-if" 75 | version = "0.1.10" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 78 | 79 | [[package]] 80 | name = "cfg-if" 81 | version = "1.0.0" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 84 | 85 | [[package]] 86 | name = "clap" 87 | version = "2.34.0" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" 90 | dependencies = [ 91 | "ansi_term", 92 | "atty", 93 | "bitflags", 94 | "strsim", 95 | "textwrap", 96 | "unicode-width", 97 | "vec_map", 98 | ] 99 | 100 | [[package]] 101 | name = "env_logger" 102 | version = "0.8.4" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" 105 | dependencies = [ 106 | "log", 107 | "regex", 108 | ] 109 | 110 | [[package]] 111 | name = "env_logger" 112 | version = "0.9.0" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" 115 | dependencies = [ 116 | "atty", 117 | "humantime", 118 | "log", 119 | "regex", 120 | "termcolor", 121 | ] 122 | 123 | [[package]] 124 | name = "exitcode" 125 | version = "1.1.2" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "de853764b47027c2e862a995c34978ffa63c1501f2e15f987ba11bd4f9bba193" 128 | 129 | [[package]] 130 | name = "futures" 131 | version = "0.3.21" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" 134 | dependencies = [ 135 | "futures-channel", 136 | "futures-core", 137 | "futures-executor", 138 | "futures-io", 139 | "futures-sink", 140 | "futures-task", 141 | "futures-util", 142 | ] 143 | 144 | [[package]] 145 | name = "futures-channel" 146 | version = "0.3.21" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" 149 | dependencies = [ 150 | "futures-core", 151 | "futures-sink", 152 | ] 153 | 154 | [[package]] 155 | name = "futures-core" 156 | version = "0.3.21" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" 159 | 160 | [[package]] 161 | name = "futures-executor" 162 | version = "0.3.21" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" 165 | dependencies = [ 166 | "futures-core", 167 | "futures-task", 168 | "futures-util", 169 | ] 170 | 171 | [[package]] 172 | name = "futures-io" 173 | version = "0.3.21" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" 176 | 177 | [[package]] 178 | name = "futures-macro" 179 | version = "0.3.21" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" 182 | dependencies = [ 183 | "proc-macro2", 184 | "quote", 185 | "syn", 186 | ] 187 | 188 | [[package]] 189 | name = "futures-sink" 190 | version = "0.3.21" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" 193 | 194 | [[package]] 195 | name = "futures-task" 196 | version = "0.3.21" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" 199 | 200 | [[package]] 201 | name = "futures-util" 202 | version = "0.3.21" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" 205 | dependencies = [ 206 | "futures-channel", 207 | "futures-core", 208 | "futures-io", 209 | "futures-macro", 210 | "futures-sink", 211 | "futures-task", 212 | "memchr", 213 | "pin-project-lite", 214 | "pin-utils", 215 | "slab", 216 | ] 217 | 218 | [[package]] 219 | name = "getrandom" 220 | version = "0.2.6" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" 223 | dependencies = [ 224 | "cfg-if 1.0.0", 225 | "libc", 226 | "wasi 0.10.2+wasi-snapshot-preview1", 227 | ] 228 | 229 | [[package]] 230 | name = "heck" 231 | version = "0.3.3" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" 234 | dependencies = [ 235 | "unicode-segmentation", 236 | ] 237 | 238 | [[package]] 239 | name = "hermit-abi" 240 | version = "0.1.19" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 243 | dependencies = [ 244 | "libc", 245 | ] 246 | 247 | [[package]] 248 | name = "humantime" 249 | version = "2.1.0" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 252 | 253 | [[package]] 254 | name = "lazy_static" 255 | version = "1.4.0" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 258 | 259 | [[package]] 260 | name = "libc" 261 | version = "0.2.126" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" 264 | 265 | [[package]] 266 | name = "log" 267 | version = "0.4.17" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 270 | dependencies = [ 271 | "cfg-if 1.0.0", 272 | ] 273 | 274 | [[package]] 275 | name = "memchr" 276 | version = "2.5.0" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 279 | 280 | [[package]] 281 | name = "memoffset" 282 | version = "0.6.5" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" 285 | dependencies = [ 286 | "autocfg", 287 | ] 288 | 289 | [[package]] 290 | name = "mio" 291 | version = "0.8.3" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | checksum = "713d550d9b44d89174e066b7a6217ae06234c10cb47819a88290d2b353c31799" 294 | dependencies = [ 295 | "libc", 296 | "log", 297 | "wasi 0.11.0+wasi-snapshot-preview1", 298 | "windows-sys", 299 | ] 300 | 301 | [[package]] 302 | name = "nix" 303 | version = "0.24.1" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "8f17df307904acd05aa8e32e97bb20f2a0df1728bbc2d771ae8f9a90463441e9" 306 | dependencies = [ 307 | "bitflags", 308 | "cfg-if 1.0.0", 309 | "libc", 310 | "memoffset", 311 | ] 312 | 313 | [[package]] 314 | name = "once_cell" 315 | version = "1.12.0" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225" 318 | 319 | [[package]] 320 | name = "pin-project-lite" 321 | version = "0.2.9" 322 | source = "registry+https://github.com/rust-lang/crates.io-index" 323 | checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" 324 | 325 | [[package]] 326 | name = "pin-utils" 327 | version = "0.1.0" 328 | source = "registry+https://github.com/rust-lang/crates.io-index" 329 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 330 | 331 | [[package]] 332 | name = "privdrop" 333 | version = "0.5.2" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "ad5b1f7e40f628a2f8f90e40d3f313be83066cc61997fdcb96cade6abf7cee93" 336 | dependencies = [ 337 | "libc", 338 | "nix", 339 | ] 340 | 341 | [[package]] 342 | name = "proc-macro-error" 343 | version = "1.0.4" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 346 | dependencies = [ 347 | "proc-macro-error-attr", 348 | "proc-macro2", 349 | "quote", 350 | "syn", 351 | "version_check", 352 | ] 353 | 354 | [[package]] 355 | name = "proc-macro-error-attr" 356 | version = "1.0.4" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 359 | dependencies = [ 360 | "proc-macro2", 361 | "quote", 362 | "version_check", 363 | ] 364 | 365 | [[package]] 366 | name = "proc-macro2" 367 | version = "1.0.39" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" 370 | dependencies = [ 371 | "unicode-ident", 372 | ] 373 | 374 | [[package]] 375 | name = "quickcheck" 376 | version = "1.0.3" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" 379 | dependencies = [ 380 | "env_logger 0.8.4", 381 | "log", 382 | "rand", 383 | ] 384 | 385 | [[package]] 386 | name = "quote" 387 | version = "1.0.18" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" 390 | dependencies = [ 391 | "proc-macro2", 392 | ] 393 | 394 | [[package]] 395 | name = "rand" 396 | version = "0.8.5" 397 | source = "registry+https://github.com/rust-lang/crates.io-index" 398 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 399 | dependencies = [ 400 | "rand_core", 401 | ] 402 | 403 | [[package]] 404 | name = "rand_core" 405 | version = "0.6.3" 406 | source = "registry+https://github.com/rust-lang/crates.io-index" 407 | checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" 408 | dependencies = [ 409 | "getrandom", 410 | ] 411 | 412 | [[package]] 413 | name = "regex" 414 | version = "1.5.6" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" 417 | dependencies = [ 418 | "aho-corasick", 419 | "memchr", 420 | "regex-syntax", 421 | ] 422 | 423 | [[package]] 424 | name = "regex-syntax" 425 | version = "0.6.26" 426 | source = "registry+https://github.com/rust-lang/crates.io-index" 427 | checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" 428 | 429 | [[package]] 430 | name = "rusty-sandbox" 431 | version = "0.2.0" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "052c4bd5534dcd95a75ae138b3e68d0b0a10dea0a4f2cc5321c8793a5c38da7b" 434 | dependencies = [ 435 | "libc", 436 | "unix_socket", 437 | ] 438 | 439 | [[package]] 440 | name = "signal-hook-registry" 441 | version = "1.4.0" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" 444 | dependencies = [ 445 | "libc", 446 | ] 447 | 448 | [[package]] 449 | name = "slab" 450 | version = "0.4.6" 451 | source = "registry+https://github.com/rust-lang/crates.io-index" 452 | checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" 453 | 454 | [[package]] 455 | name = "socket2" 456 | version = "0.4.4" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" 459 | dependencies = [ 460 | "libc", 461 | "winapi", 462 | ] 463 | 464 | [[package]] 465 | name = "strsim" 466 | version = "0.8.0" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 469 | 470 | [[package]] 471 | name = "structopt" 472 | version = "0.3.26" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" 475 | dependencies = [ 476 | "clap", 477 | "lazy_static", 478 | "structopt-derive", 479 | ] 480 | 481 | [[package]] 482 | name = "structopt-derive" 483 | version = "0.4.18" 484 | source = "registry+https://github.com/rust-lang/crates.io-index" 485 | checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" 486 | dependencies = [ 487 | "heck", 488 | "proc-macro-error", 489 | "proc-macro2", 490 | "quote", 491 | "syn", 492 | ] 493 | 494 | [[package]] 495 | name = "syn" 496 | version = "1.0.96" 497 | source = "registry+https://github.com/rust-lang/crates.io-index" 498 | checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf" 499 | dependencies = [ 500 | "proc-macro2", 501 | "quote", 502 | "unicode-ident", 503 | ] 504 | 505 | [[package]] 506 | name = "tarssh" 507 | version = "0.6.0" 508 | dependencies = [ 509 | "async-stream", 510 | "env_logger 0.9.0", 511 | "exitcode", 512 | "futures", 513 | "futures-util", 514 | "log", 515 | "privdrop", 516 | "quickcheck", 517 | "rusty-sandbox", 518 | "structopt", 519 | "tokio", 520 | "tokio-stream", 521 | ] 522 | 523 | [[package]] 524 | name = "termcolor" 525 | version = "1.1.3" 526 | source = "registry+https://github.com/rust-lang/crates.io-index" 527 | checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" 528 | dependencies = [ 529 | "winapi-util", 530 | ] 531 | 532 | [[package]] 533 | name = "textwrap" 534 | version = "0.11.0" 535 | source = "registry+https://github.com/rust-lang/crates.io-index" 536 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 537 | dependencies = [ 538 | "unicode-width", 539 | ] 540 | 541 | [[package]] 542 | name = "tokio" 543 | version = "1.19.2" 544 | source = "registry+https://github.com/rust-lang/crates.io-index" 545 | checksum = "c51a52ed6686dd62c320f9b89299e9dfb46f730c7a48e635c19f21d116cb1439" 546 | dependencies = [ 547 | "bytes", 548 | "libc", 549 | "memchr", 550 | "mio", 551 | "once_cell", 552 | "pin-project-lite", 553 | "signal-hook-registry", 554 | "socket2", 555 | "tokio-macros", 556 | "winapi", 557 | ] 558 | 559 | [[package]] 560 | name = "tokio-macros" 561 | version = "1.8.0" 562 | source = "registry+https://github.com/rust-lang/crates.io-index" 563 | checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" 564 | dependencies = [ 565 | "proc-macro2", 566 | "quote", 567 | "syn", 568 | ] 569 | 570 | [[package]] 571 | name = "tokio-stream" 572 | version = "0.1.9" 573 | source = "registry+https://github.com/rust-lang/crates.io-index" 574 | checksum = "df54d54117d6fdc4e4fea40fe1e4e566b3505700e148a6827e59b34b0d2600d9" 575 | dependencies = [ 576 | "futures-core", 577 | "pin-project-lite", 578 | "tokio", 579 | ] 580 | 581 | [[package]] 582 | name = "unicode-ident" 583 | version = "1.0.0" 584 | source = "registry+https://github.com/rust-lang/crates.io-index" 585 | checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" 586 | 587 | [[package]] 588 | name = "unicode-segmentation" 589 | version = "1.9.0" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" 592 | 593 | [[package]] 594 | name = "unicode-width" 595 | version = "0.1.9" 596 | source = "registry+https://github.com/rust-lang/crates.io-index" 597 | checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" 598 | 599 | [[package]] 600 | name = "unix_socket" 601 | version = "0.5.0" 602 | source = "registry+https://github.com/rust-lang/crates.io-index" 603 | checksum = "6aa2700417c405c38f5e6902d699345241c28c0b7ade4abaad71e35a87eb1564" 604 | dependencies = [ 605 | "cfg-if 0.1.10", 606 | "libc", 607 | ] 608 | 609 | [[package]] 610 | name = "vec_map" 611 | version = "0.8.2" 612 | source = "registry+https://github.com/rust-lang/crates.io-index" 613 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 614 | 615 | [[package]] 616 | name = "version_check" 617 | version = "0.9.4" 618 | source = "registry+https://github.com/rust-lang/crates.io-index" 619 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 620 | 621 | [[package]] 622 | name = "wasi" 623 | version = "0.10.2+wasi-snapshot-preview1" 624 | source = "registry+https://github.com/rust-lang/crates.io-index" 625 | checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" 626 | 627 | [[package]] 628 | name = "wasi" 629 | version = "0.11.0+wasi-snapshot-preview1" 630 | source = "registry+https://github.com/rust-lang/crates.io-index" 631 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 632 | 633 | [[package]] 634 | name = "winapi" 635 | version = "0.3.9" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 638 | dependencies = [ 639 | "winapi-i686-pc-windows-gnu", 640 | "winapi-x86_64-pc-windows-gnu", 641 | ] 642 | 643 | [[package]] 644 | name = "winapi-i686-pc-windows-gnu" 645 | version = "0.4.0" 646 | source = "registry+https://github.com/rust-lang/crates.io-index" 647 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 648 | 649 | [[package]] 650 | name = "winapi-util" 651 | version = "0.1.5" 652 | source = "registry+https://github.com/rust-lang/crates.io-index" 653 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 654 | dependencies = [ 655 | "winapi", 656 | ] 657 | 658 | [[package]] 659 | name = "winapi-x86_64-pc-windows-gnu" 660 | version = "0.4.0" 661 | source = "registry+https://github.com/rust-lang/crates.io-index" 662 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 663 | 664 | [[package]] 665 | name = "windows-sys" 666 | version = "0.36.1" 667 | source = "registry+https://github.com/rust-lang/crates.io-index" 668 | checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" 669 | dependencies = [ 670 | "windows_aarch64_msvc", 671 | "windows_i686_gnu", 672 | "windows_i686_msvc", 673 | "windows_x86_64_gnu", 674 | "windows_x86_64_msvc", 675 | ] 676 | 677 | [[package]] 678 | name = "windows_aarch64_msvc" 679 | version = "0.36.1" 680 | source = "registry+https://github.com/rust-lang/crates.io-index" 681 | checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" 682 | 683 | [[package]] 684 | name = "windows_i686_gnu" 685 | version = "0.36.1" 686 | source = "registry+https://github.com/rust-lang/crates.io-index" 687 | checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" 688 | 689 | [[package]] 690 | name = "windows_i686_msvc" 691 | version = "0.36.1" 692 | source = "registry+https://github.com/rust-lang/crates.io-index" 693 | checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" 694 | 695 | [[package]] 696 | name = "windows_x86_64_gnu" 697 | version = "0.36.1" 698 | source = "registry+https://github.com/rust-lang/crates.io-index" 699 | checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" 700 | 701 | [[package]] 702 | name = "windows_x86_64_msvc" 703 | version = "0.36.1" 704 | source = "registry+https://github.com/rust-lang/crates.io-index" 705 | checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" 706 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tarssh" 3 | version = "0.6.0" 4 | description = "A simple SSH tarpit server" 5 | authors = ["Thomas Hurst "] 6 | edition = "2018" 7 | license = "MIT" 8 | repository = "https://github.com/Freaky/tarssh" 9 | documentation ="https://docs.rs/tarssh" 10 | keywords = ["ssh", "tarpit", "security", "server"] 11 | readme = "README.md" 12 | 13 | [features] 14 | default = ["sandbox", "drop_privs"] 15 | sandbox = ["rusty-sandbox"] 16 | drop_privs = ["privdrop"] 17 | 18 | [dependencies] 19 | env_logger = "0.9" 20 | exitcode = "1.1" 21 | futures = "0.3" 22 | futures-util = "0.3" 23 | log = "0.4" 24 | structopt = "0.3" 25 | tokio = { version = "1.0", features = ["io-util", "macros", "net", "rt", "signal", "sync", "time"] } 26 | tokio-stream = { version = "0.1.1", features = ["net", "time"] } 27 | async-stream = "0.3.0" 28 | 29 | [target."cfg(unix)".dependencies] 30 | rusty-sandbox = { version = "0.2", optional = true } 31 | privdrop = { version = "0.5", optional = true } 32 | 33 | [dev-dependencies] 34 | quickcheck = "1.0" 35 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1-slim-buster as build 2 | 3 | WORKDIR /usr/src/tarssh 4 | 5 | # Make a blank project with our deps for Docker to cache. 6 | # We skip rusty-sandbox because it does nothing useful on Linux. 7 | COPY Cargo.toml Cargo.lock ./ 8 | RUN mkdir -p src \ 9 | && echo 'fn main() { }' >src/main.rs \ 10 | && cargo build --release --no-default-features --features drop_privs \ 11 | && rm -r target/release/.fingerprint/tarssh-* 12 | 13 | # Copy in the full project and build 14 | COPY . . 15 | RUN cargo build --release --no-default-features --features drop_privs 16 | 17 | # Use a fairly minimal enviroment for deployment 18 | FROM debian:buster-slim 19 | 20 | RUN mkdir /var/empty && chmod 0555 /var/empty 21 | COPY --from=build /usr/src/tarssh/target/release/tarssh /opt/tarssh 22 | 23 | EXPOSE 22 24 | 25 | ENTRYPOINT [ "/opt/tarssh" ] 26 | CMD [ "-v", "--user=nobody", "--chroot=/var/empty", "--listen=0.0.0.0:22" ] 27 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2019 Thomas Hurst 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Cargo](https://img.shields.io/crates/v/tarssh.svg)][crate] 2 | 3 | # tarssh 4 | 5 | A simple SSH tarpit, similar to [endlessh](https://nullprogram.com/blog/2019/03/22/). 6 | 7 | As per [RFC 4253](https://tools.ietf.org/html/rfc4253#page-4): 8 | 9 | ```txt 10 | The server MAY send other lines of data before sending the version 11 | string. Each line SHOULD be terminated by a Carriage Return and Line 12 | Feed. Such lines MUST NOT begin with "SSH-", and SHOULD be encoded 13 | in ISO-10646 UTF-8 [RFC3629] (language is not specified). Clients 14 | MUST be able to process such lines. 15 | ``` 16 | 17 | In other words, you can fool SSH clients into waiting an extremely long time for 18 | a SSH handshake to even begin simply by waffling on endlessly. My high score is 19 | just over a fortnight. 20 | 21 | The intent of this is to increase the cost of mass SSH scanning - even clients 22 | that immediately disconnect after the first response are delayed a little, and 23 | that's one less free connection for the next attack. 24 | 25 | ## Usage 26 | 27 | ```console 28 | -% cargo install tarssh 29 | -% tarssh --help 30 | tarssh 0.7.0 31 | A SSH tarpit server 32 | 33 | USAGE: 34 | tarssh [FLAGS] [OPTIONS] 35 | 36 | FLAGS: 37 | --disable-log-ident Disable module name in logs (e.g. "tarssh") 38 | --disable-log-level Disable log level in logs (e.g. "info") 39 | --disable-log-timestamps Disable timestamps in logs 40 | -h, --help Prints help information 41 | -V, --version Prints version information 42 | -v, --verbose Verbose level (repeat for more verbosity) 43 | 44 | OPTIONS: 45 | --chroot Chroot to this directory 46 | -d, --delay Seconds between responses [default: 10] 47 | -g, --group Run as this group 48 | -l, --listen ... Listen address(es) to bind to [default: 0.0.0.0:2222] 49 | -c, --max-clients Best-effort connection limit [default: 4096] 50 | -t, --timeout Socket write timeout [default: 30] 51 | -u, --user Run as this user and their primary group 52 | 53 | -% tarssh -v --disable-log-timestamps --disable-log-ident -l 0.0.0.0:2222 \[::]:2222 54 | [INFO ] init, pid: 27344, version: 0.7.0 55 | [INFO ] listen, addr: 0.0.0.0:2222 56 | [INFO ] listen, addr: [::]:2222 57 | [INFO ] privdrop, enabled: false 58 | [INFO ] sandbox, enabled: true 59 | [INFO ] start, servers: 2, max_clients: 4096, delay: 10s, timeout: 30s 60 | [INFO ] connect, peer: 127.0.0.1:61986, clients: 1 61 | [INFO ] connect, peer: 127.0.0.1:61988, clients: 2 62 | load: 1.05 cmd: tarssh 27344 [kqread] 6.92r 0.00u 0.00s 0% 4512k 63 | [INFO ] info, pid: 27344, signal: INFO, uptime: 6.92s, clients: 2, total: 2, bytes: 0 64 | [INFO ] disconnect, peer: 127.0.0.1:61986, duration: 19.80s, bytes: 24, error: "Broken pipe (os error 32)", clients: 1 65 | [INFO ] disconnect, peer: 127.0.0.1:61988, duration: 19.62s, bytes: 24, error: "Broken pipe (os error 32)", clients: 0 66 | ^C[INFO ] shutdown, pid: 27344, signal: INT, uptime: 25.39s, clients: 0, total: 2, bytes: 48 67 | ``` 68 | 69 | The `info` line is generated using a BSD `SIGINFO` signal - `SIGHUP` is also 70 | supported for Unix platforms lacking this. 71 | 72 | 73 | [Tokio]: https://tokio.rs 74 | [rusty-sandbox]: https://github.com/myfreeweb/rusty-sandbox 75 | [privdrop]: https://crates.io/crates/privdrop 76 | [crate]: https://crates.io/crates/tarssh 77 | -------------------------------------------------------------------------------- /extra/README.md: -------------------------------------------------------------------------------- 1 | # `tarssh` extras 2 | 3 | ## `tarssh` 4 | 5 | A FreeBSD rc script, with full `rc.conf` support. 6 | 7 | ## `tarssh.service` 8 | 9 | An example systemd service file. The maintainer of `tarssh` is a FreeBSD user, 10 | and cannot directly vouch for it. 11 | 12 | ## `tarssh_log_stats.rb` 13 | 14 | A simple log parser to generate some statistics on the current run of the server, 15 | giving a breakdown of how many clients have been connected and for how long. 16 | 17 | Eventually this sort of functionality should be exported via a more structured 18 | means from the server itself. 19 | -------------------------------------------------------------------------------- /extra/tarssh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # PROVIDE: tarssh 4 | # REQUIRE: DAEMON 5 | # KEYWORD: shutdown 6 | 7 | # 8 | # Supported options in /etc/rc.conf: 9 | # 10 | # tarssh_enable (bool): Enable tarssh server, set to "YES" to enable 11 | # default: "NO" 12 | # tarssh_log (str): The location for tarssh to log. 13 | # One of "SYSLOG", "NO", or an absolute file path 14 | # default: "SYSLOG" 15 | # tarssh_listen (str): Listen on given host:port pairs 16 | # default: "0.0.0.0:22 [::]:22" 17 | # tarssh_timeout (int): Network timeout in seconds 18 | # default: 30 19 | # tarssh_delay (int): Time between tarpit messages in seconds 20 | # default: 10 21 | # tarssh_max_clients (int): Maximum client count 22 | # default: 1024 23 | # tarssh_daemon_user (str): User to switch to after launch 24 | # default: nobody 25 | # tarssh_daemon_chroot (str): Directory to chroot to after launch 26 | # default: "/var/empty" 27 | # 28 | 29 | . /etc/rc.subr 30 | 31 | name="tarssh" 32 | desc="SSH tarpit server" 33 | rcvar="tarssh_enable" 34 | 35 | load_rc_config "${name}" 36 | 37 | pidfile="/var/run/${name}.pid" 38 | procname="/usr/local/sbin/tarssh" 39 | command="/usr/sbin/daemon" 40 | start_cmd=start_cmd 41 | 42 | : "${tarssh_enable:=NO}" 43 | : "${tarssh_log:=SYSLOG}" 44 | : "${tarssh_listen:=0.0.0.0:22 [::]:22}" 45 | : "${tarssh_timeout:=30}" 46 | : "${tarssh_delay:=10}" 47 | : "${tarssh_max_clients:=1024}" 48 | : "${tarssh_daemon_user:=nobody}" 49 | : "${tarssh_daemon_chroot:=/var/empty}" 50 | 51 | start_cmd() 52 | { 53 | check_startmsgs && echo "Starting ${name}." 54 | 55 | case "${tarssh_log}" in 56 | [Ss][Yy][Ss][Ll][Oo][Gg]) # syslog 57 | /usr/sbin/daemon -p "${pidfile}" -c -s info -T "${name}" \ 58 | "${procname}" --disable-log-timestamps --disable-log-level --disable-log-ident -v \ 59 | -l ${tarssh_listen} -c "${tarssh_max_clients}" -d "${tarssh_delay}" \ 60 | -u "${tarssh_daemon_user}" --chroot "${tarssh_daemon_chroot}" 61 | ;; 62 | /*) # absolute path 63 | /usr/sbin/daemon -p "${pidfile}" -c -o "${tarssh_log}" \ 64 | "${procname}" -v --disable-log-level \ 65 | -l ${tarssh_listen} -c "${tarssh_max_clients}" -d "${tarssh_delay}" \ 66 | -u "${tarssh_daemon_user}" --chroot "${tarssh_daemon_chroot}" 67 | ;; 68 | [Nn][Oo]|[Ff][Aa][Ll][Ss][Ee]|[Oo][Ff][Ff]|0) # no logging 69 | /usr/sbin/daemon -p "${pidfile}" -c -f \ 70 | "${procname}" \ 71 | -l ${tarssh_listen} -c "${tarssh_max_clients}" -d "${tarssh_delay}" \ 72 | -u "${tarssh_daemon_user}" --chroot "${tarssh_daemon_chroot}" 73 | ;; 74 | *) 75 | warn "\$tarssh_log is not set properly (one of 'SYSLOG', an absolute path, or 'NO')" 76 | return 1 77 | ;; 78 | esac 79 | } 80 | 81 | run_rc_command "$1" 82 | 83 | -------------------------------------------------------------------------------- /extra/tarssh.1: -------------------------------------------------------------------------------- 1 | .Dd December 14, 2020 2 | .Dt CHECKRESTART 1 3 | .Os 4 | .Sh NAME 5 | .Nm tarssh 6 | .Nd an SSH tarpit 7 | .Sh SYNOPSIS 8 | .Nm 9 | .Op Fl c | -max-clients Ar limit 10 | .Op Fl -chroot Ar directory 11 | .Op Fl d | -delay Ar seconds 12 | .Op Fl -disable-log-ident 13 | .Op Fl -disable-log-level 14 | .Op Fl -disable-log-timestamp 15 | .Op Fl g | -group Ar group 16 | .Op Fl h | -help 17 | .Op Fl l | -listen Ar address 18 | .Op Fl t | -timeout seconds 19 | .Op Fl u | -user Ar user 20 | .Op Fl V | -version 21 | .Op Fl v | -verbose 22 | .Sh DESCRIPTION 23 | .Nm 24 | is a daemon which indefinitely simulates the initial connection handshake of an 25 | SSH server, with the intention of trapping unwelcome clients in an endless loop. 26 | .Pp 27 | The following options are available: 28 | .Bl -tag -width indent 29 | .It Fl -chroot Ar directory 30 | .Xr chroot 2 31 | to the specificed directory on startup. 32 | This option requires root privileges. 33 | Note 34 | .Nm 35 | also supports automatic sandboxing using system-specific mechanisms such as 36 | .Xr capsicum 4 37 | which may also revoke arbitrary filesystem access. 38 | .It Fl d | -delay Ar seconds 39 | Delay each 40 | .Xr write 2 41 | by this many seconds. 42 | Each write attempts to send a single line of text. 43 | .It Fl -disable-log-ident 44 | .It Fl -disable-log-level 45 | .It Fl -disable-log-timestamp 46 | Suppress portions of log output. 47 | .It Fl g | -group Ar group 48 | Switch to the specified group name or gid after binding listen sockets. 49 | .It Fl l | -listen Ar address 50 | Listen on the specified sockets. 51 | Takes multiple arguments and may be provided 52 | more than once. 53 | .It Fl c | -max-clients Ar limit 54 | Limit connections to this many concurrent clients. 55 | .It Fl t | -timeout Ar seconds 56 | Disconnect clients after unsuccessful writes beyond this cutoff 57 | .It Fl u | -user Ar user 58 | Switch to the specified user name or uid after binding listen sockets. 59 | .It Fl h | -help 60 | Print help text. 61 | .It Fl V | -version 62 | Print version information. 63 | .It Fl v | -verbose 64 | Increase verbosity. 65 | May be specified more than once. 66 | No verbose flag logs only errors. 67 | .El 68 | .Pp 69 | All of these options can be set by the provided 70 | .Xr rc 8 71 | script for supported platforms. 72 | See %%PREFIX%%/etc/rc.d/tarssh for details. 73 | .Sh SEE ALSO 74 | .Xr chroot 2 , 75 | .Xr rc.conf 5 , 76 | .Xr sshd 8 77 | .Sh HISTORY 78 | .Nm 79 | was inspired by Endlessh by Christopher Wellons, documented in his blog post at 80 | https://nullprogram.com/blog/2019/03/22/ 81 | .Sh AUTHORS 82 | .An Thomas Hurst Aq tom@hur.st 83 | .Sh BUGS 84 | Like Endlessh, 85 | .Nm 86 | only detects clients that have disconnected from 87 | .Xr write 2 88 | errors, meaning all clients, even those that disconnect instantly, are logged as 89 | being connected for at least one delay cycle. -------------------------------------------------------------------------------- /extra/tarssh.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=tarssh 3 | Documentation=https://github.com/Freaky/tarssh 4 | # If you want to use both, you need to either: 5 | # * Run tarssh on a different port. (But that's stupid.) 6 | # * Run sshd on a different port. (See /etc/ssh/sshd_config) 7 | Conflicts=ssh.service 8 | 9 | [Service] 10 | ExecStart=/usr/local/sbin/tarssh -v -l 0.0.0.0:22 -c 1024 --chroot /tmp/empty/ --user nobody 11 | RestartSec=1min 12 | KillSignal=SIGINT 13 | 14 | [Install] 15 | WantedBy=multi-user.target 16 | -------------------------------------------------------------------------------- /extra/tarssh_log_stats.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'time' 4 | 5 | def ft(t) 6 | if t >= 86400 7 | "%.1fd" % (t / 86400.0) 8 | elsif t >= 3600 9 | "%.1fh" % (t / 3600.0) 10 | elsif t >= 60 11 | "%.1fm" % (t / 60.0) 12 | else 13 | "%.0fs" % t 14 | end 15 | end 16 | 17 | history = [] 18 | clients = {} 19 | startup = nil 20 | 21 | ARGF.each_line do |line| 22 | prefix, data = line.split('] ', 2) 23 | ts = Time.iso8601(prefix[/\d+\S+/]) rescue next 24 | case data 25 | when /^start,/ 26 | clients.clear 27 | history.clear 28 | startup = ts 29 | when /^disconnect,/ 30 | if line =~ /peer: ([^,]+)/ 31 | if (connect_ts = clients.delete($1)) 32 | history << (ts - connect_ts).to_f 33 | else 34 | warn "Can't find #{$1}" 35 | end 36 | else 37 | warn "Can't parse #{line}" 38 | end 39 | when /^connect,/ 40 | if line =~ /peer: ([^,]+)/ 41 | clients[$1] = ts 42 | else 43 | warn "Can't parse #{line}" 44 | end 45 | end 46 | end 47 | 48 | now = Time.new 49 | history += clients.values.map {|v| (now - v).to_f } 50 | history.sort! 51 | 52 | exit if history.empty? 53 | 54 | min = history.first 55 | max = history.last 56 | med = history[history.length / 2] 57 | avg = history.sum.to_f / history.length 58 | stddev = Math.sqrt(history.sum { |i| (i - avg) ** 2 } / (history.length - 1).to_f) 59 | 60 | puts "Uptime: %s" % ft((now - startup)) 61 | puts "Current Clients: #{clients.size} of #{history.size} total" 62 | puts("Connect Times: %s - %s, median %s, avg %s, stddev %s" % [ft(min), ft(max), ft(med), ft(avg), ft(stddev)]) 63 | 64 | log_history = history.group_by {|time| Math.log2(time).ceil } 65 | 66 | puts "Breakdown:" 67 | log_history.keys.sort.each do |k| 68 | a, b = 2 ** (k - 1), 2 ** k 69 | puts("%16s: %d" % ["#{ft(a)} - #{ft(b)}", log_history[k].size]) 70 | end 71 | 72 | puts 73 | puts "Current clients:" 74 | max = clients.keys.map(&:length).max 75 | ts = clients.values.map(&:to_s).map(&:length).max 76 | 77 | puts("%#{max}s | %#{ts}s | %s" % ["Peer", "Timestamp", "Connection time"]) 78 | clients.each do |client, timestamp| 79 | puts("%#{max}s | %s | %s" % [client, timestamp, ft(now - timestamp)]) 80 | end 81 | 82 | -------------------------------------------------------------------------------- /src/elapsed.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::time::{Duration, Instant}; 3 | 4 | /// A tiny type for tracking approximate Durations from a known starting point 5 | /// Wraps every 13.6 years, precision of 1 decisecond (100ms) 6 | #[derive(Copy, Clone)] 7 | pub struct Elapsed(u32); 8 | 9 | impl From for Elapsed { 10 | fn from(start: Instant) -> Self { 11 | let duration = start.elapsed(); 12 | Self(duration.as_secs() as u32 * 10 + (duration.subsec_millis() as f32 / 100.0) as u32) 13 | } 14 | } 15 | 16 | impl From for Duration { 17 | fn from(elapsed: Elapsed) -> Self { 18 | Duration::from_millis(elapsed.0 as u64 * 100) 19 | } 20 | } 21 | 22 | impl Elapsed { 23 | pub fn elapsed(&self, start: Instant) -> Duration { 24 | start.elapsed() - Duration::from(*self) 25 | } 26 | } 27 | 28 | impl fmt::Debug for Elapsed { 29 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 30 | Duration::from(*self).fmt(f) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(feature = "nightly", feature(external_doc))] 2 | #![cfg_attr(feature = "nightly", doc(include = "../README.md"))] 3 | 4 | use std::net::SocketAddr; 5 | use std::time::{Duration, Instant}; 6 | 7 | use futures::stream::{self, SelectAll, StreamExt}; 8 | use log::LevelFilter; 9 | use log::{error, info, warn}; 10 | use structopt::StructOpt; 11 | use tokio::net::{TcpSocket, TcpStream}; 12 | use tokio::time::sleep; 13 | use tokio_stream::wrappers::{IntervalStream, TcpListenerStream}; 14 | 15 | mod elapsed; 16 | mod peer_addr; 17 | mod retain_unordered; 18 | 19 | use crate::elapsed::Elapsed; 20 | use crate::peer_addr::PeerAddr; 21 | use crate::retain_unordered::RetainUnordered; 22 | 23 | #[cfg(all(unix, feature = "sandbox"))] 24 | use rusty_sandbox::Sandbox; 25 | 26 | #[cfg(all(unix, feature = "drop_privs"))] 27 | use privdrop::PrivDrop; 28 | 29 | #[cfg(all(unix, feature = "drop_privs"))] 30 | use std::path::PathBuf; 31 | 32 | #[cfg(all(unix, feature = "drop_privs"))] 33 | use std::ffi::OsString; 34 | 35 | static BANNER: &[u8] = b"My name is Yon Yonson,\r\n\ 36 | I live in Wisconsin.\r\n\ 37 | I work in a lumber yard there.\r\n\ 38 | The people I meet as\r\n\ 39 | I walk down the street,\r\n\ 40 | They say \"Hello!\"\r\n\ 41 | I say \"Hello!\"\r\n\ 42 | They say \"What's your name.\"\r\n\ 43 | I say: "; 44 | 45 | #[derive(Debug, StructOpt)] 46 | #[structopt(name = "tarssh", about = "A SSH tarpit server")] 47 | struct Config { 48 | /// Listen address(es) to bind to 49 | #[structopt(short = "l", long = "listen", default_value = "0.0.0.0:2222")] 50 | listen: Vec, 51 | /// Best-effort connection limit 52 | #[structopt(short = "c", long = "max-clients", default_value = "4096")] 53 | max_clients: std::num::NonZeroU32, 54 | /// Seconds between responses 55 | #[structopt(short = "d", long = "delay", default_value = "10")] 56 | delay: std::num::NonZeroU16, 57 | /// Socket write timeout 58 | #[structopt(short = "t", long = "timeout", default_value = "30")] 59 | timeout: u16, 60 | /// Verbose level (repeat for more verbosity) 61 | #[structopt(short = "v", long = "verbose", parse(from_occurrences))] 62 | verbose: u8, 63 | /// Disable timestamps in logs 64 | #[structopt(long)] 65 | disable_log_timestamps: bool, 66 | /// Disable module name in logs (e.g. "tarssh") 67 | #[structopt(long)] 68 | disable_log_ident: bool, 69 | /// Disable log level in logs (e.g. "info") 70 | #[structopt(long)] 71 | disable_log_level: bool, 72 | #[cfg(all(unix, feature = "drop_privs"))] 73 | #[structopt(flatten)] 74 | #[cfg(all(unix, feature = "drop_privs"))] 75 | privdrop: PrivDropConfig, 76 | } 77 | 78 | #[cfg(all(unix, feature = "drop_privs"))] 79 | #[derive(Debug, StructOpt)] 80 | struct PrivDropConfig { 81 | /// Run as this user and their primary group 82 | #[structopt(short = "u", long = "user", parse(from_os_str))] 83 | user: Option, 84 | /// Run as this group 85 | #[structopt(short = "g", long = "group", parse(from_os_str))] 86 | group: Option, 87 | /// Chroot to this directory 88 | #[structopt(long = "chroot", parse(from_os_str))] 89 | chroot: Option, 90 | } 91 | 92 | #[derive(Debug)] 93 | struct Connection { 94 | sock: TcpStream, // 24b 95 | peer: PeerAddr, // 18b, down from 32b 96 | start: Elapsed, // 4b, a decisecond duration since the daemon epoch, down from 16b 97 | bytes: u64, // 8b, bytes written 98 | failed: u16, // 2b, writes failed on WOULDBLOCK 99 | } // 56 bytes 100 | 101 | fn errx>(code: i32, message: M) -> ! { 102 | error!("{}", message.as_ref()); 103 | std::process::exit(code); 104 | } 105 | 106 | async fn listen_socket(addr: SocketAddr) -> std::io::Result { 107 | let sock = match addr { 108 | SocketAddr::V4(_) => TcpSocket::new_v4()?, 109 | SocketAddr::V6(_) => TcpSocket::new_v6()?, 110 | }; 111 | 112 | sock.set_recv_buffer_size(1) 113 | .unwrap_or_else(|err| warn!("set_recv_buffer_size(), error: {}", err)); 114 | sock.set_send_buffer_size(32) 115 | .unwrap_or_else(|err| warn!("set_send_buffer_size(), error: {}", err)); 116 | 117 | // From mio: 118 | // On platforms with Berkeley-derived sockets, this allows to quickly 119 | // rebind a socket, without needing to wait for the OS to clean up the 120 | // previous one. 121 | // 122 | // On Windows, this allows rebinding sockets which are actively in use, 123 | // which allows “socket hijacking”, so we explicitly don't set it here. 124 | // https://docs.microsoft.com/en-us/windows/win32/winsock/using-so-reuseaddr-and-so-exclusiveaddruse 125 | #[cfg(not(windows))] 126 | sock.set_reuseaddr(true)?; 127 | 128 | sock.bind(addr)?; 129 | sock.listen(1024).map(TcpListenerStream::new) 130 | } 131 | 132 | #[tokio::main(flavor = "current_thread")] 133 | async fn main() { 134 | let opt = Config::from_args(); 135 | 136 | let max_clients = u32::from(opt.max_clients) as usize; 137 | let delay = Duration::from_secs(u16::from(opt.delay) as u64); 138 | let timeout = Duration::from_secs(opt.timeout as u64); 139 | let log_level = match opt.verbose { 140 | 0 => LevelFilter::Off, 141 | 1 => LevelFilter::Info, 142 | 2 => LevelFilter::Debug, 143 | _ => LevelFilter::Trace, 144 | }; 145 | 146 | env_logger::Builder::from_default_env() 147 | .filter(None, log_level) 148 | .format_timestamp(if opt.disable_log_timestamps { 149 | None 150 | } else { 151 | Some(env_logger::fmt::TimestampPrecision::Millis) 152 | }) 153 | .format_module_path(!opt.disable_log_ident) 154 | .format_level(!opt.disable_log_level) 155 | .init(); 156 | 157 | info!( 158 | "init, pid: {}, version: {}", 159 | std::process::id(), 160 | env!("CARGO_PKG_VERSION") 161 | ); 162 | 163 | let startup = Instant::now(); 164 | 165 | let mut listeners = stream::iter(opt.listen.iter()) 166 | .then(|addr| async move { 167 | match listen_socket(*addr).await { 168 | Ok(listener) => { 169 | info!("listen, addr: {}", addr); 170 | listener 171 | } 172 | Err(err) => { 173 | errx( 174 | exitcode::OSERR, 175 | format!("listen, addr: {}, error: {}", addr, err), 176 | ); 177 | } 178 | } 179 | }) 180 | .collect::>() 181 | .await; 182 | 183 | #[cfg(all(unix, feature = "drop_privs"))] 184 | { 185 | if opt.privdrop.user.is_some() 186 | || opt.privdrop.group.is_some() 187 | || opt.privdrop.chroot.is_some() 188 | { 189 | let mut pd = PrivDrop::default(); 190 | if let Some(path) = opt.privdrop.chroot { 191 | info!("privdrop, chroot: {}", path.display()); 192 | pd = pd.chroot(path); 193 | } 194 | 195 | if let Some(user) = opt.privdrop.user { 196 | info!("privdrop, user: {}", user.to_string_lossy()); 197 | pd = pd.user(user); 198 | } 199 | 200 | if let Some(group) = opt.privdrop.group { 201 | info!("privdrop, group: {}", group.to_string_lossy()); 202 | pd = pd.group(group); 203 | } 204 | 205 | pd.apply() 206 | .unwrap_or_else(|err| errx(exitcode::OSERR, format!("privdrop, error: {}", err))); 207 | 208 | info!("privdrop, enabled: true"); 209 | } else { 210 | info!("privdrop, enabled: false"); 211 | } 212 | } 213 | 214 | #[cfg(all(unix, feature = "sandbox"))] 215 | { 216 | let sandboxed = Sandbox::new().sandbox_this_process().is_ok(); 217 | info!("sandbox, enabled: {}", sandboxed); 218 | } 219 | 220 | info!( 221 | "start, servers: {}, max_clients: {}, delay: {}s, timeout: {}s", 222 | listeners.len(), 223 | opt.max_clients, 224 | delay.as_secs(), 225 | timeout.as_secs() 226 | ); 227 | 228 | let max_tick = delay.as_secs() as usize; 229 | let mut last_tick = 0; 230 | let mut num_clients = 0; 231 | let mut total_clients: u64 = 0; 232 | let mut bytes: u64 = 0; 233 | 234 | let mut slots: Box<[Vec]> = std::iter::repeat_with(Vec::new) 235 | .take(max_tick) 236 | .collect::>>() 237 | .into_boxed_slice(); 238 | 239 | let timer = IntervalStream::new(tokio::time::interval(Duration::from_secs(1))); 240 | let mut ticker = stream::iter(0..max_tick).cycle().zip(timer); 241 | let mut signals = signal_stream(); 242 | 243 | loop { 244 | tokio::select! { 245 | Some(signal) = signals.next() => { 246 | let action = match signal { 247 | "INFO" | "HUP" => "info", 248 | _ => "shutdown", 249 | }; 250 | info!( 251 | "{}, pid: {}, signal: {}, uptime: {:.2?}, clients: {}, total: {}, bytes: {}", 252 | action, 253 | std::process::id(), 254 | signal, 255 | startup.elapsed(), 256 | num_clients, 257 | total_clients, 258 | bytes 259 | ); 260 | if action != "info" { 261 | break; 262 | } 263 | } 264 | Some((tick, _)) = ticker.next() => { 265 | last_tick = tick; 266 | slots[tick].retain_unordered(|connection| { 267 | let pos = &BANNER[connection.bytes as usize % BANNER.len()..]; 268 | let slice = &pos[..=pos.iter().position(|b| *b == b'\n').unwrap_or(pos.len() - 1)]; 269 | match connection.sock.try_write(slice) { 270 | Ok(n) => { 271 | bytes += n as u64; 272 | connection.bytes += n as u64; 273 | connection.failed = 0; 274 | true 275 | }, 276 | Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => { true }, 277 | Err(mut e) => { 278 | if e.kind() == std::io::ErrorKind::WouldBlock { 279 | connection.failed += 1; 280 | if delay * (connection.failed as u32) < timeout { 281 | return true; 282 | } 283 | e = std::io::Error::new(std::io::ErrorKind::Other, "Timed Out"); 284 | } 285 | num_clients -= 1; 286 | info!( 287 | "disconnect, peer: {}, duration: {:.2?}, bytes: {}, error: \"{}\", clients: {}", 288 | connection.peer, 289 | connection.start.elapsed(startup), 290 | connection.bytes, 291 | e, 292 | num_clients 293 | ); 294 | 295 | false 296 | } 297 | } 298 | }); 299 | } 300 | Some(client) = listeners.next(), if num_clients < max_clients => { 301 | match client { 302 | Ok(sock) => { 303 | let peer = match sock.peer_addr() { 304 | Ok(peer) => peer, 305 | Err(e) => { 306 | warn!("reject, peer: unknown, error: {:?}", e); 307 | continue; 308 | } 309 | }; 310 | num_clients += 1; 311 | total_clients += 1; 312 | 313 | info!("connect, peer: {}, clients: {}", peer, num_clients); 314 | let connection = Connection { 315 | sock, 316 | peer: peer.into(), 317 | start: startup.into(), 318 | bytes: 0, 319 | failed: 0, 320 | }; 321 | slots[last_tick].push(connection); 322 | } 323 | Err(err) => match err.kind() { 324 | std::io::ErrorKind::ConnectionRefused 325 | | std::io::ErrorKind::ConnectionAborted 326 | | std::io::ErrorKind::ConnectionReset => (), 327 | _ => { 328 | let wait = Duration::from_millis(100); 329 | warn!("accept, err: {}, wait: {:?}", err, wait); 330 | sleep(wait).await; 331 | } 332 | }, 333 | } 334 | } 335 | } 336 | } 337 | } 338 | 339 | fn signal_stream() -> impl futures::Stream + 'static { 340 | #[cfg(not(unix))] 341 | { 342 | let sig = async_stream::stream! { 343 | let _ = tokio::signal::ctrl_c().await; 344 | yield "INT"; 345 | }; 346 | sig.boxed() 347 | } 348 | 349 | #[cfg(unix)] 350 | { 351 | use tokio::signal::unix::{signal, SignalKind}; 352 | 353 | fn unix_signal_stream(kind: SignalKind, tag: &str) -> impl futures::Stream { 354 | async_stream::stream! { 355 | let mut sig = signal(kind).unwrap(); 356 | 357 | while let Some(()) = sig.recv().await { 358 | yield tag; 359 | } 360 | } 361 | } 362 | 363 | futures::stream::select_all(vec![ 364 | #[cfg(any(target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))] 365 | unix_signal_stream(SignalKind::info(), "INFO").boxed(), 366 | unix_signal_stream(SignalKind::hangup(), "HUP").boxed(), 367 | unix_signal_stream(SignalKind::terminate(), "TERM").boxed(), 368 | unix_signal_stream(SignalKind::interrupt(), "INT").boxed(), 369 | ]) 370 | } 371 | } 372 | -------------------------------------------------------------------------------- /src/peer_addr.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::net::{IpAddr, Ipv6Addr, SocketAddr}; 3 | 4 | /// A compact representation of an IP and port pair 5 | #[derive(Debug, Clone, Copy)] 6 | #[repr(packed(2))] 7 | pub struct PeerAddr { 8 | ip: u128, 9 | port: u16, 10 | } 11 | 12 | impl From<&SocketAddr> for PeerAddr { 13 | fn from(peer: &SocketAddr) -> Self { 14 | let ip = match peer.ip() { 15 | IpAddr::V4(v4) => v4.to_ipv6_mapped().into(), 16 | IpAddr::V6(v6) => v6.into(), 17 | }; 18 | 19 | Self { 20 | ip, 21 | port: peer.port(), 22 | } 23 | } 24 | } 25 | 26 | impl From<&PeerAddr> for SocketAddr { 27 | fn from(peer: &PeerAddr) -> Self { 28 | let ip = Ipv6Addr::from(peer.ip); 29 | let ip = ip 30 | .to_ipv4() 31 | .map(IpAddr::V4) 32 | .unwrap_or_else(|| IpAddr::V6(ip)); 33 | 34 | SocketAddr::new(ip, peer.port) 35 | } 36 | } 37 | 38 | impl From for PeerAddr { 39 | fn from(peer: SocketAddr) -> Self { 40 | Self::from(&peer) 41 | } 42 | } 43 | 44 | impl From for SocketAddr { 45 | fn from(peer: PeerAddr) -> Self { 46 | Self::from(&peer) 47 | } 48 | } 49 | 50 | impl fmt::Display for PeerAddr { 51 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 52 | SocketAddr::from(self).fmt(f) 53 | } 54 | } 55 | 56 | #[cfg(test)] 57 | quickcheck::quickcheck! { 58 | fn prop_peeraddr(addr: SocketAddr) -> bool { 59 | SocketAddr::from(PeerAddr::from(addr)) == addr 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/retain_unordered.rs: -------------------------------------------------------------------------------- 1 | /// Trait that provides a `retain_unordered` method. 2 | pub trait RetainUnordered { 3 | /// Retains only the elements for which the predicate returns true, without 4 | /// any guarantees over visit or final order. 5 | fn retain_unordered(&mut self, f: F) 6 | where 7 | F: FnMut(&mut T) -> bool; 8 | } 9 | 10 | impl RetainUnordered for Vec { 11 | fn retain_unordered(&mut self, mut f: F) 12 | where 13 | F: FnMut(&mut T) -> bool, 14 | { 15 | let mut i = 0; 16 | 17 | while i < self.len() { 18 | if f(&mut self[i]) { 19 | i += 1; 20 | } else if self.len() > 1 { 21 | self.swap_remove(i); 22 | } else { 23 | self.remove(i); 24 | } 25 | } 26 | } 27 | } 28 | 29 | #[cfg(test)] 30 | quickcheck::quickcheck! { 31 | fn prop_retain_unordered(test: Vec, cutoff: u32) -> bool { 32 | let mut expected = test.clone(); 33 | expected.retain(|i| *i < cutoff); 34 | expected.sort_unstable(); 35 | 36 | let mut test = test; 37 | test.retain_unordered(|i| *i < cutoff); 38 | test.sort_unstable(); 39 | 40 | test == expected 41 | } 42 | } 43 | --------------------------------------------------------------------------------