Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b46f982b9b | ||
|
|
521f8bf720 | ||
|
|
ab1e69f1af | ||
|
|
29d85a77df | ||
|
|
a79a471111 | ||
|
|
f4192fd367 | ||
|
|
e9a4972e1f | ||
|
|
2f81acf45b | ||
|
|
8f8a51adf2 | ||
|
|
1dda392d1c | ||
|
|
937903340a | ||
|
|
f33752812c | ||
|
|
9b48f03ecd | ||
|
|
32d7d2cb17 | ||
|
|
67c08a3310 | ||
|
|
b7119f7f3e | ||
|
|
cfb8645901 | ||
|
|
d7cdad29c6 | ||
|
|
ea9e6f0736 | ||
|
|
2c854eb551 | ||
|
|
1c3fd8c378 | ||
|
|
4e64b5f00a | ||
|
|
faa21b7d14 | ||
|
|
bf74950995 | ||
|
|
c027ebc7da | ||
|
|
183eafd22e | ||
|
|
e497adf530 | ||
|
|
9ef667caed | ||
|
|
002f736073 | ||
|
|
1c2a982320 | ||
|
|
9644b3a2cd | ||
|
|
a21bbb31bb | ||
|
|
8b143f8ad7 | ||
|
|
111316f446 | ||
|
|
8be97b841d | ||
|
|
7446a42e56 | ||
|
|
e9ba3fa547 | ||
|
|
9a45d26e99 | ||
|
|
6cb99747e1 | ||
|
|
bc80fbbad4 | ||
|
|
3168580b86 | ||
|
|
2e78c22644 | ||
|
|
996f818b5e | ||
|
|
90c5c1766b | ||
|
|
f570d9ff8e | ||
|
|
42a3c87c83 | ||
|
|
1e93f44d8b | ||
|
|
bf290d7c7f | ||
|
|
160dc7a3a2 | ||
|
|
2d52f239ca | ||
|
|
5900974fda | ||
|
|
d0d388c813 | ||
|
|
5492dd729c | ||
|
|
b84f1fa046 | ||
|
|
3d4db2b6a1 | ||
|
|
1967598d96 | ||
|
|
94be5711d9 | ||
|
|
55c9f2a1c8 | ||
|
|
ce880ebe8c | ||
|
|
64f60a97ed | ||
|
|
a084c1b3b9 | ||
|
|
c668636a61 | ||
|
|
730f709fd6 | ||
|
|
e9dadb4a30 | ||
|
|
036297fbb9 | ||
|
|
e7db81a478 | ||
|
|
7c7018530b | ||
|
|
3abd1abe68 | ||
|
|
a4099a4ec2 | ||
|
|
827acd9cb0 | ||
|
|
e724d1953c | ||
|
|
f9bc5abf21 | ||
|
|
82c1a9373a | ||
|
|
45ece51c63 | ||
|
|
e234127874 | ||
|
|
053a3eb0eb | ||
|
|
b03379890e | ||
|
|
52ba449b70 | ||
|
|
79768931e3 | ||
|
|
3c3a69b06b | ||
|
|
487acbe382 | ||
|
|
8115b2d62e | ||
|
|
99c3c1a650 | ||
|
|
f366ce741d | ||
|
|
6f388ae152 | ||
|
|
a11b624644 | ||
|
|
85c080ee56 | ||
|
|
b367fbe48e | ||
|
|
50aed26d29 | ||
|
|
1ceb758acf | ||
|
|
07aa04a94d | ||
|
|
42d2f6128e | ||
|
|
f71ed28ff7 | ||
|
|
bb8aa7fa80 | ||
|
|
cd9852ae9d | ||
|
|
6cd10727ee | ||
|
|
034cbccf7e | ||
|
|
6bae2a1aa2 | ||
|
|
36e303e6ad | ||
|
|
3c2c869ba9 | ||
|
|
7f2fc87aa3 | ||
|
|
6440aed8a7 | ||
|
|
51efbca7aa | ||
|
|
c78975fa87 | ||
|
|
3a7230127f | ||
|
|
6dbf9f51cb | ||
|
|
c7a265e270 | ||
|
|
e3d31b3957 | ||
|
|
b0965dc946 | ||
|
|
2c66b4203a | ||
|
|
c8596c7679 | ||
|
|
aeef44c6e0 | ||
|
|
b228abfaa9 | ||
|
|
7fbd96be7c | ||
|
|
4a7ca7c00b | ||
|
|
60a5ae0102 | ||
|
|
1b74455a35 | ||
|
|
00d1003afa | ||
|
|
4c056a9039 | ||
|
|
a40c4ed17f | ||
|
|
59c6f10fad | ||
|
|
ec2d714e39 | ||
|
|
c19726c74f | ||
|
|
2600036621 | ||
|
|
9596712a19 | ||
|
|
fe98a3b6ef | ||
|
|
f4411e551a | ||
|
|
0640d94f53 | ||
|
|
d3508f8192 | ||
|
|
aacde90cf1 | ||
|
|
81a367e40b | ||
|
|
cc418f08b4 | ||
|
|
6781108db1 | ||
|
|
ea8015747f | ||
|
|
c09fb7bcfd | ||
|
|
6b96e84878 | ||
|
|
6f8b7f42f4 | ||
|
|
24ff49622c | ||
|
|
87399a1583 | ||
|
|
578457f6bd | ||
|
|
e315f82d03 | ||
|
|
63c9fdc05a | ||
|
|
6a1dd069b1 | ||
|
|
c89fad6c58 | ||
|
|
ac83fe7da0 | ||
|
|
3a06c22b43 | ||
|
|
9524b3503d | ||
|
|
eb11342a66 | ||
|
|
7e8230f762 | ||
|
|
d51fbb7411 | ||
|
|
56a234b41f | ||
|
|
59b645aa0e | ||
|
|
f5cbfef10f | ||
|
|
0772c4948d | ||
|
|
06e37f114e | ||
|
|
18a71f2625 | ||
|
|
48f826e545 | ||
|
|
f1006e4b3c | ||
|
|
a7257acbbb | ||
|
|
23724292e4 | ||
|
|
e1e5e7a020 | ||
|
|
2e85a6e31d | ||
|
|
9b43d1c6ce | ||
|
|
7f7de101dc | ||
|
|
c353cf5fd8 | ||
|
|
94b9c0e30a | ||
|
|
042875afec | ||
|
|
edc90a4db3 | ||
|
|
028554c18f | ||
|
|
f979926275 | ||
|
|
cf1eaf8e5c | ||
|
|
618b8aa9a9 | ||
|
|
0f23a671a5 | ||
|
|
3bdac9c9c7 | ||
|
|
1085a723c4 | ||
|
|
e1c8c254a1 | ||
|
|
05e5c50f49 | ||
|
|
f42f887da3 | ||
|
|
d5b7f4a024 | ||
|
|
c5d54880fc | ||
|
|
179d7fdfb8 | ||
|
|
2176262f22 | ||
|
|
447283cf23 | ||
|
|
8b317cec1f | ||
|
|
08ccf624c1 | ||
|
|
16e45cea2b | ||
|
|
a5c36c3f52 | ||
|
|
1124c46f37 | ||
|
|
8330e4363d | ||
|
|
08ee05ff2d | ||
|
|
34c285af75 | ||
|
|
e4eeeac6ef | ||
|
|
a70bd8b374 | ||
|
|
2884a448be | ||
|
|
6ec01bb154 | ||
|
|
c09baef4f8 | ||
|
|
cf426427ed | ||
|
|
293056ef19 | ||
|
|
2888336657 | ||
|
|
800cb3ba39 | ||
|
|
096ebbf73d | ||
|
|
5e6cc410e7 | ||
|
|
5739c6175e | ||
|
|
c134747788 | ||
|
|
2c2a55723a | ||
|
|
c5fb1b006c | ||
|
|
083a20269f | ||
|
|
815b4b5e05 | ||
|
|
b7734da3a7 | ||
|
|
36d71894b9 | ||
|
|
8a6334dfe8 | ||
|
|
e7c4205ec5 | ||
|
|
1553e58cf7 | ||
|
|
2e5e5fd71e | ||
|
|
ea2e8f6fbe | ||
|
|
d6bc7e7219 | ||
|
|
5d66529231 | ||
|
|
9e3c547615 | ||
|
|
a8c723fc68 | ||
|
|
bbb8237d19 | ||
|
|
5f8e015686 | ||
|
|
9d6bca1119 | ||
|
|
4168bf612e | ||
|
|
6a1b49e7a3 | ||
|
|
c123457734 | ||
|
|
727a64d934 | ||
|
|
b1f008a9c9 | ||
|
|
b9d5cee67a | ||
|
|
cbd0369dbd | ||
|
|
f1ef3be688 | ||
|
|
71091bfe86 | ||
|
|
e797458f43 | ||
|
|
2b409c9d33 | ||
|
|
ef8852aeff | ||
|
|
98794d8187 | ||
|
|
9221567d25 | ||
|
|
c5e518889e | ||
|
|
981c1741fe | ||
|
|
a88d311ecf | ||
|
|
c782b07041 | ||
|
|
971f2523f5 | ||
|
|
d1cd45ec29 | ||
|
|
a6213bc1a3 | ||
|
|
e1689e506c | ||
|
|
85f4eb5a30 | ||
|
|
34744424d7 | ||
|
|
ba5d51788a | ||
|
|
b17b733580 | ||
|
|
3b06a34885 | ||
|
|
e3f129e6d1 | ||
|
|
604ff2acf0 | ||
|
|
a8c52917c3 | ||
|
|
cbf0f94ca1 | ||
|
|
4a7101b2f5 | ||
|
|
07e7b30f51 | ||
|
|
43c43ae91d | ||
|
|
4f19e42ebb | ||
|
|
f1810702ab | ||
|
|
0c0433d628 | ||
|
|
de98d0044e | ||
|
|
55e388241e | ||
|
|
d969699fff | ||
|
|
4542798c7b | ||
|
|
69629c4fc3 | ||
|
|
8676de1c19 | ||
|
|
90708aec1d | ||
|
|
094a6f00e7 | ||
|
|
840daac9d2 | ||
|
|
b171ed7c88 | ||
|
|
5a9cfeeb40 | ||
|
|
6a25fb1d98 | ||
|
|
bddda4d2c8 | ||
|
|
75c7d6215b | ||
|
|
d9dec00197 | ||
|
|
1634bab1b4 | ||
|
|
c58c389b1e | ||
|
|
d150802c04 | ||
|
|
dd9e075e56 | ||
|
|
d3229ff9a9 | ||
|
|
aae43ef47e | ||
|
|
387edc7b05 | ||
|
|
bb01c3b5ee | ||
|
|
89541eddfe | ||
|
|
45537cf6cd | ||
|
|
b039db77f0 | ||
|
|
8350947caf | ||
|
|
c7776873bb | ||
|
|
306df13861 | ||
|
|
88c58f3170 | ||
|
|
4e3c2a5649 | ||
|
|
d6fa72ae34 | ||
|
|
9ce001f60f | ||
|
|
f81312d63b | ||
|
|
04224b6a72 | ||
|
|
086812f2a2 | ||
|
|
85959b8c13 | ||
|
|
e29aa7747c | ||
|
|
640003f22c | ||
|
|
66de4e7ee0 | ||
|
|
7edff6d767 | ||
|
|
5198994cbd | ||
|
|
cabeb6b1cb | ||
|
|
2956073d0d | ||
|
|
2c55f1ae58 | ||
|
|
e3bd50c922 | ||
|
|
d5ed38ef60 | ||
|
|
e4937567b8 | ||
|
|
f90d56f02b | ||
|
|
fdd74c9dab | ||
|
|
8b253edaed | ||
|
|
9ebc04cece | ||
|
|
26f387adc0 | ||
|
|
a46096dab5 | ||
|
|
9c0992dcd0 | ||
|
|
347985ea52 | ||
|
|
cafcc10b2e | ||
|
|
371ef81c7b | ||
|
|
54bc6bf191 | ||
|
|
042a1c7b6c | ||
|
|
aed0d71de8 | ||
|
|
ea428a1a22 | ||
|
|
da41cb062d | ||
|
|
0a40d1539a | ||
|
|
c3d24feebe | ||
|
|
45fa80acd4 | ||
|
|
a6f04c5c4a | ||
|
|
0a09da40aa | ||
|
|
6ffd5f741e | ||
|
|
efc42af2aa | ||
|
|
4f964b7d47 | ||
|
|
3887dbb93f | ||
|
|
9c2c03f68c | ||
|
|
d6cd9392d6 | ||
|
|
12dfefaf4d | ||
|
|
663a3df7ed | ||
|
|
7d8b36aaef | ||
|
|
4564036218 | ||
|
|
5b87f96451 | ||
|
|
033e2810f2 | ||
|
|
9f35cd43bc | ||
|
|
68e26cc0fc | ||
|
|
9c63b56142 | ||
|
|
8879fa7ff4 | ||
|
|
5d532356fd | ||
|
|
9f69d36057 | ||
|
|
dc18469af6 | ||
|
|
01fb9a14ba | ||
|
|
e2ddd93835 | ||
|
|
3873117324 | ||
|
|
dcb91d95e4 | ||
|
|
4947158ff8 | ||
|
|
f544f2240b | ||
|
|
f33476468f | ||
|
|
534cf8adf8 | ||
|
|
8290cad8d6 | ||
|
|
ac3ad3a6f7 | ||
|
|
3e74f5cb69 | ||
|
|
bfb827c0b8 | ||
|
|
16250456e6 | ||
|
|
0779071f7f | ||
|
|
8bfb643796 | ||
|
|
457c1643ff | ||
|
|
4ae4853e1d | ||
|
|
d471065dd2 | ||
|
|
99b00c091c | ||
|
|
af34aa4d07 | ||
|
|
5328bfc8b0 | ||
|
|
2340f94a81 | ||
|
|
dcc558cb43 | ||
|
|
bb2973049c | ||
|
|
a26ea80036 | ||
|
|
2643a1d5e1 | ||
|
|
b9337d8267 | ||
|
|
e4a097c7dc | ||
|
|
15290b2275 | ||
|
|
6993fb2037 | ||
|
|
a311726486 | ||
|
|
030e9708f9 | ||
|
|
7f80dd1678 | ||
|
|
f5a587accf | ||
|
|
1da08dcb9a | ||
|
|
6ca22d697c | ||
|
|
ac38ca591d | ||
|
|
f2d9a2e1fa | ||
|
|
e3c05afabb | ||
|
|
c466e0641e | ||
|
|
331efa5011 | ||
|
|
fa1ddc9bba | ||
|
|
170f57c0a3 | ||
|
|
6b6b2f267c | ||
|
|
2287965c5c | ||
|
|
1ec8ab412e | ||
|
|
598b5dfd10 | ||
|
|
1a5ff5bb4b | ||
|
|
10b3f82bb7 | ||
|
|
0e582fdd16 | ||
|
|
4c933b39a1 | ||
|
|
a3495b305b | ||
|
|
4bec0a8ea8 | ||
|
|
e86b86d0e0 | ||
|
|
43d3a1ed5a | ||
|
|
0f65753dcb | ||
|
|
2ab7191a65 | ||
|
|
d7c07c4c84 | ||
|
|
478851ac4c | ||
|
|
1c7b080d6f | ||
|
|
7dd9352bae | ||
|
|
d3fdc0d796 | ||
|
|
77853ad122 | ||
|
|
71f21aa259 | ||
|
|
bcdb0df1d9 | ||
|
|
5b00fa1727 | ||
|
|
dde7d96256 | ||
|
|
405f05d42c | ||
|
|
9e1cb60a05 | ||
|
|
8045af8cd2 | ||
|
|
221cf8694f | ||
|
|
bd32f4ead4 | ||
|
|
8625f939bf | ||
|
|
8f608eaf99 | ||
|
|
f9f2bf6cf8 | ||
|
|
6fb1196244 | ||
|
|
61c43dd5d0 | ||
|
|
075025113d | ||
|
|
e3645b5c81 | ||
|
|
a444ab66e1 | ||
|
|
30856350d3 | ||
|
|
7bff40a526 | ||
|
|
a38c6aa13b | ||
|
|
9b2c58fee5 | ||
|
|
198225a8da | ||
|
|
9a9dff422e | ||
|
|
227703a57e | ||
|
|
90bbed2a17 | ||
|
|
311cafcdbc | ||
|
|
02f1f6c7d1 | ||
|
|
2182a1bd01 | ||
|
|
0a42331dab | ||
|
|
a5e5797f9e | ||
|
|
9f2a752fb4 | ||
|
|
57de97bdc4 | ||
|
|
846afbdd5a | ||
|
|
52bf521a5d | ||
|
|
bef54d1496 | ||
|
|
76b3116b42 | ||
|
|
73665ed16e | ||
|
|
21b92a1d1b | ||
|
|
f9a7586857 | ||
|
|
6c05775394 | ||
|
|
043be7d6af | ||
|
|
5526657d84 | ||
|
|
859eb0e83a | ||
|
|
172ad63553 | ||
|
|
ed41af2c80 | ||
|
|
072d7335fd | ||
|
|
8a11532a25 | ||
|
|
f057a2113a | ||
|
|
da6c95f8f1 | ||
|
|
ea5c120fa7 | ||
|
|
cd86aaf30d | ||
|
|
d09bfeb37a | ||
|
|
4614e6bfe9 | ||
|
|
03877d376c | ||
|
|
da3f34da79 | ||
|
|
872eeb6e9e | ||
|
|
9bbee9d3ed | ||
|
|
1debe0035d | ||
|
|
cf314b4c63 | ||
|
|
4dcd929ba4 | ||
|
|
5b35a09b0b | ||
|
|
734595da46 | ||
|
|
943980ee83 | ||
|
|
61b691eaa4 | ||
|
|
6dcad646fa | ||
|
|
d2c84f5f33 | ||
|
|
0eaf63683e | ||
|
|
06c1d37aea | ||
|
|
4d560f4eb5 | ||
|
|
dbc3d510d5 | ||
|
|
68e1c8e8b6 | ||
|
|
15dd31e350 | ||
|
|
b3a64271dc | ||
|
|
abb08958cb | ||
|
|
d5935c4ce7 | ||
|
|
5c613082fb | ||
|
|
2868e13db4 | ||
|
|
31ed2e8914 | ||
|
|
0426aa83e3 | ||
|
|
4deb5eff53 | ||
|
|
81ad00dc29 | ||
|
|
64cac6e0fa | ||
|
|
739e9716fc | ||
|
|
6bf70f0f9a | ||
|
|
57dfb15852 | ||
|
|
282065752c | ||
|
|
982ae0d926 | ||
|
|
53f8a15e16 | ||
|
|
fc214a78b4 | ||
|
|
0ae289c25f | ||
|
|
9326fc4a0f | ||
|
|
c05ebaf857 | ||
|
|
6f8df7e77e | ||
|
|
488853f40e | ||
|
|
57c1e33650 | ||
|
|
699b578ade | ||
|
|
ebe5a326a8 | ||
|
|
abb7a3872c | ||
|
|
f74cb456b1 | ||
|
|
492d2fb3b3 | ||
|
|
af53591ccb | ||
|
|
6405b43f5a | ||
|
|
9a71253697 | ||
|
|
4d6d57fe2c | ||
|
|
a040ab5521 | ||
|
|
a546aa2ac0 | ||
|
|
868e61bb2e | ||
|
|
c4f8ceb336 | ||
|
|
9d80fb4fa7 | ||
|
|
e659d51976 | ||
|
|
64f4aecacd | ||
|
|
afe2a9452f | ||
|
|
4a6e74d507 | ||
|
|
eb905f7e7a | ||
|
|
561e7fb2f0 | ||
|
|
e5d22a0421 | ||
|
|
33c9388586 | ||
|
|
4dfe76737b | ||
|
|
cc0b344232 | ||
|
|
abaf100da9 | ||
|
|
43602793e0 | ||
|
|
7b827991c1 | ||
|
|
f4555ecb98 | ||
|
|
2fb8a399a7 | ||
|
|
e612ddc4e6 | ||
|
|
8bf75846d3 | ||
|
|
bf1cb3a5d0 | ||
|
|
01975d7238 | ||
|
|
8c4cc77e6b | ||
|
|
b0c1ab3177 | ||
|
|
6812cc3051 | ||
|
|
2800adec34 | ||
|
|
cf1aa67a21 | ||
|
|
c9708671e8 | ||
|
|
461f9aaa6d | ||
|
|
01a74b72df | ||
|
|
d301cf3b20 | ||
|
|
5e8d45bf01 | ||
|
|
bc5b2eea7e | ||
|
|
c8a8915682 | ||
|
|
d786e24186 | ||
|
|
5b536621e6 | ||
|
|
c05f40ae70 | ||
|
|
24b9f8d8d3 | ||
|
|
bda392c9a7 | ||
|
|
3836480486 | ||
|
|
2291ba73d2 | ||
|
|
e0af8a306a | ||
|
|
cb0f4f7a92 | ||
|
|
402bea4b06 | ||
|
|
f44dda0360 | ||
|
|
965a173032 | ||
|
|
d95cae6677 | ||
|
|
2f92d5583a | ||
|
|
8d8da79640 | ||
|
|
e5b4a2f443 | ||
|
|
a0b877729e | ||
|
|
743fa1cad6 | ||
|
|
a0561d4728 | ||
|
|
b2a1bd2e7a | ||
|
|
87de96c3c4 | ||
|
|
8e21f3bf8c | ||
|
|
4019d1ff3a | ||
|
|
c004bd0527 | ||
|
|
cdc5bbd924 | ||
|
|
f41213047a | ||
|
|
2315ef2417 | ||
|
|
cb87d5b9ce | ||
|
|
92800678cd | ||
|
|
8e76a71a4c | ||
|
|
07fdea8471 | ||
|
|
8670bcc35f | ||
|
|
fbba9d16ca | ||
|
|
309d1ad660 | ||
|
|
ed48161bea | ||
|
|
f0d1aa2b9a | ||
|
|
ac93feddb5 | ||
|
|
1280fa1b47 | ||
|
|
6e731aaf93 | ||
|
|
9230165f8f | ||
|
|
818c80041b | ||
|
|
bce955212a | ||
|
|
41e0e8905f | ||
|
|
85f1699724 | ||
|
|
bafd0141df | ||
|
|
aba0f3c38c | ||
|
|
9e161d4f79 | ||
|
|
731b4079cf | ||
|
|
b4edc6cca6 | ||
|
|
4a7cbd701b | ||
|
|
f34ee8f5ef | ||
|
|
89c763789f | ||
|
|
5c48f1173a | ||
|
|
8d8ca1d4a0 | ||
|
|
8d8acd61e8 | ||
|
|
0ab3e53034 | ||
|
|
56493859cb | ||
|
|
2ef723ed96 | ||
|
|
e9288302ba | ||
|
|
8179511527 | ||
|
|
fbab4a4020 | ||
|
|
b425d15a98 | ||
|
|
fddda34465 | ||
|
|
125df4a2ed | ||
|
|
e670d11223 | ||
|
|
8453320586 | ||
|
|
99e9c834a9 | ||
|
|
dfbce6a636 | ||
|
|
916be1e4a2 | ||
|
|
90b4328fb6 | ||
|
|
76035d3379 | ||
|
|
4de31ecede | ||
|
|
ce9befaa1a | ||
|
|
5f6457ed7e | ||
|
|
12d3c91983 | ||
|
|
e0fd7a39dc | ||
|
|
ff0787e332 | ||
|
|
6ce6c0ce9e | ||
|
|
edf989ad5d | ||
|
|
3fede4c6db | ||
|
|
75b310f227 | ||
|
|
72c6ef1f1d | ||
|
|
83b23da32b | ||
|
|
3886a07775 | ||
|
|
f5064d3d5c | ||
|
|
22cbfc5826 | ||
|
|
a9d729300c | ||
|
|
4f5dd20e85 | ||
|
|
499ff66f36 | ||
|
|
61b87243d9 | ||
|
|
e069991c8d | ||
|
|
85c1cbf445 | ||
|
|
5551e64166 | ||
|
|
e0aa1423ad | ||
|
|
e609b3617c | ||
|
|
d0d8380296 | ||
|
|
d66ad34f8c | ||
|
|
2670ec43df | ||
|
|
90d0b03ba7 | ||
|
|
57eb3627b3 | ||
|
|
7e675b7557 | ||
|
|
fdeaa05f23 | ||
|
|
9fbf39f0cc | ||
|
|
14b6ea928d | ||
|
|
9452b1f922 | ||
|
|
ed12888e81 | ||
|
|
f68abcb955 | ||
|
|
4c91e8fb40 | ||
|
|
1d3e831066 | ||
|
|
ff6b1c40db | ||
|
|
5573d91a7b | ||
|
|
a8e56eec4b |
No files matched your search
@@ -2,7 +2,7 @@ pkgbase = rustscan-bin
|
||||
pkgdesc = Faster Nmap Scanning with Rust
|
||||
pkgver = 1.1.0
|
||||
pkgrel = 1
|
||||
url = https://github.com/brandonskerritt/rustscan
|
||||
url = https://github.com/rustscan/rustscan
|
||||
arch = x86_64
|
||||
license = MIT
|
||||
depends = rustup
|
||||
|
||||
@@ -115,6 +115,150 @@
|
||||
"doc",
|
||||
"design"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Ferryistaken",
|
||||
"name": "Alessandro Ferrari",
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/47927670?v=4",
|
||||
"profile": "https://github.com/Ferryistaken",
|
||||
"contributions": [
|
||||
"content"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Phenomite",
|
||||
"name": "Phenomite",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/8285537?v=4",
|
||||
"profile": "https://github.com/Phenomite",
|
||||
"contributions": [
|
||||
"content"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "SuperSandro2000",
|
||||
"name": "Sandro",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/7258858?v=4",
|
||||
"profile": "https://supersandro.de/",
|
||||
"contributions": [
|
||||
"content",
|
||||
"bug",
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "caass",
|
||||
"name": "Cass",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/25358963?v=4",
|
||||
"profile": "https://swag.lgbt",
|
||||
"contributions": [
|
||||
"platform",
|
||||
"code",
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "niklasmohrin",
|
||||
"name": "Niklas Mohrin",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/47574893?v=4",
|
||||
"profile": "https://github.com/niklasmohrin",
|
||||
"contributions": [
|
||||
"doc",
|
||||
"code",
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "tim77",
|
||||
"name": "Artem Polishchuk",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/5614476?v=4",
|
||||
"profile": "https://liberapay.com/Artem4/",
|
||||
"contributions": [
|
||||
"platform"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "buermarc",
|
||||
"name": "buermarc",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/44375277?v=4",
|
||||
"profile": "https://github.com/buermarc",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "bergabman",
|
||||
"name": "bergabman",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/44554109?v=4",
|
||||
"profile": "https://github.com/bergabman",
|
||||
"contributions": [
|
||||
"code",
|
||||
"bug",
|
||||
"design"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "dmitris",
|
||||
"name": "Dmitry Savintsev",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/31205?v=4",
|
||||
"profile": "https://github.com/dmitris",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "bofh69",
|
||||
"name": "Sebastian Andersson",
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/1444315?v=4",
|
||||
"profile": "https://github.com/bofh69",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "mattcorbin",
|
||||
"name": "Matt Corbin",
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/6537765?v=4",
|
||||
"profile": "https://github.com/mattcorbin",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "rootsploit",
|
||||
"name": "RootSploit",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/67270834?v=4",
|
||||
"profile": "http://rootsploit.com",
|
||||
"contributions": [
|
||||
"blog"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "eiffel-fl",
|
||||
"name": "eiffel-fl",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/12171754?v=4",
|
||||
"profile": "https://github.com/eiffel-fl",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "u5surf",
|
||||
"name": "Y.Horie",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/14180225?v=4",
|
||||
"profile": "https://github.com/u5surf",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "okrplay",
|
||||
"name": "Oskar",
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/32576280?v=4",
|
||||
"profile": "https://github.com/okrplay",
|
||||
"contributions": [
|
||||
"code",
|
||||
"test"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
.git
|
||||
Dockerfile
|
||||
@@ -1,4 +1 @@
|
||||
github: brandonskerritt
|
||||
patreon: brandonskerritt
|
||||
ko_fi: hacker
|
||||
custom: ["https://monzo.me/bee", "https://paypal.me/brandonskerritt"]
|
||||
github: bee-san
|
||||
@@ -7,6 +7,10 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
IMPORTANT: If you do not fill this out, your issue will be automatically closed.
|
||||
|
||||
As of November 2024, RustScan no longer offiically supports any packages other than `cargo`. If you have an issue with Docker or a `.deb`, please bring it up with whoever created it :)
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
- package-ecosystem: cargo
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: monthly
|
||||
open-pull-requests-limit: 10
|
||||
ignore:
|
||||
- dependency-name: anyhow
|
||||
versions:
|
||||
- 1.0.38
|
||||
- 1.0.39
|
||||
- dependency-name: serde
|
||||
versions:
|
||||
- 1.0.123
|
||||
- dependency-name: futures
|
||||
versions:
|
||||
- 0.3.12
|
||||
- dependency-name: rlimit
|
||||
versions:
|
||||
- 0.5.3
|
||||
@@ -0,0 +1,4 @@
|
||||
template: |
|
||||
## What’s Changed
|
||||
|
||||
$CHANGES
|
||||
@@ -0,0 +1,57 @@
|
||||
"""Provision TCP memory for repeated scans on a disposable macOS CI runner."""
|
||||
|
||||
import ctypes
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
|
||||
class Statistics(ctypes.Structure):
|
||||
# XNU's bsd/sys/mem_acct_private.h, struct memacct_statistics.
|
||||
_fields_ = [("peak", ctypes.c_uint64), ("allocated", ctypes.c_int64),
|
||||
("softlimit", ctypes.c_uint64), ("hardlimit", ctypes.c_uint64),
|
||||
("name", ctypes.c_char * 16)]
|
||||
|
||||
|
||||
def tcp_memory(new_limit: int | None = None) -> dict:
|
||||
"""Read the TCP accounting object, optionally raising its hard limit."""
|
||||
libc = ctypes.CDLL(None, use_errno=True)
|
||||
libc.sysctlnametomib.argtypes = [ctypes.c_char_p, ctypes.POINTER(ctypes.c_int),
|
||||
ctypes.POINTER(ctypes.c_size_t)]
|
||||
libc.sysctl.argtypes = [ctypes.POINTER(ctypes.c_int), ctypes.c_uint,
|
||||
ctypes.c_void_p, ctypes.POINTER(ctypes.c_size_t),
|
||||
ctypes.c_void_p, ctypes.c_size_t]
|
||||
mib = (ctypes.c_int * 24)()
|
||||
count = ctypes.c_size_t(22)
|
||||
if libc.sysctlnametomib(b"kern.memacct", mib, ctypes.byref(count)):
|
||||
raise OSError(ctypes.get_errno(), os.strerror(ctypes.get_errno()))
|
||||
|
||||
def query(operation: int, index: int, output, replacement=None):
|
||||
mib[count.value], mib[count.value + 1] = operation, index
|
||||
length = ctypes.c_size_t(ctypes.sizeof(output))
|
||||
if libc.sysctl(mib, count.value + 2, ctypes.byref(output), ctypes.byref(length),
|
||||
ctypes.byref(replacement) if replacement is not None else None,
|
||||
ctypes.sizeof(replacement) if replacement is not None else 0):
|
||||
raise OSError(ctypes.get_errno(), os.strerror(ctypes.get_errno()))
|
||||
return length.value
|
||||
|
||||
names = ctypes.create_string_buffer(8 * 16)
|
||||
length = query(5, 0, names) # MEM_ACCT_SUBSYSTEMS
|
||||
index = next(i for i in range(length // 16)
|
||||
if names.raw[i * 16:(i + 1) * 16].rstrip(b"\0") == b"TCP")
|
||||
stats = Statistics()
|
||||
query(6, index, stats) # MEM_ACCT_ALL_SUBSYSTEM_STATISTICS
|
||||
previous_limit = stats.hardlimit
|
||||
if new_limit is not None and 0 < previous_limit < new_limit:
|
||||
query(3, index, ctypes.c_uint64(), ctypes.c_uint64(new_limit))
|
||||
query(6, index, stats)
|
||||
return {"previous_hardlimit": previous_limit,
|
||||
**{name: getattr(stats, name) for name in ("peak", "allocated", "softlimit", "hardlimit")}}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# The default TCP budget is 1/32 of physical memory. Repeated 10,000-socket
|
||||
# batches hit ENOBUFS on Darwin 25.6 even on master. Give both builds 1/8
|
||||
# before timing; retain every listener check and performance threshold.
|
||||
memory = int(subprocess.check_output(["sysctl", "-n", "hw.memsize"], text=True))
|
||||
print(json.dumps(tcp_memory(memory // 8)))
|
||||
@@ -0,0 +1,984 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compare RustScan builds by scanning listeners on the loopback interface.
|
||||
|
||||
CI-only tooling for ``.github/workflows/scan-benchmark.yml``; it is not part
|
||||
of the test suite. It port-scans 127.0.0.1, so only run it on a disposable CI
|
||||
runner, never on a developer machine or a shared host.
|
||||
|
||||
``run`` starts ``serve`` (TCP listeners and UDP responders on 127.0.0.1) in a
|
||||
separate process, runs every scenario with every build (interleaving the
|
||||
builds so that drift on the runner affects them all alike), checks that all
|
||||
builds report the same open ports, and writes the raw measurements as JSON
|
||||
plus a Markdown summary of the medians. On Linux the workflow also sets up a
|
||||
host behind an artificial delay (a network namespace on the runner, with its
|
||||
own ``serve``), so that answers arrive after a round trip, as they do from a
|
||||
remote host, instead of while the probe is being sent.
|
||||
|
||||
Measured per run:
|
||||
|
||||
* scan time: RustScan's own "Portscan" timer (``RUST_LOG=rustscan=info``),
|
||||
reported as throughput (sockets per second);
|
||||
* wall time of the whole process, and the time until the first ``Open`` line
|
||||
appears on stdout (latency to the first result);
|
||||
* peak RSS and CPU time (user + system) of the process;
|
||||
* peak number of open file descriptors (Windows: handles) and of threads,
|
||||
sampled every few milliseconds while the scan runs.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import selectors
|
||||
import signal
|
||||
import socket
|
||||
import statistics
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from dataclasses import asdict, dataclass, field
|
||||
|
||||
LOOPBACK = "127.0.0.1"
|
||||
|
||||
# Ports `serve` listens on. They stay below 32768, i.e. below the ephemeral
|
||||
# port ranges of Linux (32768-60999), macOS and Windows (49152-65535): a scan
|
||||
# of a port inside the ephemeral range can occasionally connect a socket to
|
||||
# itself and report a bogus open port.
|
||||
TCP_PORTS = tuple(range(1024, 32768, 101)) # 315 open TCP ports
|
||||
UDP_PORTS = tuple(range(1031, 32768, 997)) # 32 open UDP ports
|
||||
SWEEP = (1, 32767)
|
||||
|
||||
SAMPLE_INTERVAL = 0.005 # seconds between fd/thread samples
|
||||
RUN_TIMEOUT = 300 # seconds before a run is killed and counted as failed
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Listener process
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
def serve(addresses: list[str]) -> int:
|
||||
"""Accepts (and closes) TCP connections and answers every UDP datagram."""
|
||||
selector = selectors.DefaultSelector()
|
||||
bound: dict[str, dict[str, list[int]]] = {"tcp": {}, "udp": {}, "busy": {}}
|
||||
|
||||
for address in addresses:
|
||||
for kind, ports in (("tcp", TCP_PORTS), ("udp", UDP_PORTS)):
|
||||
sock_type = socket.SOCK_STREAM if kind == "tcp" else socket.SOCK_DGRAM
|
||||
for port in ports:
|
||||
sock = socket.socket(socket.AF_INET, sock_type)
|
||||
if os.name != "nt":
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
try:
|
||||
sock.bind((address, port))
|
||||
except OSError:
|
||||
sock.close()
|
||||
bound["busy"].setdefault(f"{kind} {address}", []).append(port)
|
||||
continue
|
||||
if kind == "tcp":
|
||||
sock.listen(1024)
|
||||
sock.setblocking(False)
|
||||
selector.register(sock, selectors.EVENT_READ, kind)
|
||||
bound[kind].setdefault(address, []).append(port)
|
||||
|
||||
print(json.dumps(bound), flush=True)
|
||||
|
||||
while True:
|
||||
for key, _ in selector.select(timeout=1.0):
|
||||
sock = key.fileobj
|
||||
if key.data == "tcp":
|
||||
while True:
|
||||
try:
|
||||
conn, _ = sock.accept()
|
||||
except OSError: # BlockingIOError once the queue is empty
|
||||
break
|
||||
conn.close()
|
||||
else:
|
||||
while True:
|
||||
try:
|
||||
_, peer = sock.recvfrom(4096)
|
||||
except ConnectionResetError:
|
||||
# Windows reports an earlier ICMP error here; skip it.
|
||||
continue
|
||||
except OSError: # BlockingIOError once the queue is empty
|
||||
break
|
||||
try:
|
||||
sock.sendto(b"rustscan-benchmark\n", peer)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def start_server(addresses: list[str]) -> tuple[subprocess.Popen, dict]:
|
||||
args = [arg for address in addresses for arg in ("--address", address)]
|
||||
proc = subprocess.Popen(
|
||||
[sys.executable, os.path.abspath(__file__), "serve", *args],
|
||||
stdout=subprocess.PIPE,
|
||||
stdin=subprocess.DEVNULL,
|
||||
text=True,
|
||||
)
|
||||
line = proc.stdout.readline()
|
||||
if not line:
|
||||
raise SystemExit("the listener process exited before it was ready")
|
||||
return proc, json.loads(line)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Scenarios
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Scenario:
|
||||
name: str
|
||||
title: str
|
||||
addresses: tuple[str, ...]
|
||||
ranges: tuple[tuple[int, int], ...] = ()
|
||||
ports: tuple[int, ...] = ()
|
||||
batch: int = 4500
|
||||
timeout_ms: int = 1500
|
||||
udp: bool = False
|
||||
repeat: int = 1 # extra samples for very short scenarios
|
||||
excluded_ports: tuple[int, ...] = ()
|
||||
|
||||
@property
|
||||
def sockets(self) -> int:
|
||||
span = sum(end - start + 1 for start, end in self.ranges)
|
||||
excluded = sum(self.covers(port) for port in set(self.excluded_ports))
|
||||
return len(self.addresses) * (len(self.ports) + span - excluded)
|
||||
|
||||
def covers(self, port: int) -> bool:
|
||||
return port in self.ports or any(s <= port <= e for s, e in self.ranges)
|
||||
|
||||
def args(self) -> list[str]:
|
||||
args = ["-a", ",".join(self.addresses)]
|
||||
if self.ports:
|
||||
args += ["-p", ",".join(map(str, self.ports))]
|
||||
else:
|
||||
args += ["-r", ",".join(f"{s}-{e}" for s, e in self.ranges)]
|
||||
args += ["-b", str(self.batch), "-t", str(self.timeout_ms)]
|
||||
if self.udp:
|
||||
args.append("--udp")
|
||||
if self.excluded_ports:
|
||||
args += ["--exclude-ports", ",".join(map(str, self.excluded_ports))]
|
||||
return args
|
||||
|
||||
def expected(self, listeners: dict) -> set[str]:
|
||||
by_address = listeners["udp" if self.udp else "tcp"]
|
||||
return {
|
||||
f"{address}:{port}"
|
||||
for address in self.addresses
|
||||
for port in by_address.get(address, [])
|
||||
if self.covers(port) and port not in self.excluded_ports
|
||||
}
|
||||
|
||||
def describe(self) -> str:
|
||||
hosts = f"{len(self.addresses)} addresses" if len(self.addresses) > 1 else self.addresses[0]
|
||||
return f"{self.title}: `{' '.join(self.args()[2:])}` on {hosts}"
|
||||
|
||||
|
||||
def build_scenarios(
|
||||
system: str, filtered: tuple[int, int] | None, delayed: str | None, delay_ms: int
|
||||
) -> list[Scenario]:
|
||||
lo = (LOOPBACK,)
|
||||
if system == "Windows":
|
||||
# Windows retries a refused connection for about a second, so every
|
||||
# closed port there takes as long as a filtered one: sweep fewer.
|
||||
sweep, small_sweep = ((1, 8191),), ((1, 2047),)
|
||||
else:
|
||||
sweep = small_sweep = (SWEEP,)
|
||||
|
||||
def ports(ranges: tuple[tuple[int, int], ...]) -> str:
|
||||
return f"{sum(e - s + 1 for s, e in ranges):,} ports"
|
||||
|
||||
scenarios = [
|
||||
Scenario("tcp-1-port", "TCP, 1 open port", lo, ports=(TCP_PORTS[0],), repeat=3),
|
||||
Scenario("tcp-open", "TCP, open listeners only", lo, ports=TCP_PORTS, repeat=3),
|
||||
Scenario("tcp-sweep", f"TCP, {ports(sweep)}, default batch", lo, sweep),
|
||||
Scenario(
|
||||
"tcp-sweep-excluded",
|
||||
f"TCP, {ports(sweep)}, 1,024 excluded ports",
|
||||
lo,
|
||||
sweep,
|
||||
excluded_ports=tuple(range(1024, 2048)),
|
||||
),
|
||||
Scenario(
|
||||
"tcp-sweep-b500", f"TCP, {ports(small_sweep)}, small batch", lo, small_sweep, batch=500
|
||||
),
|
||||
Scenario("tcp-sweep-b10000", f"TCP, {ports(sweep)}, large batch", lo, sweep, batch=10000),
|
||||
]
|
||||
if system == "Linux":
|
||||
# Linux answers on all of 127.0.0.0/8; other systems only on 127.0.0.1.
|
||||
hosts = tuple(f"127.0.0.{i}" for i in range(1, 9))
|
||||
scenarios.append(
|
||||
Scenario("tcp-8-hosts", "TCP, 8 hosts x 4,096 ports", hosts, ((1, 4096),))
|
||||
)
|
||||
if delayed:
|
||||
scenarios.append(
|
||||
Scenario("tcp-delayed", f"TCP, {ports(sweep)}, {delay_ms} ms away", (delayed,), sweep)
|
||||
)
|
||||
if filtered:
|
||||
dropped = (filtered,)
|
||||
scenarios += [
|
||||
Scenario(
|
||||
"tcp-filtered", "TCP, 2,000 filtered ports", lo, dropped, batch=500, timeout_ms=250
|
||||
),
|
||||
Scenario(
|
||||
"tcp-mixed",
|
||||
f"TCP, {ports(sweep)} + 2,000 filtered ports",
|
||||
lo,
|
||||
sweep + dropped,
|
||||
timeout_ms=500,
|
||||
),
|
||||
]
|
||||
scenarios.append(
|
||||
Scenario("udp-sweep", f"UDP, {ports(sweep)}", lo, sweep, timeout_ms=500, udp=True)
|
||||
)
|
||||
scenarios.append(
|
||||
Scenario(
|
||||
"udp-open", "UDP, echo responders only", lo, ports=UDP_PORTS,
|
||||
timeout_ms=500, udp=True, repeat=9,
|
||||
)
|
||||
)
|
||||
if delayed:
|
||||
scenarios.append(
|
||||
Scenario(
|
||||
"udp-delayed",
|
||||
f"UDP, {ports(sweep)}, {delay_ms} ms away",
|
||||
(delayed,),
|
||||
sweep,
|
||||
timeout_ms=500,
|
||||
udp=True,
|
||||
)
|
||||
)
|
||||
if filtered:
|
||||
scenarios.append(
|
||||
Scenario(
|
||||
"udp-filtered",
|
||||
"UDP, 2,000 filtered ports",
|
||||
lo,
|
||||
(filtered,),
|
||||
batch=500,
|
||||
timeout_ms=250,
|
||||
udp=True,
|
||||
)
|
||||
)
|
||||
return scenarios
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Running one scan
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
@dataclass
|
||||
class RunResult:
|
||||
build: str
|
||||
scenario: str
|
||||
sockets: int = 0
|
||||
ok: bool = True
|
||||
error: str = ""
|
||||
returncode: int | None = None
|
||||
wall_s: float | None = None
|
||||
scan_s: float | None = None
|
||||
first_open_s: float | None = None
|
||||
rss_kb: int | None = None
|
||||
cpu_s: float | None = None
|
||||
peak_fds: int | None = None
|
||||
peak_threads: int | None = None
|
||||
open: list[str] = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def rate(self) -> float | None:
|
||||
return self.sockets / self.scan_s if self.scan_s else None
|
||||
|
||||
|
||||
SCAN_TIME = re.compile(r"Portscan\s*\|\s*([0-9.]+)\s*s")
|
||||
OPEN_LINE = re.compile(r"^Open (\S+)$")
|
||||
SUMMARY_LINE = re.compile(r"^(\S+) -> \[([0-9,]*)\]$")
|
||||
|
||||
|
||||
class Sampler(threading.Thread):
|
||||
"""Samples the open descriptors and the threads of a running process."""
|
||||
|
||||
def __init__(self, proc: subprocess.Popen):
|
||||
super().__init__(daemon=True)
|
||||
self.proc = proc
|
||||
self.done = threading.Event()
|
||||
self.peak_fds: int | None = None
|
||||
self.peak_threads: int | None = None
|
||||
self.system = platform.system()
|
||||
self.libproc = darwin_libproc() if self.system == "Darwin" else None
|
||||
|
||||
def sample(self) -> tuple[int | None, int | None]:
|
||||
pid = self.proc.pid
|
||||
if self.system == "Linux":
|
||||
fds = len(os.listdir(f"/proc/{pid}/fd"))
|
||||
threads = None
|
||||
with open(f"/proc/{pid}/status", encoding="ascii", errors="replace") as status:
|
||||
for line in status:
|
||||
if line.startswith("Threads:"):
|
||||
threads = int(line.split()[1])
|
||||
break
|
||||
return fds, threads
|
||||
if self.libproc is not None:
|
||||
return darwin_fds_and_threads(self.libproc, pid)
|
||||
if self.system == "Windows":
|
||||
return windows_handle_count(self.proc), None
|
||||
return None, None
|
||||
|
||||
def run(self) -> None:
|
||||
while not self.done.is_set():
|
||||
try:
|
||||
fds, threads = self.sample()
|
||||
except (OSError, ValueError):
|
||||
return # the process is gone
|
||||
if fds is not None:
|
||||
self.peak_fds = max(self.peak_fds or 0, fds)
|
||||
if threads is not None:
|
||||
self.peak_threads = max(self.peak_threads or 0, threads)
|
||||
self.done.wait(SAMPLE_INTERVAL)
|
||||
|
||||
|
||||
def darwin_libproc():
|
||||
import ctypes
|
||||
|
||||
try:
|
||||
libproc = ctypes.CDLL("/usr/lib/libproc.dylib")
|
||||
except OSError:
|
||||
return None
|
||||
libproc.proc_pidinfo.argtypes = [
|
||||
ctypes.c_int,
|
||||
ctypes.c_int,
|
||||
ctypes.c_uint64,
|
||||
ctypes.c_void_p,
|
||||
ctypes.c_int,
|
||||
]
|
||||
libproc.proc_pidinfo.restype = ctypes.c_int
|
||||
return libproc
|
||||
|
||||
|
||||
def darwin_fds_and_threads(libproc, pid: int) -> tuple[int | None, int | None]:
|
||||
import ctypes
|
||||
|
||||
proc_pidlistfds, proc_pidtaskinfo = 1, 4
|
||||
size = libproc.proc_pidinfo(pid, proc_pidlistfds, 0, None, 0)
|
||||
if size <= 0:
|
||||
raise OSError("process is gone")
|
||||
buf = ctypes.create_string_buffer(size)
|
||||
used = libproc.proc_pidinfo(pid, proc_pidlistfds, 0, ctypes.addressof(buf), size)
|
||||
if used <= 0:
|
||||
raise OSError("process is gone")
|
||||
fds = used // 8 # sizeof(struct proc_fdinfo)
|
||||
|
||||
class TaskInfo(ctypes.Structure): # struct proc_taskinfo
|
||||
_fields_ = [
|
||||
*((name, ctypes.c_uint64) for name in ("vsize", "rsize", "user", "system", "tu", "ts")),
|
||||
*(
|
||||
(name, ctypes.c_int32)
|
||||
for name in (
|
||||
"policy",
|
||||
"faults",
|
||||
"pageins",
|
||||
"cow_faults",
|
||||
"messages_sent",
|
||||
"messages_received",
|
||||
"syscalls_mach",
|
||||
"syscalls_unix",
|
||||
"csw",
|
||||
"threadnum",
|
||||
"numrunning",
|
||||
"priority",
|
||||
)
|
||||
),
|
||||
]
|
||||
|
||||
info = TaskInfo()
|
||||
got = libproc.proc_pidinfo(
|
||||
pid, proc_pidtaskinfo, 0, ctypes.addressof(info), ctypes.sizeof(info)
|
||||
)
|
||||
return fds, (info.threadnum if got == ctypes.sizeof(info) else None)
|
||||
|
||||
|
||||
def windows_handle_count(proc: subprocess.Popen) -> int | None:
|
||||
import ctypes
|
||||
from ctypes import wintypes
|
||||
|
||||
count = wintypes.DWORD()
|
||||
handle = wintypes.HANDLE(int(proc._handle)) # Popen keeps the handle open
|
||||
if not ctypes.windll.kernel32.GetProcessHandleCount(handle, ctypes.byref(count)):
|
||||
raise OSError("process is gone")
|
||||
return count.value
|
||||
|
||||
|
||||
def windows_memory_and_cpu(proc: subprocess.Popen) -> tuple[int | None, float | None]:
|
||||
import ctypes
|
||||
from ctypes import wintypes
|
||||
|
||||
class Counters(ctypes.Structure): # PROCESS_MEMORY_COUNTERS
|
||||
_fields_ = [
|
||||
("cb", wintypes.DWORD),
|
||||
("PageFaultCount", wintypes.DWORD),
|
||||
("PeakWorkingSetSize", ctypes.c_size_t),
|
||||
("WorkingSetSize", ctypes.c_size_t),
|
||||
("QuotaPeakPagedPoolUsage", ctypes.c_size_t),
|
||||
("QuotaPagedPoolUsage", ctypes.c_size_t),
|
||||
("QuotaPeakNonPagedPoolUsage", ctypes.c_size_t),
|
||||
("QuotaNonPagedPoolUsage", ctypes.c_size_t),
|
||||
("PagefileUsage", ctypes.c_size_t),
|
||||
("PeakPagefileUsage", ctypes.c_size_t),
|
||||
]
|
||||
|
||||
handle = wintypes.HANDLE(int(proc._handle))
|
||||
counters = Counters()
|
||||
counters.cb = ctypes.sizeof(counters)
|
||||
rss_kb = None
|
||||
if ctypes.windll.psapi.GetProcessMemoryInfo(handle, ctypes.byref(counters), counters.cb):
|
||||
rss_kb = counters.PeakWorkingSetSize // 1024
|
||||
|
||||
times = [wintypes.FILETIME() for _ in range(4)] # creation, exit, kernel, user
|
||||
cpu_s = None
|
||||
if ctypes.windll.kernel32.GetProcessTimes(handle, *(ctypes.byref(t) for t in times)):
|
||||
ticks = sum((t.dwHighDateTime << 32) | t.dwLowDateTime for t in times[2:])
|
||||
cpu_s = ticks / 1e7 # 100 ns units
|
||||
return rss_kb, cpu_s
|
||||
|
||||
|
||||
def run_once(name: str, binary: str, scenario: Scenario) -> RunResult:
|
||||
result = RunResult(build=name, scenario=scenario.name, sockets=scenario.sockets)
|
||||
cmd = [
|
||||
binary,
|
||||
*scenario.args(),
|
||||
"--scripts",
|
||||
"none",
|
||||
"--accessible",
|
||||
"--no-banner",
|
||||
"--no-config",
|
||||
]
|
||||
env = dict(os.environ, RUST_LOG="rustscan=info")
|
||||
stdout_lines: list[str] = []
|
||||
stderr_chunks: list[bytes] = []
|
||||
|
||||
start = time.perf_counter()
|
||||
proc = subprocess.Popen(
|
||||
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.DEVNULL, env=env
|
||||
)
|
||||
|
||||
def read_stdout() -> None:
|
||||
for raw in proc.stdout:
|
||||
line = raw.decode("utf-8", "replace").rstrip("\r\n")
|
||||
if result.first_open_s is None and line.startswith("Open "):
|
||||
result.first_open_s = time.perf_counter() - start
|
||||
stdout_lines.append(line)
|
||||
|
||||
readers = [
|
||||
threading.Thread(target=read_stdout, daemon=True),
|
||||
threading.Thread(target=lambda: stderr_chunks.append(proc.stderr.read()), daemon=True),
|
||||
]
|
||||
for reader in readers:
|
||||
reader.start()
|
||||
sampler = Sampler(proc)
|
||||
sampler.start()
|
||||
# On Unix the child is reaped with os.wait4 below, so the watchdog must
|
||||
# not go through Popen (which would try to reap it as well).
|
||||
kill = proc.kill if os.name == "nt" else lambda: os.kill(proc.pid, signal.SIGKILL)
|
||||
watchdog = threading.Timer(RUN_TIMEOUT, kill)
|
||||
watchdog.start()
|
||||
|
||||
try:
|
||||
if hasattr(os, "wait4"):
|
||||
_, status, usage = os.wait4(proc.pid, 0)
|
||||
result.wall_s = time.perf_counter() - start
|
||||
proc.returncode = os.waitstatus_to_exitcode(status)
|
||||
maxrss = usage.ru_maxrss # KiB on Linux, bytes on macOS
|
||||
result.rss_kb = maxrss // 1024 if platform.system() == "Darwin" else maxrss
|
||||
result.cpu_s = usage.ru_utime + usage.ru_stime
|
||||
else:
|
||||
proc.wait()
|
||||
result.wall_s = time.perf_counter() - start
|
||||
result.rss_kb, result.cpu_s = windows_memory_and_cpu(proc)
|
||||
finally:
|
||||
watchdog.cancel()
|
||||
sampler.done.set()
|
||||
sampler.join()
|
||||
for reader in readers:
|
||||
reader.join()
|
||||
|
||||
result.returncode = proc.returncode
|
||||
result.peak_fds = sampler.peak_fds
|
||||
result.peak_threads = sampler.peak_threads
|
||||
stderr = b"".join(stderr_chunks).decode("utf-8", "replace")
|
||||
|
||||
opened: set[str] = set()
|
||||
summarised: set[str] = set()
|
||||
for line in stdout_lines:
|
||||
if match := OPEN_LINE.match(line):
|
||||
opened.add(match.group(1))
|
||||
elif match := SUMMARY_LINE.match(line):
|
||||
summarised.update(f"{match.group(1)}:{p}" for p in match.group(2).split(",") if p)
|
||||
result.open = sorted(opened)
|
||||
|
||||
if scan_time := SCAN_TIME.search(stderr):
|
||||
result.scan_s = float(scan_time.group(1))
|
||||
|
||||
problems = []
|
||||
if proc.returncode != 0:
|
||||
problems.append(f"exit code {proc.returncode}")
|
||||
if result.scan_s is None:
|
||||
problems.append("no 'Portscan' timer in the log")
|
||||
if summarised != opened:
|
||||
problems.append(f"'Open' lines and the summary disagree: {sorted(summarised ^ opened)[:5]}")
|
||||
if problems:
|
||||
result.ok = False
|
||||
tail = "\n".join(stderr.strip().splitlines()[-15:])
|
||||
result.error = "; ".join(problems) + (f"\nstderr tail:\n{tail}" if tail else "")
|
||||
return result
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Environment
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
def sysctl(*names: str) -> list[str]:
|
||||
return subprocess.run(
|
||||
["sysctl", "-n", *names], capture_output=True, text=True, check=True
|
||||
).stdout.split("\n")
|
||||
|
||||
|
||||
def ephemeral_range() -> tuple[int, int] | None:
|
||||
system = platform.system()
|
||||
try:
|
||||
if system == "Linux":
|
||||
with open("/proc/sys/net/ipv4/ip_local_port_range", encoding="ascii") as f:
|
||||
low, high = map(int, f.read().split())
|
||||
return low, high
|
||||
if system == "Darwin":
|
||||
first, last = sysctl("net.inet.ip.portrange.first", "net.inet.ip.portrange.last")[:2]
|
||||
return int(first), int(last)
|
||||
except (OSError, ValueError, subprocess.CalledProcessError):
|
||||
return None
|
||||
return (49152, 65535) if system == "Windows" else None
|
||||
|
||||
|
||||
def diagnose_tcp(binary: str, scenario: Scenario, listeners: dict) -> dict:
|
||||
"""Untimed failure capture on a disposable macOS runner, using debug logs."""
|
||||
from macos_tcp_memory import tcp_memory
|
||||
|
||||
def states() -> dict[str, int]:
|
||||
snapshot = subprocess.run(
|
||||
["netstat", "-an", "-p", "tcp"], capture_output=True, text=True, check=True
|
||||
)
|
||||
counts: dict[str, int] = {}
|
||||
for line in snapshot.stdout.splitlines():
|
||||
if line.startswith("tcp"):
|
||||
state = line.split()[-1]
|
||||
counts[state] = counts.get(state, 0) + 1
|
||||
return counts
|
||||
|
||||
before = states()
|
||||
memory_before = tcp_memory()
|
||||
probe = subprocess.run(
|
||||
[binary, *scenario.args(), "--scripts", "none", "--accessible", "--no-banner", "--no-config"],
|
||||
env=dict(os.environ, RUST_LOG="rustscan=info,rustscan::scanner=debug"),
|
||||
capture_output=True, text=True, check=True,
|
||||
)
|
||||
opened = {
|
||||
match.group(1) for line in probe.stdout.splitlines()
|
||||
if (match := OPEN_LINE.match(line))
|
||||
}
|
||||
return {
|
||||
"before": before, "after": states(),
|
||||
"memory_before": memory_before, "memory_after": tcp_memory(),
|
||||
"missing": sorted(scenario.expected(listeners) - opened),
|
||||
"errors": [line for line in probe.stderr.splitlines()
|
||||
if "Typical socket connection errors" in line],
|
||||
}
|
||||
|
||||
|
||||
def environment() -> dict[str, object]:
|
||||
system = platform.system()
|
||||
info: dict[str, object] = {
|
||||
"system": system,
|
||||
"release": platform.release(),
|
||||
"machine": platform.machine(),
|
||||
"cpus": os.cpu_count(),
|
||||
"cpu": platform.processor(),
|
||||
"python": platform.python_version(),
|
||||
"ephemeral_ports": ephemeral_range(),
|
||||
}
|
||||
try:
|
||||
if system == "Linux":
|
||||
with open("/proc/cpuinfo", encoding="ascii", errors="replace") as f:
|
||||
for line in f:
|
||||
if line.lower().startswith(("model name", "cpu model")):
|
||||
info["cpu"] = line.split(":", 1)[1].strip()
|
||||
break
|
||||
elif system == "Darwin":
|
||||
info["cpu"] = sysctl("machdep.cpu.brand_string")[0].strip()
|
||||
except (OSError, subprocess.CalledProcessError):
|
||||
pass
|
||||
try:
|
||||
import resource
|
||||
|
||||
info["open_file_limit"] = resource.getrlimit(resource.RLIMIT_NOFILE)[0]
|
||||
except (ImportError, OSError, ValueError):
|
||||
pass
|
||||
return info
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Analysis and reporting
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
Results = dict[str, dict[str, list[RunResult]]]
|
||||
|
||||
# key, title, formatter, higher is better
|
||||
METRICS = (
|
||||
("rate", "Throughput (sockets/s, from RustScan's scan timer)", lambda v: f"{v:,.0f}", True),
|
||||
("wall_s", "Wall time of the whole process", lambda v: f"{v * 1000:,.0f} ms", False),
|
||||
("first_open_s", "Time to the first open port", lambda v: f"{v * 1000:,.1f} ms", False),
|
||||
("rss_kb", "Peak RSS", lambda v: f"{v / 1024:,.1f} MiB", False),
|
||||
("cpu_s", "CPU time (user + system)", lambda v: f"{v * 1000:,.0f} ms", False),
|
||||
("peak_fds", "Peak open descriptors (sampled; Windows: handles)", lambda v: f"{v:,.0f}", False),
|
||||
("peak_threads", "Peak threads (sampled)", lambda v: f"{v:,.0f}", False),
|
||||
)
|
||||
FLAG_WORSE = 10.0 # percent
|
||||
|
||||
|
||||
def metric_values(runs: list[RunResult], key: str) -> list[float]:
|
||||
return [float(getattr(r, key)) for r in runs if r.ok and getattr(r, key) is not None]
|
||||
|
||||
|
||||
def median(values: list[float]) -> float | None:
|
||||
return statistics.median(values) if values else None
|
||||
|
||||
|
||||
def worse_by(value: float, base: float, higher_is_better: bool) -> float:
|
||||
"""How much worse `value` is than `base`, in percent (negative: better)."""
|
||||
pct = (value / base - 1.0) * 100.0
|
||||
return -pct if higher_is_better else pct
|
||||
|
||||
|
||||
def check_open_ports(
|
||||
results: Results, scenarios: list[Scenario], listeners: dict, baseline: str
|
||||
) -> tuple[list[str], list[str], list[str]]:
|
||||
"""Returns (failed runs, open-port mismatches, notes).
|
||||
|
||||
No build may miss more listeners than the baseline: none at all if the
|
||||
baseline found every listener in every run, or barely more if it did not
|
||||
either (a runner too slow for the scenario's timeout). Ports that are
|
||||
open beyond the listeners (services already running on the runner) must
|
||||
match what the baseline found: a build may not miss a port the baseline
|
||||
found in every run, nor report one the baseline never found.
|
||||
"""
|
||||
failures, mismatches, notes = [], [], []
|
||||
for sc in scenarios:
|
||||
expected = sc.expected(listeners)
|
||||
per_build = results[sc.name]
|
||||
|
||||
def missed(runs: list[RunResult]) -> int:
|
||||
return sum(len(expected - set(r.open)) for r in runs if r.ok)
|
||||
|
||||
base_missed = missed(per_build[baseline])
|
||||
allowed = base_missed + max(3, base_missed // 4) if base_missed else 0
|
||||
if base_missed:
|
||||
notes.append(
|
||||
f"`{sc.name}`: `{baseline}` itself missed {base_missed} of "
|
||||
f"{len(expected) * len(per_build[baseline])} listener answers"
|
||||
)
|
||||
|
||||
base_extra = [set(r.open) - expected for r in per_build[baseline] if r.ok]
|
||||
always = set.intersection(*base_extra) if base_extra else set()
|
||||
ever = set.union(*base_extra) if base_extra else set()
|
||||
for name, runs in per_build.items():
|
||||
for index, res in enumerate(runs, start=1):
|
||||
if not res.ok:
|
||||
failures.append(f"`{sc.name}` `{name}` run {index}: {res.error.splitlines()[0]}")
|
||||
if name == baseline:
|
||||
continue
|
||||
if (count := missed(runs)) > allowed:
|
||||
mismatches.append(
|
||||
f"`{sc.name}` `{name}`: missed {count} listener answers "
|
||||
f"(`{baseline}`: {base_missed})"
|
||||
)
|
||||
for index, res in enumerate(runs, start=1):
|
||||
if not res.ok:
|
||||
continue
|
||||
found = set(res.open)
|
||||
where = f"`{sc.name}` `{name}` run {index}"
|
||||
if lost := always - found:
|
||||
mismatches.append(f"{where}: missed ports `{baseline}` always found {sorted(lost)[:5]}")
|
||||
if bogus := (found - expected) - ever:
|
||||
mismatches.append(f"{where}: reported ports `{baseline}` never found {sorted(bogus)[:5]}")
|
||||
return failures, mismatches, notes
|
||||
|
||||
|
||||
def gate(
|
||||
results: Results,
|
||||
scenarios: list[Scenario],
|
||||
baseline: str,
|
||||
candidate: str,
|
||||
max_slowdown: float,
|
||||
max_memory: float,
|
||||
) -> list[str]:
|
||||
"""Scenarios where `candidate` is clearly worse than `baseline`."""
|
||||
problems = []
|
||||
for sc in scenarios:
|
||||
checks = [("wall_s", False, max_slowdown, 0.002), ("rss_kb", False, max_memory, 1024)]
|
||||
if sc.sockets >= 1000: # throughput is meaningless for a handful of sockets
|
||||
checks.append(("rate", True, max_slowdown, 0))
|
||||
for key, higher, limit, floor in checks:
|
||||
base = median(metric_values(results[sc.name][baseline], key))
|
||||
value = median(metric_values(results[sc.name][candidate], key))
|
||||
if not base or value is None or abs(value - base) <= floor:
|
||||
continue
|
||||
if (worse := worse_by(value, base, higher)) > limit:
|
||||
problems.append(f"{sc.name}: {key} {worse:+.1f}% worse than {baseline} (limit {limit}%)")
|
||||
return problems
|
||||
|
||||
|
||||
def render_markdown(report: dict, results: Results, scenarios: list[Scenario]) -> str:
|
||||
builds = report["builds"]
|
||||
baseline = report["baseline"]
|
||||
env = report["environment"]
|
||||
out = [f"### Scan benchmark: {env['system']} {env['machine']}", ""]
|
||||
out.append(
|
||||
"; ".join(f"`{b['name']}`: {b['label']}" for b in builds)
|
||||
+ f". {report['runs']} interleaved runs per build and scenario (`tcp-1-port`: x3), medians."
|
||||
f" Runner: {env.get('cpu') or '?'}, {env['cpus']} CPUs, {env['system']} {env['release']},"
|
||||
f" open-file limit {env.get('open_file_limit', 'n/a')}."
|
||||
)
|
||||
out.append("")
|
||||
|
||||
problems = report["failures"] + report["mismatches"]
|
||||
tcp = sum(len(ports) for ports in report["listeners"]["tcp"].values())
|
||||
udp = sum(len(ports) for ports in report["listeners"]["udp"].values())
|
||||
if not problems:
|
||||
out.append(
|
||||
f"✅ Every run of every build exited cleanly; no build missed more of the {tcp} TCP "
|
||||
f"and {udp} UDP listeners than `{baseline}`, or disagreed with it about other ports."
|
||||
)
|
||||
else:
|
||||
out.append(f"❌ {len(report['failures'])} failed runs, {len(report['mismatches'])} open-port mismatches:")
|
||||
out += [f"- {p}" for p in problems[:25]]
|
||||
if report["notes"]:
|
||||
out.append("")
|
||||
out += [f"- ⚠️ {note}" for note in report["notes"]]
|
||||
out.append("")
|
||||
out.append("| Scenario | What is scanned |")
|
||||
out.append("|---|---|")
|
||||
out += [f"| `{sc.name}` | {sc.describe()} ({sc.sockets:,} sockets) |" for sc in scenarios]
|
||||
out.append("")
|
||||
out.append(f"Changes are relative to `{baseline}`; ❗ marks a median more than {FLAG_WORSE:.0f}% worse.")
|
||||
out.append("")
|
||||
|
||||
for key, title, fmt, higher in METRICS:
|
||||
rows = []
|
||||
for sc in scenarios:
|
||||
base_value = median(metric_values(results[sc.name][baseline], key))
|
||||
cells = []
|
||||
for build in builds:
|
||||
value = median(metric_values(results[sc.name][build["name"]], key))
|
||||
if value is None:
|
||||
cells.append("–")
|
||||
continue
|
||||
cell = fmt(value)
|
||||
if build["name"] != baseline and base_value:
|
||||
pct = (value / base_value - 1.0) * 100.0
|
||||
flag = " ❗" if worse_by(value, base_value, higher) > FLAG_WORSE else ""
|
||||
cell += f" ({pct:+.1f}%{flag})"
|
||||
cells.append(cell)
|
||||
if any(c != "–" for c in cells):
|
||||
rows.append(f"| `{sc.name}` | " + " | ".join(cells) + " |")
|
||||
out.append(f"**{title}**")
|
||||
out.append("")
|
||||
if rows:
|
||||
out.append("| Scenario | " + " | ".join(f"`{b['name']}`" for b in builds) + " |")
|
||||
out.append("|---|" + "---:|" * len(builds))
|
||||
out += rows
|
||||
else:
|
||||
out.append("_Not measured on this platform._")
|
||||
out.append("")
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Main
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
def parse_pairs(values: list[str], option: str) -> dict[str, str]:
|
||||
pairs = {}
|
||||
for value in values:
|
||||
name, sep, rest = value.partition("=")
|
||||
if not sep or not name or not rest:
|
||||
raise SystemExit(f"--{option} expects NAME=VALUE, got {value!r}")
|
||||
pairs[name] = rest
|
||||
return pairs
|
||||
|
||||
|
||||
def run(args: argparse.Namespace) -> int:
|
||||
builds = parse_pairs(args.build, "build")
|
||||
labels = parse_pairs(args.label, "label")
|
||||
baseline = args.baseline or next(iter(builds))
|
||||
if baseline not in builds:
|
||||
raise SystemExit(f"unknown baseline build {baseline!r}")
|
||||
for name, path in builds.items():
|
||||
if not os.path.isfile(path):
|
||||
raise SystemExit(f"build {name!r}: {path} does not exist")
|
||||
if args.gate and args.gate not in builds:
|
||||
raise SystemExit(f"unknown --gate build {args.gate!r}")
|
||||
if args.delayed_host and not args.delayed_listeners:
|
||||
raise SystemExit("--delayed-host needs --delayed-listeners")
|
||||
|
||||
filtered = None
|
||||
if args.filtered:
|
||||
low, _, high = args.filtered.partition("-")
|
||||
filtered = (int(low), int(high))
|
||||
|
||||
scenarios = build_scenarios(platform.system(), filtered, args.delayed_host, args.delay_ms)
|
||||
if args.only:
|
||||
wanted = set(args.only.split(","))
|
||||
scenarios = [sc for sc in scenarios if sc.name in wanted]
|
||||
|
||||
if ephemeral := ephemeral_range():
|
||||
low, high = ephemeral
|
||||
for sc in scenarios:
|
||||
for start, end in sc.ranges:
|
||||
if start <= high and end >= low:
|
||||
raise SystemExit(
|
||||
f"scenario {sc.name} scans {start}-{end}, which overlaps the ephemeral "
|
||||
f"port range {low}-{high}: a scan could connect a socket to itself"
|
||||
)
|
||||
|
||||
server, listeners = start_server([LOOPBACK])
|
||||
if args.delayed_host:
|
||||
# The workflow runs a second `serve` on the delayed host (inside a
|
||||
# network namespace) and stores its first line in this file.
|
||||
with open(args.delayed_listeners, encoding="utf-8") as f:
|
||||
far = json.loads(f.readline())
|
||||
for kind in ("tcp", "udp"):
|
||||
listeners[kind][args.delayed_host] = far[kind].get(args.delayed_host, [])
|
||||
for kind in ("tcp", "udp"):
|
||||
counts = {address: len(ports) for address, ports in listeners[kind].items()}
|
||||
print(f"{kind} listeners: {counts}", flush=True)
|
||||
if listeners["busy"]:
|
||||
print(f"already in use: {listeners['busy']}", flush=True)
|
||||
|
||||
names = list(builds)
|
||||
results: Results = {sc.name: {name: [] for name in names} for sc in scenarios}
|
||||
diagnostics = []
|
||||
try:
|
||||
warmup = next((sc for sc in scenarios if sc.name == "tcp-sweep"), scenarios[0])
|
||||
for name in names:
|
||||
run_once(name, builds[name], warmup)
|
||||
|
||||
rounds = args.runs * max(sc.repeat for sc in scenarios)
|
||||
for index in range(rounds):
|
||||
shift = index % len(names)
|
||||
order = names[shift:] + names[:shift]
|
||||
for sc in scenarios:
|
||||
if index >= args.runs * sc.repeat:
|
||||
continue
|
||||
for name in order:
|
||||
res = run_once(name, builds[name], sc)
|
||||
results[sc.name][name].append(res)
|
||||
print(
|
||||
f"[{index + 1}/{rounds}] {sc.name:<17} {name:<14} ok={res.ok} "
|
||||
f"scan={res.scan_s} wall={res.wall_s} first={res.first_open_s} "
|
||||
f"rss={res.rss_kb} cpu={res.cpu_s} fds={res.peak_fds} "
|
||||
f"threads={res.peak_threads} open={len(res.open)}",
|
||||
flush=True,
|
||||
)
|
||||
if not res.ok:
|
||||
print(f" {res.error}", flush=True)
|
||||
if platform.system() == "Darwin" and not diagnostics and any(
|
||||
sc.expected(listeners) - set(results[sc.name][name][-1].open) for name in names
|
||||
):
|
||||
# A failed pair cannot support a performance claim. Capture
|
||||
# its OS errors immediately, outside both measured scans.
|
||||
for name in names:
|
||||
record = {"build": name, "scenario": sc.name,
|
||||
**diagnose_tcp(builds[name], sc, listeners)}
|
||||
diagnostics.append(record)
|
||||
print("TCP diagnosis: " + json.dumps(record), flush=True)
|
||||
finally:
|
||||
server.kill()
|
||||
server.wait()
|
||||
|
||||
failures, mismatches, notes = check_open_ports(results, scenarios, listeners, baseline)
|
||||
report = {
|
||||
"environment": environment(),
|
||||
"baseline": baseline,
|
||||
"builds": [{"name": n, "label": labels.get(n, n), "path": builds[n]} for n in names],
|
||||
"runs": args.runs,
|
||||
"listeners": listeners,
|
||||
"scenarios": [asdict(sc) | {"sockets": sc.sockets} for sc in scenarios],
|
||||
"results": {
|
||||
sc: {n: [asdict(r) | {"rate": r.rate} for r in runs] for n, runs in per.items()}
|
||||
for sc, per in results.items()
|
||||
},
|
||||
"failures": failures,
|
||||
"mismatches": mismatches,
|
||||
"notes": notes,
|
||||
"diagnostics": diagnostics,
|
||||
}
|
||||
markdown = render_markdown(report, results, scenarios)
|
||||
if args.json:
|
||||
with open(args.json, "w", encoding="utf-8") as f:
|
||||
json.dump(report, f, indent=1)
|
||||
if args.markdown:
|
||||
with open(args.markdown, "w", encoding="utf-8") as f:
|
||||
f.write(markdown + "\n")
|
||||
print(markdown)
|
||||
|
||||
status = 1 if failures or mismatches else 0
|
||||
if args.gate:
|
||||
for problem in gate(results, scenarios, baseline, args.gate, args.max_slowdown, args.max_memory):
|
||||
print(f"::error::performance regression: {problem}", flush=True)
|
||||
status = 1
|
||||
return status
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
|
||||
)
|
||||
sub = parser.add_subparsers(dest="command", required=True)
|
||||
listen = sub.add_parser("serve", help="run the loopback listeners (started by `run`)")
|
||||
listen.add_argument("--address", action="append", default=[], help="address to listen on")
|
||||
bench = sub.add_parser("run", help="benchmark the given builds")
|
||||
bench.add_argument("--build", action="append", required=True, metavar="NAME=PATH")
|
||||
bench.add_argument("--label", action="append", default=[], metavar="NAME=TEXT")
|
||||
bench.add_argument("--baseline", help="build the others are compared with (default: the first)")
|
||||
bench.add_argument("--runs", type=int, default=9, help="interleaved runs per build and scenario")
|
||||
bench.add_argument(
|
||||
"--filtered", metavar="START-END", help="ports the runner drops; enables filtered scenarios"
|
||||
)
|
||||
bench.add_argument(
|
||||
"--delayed-host",
|
||||
metavar="ADDRESS",
|
||||
help="address behind an artificial delay, with its own `serve`; enables delayed scenarios",
|
||||
)
|
||||
bench.add_argument(
|
||||
"--delayed-listeners", metavar="FILE", help="first output line of that host's `serve`"
|
||||
)
|
||||
bench.add_argument("--delay-ms", type=int, default=5, help="that delay, for the summary")
|
||||
bench.add_argument("--only", metavar="NAMES", help="comma-separated scenarios to run")
|
||||
bench.add_argument("--json", help="write the raw results to this file")
|
||||
bench.add_argument("--markdown", help="write the Markdown summary to this file")
|
||||
bench.add_argument("--gate", metavar="NAME", help="fail if this build regresses against the baseline")
|
||||
bench.add_argument("--max-slowdown", type=float, default=15.0, help="percent, throughput and wall time")
|
||||
bench.add_argument("--max-memory", type=float, default=25.0, help="percent, peak RSS")
|
||||
args = parser.parse_args()
|
||||
# The summary contains emoji; Windows runners default to a legacy code page.
|
||||
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||
return serve(args.address or [LOOPBACK]) if args.command == "serve" else run(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,10 +0,0 @@
|
||||
name: Check Markdown links
|
||||
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
markdown-link-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: gaurav-nelson/github-action-markdown-link-check@v1
|
||||
@@ -1,55 +0,0 @@
|
||||
on: [push, pull_request]
|
||||
|
||||
name: Continuous integration
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
|
||||
test:
|
||||
name: Test Suite
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
|
||||
fmt:
|
||||
name: Rustfmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- run: rustup component add rustfmt
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
|
||||
links:
|
||||
name: markdown-link-check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: gaurav-nelson/github-action-markdown-link-check@v1
|
||||
@@ -0,0 +1,201 @@
|
||||
name: CD Pipeline
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
# Build jobs only need to read the repository. The publish job below is the
|
||||
# only job that gets write access, and it only runs for tag pushes.
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build-nix:
|
||||
env:
|
||||
IN_PIPELINE: true
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
type: [ubuntu-x64, ubuntu-x86, armv7, aarch64]
|
||||
include:
|
||||
- type: ubuntu-x64
|
||||
os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-musl
|
||||
name: x86_64-linux-rustscan
|
||||
path: target/x86_64-unknown-linux-musl/release/rustscan
|
||||
pkg_config_path: /usr/lib/x86_64-linux-gnu/pkgconfig
|
||||
- type: ubuntu-x86
|
||||
os: ubuntu-latest
|
||||
target: i686-unknown-linux-musl
|
||||
name: x86-linux-rustscan
|
||||
path: target/i686-unknown-linux-musl/release/rustscan
|
||||
pkg_config_path: /usr/lib/i686-linux-gnu/pkgconfig
|
||||
- type: armv7
|
||||
os: ubuntu-latest
|
||||
target: armv7-unknown-linux-gnueabihf
|
||||
name: armv7-linux-rustscan
|
||||
path: target/armv7-unknown-linux-gnueabihf/release/rustscan
|
||||
pkg_config_path: /usr/lib/x86_64-linux-gnu/pkgconfig
|
||||
- type: aarch64
|
||||
os: ubuntu-latest
|
||||
target: aarch64-unknown-linux-gnu
|
||||
name: aarch64-linux-rustscan
|
||||
path: target/aarch64-unknown-linux-gnu/release/rustscan
|
||||
pkg_config_path: /usr/lib/x86_64-linux-gnu/pkgconfig
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Cache cargo & target directories
|
||||
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
- name: Build binary
|
||||
uses: houseabsolute/actions-rust-cross@21b0f18dc621b25bfae556ff2791fca4173121e8 # v1.0.8
|
||||
with:
|
||||
command: build
|
||||
target: ${{ matrix.target }}
|
||||
args: "--locked --release"
|
||||
strip: true
|
||||
toolchain: stable
|
||||
- name: Package and upload artifacts
|
||||
uses: houseabsolute/actions-rust-release@9e126fd5fc18f4cecf358b3909f3991ad59663dc # v1.0.1
|
||||
with:
|
||||
executable-name: rustscan
|
||||
target: ${{ matrix.target }}
|
||||
archive-name: ${{ matrix.name }}
|
||||
changes-file: ""
|
||||
|
||||
build-deb:
|
||||
needs: [build-nix]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
IN_PIPELINE: true
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Install cargo-deb
|
||||
run: cargo install -f cargo-deb
|
||||
- uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
|
||||
with:
|
||||
packages: musl-tools # provides musl-gcc
|
||||
version: 1.0
|
||||
- name: Install musl toolchain
|
||||
run: rustup target add x86_64-unknown-linux-musl
|
||||
- name: Deb Build
|
||||
run: cargo deb --target=x86_64-unknown-linux-musl
|
||||
- name: Upload Deb Artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: rustscan.deb
|
||||
path: ./target/x86_64-unknown-linux-musl/debian/*
|
||||
|
||||
build-macos:
|
||||
env:
|
||||
IN_PIPELINE: true
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Cache cargo & target directories
|
||||
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
- name: Build binary
|
||||
uses: houseabsolute/actions-rust-cross@21b0f18dc621b25bfae556ff2791fca4173121e8 # v1.0.8
|
||||
with:
|
||||
command: build
|
||||
target: x86_64-apple-darwin
|
||||
args: "--locked --release"
|
||||
strip: true
|
||||
toolchain: stable
|
||||
- name: Package and upload artifacts
|
||||
uses: houseabsolute/actions-rust-release@9e126fd5fc18f4cecf358b3909f3991ad59663dc # v1.0.1
|
||||
with:
|
||||
executable-name: rustscan
|
||||
target: x86_64-apple-darwin
|
||||
archive-name: x86_64-macos-rustscan
|
||||
changes-file: ""
|
||||
|
||||
build-macos-aarch64:
|
||||
env:
|
||||
IN_PIPELINE: true
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Cache cargo & target directories
|
||||
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
- name: Build binary
|
||||
uses: houseabsolute/actions-rust-cross@21b0f18dc621b25bfae556ff2791fca4173121e8 # v1.0.8
|
||||
with:
|
||||
command: build
|
||||
target: aarch64-apple-darwin
|
||||
args: "--locked --release"
|
||||
strip: true
|
||||
toolchain: stable
|
||||
- name: Package and upload artifacts
|
||||
uses: houseabsolute/actions-rust-release@9e126fd5fc18f4cecf358b3909f3991ad59663dc # v1.0.1
|
||||
with:
|
||||
executable-name: rustscan
|
||||
target: aarch64-apple-darwin
|
||||
archive-name: aarch64-macos-rustscan
|
||||
changes-file: ""
|
||||
|
||||
build-windows:
|
||||
env:
|
||||
IN_PIPELINE: true
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
type: [windows-x64, windows-x86]
|
||||
include:
|
||||
- type: windows-x64
|
||||
os: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
name: x86_64-windows-rustscan
|
||||
- type: windows-x86
|
||||
os: windows-latest
|
||||
target: i686-pc-windows-msvc
|
||||
name: x86-windows-rustscan
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Cache cargo & target directories
|
||||
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
- name: Build binary
|
||||
uses: houseabsolute/actions-rust-cross@21b0f18dc621b25bfae556ff2791fca4173121e8 # v1.0.8
|
||||
with:
|
||||
command: build
|
||||
target: ${{ matrix.target }}
|
||||
args: "--locked --release"
|
||||
strip: true
|
||||
toolchain: stable
|
||||
- name: Package and upload artifacts
|
||||
uses: houseabsolute/actions-rust-release@9e126fd5fc18f4cecf358b3909f3991ad59663dc # v1.0.1
|
||||
with:
|
||||
executable-name: rustscan
|
||||
target: ${{ matrix.target }}
|
||||
archive-name: ${{ matrix.name }}
|
||||
changes-file: ""
|
||||
|
||||
# Runs only for tag pushes (tags can only be created by maintainers), and
|
||||
# only after every build job succeeded, so a release never ships partially.
|
||||
publish:
|
||||
if: github.ref_type == 'tag'
|
||||
needs:
|
||||
- build-nix
|
||||
- build-deb
|
||||
- build-macos
|
||||
- build-macos-aarch64
|
||||
- build-windows
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write # create the release / upload release assets
|
||||
steps:
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
merge-multiple: true
|
||||
- name: Publish artifacts to GitHub release
|
||||
uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
|
||||
with:
|
||||
fail_on_unmatched_files: true
|
||||
files: |
|
||||
*.deb
|
||||
*.sha256
|
||||
*.tar.gz
|
||||
*.zip
|
||||
@@ -1,18 +0,0 @@
|
||||
name: Check Markdown links
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
schedule:
|
||||
# Run everyday at 9:00 AM (See https://pubs.opengroup.org/onlinepubs/9699919799/utilities/crontab.html#tag_20_25_07)
|
||||
- cron: "0 9 * * *"
|
||||
|
||||
jobs:
|
||||
markdown-link-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: gaurav-nelson/github-action-markdown-link-check@v1
|
||||
with:
|
||||
use-quiet-mode: 'yes'
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"_comment": "Config for markdown-link-check. Resolves issue #540.",
|
||||
"ignorePatterns": [
|
||||
{
|
||||
"pattern": "^https://github.com/RustScan/RustScan/workflows/Continuous%20integration/badge.svg"
|
||||
},
|
||||
{
|
||||
"pattern": "^https://crates.io/crates/rustscan/"
|
||||
},
|
||||
{
|
||||
"pattern": "^https://bees.substack.com/p/making-hacking-accessible"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
name: Scan benchmark
|
||||
|
||||
# Compares the scanning speed and resource use of a change with its base.
|
||||
#
|
||||
# Each run builds both versions in release mode and runs
|
||||
# .github/scan-benchmark/scan_bench.py, which scans listeners it opens on
|
||||
# 127.0.0.1 inside the runner (plus closed and, on Linux, filtered ports) and
|
||||
# reports medians over interleaved runs. Nothing outside the runner is
|
||||
# scanned. The unit tests deliberately never open sockets; this workflow is
|
||||
# where scanning behaviour and performance are measured.
|
||||
#
|
||||
# It only runs for pull requests that touch the scanner, its runtime or its
|
||||
# dependencies, or when started by hand, so it does not slow down normal CI.
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [master]
|
||||
paths:
|
||||
- "src/scanner/**"
|
||||
- "src/main.rs"
|
||||
- "src/lib.rs"
|
||||
- "Cargo.toml"
|
||||
- "Cargo.lock"
|
||||
- ".github/scan-benchmark/**"
|
||||
- ".github/workflows/scan-benchmark.yml"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
base:
|
||||
description: "Branch, tag or commit to compare against"
|
||||
default: "master"
|
||||
runs:
|
||||
description: "Interleaved runs per build and scenario"
|
||||
default: "9"
|
||||
extra:
|
||||
description: "More builds to compare, as space-separated NAME=REF pairs"
|
||||
default: ""
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: scan-benchmark-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
benchmark:
|
||||
name: Benchmark (${{ matrix.os }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 90
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, ubuntu-24.04-arm, macos-latest, windows-latest]
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
env:
|
||||
# Ports the Linux runners drop, to measure filtered ports (timeouts).
|
||||
# They lie above Linux's ephemeral port range (32768-60999).
|
||||
FILTERED_PORTS: "61000-62999"
|
||||
# The Linux runners' host behind a 5 ms delay (see "Set up a host 5 ms away").
|
||||
DELAYED_HOST: "10.200.0.2"
|
||||
RUNS: ${{ inputs.runs || '9' }}
|
||||
EXTRA: ${{ inputs.extra }}
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Check the benchmark script
|
||||
run: python -m py_compile .github/scan-benchmark/scan_bench.py
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2.0.0
|
||||
with:
|
||||
toolchain: stable
|
||||
# build.rs formats the generated payload table with rustfmt.
|
||||
components: rustfmt
|
||||
# Caching is handled by Swatinem/rust-cache below.
|
||||
cache: false
|
||||
rustflags: ""
|
||||
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
|
||||
- name: Build the change
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
PR_HEAD: ${{ github.event.pull_request.head.sha }}
|
||||
run: |
|
||||
exe=""
|
||||
if [ "$RUNNER_OS" = Windows ]; then exe=".exe"; fi
|
||||
echo "EXE=$exe" >> "$GITHUB_ENV"
|
||||
mkdir -p "$RUNNER_TEMP/bin"
|
||||
cargo build --release --locked
|
||||
cp "target/release/rustscan$exe" "$RUNNER_TEMP/bin/head$exe"
|
||||
if [ -n "$PR_NUMBER" ]; then
|
||||
label="#$PR_NUMBER at ${PR_HEAD::7}, merged into its base"
|
||||
else
|
||||
label="$(git log -1 --format='%h %s' HEAD)"
|
||||
fi
|
||||
echo "HEAD_LABEL=$label" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Build the base and any extra builds
|
||||
env:
|
||||
EVENT: ${{ github.event_name }}
|
||||
PR_BASE: ${{ github.event.pull_request.base.sha }}
|
||||
INPUT_BASE: ${{ inputs.base }}
|
||||
run: |
|
||||
build_ref() {
|
||||
local name="$1" ref="$2"
|
||||
if [[ ! "$name" =~ ^[A-Za-z0-9._+-]+$ ]]; then
|
||||
echo "::error::invalid build name '$name'"
|
||||
exit 1
|
||||
fi
|
||||
git fetch --no-tags --depth=1 origin "$ref"
|
||||
git worktree add --detach "$RUNNER_TEMP/src-$name" FETCH_HEAD
|
||||
cargo build --release --locked \
|
||||
--manifest-path "$RUNNER_TEMP/src-$name/Cargo.toml" --target-dir target
|
||||
cp "target/release/rustscan$EXE" "$RUNNER_TEMP/bin/$name$EXE"
|
||||
echo "$name=$(git log -1 --format='%h %s' FETCH_HEAD)" >> "$RUNNER_TEMP/labels"
|
||||
}
|
||||
|
||||
if [ "$EVENT" = pull_request ]; then base="$PR_BASE"; else base="${INPUT_BASE:-master}"; fi
|
||||
build_ref base "$base"
|
||||
for spec in $EXTRA; do
|
||||
build_ref "${spec%%=*}" "${spec#*=}"
|
||||
done
|
||||
cat "$RUNNER_TEMP/labels"
|
||||
|
||||
- name: Drop traffic to the filtered ports
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
if ! command -v iptables >/dev/null; then
|
||||
echo "::warning::iptables is not available; skipping the filtered-port scenarios"
|
||||
exit 0
|
||||
fi
|
||||
for proto in tcp udp; do
|
||||
sudo iptables -I INPUT -i lo -p "$proto" --dport "${FILTERED_PORTS/-/:}" -j DROP
|
||||
done
|
||||
sudo iptables -S INPUT
|
||||
echo "FILTERED_OK=1" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Set up a host 5 ms away
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
# A network namespace on the runner, reached over a veth pair whose
|
||||
# outbound side delays every packet. Its answers (SYN-ACKs, resets,
|
||||
# ICMP errors, UDP replies) arrive a round trip after the probe, as
|
||||
# from a remote host, instead of while the probe is being sent.
|
||||
skip() {
|
||||
echo "::warning::$1; skipping the delayed-host scenarios"
|
||||
exit 0
|
||||
}
|
||||
sudo ip netns add far || skip "cannot create a network namespace"
|
||||
sudo ip link add rs-near type veth peer name rs-far || skip "cannot create a veth pair"
|
||||
sudo ip link set rs-far netns far || skip "cannot move the veth end"
|
||||
sudo ip addr add 10.200.0.1/30 dev rs-near || skip "cannot address the veth pair"
|
||||
sudo ip link set rs-near up || skip "cannot bring up the veth pair"
|
||||
sudo ip netns exec far ip addr add "$DELAYED_HOST/30" dev rs-far || skip "cannot address the namespace"
|
||||
sudo ip netns exec far ip link set rs-far up || skip "cannot bring up the namespace"
|
||||
sudo ip netns exec far ip link set lo up || true
|
||||
# Answer every probe to a closed UDP port, like the loopback
|
||||
# interface does (ICMP errors are rate limited everywhere else).
|
||||
for setting in icmp_ratelimit=0 icmp_msgs_per_sec=100000000 icmp_msgs_burst=100000000; do
|
||||
sudo ip netns exec far sysctl -qw "net.ipv4.$setting" || true
|
||||
done
|
||||
sudo tc qdisc add dev rs-near root netem delay 5ms limit 1000000 || skip "netem is not available"
|
||||
tc qdisc show dev rs-near
|
||||
|
||||
sudo ip netns exec far "$(command -v python)" .github/scan-benchmark/scan_bench.py \
|
||||
serve --address "$DELAYED_HOST" > "$RUNNER_TEMP/far.json" 2> "$RUNNER_TEMP/far.log" &
|
||||
for _ in $(seq 100); do
|
||||
if [ -s "$RUNNER_TEMP/far.json" ]; then break; fi
|
||||
sleep 0.1
|
||||
done
|
||||
if [ ! -s "$RUNNER_TEMP/far.json" ]; then
|
||||
cat "$RUNNER_TEMP/far.log" || true
|
||||
skip "the delayed host's listeners did not start"
|
||||
fi
|
||||
head -c 200 "$RUNNER_TEMP/far.json"; echo
|
||||
echo "DELAYED_OK=1" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Provision macOS TCP memory for repeated socket batches
|
||||
if: runner.os == 'macOS'
|
||||
run: |
|
||||
# XNU's TCP accounting hard limit otherwise makes both builds miss
|
||||
# listeners with ENOBUFS during repeated sweeps. Apply one budget to
|
||||
# both builds before timing; keep all input sizes and gates intact.
|
||||
sudo "$(command -v python)" .github/scan-benchmark/macos_tcp_memory.py
|
||||
|
||||
- name: Describe the runner
|
||||
continue-on-error: true
|
||||
run: |
|
||||
uname -a
|
||||
if [ "$RUNNER_OS" = Linux ]; then
|
||||
nproc
|
||||
grep -m1 'model name' /proc/cpuinfo || true
|
||||
sysctl net.ipv4.ip_local_port_range net.ipv4.tcp_tw_reuse net.core.somaxconn || true
|
||||
ss -Hltnu | sort -k5 | head -n 50 || true
|
||||
elif [ "$RUNNER_OS" = macOS ]; then
|
||||
for oid in hw.ncpu machdep.cpu.brand_string kern.maxfilesperproc \
|
||||
net.inet.ip.portrange.first net.inet.ip.portrange.last \
|
||||
kern.ipc.maxsockbuf kern.ipc.nmbclusters; do
|
||||
sysctl "$oid" || true
|
||||
done
|
||||
fi
|
||||
|
||||
- name: Run the benchmark
|
||||
run: |
|
||||
# Allow the largest batch size without RustScan shrinking it to fit
|
||||
# the descriptor limit (macOS defaults to 256, and caps processes at
|
||||
# kern.maxfilesperproc, 10240 on its runners).
|
||||
if [ "$RUNNER_OS" != Windows ]; then
|
||||
for limit in 65536 24576 10240; do
|
||||
ulimit -n "$limit" 2>/dev/null && break
|
||||
done
|
||||
echo "open-file limit: $(ulimit -n)"
|
||||
fi
|
||||
|
||||
# Fail on wrong results, or if the change is clearly slower or
|
||||
# bigger than its base (see --max-slowdown/--max-memory).
|
||||
args=(--runs "$RUNS" --gate head --json bench-results.json --markdown bench-results.md)
|
||||
if [ -n "${FILTERED_OK:-}" ]; then args+=(--filtered "$FILTERED_PORTS"); fi
|
||||
if [ -n "${DELAYED_OK:-}" ]; then
|
||||
args+=(--delayed-host "$DELAYED_HOST" --delayed-listeners "$RUNNER_TEMP/far.json")
|
||||
fi
|
||||
args+=(--build "base=$RUNNER_TEMP/bin/base$EXE")
|
||||
args+=(--build "head=$RUNNER_TEMP/bin/head$EXE" --label "head=$HEAD_LABEL")
|
||||
while IFS= read -r line; do
|
||||
name="${line%%=*}"
|
||||
args+=(--label "$line")
|
||||
if [ "$name" != base ]; then
|
||||
args+=(--build "$name=$RUNNER_TEMP/bin/$name$EXE")
|
||||
fi
|
||||
done < "$RUNNER_TEMP/labels"
|
||||
|
||||
python .github/scan-benchmark/scan_bench.py run "${args[@]}"
|
||||
|
||||
- name: Publish the summary
|
||||
if: always()
|
||||
run: |
|
||||
if [ -f bench-results.md ]; then cat bench-results.md >> "$GITHUB_STEP_SUMMARY"; fi
|
||||
|
||||
- name: Upload the results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: scan-benchmark-${{ matrix.os }}
|
||||
path: |
|
||||
bench-results.json
|
||||
bench-results.md
|
||||
if-no-files-found: ignore
|
||||
@@ -0,0 +1,45 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [master]
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test Suite
|
||||
strategy:
|
||||
matrix:
|
||||
# ubuntu-24.04-arm is the GitHub-hosted arm64 Linux runner;
|
||||
# macos-latest already runs on Apple Silicon (arm64).
|
||||
os: [ubuntu-latest, ubuntu-24.04-arm, macos-latest, windows-latest]
|
||||
rust: [stable]
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2.0.0
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
components: clippy, rustfmt
|
||||
# Caching is handled by Swatinem/rust-cache below.
|
||||
cache: false
|
||||
# Keep RUSTFLAGS unset; `just test` already denies Clippy warnings.
|
||||
rustflags: ""
|
||||
|
||||
- uses: taiki-e/install-action@nextest
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Install Just
|
||||
uses: extractions/setup-just@v4
|
||||
|
||||
- name: Run tests
|
||||
run: just test
|
||||
@@ -1,6 +1,8 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
# generated file by build.rs for mapping nmap-payloads to rust
|
||||
src/generated.rs
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
@@ -43,3 +45,82 @@ pkg/
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/rust,archlinuxpackages
|
||||
*.deb
|
||||
|
||||
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
# .idea/artifacts
|
||||
# .idea/compiler.xml
|
||||
# .idea/jarRepositories.xml
|
||||
# .idea/modules.xml
|
||||
# .idea/*.iml
|
||||
# .idea/modules
|
||||
# *.iml
|
||||
# *.ipr
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
.idea/*
|
||||
.idea
|
||||
.idea*
|
||||
|
||||
settings.json
|
||||
@@ -0,0 +1,3 @@
|
||||
[submodule "src/rustscan_scripting_engine"]
|
||||
path = src/rustscan_scripting_engine
|
||||
url = https://github.com/RustScan/rustscan_scripting_engine
|
||||
@@ -1,56 +1,33 @@
|
||||
language: rust
|
||||
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
# - windows
|
||||
|
||||
- linux
|
||||
- osx
|
||||
arch:
|
||||
- amd64
|
||||
- arm64
|
||||
|
||||
- amd64
|
||||
- arm64
|
||||
services:
|
||||
- docker
|
||||
|
||||
- docker
|
||||
before_install:
|
||||
- rustup component add rustfmt
|
||||
|
||||
# cargo test
|
||||
rust:
|
||||
- stable
|
||||
- beta
|
||||
- stable
|
||||
- beta
|
||||
jobs:
|
||||
fast_finish: true
|
||||
|
||||
# Cargo fmt
|
||||
script:
|
||||
- cargo build
|
||||
- cargo test
|
||||
- cargo fmt -- --check
|
||||
|
||||
# automatic deployment to cargo on release
|
||||
# deploy:
|
||||
|
||||
# provider: releases
|
||||
# api_key: "GITHUB OAUTH TOKEN"
|
||||
# file: "FILE TO UPLOAD"
|
||||
# skip_cleanup: true
|
||||
# on:
|
||||
# tags: true
|
||||
|
||||
# after_success:
|
||||
# - wget https://raw.githubusercontent.com/DiscordHooks/travis-ci-discord-webhook/master/send.sh
|
||||
# - chmod +x send.sh
|
||||
# - ./send.sh success $WEBHOOK_URL
|
||||
|
||||
# only deploy on master branch
|
||||
# on:
|
||||
# branch: master
|
||||
# provider: cargo
|
||||
# token: $crates
|
||||
|
||||
|
||||
after_failure:
|
||||
- wget https://raw.githubusercontent.com/DiscordHooks/travis-ci-discord-webhook/master/send.sh
|
||||
- chmod +x send.sh
|
||||
- ./send.sh failure $WEBHOOK_URL
|
||||
after_success: |
|
||||
if [ -n "${GITHUB_TOKEN}" ]; then
|
||||
[ "${TRAVIS_BRANCH} " = master ] &&
|
||||
[ "${TRAVIS_PULL_REQUEST}" = false ] &&
|
||||
cargo doc &&
|
||||
echo "<meta http-equiv=refresh content=0;url=rustscan/index.html>" > target/doc/index.html &&
|
||||
sudo pip install ghp-import &&
|
||||
ghp-import -n target/doc &&
|
||||
git push -fq https://${GITHUB_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git gh-pages
|
||||
fi
|
||||
env:
|
||||
global:
|
||||
secure: Gf24LG7Hr3Q0drWyPrvISTL35Z57DxdS/wIOo7v+ZdauzY3Qjvv3Lh4TY//C4sdN0g1CLj4yjg7LQOqF4XRdh2Qah7JZPv/i2B5uz9nwjTsoRzmmMWML9VtfT1u53nSFqsNeXbvpmoNG9rfMXcoiyd+Lv+EgniHphDEBlXvS9n9KMcpOi1F2x7bsH0EK37wQYKMTpCCd5TPUPdZou7BJNiZS3ikRLpTRFF7uIN4gYtRH9kU5uBSphWgnJ6eBm2ItzZcUPOgrjAoF4RkmZAzGG4idDh1Qkf3No3nZwT3U5NSk8h7owtJ4oADlRgYqYKCB4YcC9cAKCJgDsyMlKKMkEyagvw1seFnBLCY3ljsW4EklpVEZ0rVqZOx4pTkIaxIxytLTC9pHmdDSnhdhFknOcVAQQWjg+nAZfD07QUeAqUdr0i/vxQoZitvbNY0znzEarycE1waKvwlkiB4gt385U1CBfYldydnOnRKkdtLNrCLClu13HU0djmSXMhojQJhTuNxTHtZAf5fKcWIWwkVbNZ5MNKYjQh4rKufqW3o1iHphB8dfAGsDEPv0RQMtVbu6H4+clCouLkFlxHOIrrOJ5SXQ8cIATPhnJR7y4cSDdRaN0DRLRjeLAIKlbOGBtNuFC+BftocaTY5qN42aKAMqQcT30ycfiLuRVJNiQJ5pkto=
|
||||
@@ -14,22 +14,22 @@ appearance, race, religion, or sexual identity and orientation.
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
- Using welcoming and inclusive language
|
||||
- Being respectful of differing viewpoints and experiences
|
||||
- Gracefully accepting constructive criticism
|
||||
- Focusing on what is best for the community
|
||||
- Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
- The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
- Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
@@ -55,7 +55,7 @@ further defined and clarified by project maintainers.
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at brandon@skerritt.blog. All
|
||||
reported by contacting the project team at our discord via a private message to any of the owners. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
@@ -67,10 +67,10 @@ members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
This Code of Conduct is adapted from the Contributor Covenant homepage, version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
homepage: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
||||
@@ -1,33 +1,75 @@
|
||||
[package]
|
||||
name = "rustscan"
|
||||
version = "1.7.1"
|
||||
authors = ["Bee <brando[email protected]>"]
|
||||
version = "2.4.1"
|
||||
authors = ["Autumn <autum[email protected]>"]
|
||||
edition = "2018"
|
||||
description = "Faster Nmap Scanning with Rust"
|
||||
homepage = "https://github.com/rustscan/rustscan"
|
||||
repository = "https://github.com/rustscan/rustscan"
|
||||
license = "MIT"
|
||||
license = "GPL-3.0-only"
|
||||
keywords = ["port", "scanning", "nmap"]
|
||||
categories = ["command-line-utilities"]
|
||||
readme="README.md"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
readme = "README.md"
|
||||
exclude = [
|
||||
".github/*",
|
||||
"pictures/*",
|
||||
"rustscan-debbuilder/*",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
colored = "2.0.0"
|
||||
structopt = "0.3.16"
|
||||
async-std = "1.6.2"
|
||||
futures = "0.3"
|
||||
rlimit = "0.4.0"
|
||||
shell-words = "1.0.0"
|
||||
log = "0.4.0"
|
||||
env_logger = "0.7.1"
|
||||
dirs = "3.0.1"
|
||||
clap = { version = "4.6.1", features = ["derive", "wrap_help"] }
|
||||
colored = "3.1.1"
|
||||
# Async runtime for the scanner. hickory-resolver already depends on tokio, so
|
||||
# this adds no crates; only the features the scanner itself uses are listed.
|
||||
tokio = { version = "1.44.2", features = ["rt", "net", "time"] }
|
||||
# Only `FuturesUnordered`/`StreamExt` are used; the executor is not needed.
|
||||
futures = { version = "0.3", default-features = false, features = ["std"] }
|
||||
rlimit = "0.11.0"
|
||||
log = "0.4.32"
|
||||
env_logger = "0.11.10"
|
||||
anstream = "=1.0.0"
|
||||
dirs = "7.0.0"
|
||||
gcd = "2.0.1"
|
||||
rand = "0.7.3"
|
||||
colorful = "0.2.1"
|
||||
rand = "0.10.1"
|
||||
colorful = "0.3.2"
|
||||
ansi_term = "0.12.1"
|
||||
toml = "1.1.2"
|
||||
serde = "1.0.124"
|
||||
serde_derive = "1.0.116"
|
||||
cidr-utils = "0.7.1"
|
||||
itertools = "0.15.0"
|
||||
hickory-resolver = { version = "0.24.3", features = ["dns-over-rustls"] }
|
||||
anyhow = "1.0.40"
|
||||
text_placeholder = { version = "0.5", features = ["struct_context"] }
|
||||
once_cell = "1.21.4"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows-sys = { version = "0.61", features = ["Win32_Networking_WinSock"] }
|
||||
|
||||
[dev-dependencies]
|
||||
parameterized = "2.0.0"
|
||||
criterion = { version = "0.8", features = ["html_reports"] }
|
||||
|
||||
[package.metadata.deb]
|
||||
depends = "$auto, nmap"
|
||||
section = "rust"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
panic = 'abort'
|
||||
strip = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[[bin]]
|
||||
name = "rustscan"
|
||||
path = "src/main.rs"
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ["cfg(tarpaulin_include)"] }
|
||||
|
||||
[[bench]]
|
||||
name = "benchmark_helpers"
|
||||
harness = false
|
||||
@@ -1,22 +0,0 @@
|
||||
FROM rust:alpine as builder
|
||||
RUN apk update && \
|
||||
apk add build-base && \
|
||||
rm -rf /var/cache/apk/*
|
||||
|
||||
WORKDIR /usr/src/rustscan
|
||||
COPY . .
|
||||
RUN cargo install --path .
|
||||
|
||||
FROM alpine
|
||||
LABEL author="Hydragyrum <https://github.com/Hydragyrum>"
|
||||
RUN ulimit -n 100000 && \
|
||||
apk update && \
|
||||
apk add \
|
||||
nmap nmap-scripts wget \
|
||||
&& \
|
||||
rm -rf /var/cache/apk/*
|
||||
COPY --from=builder /usr/local/cargo/bin/rustscan /usr/local/bin/rustscan
|
||||
RUN addgroup -S rustscan && \
|
||||
adduser -S -G rustscan rustscan
|
||||
USER rustscan
|
||||
ENTRYPOINT ["/usr/local/bin/rustscan"]
|
||||
@@ -10,12 +10,12 @@ build:
|
||||
build-linux:
|
||||
@echo 'Building for Linux... 🐧'
|
||||
cross build --release --target=x86_64-unknown-linux-musl
|
||||
mkdir -p target/release-archives && tar -czf target/release-archives/rustscan-linux.tar.gz target/x86_64-unknown-linux-musl/release/rustscan
|
||||
mkdir -p target/release-archives && tar -C target/x86_64-unknown-linux-musl/release -czf target/release-archives/rustscan-linux.tar.gz rustscan
|
||||
|
||||
build-mac:
|
||||
@echo 'Building for MacOS... 🍏'
|
||||
cross build --release --target=x86_64-apple-darwin
|
||||
mkdir -p target/release-archives && tar -czf target/release-archives/rustscan-mac.tar.gz target/x86_64-apple-darwin/release/rustscan
|
||||
mkdir -p target/release-archives && tar -C target/x86_64-apple-darwin/release -czf target/release-archives/rustscan-mac.tar.gz rustscan
|
||||
|
||||
shasum:
|
||||
shasum -a 256 target/release-archives/rustscan-*.tar.gz
|
||||
@@ -1,371 +1,143 @@
|
||||
<p align="center">
|
||||
➡️
|
||||
<a href="https://discord.gg/GFrQsGy">Discord</a> |
|
||||
<a href="https://github.com/RustScan/RustScan#-full-installation-guide">Installation Guide</a> |
|
||||
<a href="https://github.com/RustScan/RustScan#-usage">Usage Guide</a>
|
||||
⬅️
|
||||
<br>
|
||||
<img src="pictures/rustscan.png">
|
||||
</p>
|
||||
<p align="center">
|
||||
<u><b> Turns a 17 minutes Nmap scan into 19 seconds. </b></u><br> Find all open ports <b>fast</b> with RustScan, automatically pipe them into Nmap.
|
||||
</p>
|
||||
<p align="center">
|
||||
<img alt="AUR version" src="https://img.shields.io/aur/version/rustscan-bin">
|
||||
<img src="https://img.shields.io/badge/Built%20with-Rust-Purple">
|
||||
<img alt="GitHub commit activity" src="https://img.shields.io/github/commit-activity/w/brandonskerritt/rustscan">
|
||||
<img alt="GitHub All Releases" src="https://img.shields.io/github/downloads/rustscan/rustscan/total?label=GitHub%20Downloads">
|
||||
<img alt="Crates.io" src="https://img.shields.io/crates/d/rustscan?label=Cargo%20Downloads">
|
||||
<img alt="Discord" src="https://img.shields.io/discord/736614461313515576">
|
||||
<img alt="Actions" src="https://github.com/RustScan/RustScan/workflows/Continuous%20integration/badge.svg?branch=master">
|
||||
</p>
|
||||
<hr>
|
||||
<div align="center" markdown="1">
|
||||
|
||||
| <p align="center"><a href="https://hub.docker.com/r/cmnatic/rustscan">🐋 Docker (Recommended) </a></p> | <p align="center"><a href="https://github.com/RustScan/RustScan/releases">👩💻 Kali / Debian </p> | <p align="center"><a href="https://aur.archlinux.org/packages/rustscan/">🏗️ Arch </a></p> | <p align="center"><a href="https://crates.io/crates/rustscan">🔧 Cargo (Universal) </a></p> |
|
||||
| ---- | ---- | ---- | --- |
|
||||
| <p align="center"><img src="pictures/docker.png" /></p> | <p align="center"><img src="pictures/kali.png" /></p> | <p align="center"><img src="pictures/arch.png" /></p> | <p align="center"><img src="pictures/rust.png" /></p>
|
||||
| `docker pull cmnatic/rustscan:debian-buster` <p>[Usage](https://github.com/RustScan/RustScan#docker-whale) | [Read the install guide](https://github.com/brandonskerritt/RustScan/blob/master/README.md#%EF%B8%8F-debian--kali) | `yay -S rustscan` | `cargo install rustscan`
|
||||
➡️ [Discord][discord] | [Installation Guide][toc-install] | [Usage Guide][usage-guide] ⬅️
|
||||
|
||||
<hr>
|
||||
<img src="pictures/rustscan.png" height=400px width=400px>
|
||||
|
||||
<!--<u>**The Modern Port Scanner.**</u>-->
|
||||
**Fast, smart, effective.**
|
||||
|
||||
![Arch Linux package][badge-1] ![Built with Rust][badge-2] ![GitHub All Releases][badge-3] ![Crates.io][badge-4] ![Discord][badge-5] ![Actions][badge-6]
|
||||
|
||||
</div>
|
||||
|
||||
# 🤔 What is this?
|
||||
1. Find ports quickly using Rustscan (**3 seconds at its fastest**).
|
||||
2. Automatically runs `nmap` on those ports.
|
||||
3. ???
|
||||
4. Profit!
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>⚡ Nmap <b>with RustScan</b> ⚡ </th>
|
||||
<th>🐢 Nmap 🐢</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Gif</th>
|
||||
<td><img src="pictures/with_rustscan.gif" alt="The guy she tells you not to worry about"></td>
|
||||
<td><img src="pictures/without_rustscan.gif" alt="You"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<td><b>39 seconds</b></td>
|
||||
<td><b>17 minutes and 41 seconds</b></td>
|
||||
</tr>
|
||||
</table>
|
||||
![fast][speed-1]
|
||||
|
||||
<sub><b>Note</b> This is an older gif. RustScan's current top speed is 3 seconds for all 65k ports. This gif is 26 seconds.</sub><br>
|
||||
The Modern Port Scanner. **Find ports quickly (3 seconds at its fastest)**. Run scripts through our scripting engine (Python, Lua, Shell supported).
|
||||
|
||||
RustScans **only** job is to reduce the friction between finding open ports and inputting them into nmap.
|
||||
# 🛠️ Installation
|
||||
|
||||
# ✨ Features
|
||||
* Scans all 65k ports in **8 seconds** (on 10k batch size).
|
||||
* Saves you time by automatically piping it into Nmap. No more manual copying and pasting!
|
||||
* Does one thing and does it well. **Only** purpose is to improve Nmap, not replace it!
|
||||
* Let's you choose what Nmap commands to run, or uses the default.
|
||||
* IPv6 Support
|
||||
You can install RustScan's binary from our [releases page](https://github.com/RustScan/RustScan/releases).
|
||||
|
||||
## ‼️ Important Links
|
||||
We would prefer you to install with a package manager so it is tested and works for your system.
|
||||
|
||||
| Installation Guide | Documentation | Discord |
|
||||
| ------------------ | ------------- | ------- |
|
||||
| 📖 [Installation Guide](https://github.com/RustScan/RustScan#-full-installation-guide) | 📚 [Documentation](https://github.com/RustScan/RustScan/issues/89) | 🦜 [Discord](https://discord.gg/GFrQsGy)
|
||||
|
||||
## 🙋 Table of Contents
|
||||
* 📖 [Installation Guide](https://github.com/RustScan/RustScan#-full-installation-guide)
|
||||
* 🐋 [Docker Usage](https://github.com/RustScan/RustScan#docker-whale)
|
||||
* 🦜 [Discord](https://discord.gg/GFrQsGy)
|
||||
* 🤸 [Usage](https://github.com/RustScan/RustScan#-usage)
|
||||
* 🎪 [Community](https://github.com/RustScan/RustScan#-community)
|
||||
|
||||
# 🔭 Why RustScan?
|
||||
Why spend time running fast scans and manually copying the ports, or waiting for a 20 minute scan to finish when you can just do all 65k ports in less than a minute?
|
||||
|
||||

|
||||
|
||||
**RustScan running in 8 seconds and finding all open ports out of 65k.**
|
||||
|
||||
## 📊 RustScan vs Nmap vs MassScan
|
||||
|
||||
| **Name** | RustScan | Nmap | Masscan |
|
||||
| ------------------------------------------------------------------------------------------ | -------- | ---- | ------- |
|
||||
| Fast | ✅ | ❌ | ✅ |
|
||||
| Actually useful | ❌ | ✅ | ❌ |
|
||||
| Realises it's not useful, and pipes the only useful data into the only useful port scanner | ✅ | ❌ | ❌ |
|
||||
|
||||
# 📖 Full Installation Guide
|
||||
**You need Nmap**. If you have Kali Linux or Parrot OS installed, you already have Nmap. If not, [follow the nmap install guide](https://nmap.org/download.html).
|
||||
|
||||
The easiest way to install RustScan is to use one of the packages provided for your system, such as HomeBrew or Yay for Arch Linux.
|
||||
|
||||
The most universal way is to use `cargo`, Rust's built in package manager (think Pip but for Rust). [Follow this guide to installing Rust & Cargo](https://doc.rust-lang.org/cargo/getting-started/installation.html).
|
||||
|
||||
If you face any issues at all, please leave a GitHub issue. I have only tested this on Linux, so there may be issues for Mac OS or Windows.
|
||||
|
||||
Note: sometimes Rust doesn't add Cargo to the path. Please see [this issue](https://github.com/rust-lang/rustup/issues/2436) for how to fix that.
|
||||
|
||||
## 🖥️ Debian / Kali
|
||||
|
||||
Download the .deb file from the releases page:
|
||||
|
||||
[https://github.com/brandonskerritt/RustScan/releases](https://github.com/brandonskerritt/RustScan/releases)
|
||||
|
||||
Run the commpand `dpkg -i` on the file.
|
||||
|
||||
Note: sometimes you can double click the file to achieve the same result.
|
||||
|
||||
## Docker :whale:
|
||||
|
||||
Docker is the recommended way of installing RustScan. This is because:
|
||||
* It has a high open file descriptor limit, which is one of the [main problems](https://github.com/RustScan/RustScan/issues/40) with RustScan. Now you don't have to fiddle around trying to understand your OS.
|
||||
* It works on all systems, regardless of OS. Even Windows, which we don't officially support.
|
||||
* The Docker image uses the latest build from Cargo, our main source-of-truth package. This means that you will always be using the latest version.
|
||||
* No need to install Rust, Cargo, or Nmap.
|
||||
|
||||
To install Docker, [follow their guide](https://docs.docker.com/engine/install/).
|
||||
|
||||
**Once Docker is installed, you can either build your own image using the `Dockerfile` (alpine) provided in the repo, or alternatively, use the published Docker image like below (most convenient)**
|
||||
|
||||
Simply run this command against the IP you want to target:
|
||||
|
||||
```
|
||||
docker run -it --rm --name rustscan cmnatic/rustscan:debian-buster rustscan 127.0.0.1
|
||||
```
|
||||
|
||||
Note: this will scan the Docker's localhost, not your own.
|
||||
|
||||
Once done, you will no longer need to re-download the image (except when RustScan updates) and can use RustScan like a normal application.
|
||||
|
||||
You will have to run this command every time, so we suggest aliasing it to something memorable.
|
||||
|
||||
```
|
||||
alias rustscan='docker run -it --rm --name rustscan cmnatic/rustscan:debian-buster rustscan'
|
||||
```
|
||||
|
||||
Then we can:
|
||||
|
||||
```
|
||||
rustscan 127.0.0.1 -t 500 -b 1500 -- -A
|
||||
```
|
||||
|
||||
#### To build your own image:
|
||||
|
||||
Download the repo:
|
||||
```
|
||||
git clone https://github.com/RustScan/RustScan.git
|
||||
```
|
||||
|
||||
Build away!
|
||||
```
|
||||
docker build -t <yourimagename> .
|
||||
```
|
||||
|
||||
## 🍺 HomeBrew
|
||||
|
||||
**Note for Mac users** Mac OS has a very, very small ulimit size. This will negatively impact RustScan by a significant amount. Please use the Docker container, or tell RustScan to up the ulimit size on every run.
|
||||
|
||||
Tap the brew:
|
||||
|
||||
```
|
||||
brew tap brandonskerritt/rustscan
|
||||
```
|
||||
|
||||
Install it:
|
||||
|
||||
```
|
||||
brew install rustscan
|
||||
```
|
||||
|
||||
## 🔧 Building it yourself
|
||||
|
||||
1. Git clone the repo.
|
||||
2. Install Rust. You can do this with `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh` which I took from the Rust website https://www.rust-lang.org/tools/install
|
||||
3. cd into the Git repo, and run `cargo build --release`
|
||||
4. The binary is located at `target/release/rustscan`
|
||||
5. Symlink to the binary or something. Whatever you want!
|
||||
|
||||
## 🦊 Community Distributions
|
||||
Here are all of RustScan's community distributions.
|
||||
|
||||
If you maintain a community distribution and want it listed here, leave an issue / pull request / Discord message or however you want to let us know.
|
||||
|
||||
* [OpenSuse](https://software.opensuse.org/package/rustscan?search_term=rustscan)
|
||||
RustScan is in many repositories already. Install it with whatever tools you wish:
|
||||
|
||||
[](https://repology.org/project/rustscan/versions)
|
||||
|
||||
RustScan only officially supports Cargo installations, if you want to use that please install Rust and then `cargo install rustscan`
|
||||
|
||||
Example installations include:
|
||||
|
||||
MacOS:
|
||||
|
||||
```
|
||||
brew install rustscan
|
||||
```
|
||||
|
||||
Arch:
|
||||
|
||||
```
|
||||
pacman -S rustscan
|
||||
```
|
||||
|
||||
# ✨ Features
|
||||
|
||||
- Scans all 65k ports in **3 seconds**.
|
||||
- Full scripting engine support. Automatically pipe results into Nmap, or use our scripts (or write your own) to do whatever you want.
|
||||
- Adaptive learning. RustScan improves the more you use it. No bloated machine learning here, just basic maths.
|
||||
- The usuals you would expect. IPv6, CIDR, file input and more.
|
||||
- Automatically pipes ports into Nmap.
|
||||
|
||||
## ‼️ Important Links
|
||||
|
||||
| <!--Installation Guide--> | <!--Documentation--> | <!--Discord--> |
|
||||
| :----------------------------------------: | :------------------------------------: | :-------------------------: |
|
||||
| :book: [Installation Guide][toc-install] | :books: [Documentation][links-table-2] | :parrot: [Discord][discord] |
|
||||
| | :penguin: [ARM Support](docs/arm-support.md) | |
|
||||
|
||||
## 🙋 Table of Contents
|
||||
|
||||
- 📖 [Installation Guide][toc-install]
|
||||
- 🐋 [Docker Usage][toc-docker-usage]
|
||||
- 🐧 [ARM Support](docs/arm-support.md)
|
||||
- 🦜 [Discord][discord]
|
||||
- 🤸 [Usage][usage-1]
|
||||
|
||||
# 🔭 Why RustScan?
|
||||
|
||||
RustScan is a modern take on the port scanner. Sleek & fast. All while providing extensive extendability to you.
|
||||
|
||||
Not to mention RustScan uses Adaptive Learning to improve itself over time, making it the best port scanner for **you**.
|
||||
|
||||
## 🧋 Speed
|
||||
|
||||
![fast][speed-1]
|
||||
|
||||
Speed is guaranteed via RustScan. However, if you want to run a slow scan due to stealth, that is possible too.
|
||||
|
||||
Firstly, let's talk code.
|
||||
|
||||
We have tests that check to see if RustScan is significantly slower than the previous version. If it is, the continuous integration fails, and we can't commit code to master unless we make it faster.
|
||||
|
||||
[HyperFine][speed-2] is used to monitor RustScan's performance over time to answer the question, "Are we getting faster? Are we getting slower?".
|
||||
|
||||
Every pull request is reviewed by **one** person, but more often than not, **two** people review it. We test it manually and ensure the code doesn't negatively affect performance.
|
||||
|
||||
[Read more here][speed-3].
|
||||
|
||||
## ⚙️ Extensible
|
||||
|
||||
![scripts][extensible-1]
|
||||
|
||||
### _RustScan piping results into the custom Python script_
|
||||
|
||||
RustScan has a new scripting engine that allows anyone to write scripts in most languages. Python, Lua, and Shell are all supported.
|
||||
|
||||
Want to take your found ports and pipe them into Nmap for further analysis? That's possible. Want to run `smb-enum` if SMB is found open? Possible.
|
||||
|
||||
The possibilities are endless -- and you can write scripts in whatever language you feel comfortable with.
|
||||
|
||||
[Read more here][extensible-2].
|
||||
|
||||
## 🌊 Adaptive
|
||||
|
||||
![adaptive][adaptive-1]
|
||||
|
||||
### _RustScan automatically fine-tunes itself to match the host OS_
|
||||
|
||||
RustScan has a cool set of features called "Adaptive Learning". These features "learn" about the environment you are scanning and how _you_ use RustScan to **improve itself over time**.
|
||||
|
||||
We use this umbrella term for any feature that fits this criterion. The list constantly changes, so [check out our wiki for more information][adaptive-learning].
|
||||
|
||||
## 👩🦯 Accessible
|
||||
|
||||
![fast][accessible-1]
|
||||
|
||||
RustScan is one of the first penetration testing tools that aims to be entirely accessible.
|
||||
|
||||
[Most penetration testing tools are not accessible][accessible-2], which negatively affects the whole industry.
|
||||
|
||||
RustScan has continuous integration testing that aims to ensure it is accessible, and we are constantly working on ways to improve our accessibility and ensure _everyone_ can use RustScan.
|
||||
|
||||
# 🤸 Usage
|
||||
|
||||
```
|
||||
rustscan -h
|
||||
```
|
||||
We have 2 usage guides. [Basic Usage][usage-1] and [Things you may want to do][usage-2].
|
||||
|
||||
```
|
||||
rustscan 1.7.1
|
||||
Fast Port Scanner built in Rust. WARNING Do not use this program against sensitive infrastructure since the specified
|
||||
server may not be able to handle this many socket connections at once. - Discord https://discord.gg/GFrQsGy - GitHub
|
||||
https://github.com/RustScan/RustScan
|
||||
|
||||
USAGE:
|
||||
rustscan [FLAGS] [OPTIONS] <ips>... [-- <command>...]
|
||||
|
||||
FLAGS:
|
||||
-a, --accessible
|
||||
-h, --help Prints help information
|
||||
-q, --quiet Quiet mode. Only output the ports. No Nmap. Useful for grep or outputting to a file
|
||||
-V, --version Prints version information
|
||||
|
||||
OPTIONS:
|
||||
-b, --batch-size <batch-size> The batch size for port scanning, it increases or slows the speed of scanning.
|
||||
Depends on the open file limit of your OS. If you do 65535 it will do every port
|
||||
at the same time. Although, your OS may not support this [default: 4500]
|
||||
--scan-order <scan-order> The order of scanning to be performed. The "serial" option will scan ports in
|
||||
ascending order while the "random" option will scan ports randomly [default:
|
||||
serial] [possible values: Serial, Random]
|
||||
-t, --timeout <timeout> The timeout in milliseconds before a port is assumed to be closed [default: 1500]
|
||||
-u, --ulimit <ulimit> Automatically ups the ULIMIT with the value you provided
|
||||
|
||||
ARGS:
|
||||
<ips>... A list of comma separated IP addresses to be scanned
|
||||
<command>... The Nmap arguments to run. To use the argument -A, end RustScan's args with '-- -A'. Example:
|
||||
'rustscan -T 1500 127.0.0.1 -- -A -sC'. This command adds -Pn -vvv -p $PORTS automatically to
|
||||
nmap. For things like --script '(safe and vuln)' enclose it in quotations marks \"'(safe and
|
||||
vuln)'\"")
|
||||
```
|
||||
|
||||
The format is `rustscan -b 500 -T 1500 192.168.0.1` to scan 192.168.0.1 with 500 batch size with a timeout of 1500ms. The timeout is how long RustScan waits for a response until it assumes the port is closed.
|
||||
|
||||
The batch size determines how fast RustScan is. Set it to 65k, and it will scan all 65k ports at the same time. This means at at 65k batch size, RustScan will take TIMEOUT long to scan all ports. Essentially, if timeout is 1000ms, **RustScan can scan in 1 second**.
|
||||
|
||||
Your operating system may not support this, but it is worth it to play around and see where your open file limit is. Shortly I will be releasing a dockerised version with a much larger open file limit, so this will be possible.
|
||||
|
||||
## ⚠️ WARNING
|
||||
|
||||
This program, by default, scans 5000 ports at a time (5000 per second).
|
||||
|
||||
This may cause damage to a server, or may make it incredibly obvious you are scanning the server.
|
||||
|
||||
There are 2 ways to deal with this;
|
||||
1. Decrease batch size
|
||||
`rustscan -b 10` will run 10 port scans for 1 second, and then another 10 for 1 second and so on.
|
||||
2. Increase timeout
|
||||
`rustscan -T 5000` will mean RustScan waits 5 seconds until it scans the next ports.
|
||||
|
||||
You can also use both of these at the same time, to make it as slow or as fast as you want. A fun favourite is 65535 batch size with 1 second timeout. Theoretically, this scans all 65535 ports in 1 second.
|
||||
|
||||
**Please** do not use this tool against sensitive servers. It is designed mainly for Capture the Flag events, not real world servers with sensitive data.
|
||||
|
||||
## 🚨 Thread Paniced at Main: Too Many Open Files
|
||||
This is the most common error found in RustScan.
|
||||
|
||||
The open file limit is how many open sockets you can have at any given time.
|
||||
|
||||
This limit changes from OS to OS.
|
||||
|
||||
RustScan does not automatically create defaults (other than 5000) like Nmap does with their -T1, -T2 system.
|
||||
|
||||
By figuring out for yourself the optimal batch size, you will know that RustScan is the most optimised port scanner for your system.
|
||||
|
||||
There are 2 things you can do:
|
||||
1. Decrease batch size
|
||||
2. Increase open file limit
|
||||
|
||||
Decreasing batch size slows down the program, so as long as it isn't too drastic, this is a good option.
|
||||
|
||||
Run these 3 commands:
|
||||
|
||||
```
|
||||
ulimit -a
|
||||
ulimit -Hn
|
||||
ulimit -Sn
|
||||
```
|
||||
|
||||
They will give you an idea on the open file limit of your OS.
|
||||
|
||||
If it says "250", run `rustscan -b 240` for a batch size of 240.
|
||||
|
||||
Increasing the open file limit increases speed, but poses danger. Although, **opening more file sockets on the specified IP address may damage it**.
|
||||
|
||||
To open more, set the ulimit to a higher number:
|
||||
|
||||
```
|
||||
ulimit -n 5000
|
||||
```
|
||||
|
||||
**Mac OS**
|
||||
Mac OS has, from what I can tell, a naturally very low open file descriptor limit. The limit for Ubuntu is 8800. The limit for Mac OS is 255!
|
||||
|
||||
In this case, I would say it is safe to increase the open file limit. As most Linux based OS' have limits in the thousands.
|
||||
|
||||
Although, if this breaks anything, please don't blame me.
|
||||
|
||||
**Windows Subsystem for Linux**
|
||||
Windows Subsystem for Linux does not support ulimit (see issue #39).
|
||||
|
||||
The best way is to use it on a host computer, in Docker, or in a VM that isn't WSL.
|
||||
|
||||
**Automatic Ulimit updating**
|
||||
We are currently working on automatic Ulimit updating. If it is too high, it will lower itself. If it is too low, it will suggest a higher Ulimit. Watch [this issue](https://github.com/brandonskerritt/RustScan/issues/25) for more.
|
||||
|
||||
## 🔌 Nmap Custom Flags
|
||||
To run your own nmap commands, end the RustScan command with `-- -A` where `--` indicates "end of RustScan flags, please do not parse anything further" and any flags after that will be entered into nmap.
|
||||
|
||||
RustScan automatically runs `nmap -vvv -p $PORTS $IP`. To make it run `-A`, execute the command `rustscan 127.0.0.1 -- -A`.
|
||||
|
||||
If you want to run commands such as `--script (vuln and safe)`, you will need to enclose it in quotations like so `--script '"(vuln and safe) or default"'`.
|
||||
|
||||
## 🎯 Increasing speed / accuracy
|
||||
* Batch size
|
||||
|
||||
This increases speed, by allowing us to process more at once. Something experimental I am working on is changing the open file limit. You can do this manually with `ulimit -n 70000` and then running rustscan with `-b 65535`. This _should_ scan all 65535 ports at the exact same time. But this is extremely experimental.
|
||||
|
||||
For non-experimental speed increases, slowly increase the batch size until it no longer gets open ports, or it breaks.
|
||||
|
||||
* Accuracy (and some speed)
|
||||
|
||||
To increase accuracy, the easiest way is to increase the timeout. The default is 1.5 seconds, by setting it to 4 seconds (4000) we are telling RustScan "if we do not hear back from a port in 4 seconds, assume it is closed".
|
||||
|
||||
Decreasing accuracy gives some speed bonus, but my testing found that batch size dramatically changed the speed whereas timeout did, but not so much.
|
||||
We also have documentation about our config file [here][config-file-here].
|
||||
|
||||
# 🎪 Community
|
||||
|
||||
Howdy Space Cow-Person 🤠🌌
|
||||
|
||||
RustScan is always looking for contributors. Whether that's spelling mistakes or major changes, your help is **wanted** and welcomed here.
|
||||
|
||||
Before contributing, read our [code of conduct](https://github.com/RustScan/RustScan/blob/master/CODE_OF_CONDUCT.md).
|
||||
|
||||
TL;DR if you abuse members of our community you will be **perma-banned** 🤗
|
||||
|
||||
RustScan has 2 major labels for GitHub issues you should look at:
|
||||
* Good First issue
|
||||
These are issues for newcomers to open source!
|
||||
[https://github.com/RustScan/RustScan/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22](https://github.com/RustScan/RustScan/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22)
|
||||
* Help wanted
|
||||
These are issues that aren't really for newcomers, but we could still do wiht help!
|
||||
[https://github.com/RustScan/RustScan/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22+label%3A%22help+wanted%22](https://github.com/RustScan/RustScan/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22+label%3A%22help+wanted%22)
|
||||
|
||||
If you want to, solve the issue or comment on the issue for help.
|
||||
|
||||
The flow for contributing to open source software is:
|
||||
* Fork the repo
|
||||
* Make changes
|
||||
* Pull request to the repo
|
||||
|
||||
And then comment on the issue that you've done.
|
||||
|
||||
RustScan also has some `// TODO`'s in the codebase, which are meant more for the core team but we wouldn't say no to help with these issues.
|
||||
|
||||
If you have any feature suggestions or bugs, leave a GitHub issue. We welcome any and all support :D
|
||||
|
||||
We communicate over Discord. [Click here](https://discord.gg/GFrQsGy) to join our Discord community!
|
||||
|
||||
## Rewarding you
|
||||
I cannot pay you :-( But, I can place your GitHub profile on the README under `#Contributors` as a thank you! :)
|
||||
|
||||
|
||||
Please read the [contributing.md file](contributing.md)
|
||||
[Contributing][community-1] Read this to learn how.
|
||||
|
||||
## Contributors ✨
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
[](#contributors-)
|
||||
|
||||
[](#contributors-)
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
|
||||
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
||||
@@ -375,7 +147,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
<!-- markdownlint-disable -->
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center"><a href="https://skerritt.blog"><img src="https://avatars3.githubusercontent.com/u/10378052?v=4" width="100px;" alt=""/><br /><sub><b>Brandon</b></sub></a><br /><a href="#infra-brandonskerritt" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/RustScan/RustScan/commits?author=brandonskerritt" title="Tests">⚠️</a> <a href="https://github.com/RustScan/RustScan/commits?author=brandonskerritt" title="Code">💻</a> <a href="#design-brandonskerritt" title="Design">🎨</a></td>
|
||||
<td align="center"><a href="https://skerritt.blog"><img src="https://avatars3.githubusercontent.com/u/10378052?v=4" width="100px;" alt=""/><br /><sub><b>Bee</b></sub></a><br /><a href="#infra-beeskerritt" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/RustScan/RustScan/commits?author=beeskerritt" title="Tests">⚠️</a> <a href="https://github.com/RustScan/RustScan/commits?author=beesan" title="Code">💻</a> <a href="#design-beeskerritt" title="Design">🎨</a></td>
|
||||
<td align="center"><a href="https://sakiir.ovh"><img src="https://avatars1.githubusercontent.com/u/9950578?v=4" width="100px;" alt=""/><br /><sub><b>SakiiR</b></sub></a><br /><a href="https://github.com/RustScan/RustScan/commits?author=SakiiR" title="Code">💻</a> <a href="https://github.com/RustScan/RustScan/issues?q=author%3ASakiiR" title="Bug reports">🐛</a></td>
|
||||
<td align="center"><a href="https://github.com/smackhack"><img src="https://avatars2.githubusercontent.com/u/48143394?v=4" width="100px;" alt=""/><br /><sub><b>smackhack</b></sub></a><br /><a href="#ideas-smackhack" title="Ideas, Planning, & Feedback">🤔</a> <a href="#example-smackhack" title="Examples">💡</a></td>
|
||||
<td align="center"><a href="http://bernardoamc.github.io/"><img src="https://avatars0.githubusercontent.com/u/428984?v=4" width="100px;" alt=""/><br /><sub><b>Bernardo Araujo</b></sub></a><br /><a href="https://github.com/RustScan/RustScan/commits?author=bernardoamc" title="Code">💻</a> <a href="https://github.com/RustScan/RustScan/issues?q=author%3Abernardoamc" title="Bug reports">🐛</a> <a href="#design-bernardoamc" title="Design">🎨</a></td>
|
||||
@@ -385,14 +157,81 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/Atul9"><img src="https://avatars1.githubusercontent.com/u/3390330?v=4" width="100px;" alt=""/><br /><sub><b>Atul Bhosale</b></sub></a><br /><a href="https://github.com/RustScan/RustScan/commits?author=Atul9" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://tgotwig.me"><img src="https://avatars0.githubusercontent.com/u/30773779?v=4" width="100px;" alt=""/><br /><sub><b>Thomas Gotwig</b></sub></a><br /><a href="#platform-TGotwig" title="Packaging/porting to new platform">📦</a></td>
|
||||
<td align="center"><a href="https://tgotwig.dev"><img src="https://avatars0.githubusercontent.com/u/30773779?v=4" width="100px;" alt=""/><br /><sub><b>Thomas Gotwig</b></sub></a><br /><a href="#platform-TGotwig" title="Packaging/porting to new platform">📦</a></td>
|
||||
<td align="center"><a href="https://github.com/remigourdon"><img src="https://avatars3.githubusercontent.com/u/2874133?v=4" width="100px;" alt=""/><br /><sub><b>Rémi Gourdon</b></sub></a><br /><a href="https://github.com/RustScan/RustScan/commits?author=remigourdon" title="Documentation">📖</a> <a href="https://github.com/RustScan/RustScan/commits?author=remigourdon" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://cmnatic.co.uk"><img src="https://avatars3.githubusercontent.com/u/4163116?v=4" width="100px;" alt=""/><br /><sub><b>Ben (CMNatic)</b></sub></a><br /><a href="https://github.com/RustScan/RustScan/commits?author=cmnatic" title="Code">💻</a> <a href="https://github.com/RustScan/RustScan/commits?author=cmnatic" title="Documentation">📖</a> <a href="#design-cmnatic" title="Design">🎨</a></td>
|
||||
<td align="center"><a href="https://github.com/Ferryistaken"><img src="https://avatars3.githubusercontent.com/u/47927670?v=4" width="100px;" alt=""/><br /><sub><b>Alessandro Ferrari</b></sub></a><br /><a href="#content-Ferryistaken" title="Content">🖋</a></td>
|
||||
<td align="center"><a href="https://github.com/Phenomite"><img src="https://avatars2.githubusercontent.com/u/8285537?v=4" width="100px;" alt=""/><br /><sub><b>Phenomite</b></sub></a><br /><a href="#content-Phenomite" title="Content">🖋</a></td>
|
||||
<td align="center"><a href="https://supersandro.de/"><img src="https://avatars2.githubusercontent.com/u/7258858?v=4" width="100px;" alt=""/><br /><sub><b>Sandro</b></sub></a><br /><a href="#content-SuperSandro2000" title="Content">🖋</a> <a href="https://github.com/RustScan/RustScan/issues?q=author%3ASuperSandro2000" title="Bug reports">🐛</a> <a href="https://github.com/RustScan/RustScan/commits?author=SuperSandro2000" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://swag.lgbt"><img src="https://avatars2.githubusercontent.com/u/25358963?v=4" width="100px;" alt=""/><br /><sub><b>Cass</b></sub></a><br /><a href="#platform-caass" title="Packaging/porting to new platform">📦</a> <a href="https://github.com/RustScan/RustScan/commits?author=caass" title="Code">💻</a> <a href="https://github.com/RustScan/RustScan/issues?q=author%3Acaass" title="Bug reports">🐛</a></td>
|
||||
<td align="center"><a href="https://github.com/niklasmohrin"><img src="https://avatars0.githubusercontent.com/u/47574893?v=4" width="100px;" alt=""/><br /><sub><b>Niklas Mohrin</b></sub></a><br /><a href="https://github.com/RustScan/RustScan/commits?author=niklasmohrin" title="Documentation">📖</a> <a href="https://github.com/RustScan/RustScan/commits?author=niklasmohrin" title="Code">💻</a> <a href="https://github.com/RustScan/RustScan/issues?q=author%3Aniklasmohrin" title="Bug reports">🐛</a></td>
|
||||
<td align="center"><a href="https://liberapay.com/Artem4/"><img src="https://avatars0.githubusercontent.com/u/5614476?v=4" width="100px;" alt=""/><br /><sub><b>Artem Polishchuk</b></sub></a><br /><a href="#platform-tim77" title="Packaging/porting to new platform">📦</a></td>
|
||||
<td align="center"><a href="https://github.com/buermarc"><img src="https://avatars2.githubusercontent.com/u/44375277?v=4" width="100px;" alt=""/><br /><sub><b>buermarc</b></sub></a><br /><a href="https://github.com/RustScan/RustScan/commits?author=buermarc" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/bergabman"><img src="https://avatars1.githubusercontent.com/u/44554109?v=4" width="100px;" alt=""/><br /><sub><b>bergabman</b></sub></a><br /><a href="https://github.com/RustScan/RustScan/commits?author=bergabman" title="Code">💻</a> <a href="https://github.com/RustScan/RustScan/issues?q=author%3Abergabman" title="Bug reports">🐛</a> <a href="#design-bergabman" title="Design">🎨</a></td>
|
||||
<td align="center"><a href="https://github.com/dmitris"><img src="https://avatars0.githubusercontent.com/u/31205?v=4" width="100px;" alt=""/><br /><sub><b>Dmitry Savintsev</b></sub></a><br /><a href="https://github.com/RustScan/RustScan/commits?author=dmitris" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/bofh69"><img src="https://avatars3.githubusercontent.com/u/1444315?v=4" width="100px;" alt=""/><br /><sub><b>Sebastian Andersson</b></sub></a><br /><a href="https://github.com/RustScan/RustScan/commits?author=bofh69" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/mattcorbin"><img src="https://avatars3.githubusercontent.com/u/6537765?v=4" width="100px;" alt=""/><br /><sub><b>Matt Corbin</b></sub></a><br /><a href="https://github.com/RustScan/RustScan/commits?author=mattcorbin" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://rootsploit.com"><img src="https://avatars2.githubusercontent.com/u/67270834?v=4" width="100px;" alt=""/><br /><sub><b>RootSploit</b></sub></a><br /><a href="#blog-rootsploit" title="Blogposts">📝</a></td>
|
||||
<td align="center"><a href="https://github.com/eiffel-fl"><img src="https://avatars2.githubusercontent.com/u/12171754?v=4" width="100px;" alt=""/><br /><sub><b>eiffel-fl</b></sub></a><br /><a href="https://github.com/RustScan/RustScan/commits?author=eiffel-fl" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/u5surf"><img src="https://avatars1.githubusercontent.com/u/14180225?v=4" width="100px;" alt=""/><br /><sub><b>Y.Horie</b></sub></a><br /><a href="https://github.com/RustScan/RustScan/commits?author=u5surf" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/okrplay"><img src="https://avatars3.githubusercontent.com/u/32576280?v=4" width="100px;" alt=""/><br /><sub><b>Oskar</b></sub></a><br /><a href="https://github.com/RustScan/RustScan/commits?author=okrplay" title="Code">💻</a> <a href="https://github.com/RustScan/RustScan/commits?author=okrplay" title="Tests">⚠️</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- markdownlint-enable -->
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
|
||||
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
|
||||
|
||||
<!--Links-->
|
||||
|
||||
[Docker]: https://hub.docker.com/r/cmnatic/rustscan "This is the recommended distribution of rustscan"
|
||||
[kali]: https://github.com/RustScan/RustScan/wiki/Installation-Guide#%EF%B8%8F-debian--kali "Read the install guide"
|
||||
[Kali/Debian]: https://github.com/RustScan/RustScan/releases "Kali Debian"
|
||||
[Arch-Linux]: https://archlinux.org/packages/extra/x86_64/rustscan/ "Arch Linux installation of Rustscan"
|
||||
[Homebrew]: https://formulae.brew.sh/formula/rustscan "Homebrew install of Rustscan"
|
||||
[usage-1]: https://github.com/RustScan/RustScan/wiki/Usage "Basic Usage of Rustscan"
|
||||
[usage-0]: https://github.com/RustScan/RustScan/wiki/Installation-Guide#docker-whale "Use Docker Rustscan"
|
||||
[config-file-here]: https://github.com/RustScan/RustScan/wiki/Config-File "RustScan Configuration File"
|
||||
[usage-2]: https://github.com/RustScan/RustScan/wiki/Things-you-may-want-to-do-with-RustScan-but-don't-understand-how "Things you may want to do with rustscan but don't know how"
|
||||
[community-1]: https://github.com/RustScan/RustScan/wiki/Contributing "Learn how to contribute"
|
||||
[distributions-1]: https://software.opensuse.org/package/rustscan?search_term=rustscan "Open Suse rustscan distribution"
|
||||
[distributions-2]: https://copr.fedorainfracloud.org/coprs/atim/rustscan/ "Rustscan in Fedora"
|
||||
[repology-1]: https://repology.org/project/rustscan/versions "Packaging Status"
|
||||
[install-1]: https://github.com/RustScan/RustScan/wiki/Installation-Guide "Installation guide"
|
||||
[accessible-2]: https://bees.substack.com/p/making-hacking-accessible "Making Hacking Accessible"
|
||||
[extensible-2]: https://github.com/RustScan/RustScan/wiki/RustScan-Scripting-Engine "Scripting Engine"
|
||||
[speed-2]: https://github.com/sharkdp/hyperfine "Hyperfine"
|
||||
[speed-3]: https://github.com/RustScan/RustScan/wiki/Increasing-Speed-&-Accuracy "Increasing Speed & Accuracy"
|
||||
[toc-community]: https://github.com/RustScan/RustScan#-community "Community"
|
||||
[links-table-1]: https://github.com/RustScan/RustScan#-full-installation-guide "Full installation guide"
|
||||
[links-table-2]: https://github.com/bee-san/RustScan/wiki "Rustscan"
|
||||
[discord]: http://discord.skerritt.blog "Discord blog"
|
||||
[toc-install]: https://github.com/RustScan/RustScan/wiki/Installation-Guide "Installation Guide Wiki"
|
||||
[toc-docker-usage]: https://github.com/RustScan/RustScan/wiki/Installation-Guide#docker- "Docker Installation Guide Wiki"
|
||||
[usage-guide]: https://github.com/RustScan/RustScan#-usage
|
||||
[adaptive-learning]: https://github.com/RustScan/RustScan/wiki/Adaptive-Learning "Adaptive Learning"
|
||||
|
||||
<!--Pictures-->
|
||||
|
||||
[DockerPic]: https://github.com/RustScan/RustScan/blob/master/pictures/docker.png?raw=true "Docker install"
|
||||
[Kali1]: https://github.com/RustScan/RustScan/blob/master/pictures/kali.png?raw=true "Kali Picture"
|
||||
[Arch]: https://github.com/RustScan/RustScan/blob/master/pictures/arch.png?raw=true "Arch Linux"
|
||||
[Apple]: https://raw.githubusercontent.com/RustScan/RustScan/master/pictures/apple.png?size "Apple"
|
||||
[rustscan-svg]: https://repology.org/badge/vertical-allrepos/rustscan.svg "Picture of rustscan repology"
|
||||
[accessible-1]: pictures/accessible.gif "Fast"
|
||||
[adaptive-1]: pictures/adaptive.gif "Adaptive"
|
||||
[extensible-1]: pictures/scripts.gif "Scripts"
|
||||
[speed-1]: pictures/fast.gif "Speed"
|
||||
[badge-1]: https://img.shields.io/archlinux/v/extra/x86_64/rustscan?style=plastic&logo=archlinux&link=https%3A%2F%2Farchlinux.org%2Fpackages%2Fextra%2Fx86_64%2Frustscan%2F
|
||||
[badge-2]: https://img.shields.io/badge/Built%20with-Rust-Purple
|
||||
[badge-3]: https://img.shields.io/github/downloads/rustscan/rustscan/total?label=GitHub%20Downloads
|
||||
[badge-4]: https://img.shields.io/crates/d/rustscan?label=Cargo%20Downloads
|
||||
[badge-5]: https://img.shields.io/discord/754001738184392704
|
||||
[badge-6]: https://github.com/RustScan/RustScan/actions/workflows/build.yml/badge.svg?branch=master
|
||||
@@ -0,0 +1,133 @@
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
use rustscan::generated::get_parsed_data;
|
||||
use rustscan::input::{Opts, PortRanges, ScanOrder};
|
||||
use rustscan::port_strategy::PortStrategy;
|
||||
use rustscan::scanner::{build_udp_payload_lookup, Scanner};
|
||||
use std::collections::BTreeMap;
|
||||
use std::hint::black_box;
|
||||
use std::net::IpAddr;
|
||||
use std::time::Duration;
|
||||
|
||||
fn bench_address() {
|
||||
let _addrs = ["127.0.0.1".parse::<IpAddr>().unwrap()];
|
||||
}
|
||||
|
||||
fn bench_port_strategy() {
|
||||
let range = PortRanges(vec![(1, 1_000)]);
|
||||
let _strategy = PortStrategy::pick(&Some(range.clone()), None, ScanOrder::Serial);
|
||||
}
|
||||
|
||||
fn bench_address_parsing() {
|
||||
let opts = Opts {
|
||||
addresses: vec![
|
||||
"127.0.0.1".to_owned(),
|
||||
"10.2.0.1".to_owned(),
|
||||
"192.168.0.0/24".to_owned(),
|
||||
],
|
||||
exclude_addresses: Some(vec![
|
||||
"10.0.0.0/8".to_owned(),
|
||||
"172.16.0.0/12".to_owned(),
|
||||
"192.168.0.0/16".to_owned(),
|
||||
"172.16.0.1".to_owned(),
|
||||
]),
|
||||
..Default::default()
|
||||
};
|
||||
let _ips = rustscan::address::parse_addresses(&opts);
|
||||
}
|
||||
|
||||
// Replicates the old UDP payload selection behavior:
|
||||
// scan the whole UDP payload map and find the last payload whose port list contains `port`.
|
||||
fn old_payload_for_port(udp_map: &'static BTreeMap<Vec<u16>, Vec<u8>>, port: u16) -> &'static [u8] {
|
||||
let mut payload: &'static [u8] = b"";
|
||||
for (ports, value) in udp_map.iter() {
|
||||
if ports.contains(&port) {
|
||||
payload = value.as_slice();
|
||||
}
|
||||
}
|
||||
payload
|
||||
}
|
||||
|
||||
fn criterion_benchmark(c: &mut Criterion) {
|
||||
// Benching helper functions
|
||||
c.bench_function("parse address", |b| b.iter(bench_address));
|
||||
|
||||
c.bench_function("port strategy", |b| b.iter(bench_port_strategy));
|
||||
|
||||
let mut address_group = c.benchmark_group("address parsing");
|
||||
address_group.measurement_time(Duration::from_secs(10));
|
||||
address_group.bench_function("parse addresses with exclusions", |b| {
|
||||
b.iter(bench_address_parsing)
|
||||
});
|
||||
address_group.finish();
|
||||
|
||||
// Exercise production port preparation without opening a socket. The
|
||||
// scanner has no target addresses, and runtime construction is not timed.
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
let mut preparation = c.benchmark_group("port preparation");
|
||||
preparation.sample_size(20);
|
||||
preparation.warm_up_time(Duration::from_millis(500));
|
||||
preparation.measurement_time(Duration::from_secs(2));
|
||||
for (port_count, excluded_count) in [
|
||||
(1, 1),
|
||||
(16, 4),
|
||||
(64, 1),
|
||||
(64, 4096),
|
||||
(4096, 0),
|
||||
(4096, 64),
|
||||
(4096, 4096),
|
||||
(65535, 0),
|
||||
(65535, 4),
|
||||
(65535, 64),
|
||||
(65535, 1024),
|
||||
(65535, 4096),
|
||||
] {
|
||||
let scanner = Scanner::new(
|
||||
&[],
|
||||
500,
|
||||
Duration::from_millis(100),
|
||||
1,
|
||||
true,
|
||||
PortStrategy::Manual((1..=port_count).collect()),
|
||||
true,
|
||||
(0..excluded_count)
|
||||
.map(|i| (i * 13 % 65536) as u16)
|
||||
.collect(),
|
||||
false,
|
||||
);
|
||||
preparation.bench_function(
|
||||
format!("{port_count} ports, {excluded_count} exclusions"),
|
||||
|b| b.iter(|| black_box(runtime.block_on(black_box(&scanner).run_with_status()))),
|
||||
);
|
||||
}
|
||||
preparation.finish();
|
||||
|
||||
// UDP payload lookup micro-benchmark: compares the old linear scan of the
|
||||
// payload map with the precomputed port -> payload lookup. No sockets.
|
||||
let udp_map = get_parsed_data();
|
||||
let lookup = build_udp_payload_lookup(udp_map);
|
||||
let ports: Vec<u16> = (1..=4096).collect();
|
||||
|
||||
c.bench_function("udp payload lookup/old scan map 1..4096", |b| {
|
||||
b.iter(|| {
|
||||
for &p in ports.iter() {
|
||||
let payload = old_payload_for_port(black_box(udp_map), black_box(p));
|
||||
black_box(payload);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
c.bench_function("udp payload lookup/new hashmap 1..4096", |b| {
|
||||
b.iter(|| {
|
||||
for &p in ports.iter() {
|
||||
let payload = lookup.get(&p).copied().unwrap_or(b"");
|
||||
black_box(payload);
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(benches, criterion_benchmark);
|
||||
criterion_main!(benches);
|
||||
@@ -0,0 +1,306 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs::{self, File};
|
||||
|
||||
use std::env;
|
||||
use std::io::{BufReader, Read};
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
// Reads in a file with payloads based on port
|
||||
pub fn main() {
|
||||
let mut file_path = env::current_dir().expect("cant find curr dir");
|
||||
file_path.push("./nmap-payloads");
|
||||
|
||||
let mut data = String::new();
|
||||
let file = File::open(&file_path).expect("File not found.");
|
||||
let mut file_buf = BufReader::new(file);
|
||||
file_buf
|
||||
.read_to_string(&mut data)
|
||||
.expect("unable to read file");
|
||||
|
||||
let mut fp_map: BTreeMap<i32, String> = BTreeMap::new();
|
||||
|
||||
let mut count = 0;
|
||||
let mut capturing = false;
|
||||
let mut curr = String::new();
|
||||
|
||||
for line in data.trim().split('\n') {
|
||||
// Strip a trailing carriage return so CRLF checkouts parse
|
||||
// identically to LF ones (otherwise `\r` pollutes port tokens and
|
||||
// blank lines stop looking blank).
|
||||
let line = line.strip_suffix('\r').unwrap_or(line);
|
||||
if line.contains('#') || line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if line.starts_with("udp") {
|
||||
if !curr.is_empty() {
|
||||
fp_map.insert(count, curr);
|
||||
curr = String::new();
|
||||
}
|
||||
capturing = true;
|
||||
count += 1;
|
||||
}
|
||||
|
||||
if capturing {
|
||||
if !curr.is_empty() {
|
||||
curr.push(' ');
|
||||
}
|
||||
curr.push_str(line);
|
||||
}
|
||||
}
|
||||
|
||||
let pb_linenr = ports_v(&fp_map);
|
||||
let payb_linenr = payloads_v(&fp_map);
|
||||
let map = port_payload_map(pb_linenr, payb_linenr);
|
||||
|
||||
generate_code(map);
|
||||
}
|
||||
|
||||
/// Generates a file called Generated.rs and calls cargo fmt from the command line
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `port_payload_map` - A BTreeMap mapping port numbers to payload data
|
||||
fn generate_code(port_payload_map: BTreeMap<Vec<u16>, Vec<u8>>) {
|
||||
let dest_path = PathBuf::from("src/generated.rs");
|
||||
|
||||
let mut generated_code = String::new();
|
||||
generated_code.push_str("use std::collections::BTreeMap;\n");
|
||||
generated_code.push_str("use once_cell::sync::Lazy;\n\n");
|
||||
|
||||
generated_code.push_str("fn generated_data() -> BTreeMap<Vec<u16>, Vec<u8>> {\n");
|
||||
generated_code.push_str(" let mut map = BTreeMap::new();\n");
|
||||
|
||||
for (ports, payloads) in port_payload_map {
|
||||
generated_code.push_str(" map.insert(vec![");
|
||||
generated_code.push_str(
|
||||
&ports
|
||||
.iter()
|
||||
.map(|&p| p.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(","),
|
||||
);
|
||||
generated_code.push_str("], vec![");
|
||||
generated_code.push_str(
|
||||
&payloads
|
||||
.iter()
|
||||
.map(|&p| p.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(","),
|
||||
);
|
||||
generated_code.push_str("]);\n");
|
||||
}
|
||||
|
||||
generated_code.push_str(" map\n");
|
||||
generated_code.push_str("}\n\n");
|
||||
|
||||
generated_code.push_str(
|
||||
"static PARSED_DATA: Lazy<BTreeMap<Vec<u16>, Vec<u8>>> = Lazy::new(generated_data);\n",
|
||||
);
|
||||
generated_code.push_str("pub fn get_parsed_data() -> &'static BTreeMap<Vec<u16>, Vec<u8>> {\n");
|
||||
generated_code.push_str(" &PARSED_DATA\n");
|
||||
generated_code.push_str("}\n");
|
||||
|
||||
fs::write(dest_path, generated_code).unwrap();
|
||||
|
||||
// format the generated code
|
||||
Command::new("cargo")
|
||||
.arg("fmt")
|
||||
.arg("--all")
|
||||
.output()
|
||||
.expect("Failed to execute cargo fmt");
|
||||
}
|
||||
|
||||
/// Creates a BTreeMap of line numbers mapped to a Vec<u16> of ports
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `fp_map` - A BTreeMap containing the parsed file data
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A BTreeMap where keys are line numbers and values are vectors of ports
|
||||
fn ports_v(fp_map: &BTreeMap<i32, String>) -> BTreeMap<i32, Vec<u16>> {
|
||||
let mut pb_linenr: BTreeMap<i32, Vec<u16>> = BTreeMap::new();
|
||||
let mut port_list: Vec<u16> = Vec::new();
|
||||
|
||||
for (&line_nr, ports) in fp_map {
|
||||
if ports.contains("udp ") {
|
||||
let remain = &ports[4..];
|
||||
let mut start = remain.split(' ');
|
||||
|
||||
let ports = start.next().unwrap();
|
||||
let port_segments: Vec<&str> = ports.split(',').collect();
|
||||
|
||||
for segment in port_segments {
|
||||
if segment.contains('-') {
|
||||
let range: Vec<&str> = segment.trim().split('-').collect();
|
||||
let start = range[0].parse::<u16>().unwrap();
|
||||
let end = range[1].parse::<u16>().unwrap();
|
||||
|
||||
for port in start..end {
|
||||
port_list.push(port);
|
||||
}
|
||||
} else if !segment.is_empty() {
|
||||
match segment.parse::<u16>() {
|
||||
Ok(port) => port_list.push(port),
|
||||
Err(_) => println!("Error parsing port: {segment}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pb_linenr.insert(line_nr, port_list.clone());
|
||||
port_list.clear();
|
||||
}
|
||||
|
||||
pb_linenr
|
||||
}
|
||||
|
||||
/// Parses out the Payloads into a BTreeMap of line numbers mapped to vectors of payload bytes
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `fp_map` - A BTreeMap containing the parsed file data
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A BTreeMap where keys are line numbers and values are vectors of payload bytes
|
||||
fn payloads_v(fp_map: &BTreeMap<i32, String>) -> BTreeMap<i32, Vec<u8>> {
|
||||
let mut payb_linenr: BTreeMap<i32, Vec<u8>> = BTreeMap::new();
|
||||
|
||||
for (&line_nr, data) in fp_map {
|
||||
if data.contains('"') {
|
||||
let start = data.find('"').expect("payload opening \" not found");
|
||||
// Pass from the opening quote: the decoder pairs quotes itself
|
||||
// to split multi-line segments.
|
||||
payb_linenr.insert(line_nr, parser(data[start..].trim()));
|
||||
}
|
||||
}
|
||||
|
||||
payb_linenr
|
||||
}
|
||||
|
||||
/// Converts a quoted Nmap payload block to bytes.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `payload` - The raw block text starting at its first opening quote:
|
||||
/// one or more `"..."` segments separated by whitespace
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The decoded probe bytes: segments are decoded with [`decode_segment`]
|
||||
/// and concatenated with no separators, so multi-line probes reassemble
|
||||
/// exactly.
|
||||
fn parser(payload: &str) -> Vec<u8> {
|
||||
let mut bytes: Vec<u8> = Vec::new();
|
||||
let mut rest = payload.trim();
|
||||
while let Some(open) = rest.find('"') {
|
||||
let after_open = &rest[open + 1..];
|
||||
match split_segment(after_open) {
|
||||
Some((segment, remainder)) => {
|
||||
decode_segment(segment, &mut bytes);
|
||||
rest = remainder;
|
||||
}
|
||||
None => {
|
||||
// Unterminated trailing quote: decode what's left literally.
|
||||
decode_segment(after_open, &mut bytes);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bytes
|
||||
}
|
||||
|
||||
/// Splits off the first `"..."` segment: `text` must start just after an
|
||||
/// opening quote. Escaped quotes (`\"`) do not terminate the segment.
|
||||
///
|
||||
/// Returns the segment body and the remainder after the closing quote.
|
||||
fn split_segment(text: &str) -> Option<(&str, &str)> {
|
||||
let mut escaped = false;
|
||||
for (i, c) in text.char_indices() {
|
||||
if escaped {
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
if c == '\\' {
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
if c == '"' {
|
||||
return Some((&text[..i], &text[i + 1..]));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Decodes one quoted Nmap payload segment in C-style escape format:
|
||||
/// `\xNN` becomes a byte, common single-char escapes (`\\`, `\"`,
|
||||
/// `\0`, `\n`, `\r`, `\t`) decode to their value, and every other
|
||||
/// character contributes its literal bytes.
|
||||
///
|
||||
/// Preserving literal text matters: several probes embed plain-text
|
||||
/// protocol words (the SNMP `public` community string, NetBIOS names,
|
||||
/// LDAP `objectClass`, SSDP headers). The previous hexdigits-only
|
||||
/// decoding mangled them into wrong bytes, so agents never answered.
|
||||
fn decode_segment(segment: &str, bytes: &mut Vec<u8>) {
|
||||
let mut chars = segment.chars();
|
||||
while let Some(c) = chars.next() {
|
||||
if c != '\\' {
|
||||
let mut buf = [0; 4];
|
||||
bytes.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
|
||||
continue;
|
||||
}
|
||||
match chars.next() {
|
||||
Some('x') => {
|
||||
let hi = chars.next().unwrap_or('0');
|
||||
let lo = chars.next().unwrap_or('0');
|
||||
let hex: String = [hi, lo].iter().collect();
|
||||
bytes.push(u8::from_str_radix(&hex, 16).unwrap_or(0));
|
||||
}
|
||||
Some('0') => bytes.push(0),
|
||||
Some('n') => bytes.push(b'\n'),
|
||||
Some('r') => bytes.push(b'\r'),
|
||||
Some('t') => bytes.push(b'\t'),
|
||||
Some('\\') => bytes.push(b'\\'),
|
||||
Some('"') => bytes.push(b'"'),
|
||||
// Unknown escape: keep the character literally, matching Nmap,
|
||||
// which passes unrecognized escapes through untouched.
|
||||
Some(other) => {
|
||||
let mut buf = [0; 4];
|
||||
bytes.extend_from_slice(other.encode_utf8(&mut buf).as_bytes());
|
||||
}
|
||||
None => bytes.push(b'\\'),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Combines the ports BTreeMap and the Payloads BTreeMap
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `pb_linenr` - A BTreeMap mapping line numbers to vectors of ports
|
||||
/// * `payb_linenr` - A BTreeMap mapping line numbers to vectors of payload bytes
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A BTreeMap mapping vectors of ports to vectors of payload bytes
|
||||
fn port_payload_map(
|
||||
pb_linenr: BTreeMap<i32, Vec<u16>>,
|
||||
payb_linenr: BTreeMap<i32, Vec<u8>>,
|
||||
) -> BTreeMap<Vec<u16>, Vec<u8>> {
|
||||
let mut ppm_fin: BTreeMap<Vec<u16>, Vec<u8>> = BTreeMap::new();
|
||||
|
||||
for (port_linenr, ports) in pb_linenr {
|
||||
for (pay_linenr, payloads) in &payb_linenr {
|
||||
if pay_linenr == &port_linenr {
|
||||
ppm_fin.insert(ports.to_vec(), payloads.to_vec());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ppm_fin
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# Dockerfile for RustScan development environment
|
||||
# Provides a containerized setup with Rust and development tools
|
||||
FROM rust
|
||||
# Install rustfmt for code formatting and clippy for linting.
|
||||
RUN rustup component add rustfmt clippy
|
||||
@@ -7,19 +7,21 @@ Before contributing, read our [code of conduct](https://github.com/RustScan/Rust
|
||||
TL;DR if you abuse members of our community you will be **perma-banned** with no chance to get unbanned. No warnings either. 🤗
|
||||
|
||||
RustScan has 2 major labels for GitHub issues you should look at:
|
||||
* Good First issue
|
||||
These are issues for newcomers to open source!
|
||||
[https://github.com/RustScan/RustScan/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22](https://github.com/RustScan/RustScan/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22)
|
||||
* Help wanted
|
||||
These are issues that aren't really for newcomers, but we could still do wiht help!
|
||||
[https://github.com/RustScan/RustScan/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22+label%3A%22help+wanted%22](https://github.com/RustScan/RustScan/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22+label%3A%22help+wanted%22)
|
||||
|
||||
- Good First issue
|
||||
These are issues for newcomers to open source!
|
||||
[https://github.com/RustScan/RustScan/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22](https://github.com/RustScan/RustScan/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22)
|
||||
- Help wanted
|
||||
These are issues that aren't really for newcomers, but we could still do with help!
|
||||
[https://github.com/RustScan/RustScan/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22+label%3A%22help+wanted%22](https://github.com/RustScan/RustScan/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22+label%3A%22help+wanted%22)
|
||||
|
||||
If you want to, solve the issue or comment on the issue for help.
|
||||
|
||||
The flow for contributing to open source software is:
|
||||
* Fork the repo
|
||||
* Make changes
|
||||
* Pull request to the repo
|
||||
|
||||
- Fork the repo
|
||||
- Make changes
|
||||
- Pull request to the repo
|
||||
|
||||
And then comment on the issue that you've done.
|
||||
|
||||
@@ -28,4 +30,54 @@ RustScan also has some `// TODO`'s in the codebase, which are meant more for the
|
||||
If you have any feature suggestions or bugs, leave a GitHub issue. We welcome any and all support :D
|
||||
|
||||
## Rewarding you
|
||||
|
||||
I cannot pay you :-( But, I can place your GitHub profile on the README under `#Contributors` as a thank you! :)
|
||||
|
||||
## Contributing development environment
|
||||
|
||||
To ease contribution to RustScan, you can use the `contributing.Dockerfile` to create a Docker image ready to build and play with RustScan.
|
||||
To build it you just need to run:
|
||||
|
||||
```bash
|
||||
you@home:~/RustScan$ docker build -t rustscan_contributing -f contributing.Dockerfile
|
||||
```
|
||||
|
||||
Then you need to run the container with a volume so it can access, _with read and write permissions_, to RustScan files:
|
||||
|
||||
```bash
|
||||
you@home:~/RustScan$ docker run -ti --rm -v "$PWD":/rustscan -w /rustscan rustscan_contributing bash
|
||||
```
|
||||
|
||||
You can now modify RustScan files with your favorite editor, once you want to compile and test your modifications, type the following in the container prompt:
|
||||
|
||||
```bash
|
||||
root@container:/rustscan# cargo build
|
||||
```
|
||||
|
||||
You can check the command-line interface without starting a scan:
|
||||
|
||||
```bash
|
||||
root@container:/rustscan# cargo run -- --help
|
||||
```
|
||||
|
||||
You can also format, lint with `clippy` and test the code with the following commands:
|
||||
|
||||
```bash
|
||||
root@container:/rustscan# cargo fmt
|
||||
root@container:/rustscan# cargo clippy
|
||||
root@container:/rustscan# cargo test
|
||||
```
|
||||
|
||||
Automated tests and benchmarks must run without network traffic or external script
|
||||
execution. Keep coverage focused on parsing, configuration, port ordering, socket
|
||||
address iteration, and timing helpers. Do not add tests that run TCP/UDP scans
|
||||
(including localhost), resolve hostnames, or launch Nmap or other scripts.
|
||||
|
||||
Scanning behaviour and speed are measured separately, by the opt-in "Scan benchmark"
|
||||
GitHub Actions workflow (`.github/workflows/scan-benchmark.yml`). It runs for pull
|
||||
requests that touch the scanner, its runtime or its dependencies, and can be started
|
||||
by hand from the Actions tab. It builds the change and its base in release mode,
|
||||
scans listeners that it opens on 127.0.0.1 inside the runner, checks that both
|
||||
builds find the same open ports, and adds a comparison table to the job summary. Its
|
||||
script (`.github/scan-benchmark/scan_bench.py`) port-scans the loopback interface,
|
||||
so it is meant for disposable CI runners only.
|
||||
@@ -0,0 +1,38 @@
|
||||
# ARM support
|
||||
|
||||
This page covers which ARM targets RustScan's CI builds and tests, and how to build ARM binaries yourself.
|
||||
|
||||
## What CI covers
|
||||
|
||||
| Target | Built by | Tested by |
|
||||
| --- | --- | --- |
|
||||
| `aarch64-unknown-linux-gnu` (64-bit ARM Linux) | `build-nix` job in [build.yml](../.github/workflows/build.yml) | `Test Suite (ubuntu-24.04-arm)` in [test.yml](../.github/workflows/test.yml) |
|
||||
| `armv7-unknown-linux-gnueabihf` (32-bit ARM Linux) | `build-nix` job in [build.yml](../.github/workflows/build.yml) | not tested in CI |
|
||||
| `aarch64-apple-darwin` (Apple Silicon macOS) | `build-macos-aarch64` job in [build.yml](../.github/workflows/build.yml) | `Test Suite (macos-latest)` in [test.yml](../.github/workflows/test.yml) |
|
||||
|
||||
- **Builds** use [`houseabsolute/actions-rust-cross`](https://github.com/houseabsolute/actions-rust-cross). It cross-compiles the Linux ARM targets with [`cross`](https://github.com/cross-rs/cross) on x86_64 runners.
|
||||
- **Tests** run natively on GitHub-hosted arm64 runners: `ubuntu-24.04-arm` for Linux, and `macos-latest`, which runs on Apple Silicon. They run the same `just test` recipe as every other platform.
|
||||
|
||||
## Building ARM binaries locally
|
||||
|
||||
The simplest option is `cross`, which runs the build in a container that already has the right toolchain and linker. It needs Docker or Podman.
|
||||
|
||||
```sh
|
||||
cargo install cross
|
||||
cross build --locked --release --target aarch64-unknown-linux-gnu
|
||||
# or, for 32-bit ARM:
|
||||
cross build --locked --release --target armv7-unknown-linux-gnueabihf
|
||||
```
|
||||
|
||||
The binary ends up at `target/<target>/release/rustscan`.
|
||||
|
||||
On an ARM machine (for example a Raspberry Pi running a 64-bit OS, or an Apple Silicon Mac) you don't need `cross`; a normal native build works:
|
||||
|
||||
```sh
|
||||
cargo build --locked --release
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **`error[E0463]: can't find crate for std`, or linker errors:** you're cross-compiling with plain `cargo`. Either use `cross`, or install the target (`rustup target add <target>`) along with a matching cross linker.
|
||||
- **`cross` can't pull its image:** check that Docker or Podman is running and that your user can reach it.
|
||||
@@ -0,0 +1,151 @@
|
||||
{
|
||||
"metadata": {
|
||||
"base": "67c08a33100362c54b56fde784b0fd234f70fa11",
|
||||
"candidate": "a05592a8a88b77ad85d961137943dfadfe98b612",
|
||||
"environment": "Linux-7.2.6-1-cachyos-x86_64-with-glibc2.44",
|
||||
"rust": "rustc 1.98.1 (48a229cea 2026-09-01) (Arch Linux rust 1:1.98.1-1.1)",
|
||||
"hosts_bytes": 2760051,
|
||||
"profile": "release with LTO",
|
||||
"network_namespace": "unshare --user --map-root-user --net",
|
||||
"common_args": [
|
||||
"--no-config",
|
||||
"--config-path",
|
||||
"/dev/null",
|
||||
"--no-banner",
|
||||
"--scripts",
|
||||
"none",
|
||||
"--greppable",
|
||||
"--ports",
|
||||
"80",
|
||||
"--exclude-ports",
|
||||
"80"
|
||||
],
|
||||
"workloads": {
|
||||
"literal-ipv4": [
|
||||
"-a",
|
||||
"127.0.0.1"
|
||||
],
|
||||
"mixed-cidrs": [
|
||||
"-a",
|
||||
"127.0.0.1,192.0.2.0/30,2001:db8::/126",
|
||||
"--exclude-addresses",
|
||||
"192.0.2.1,2001:db8::1"
|
||||
]
|
||||
},
|
||||
"warmups_each": 2,
|
||||
"samples_each": 20,
|
||||
"binary_cpus": "0-3",
|
||||
"harness_cpus": "8-9",
|
||||
"binary_sha256": {
|
||||
"base": "4d6b090218ff36249ed95f14ec1a82626e8ea33854f3823f19a6de247b1524b8",
|
||||
"dns": "a9d6bf949f56c53d18cc85bc9b53b21bd85a63bcdd04ad78992d392c675c77ac"
|
||||
}
|
||||
},
|
||||
"summary": {
|
||||
"literal-ipv4": {
|
||||
"base_ms": 168.23068849999999,
|
||||
"dns_ms": 2.248762,
|
||||
"samples_each": 20
|
||||
},
|
||||
"mixed-cidrs": {
|
||||
"base_ms": 178.0855925,
|
||||
"dns_ms": 2.4539600000000004,
|
||||
"samples_each": 20
|
||||
}
|
||||
},
|
||||
"samples": {
|
||||
"literal-ipv4": {
|
||||
"base": [
|
||||
143.391226,
|
||||
146.562804,
|
||||
149.615525,
|
||||
151.188597,
|
||||
152.269193,
|
||||
159.499601,
|
||||
164.926958,
|
||||
164.347529,
|
||||
167.208931,
|
||||
165.433184,
|
||||
172.246939,
|
||||
169.252446,
|
||||
172.777899,
|
||||
170.729249,
|
||||
170.092681,
|
||||
171.863255,
|
||||
169.612842,
|
||||
170.857396,
|
||||
173.117403,
|
||||
175.206345
|
||||
],
|
||||
"dns": [
|
||||
2.241793,
|
||||
1.817341,
|
||||
2.39956,
|
||||
2.048986,
|
||||
3.123401,
|
||||
1.820017,
|
||||
3.615094,
|
||||
2.165312,
|
||||
2.675535,
|
||||
2.103261,
|
||||
2.531392,
|
||||
2.215141,
|
||||
2.572939,
|
||||
2.063803,
|
||||
2.671431,
|
||||
2.059846,
|
||||
2.598858,
|
||||
2.230916,
|
||||
2.678259,
|
||||
2.255731
|
||||
]
|
||||
},
|
||||
"mixed-cidrs": {
|
||||
"base": [
|
||||
175.504767,
|
||||
178.797842,
|
||||
186.183728,
|
||||
176.885958,
|
||||
177.989816,
|
||||
180.028568,
|
||||
177.66299,
|
||||
174.757221,
|
||||
175.468485,
|
||||
175.328023,
|
||||
177.639639,
|
||||
175.637593,
|
||||
177.922435,
|
||||
185.394396,
|
||||
182.587204,
|
||||
180.58311,
|
||||
178.961137,
|
||||
178.293186,
|
||||
180.482419,
|
||||
178.181369
|
||||
],
|
||||
"dns": [
|
||||
3.275193,
|
||||
2.114074,
|
||||
2.700771,
|
||||
2.183317,
|
||||
2.606538,
|
||||
2.368046,
|
||||
2.599702,
|
||||
2.238105,
|
||||
2.683892,
|
||||
2.292249,
|
||||
2.528366,
|
||||
2.114478,
|
||||
2.668921,
|
||||
2.103271,
|
||||
3.275514,
|
||||
2.27974,
|
||||
3.103311,
|
||||
2.265298,
|
||||
2.580743,
|
||||
2.379554
|
||||
]
|
||||
}
|
||||
},
|
||||
"invocations": 88
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
# Lazy DNS initialization benchmark
|
||||
|
||||
Compared current Tokio master `67c08a33100362c54b56fde784b0fd234f70fa11` with the lazy-resolver implementation
|
||||
at `a05592a8a88b77ad85d961137943dfadfe98b612` on 2026-10-02.
|
||||
|
||||
## Method
|
||||
|
||||
Both binaries are release builds with LTO, using Rust 1.98.1 on an Intel Core
|
||||
Ultra 7 165U running Linux 7.2.6-1-cachyos. Each workload used twenty measured
|
||||
runs and two warmups per binary, alternating A/B and B/A order. Binaries used
|
||||
CPUs 0-3; the harness used CPUs 8-9. No builds ran during measurement.
|
||||
|
||||
The harness ran in a private network namespace. Every invocation excluded
|
||||
its only requested port, so no scan traffic was sent. Configuration and
|
||||
scripts were disabled. Timing spans process launch through exit, including
|
||||
the same `taskset` affinity launcher for both builds.
|
||||
|
||||
Common arguments:
|
||||
|
||||
```sh
|
||||
--no-config --config-path /dev/null --no-banner --scripts none --greppable \
|
||||
--ports 80 --exclude-ports 80
|
||||
```
|
||||
|
||||
The literal case adds `-a 127.0.0.1`. The mixed case adds
|
||||
`-a 127.0.0.1,192.0.2.0/30,2001:db8::/126` and
|
||||
`--exclude-addresses 192.0.2.1,2001:db8::1`. Both use `RUST_LOG=rustscan=info`.
|
||||
|
||||
## Results
|
||||
|
||||
| Workload | Master median | Lazy DNS median | Time saved |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| Literal IPv4 | 168.23 ms | 2.25 ms | 165.98 ms (98.66%) |
|
||||
| IPv4/IPv6 CIDRs and exclusions | 178.09 ms | 2.45 ms | 175.63 ms (98.62%) |
|
||||
|
||||
All 88 invocations exited successfully with the expected empty scan output.
|
||||
A separate syscall trace of the optimized literal-IP invocation recorded no
|
||||
reads of `/etc/hosts` or `/etc/resolv.conf` and no network connects or sends.
|
||||
|
||||
This host has a 2,760,051-byte hosts file. Eager Hickory initialization parses
|
||||
that file even for numeric targets. Systems with smaller hosts files will
|
||||
save less; this measures CLI startup, not socket throughput. Hostnames that
|
||||
need fallback DNS still pay resolver initialization once, and parsing order,
|
||||
file handling, deduplication and exclusions keep their existing behavior.
|
||||
|
||||
[Raw samples, setup and binary hashes](lazy-dns-benchmark-results.json) are
|
||||
included. Network-free regression tests verify that literal IPv4/IPv6 targets,
|
||||
CIDRs, exclusions, duplicates and empty inputs do not construct the resolver.
|
||||
@@ -0,0 +1,114 @@
|
||||
# Port exclusion preparation benchmark
|
||||
|
||||
Compared Tokio master `b7119f7f3eb1601e81126ef3572699ed7b3b7c0b` with
|
||||
implementation `270561d553688acbd99f61c460cdfa7c76c1423e` on 2026-10-02.
|
||||
|
||||
## Method
|
||||
|
||||
Release builds with LTO, Rust 1.98.1, Intel Core Ultra 7 165U, Linux
|
||||
7.2.6-1-cachyos. Both versions use the same lockfile and identical
|
||||
`benches/benchmark_helpers.rs`; copy the final benchmark harness into the
|
||||
baseline worktree before building it. The benchmark runs the production
|
||||
`Scanner::run_with_status` with **zero target addresses**, so it opens no
|
||||
sockets and does no DNS or scripting. It includes generating/cloning the
|
||||
port list, filtering and empty scan setup. Scanner construction and the
|
||||
current-thread Tokio runtime construction are outside the timer.
|
||||
|
||||
Manual ports are `1..=port_count`; exclusions are
|
||||
`(i * 13 % 65536) as u16` for `i` in `0..excluded_count`. Each case has two
|
||||
independent twenty-sample Criterion runs per build, a 500 ms warmup and a
|
||||
2 s target measurement. Cases run in A/B then B/A order on CPU 0. No builds
|
||||
or other local checks ran during timing. CPU frequency was not fixed on
|
||||
this shared laptop, so the table retains both repetitions rather than
|
||||
selecting one result. Values are Criterion's mean point estimates.
|
||||
|
||||
Build command for each worktree:
|
||||
|
||||
```sh
|
||||
cargo bench --locked --bench benchmark_helpers --no-run
|
||||
```
|
||||
|
||||
The saved benchmark executables are then run serially, for each case:
|
||||
|
||||
```sh
|
||||
CARGO_TARGET_DIR=/tmp/preparation-results taskset -c 0 /path/to/benchmark_helpers \
|
||||
--bench 'port preparation/65535 ports, 4096 exclusions' --save-baseline base-a
|
||||
```
|
||||
|
||||
Repeat with the candidate binary as `head-a`, then the candidate as
|
||||
`head-b` and the baseline as `base-b`. To run the group normally without
|
||||
comparing builds:
|
||||
|
||||
```sh
|
||||
cargo bench --locked --bench benchmark_helpers -- 'port preparation'
|
||||
```
|
||||
|
||||
## Results
|
||||
|
||||
Times are microseconds; A and B are separate matched repetitions.
|
||||
|
||||
| Ports | Exclusions | Master µs A / B | Optimized µs A / B | Time saved A / B |
|
||||
| ---: | ---: | ---: | ---: | ---: |
|
||||
| 1 | 1 | 0.289 / 0.286 | 0.261 / 0.271 | 9.7% / 5.2% |
|
||||
| 16 | 4 | 0.260 / 0.268 | 0.232 / 0.237 | 10.6% / 11.6% |
|
||||
| 64 | 1 | 0.433 / 0.437 | 0.237 / 0.239 | 45.2% / 45.4% |
|
||||
| 64 | 4,096 | 6.231 / 10.093 | 2.484 / 3.038 | 60.1% / 69.9% |
|
||||
| 4,096 | 0 | 10.889 / 10.037 | 0.335 / 0.397 | 96.9% / 96.0% |
|
||||
| 4,096 | 64 | 20.680 / 20.363 | 2.930 / 3.064 | 85.8% / 85.0% |
|
||||
| 4,096 | 4,096 | 524.223 / 519.670 | 6.805 / 6.710 | 98.7% / 98.7% |
|
||||
| 65,535 | 0 | 166.113 / 115.686 | 3.655 / 2.333 | 97.8% / 98.0% |
|
||||
| 65,535 | 4 | 175.717 / 181.343 | 29.769 / 30.230 | 83.1% / 83.3% |
|
||||
| 65,535 | 64 | 207.617 / 206.910 | 31.595 / 31.462 | 84.8% / 84.8% |
|
||||
| 65,535 | 1,024 | 2018.463 / 3271.564 | 33.403 / 33.520 | 98.3% / 99.0% |
|
||||
| 65,535 | 4,096 | 8811.234 / 8469.706 | 59.853 / 63.211 | 99.3% / 99.3% |
|
||||
|
||||
All twelve cases improved in both repetitions. This measures **preparation,
|
||||
not end-to-end network scan speed**: many scans are dominated by socket I/O,
|
||||
timeouts and reporting. In particular, the default no-exclusion saving is
|
||||
only a fraction of a millisecond.
|
||||
|
||||
## Implementation and correctness
|
||||
|
||||
With no exclusions, keep the already generated vector. For fewer than 64
|
||||
ports, filter in place with linear membership checks; this avoids bitmap
|
||||
setup and a second allocation. Larger lists use an 8 KiB bitmap covering
|
||||
all 65,536 values representable by `u16`, then retain ports in place. This
|
||||
changes the large-list filtering cost from ports × exclusions to ports +
|
||||
exclusions, while preserving order, duplicate ports and boundary values.
|
||||
The helper is kept out of line: release assembly showed an inlined bitmap
|
||||
reserved 8,424 bytes in every async poll, whereas the final poll frame uses
|
||||
344 bytes and the helper reserves its bitmap only during preparation.
|
||||
|
||||
Exploration found that in-place linear membership slows large cases. A
|
||||
first production candidate also slowed 64 ports with 4,096 exclusions;
|
||||
using the bitmap for that case corrected the regression before the final
|
||||
matched runs. The cutoff is a performance choice, not a limit on input.
|
||||
|
||||
One focused network-free regression test compares the implementation with
|
||||
an independent membership oracle for empty inputs, endpoints 0/65535,
|
||||
duplicate ports/exclusions, all ports, and a frozen random order.
|
||||
`cargo test --locked` passes 74 tests with one pre-existing ignored doctest.
|
||||
All-target Clippy, formatting, documentation and diff checks pass. The
|
||||
CI-only scan harness adds an excluded-port sweep using the existing
|
||||
listeners, checking expected results and using the actual attempted socket
|
||||
count for throughput.
|
||||
|
||||
[Raw Criterion samples, confidence intervals, revisions and binary hashes](port-exclusion-benchmark-results.json)
|
||||
are included. Hosted correctness/performance results are linked in the PR.
|
||||
|
||||
## macOS runner setup
|
||||
|
||||
Repeated hosted sweeps on Darwin 25.6 returned `ENOBUFS` (error 55) on
|
||||
both master and the candidate, missing fixture listeners. Such runs are
|
||||
invalid for performance claims. The workflow provisions the TCP memory
|
||||
budget once before either build is measured, raising it from the default
|
||||
1/32 to 1/8 of physical memory when necessary, and prints the actual limit
|
||||
and allocation counters. It retains every scenario, repeat, listener
|
||||
comparison and performance threshold. An immediate untimed diagnostic
|
||||
scan records errors and kernel counters if a measured pair misses a
|
||||
listener; that failed dataset is not used to claim a speedup.
|
||||
|
||||
This uses XNU's [TCP budget initialization](https://github.com/apple-oss-distributions/xnu/blob/main/bsd/netinet/tcp_subr.c)
|
||||
and [memory accounting interface](https://github.com/apple-oss-distributions/xnu/blob/main/bsd/sys/mem_acct_private.h).
|
||||
It changes the disposable benchmark environment, not RustScan's behavior
|
||||
or the operating system configuration on users' machines.
|
||||
@@ -0,0 +1,4 @@
|
||||
tags = ["core_approved", "example"]
|
||||
ports = ["80", "443", "8080"]
|
||||
developer = ["example", "https://example.org"]
|
||||
directory = "fixtures/.rustscan_scripts"
|
||||
@@ -0,0 +1,7 @@
|
||||
Metadata-only test fixture; not executable.
|
||||
#tags = ["core_approved", "example"]
|
||||
#developer = [ "example", "https://example.org" ]
|
||||
#ports_separator = ","
|
||||
#call_format = "fixture {{ip}} {{port}}"
|
||||
|
||||
The parser reads only the metadata above. This file contains no script body.
|
||||
@@ -0,0 +1,7 @@
|
||||
Metadata-only test fixture; not executable.
|
||||
#tags = "core_approved"
|
||||
#developer = [ "example", "https://example.org" ]
|
||||
#ports_separator = ","
|
||||
#call_format = "fixture {{ip}} {{port}}"
|
||||
|
||||
# tags has to be an array, thus this file won't be parsed
|
||||
@@ -0,0 +1,42 @@
|
||||
# https://github.com/minimaxir/big-list-of-naughty-strings
|
||||
|
||||
|
||||
|
||||
|
||||
Ω≈ç√∫˜µ≤≥÷
|
||||
åß∂ƒ©˙∆˚¬…æ
|
||||
œ∑´®†¥¨ˆøπ“‘
|
||||
¡™£¢∞§¶•ªº–≠
|
||||
¸˛Ç◊ı˜Â¯˘¿
|
||||
ÅÍÎÏ˝ÓÔÒÚÆ☃
|
||||
Œ„´‰ˇÁ¨ˆØ∏”’
|
||||
`⁄€‹›fifl‡°·‚—±
|
||||
⅛⅜⅝⅞
|
||||
ЁЂЃЄЅІЇЈЉЊЋЌЍЎЏАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюя
|
||||
٠١٢٣٤٥٦٧٨٩
|
||||
|
||||
|
||||
|
||||
⁰⁴⁵
|
||||
₀₁₂
|
||||
⁰⁴⁵₀₁₂
|
||||
ด้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็ ด้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็ ด้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็
|
||||
|
||||
田中さんにあげて下さい
|
||||
パーティーへ行かないか
|
||||
和製漢語
|
||||
部落格
|
||||
사회과학원 어학연구소
|
||||
찦차를 타고 온 펲시맨과 쑛다리 똠방각하
|
||||
社會科學院語學研究所
|
||||
울란바토르
|
||||
𠜎𠜱𠝹𠱓𠱸𠲖𠳏
|
||||
𝐓𝐡𝐞 𝐪𝐮𝐢𝐜𝐤 𝐛𝐫𝐨𝐰𝐧 𝐟𝐨𝐱 𝐣𝐮𝐦𝐩𝐬 𝐨𝐯𝐞𝐫 𝐭𝐡𝐞 𝐥𝐚𝐳𝐲 𝐝𝐨𝐠
|
||||
𝕿𝖍𝖊 𝖖𝖚𝖎𝖈𝖐 𝖇𝖗𝖔𝖜𝖓 𝖋𝖔𝖝 𝖏𝖚𝖒𝖕𝖘 𝖔𝖛𝖊𝖗 𝖙𝖍𝖊 𝖑𝖆𝖟𝖞 𝖉𝖔𝖌
|
||||
𝑻𝒉𝒆 𝒒𝒖𝒊𝒄𝒌 𝒃𝒓𝒐𝒘𝒏 𝒇𝒐𝒙 𝒋𝒖𝒎𝒑𝒔 𝒐𝒗𝒆𝒓 𝒕𝒉𝒆 𝒍𝒂𝒛𝒚 𝒅𝒐𝒈
|
||||
𝓣𝓱𝓮 𝓺𝓾𝓲𝓬𝓴 𝓫𝓻𝓸𝔀𝓷 𝓯𝓸𝔁 𝓳𝓾𝓶𝓹𝓼 𝓸𝓿𝓮𝓻 𝓽𝓱𝓮 𝓵𝓪𝔃𝔂 𝓭𝓸𝓰
|
||||
𝕋𝕙𝕖 𝕢𝕦𝕚𝕔𝕜 𝕓𝕣𝕠𝕨𝕟 𝕗𝕠𝕩 𝕛𝕦𝕞𝕡𝕤 𝕠𝕧𝕖𝕣 𝕥𝕙𝕖 𝕝𝕒𝕫𝕪 𝕕𝕠𝕘
|
||||
𝚃𝚑𝚎 𝚚𝚞𝚒𝚌𝚔 𝚋𝚛𝚘𝚠𝚗 𝚏𝚘𝚡 𝚓𝚞𝚖𝚙𝚜 𝚘𝚟𝚎𝚛 𝚝𝚑𝚎 𝚕𝚊𝚣𝚢 𝚍𝚘𝚐
|
||||
⒯⒣⒠ ⒬⒰⒤⒞⒦ ⒝⒭⒪⒲⒩ ⒡⒪⒳ ⒥⒰⒨⒫⒮ ⒪⒱⒠⒭ ⒯⒣⒠ ⒧⒜⒵⒴ ⒟⒪⒢
|
||||
|
||||
ثم نفس سقطت وبالتحديد،, جزيرتي باستخدام أن دنو. إذ هنا؟ الستار وتنصيب كان. أهّل ايطاليا، بريطانيا-فرنسا قد أخذ. سليمان، إتفاقية بين ما, يذكر الحدود أي بعد, معاملة بولندا، الإطلاق عل إيو.
|
||||
@@ -0,0 +1,14 @@
|
||||
# Test/Example ScriptConfig file
|
||||
|
||||
# Tags to filter on scripts. Only scripts containing all these tags will run.
|
||||
tags = ["core_approved", "example"]
|
||||
|
||||
# If it's present then only those scripts will run which has a tag ports = "80". Not yet implemented.
|
||||
#
|
||||
# ex.:
|
||||
# ports = ["80"]
|
||||
# ports = ["80","81","8080"]
|
||||
ports = ["80"]
|
||||
|
||||
# Only this developer(s) scripts to run. Not yet implemented.
|
||||
developer = ["example"]
|
||||
@@ -0,0 +1,18 @@
|
||||
test:
|
||||
cargo test
|
||||
cargo clippy -- --deny warnings
|
||||
cargo clippy --tests -- --deny warnings
|
||||
cargo fmt --check
|
||||
cargo doc --workspace --all-features --no-deps --document-private-items
|
||||
|
||||
fix:
|
||||
cargo fmt
|
||||
cargo clippy --fix
|
||||
|
||||
# release only runs if test runs
|
||||
release: test
|
||||
git checkout master
|
||||
git pull
|
||||
cargo generate-lockfile
|
||||
cargo publish
|
||||
git push
|
||||
@@ -0,0 +1,500 @@
|
||||
# Nmap nmap payload database -*- mode: fundamental; -*-
|
||||
# $Id$
|
||||
#
|
||||
# These payloads are sent with every host discovery or port scan probe
|
||||
# by default. This database should only include payloads that are
|
||||
# unlikely to crash services, trip IDS alerts, or change state on the
|
||||
# server. The idea behind these is to evoke a response using a payload.
|
||||
# Some of them are taken from nmap-service-probes.
|
||||
#
|
||||
# This collection of data is (C) 1996-2022 by Nmap Software LLC. It
|
||||
# is distributed under the Nmap Public Source license as provided in
|
||||
# the LICENSE file of the source distribution or at
|
||||
# https://nmap.org/npsl/. Note that this free license does not allow
|
||||
# incorporation of Nmap or its data files within proprietary
|
||||
# software. We sell a separate Nmap OEM for that as described
|
||||
# (including pricing) at https://nmap.org/npsl/.
|
||||
#
|
||||
# Each entry begins with a protocol (only "udp" is supported) followed
|
||||
# by a comma-separated list of ports, followed by one or more quoted
|
||||
# strings containing the payload. These elements may be broken across
|
||||
# several lines. For future expansion, additional keywords may follow
|
||||
# the payload data. Any data following one of these keywords must be on
|
||||
# the same line as the keyword so that unknown keywords can be ignored
|
||||
# by the parser. Currently this file contains some entries with the
|
||||
# "source" keyword to specify a desired source port, but it is not
|
||||
# honored by Nmap.
|
||||
#
|
||||
# Multiple payloads may be defined for a single protocol and port, in
|
||||
# which case they will be all be sent concurrently. There is a limit
|
||||
# of 255 payloads per port.
|
||||
#
|
||||
# Lines longer than 1024 characters will be ignored.
|
||||
#
|
||||
# Example:
|
||||
# udp 1234 "payloaddatapayloaddata"
|
||||
# "payloaddatapayloaddata"
|
||||
# source 5678
|
||||
|
||||
# GenericLines. Use for the echo service.
|
||||
udp 7 "\x0D\x0A\x0D\x0A"
|
||||
# DNSStatusRequest
|
||||
# Sent for TFTP (69) as well due to its ability to provoke an error response
|
||||
udp 53,69,5353,26198 "\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
# DNS VER
|
||||
udp 53,5353,26198
|
||||
"\x77\x77\x01\x00\x00"
|
||||
"\x01\x00\x00\x00\x00\x00\x00\x07version\x04bind\x00\x00\x10\x00\x03"
|
||||
|
||||
# DHCP INFORM
|
||||
udp 67
|
||||
"\x01\x01\x06\x00"
|
||||
"\x01\x23\x45\x67\x00\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x00"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x35\xd4\xd8\x51\x00\x00\x00\x00"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x63\x82\x53\x63\x35\x01"
|
||||
"\x08\xff"
|
||||
|
||||
# TFTP GET
|
||||
udp 69 "\x00\x01r7tftp.txt\x00octet\x00"
|
||||
|
||||
# QUIC packet with unsupported version Q999
|
||||
# Also found on 443, but need to check whether DTLS or QUIC is more prevalent
|
||||
udp 80 "\r12345678Q999\x00"
|
||||
# RPCCheck
|
||||
udp 111
|
||||
"\x72\xFE\x1D\x13\x00\x00\x00\x00\x00\x00\x00\x02\x00\x01\x86\xA0"
|
||||
"\x00\x01\x97\x7C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
# ONCRPC CALL
|
||||
udp 111,2049,4045,32768-65535
|
||||
"\x3e\xec\xe3\xca\x00\x00\x00\x00\x00\x00\x00\x02\x00"
|
||||
"\xbc\x61\x4e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
# NTPRequest
|
||||
udp 123
|
||||
"\xE3\x00\x04\xFA\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\xC5\x4F\x23\x4B\x71\xB1\x52\xF3"
|
||||
# NTP REQ
|
||||
udp 123
|
||||
"\xd9\x00\x0a\xfa\x00\x00\x00"
|
||||
"\x00\x00\x01\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc6"
|
||||
"\xf1\x5e\xdb\x78\x00\x00\x00"
|
||||
# DCERPC CALL
|
||||
udp 135,1025-1199
|
||||
"\x05\x00\x0b\x03\x10\x00\x00\x00\x48\x00"
|
||||
"\x00\x00\x01\x00\x00\x00\xb8\x10\xb8\x10\x00\x00\x00\x00\x01\x00\x00"
|
||||
"\x00\x00\x00\x01\x00\x01\x23\x45\x67\x89\xab\xcd\xef\x01\x23\x45\x67"
|
||||
"\x89\xab\xcd\xef\xe7\x03\x00\x00\xfe\xdc\xba\x98\x76\x54\x32\x10\x01"
|
||||
"\x23\x45\x67\x89\xab\xcd\xef\xe7\x03\x00\x00"
|
||||
# NBTStat
|
||||
udp 137
|
||||
"\x80\xF0\x00\x10\x00\x01\x00\x00\x00\x00\x00\x00"
|
||||
"\x20CKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x00\x00\x21\x00\x01"
|
||||
# CIFS NS NAME QUERY UC
|
||||
udp 137
|
||||
"\x01\x91\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00"
|
||||
"\x20CKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x00\x00\x21\x00\x01"
|
||||
# CIFS NS NAME QUERY BC
|
||||
udp 137
|
||||
"\x01\x91\x00\x10\x00\x01\x00\x00\x00\x00\x00\x00"
|
||||
"\x20CKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x00\x00\x21\x00\x01"
|
||||
# SNMPv3GetRequest
|
||||
udp 161,260,3401
|
||||
"\x30\x3A\x02\x01\x03\x30\x0F\x02\x02\x4A\x69\x02\x03\x00\xFF\xE3"
|
||||
"\x04\x01\x04\x02\x01\x03\x04\x10\x30\x0E\x04\x00\x02\x01\x00\x02"
|
||||
"\x01\x00\x04\x00\x04\x00\x04\x00\x30\x12\x04\x00\x04\x00\xA0\x0C"
|
||||
"\x02\x02\x37\xF0\x02\x01\x00\x02\x01\x00\x30\x00"
|
||||
# SNMP PUBLIC WALK
|
||||
udp 161,260,3401
|
||||
"\x30\x1f\x02\x01\x00\x04\x06public\xa1\x12\x02\x01\x00\x02"
|
||||
"\x01\x00\x02\x01\x00\x30\x07\x30\x05\x06\x01\x00\x05\x00"
|
||||
|
||||
# Sqlping - disabled because it trips a Snort rule with SID 2049
|
||||
# ("MS-SQL ping attempt").
|
||||
# udp 1434 "\x02"
|
||||
|
||||
# xdmcp - X Display Manager Control Protocol. Version 1, packet type
|
||||
# Query (2), no authorization names. We expect a Willing or Unwilling
|
||||
# packet in reply.
|
||||
# http://cgit.freedesktop.org/xorg/doc/xorg-docs/plain/hardcopy/XDMCP/xdmcp.PS.gz
|
||||
udp 177 "\x00\x01\x00\x02\x00\x01\x00"
|
||||
|
||||
# Connectionless LDAP - used by Microsoft Active Directory
|
||||
udp 389
|
||||
"\x30\x84\x00\x00\x00\x2d\x02\x01\x07\x63\x84\x00\x00\x00\x24\x04\x00"
|
||||
"\x0a\x01\x00\x0a\x01\x00\x02\x01\x00\x02\x01\x64\x01\x01\x00\x87\x0b"
|
||||
"objectClass0\x84\x00\x00\x00\x00"
|
||||
|
||||
|
||||
# svrloc
|
||||
udp 427
|
||||
"\x02\x01\x00\x006 \x00\x00\x00\x00\x00\x01\x00\x02en\x00\x00\x00\x15"
|
||||
"service:service-agent\x00\x07default\x00\x00\x00\x00"
|
||||
|
||||
# DTLS
|
||||
udp 443,853,3391,4433,4740,5349,5684,5868,6514,6636,8232,10161,10162,12346,12446,12546,12646,12746,12846,12946,13046
|
||||
# DTLS 1.0, length 52
|
||||
"\x16\xfe\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x36"
|
||||
# ClientHello, length 40, sequence 0, offset 0
|
||||
"\x01\x00\x00\x2a\x00\x00\x00\x00\x00\x00\x00\x2a"
|
||||
# DTLS 1.2
|
||||
"\xfe\xfd"
|
||||
# Random
|
||||
"\x00\x00\x00\x00\x7c\x77\x40\x1e\x8a\xc8\x22\xa0\xa0\x18\xff\x93"
|
||||
"\x08\xca\xac\x0a\x64\x2f\xc9\x22\x64\xbc\x08\xa8\x16\x89\x19\x3f"
|
||||
# Session id length 0, cookie length 0
|
||||
"\x00\x00"
|
||||
# Cipher suites, mandatory TLS_RSA_WITH_AES_128_CBC_SHA
|
||||
"\x00\x02\x00\x2f"
|
||||
# Compressors (NULL)
|
||||
"\x01\x00"
|
||||
|
||||
# Internet Key Exchange version 1, phase 1 Main Mode. We offer every
|
||||
# combination of (DES, 3DES) and (MD5, SHA) in the hope that one of them will
|
||||
# be acceptable. Because we use a fixed cookie, we set the association lifetime
|
||||
# to 1 second to reduce the chance that repeated probes will look like
|
||||
# retransmissions (and therefore not get a response). This payload comes from
|
||||
# ike-scan --lifetime 1 --cookie 0011223344556677 --trans=5,2,1,2 --trans=5,1,1,2 --trans=1,2,1,2 --trans=1,1,1,2
|
||||
# We expect another phase 1 message in response. This payload works better with
|
||||
# a source port of 500 or a randomized initiator cookie.
|
||||
udp 500
|
||||
# Initiator cookie 0x0011223344556677, responder cookie 0x0000000000000000.
|
||||
"\x00\x11\x22\x33\x44\x55\x66\x77\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
# Version 1, Main Mode, flags 0x00, message ID 0x00000000, length 192.
|
||||
"\x01\x10\x02\x00\x00\x00\x00\x00\x00\x00\x00\xC0"
|
||||
# Security Association payload, length 164, IPSEC, IDENTITY.
|
||||
"\x00\x00\x00\xA4\x00\x00\x00\x01\x00\x00\x00\x01"
|
||||
# Proposal 1, length 152, ISAKMP, 4 transforms.
|
||||
"\x00\x00\x00\x98\x01\x01\x00\x04"
|
||||
# Transform 1, 3DES-CBC, SHA, PSK, group 2.
|
||||
"\x03\x00\x00\x24\x01\x01\x00\x00\x80\x01\x00\x05\x80\x02\x00\x02"
|
||||
"\x80\x03\x00\x01\x80\x04\x00\x02"
|
||||
"\x80\x0B\x00\x01\x00\x0C\x00\x04\x00\x00\x00\x01"
|
||||
# Transform 2, 3DES-CBC, MD5, PSK, group 2.
|
||||
"\x03\x00\x00\x24\x02\x01\x00\x00\x80\x01\x00\x05\x80\x02\x00\x01"
|
||||
"\x80\x03\x00\x01\x80\x04\x00\x02"
|
||||
"\x80\x0B\x00\x01\x00\x0C\x00\x04\x00\x00\x00\x01"
|
||||
# Transform 3, DES-CBC, SHA, PSK, group 2.
|
||||
"\x03\x00\x00\x24\x03\x01\x00\x00\x80\x01\x00\x01\x80\x02\x00\x02"
|
||||
"\x80\x03\x00\x01\x80\x04\x00\x02"
|
||||
"\x80\x0B\x00\x01\x00\x0C\x00\x04\x00\x00\x00\x01"
|
||||
# Transform 4, DES-CBC, MD5, PSK, group 2.
|
||||
"\x00\x00\x00\x24\x04\x01\x00\x00\x80\x01\x00\x01\x80\x02\x00\x01"
|
||||
"\x80\x03\x00\x01\x80\x04\x00\x02"
|
||||
"\x80\x0B\x00\x01\x00\x0C\x00\x04\x00\x00\x00\x01"
|
||||
# IPSEC START
|
||||
udp 500,4500
|
||||
"\x31\x27\xfc"
|
||||
"\xb0\x38\x10\x9e\x89\x00\x00\x00\x00\x00\x00\x00\x00\x01\x10\x02\x00"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\xcc\x0d\x00\x00\x5c\x00\x00\x00\x01\x00"
|
||||
"\x00\x00\x01\x00\x00\x00\x50\x01\x01\x00\x02\x03\x00\x00\x24\x01\x01"
|
||||
"\x00\x00\x80\x01\x00\x05\x80\x02\x00\x02\x80\x04\x00\x02\x80\x03\x00"
|
||||
"\x03\x80\x0b\x00\x01\x00\x0c\x00\x04\x00\x00\x0e\x10\x00\x00\x00\x24"
|
||||
"\x02\x01\x00\x00\x80\x01\x00\x05\x80\x02\x00\x01\x80\x04\x00\x02\x80"
|
||||
"\x03\x00\x03\x80\x0b\x00\x01\x00\x0c\x00\x04\x00\x00\x0e\x10\x0d\x00"
|
||||
"\x00\x18\x1e\x2b\x51\x69\x05\x99\x1c\x7d\x7c\x96\xfc\xbf\xb5\x87\xe4"
|
||||
"\x61\x00\x00\x00\x04\x0d\x00\x00\x14\x40\x48\xb7\xd5\x6e\xbc\xe8\x85"
|
||||
"\x25\xe7\xde\x7f\x00\xd6\xc2\xd3\x0d\x00\x00\x14\x90\xcb\x80\x91\x3e"
|
||||
"\xbb\x69\x6e\x08\x63\x81\xb5\xec\x42\x7b\x1f\x00\x00\x00\x14\x26\x24"
|
||||
"\x4d\x38\xed\xdb\x61\xb3\x17\x2a\x36\xe3\xd0\xcf\xb8\x19"
|
||||
|
||||
# Routing Information Protocol version 1. Special-case request for the entire
|
||||
# routing table (address family 0, address 0.0.0.0, metric 16). RFC 1058,
|
||||
# section 3.4.1.
|
||||
udp 520
|
||||
"\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x10"
|
||||
|
||||
# RMCP ASF ping
|
||||
udp 623
|
||||
"\x06\x00\xff\x06" # RMCP version 6, sequence 0xff, normal RMCP class ASF
|
||||
"\x00\x00\x11\xbe" # IAN ASF code 4542
|
||||
"\x80\x00\x00\x00" # payload-less ASF presence ping
|
||||
|
||||
# IPMI
|
||||
# RMCP Get Channel Auth Capabilities
|
||||
udp 623
|
||||
"\x06\x00\xff\x07\x00\x00\x00\x00\x00\x00\x00\x00\x00\x09\x20\x18"
|
||||
"\xc8\x81\x00\x38\x8e\x04\xb5"
|
||||
|
||||
# serialnumberd. This service runs on Mac OS X Server. This probe
|
||||
# requests the serial number of another server. In response we expect a
|
||||
# packet starting with "SNRESPS:", followed by some data whose purpose
|
||||
# is not known.
|
||||
udp 626 "SNQUERY: 127.0.0.1:AAAAAA:xsvr"
|
||||
|
||||
# OpenVPN P_CONTROL_HARD_RESET_CLIENT_V2
|
||||
# Byte 0; 0x38 opcode
|
||||
# Byte 1-8: Session ID, random
|
||||
# Byte 9: Message packet-id array length (0)
|
||||
# Byte 10-13: Message packet-id (0)
|
||||
udp 1194 "8d\xc1x\x01\xb8\x9b\xcb\x8f\0\0\0\0\0"
|
||||
# OpenVPN when in PKI mode and without the "HMAC Firewall" setting enabled
|
||||
# (tls-auth) should respond to the following probe, which is
|
||||
# 0x38<8 random bytes><4 null bytes>
|
||||
udp 1194
|
||||
"\x38\x01\x02\x03\x04\x05\x06\x07\x08\x00\x00\x00\x00"
|
||||
|
||||
# Citrix MetaFrame application browser service
|
||||
# Original idea from http://sh0dan.org/oldfiles/hackingcitrix.html
|
||||
# Payload contents copied from Wireshark capture of Citrix Program
|
||||
# Neighborhood client application. The application uses this payload to
|
||||
# locate Citrix servers on the local network. Response to this probe is
|
||||
# a 48 byte UDP payload as shown here:
|
||||
#
|
||||
# 0000 30 00 02 31 02 fd a8 e3 02 00 06 44 c0 a8 80 55
|
||||
# 0010 00 00 00 00 00 00 00 00 00 00 00 00 02 00 06 44
|
||||
# 0020 c0 a8 80 56 00 00 00 00 00 00 00 00 00 00 00 00
|
||||
#
|
||||
# The first 12 bytes appear to be the same in all responses.
|
||||
#
|
||||
# Bytes 0x00 appears to be a packet length field
|
||||
# Bytes 0x0C - 0x0F are the IP address of the server
|
||||
# Bytes 0x10 - 0x13 may vary, 0x14 - 0x1F do not appear to
|
||||
# Bytes 0x20 - 0x23 are the IP address of the primary system in a server farm
|
||||
# configuration
|
||||
# Bytes 0x24 - 0x27 can vary, 0x28 - 0x2F do not appear to
|
||||
udp 1604
|
||||
"\x1e\x00\x01\x30\x02\xfd\xa8\xe3\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
|
||||
# RADIUS Access-Request. This is a degenerate packet with no username or
|
||||
# password; we expect an Access-Reject in response. The Identifier and Request
|
||||
# Authenticator are both 0. It was generated by running
|
||||
# echo 'User-Password = ""' | radclient <ip> auth ""
|
||||
# and then manually stripping out the password.
|
||||
#
|
||||
# Section 2 of the RFC says "A request from a client for which the
|
||||
# RADIUS server does not have a shared secret MUST be silently
|
||||
# discarded." So this payload only works when the server is configured
|
||||
# (or misconfigured) to know the scanning machine as a client.
|
||||
#
|
||||
# RFC 2865: "The early deployment of RADIUS was done using UDP port
|
||||
# number 1645, which conflicts with the "datametrics" service. The
|
||||
# officially assigned port number for RADIUS is 1812.
|
||||
udp 1645,1812
|
||||
"\x01\x00\x00\x14"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
|
||||
# L2TP ICRQ
|
||||
udp 1701
|
||||
"\xc8\x02"
|
||||
"\x00\x3c\x00\x00\x00\x00\x00\x00\x00\x00\x80\x08\x00\x00\x00\x00\x00"
|
||||
"\x01\x80\x08\x00\x00\x00\x02\x01\x00\x80\x0e\x00\x00\x00\x07"
|
||||
"nxp-scan\x80\x0a\x00\x00\x00\x03\x00\x00\x00\x03\x80"
|
||||
"\x08\x00\x00\x00\x09\x00\x00"
|
||||
|
||||
# UPNP MSEARCH
|
||||
udp 1900
|
||||
"M-SEARCH * HTTP/1.1\r\nHost: 239.255.255.250:1900\r\n"
|
||||
"Man: \"ssdp:discover\"\r\nMX: 5\r\nST: ssdp:all\r\n\r\n"
|
||||
|
||||
# NFS version 2, RFC 1831. XID 0x00000000, program 100003 (NFS), procedure
|
||||
# NFSPROC_NULL (does nothing, see section 2.2.1), null authentication (see
|
||||
# section 9.1).
|
||||
udp 2049
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x01\x86\xA3"
|
||||
"\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
|
||||
# GPRS Tunneling Protocol (GTP)
|
||||
udp 2123,2152
|
||||
# GTPv1, protocol 1
|
||||
"\x32"
|
||||
# EchoRequest
|
||||
"\x01"
|
||||
# message length
|
||||
"\x00\x04"
|
||||
# Tunnel endpoint identifier
|
||||
"\x00\x00\x42\x00"
|
||||
# sequence number
|
||||
"\x13\x37"
|
||||
# N-PDU number
|
||||
"\x00"
|
||||
# next extension header type
|
||||
"\x00"
|
||||
|
||||
# GPRS Tunneling Protocol (GTP) "prime" v2
|
||||
# This same packet can be used for GTP v2 on ports 2123 and 2152 if you change
|
||||
# the first byte from \x4e to \x40
|
||||
udp 3386
|
||||
# GTP'v2
|
||||
"\x4e"
|
||||
# EchoRequest
|
||||
"\x01"
|
||||
# message length
|
||||
"\x00\x04"
|
||||
# sequence number
|
||||
"\xde\xfe\xc8\x00"
|
||||
|
||||
# Freelancer game server status query
|
||||
# http://sourceforge.net/projects/gameq/
|
||||
# (relevant files: games.ini, packets.ini, freelancer.php)
|
||||
udp 2302 "\x00\x02\xf1\x26\x01\x26\xf0\x90\xa6\xf0\x26\x57\x4e\xac\xa0\xec\xf8\x68\xe4\x8d\x21"
|
||||
|
||||
# Apple Remote Desktop (ARD)
|
||||
udp 3283 "\0\x14\0\x01\x03"
|
||||
|
||||
# STUN Binding request, see RFC 5389 Section 6
|
||||
# message type = 0x001, Binding (see Section 18.1)
|
||||
# message length = 0
|
||||
# magic cookie = 0x2112a442
|
||||
# transaction ID = "\x00"*12
|
||||
udp 3478 "\x00\x01\x00\x00\x21\x12\xa4\x42\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
|
||||
# Sun Service Tag Discovery protocol (stdiscover)
|
||||
# http://arc.opensolaris.org/caselog/PSARC/2006/638/stdiscover_protocolv2.pdf
|
||||
# Would work better with a varying cookie; the second and later sends of this
|
||||
# probe will be interpreted as resends by the server and will be ignored.
|
||||
udp 6481 "[PROBE] 0000"
|
||||
|
||||
# NAT-PMP external IP address request. See section 3.2 of
|
||||
# http://files.dns-sd.org/draft-cheshire-nat-pmp.txt.
|
||||
udp 5351 "\x00\x00"
|
||||
|
||||
# DNS Service Discovery (DNS-SD) service query, as used in Zeroconf.
|
||||
# Transaction ID 0x0000, flags 0x0000, 1 question: PTR query for
|
||||
# _services._dns-sd._udp.local. If the remote host supports DNS-SD it will send
|
||||
# back a list of all its services. This is the same as a packet capture of
|
||||
# dns-sd -B _services._dns-sd._udp .
|
||||
# See section 9 of
|
||||
# http://files.dns-sd.org/draft-cheshire-dnsext-dns-sd.txt.
|
||||
# This first probe is a QU probe, meaning a unicast response is desired
|
||||
udp 5353
|
||||
"\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00"
|
||||
"\x09_services\x07_dns-sd\x04_udp\x05local\x00\x00\x0C\x80\x01"
|
||||
# This second probe is a QM probe, meaning a unicast response is desired
|
||||
udp 5353
|
||||
"\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00"
|
||||
"\x09_services\x07_dns-sd\x04_udp\x05local\x00\x00\x0C\x00\x01"
|
||||
|
||||
# PCANY STATUS
|
||||
udp 5632 "ST"
|
||||
|
||||
# CoAP GET .well-known/core
|
||||
udp 5683 "@\x01\x01\xce\xbb.well-known\x04core"
|
||||
|
||||
# UT2K PING
|
||||
udp 7777 "None\x00"
|
||||
|
||||
# Ubiquiti Discovery Service - v1
|
||||
udp 10001 "\x01\x00\x00\x00"
|
||||
|
||||
# Amanda backup service noop request. I think that this does nothing on the
|
||||
# server but only asks it to send back its feature list. In reply we expect an
|
||||
# ACK or (more likely) an ERROR. I couldn't find good online documentation of
|
||||
# the Amanda network protocol. There is parsing code in the Amanda source at
|
||||
# common-src/security-util.c. This is based on a packet capture of
|
||||
# amcheck <config> <host>
|
||||
udp 10080
|
||||
"Amanda 2.6 REQ HANDLE 000-00000000 SEQ 0\n"
|
||||
"SERVICE noop\n"
|
||||
|
||||
# VxWorks Wind River Debugger
|
||||
udp 17185
|
||||
# Random XID
|
||||
"\x00\x00\x00\x00"
|
||||
# RPC version 2 procedure call
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x02"
|
||||
# WDB version 1
|
||||
"\x55\x55\x55\x55\x00\x00\x00\x01"
|
||||
# WDB_TARGET_PING
|
||||
"\x00\x00\x00\x00"
|
||||
# RPC Auth NULL
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
# Checksum
|
||||
"\xff\xff\x55\x13"
|
||||
# WDB wrapper (length and sequence number)
|
||||
"\x00\x00\x00\x30\x00\x00\x00\x01"
|
||||
# Empty data?
|
||||
"\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
# VXWORKS DEBUG (alternative?)
|
||||
udp 17185
|
||||
"\x72\x37\x72\x37\x00\x00\x00"
|
||||
"\x00\x00\x00\x00\x02\x55\x55\x55\x55\x00\x00\x00\x01\x00\x00\x00\x01"
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff"
|
||||
"\xff\x55\x10\x00\x00\x00\x3c\x00\x00\x00\x03\x00\x00\x00\x02\x00\x00"
|
||||
"\x00\x00\x00\x00\x00\x00"
|
||||
|
||||
# Quake 2 and Quake 3 game servers (and servers of derived games like Nexuiz).
|
||||
# Gets game information from the server (see probe responses in
|
||||
# nmap-service-probes). These services typically run on a base port or a
|
||||
# few numbers higher.
|
||||
# Quake 2. Typical ports: 27910-27914.
|
||||
udp 27910-27914 "\xff\xff\xff\xffstatus"
|
||||
# Quake 3. Typical ports:
|
||||
# 26000-26004: Nexuiz
|
||||
# 27960-27964: Various games
|
||||
# 30720-30724: Tremulous
|
||||
# 44400: Warsow
|
||||
udp 26000-26004,27960-27964,30720-30724,44400 "\xff\xff\xff\xffgetstatus"
|
||||
|
||||
# Murmur 1.2.X (Mumble server)
|
||||
# UDP ping. "abcdefgh" is an identifier. See
|
||||
# http://mumble.sourceforge.net/Protocol.
|
||||
udp 64738 "\x00\x00\x00\x00abcdefgh"
|
||||
|
||||
# Ventrilo 2.1.2+
|
||||
# UDP general status request (encrypted).
|
||||
# See http://aluigi.altervista.org/papers.htm#ventrilo
|
||||
udp 3784
|
||||
"\x01\xe7\xe5\x75\x31\xa3\x17\x0b\x21\xcf\xbf\x2b\x99\x4e\xdd\x19\xac\xde\x08\x5f\x8b\x24\x0a\x11\x19\xb6\x73\x6f\xad\x28\x13\xd2\x0a\xb9\x12\x75"
|
||||
|
||||
# Kademlia (kad) as used by various P2P applications. Send a Kademlia ping
|
||||
# 4665, 4666, 4672, 6429: eDonkey/eMule and variants
|
||||
udp 4665,4666,4672,6429 "\xE4\x60"
|
||||
|
||||
# TeamSpeak 2
|
||||
# UDP login request
|
||||
# See http://wiki.wireshark.org/TeamSpeak2
|
||||
udp 8767
|
||||
"\xf4\xbe\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x002x\xba\x85\tTeamSpeak\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\nWindows XP\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00 \x00<\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08nickname\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
|
||||
# TS3INIT1
|
||||
udp 9987
|
||||
"TS3INIT1\x00\x65\x00\x00\x88\x0a\x39\x7b\x0f\x00\x5b\x55\x72\xef\xdc\x78\x32\x6b\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
|
||||
# TeamSpeak 3
|
||||
# UDP login request (encrypted)
|
||||
# http://seclists.org/nmap-dev/2013/q3/72
|
||||
udp 9987
|
||||
"\x05\xca\x7f\x16\x9c\x11\xf9\x89\x00\x00\x00\x00\x02\x9d\x74\x8b\x45\xaa\x7b\xef\xb9\x9e\xfe\xad\x08\x19\xba\xcf\x41\xe0\x16\xa2\x32\x6c\xf3\xcf\xf4\x8e\x3c\x44\x83\xc8\x8d\x51\x45\x6f\x90\x95\x23\x3e\x00\x97\x2b\x1c\x71\xb2\x4e\xc0\x61\xf1\xd7\x6f\xc5\x7e\xf6\x48\x52\xbf\x82\x6a\xa2\x3b\x65\xaa\x18\x7a\x17\x38\xc3\x81\x27\xc3\x47\xfc\xa7\x35\xba\xfc\x0f\x9d\x9d\x72\x24\x9d\xfc\x02\x17\x6d\x6b\xb1\x2d\x72\xc6\xe3\x17\x1c\x95\xd9\x69\x99\x57\xce\xdd\xdf\x05\xdc\x03\x94\x56\x04\x3a\x14\xe5\xad\x9a\x2b\x14\x30\x3a\x23\xa3\x25\xad\xe8\xe6\x39\x8a\x85\x2a\xc6\xdf\xe5\x5d\x2d\xa0\x2f\x5d\x9c\xd7\x2b\x24\xfb\xb0\x9c\xc2\xba\x89\xb4\x1b\x17\xa2\xb6"
|
||||
|
||||
# Memcached
|
||||
# version request (shorter response than stats)
|
||||
# https://github.com/memcached/memcached/blob/master/doc/protocol.txt
|
||||
udp 11211
|
||||
"\0\x01\0\0\0\x01\0\0version\r\n"
|
||||
|
||||
# Steam, typically using a port in 27015-27030. Send a "Source Engine query"
|
||||
udp 27015-27030
|
||||
"\xff\xff\xff\xffTSource Engine Query\x00"
|
||||
|
||||
# TRIN00 UNIX PING
|
||||
udp 27444 "png l44adsl"
|
||||
|
||||
# BO PING
|
||||
udp 31337
|
||||
"\xce\x63\xd1\xd2\x16\xe7\x13\xcf\x38"
|
||||
"\xa5\xa5\x86\xb2\x75\x4b\x99\xaa\x32\x58"
|
||||
|
||||
# TRIN00 WIN PING
|
||||
udp 34555 "png []..Ks"
|
||||
|
||||
# Beckhoff ADS discovery request
|
||||
# https://github.com/ONE75/adsclient/blob/master/src/AdsClient.Finder/DeviceFinder.cs#L49-L64
|
||||
udp 48899
|
||||
"\x03\x66\x14\x71\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01\x01\x10\x27\x00\x00\x00\x00"
|
||||
|
After Width: | Height: | Size: 134 KiB |
@@ -0,0 +1,131 @@
|
||||
# The configurations that used for the recording, feel free to edit them
|
||||
config:
|
||||
|
||||
# Specify a command to be executed
|
||||
# like `/bin/bash -l`, `ls`, or any other commands
|
||||
# the default is bash for Linux
|
||||
# or powershell.exe for Windows
|
||||
command: zsh
|
||||
|
||||
# Specify the current working directory path
|
||||
# the default is the current working directory path
|
||||
cwd: /home/bee/Documents/RustScan
|
||||
|
||||
# Export additional ENV variables
|
||||
env:
|
||||
recording: true
|
||||
|
||||
# Explicitly set the number of columns
|
||||
# or use `auto` to take the current
|
||||
# number of columns of your shell
|
||||
cols: 107
|
||||
|
||||
# Explicitly set the number of rows
|
||||
# or use `auto` to take the current
|
||||
# number of rows of your shell
|
||||
rows: 43
|
||||
|
||||
# Amount of times to repeat GIF
|
||||
# If value is -1, play once
|
||||
# If value is 0, loop indefinitely
|
||||
# If value is a positive number, loop n times
|
||||
repeat: 0
|
||||
|
||||
# Quality
|
||||
# 1 - 100
|
||||
quality: 100
|
||||
|
||||
# Delay between frames in ms
|
||||
# If the value is `auto` use the actual recording delays
|
||||
frameDelay: auto
|
||||
|
||||
# Maximum delay between frames in ms
|
||||
# Ignored if the `frameDelay` isn't set to `auto`
|
||||
# Set to `auto` to prevent limiting the max idle time
|
||||
maxIdleTime: 2000
|
||||
|
||||
# The surrounding frame box
|
||||
# The `type` can be null, window, floating, or solid`
|
||||
# To hide the title use the value null
|
||||
# Don't forget to add a backgroundColor style with a null as type
|
||||
frameBox:
|
||||
type: floating
|
||||
title: Terminalizer
|
||||
style:
|
||||
border: 0px black solid
|
||||
# boxShadow: none
|
||||
# margin: 0px
|
||||
|
||||
# Add a watermark image to the rendered gif
|
||||
# You need to specify an absolute path for
|
||||
# the image on your machine or a URL, and you can also
|
||||
# add your own CSS styles
|
||||
watermark:
|
||||
imagePath: null
|
||||
style:
|
||||
position: absolute
|
||||
right: 15px
|
||||
bottom: 15px
|
||||
width: 100px
|
||||
opacity: 0.9
|
||||
|
||||
# Cursor style can be one of
|
||||
# `block`, `underline`, or `bar`
|
||||
cursorStyle: block
|
||||
|
||||
# Font family
|
||||
# You can use any font that is installed on your machine
|
||||
# in CSS-like syntax
|
||||
fontFamily: "Monaco, Lucida Console, Ubuntu Mono, Monospace"
|
||||
|
||||
# The size of the font
|
||||
fontSize: 12
|
||||
|
||||
# The height of lines
|
||||
lineHeight: 1
|
||||
|
||||
# The spacing between letters
|
||||
letterSpacing: 0
|
||||
|
||||
# Theme
|
||||
theme:
|
||||
background: "transparent"
|
||||
foreground: "#afafaf"
|
||||
cursor: "#c7c7c7"
|
||||
black: "#232628"
|
||||
red: "#fc4384"
|
||||
green: "#b3e33b"
|
||||
yellow: "#ffa727"
|
||||
blue: "#75dff2"
|
||||
magenta: "#ae89fe"
|
||||
cyan: "#708387"
|
||||
white: "#d5d5d0"
|
||||
brightBlack: "#626566"
|
||||
brightRed: "#ff7fac"
|
||||
brightGreen: "#c8ed71"
|
||||
brightYellow: "#ebdf86"
|
||||
brightBlue: "#75dff2"
|
||||
brightMagenta: "#ae89fe"
|
||||
brightCyan: "#b1c6ca"
|
||||
brightWhite: "#f9f9f4"
|
||||
|
||||
# Records, feel free to edit them
|
||||
records:
|
||||
- delay: 436
|
||||
content: "\e[1m\e[7m%\e[27m\e[1m\e[0m \r \r\e]2;bee@beehive:~/Documents/RustScan\a\e]1;..ents/RustScan\a"
|
||||
- delay: 162
|
||||
content: "\r\e[0m\e[27m\e[24m\e[J\r\n\e[1m\e[0m\e[1m\e[36mRustScan\e[0m\e[36m\e[39m\e[1m \e[0m\e[1mon \e[0m\e[1m\e[37m\e[1m\e[37m\e[0m\e[37m\e[1m\e[37m\e[35m master\e[0m\e[35m\e[39m\e[1m\e[0m\e[1m\e[0m\e[1m\e[31m [?]\e[0m\e[31m\e[39m\e[1m\e[0m\e[0m\e[39m\e[1m \e[0m\e[1mis \e[0m\e[1m\e[31m\U0001F4E6 vrustscan:1.10.0\e[0m\e[31m\e[39m\e[1m \e[0m\e[1mvia \e[0m\e[1m\e[31m\U0001D5E5 v1.46.0\e[0m\e[31m\e[39m\e[1m \e[0m\r\n\e[1m\e[0m\e[1m\e[32m➜ \e[0m\e[32m\e[39m\e[1m\e[0m\e[K\e[?1h\e=\e[?2004h"
|
||||
- delay: 471
|
||||
content: "r\brustscan 127.0.0.1 --accessible --no-nmap --ulimit 5000\e[55D\e[7mr\e[7mu\e[7ms\e[7mt\e[7ms\e[7mc\e[7ma\e[7mn\e[7m \e[7m1\e[7m2\e[7m7\e[7m.\e[7m0\e[7m.\e[7m0\e[7m.\e[7m1\e[7m \e[7m-\e[7m-\e[7ma\e[7mc\e[7mc\e[7me\e[7ms\e[7ms\e[7mi\e[7mb\e[7ml\e[7me\e[7m \e[7m-\e[7m-\e[7mn\e[7mo\e[7m-\e[7mn\e[7mm\e[7ma\e[7mp\e[7m \e[7m-\e[7m-\e[7mu\e[7ml\e[7mi\e[7mm\e[7mi\e[7mt\e[7m \e[7m5\e[7m0\e[7m0\e[7m0\e[27m"
|
||||
- delay: 269
|
||||
content: "\e[55D\e[27mr\e[27mu\e[27ms\e[27mt\e[27ms\e[27mc\e[27ma\e[27mn\e[27m \e[27m1\e[27m2\e[27m7\e[27m.\e[27m0\e[27m.\e[27m0\e[27m.\e[27m1\e[27m \e[27m-\e[27m-\e[27ma\e[27mc\e[27mc\e[27me\e[27ms\e[27ms\e[27mi\e[27mb\e[27ml\e[27me\e[27m \e[27m-\e[27m-\e[27mn\e[27mo\e[27m-\e[27mn\e[27mm\e[27ma\e[27mp\e[27m \e[27m-\e[27m-\e[27mu\e[27ml\e[27mi\e[27mm\e[27mi\e[27mt\e[27m \e[27m5\e[27m0\e[27m0\e[27m0\e[?1l\e>\e[?2004l\r\r\n\e]2;rustscan 127.0.0.1 --accessible --no-nmap --ulimit 5000\a\e]1;rustscan\aAutomatically increasing ulimit value to 5000.\r\n"
|
||||
- delay: 1536
|
||||
content: "Open 127.0.0.1:38602\r\n"
|
||||
- delay: 395
|
||||
content: "Open 127.0.0.1:50078\r\n"
|
||||
- delay: 450
|
||||
content: "127.0.0.1 -> [38602,50078]\r\n\e[1m\e[7m%\e[27m\e[1m\e[0m \r \r\e]2;bee@beehive:~/Documents/RustScan\a\e]1;..ents/RustScan\a"
|
||||
- delay: 143
|
||||
content: "\r\e[0m\e[27m\e[24m\e[J\r\n\e[1m\e[0m\e[1m\e[36mRustScan\e[0m\e[36m\e[39m\e[1m \e[0m\e[1mon \e[0m\e[1m\e[37m\e[1m\e[37m\e[0m\e[37m\e[1m\e[37m\e[35m master\e[0m\e[35m\e[39m\e[1m\e[0m\e[1m\e[0m\e[1m\e[31m [?]\e[0m\e[31m\e[39m\e[1m\e[0m\e[0m\e[39m\e[1m \e[0m\e[1mis \e[0m\e[1m\e[31m\U0001F4E6 vrustscan:1.10.0\e[0m\e[31m\e[39m\e[1m \e[0m\e[1mvia \e[0m\e[1m\e[31m\U0001D5E5 v1.46.0\e[0m\e[31m\e[39m\e[1m \e[0m\e[1mtook \e[0m\e[1m\e[33m2s\e[0m\e[33m\e[39m\e[1m \e[0m\r\n\e[1m\e[0m\e[1m\e[32m➜ \e[0m\e[32m\e[39m\e[1m\e[0m\e[K\e[?1h\e=\e[?2004h"
|
||||
- delay: 563
|
||||
content: "\e[?2004l\r\r\n"
|
||||
|
After Width: | Height: | Size: 160 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 258 KiB |
@@ -0,0 +1 @@
|
||||
cargo publish && cargo build --release && git add Cargo.lock && git commit -m 'Updating lock file' && gpsup
|
||||
@@ -1,20 +0,0 @@
|
||||
FROM rust:latest
|
||||
|
||||
RUN git clone https://github.com/brandonskerritt/RustScan
|
||||
WORKDIR "/RustScan"
|
||||
RUN git pull --force
|
||||
RUN cargo install cargo-deb
|
||||
|
||||
RUN apt update -y && apt upgrade -y
|
||||
RUN apt install libc6-dev-i386 -y
|
||||
RUN git clone --depth=1 https://github.com/raspberrypi/tools /raspberrypi-tools
|
||||
ENV PATH=/raspberrypi-tools/arm-bcm2708/gcc-linaro-arm-linux-gnueabihf-raspbian-x64/bin/:$PATH
|
||||
ENV CARGO_TARGET_ARM_UNKNOWN_LINUX_GNUEABIHF_LINKER=arm-linux-gnueabihf-gcc
|
||||
RUN mkdir /root/.cargo
|
||||
RUN echo "[target.arm-unknown-linux-gnueabihf]" >> /root/.cargo/config
|
||||
RUN echo "strip = { path = \"arm-linux-gnueabihf-strip\" }" >> /root/.cargo/config
|
||||
RUN echo "objcopy = { path = \"arm-linux-gnueabihf-objcopy\" }" >> /root/.cargo/config
|
||||
|
||||
COPY ./entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
@@ -1,17 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
cd /RustScan
|
||||
git pull --force
|
||||
|
||||
#amd64
|
||||
cargo deb
|
||||
|
||||
#arm64
|
||||
rustup target add arm-unknown-linux-gnueabihf
|
||||
cargo deb --target=arm-unknown-linux-gnueabihf
|
||||
|
||||
#i386
|
||||
rustup target add i686-unknown-linux-gnu
|
||||
cargo deb --target=i686-unknown-linux-gnu
|
||||
|
||||
find target/ -name \*.deb -exec cp {} /debs \;
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/bin/bash
|
||||
docker build -t rustscan-builder . || exit
|
||||
|
||||
# This creates a volume which binds your currentdirectory/debs to
|
||||
# the location where the deb files get spat out in the container.
|
||||
# You don't need to worry about it. Just chmod +x run.sh && ./run.sh and
|
||||
# you'll get yer .deb file in a few minutes. It runs faster after you've used it the first time.
|
||||
docker run -v "$(pwd)/debs:/debs" rustscan-builder
|
||||
@@ -0,0 +1,550 @@
|
||||
//! Provides functions to parse input IP addresses, CIDRs or files.
|
||||
use std::cell::LazyCell;
|
||||
use std::collections::BTreeSet;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{prelude::*, BufReader};
|
||||
use std::net::{IpAddr, SocketAddr, ToSocketAddrs};
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
|
||||
use cidr_utils::cidr::{IpCidr, IpInet};
|
||||
use hickory_resolver::{
|
||||
config::{NameServerConfig, Protocol, ResolverConfig, ResolverOpts},
|
||||
Resolver,
|
||||
};
|
||||
use log::debug;
|
||||
|
||||
use crate::input::Opts;
|
||||
use crate::warning;
|
||||
|
||||
/// Parses the string(s) into IP addresses.
|
||||
///
|
||||
/// Goes through all possible IP inputs (files or via argparsing).
|
||||
///
|
||||
/// ```rust
|
||||
/// # use rustscan::input::Opts;
|
||||
/// # use rustscan::address::parse_addresses;
|
||||
/// let mut opts = Opts::default();
|
||||
/// opts.addresses = vec!["192.168.0.0/30".to_owned()];
|
||||
///
|
||||
/// let ips = parse_addresses(&opts);
|
||||
/// ```
|
||||
///
|
||||
/// Finally, any duplicates are removed to avoid excessive scans.
|
||||
///
|
||||
/// # Blocking
|
||||
///
|
||||
/// This function blocks: it reads files and resolves host names, and
|
||||
/// hickory's synchronous [`Resolver`] runs a Tokio runtime of its own, which
|
||||
/// Tokio refuses to drop inside an asynchronous context. Call it before you
|
||||
/// start your runtime (as the `rustscan` binary does) or from
|
||||
/// `tokio::task::spawn_blocking`, not directly from async code.
|
||||
pub fn parse_addresses(input: &Opts) -> Vec<IpAddr> {
|
||||
parse_addresses_with_resolver(input, || get_resolver(&input.resolver))
|
||||
}
|
||||
|
||||
fn parse_addresses_with_resolver(
|
||||
input: &Opts,
|
||||
create_resolver: impl FnOnce() -> Resolver,
|
||||
) -> Vec<IpAddr> {
|
||||
let mut ips: Vec<IpAddr> = Vec::new();
|
||||
let mut unresolved_addresses: Vec<&str> = Vec::new();
|
||||
// Most scans use literal IPs or CIDRs. Defer the resolver's runtime and
|
||||
// hosts-file loading until a DNS fallback actually needs them, then reuse
|
||||
// that resolver for the rest of the targets and exclusions.
|
||||
let backup_resolver = LazyCell::new(create_resolver);
|
||||
let resolver = || &*backup_resolver;
|
||||
|
||||
for address in &input.addresses {
|
||||
let parsed_ips = parse_address_with_resolver(address, &resolver);
|
||||
if !parsed_ips.is_empty() {
|
||||
ips.extend(parsed_ips);
|
||||
} else {
|
||||
unresolved_addresses.push(address);
|
||||
}
|
||||
}
|
||||
|
||||
// If we got to this point this can only be a file path or the wrong input.
|
||||
for file_path in unresolved_addresses {
|
||||
let file_path = Path::new(file_path);
|
||||
|
||||
if !file_path.is_file() {
|
||||
warning!(
|
||||
format!("Host {file_path:?} could not be resolved."),
|
||||
input.greppable,
|
||||
input.accessible
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Ok(x) = read_ips_from_file(file_path, &resolver) {
|
||||
ips.extend(x);
|
||||
} else {
|
||||
warning!(
|
||||
format!("Host {file_path:?} could not be resolved."),
|
||||
input.greppable,
|
||||
input.accessible
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let excluded_cidrs = parse_excluded_networks_with_resolver(&input.exclude_addresses, &resolver);
|
||||
|
||||
// Remove duplicated/excluded IPs.
|
||||
let mut seen = BTreeSet::new();
|
||||
ips.retain(|ip| seen.insert(*ip) && !excluded_cidrs.iter().any(|cidr| cidr.contains(ip)));
|
||||
|
||||
ips
|
||||
}
|
||||
|
||||
/// Given a string, parse it as a host, IP address, or CIDR.
|
||||
///
|
||||
/// This allows us to pass files as hosts or cidr or IPs easily
|
||||
/// Call this every time you have a possible IP-or-host.
|
||||
///
|
||||
/// If the address is a domain, we can self-resolve the domain locally
|
||||
/// or resolve it by dns resolver list.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use rustscan::address::parse_address;
|
||||
/// # use hickory_resolver::Resolver;
|
||||
/// let ips = parse_address("127.0.0.1", &Resolver::default().unwrap());
|
||||
/// ```
|
||||
pub fn parse_address(address: &str, resolver: &Resolver) -> Vec<IpAddr> {
|
||||
parse_address_with_resolver(address, &|| resolver)
|
||||
}
|
||||
|
||||
fn parse_address_with_resolver<'a>(
|
||||
address: &str,
|
||||
resolver: &impl Fn() -> &'a Resolver,
|
||||
) -> Vec<IpAddr> {
|
||||
if let Ok(addr) = IpAddr::from_str(address) {
|
||||
// `address` is an IP string
|
||||
vec![addr]
|
||||
} else if let Ok(net_addr) = IpInet::from_str(address) {
|
||||
// `address` is a CIDR string
|
||||
net_addr.network().into_iter().addresses().collect()
|
||||
} else {
|
||||
// `address` is a hostname or DNS name
|
||||
// attempt default DNS lookup
|
||||
match format!("{address}:80").to_socket_addrs() {
|
||||
Ok(mut iter) => vec![iter.next().unwrap().ip()],
|
||||
// default lookup didn't work, so try again with the dedicated resolver
|
||||
Err(_) => resolve_ips_from_host(address, resolver),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Uses DNS to get the IPS associated with host
|
||||
fn resolve_ips_from_host<'a>(
|
||||
source: &str,
|
||||
backup_resolver: &impl Fn() -> &'a Resolver,
|
||||
) -> Vec<IpAddr> {
|
||||
let mut ips: Vec<IpAddr> = Vec::new();
|
||||
|
||||
if let Ok(addrs) = source.to_socket_addrs() {
|
||||
for ip in addrs {
|
||||
ips.push(ip.ip());
|
||||
}
|
||||
} else if let Ok(addrs) = backup_resolver().lookup_ip(source) {
|
||||
ips.extend(addrs.iter());
|
||||
}
|
||||
|
||||
ips
|
||||
}
|
||||
|
||||
/// Parses excluded networks from a list of addresses.
|
||||
///
|
||||
/// This function handles three types of inputs:
|
||||
/// 1. CIDR notation (e.g. "192.168.0.0/24")
|
||||
/// 2. Single IP addresses (e.g. "192.168.0.1")
|
||||
/// 3. Hostnames that need to be resolved (e.g. "example.com")
|
||||
///
|
||||
/// ```rust
|
||||
/// # use rustscan::address::parse_excluded_networks;
|
||||
/// # use hickory_resolver::Resolver;
|
||||
/// let resolver = Resolver::default().unwrap();
|
||||
/// let excluded = parse_excluded_networks(&Some(vec!["192.168.0.0/24".to_owned()]), &resolver);
|
||||
/// ```
|
||||
pub fn parse_excluded_networks(
|
||||
exclude_addresses: &Option<Vec<String>>,
|
||||
resolver: &Resolver,
|
||||
) -> Vec<IpCidr> {
|
||||
parse_excluded_networks_with_resolver(exclude_addresses, &|| resolver)
|
||||
}
|
||||
|
||||
fn parse_excluded_networks_with_resolver<'a>(
|
||||
exclude_addresses: &Option<Vec<String>>,
|
||||
resolver: &impl Fn() -> &'a Resolver,
|
||||
) -> Vec<IpCidr> {
|
||||
exclude_addresses
|
||||
.iter()
|
||||
.flatten()
|
||||
.flat_map(|addr| parse_single_excluded_address(addr, resolver))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Parses a single address into an IpCidr, handling CIDR notation, IP addresses, and hostnames.
|
||||
fn parse_single_excluded_address<'a>(
|
||||
addr: &str,
|
||||
resolver: &impl Fn() -> &'a Resolver,
|
||||
) -> Vec<IpCidr> {
|
||||
if let Ok(cidr) = IpCidr::from_str(addr) {
|
||||
return vec![cidr];
|
||||
}
|
||||
|
||||
if let Ok(ip) = IpAddr::from_str(addr) {
|
||||
return vec![IpCidr::new_host(ip)];
|
||||
}
|
||||
|
||||
resolve_ips_from_host(addr, resolver)
|
||||
.into_iter()
|
||||
.map(IpCidr::new_host)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Derive a DNS resolver.
|
||||
///
|
||||
/// 1. if the `resolver` parameter has been set:
|
||||
/// 1. assume the parameter is a path and attempt to read IPs.
|
||||
/// 2. parse the input as a comma-separated list of IPs.
|
||||
/// 2. if `resolver` is not set:
|
||||
/// 1. attempt to derive a resolver from the system config. (e.g.
|
||||
/// `/etc/resolv.conf` on *nix).
|
||||
/// 2. finally, build a CloudFlare-based resolver (default
|
||||
/// behaviour).
|
||||
fn get_resolver(resolver: &Option<String>) -> Resolver {
|
||||
match resolver {
|
||||
Some(r) => {
|
||||
let mut config = ResolverConfig::new();
|
||||
let resolver_ips = match read_resolver_from_file(r) {
|
||||
Ok(ips) => ips,
|
||||
Err(_) => r
|
||||
.split(',')
|
||||
.filter_map(|r| IpAddr::from_str(r).ok())
|
||||
.collect::<Vec<_>>(),
|
||||
};
|
||||
for ip in resolver_ips {
|
||||
config.add_name_server(NameServerConfig::new(
|
||||
SocketAddr::new(ip, 53),
|
||||
Protocol::Udp,
|
||||
));
|
||||
}
|
||||
Resolver::new(config, resolver_opts()).unwrap()
|
||||
}
|
||||
None => match system_resolver() {
|
||||
Ok(resolver) => resolver,
|
||||
Err(_) => Resolver::new(ResolverConfig::cloudflare_tls(), resolver_opts()).unwrap(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// `true` on Windows when the `SystemRoot` environment variable is unset.
|
||||
///
|
||||
/// hickory-resolver locates the hosts file via
|
||||
/// `std::env::var_os("SystemRoot").expect(...)`, which panics when the
|
||||
/// variable is missing. Processes spawned with a minimal environment
|
||||
/// (services, scheduled tasks, WMI) can lack `SystemRoot`, and the panic is
|
||||
/// fatal under this crate's `panic = "abort"` release profile.
|
||||
fn windows_system_root_missing() -> bool {
|
||||
cfg!(windows) && std::env::var_os("SystemRoot").is_none()
|
||||
}
|
||||
|
||||
/// Derives a resolver from the system configuration, e.g. `/etc/resolv.conf`
|
||||
/// on *nix or the registry on Windows.
|
||||
///
|
||||
/// Returns an error without touching the system configuration when doing so
|
||||
/// would panic inside hickory-resolver (see [`windows_system_root_missing`]).
|
||||
fn system_resolver() -> std::io::Result<Resolver> {
|
||||
if windows_system_root_missing() {
|
||||
debug!("SystemRoot is not set; skipping system resolver configuration");
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
"SystemRoot environment variable is not set",
|
||||
));
|
||||
}
|
||||
|
||||
Resolver::from_system_conf()
|
||||
}
|
||||
|
||||
/// Resolver options that are safe to use in the current environment.
|
||||
///
|
||||
/// hickory-resolver eagerly loads the hosts file when `use_hosts_file` is set
|
||||
/// (the default), which hits the same missing-`SystemRoot` panic described in
|
||||
/// [`windows_system_root_missing`]; disable it in that case.
|
||||
fn resolver_opts() -> ResolverOpts {
|
||||
let mut opts = ResolverOpts::default();
|
||||
if windows_system_root_missing() {
|
||||
opts.use_hosts_file = false;
|
||||
}
|
||||
opts
|
||||
}
|
||||
|
||||
/// Parses and input file of IPs for use in DNS resolution.
|
||||
fn read_resolver_from_file(path: &str) -> Result<Vec<IpAddr>, std::io::Error> {
|
||||
let ips = fs::read_to_string(path)?
|
||||
.lines()
|
||||
.filter_map(|line| IpAddr::from_str(line.trim()).ok())
|
||||
.collect();
|
||||
|
||||
Ok(ips)
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
/// Parses an input file of IPs and uses those
|
||||
fn read_ips_from_file<'a>(
|
||||
ips: &std::path::Path,
|
||||
backup_resolver: &impl Fn() -> &'a Resolver,
|
||||
) -> Result<Vec<IpAddr>, std::io::Error> {
|
||||
let file = File::open(ips)?;
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
let mut ips: Vec<IpAddr> = Vec::new();
|
||||
|
||||
for address_line in reader.lines() {
|
||||
if let Ok(address) = address_line {
|
||||
ips.extend(parse_address_with_resolver(&address, backup_resolver));
|
||||
} else {
|
||||
debug!("Line in file is not valid");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ips)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{parse_addresses, parse_addresses_with_resolver, Opts};
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
|
||||
#[test]
|
||||
fn literal_targets_and_exclusions_do_not_initialize_a_resolver() {
|
||||
let opts = Opts {
|
||||
addresses: vec![
|
||||
"192.0.2.0/30".to_owned(),
|
||||
"192.0.2.2".to_owned(),
|
||||
"2001:db8::/126".to_owned(),
|
||||
"2001:db8::3".to_owned(),
|
||||
],
|
||||
exclude_addresses: Some(vec![
|
||||
"192.0.2.0/31".to_owned(),
|
||||
"192.0.2.3".to_owned(),
|
||||
"2001:db8::/127".to_owned(),
|
||||
"2001:db8::3".to_owned(),
|
||||
]),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let ips = parse_addresses_with_resolver(&opts, || {
|
||||
panic!("literal targets and exclusions must not initialize DNS")
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
ips,
|
||||
[
|
||||
"192.0.2.2".parse::<IpAddr>().unwrap(),
|
||||
"2001:db8::2".parse::<IpAddr>().unwrap(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_targets_do_not_initialize_a_resolver() {
|
||||
assert!(parse_addresses_with_resolver(&Opts::default(), || {
|
||||
panic!("empty targets must not initialize DNS")
|
||||
})
|
||||
.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_correct_addresses() {
|
||||
let opts = Opts {
|
||||
addresses: vec!["127.0.0.1".to_owned(), "192.168.0.0/30".to_owned()],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let ips = parse_addresses(&opts);
|
||||
|
||||
assert_eq!(
|
||||
ips,
|
||||
[
|
||||
Ipv4Addr::new(127, 0, 0, 1),
|
||||
Ipv4Addr::new(192, 168, 0, 0),
|
||||
Ipv4Addr::new(192, 168, 0, 1),
|
||||
Ipv4Addr::new(192, 168, 0, 2),
|
||||
Ipv4Addr::new(192, 168, 0, 3)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_addresses_with_address_exclusions() {
|
||||
let opts = Opts {
|
||||
addresses: vec!["192.168.0.0/30".to_owned()],
|
||||
exclude_addresses: Some(vec!["192.168.0.1".to_owned()]),
|
||||
..Default::default()
|
||||
};
|
||||
let ips = parse_addresses(&opts);
|
||||
|
||||
assert_eq!(
|
||||
ips,
|
||||
[
|
||||
Ipv4Addr::new(192, 168, 0, 0),
|
||||
Ipv4Addr::new(192, 168, 0, 2),
|
||||
Ipv4Addr::new(192, 168, 0, 3)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_addresses_with_cidr_exclusions() {
|
||||
let opts = Opts {
|
||||
addresses: vec!["192.168.0.0/29".to_owned()],
|
||||
exclude_addresses: Some(vec!["192.168.0.0/30".to_owned()]),
|
||||
..Default::default()
|
||||
};
|
||||
let ips = parse_addresses(&opts);
|
||||
|
||||
assert_eq!(
|
||||
ips,
|
||||
[
|
||||
Ipv4Addr::new(192, 168, 0, 4),
|
||||
Ipv4Addr::new(192, 168, 0, 5),
|
||||
Ipv4Addr::new(192, 168, 0, 6),
|
||||
Ipv4Addr::new(192, 168, 0, 7),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_addresses_with_incorrect_address_exclusions() {
|
||||
let opts = Opts {
|
||||
addresses: vec!["192.168.0.0/30".to_owned()],
|
||||
exclude_addresses: Some(vec!["192.168.0.1".to_owned()]),
|
||||
..Default::default()
|
||||
};
|
||||
let ips = parse_addresses(&opts);
|
||||
|
||||
assert_eq!(
|
||||
ips,
|
||||
[
|
||||
Ipv4Addr::new(192, 168, 0, 0),
|
||||
Ipv4Addr::new(192, 168, 0, 2),
|
||||
Ipv4Addr::new(192, 168, 0, 3)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_duplicate_cidrs() {
|
||||
let opts = Opts {
|
||||
addresses: vec!["79.98.104.0/21".to_owned(), "79.98.104.0/24".to_owned()],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let ips = parse_addresses(&opts);
|
||||
|
||||
assert_eq!(ips.len(), 2_048);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_overspecific_cidr() {
|
||||
// a canonical CIDR string has 0 in all host bits, but we want to treat any CIDR-like string as CIDR
|
||||
let opts = Opts {
|
||||
addresses: vec!["192.128.1.1/24".to_owned()],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let ips = parse_addresses(&opts);
|
||||
|
||||
assert_eq!(ips.len(), 256);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_non_canonical_cidr_mid_block() {
|
||||
// 192.168.1.13/29: .13 = 0000 1101, mask clears last 3 bits → .8 = 0000 1000
|
||||
// network is 192.168.1.8/29, spanning .8 through .15
|
||||
let opts = Opts {
|
||||
addresses: vec!["192.168.1.13/29".to_owned()],
|
||||
..Default::default()
|
||||
};
|
||||
let ips = parse_addresses(&opts);
|
||||
assert_eq!(
|
||||
ips,
|
||||
[
|
||||
Ipv4Addr::new(192, 168, 1, 8),
|
||||
Ipv4Addr::new(192, 168, 1, 9),
|
||||
Ipv4Addr::new(192, 168, 1, 10),
|
||||
Ipv4Addr::new(192, 168, 1, 11),
|
||||
Ipv4Addr::new(192, 168, 1, 12),
|
||||
Ipv4Addr::new(192, 168, 1, 13),
|
||||
Ipv4Addr::new(192, 168, 1, 14),
|
||||
Ipv4Addr::new(192, 168, 1, 15),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_non_canonical_cidr_last_in_block() {
|
||||
// 192.168.1.15/29: last address in the block, should still resolve to same .8–.15 network
|
||||
let opts = Opts {
|
||||
addresses: vec!["192.168.1.15/29".to_owned()],
|
||||
..Default::default()
|
||||
};
|
||||
let ips = parse_addresses(&opts);
|
||||
assert_eq!(
|
||||
ips,
|
||||
[
|
||||
Ipv4Addr::new(192, 168, 1, 8),
|
||||
Ipv4Addr::new(192, 168, 1, 9),
|
||||
Ipv4Addr::new(192, 168, 1, 10),
|
||||
Ipv4Addr::new(192, 168, 1, 11),
|
||||
Ipv4Addr::new(192, 168, 1, 12),
|
||||
Ipv4Addr::new(192, 168, 1, 13),
|
||||
Ipv4Addr::new(192, 168, 1, 14),
|
||||
Ipv4Addr::new(192, 168, 1, 15),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_non_canonical_cidr_crosses_third_octet() {
|
||||
// 192.168.1.5/23: host bits span into the third octet
|
||||
// .1.5 in 23-bit context → network is 192.168.0.0/23, spanning .0.0 through .1.255 (512 addresses)
|
||||
let opts = Opts {
|
||||
addresses: vec!["192.168.1.5/23".to_owned()],
|
||||
..Default::default()
|
||||
};
|
||||
let ips = parse_addresses(&opts);
|
||||
assert_eq!(
|
||||
ips.first(),
|
||||
Some(&IpAddr::V4(Ipv4Addr::new(192, 168, 0, 0)))
|
||||
);
|
||||
assert_eq!(
|
||||
ips.last(),
|
||||
Some(&IpAddr::V4(Ipv4Addr::new(192, 168, 1, 255)))
|
||||
);
|
||||
assert_eq!(ips.len(), 512);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_non_canonical_cidr_slash30() {
|
||||
// 10.0.0.7/30: .7 = 0000 0111, mask clears last 2 bits → .4 = 0000 0100
|
||||
// network is 10.0.0.4/30, spanning .4 through .7
|
||||
let opts = Opts {
|
||||
addresses: vec!["10.0.0.7/30".to_owned()],
|
||||
..Default::default()
|
||||
};
|
||||
let ips = parse_addresses(&opts);
|
||||
assert_eq!(
|
||||
ips,
|
||||
[
|
||||
Ipv4Addr::new(10, 0, 0, 4),
|
||||
Ipv4Addr::new(10, 0, 0, 5),
|
||||
Ipv4Addr::new(10, 0, 0, 6),
|
||||
Ipv4Addr::new(10, 0, 0, 7),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
//! Provides functionality to capture timing information for scans.
|
||||
//!
|
||||
//! # Usage
|
||||
//!
|
||||
//! ```rust
|
||||
//! // Initiate Benchmark vector
|
||||
//! # use rustscan::benchmark::{Benchmark, NamedTimer};
|
||||
//! # use log::info;
|
||||
//! let mut bm = Benchmark::init();
|
||||
//! // Start named timer with name
|
||||
//! let mut example_bench = NamedTimer::start("Example Bench");
|
||||
//! // Stop named timer
|
||||
//! example_bench.end();
|
||||
//! // Add named timer to Benchmarks
|
||||
//! bm.push(example_bench);
|
||||
//! // Print Benchmark Summary
|
||||
//! info!("{}", bm.summary());
|
||||
//! ```
|
||||
use std::time::Instant;
|
||||
|
||||
/// A Benchmark struct to hold NamedTimers with name, start and end Instants,
|
||||
#[derive(Debug)]
|
||||
pub struct Benchmark {
|
||||
named_timers: Vec<NamedTimer>,
|
||||
}
|
||||
|
||||
impl Benchmark {
|
||||
pub fn init() -> Self {
|
||||
Self {
|
||||
named_timers: Vec::new(),
|
||||
}
|
||||
}
|
||||
pub fn push(&mut self, timer: NamedTimer) {
|
||||
self.named_timers.push(timer);
|
||||
}
|
||||
|
||||
/// Summary of the benchmarks will destruct the vector,
|
||||
/// formats every element the same way and return
|
||||
/// a single String with all the available information
|
||||
/// for easy printing
|
||||
pub fn summary(&self) -> String {
|
||||
let mut summary = String::from("\nRustScan Benchmark Summary");
|
||||
|
||||
for timer in &self.named_timers {
|
||||
if let Some(start) = timer.start {
|
||||
if let Some(end) = timer.end {
|
||||
let runtime_secs = end.saturating_duration_since(start).as_secs_f32();
|
||||
summary.push_str(&format!("\n{0: <10} | {1: <10}s", timer.name, runtime_secs));
|
||||
}
|
||||
}
|
||||
}
|
||||
summary
|
||||
}
|
||||
}
|
||||
|
||||
/// The purpose of NamedTimer is to hold a name,
|
||||
/// start Instant and end Instant for a specific timer.
|
||||
/// The given name will be presented in the benchmark summary,
|
||||
/// start and end Instants will be used for calculating runtime.
|
||||
#[derive(Debug)]
|
||||
pub struct NamedTimer {
|
||||
name: &'static str,
|
||||
start: Option<Instant>,
|
||||
end: Option<Instant>,
|
||||
}
|
||||
|
||||
impl NamedTimer {
|
||||
pub fn start(name: &'static str) -> Self {
|
||||
Self {
|
||||
name,
|
||||
start: Some(Instant::now()),
|
||||
end: None,
|
||||
}
|
||||
}
|
||||
pub fn end(&mut self) {
|
||||
self.end = Some(Instant::now());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn benchmark() {
|
||||
let mut benchmarks = Benchmark::init();
|
||||
let mut test_timer = NamedTimer::start("test");
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
test_timer.end();
|
||||
benchmarks.push(test_timer);
|
||||
benchmarks.push(NamedTimer::start("only_start"));
|
||||
assert!(benchmarks
|
||||
.summary()
|
||||
.contains("\nRustScan Benchmark Summary\ntest | 0."));
|
||||
assert!(!benchmarks.summary().contains("only_start"));
|
||||
}
|
||||
@@ -0,0 +1,882 @@
|
||||
//! Provides a means to read, parse and hold configuration options for scans.
|
||||
use clap::parser::ValueSource;
|
||||
use clap::{CommandFactory, FromArgMatches, Parser, ValueEnum};
|
||||
use serde_derive::Deserialize;
|
||||
use std::collections::HashSet;
|
||||
use std::ffi::OsString;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
const LOWEST_PORT_NUMBER: u16 = 1;
|
||||
const TOP_PORT_NUMBER: u16 = 65535;
|
||||
|
||||
/// Represents the strategy in which the port scanning will run.
|
||||
/// - Serial will run from start to end, for example 1 to 1_000.
|
||||
/// - Random will randomize the order in which ports will be scanned.
|
||||
#[derive(Deserialize, Debug, ValueEnum, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ScanOrder {
|
||||
Serial,
|
||||
Random,
|
||||
}
|
||||
|
||||
/// Represents the scripts variant.
|
||||
/// - none will avoid running any script, only portscan results will be shown.
|
||||
/// - default will run the default embedded nmap script, that's part of RustScan since the beginning.
|
||||
/// - custom will read the ScriptConfig file and the available scripts in the predefined folders
|
||||
#[derive(Deserialize, Debug, ValueEnum, Clone, PartialEq, Eq, Copy)]
|
||||
pub enum ScriptsRequired {
|
||||
#[serde(alias = "none")]
|
||||
None,
|
||||
#[serde(alias = "default")]
|
||||
Default,
|
||||
#[serde(alias = "custom")]
|
||||
Custom,
|
||||
}
|
||||
|
||||
/// The port ranges to scan, as inclusive `(start, end)` pairs.
|
||||
///
|
||||
/// Parsed from `-r/--range` (e.g. `1-500,1000-2500`) and from the `range`
|
||||
/// key of the configuration file.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct PortRanges(pub Vec<(u16, u16)>);
|
||||
|
||||
/// Accepted spellings of `range` in the configuration file.
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum PortRangesConfig {
|
||||
/// `range = { start = 1, end = 1000 }`: the format used before multiple
|
||||
/// ranges were supported, kept so existing configuration files still work.
|
||||
Single { start: u16, end: u16 },
|
||||
/// `range = "1-500,1000-2500"`: the same syntax as `--range`.
|
||||
Text(String),
|
||||
/// `range = [[1, 500], [1000, 2500]]`.
|
||||
Pairs(Vec<(u16, u16)>),
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for PortRanges {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let pairs = match PortRangesConfig::deserialize(deserializer)? {
|
||||
PortRangesConfig::Single { start, end } => vec![(start, end)],
|
||||
PortRangesConfig::Text(text) => {
|
||||
return parse_ranges(&text).map_err(serde::de::Error::custom)
|
||||
}
|
||||
PortRangesConfig::Pairs(pairs) => pairs,
|
||||
};
|
||||
|
||||
if pairs.is_empty() {
|
||||
return Err(serde::de::Error::custom("expected at least one port range"));
|
||||
}
|
||||
if let Some(&(start, end)) = pairs.iter().find(|&&(start, end)| start > end) {
|
||||
return Err(serde::de::Error::custom(format!(
|
||||
"invalid port range {start}-{end}: start must not be greater than end"
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(PortRanges(pairs))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
/// Parse a single `start-end` token (e.g. "100-200") into `(start, end)`.
|
||||
/// Returns `None` when the token is malformed, cannot be parsed as `u16`,
|
||||
/// or `start > end`.
|
||||
fn parse_range(input: &str) -> Option<(u16, u16)> {
|
||||
let mut parts = input.trim().splitn(2, '-').map(str::trim);
|
||||
let a = parts.next()?;
|
||||
let b = parts.next()?;
|
||||
|
||||
let start = a.parse::<u16>().ok()?;
|
||||
let end = b.parse::<u16>().ok()?;
|
||||
|
||||
if start <= end {
|
||||
Some((start, end))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
/// Parse a comma-separated list of `start-end` ranges into `PortRanges`.
|
||||
///
|
||||
/// Errors with a helpful message identifying the bad token.
|
||||
fn parse_ranges(input: &str) -> Result<PortRanges, String> {
|
||||
let s = input.trim();
|
||||
if s.is_empty() {
|
||||
return Err("empty input: expected one or more comma-separated 'start-end' pairs".into());
|
||||
}
|
||||
|
||||
let ranges_res: Result<Vec<(u16, u16)>, String> = s
|
||||
.split(',')
|
||||
.map(|token| {
|
||||
let t = token.trim();
|
||||
parse_range(t).ok_or_else(|| {
|
||||
format!(
|
||||
"invalid range token `{}` — expected `start-end` with 0 <= start <= end <= 65535",
|
||||
t
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
ranges_res.map(PortRanges)
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
#[command(
|
||||
name = "rustscan",
|
||||
version = env!("CARGO_PKG_VERSION"),
|
||||
max_term_width = 120,
|
||||
help_template = "{bin} {version}\n{about}\n\nUSAGE:\n {usage}\n\nOPTIONS:\n{options}",
|
||||
)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
/// Fast Port Scanner built in Rust.
|
||||
/// WARNING Do not use this program against sensitive infrastructure since the
|
||||
/// specified server may not be able to handle this many socket connections at once.
|
||||
/// - Discord <http://discord.skerritt.blog>
|
||||
/// - GitHub <https://github.com/RustScan/RustScan>
|
||||
pub struct Opts {
|
||||
/// A comma-delimited list or newline-delimited file of separated CIDRs, IPs, or hosts to be scanned.
|
||||
#[arg(short, long, value_delimiter = ',')]
|
||||
pub addresses: Vec<String>,
|
||||
|
||||
/// A list of comma separated ports to be scanned. Example: 80,443,8080.
|
||||
#[arg(short, long, value_delimiter = ',')]
|
||||
pub ports: Option<Vec<u16>>,
|
||||
|
||||
/// Ranges of ports as comma-separated start-end pairs. Example: 1-500,1000-2500,4000-7000
|
||||
#[arg(short, long, conflicts_with = "ports", value_parser = parse_ranges)]
|
||||
pub range: Option<PortRanges>,
|
||||
|
||||
/// Whether to ignore the configuration file or not.
|
||||
#[arg(short, long)]
|
||||
pub no_config: bool,
|
||||
|
||||
/// Hide the banner
|
||||
#[arg(long)]
|
||||
pub no_banner: bool,
|
||||
|
||||
/// Custom path to config file
|
||||
#[arg(short, long, value_parser)]
|
||||
pub config_path: Option<PathBuf>,
|
||||
|
||||
/// Greppable mode. Only output the ports. No Nmap. Useful for grep or outputting to a file.
|
||||
#[arg(short, long)]
|
||||
pub greppable: bool,
|
||||
|
||||
/// Accessible mode. Turns off features which negatively affect screen readers.
|
||||
#[arg(long)]
|
||||
pub accessible: bool,
|
||||
|
||||
/// A comma-delimited list or file of DNS resolvers.
|
||||
#[arg(long)]
|
||||
pub resolver: Option<String>,
|
||||
|
||||
/// The batch size for port scanning, it increases or slows the speed of
|
||||
/// scanning. Depends on the open file limit of your OS. If you do 65535
|
||||
/// it will do every port at the same time. Although, your OS may not
|
||||
/// support this.
|
||||
#[arg(short, long, default_value = "4500")]
|
||||
pub batch_size: usize,
|
||||
|
||||
/// The timeout in milliseconds before a port is assumed to be closed.
|
||||
#[arg(short, long, default_value = "1500")]
|
||||
pub timeout: u32,
|
||||
|
||||
/// The number of tries before a port is assumed to be closed.
|
||||
/// If set to 0, rustscan will correct it to 1.
|
||||
#[arg(long, default_value = "1")]
|
||||
pub tries: u8,
|
||||
|
||||
/// Automatically increases the Unix file-descriptor limit.
|
||||
#[cfg_attr(not(unix), arg(hide = true))]
|
||||
#[arg(short, long)]
|
||||
pub ulimit: Option<usize>,
|
||||
|
||||
/// The order of scanning to be performed. The "serial" option will
|
||||
/// scan ports in ascending order while the "random" option will scan
|
||||
/// ports randomly.
|
||||
#[arg(long, value_enum, ignore_case = true, default_value = "serial")]
|
||||
pub scan_order: ScanOrder,
|
||||
|
||||
/// Level of scripting required for the run.
|
||||
#[arg(long, value_enum, ignore_case = true, default_value = "default")]
|
||||
pub scripts: ScriptsRequired,
|
||||
|
||||
/// Use the top 1000 ports.
|
||||
#[arg(long)]
|
||||
pub top: bool,
|
||||
|
||||
/// The Script arguments to run.
|
||||
/// To use the argument -A, end RustScan's args with '-- -A'.
|
||||
/// Example: 'rustscan -t 1500 -a 127.0.0.1 -- -A -sC'.
|
||||
/// This command adds -Pn -vvv -p $PORTS automatically to nmap.
|
||||
/// For things like --script '(safe and vuln)' enclose it in quotations marks \"'(safe and vuln)'\"
|
||||
#[arg(last = true)]
|
||||
pub command: Vec<String>,
|
||||
|
||||
/// A list of comma separated ports to be excluded from scanning. Example: 80,443,8080.
|
||||
#[arg(short, long, value_delimiter = ',')]
|
||||
pub exclude_ports: Option<Vec<u16>>,
|
||||
|
||||
/// A list of comma separated CIDRs, IPs, or hosts to be excluded from scanning.
|
||||
#[arg(short = 'x', long = "exclude-addresses", value_delimiter = ',')]
|
||||
pub exclude_addresses: Option<Vec<String>>,
|
||||
|
||||
/// UDP scanning mode, finds UDP ports that send back responses
|
||||
#[arg(long)]
|
||||
pub udp: bool,
|
||||
|
||||
/// Also list TCP ports that actively refused the connection (closed).
|
||||
/// Closed ports are only printed; scripts are never run against them.
|
||||
#[arg(long)]
|
||||
pub closed: bool,
|
||||
|
||||
/// Milliseconds to wait after scanning a port (on every address) before
|
||||
/// scanning the next one, for slow, low-noise scans. 0 disables the delay.
|
||||
#[arg(long, default_value = "0", value_name = "MS")]
|
||||
pub interval: u64,
|
||||
|
||||
/// Ids of the options given explicitly on the command line. The
|
||||
/// configuration file never overrides these (see [`Opts::merge`]).
|
||||
#[arg(skip)]
|
||||
pub cli_args: HashSet<String>,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl Opts {
|
||||
pub fn read() -> Self {
|
||||
Self::try_read_from(std::env::args_os()).unwrap_or_else(|e| e.exit())
|
||||
}
|
||||
|
||||
/// Parses `args` like [`Opts::read`] does, returning an error instead of
|
||||
/// exiting.
|
||||
fn try_read_from<I, T>(args: I) -> Result<Self, clap::Error>
|
||||
where
|
||||
I: IntoIterator<Item = T>,
|
||||
T: Into<OsString> + Clone,
|
||||
{
|
||||
let mut command = Self::command();
|
||||
let mut matches = command.try_get_matches_from_mut(args)?;
|
||||
|
||||
// clap fills in default values, so the parsed struct alone cannot
|
||||
// tell `-b 4500` apart from no `-b` at all. Record what the user
|
||||
// actually typed so that `merge` lets it win over the config file.
|
||||
let cli_args = command
|
||||
.get_arguments()
|
||||
.map(|arg| arg.get_id().as_str())
|
||||
.filter(|id| matches.value_source(id) == Some(ValueSource::CommandLine))
|
||||
.map(str::to_owned)
|
||||
.collect();
|
||||
|
||||
let mut opts =
|
||||
Self::from_arg_matches_mut(&mut matches).map_err(|e| e.format(&mut command))?;
|
||||
opts.cli_args = cli_args;
|
||||
|
||||
if opts.ports.is_none() && opts.range.is_none() {
|
||||
opts.range = Some(PortRanges(vec![(LOWEST_PORT_NUMBER, TOP_PORT_NUMBER)]));
|
||||
}
|
||||
|
||||
Ok(opts)
|
||||
}
|
||||
|
||||
/// Validates options whose availability or semantics depend on the
|
||||
/// operating system.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error when an option is unsupported on the current platform.
|
||||
pub fn validate_platform(&self) -> Result<(), String> {
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
if self.ulimit.is_some() {
|
||||
return Err(
|
||||
"--ulimit is only supported on Unix-like operating systems. \
|
||||
On Windows, use --batch-size (-b) to control scan concurrency."
|
||||
.to_owned(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reads the command line arguments into an Opts struct and merge
|
||||
/// values found within the user configuration file. Options given on the
|
||||
/// command line take precedence over the configuration file.
|
||||
pub fn merge(&mut self, config: &Config) {
|
||||
if !self.no_config {
|
||||
self.merge_required(config);
|
||||
self.merge_optional(config);
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_required(&mut self, config: &Config) {
|
||||
macro_rules! merge_required {
|
||||
($($field: ident),+) => {
|
||||
$(
|
||||
if let Some(e) = &config.$field {
|
||||
if !self.cli_args.contains(stringify!($field)) {
|
||||
self.$field = e.clone();
|
||||
}
|
||||
}
|
||||
)+
|
||||
}
|
||||
}
|
||||
|
||||
merge_required!(
|
||||
addresses, greppable, accessible, batch_size, timeout, tries, scan_order, scripts,
|
||||
command, udp, no_banner, closed, interval
|
||||
);
|
||||
}
|
||||
|
||||
fn merge_optional(&mut self, config: &Config) {
|
||||
macro_rules! merge_optional {
|
||||
($($field: ident),+) => {
|
||||
$(
|
||||
if config.$field.is_some() && !self.cli_args.contains(stringify!($field)) {
|
||||
self.$field = config.$field.clone();
|
||||
}
|
||||
)+
|
||||
}
|
||||
}
|
||||
|
||||
// Only use top ports when the user asks for them
|
||||
if self.top && config.ports.is_some() {
|
||||
self.ports = config.ports.clone();
|
||||
}
|
||||
|
||||
merge_optional!(range, resolver, ulimit, exclude_ports, exclude_addresses);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Opts {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
addresses: vec![],
|
||||
ports: None,
|
||||
range: None,
|
||||
greppable: true,
|
||||
batch_size: 0,
|
||||
timeout: 0,
|
||||
tries: 0,
|
||||
ulimit: None,
|
||||
command: vec![],
|
||||
accessible: false,
|
||||
resolver: None,
|
||||
scan_order: ScanOrder::Serial,
|
||||
no_config: true,
|
||||
no_banner: false,
|
||||
top: false,
|
||||
scripts: ScriptsRequired::Default,
|
||||
config_path: None,
|
||||
exclude_ports: None,
|
||||
exclude_addresses: None,
|
||||
udp: false,
|
||||
closed: false,
|
||||
interval: 0,
|
||||
cli_args: HashSet::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Struct used to deserialize the options specified within our config file.
|
||||
/// These will be further merged with our command line arguments in order to
|
||||
/// generate the final Opts struct.
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Config {
|
||||
addresses: Option<Vec<String>>,
|
||||
ports: Option<Vec<u16>>,
|
||||
range: Option<PortRanges>,
|
||||
greppable: Option<bool>,
|
||||
accessible: Option<bool>,
|
||||
batch_size: Option<usize>,
|
||||
timeout: Option<u32>,
|
||||
tries: Option<u8>,
|
||||
ulimit: Option<usize>,
|
||||
resolver: Option<String>,
|
||||
scan_order: Option<ScanOrder>,
|
||||
command: Option<Vec<String>>,
|
||||
scripts: Option<ScriptsRequired>,
|
||||
exclude_ports: Option<Vec<u16>>,
|
||||
exclude_addresses: Option<Vec<String>>,
|
||||
udp: Option<bool>,
|
||||
no_banner: Option<bool>,
|
||||
closed: Option<bool>,
|
||||
interval: Option<u64>,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
#[allow(clippy::doc_link_with_quotes)]
|
||||
#[allow(clippy::manual_unwrap_or_default)]
|
||||
impl Config {
|
||||
/// Reads the configuration file with TOML format and parses it into a
|
||||
/// Config struct.
|
||||
///
|
||||
/// # Format
|
||||
///
|
||||
/// addresses = ["127.0.0.1", "127.0.0.1"]
|
||||
/// ports = [80, 443, 8080]
|
||||
/// greppable = true
|
||||
/// scan_order = "Serial"
|
||||
/// exclude_ports = [8080, 9090, 80]
|
||||
/// udp = false
|
||||
/// closed = false
|
||||
/// interval = 0
|
||||
///
|
||||
pub fn read(custom_config_path: Option<PathBuf>) -> Self {
|
||||
let mut content = String::new();
|
||||
let config_path = custom_config_path.unwrap_or_else(|| {
|
||||
// Try the XDG-idiomatic location first, then fall back to legacy
|
||||
// paths so existing users keep working unchanged.
|
||||
for path in [
|
||||
default_config_path(),
|
||||
legacy_dot_config_path(),
|
||||
old_default_config_path(),
|
||||
] {
|
||||
if path.exists() {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
default_config_path()
|
||||
});
|
||||
|
||||
if config_path.exists() {
|
||||
content = match fs::read_to_string(config_path) {
|
||||
Ok(content) => content,
|
||||
Err(_) => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
let config: Config = match toml::from_str(&content) {
|
||||
Ok(config) => config,
|
||||
Err(e) => {
|
||||
crate::tui::println_safe(format_args!(
|
||||
"Found {e} in configuration file.\nAborting scan.\n"
|
||||
));
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
config
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the preferred config file path: `$XDG_CONFIG_HOME/rustscan/config.toml`
|
||||
/// on Linux (with the usual `~/.config` fallback when the variable is unset),
|
||||
/// and the platform-equivalent `dirs::config_dir()` location on macOS / Windows.
|
||||
pub fn default_config_path() -> PathBuf {
|
||||
let Some(mut config_path) = dirs::config_dir() else {
|
||||
panic!("Could not infer config file path.");
|
||||
};
|
||||
config_path.push("rustscan");
|
||||
config_path.push("config.toml");
|
||||
config_path
|
||||
}
|
||||
|
||||
/// Returns the transitional `$XDG_CONFIG_HOME/.rustscan.toml` path that older
|
||||
/// builds wrote to. Kept readable for backwards compatibility.
|
||||
pub fn legacy_dot_config_path() -> PathBuf {
|
||||
let Some(mut config_path) = dirs::config_dir() else {
|
||||
panic!("Could not infer config file path.");
|
||||
};
|
||||
config_path.push(".rustscan.toml");
|
||||
config_path
|
||||
}
|
||||
|
||||
/// Returns the deprecated home directory config path used for backwards compatibility.
|
||||
pub fn old_default_config_path() -> PathBuf {
|
||||
let Some(mut config_path) = dirs::home_dir() else {
|
||||
panic!("Could not infer config file path.");
|
||||
};
|
||||
config_path.push(".rustscan.toml");
|
||||
config_path
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use clap::{CommandFactory, Parser};
|
||||
use parameterized::parameterized;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use super::{Config, Opts, PortRanges, ScanOrder, ScriptsRequired};
|
||||
|
||||
impl Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
addresses: Some(vec!["127.0.0.1".to_owned()]),
|
||||
ports: None,
|
||||
range: None,
|
||||
greppable: Some(true),
|
||||
batch_size: Some(25_000),
|
||||
timeout: Some(1_000),
|
||||
tries: Some(1),
|
||||
ulimit: None,
|
||||
command: Some(vec!["-A".to_owned()]),
|
||||
accessible: Some(true),
|
||||
resolver: None,
|
||||
scan_order: Some(ScanOrder::Random),
|
||||
scripts: None,
|
||||
exclude_ports: None,
|
||||
exclude_addresses: None,
|
||||
udp: Some(false),
|
||||
no_banner: None,
|
||||
closed: Some(false),
|
||||
interval: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_config_script_mode(value: &str, expected: ScriptsRequired) {
|
||||
let config: Config = toml::from_str(&format!("scripts = \"{value}\""))
|
||||
.expect("documented script mode must deserialize");
|
||||
assert_eq!(config.scripts, Some(expected));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_scripts_lowercase_none() {
|
||||
assert_config_script_mode("none", ScriptsRequired::None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_scripts_lowercase_default() {
|
||||
assert_config_script_mode("default", ScriptsRequired::Default);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_scripts_lowercase_custom() {
|
||||
assert_config_script_mode("custom", ScriptsRequired::Custom);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_scripts_legacy_and_unknown_controls() {
|
||||
assert_config_script_mode("None", ScriptsRequired::None);
|
||||
assert_config_script_mode("Default", ScriptsRequired::Default);
|
||||
assert_config_script_mode("Custom", ScriptsRequired::Custom);
|
||||
for invalid in ["disabled", "unknown", ""] {
|
||||
assert!(toml::from_str::<Config>(&format!("scripts = \"{invalid}\"")).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_read_accepts_documented_lowercase_script_modes() {
|
||||
for (value, expected) in [
|
||||
("none", ScriptsRequired::None),
|
||||
("default", ScriptsRequired::Default),
|
||||
("custom", ScriptsRequired::Custom),
|
||||
] {
|
||||
let fixture = std::env::temp_dir().join(format!(
|
||||
"rustscan-config-scripts-{}-{value}.toml",
|
||||
std::process::id()
|
||||
));
|
||||
std::fs::write(&fixture, format!("scripts = \"{value}\"\n")).unwrap();
|
||||
let config = Config::read(Some(fixture.clone()));
|
||||
std::fs::remove_file(fixture).unwrap();
|
||||
assert_eq!(config.scripts, Some(expected));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_cli() {
|
||||
Opts::command().debug_assert();
|
||||
}
|
||||
|
||||
#[parameterized(input = {
|
||||
vec!["rustscan", "--addresses", "127.0.0.1"],
|
||||
vec!["rustscan", "--addresses", "127.0.0.1", "--", "-sCV"],
|
||||
vec!["rustscan", "--addresses", "127.0.0.1", "--", "-A"],
|
||||
vec!["rustscan", "-t", "1500", "-a", "127.0.0.1", "--", "-A", "-sC"],
|
||||
vec!["rustscan", "--addresses", "127.0.0.1", "--", "--script", r#""'(safe and vuln)'""#],
|
||||
}, command = {
|
||||
vec![],
|
||||
vec!["-sCV".to_owned()],
|
||||
vec!["-A".to_owned()],
|
||||
vec!["-A".to_owned(), "-sC".to_owned()],
|
||||
vec!["--script".to_owned(), "\"'(safe and vuln)'\"".to_owned()],
|
||||
})]
|
||||
fn parse_trailing_command(input: Vec<&str>, command: Vec<String>) {
|
||||
let opts = Opts::parse_from(input);
|
||||
|
||||
assert_eq!(vec!["127.0.0.1".to_owned()], opts.addresses);
|
||||
assert_eq!(command, opts.command);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_explicit_batch_size() {
|
||||
let opts = Opts::parse_from(["rustscan", "-a", "127.0.0.1", "-b", "1234"]);
|
||||
|
||||
assert_eq!(opts.batch_size, 1234);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_explicit_long_batch_size() {
|
||||
let opts = Opts::parse_from(["rustscan", "-a", "127.0.0.1", "--batch-size", "4321"]);
|
||||
|
||||
assert_eq!(opts.batch_size, 4321);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
fn windows_platform_validation_accepts_batch_size() {
|
||||
let opts = Opts::parse_from(["rustscan", "-a", "127.0.0.1", "--batch-size", "500"]);
|
||||
|
||||
assert!(opts.validate_platform().is_ok());
|
||||
assert_eq!(opts.batch_size, 500);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
fn windows_rejects_ulimit() {
|
||||
let opts = Opts::parse_from(["rustscan", "-a", "127.0.0.1", "--ulimit", "5000"]);
|
||||
|
||||
let error = opts
|
||||
.validate_platform()
|
||||
.expect_err("Windows must reject --ulimit");
|
||||
|
||||
assert!(error.contains("--ulimit"));
|
||||
assert!(error.contains("Unix"));
|
||||
assert!(error.contains("--batch-size"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
fn windows_hides_ulimit_from_help() {
|
||||
let help = Opts::command().render_long_help().to_string();
|
||||
|
||||
assert!(
|
||||
!help.contains("--ulimit"),
|
||||
"--ulimit should not be advertised on Windows"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
fn windows_rejects_ulimit_from_config_merge() {
|
||||
let mut opts = Opts {
|
||||
no_config: false,
|
||||
..Default::default()
|
||||
};
|
||||
let mut config = Config::default();
|
||||
config.ulimit = Some(5_000);
|
||||
|
||||
opts.merge(&config);
|
||||
|
||||
let error = opts
|
||||
.validate_platform()
|
||||
.expect_err("Windows must reject --ulimit supplied by configuration");
|
||||
|
||||
assert!(error.contains("--ulimit"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn unix_platform_validation_accepts_ulimit() {
|
||||
let opts = Opts::parse_from(["rustscan", "-a", "127.0.0.1", "--ulimit", "5000"]);
|
||||
|
||||
assert!(opts.validate_platform().is_ok());
|
||||
assert_eq!(opts.ulimit, Some(5_000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opts_no_merge_when_config_is_ignored() {
|
||||
let mut opts = Opts::default();
|
||||
let config = Config::default();
|
||||
|
||||
opts.merge(&config);
|
||||
|
||||
assert_eq!(opts.addresses, vec![] as Vec<String>);
|
||||
assert!(opts.greppable);
|
||||
assert!(!opts.accessible);
|
||||
assert_eq!(opts.timeout, 0);
|
||||
assert_eq!(opts.command, vec![] as Vec<String>);
|
||||
assert_eq!(opts.scan_order, ScanOrder::Serial);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opts_merge_required_arguments() {
|
||||
let mut opts = Opts::default();
|
||||
let config = Config::default();
|
||||
|
||||
opts.merge_required(&config);
|
||||
|
||||
assert_eq!(opts.addresses, config.addresses.unwrap());
|
||||
assert_eq!(opts.greppable, config.greppable.unwrap());
|
||||
assert_eq!(opts.timeout, config.timeout.unwrap());
|
||||
assert_eq!(opts.command, config.command.unwrap());
|
||||
assert_eq!(opts.accessible, config.accessible.unwrap());
|
||||
assert_eq!(opts.scan_order, config.scan_order.unwrap());
|
||||
assert_eq!(opts.scripts, ScriptsRequired::Default);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opts_merge_optional_arguments() {
|
||||
let mut opts = Opts::default();
|
||||
let mut config = Config::default();
|
||||
config.range = Some(PortRanges(vec![(1, 1_000)]));
|
||||
config.ulimit = Some(1_000);
|
||||
config.resolver = Some("1.1.1.1".to_owned());
|
||||
|
||||
opts.merge_optional(&config);
|
||||
|
||||
assert_eq!(opts.range, config.range);
|
||||
assert_eq!(opts.ulimit, config.ulimit);
|
||||
assert_eq!(opts.resolver, config.resolver);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_arguments_override_config() {
|
||||
// Regression test for #722: the config file used to silently replace
|
||||
// options given on the command line, e.g. `-r` with its `range`.
|
||||
let mut opts = Opts::try_read_from([
|
||||
"rustscan",
|
||||
"-a",
|
||||
"192.168.0.1",
|
||||
"-r",
|
||||
"8600-8650",
|
||||
"-b",
|
||||
"100",
|
||||
// Passing clap's default value explicitly must still win.
|
||||
"-t",
|
||||
"1500",
|
||||
"--tries",
|
||||
"3",
|
||||
"--scan-order",
|
||||
"serial",
|
||||
"--scripts",
|
||||
"none",
|
||||
"-g",
|
||||
"--",
|
||||
"-sV",
|
||||
])
|
||||
.unwrap();
|
||||
let config: Config = toml::from_str(
|
||||
r#"
|
||||
addresses = ["127.0.0.1"]
|
||||
range = { start = 1, end = 100 }
|
||||
batch_size = 25000
|
||||
timeout = 1000
|
||||
tries = 1
|
||||
scan_order = "Random"
|
||||
scripts = "Custom"
|
||||
greppable = false
|
||||
command = ["-A"]
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
opts.merge(&config);
|
||||
|
||||
assert_eq!(opts.addresses, vec!["192.168.0.1".to_owned()]);
|
||||
assert_eq!(opts.range, Some(PortRanges(vec![(8_600, 8_650)])));
|
||||
assert_eq!(opts.batch_size, 100);
|
||||
assert_eq!(opts.timeout, 1_500);
|
||||
assert_eq!(opts.tries, 3);
|
||||
assert_eq!(opts.scan_order, ScanOrder::Serial);
|
||||
assert_eq!(opts.scripts, ScriptsRequired::None);
|
||||
assert!(opts.greppable);
|
||||
assert_eq!(opts.command, vec!["-sV".to_owned()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_fills_arguments_not_given_on_cli() {
|
||||
let mut opts = Opts::try_read_from(["rustscan", "-a", "192.168.0.1"]).unwrap();
|
||||
let config: Config = toml::from_str(
|
||||
r#"
|
||||
addresses = ["127.0.0.1"]
|
||||
range = { start = 1, end = 100 }
|
||||
batch_size = 25000
|
||||
timeout = 1000
|
||||
tries = 3
|
||||
scan_order = "Random"
|
||||
greppable = true
|
||||
command = ["-A"]
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
opts.merge(&config);
|
||||
|
||||
// Only `-a` was typed; clap defaults such as `-b 4500` must not count.
|
||||
assert_eq!(opts.cli_args, HashSet::from(["addresses".to_owned()]));
|
||||
assert_eq!(opts.addresses, vec!["192.168.0.1".to_owned()]);
|
||||
assert_eq!(opts.range, Some(PortRanges(vec![(1, 100)])));
|
||||
assert_eq!(opts.batch_size, 25_000);
|
||||
assert_eq!(opts.timeout, 1_000);
|
||||
assert_eq!(opts.tries, 3);
|
||||
assert_eq!(opts.scan_order, ScanOrder::Random);
|
||||
assert!(opts.greppable);
|
||||
assert_eq!(opts.command, vec!["-A".to_owned()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_comma_separated_ranges() {
|
||||
let opts = Opts::parse_from([
|
||||
"rustscan",
|
||||
"-a",
|
||||
"127.0.0.1",
|
||||
"-r",
|
||||
"1-100, 200-300,5000-5100",
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
opts.range,
|
||||
Some(PortRanges(vec![(1, 100), (200, 300), (5_000, 5_100)]))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_malformed_ranges() {
|
||||
for range in ["", "300-200", "1-100,", "1-2-3", "a-b", "1-70000"] {
|
||||
assert!(
|
||||
Opts::try_parse_from(["rustscan", "-a", "127.0.0.1", "-r", range]).is_err(),
|
||||
"{:?} should be rejected",
|
||||
range
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_range_accepts_legacy_table() {
|
||||
let config: Config = toml::from_str("range = { start = 1, end = 1000 }").unwrap();
|
||||
|
||||
assert_eq!(config.range, Some(PortRanges(vec![(1, 1_000)])));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_range_accepts_string_and_pairs() {
|
||||
let expected = Some(PortRanges(vec![(1, 100), (200, 300)]));
|
||||
|
||||
let config: Config = toml::from_str("range = \"1-100,200-300\"").unwrap();
|
||||
assert_eq!(config.range, expected);
|
||||
|
||||
let config: Config = toml::from_str("range = [[1, 100], [200, 300]]").unwrap();
|
||||
assert_eq!(config.range, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_range_rejects_invalid_ranges() {
|
||||
for range in [
|
||||
"range = { start = 10, end = 1 }",
|
||||
"range = [[10, 1]]",
|
||||
"range = []",
|
||||
"range = \"10-1\"",
|
||||
] {
|
||||
assert!(
|
||||
toml::from_str::<Config>(range).is_err(),
|
||||
"{:?} should be rejected",
|
||||
range
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_interval_in_milliseconds() {
|
||||
let opts = Opts::parse_from(["rustscan", "-a", "127.0.0.1", "--interval", "250"]);
|
||||
assert_eq!(opts.interval, 250);
|
||||
|
||||
let opts = Opts::parse_from(["rustscan", "-a", "127.0.0.1"]);
|
||||
assert_eq!(opts.interval, 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
//! This crate exposes the internal functionality of the
|
||||
//! [RustScan](https://rustscan.github.io/RustScan) port scanner.
|
||||
//!
|
||||
//! ## Example: perform a scan against localhost
|
||||
//!
|
||||
//! The core scanning behaviour is managed by
|
||||
//! [`Scanner`](crate::scanner::Scanner) which in turn requires a
|
||||
//! [`PortStrategy`](crate::port_strategy::PortStrategy).
|
||||
//!
|
||||
//! The scanner is asynchronous and runs on [Tokio](https://docs.rs/tokio):
|
||||
//! await [`Scanner::run`](crate::scanner::Scanner::run) from a Tokio runtime
|
||||
//! with the I/O and time drivers enabled. It never spawns tasks, so a
|
||||
//! current-thread runtime is enough (see [`Scanner`](crate::scanner::Scanner)
|
||||
//! for details).
|
||||
//!
|
||||
//! This example is compiled but never executed by the test suite.
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use std::{net::IpAddr, time::Duration};
|
||||
//!
|
||||
//! use rustscan::input::{PortRanges, ScanOrder};
|
||||
//! use rustscan::port_strategy::PortStrategy;
|
||||
//! use rustscan::scanner::Scanner;
|
||||
//!
|
||||
//! fn main() {
|
||||
//! let addrs = vec!["127.0.0.1".parse::<IpAddr>().unwrap()];
|
||||
//! let range = PortRanges(vec![(1, 1_000)]);
|
||||
//! let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random); // can be serial, random or manual https://github.com/RustScan/RustScan/blob/master/src/port_strategy/mod.rs
|
||||
//! let scanner = Scanner::new(
|
||||
//! &addrs, // the addresses to scan
|
||||
//! 10, // batch_size is how many ports at a time should be scanned
|
||||
//! Duration::from_millis(100), //T imeout is the time RustScan should wait before declaring a port closed. As datatype Duration.
|
||||
//! 1, // Tries, how many retries should RustScan do?
|
||||
//! true, // greppable is whether or not RustScan should print things, or wait until the end to print only the ip
|
||||
//! strategy, // the port strategy used
|
||||
//! true, // accessible, should the output be A11Y compliant?
|
||||
//! vec![9000], // What ports should RustScan exclude?
|
||||
//! false, // is this a UDP scan?
|
||||
//! );
|
||||
//!
|
||||
//! // Inside an async context (e.g. `#[tokio::main]`) this is simply
|
||||
//! // `scanner.run().await`.
|
||||
//! let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
//! .enable_all()
|
||||
//! .build()
|
||||
//! .unwrap();
|
||||
//! let scan_result = runtime.block_on(scanner.run());
|
||||
//!
|
||||
//! println!("{:?}", scan_result);
|
||||
//! }
|
||||
//! ```
|
||||
#![allow(clippy::needless_doctest_main)]
|
||||
|
||||
pub mod tui;
|
||||
|
||||
pub mod input;
|
||||
|
||||
pub mod scanner;
|
||||
|
||||
pub mod port_strategy;
|
||||
|
||||
pub mod benchmark;
|
||||
|
||||
pub mod scripts;
|
||||
|
||||
pub mod address;
|
||||
|
||||
pub mod generated;
|
||||
@@ -1,386 +1,538 @@
|
||||
extern crate shell_words;
|
||||
#![deny(clippy::all)]
|
||||
#![warn(clippy::pedantic)]
|
||||
#![allow(clippy::doc_markdown, clippy::if_not_else, clippy::non_ascii_literal)]
|
||||
|
||||
mod tui;
|
||||
use rustscan::benchmark::{Benchmark, NamedTimer};
|
||||
use rustscan::input::{self, Config, Opts, ScriptsRequired};
|
||||
use rustscan::port_strategy::PortStrategy;
|
||||
use rustscan::scanner::{PortStatus, Scanner};
|
||||
use rustscan::scripts::{init_scripts, Script, ScriptFile};
|
||||
use rustscan::tui::println_safe;
|
||||
use rustscan::{detail, funny_opening, output, warning};
|
||||
|
||||
mod scanner;
|
||||
use scanner::Scanner;
|
||||
|
||||
mod port_strategy;
|
||||
use port_strategy::PortStrategy;
|
||||
|
||||
use colorful::Color;
|
||||
use colorful::Colorful;
|
||||
use futures::executor::block_on;
|
||||
use rlimit::Resource;
|
||||
use rlimit::{getrlimit, setrlimit};
|
||||
use colorful::{Color, Colorful};
|
||||
use std::collections::HashMap;
|
||||
use std::process::Command;
|
||||
use std::{net::IpAddr, time::Duration};
|
||||
use structopt::{clap::arg_enum, StructOpt};
|
||||
use std::net::IpAddr;
|
||||
use std::string::ToString;
|
||||
use std::time::Duration;
|
||||
|
||||
use rustscan::address::parse_addresses;
|
||||
|
||||
extern crate colorful;
|
||||
extern crate dirs;
|
||||
|
||||
const LOWEST_PORT_NUMBER: u16 = 1;
|
||||
const TOP_PORT_NUMBER: u16 = 65535;
|
||||
// Average value for Ubuntu
|
||||
const DEFAULT_FILE_DESCRIPTORS_LIMIT: rlimit::rlim = 8000;
|
||||
#[cfg(unix)]
|
||||
const DEFAULT_FILE_DESCRIPTORS_LIMIT: usize = 8000;
|
||||
// Safest batch size based on experimentation
|
||||
const AVERAGE_BATCH_SIZE: rlimit::rlim = 3000;
|
||||
#[cfg(unix)]
|
||||
const AVERAGE_BATCH_SIZE: usize = 3000;
|
||||
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
arg_enum! {
|
||||
#[derive(Debug, StructOpt)]
|
||||
pub enum ScanOrder {
|
||||
Serial,
|
||||
Random,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(StructOpt, Debug)]
|
||||
#[structopt(name = "rustscan", setting = structopt::clap::AppSettings::TrailingVarArg)]
|
||||
/// Fast Port Scanner built in Rust.
|
||||
/// WARNING Do not use this program against sensitive infrastructure since the
|
||||
/// specified server may not be able to handle this many socket connections at once.
|
||||
/// - Discord https://discord.gg/GFrQsGy
|
||||
/// - GitHub https://github.com/RustScan/RustScan
|
||||
struct Opts {
|
||||
/// A list of comma separated IP addresses to be scanned.
|
||||
#[structopt(use_delimiter = true, parse(try_from_str), required = true)]
|
||||
ips: Vec<IpAddr>,
|
||||
|
||||
///Quiet mode. Only output the ports. No Nmap. Useful for grep or outputting to a file.
|
||||
#[structopt(short, long)]
|
||||
quiet: bool,
|
||||
|
||||
//Accessible mode. Turns off features which negatively affect screen readers.
|
||||
#[structopt(short, long)]
|
||||
accessible: bool,
|
||||
|
||||
/// The batch size for port scanning, it increases or slows the speed of
|
||||
/// scanning. Depends on the open file limit of your OS. If you do 65535
|
||||
/// it will do every port at the same time. Although, your OS may not
|
||||
/// support this.
|
||||
#[structopt(short, long, default_value = "4500")]
|
||||
batch_size: u16,
|
||||
|
||||
/// The timeout in milliseconds before a port is assumed to be closed.
|
||||
#[structopt(short, long, default_value = "1500")]
|
||||
timeout: u32,
|
||||
|
||||
/// Automatically ups the ULIMIT with the value you provided.
|
||||
#[structopt(short, long)]
|
||||
ulimit: Option<rlimit::rlim>,
|
||||
|
||||
/// The order of scanning to be performed. The "serial" option will
|
||||
/// scan ports in ascending order while the "random" option will scan
|
||||
/// ports randomly.
|
||||
#[structopt(long, possible_values = &ScanOrder::variants(), case_insensitive = true, default_value = "serial")]
|
||||
scan_order: ScanOrder,
|
||||
|
||||
/// The Nmap arguments to run.
|
||||
/// To use the argument -A, end RustScan's args with '-- -A'.
|
||||
/// Example: 'rustscan -T 1500 127.0.0.1 -- -A -sC'.
|
||||
/// This command adds -Pn -vvv -p $PORTS automatically to nmap.
|
||||
/// For things like --script '(safe and vuln)' enclose it in quotations marks \"'(safe and vuln)'\"")
|
||||
#[structopt(last = true)]
|
||||
command: Vec<String>,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
#[allow(clippy::too_many_lines)]
|
||||
/// Faster Nmap scanning with Rust
|
||||
/// If you're looking for the actual scanning, check out the module Scanner
|
||||
fn main() {
|
||||
#[cfg(not(unix))]
|
||||
let _ = ansi_term::enable_ansi_support();
|
||||
|
||||
env_logger::init();
|
||||
let mut benchmarks = Benchmark::init();
|
||||
let mut rustscan_bench = NamedTimer::start("RustScan");
|
||||
|
||||
info!("Starting up");
|
||||
let opts = Opts::from_args();
|
||||
info!("Mains() `opts` arguments are {:?}", opts);
|
||||
let mut opts: Opts = Opts::read();
|
||||
let config = Config::read(opts.config_path.clone());
|
||||
opts.merge(&config);
|
||||
|
||||
if !opts.quiet && !opts.accessible {
|
||||
print_opening();
|
||||
if let Err(message) = opts.validate_platform() {
|
||||
eprintln!("error: {message}");
|
||||
std::process::exit(2);
|
||||
}
|
||||
|
||||
let ulimit: rlimit::rlim = adjust_ulimit_size(&opts);
|
||||
let batch_size: u16 = infer_batch_size(&opts, ulimit);
|
||||
debug!("Main() `opts` arguments are {opts:?}");
|
||||
|
||||
let scanner = Scanner::new(
|
||||
&opts.ips,
|
||||
let scripts_to_run: Vec<ScriptFile> = match init_scripts(&opts.scripts) {
|
||||
Ok(scripts_to_run) => scripts_to_run,
|
||||
Err(e) => {
|
||||
warning!(
|
||||
format!("Initiating scripts failed!\n{e}"),
|
||||
opts.greppable,
|
||||
opts.accessible
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
debug!("Scripts initialized {scripts_to_run:?}");
|
||||
|
||||
if !opts.greppable && !opts.accessible && !opts.no_banner {
|
||||
print_opening(&opts);
|
||||
}
|
||||
|
||||
let ips: Vec<IpAddr> = parse_addresses(&opts);
|
||||
|
||||
if ips.is_empty() {
|
||||
warning!(
|
||||
"No IPs could be resolved, aborting scan.",
|
||||
opts.greppable,
|
||||
opts.accessible
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
let batch_size = effective_batch_size(&opts);
|
||||
debug!("Effective batch size: {batch_size}");
|
||||
|
||||
let mut scanner = Scanner::new(
|
||||
&ips,
|
||||
batch_size,
|
||||
Duration::from_millis(opts.timeout.into()),
|
||||
opts.quiet,
|
||||
PortStrategy::pick(LOWEST_PORT_NUMBER, TOP_PORT_NUMBER, opts.scan_order),
|
||||
);
|
||||
|
||||
let scan_result = block_on(scanner.run());
|
||||
let mut ports_per_ip = HashMap::new();
|
||||
|
||||
for socket in scan_result {
|
||||
ports_per_ip
|
||||
.entry(socket.ip())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(socket.port());
|
||||
opts.tries,
|
||||
opts.greppable,
|
||||
PortStrategy::pick(&opts.range, opts.ports, opts.scan_order),
|
||||
opts.accessible,
|
||||
opts.exclude_ports.unwrap_or_default(),
|
||||
opts.udp,
|
||||
)
|
||||
.with_open_port_output();
|
||||
if opts.closed {
|
||||
scanner = scanner.with_closed_ports();
|
||||
}
|
||||
if opts.interval > 0 {
|
||||
scanner = scanner.with_interval(Duration::from_millis(opts.interval));
|
||||
}
|
||||
debug!("Scanner finished building: {scanner:?}");
|
||||
|
||||
for ip in opts.ips {
|
||||
if ports_per_ip.contains_key(&ip) {
|
||||
continue;
|
||||
let mut portscan_bench = NamedTimer::start("Portscan");
|
||||
let scan_result = match run_scan(&scanner) {
|
||||
Ok(scan_result) => scan_result,
|
||||
Err(e) => {
|
||||
eprintln!("error: could not start the scan: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
portscan_bench.end();
|
||||
benchmarks.push(portscan_bench);
|
||||
|
||||
// If we got here it means the IP was not found within the HashMap, this
|
||||
// means the scan couldn't find any open ports for it.
|
||||
let mut ports_per_ip: HashMap<IpAddr, Vec<u16>> = HashMap::new();
|
||||
let mut closed_ports_per_ip: HashMap<IpAddr, Vec<u16>> = HashMap::new();
|
||||
|
||||
let x = format!("{} Looks like I didn't find any open ports for {:?}. This is usually caused by a high batch size.
|
||||
\n*I used {} batch size, consider lowering to {} with {} or a comfortable number for your system.
|
||||
\n Alternatively, increase the timeout if your ping is high. Rustscan -t 2000 for 2000 second timeout.\n",
|
||||
"ERROR",
|
||||
ip,
|
||||
opts.batch_size,
|
||||
(opts.batch_size / 2).to_string(),
|
||||
"'rustscan -b <batch_size> <ip address>'");
|
||||
warning!(x, opts.quiet);
|
||||
for status in scan_result {
|
||||
let (map, socket) = match status {
|
||||
PortStatus::Open(socket) => (&mut ports_per_ip, socket),
|
||||
PortStatus::Closed(socket) => (&mut closed_ports_per_ip, socket),
|
||||
};
|
||||
map.entry(socket.ip()).or_default().push(socket.port());
|
||||
}
|
||||
|
||||
for (ip, ports) in ports_per_ip.iter_mut() {
|
||||
let nmap_str_ports: Vec<String> = ports.into_iter().map(|port| port.to_string()).collect();
|
||||
if let Some(x) = no_open_ports_warning(&ips, &ports_per_ip, &closed_ports_per_ip, batch_size) {
|
||||
warning!(x, opts.greppable, opts.accessible);
|
||||
}
|
||||
|
||||
detail!("Starting Nmap", opts.quiet);
|
||||
let mut script_bench = NamedTimer::start("Scripts");
|
||||
for (ip, ports) in &ports_per_ip {
|
||||
let vec_str_ports: Vec<String> = ports.iter().map(ToString::to_string).collect();
|
||||
|
||||
// nmap port style is 80,443. Comma separated with no spaces.
|
||||
let ports_str = nmap_str_ports.join(",");
|
||||
let ports_str = vec_str_ports.join(",");
|
||||
|
||||
// if quiet mode is on nmap should not be spawned
|
||||
if opts.quiet {
|
||||
println!("{}", ports_str);
|
||||
// if option scripts is none, no script will be spawned
|
||||
if opts.greppable || opts.scripts == ScriptsRequired::None {
|
||||
println_safe(format_args!("{ip} -> [{ports_str}]"));
|
||||
continue;
|
||||
}
|
||||
detail!("Starting Script(s)", opts.greppable, opts.accessible);
|
||||
|
||||
let addr = ip.to_string();
|
||||
let user_nmap_args =
|
||||
shell_words::split(&opts.command.join(" ")).expect("failed to parse nmap arguments");
|
||||
let nmap_args = build_nmap_arguments(&addr, &ports_str, &user_nmap_args, ip.is_ipv6());
|
||||
// Run all the scripts we found and parsed based on the script config file tags field.
|
||||
for script_f in &scripts_to_run {
|
||||
// Clone only the script being run instead of the whole list per IP.
|
||||
let mut script_f = script_f.clone();
|
||||
// This part allows us to add commandline arguments to the Script call_format, appending them to the end of the command.
|
||||
if !opts.command.is_empty() {
|
||||
let user_extra_args = &opts.command.join(" ");
|
||||
debug!("Extra args vec {user_extra_args:?}");
|
||||
if script_f.call_format.is_some() {
|
||||
let mut call_f = script_f.call_format.unwrap();
|
||||
call_f.push(' ');
|
||||
call_f.push_str(user_extra_args);
|
||||
output!(
|
||||
format!("Running script {:?} on ip {}\nDepending on the complexity of the script, results may take some time to appear.", call_f, &ip),
|
||||
opts.greppable,
|
||||
opts.accessible
|
||||
);
|
||||
debug!("Call format {call_f}");
|
||||
script_f.call_format = Some(call_f);
|
||||
}
|
||||
}
|
||||
|
||||
output!(format!(
|
||||
"The Nmap command to be run is nmap {}\n",
|
||||
&nmap_args.join(" ")
|
||||
));
|
||||
// Building the script with the arguments from the ScriptFile, and ip-ports.
|
||||
let script = Script::build(
|
||||
script_f.path,
|
||||
*ip,
|
||||
ports.clone(),
|
||||
script_f.port,
|
||||
script_f.ports_separator,
|
||||
script_f.tags,
|
||||
script_f.call_format,
|
||||
);
|
||||
match script.run() {
|
||||
Ok(script_result) => {
|
||||
detail!(script_result.clone(), opts.greppable, opts.accessible);
|
||||
}
|
||||
Err(e) => {
|
||||
warning!(&format!("Error {e}"), opts.greppable, opts.accessible);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Runs the nmap command and spawns it as a process.
|
||||
let mut child = Command::new("nmap")
|
||||
.args(&nmap_args)
|
||||
.spawn()
|
||||
.expect("failed to execute nmap process");
|
||||
// Closed ports are only listed; scripts are never run against them.
|
||||
if opts.closed {
|
||||
println_safe(format_args!("closed ports:"));
|
||||
for (ip, ports) in &closed_ports_per_ip {
|
||||
let ports_str = ports
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
println_safe(format_args!("{ip} -> [{ports_str}]"));
|
||||
}
|
||||
}
|
||||
|
||||
child.wait().expect("failed to wait on nmap process");
|
||||
// To use the runtime benchmark, run the process as: RUST_LOG=info ./rustscan
|
||||
script_bench.end();
|
||||
benchmarks.push(script_bench);
|
||||
rustscan_bench.end();
|
||||
benchmarks.push(rustscan_bench);
|
||||
debug!("Benchmarks raw {benchmarks:?}");
|
||||
info!("{}", benchmarks.summary());
|
||||
}
|
||||
|
||||
/// Builds the hint printed when the scan found no open ports.
|
||||
///
|
||||
/// A host without open ports is normal when another host answered (a subnet
|
||||
/// sweep for one port): replies got through at this batch size and timeout,
|
||||
/// so batch size and timeout are unlikely to be the cause. The hint is printed
|
||||
/// once, and only when no host answered with an open or, under `--closed`, a
|
||||
/// closed port.
|
||||
fn no_open_ports_warning(
|
||||
ips: &[IpAddr],
|
||||
ports_per_ip: &HashMap<IpAddr, Vec<u16>>,
|
||||
closed_ports_per_ip: &HashMap<IpAddr, Vec<u16>>,
|
||||
batch_size: usize,
|
||||
) -> Option<String> {
|
||||
if !ports_per_ip.is_empty() || !closed_ports_per_ip.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let target = match ips {
|
||||
[ip] => format!("for {ip:?}"),
|
||||
_ => format!("on any of the {} addresses", ips.len()),
|
||||
};
|
||||
Some(format!("Looks like I didn't find any open ports {}. This is usually caused by a high batch size.
|
||||
\n*I used {} batch size, consider lowering it with {} or a comfortable number for your system.
|
||||
\n Alternatively, increase the timeout if your ping is high. Rustscan -t 2000 for 2000 milliseconds (2s) timeout.\n",
|
||||
target,
|
||||
batch_size,
|
||||
"'rustscan -b <batch_size> -a <ip address>'"))
|
||||
}
|
||||
|
||||
/// Determines the actual batch size used by the scanner.
|
||||
///
|
||||
/// Unix systems may reduce the requested batch size according to the process
|
||||
/// file-descriptor limit. Other platforms, including Windows, use the batch
|
||||
/// size explicitly requested by the user.
|
||||
fn effective_batch_size(opts: &Opts) -> usize {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
infer_batch_size(opts, adjust_ulimit_size(opts))
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
opts.batch_size
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs the scan to completion on a current-thread Tokio runtime.
|
||||
///
|
||||
/// The scanner drives every socket from a single future and never spawns
|
||||
/// tasks, so one thread is all it can use: a current-thread runtime runs the
|
||||
/// scan, the I/O driver and the timers on the calling thread, without the
|
||||
/// worker threads and cross-thread wake-ups of a multi-threaded runtime.
|
||||
///
|
||||
/// The runtime only exists for the duration of the scan. Address resolution
|
||||
/// (blocking DNS lookups and file reads) happens before it is created, so it
|
||||
/// can never stall the scan, and scripts run after it is gone.
|
||||
fn run_scan(scanner: &Scanner) -> std::io::Result<Vec<PortStatus>> {
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_io()
|
||||
.enable_time()
|
||||
.build()?;
|
||||
|
||||
Ok(runtime.block_on(scanner.run_with_status()))
|
||||
}
|
||||
|
||||
/// Prints the opening title of RustScan
|
||||
fn print_opening() {
|
||||
info!("Printing opening");
|
||||
#[allow(clippy::items_after_statements, clippy::needless_raw_string_hashes)]
|
||||
fn print_opening(opts: &Opts) {
|
||||
debug!("Printing opening");
|
||||
let s = r#".----. .-. .-. .----..---. .----. .---. .--. .-. .-.
|
||||
| {} }| { } |{ {__ {_ _}{ {__ / ___} / {} \ | `| |
|
||||
| .-. \| {_} |.-._} } | | .-._} }\ }/ /\ \| |\ |
|
||||
`-' `-'`-----'`----' `-' `----' `---' `-' `-'`-' `-'
|
||||
Faster Nmap scanning with Rust."#;
|
||||
println!("{}", s.gradient(Color::Green).bold());
|
||||
The Modern Day Port Scanner."#;
|
||||
|
||||
println_safe(format_args!("{}", s.gradient(Color::Green).bold()));
|
||||
let info = r#"________________________________________
|
||||
: https://discord.gg/GFrQsGy :
|
||||
: http://discord.skerritt.blog :
|
||||
: https://github.com/RustScan/RustScan :
|
||||
--------------------------------------"#;
|
||||
println!("{}", info.gradient(Color::Yellow).bold());
|
||||
println_safe(format_args!("{}", info.gradient(Color::Yellow).bold()));
|
||||
funny_opening!();
|
||||
|
||||
let config_path = match dirs::config_dir() {
|
||||
Some(mut path) => {
|
||||
path.push("rustscan");
|
||||
path.push("config.toml");
|
||||
path
|
||||
}
|
||||
None => panic!("Couldn't find config dir."),
|
||||
};
|
||||
let config_path = opts
|
||||
.config_path
|
||||
.clone()
|
||||
.unwrap_or_else(input::default_config_path);
|
||||
|
||||
detail!(format!(
|
||||
"{} {:?}",
|
||||
"The config file is expected to be at", config_path
|
||||
));
|
||||
}
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
fn build_nmap_arguments<'a>(
|
||||
addr: &'a str,
|
||||
ports: &'a str,
|
||||
user_args: &'a Vec<String>,
|
||||
is_ipv6: bool,
|
||||
) -> Vec<&'a str> {
|
||||
let mut arguments: Vec<&str> = user_args.iter().map(AsRef::as_ref).collect();
|
||||
arguments.push("-A");
|
||||
arguments.push("-vvv");
|
||||
detail!(
|
||||
format!("The config file is expected to be at {config_path:?}"),
|
||||
opts.greppable,
|
||||
opts.accessible
|
||||
);
|
||||
|
||||
if is_ipv6 {
|
||||
arguments.push("-6");
|
||||
if opts.config_path.is_none() {
|
||||
let old_config_path = input::old_default_config_path();
|
||||
detail!(
|
||||
format!(
|
||||
"For backwards compatibility, the config file may also be at {old_config_path:?}"
|
||||
),
|
||||
opts.greppable,
|
||||
opts.accessible
|
||||
);
|
||||
}
|
||||
|
||||
arguments.push("-p");
|
||||
arguments.push(ports);
|
||||
arguments.push(addr);
|
||||
|
||||
arguments
|
||||
}
|
||||
|
||||
fn adjust_ulimit_size(opts: &Opts) -> rlimit::rlim {
|
||||
if opts.ulimit.is_some() {
|
||||
let limit: rlimit::rlim = opts.ulimit.unwrap();
|
||||
#[cfg(unix)]
|
||||
fn adjust_ulimit_size(opts: &Opts) -> usize {
|
||||
use rlimit::Resource;
|
||||
use std::convert::TryInto;
|
||||
|
||||
match setrlimit(Resource::NOFILE, limit, limit) {
|
||||
Ok(_) => {
|
||||
detail!(
|
||||
format!("Automatically increasing ulimit value to {}.", limit),
|
||||
opts.quiet
|
||||
);
|
||||
}
|
||||
Err(_) => println!("{}", "ERROR. Failed to set ulimit value."),
|
||||
if let Some(limit) = opts.ulimit {
|
||||
let limit = limit as u64;
|
||||
if Resource::NOFILE.set(limit, limit).is_ok() {
|
||||
detail!(
|
||||
format!("Automatically increasing ulimit value to {limit}."),
|
||||
opts.greppable,
|
||||
opts.accessible
|
||||
);
|
||||
} else {
|
||||
warning!(
|
||||
"ERROR. Failed to set ulimit value.",
|
||||
opts.greppable,
|
||||
opts.accessible
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let (rlim, _) = getrlimit(Resource::NOFILE).unwrap();
|
||||
|
||||
rlim
|
||||
let (soft, _) = Resource::NOFILE.get().unwrap();
|
||||
// On 32-bit targets the soft limit (e.g. RLIM_INFINITY) may not fit in a
|
||||
// usize. Fall back to the conservative default rather than usize::MAX,
|
||||
// which would skip every batch-size adjustment in infer_batch_size.
|
||||
soft.try_into().unwrap_or(DEFAULT_FILE_DESCRIPTORS_LIMIT)
|
||||
}
|
||||
|
||||
fn infer_batch_size(opts: &Opts, ulimit: rlimit::rlim) -> u16 {
|
||||
let mut batch_size: rlimit::rlim = opts.batch_size.into();
|
||||
#[cfg(unix)]
|
||||
fn infer_batch_size(opts: &Opts, ulimit: usize) -> usize {
|
||||
let mut batch_size = opts.batch_size;
|
||||
|
||||
// Adjust the batch size when the ulimit value is lower than the desired batch size
|
||||
if ulimit < batch_size {
|
||||
warning!("File limit is lower than default batch size. Consider upping with --ulimt. May cause harm to sensitive servers",
|
||||
opts.quiet
|
||||
warning!("File limit is lower than default batch size. Consider upping with --ulimit. May cause harm to sensitive servers",
|
||||
opts.greppable, opts.accessible
|
||||
);
|
||||
|
||||
// When the OS supports high file limits like 8000, but the user
|
||||
// selected a batch size higher than this we should reduce it to
|
||||
// a lower number.
|
||||
if ulimit < AVERAGE_BATCH_SIZE {
|
||||
// ulimit is smaller than aveage batch size
|
||||
// ulimit is smaller than average batch size
|
||||
// user must have very small ulimit
|
||||
// decrease batch size to half of ulimit
|
||||
warning!("Your file limit is very small, which negatively impacts RustScan's speed. Use the Docker image, or up the Ulimit with '--ulimit 5000'. ");
|
||||
warning!("Your file limit is very small, which negatively impacts RustScan's speed. Use the Docker image, or up the Ulimit with '--ulimit 5000'. ", opts.greppable, opts.accessible);
|
||||
info!("Halving batch_size because ulimit is smaller than average batch size");
|
||||
batch_size = ulimit / 2
|
||||
batch_size = ulimit / 2;
|
||||
} else if ulimit > DEFAULT_FILE_DESCRIPTORS_LIMIT {
|
||||
info!("Batch size is now average batch size");
|
||||
batch_size = AVERAGE_BATCH_SIZE
|
||||
batch_size = AVERAGE_BATCH_SIZE;
|
||||
} else {
|
||||
batch_size = ulimit - 100
|
||||
batch_size = ulimit - 100;
|
||||
}
|
||||
}
|
||||
// When the ulimit is higher than the batch size let the user know that the
|
||||
// batch size can be increased unless they specified the ulimit themselves.
|
||||
else if ulimit + 2 > batch_size && (opts.ulimit.is_none()) {
|
||||
detail!(format!(
|
||||
"File limit higher than batch size. Can increase speed by increasing batch size '-b {}'.",
|
||||
ulimit - 100
|
||||
), opts.quiet);
|
||||
detail!(format!("File limit higher than batch size. Can increase speed by increasing batch size '-b {}'.", ulimit - 100),
|
||||
opts.greppable, opts.accessible);
|
||||
}
|
||||
|
||||
batch_size as u16
|
||||
batch_size
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{adjust_ulimit_size, infer_batch_size, print_opening, Opts, ScanOrder};
|
||||
use std::{net::IpAddr, str::FromStr};
|
||||
#[cfg(windows)]
|
||||
use super::effective_batch_size;
|
||||
#[cfg(unix)]
|
||||
use super::{adjust_ulimit_size, infer_batch_size};
|
||||
use super::{no_open_ports_warning, print_opening, Opts};
|
||||
use std::collections::HashMap;
|
||||
use std::net::IpAddr;
|
||||
|
||||
fn hosts(n: u8) -> Vec<IpAddr> {
|
||||
(1..=n).map(|i| IpAddr::from([192, 0, 2, i])).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_open_ports_silent_when_another_host_answered() {
|
||||
// Sweeping a subnet for one port: one host has it open, the rest do not.
|
||||
let ips = hosts(254);
|
||||
let ports_per_ip = HashMap::from([(ips[0], vec![22])]);
|
||||
|
||||
assert!(no_open_ports_warning(&ips, &ports_per_ip, &HashMap::new(), 4500).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_open_ports_silent_when_a_host_answered_closed() {
|
||||
// With --closed, a refused connection also proves replies get through.
|
||||
let ips = hosts(254);
|
||||
let closed_ports_per_ip = HashMap::from([(ips[0], vec![443])]);
|
||||
|
||||
assert!(no_open_ports_warning(&ips, &HashMap::new(), &closed_ports_per_ip, 4500).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_open_ports_warns_once_when_nothing_found() {
|
||||
let warning = no_open_ports_warning(&hosts(254), &HashMap::new(), &HashMap::new(), 4500);
|
||||
|
||||
let warning = warning.expect("one hint when nothing was found");
|
||||
assert!(warning.contains("on any of the 254 addresses."));
|
||||
assert!(warning.contains("4500 batch size"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_open_ports_names_the_single_host() {
|
||||
let warning = no_open_ports_warning(&hosts(1), &HashMap::new(), &HashMap::new(), 4500);
|
||||
|
||||
let warning = warning.expect("one hint for the single host");
|
||||
assert!(warning.contains("for 192.0.2.1."));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn batch_size_lowered() {
|
||||
let opts = Opts {
|
||||
ips: vec![IpAddr::from_str("127.0.0.1").unwrap()],
|
||||
quiet: true,
|
||||
batch_size: 50_000,
|
||||
timeout: 1_000,
|
||||
ulimit: Some(2_000),
|
||||
command: Vec::new(),
|
||||
accessible: false,
|
||||
scan_order: ScanOrder::Serial,
|
||||
..Default::default()
|
||||
};
|
||||
let batch_size = infer_batch_size(&opts, 120);
|
||||
|
||||
assert!(batch_size < 50_000);
|
||||
assert!(batch_size < opts.batch_size);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn batch_size_lowered_average_size() {
|
||||
let opts = Opts {
|
||||
ips: vec![IpAddr::from_str("127.0.0.1").unwrap()],
|
||||
quiet: true,
|
||||
batch_size: 50_000,
|
||||
timeout: 1_000,
|
||||
ulimit: Some(2_000),
|
||||
command: Vec::new(),
|
||||
accessible: false,
|
||||
scan_order: ScanOrder::Serial,
|
||||
..Default::default()
|
||||
};
|
||||
let batch_size = infer_batch_size(&opts, 9_000);
|
||||
|
||||
assert!(batch_size == 3_000);
|
||||
assert_eq!(batch_size, 3_000);
|
||||
}
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn batch_size_equals_ulimit_lowered() {
|
||||
// because ulimit and batch size are same size, batch size is lowered
|
||||
// to ULIMIT - 100
|
||||
let opts = Opts {
|
||||
ips: vec![IpAddr::from_str("127.0.0.1").unwrap()],
|
||||
quiet: true,
|
||||
batch_size: 50_000,
|
||||
timeout: 1_000,
|
||||
ulimit: Some(2_000),
|
||||
command: Vec::new(),
|
||||
accessible: false,
|
||||
scan_order: ScanOrder::Serial,
|
||||
..Default::default()
|
||||
};
|
||||
let batch_size = infer_batch_size(&opts, 5_000);
|
||||
|
||||
assert!(batch_size == 4_900);
|
||||
assert_eq!(batch_size, 4_900);
|
||||
}
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn batch_size_adjusted_2000() {
|
||||
// ulimit == batch_size
|
||||
let opts = Opts {
|
||||
ips: vec![IpAddr::from_str("127.0.0.1").unwrap()],
|
||||
quiet: true,
|
||||
batch_size: 50_000,
|
||||
timeout: 1_000,
|
||||
ulimit: Some(2_000),
|
||||
command: Vec::new(),
|
||||
accessible: false,
|
||||
scan_order: ScanOrder::Serial,
|
||||
..Default::default()
|
||||
};
|
||||
let batch_size = adjust_ulimit_size(&opts);
|
||||
|
||||
assert!(batch_size == 2_000);
|
||||
assert_eq!(batch_size, 2_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_print_opening_no_panic() {
|
||||
// print opening should not panic
|
||||
print_opening();
|
||||
assert!(1 == 1);
|
||||
}
|
||||
#[test]
|
||||
fn test_high_ulimit_no_quiet_mode() {
|
||||
#[cfg(unix)]
|
||||
fn test_high_ulimit_no_greppable_mode() {
|
||||
let opts = Opts {
|
||||
ips: vec![IpAddr::from_str("127.0.0.1").unwrap()],
|
||||
quiet: false,
|
||||
batch_size: 10,
|
||||
timeout: 1_000,
|
||||
ulimit: None,
|
||||
command: Vec::new(),
|
||||
accessible: true,
|
||||
scan_order: ScanOrder::Serial,
|
||||
greppable: false,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
infer_batch_size(&opts, 1_000_000);
|
||||
let batch_size = infer_batch_size(&opts, 1_000_000);
|
||||
|
||||
assert!(1 == 1);
|
||||
assert_eq!(batch_size, opts.batch_size);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_print_opening_no_panic() {
|
||||
let opts = Opts::default();
|
||||
// print opening should not panic
|
||||
print_opening(&opts);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
fn windows_batch_size_uses_requested_value() {
|
||||
let opts = Opts {
|
||||
batch_size: 50,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert_eq!(effective_batch_size(&opts), 50);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
fn windows_batch_size_preserves_large_requested_value() {
|
||||
let opts = Opts {
|
||||
batch_size: 12_345,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert_eq!(effective_batch_size(&opts), 12_345);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
fn windows_batch_size_preserves_single_connection() {
|
||||
let opts = Opts {
|
||||
batch_size: 1,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert_eq!(effective_batch_size(&opts), 1);
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,49 @@
|
||||
//! Provides a means to hold configuration options specifically for port scanning.
|
||||
mod range_iterator;
|
||||
use super::ScanOrder;
|
||||
use crate::input::{PortRanges, ScanOrder};
|
||||
use rand::rng;
|
||||
use rand::seq::SliceRandom;
|
||||
use range_iterator::RangeIterator;
|
||||
|
||||
/// Represents options of port scanning.
|
||||
///
|
||||
/// Right now all these options involve ranges, but in the future
|
||||
/// it will also contain custom lists of ports.
|
||||
#[derive(Debug)]
|
||||
pub enum PortStrategy {
|
||||
Manual(Vec<u16>),
|
||||
Serial(SerialRange),
|
||||
Random(RandomRange),
|
||||
}
|
||||
|
||||
impl PortStrategy {
|
||||
pub fn pick(start: u16, end: u16, scan_type: ScanOrder) -> Self {
|
||||
match scan_type {
|
||||
ScanOrder::Serial => PortStrategy::Serial(SerialRange { start, end }),
|
||||
ScanOrder::Random => PortStrategy::Random(RandomRange { start, end }),
|
||||
pub fn pick(range: &Option<PortRanges>, ports: Option<Vec<u16>>, order: ScanOrder) -> Self {
|
||||
match order {
|
||||
ScanOrder::Serial if ports.is_none() => {
|
||||
let port_ranges = range.as_ref().unwrap();
|
||||
PortStrategy::Serial(SerialRange {
|
||||
range: port_ranges.0.clone(),
|
||||
})
|
||||
}
|
||||
ScanOrder::Random if ports.is_none() => {
|
||||
let port_ranges = range.as_ref().unwrap();
|
||||
PortStrategy::Random(RandomRange {
|
||||
range: port_ranges.0.clone(),
|
||||
})
|
||||
}
|
||||
ScanOrder::Serial => PortStrategy::Manual(ports.unwrap()),
|
||||
ScanOrder::Random => {
|
||||
let mut rng = rng();
|
||||
let mut ports = ports.unwrap();
|
||||
ports.shuffle(&mut rng);
|
||||
PortStrategy::Manual(ports)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn order(&self) -> Vec<u16> {
|
||||
match self {
|
||||
PortStrategy::Manual(ports) => ports.clone(),
|
||||
PortStrategy::Serial(range) => range.generate(),
|
||||
PortStrategy::Random(range) => range.generate(),
|
||||
}
|
||||
@@ -35,22 +58,22 @@ trait RangeOrder {
|
||||
|
||||
/// As the name implies SerialRange will always generate a vector in
|
||||
/// ascending order.
|
||||
#[derive(Debug)]
|
||||
pub struct SerialRange {
|
||||
start: u16,
|
||||
end: u16,
|
||||
range: Vec<(u16, u16)>,
|
||||
}
|
||||
|
||||
impl RangeOrder for SerialRange {
|
||||
fn generate(&self) -> Vec<u16> {
|
||||
(self.start..self.end).collect()
|
||||
RangeIterator::new_serial(&self.range).collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// As the name implies RandomRange will always generate a vector with
|
||||
/// a random order. This vector is built following the LCG algorithm.
|
||||
#[derive(Debug)]
|
||||
pub struct RandomRange {
|
||||
start: u16,
|
||||
end: u16,
|
||||
range: Vec<(u16, u16)>,
|
||||
}
|
||||
|
||||
impl RangeOrder for RandomRange {
|
||||
@@ -64,30 +87,64 @@ impl RangeOrder for RandomRange {
|
||||
// port numbers close to each other are pretty slim due to the way the
|
||||
// algorithm works.
|
||||
fn generate(&self) -> Vec<u16> {
|
||||
RangeIterator::new(self.start.into(), self.end.into()).collect()
|
||||
RangeIterator::new_random(&self.range).collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::PortStrategy;
|
||||
use crate::ScanOrder;
|
||||
use crate::input::{PortRanges, ScanOrder};
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[test]
|
||||
fn serial_strategy() {
|
||||
let strategy = PortStrategy::pick(1, 100, ScanOrder::Serial);
|
||||
let result = strategy.order();
|
||||
let expected_range = (1..100).into_iter().collect::<Vec<u16>>();
|
||||
assert_eq!(expected_range, result);
|
||||
fn expected_ports_from_ranges(input: &[(u16, u16)]) -> Vec<u16> {
|
||||
let mut s = HashSet::new();
|
||||
for &(start, end) in input {
|
||||
for p in start..=end {
|
||||
s.insert(p);
|
||||
}
|
||||
}
|
||||
let mut v: Vec<u16> = s.into_iter().collect();
|
||||
v.sort_unstable();
|
||||
v
|
||||
}
|
||||
#[test]
|
||||
fn random_strategy() {
|
||||
let strategy = PortStrategy::pick(1, 100, ScanOrder::Random);
|
||||
let mut result = strategy.order();
|
||||
let expected_range = (1..100).into_iter().collect::<Vec<u16>>();
|
||||
assert_ne!(expected_range, result);
|
||||
fn serial_strategy_with_range() {
|
||||
let ranges = PortRanges(vec![(1u16, 10u16), (20u16, 30u16), (100u16, 110u16)]);
|
||||
let strategy = PortStrategy::pick(&Some(ranges.clone()), None, ScanOrder::Serial);
|
||||
let result = strategy.order();
|
||||
let expected = expected_ports_from_ranges(&ranges.0);
|
||||
|
||||
result.sort();
|
||||
assert_eq!(expected_range, result);
|
||||
assert_eq!(expected, result);
|
||||
}
|
||||
#[test]
|
||||
fn random_strategy_with_range() {
|
||||
let ranges = PortRanges(vec![(1u16, 10u16), (20u16, 30u16), (100u16, 110u16)]);
|
||||
let strategy = PortStrategy::pick(&Some(ranges.clone()), None, ScanOrder::Random);
|
||||
let mut result = strategy.order();
|
||||
let expected = expected_ports_from_ranges(&ranges.0);
|
||||
|
||||
assert_ne!(expected, result);
|
||||
result.sort_unstable();
|
||||
|
||||
assert_eq!(expected, result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serial_strategy_with_ports() {
|
||||
let strategy = PortStrategy::pick(&None, Some(vec![80, 443]), ScanOrder::Serial);
|
||||
let result = strategy.order();
|
||||
assert_eq!(vec![80, 443], result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn random_strategy_with_ports() {
|
||||
let strategy = PortStrategy::pick(&None, Some((1..10).collect()), ScanOrder::Random);
|
||||
let mut result = strategy.order();
|
||||
let expected = (1..10).collect::<Vec<u16>>();
|
||||
assert_ne!(expected, result);
|
||||
|
||||
result.sort_unstable();
|
||||
assert_eq!(expected, result);
|
||||
}
|
||||
}
|
||||
@@ -1,66 +1,196 @@
|
||||
use gcd::Gcd;
|
||||
use rand::Rng;
|
||||
use rand::RngExt;
|
||||
use std::convert::TryInto;
|
||||
|
||||
/// Yields ports produced from a collection of (possibly overlapping)
|
||||
/// inclusive `u16` ranges in two modes:
|
||||
///
|
||||
/// **Randomized** — created by `RangeIterator::new_random`:
|
||||
/// - The algorithm generates a permutation of indices `0..N-1` using the
|
||||
/// additive congruential step `x_{i+1} = (x_i + step) % N`.
|
||||
/// - `step` is chosen so `gcd(step, N) == 1` to ensure the sequence is a
|
||||
/// full-length cycle (visits each index exactly once).
|
||||
/// - `x_0` (the seed stored as `normalized_first_pick`) is chosen uniformly
|
||||
/// in `0..N`.
|
||||
///
|
||||
/// For more information: <https://en.wikipedia.org/wiki/Linear_congruential_generator>
|
||||
///
|
||||
/// **Serial** — `RangeIterator::new_serial`:
|
||||
/// Iterates the input ranges in the **original input order** and yields
|
||||
/// each port the first time it is encountered. Duplicate ports (from
|
||||
/// overlapping ranges) are skipped using a 65_536-entry "seen" table.
|
||||
pub struct RangeIterator {
|
||||
active: bool,
|
||||
normalized_end: u32,
|
||||
total: u32,
|
||||
normalized_first_pick: u32,
|
||||
normalized_pick: u32,
|
||||
actual_start: u32,
|
||||
step: u32,
|
||||
ranges: Vec<(u32, u32)>,
|
||||
prefix: Vec<u32>,
|
||||
serial_itr: Option<Box<dyn Iterator<Item = u16>>>,
|
||||
/// `seen[port]` is true once `port` has been yielded (serial mode only).
|
||||
serial_seen: Option<Vec<bool>>,
|
||||
}
|
||||
|
||||
/// An iterator that follows the `Linear Congruential Generator` algorithm.
|
||||
///
|
||||
/// For more information: https://en.wikipedia.org/wiki/Linear_congruential_generator
|
||||
impl RangeIterator {
|
||||
/// Receives the the start and end of a range and normalize
|
||||
/// these values before selecting a coprime for the end of the range
|
||||
/// which will server as the step for the algorithm.
|
||||
/// Construct a randomized iterator (LCG permutation).
|
||||
///
|
||||
/// For example, the range `1000-2500` will be normalized to `0-1500`
|
||||
/// before going through the algorithm.
|
||||
pub fn new(start: u32, end: u32) -> Self {
|
||||
let normalized_end = end - start;
|
||||
let step = pick_random_coprime(normalized_end);
|
||||
/// Preconditions:
|
||||
/// - `input` must contain at least one `(u16,u16)`
|
||||
/// and each pair must satisfy `start <= end`.
|
||||
pub fn new_random(input: &[(u16, u16)]) -> Self {
|
||||
// normalize & merge into (start, len) u32 pairs
|
||||
// Example: [(10,12),(11,15)] -> merged [(10,6)]
|
||||
let mut ranges: Vec<(u32, u32)> = input
|
||||
.iter()
|
||||
.map(|(s, e)| {
|
||||
let start = *s as u32;
|
||||
let end_excl = (*e as u32) + 1; // convert inclusive -> exclusive
|
||||
(start, end_excl)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Randomly choose a number within the range to be the first
|
||||
// and assign it as a pick.
|
||||
let mut rng = rand::thread_rng();
|
||||
let normalized_first_pick = rng.gen_range(0, normalized_end);
|
||||
ranges.sort_unstable_by_key(|&(s, _)| s);
|
||||
|
||||
let mut merged: Vec<(u32, u32)> = Vec::with_capacity(ranges.len());
|
||||
if !ranges.is_empty() {
|
||||
let mut iter = ranges.into_iter();
|
||||
let (mut cur_s, mut cur_end) = iter.next().unwrap(); // cur_end exclusive
|
||||
for (s, end_excl) in iter {
|
||||
if s <= cur_end {
|
||||
// overlap/adjacent -> extend
|
||||
if end_excl > cur_end {
|
||||
cur_end = end_excl;
|
||||
}
|
||||
} else {
|
||||
// push disjoint segment as (start, len)
|
||||
merged.push((cur_s, cur_end - cur_s)); // len = excl - start
|
||||
cur_s = s;
|
||||
cur_end = end_excl;
|
||||
}
|
||||
}
|
||||
merged.push((cur_s, cur_end - cur_s));
|
||||
}
|
||||
// build prefix sums (prefix[0] = 0; prefix.len() == merged.len() + 1)
|
||||
let prefix = merged.iter().fold(vec![0u32], |mut acc, (_, len)| {
|
||||
let last = acc.last().unwrap();
|
||||
acc.push(last.saturating_add(*len));
|
||||
acc
|
||||
});
|
||||
|
||||
// total is guaranteed > 0 by precondition (input.len() >= 1)
|
||||
let total = *prefix.last().unwrap();
|
||||
|
||||
// pick step and seed
|
||||
let step = pick_random_coprime(total);
|
||||
let mut rng = rand::rng();
|
||||
let first = rng.random_range(0..total);
|
||||
|
||||
Self {
|
||||
active: true,
|
||||
normalized_end,
|
||||
total,
|
||||
normalized_first_pick: first,
|
||||
normalized_pick: first,
|
||||
step,
|
||||
normalized_first_pick,
|
||||
normalized_pick: normalized_first_pick,
|
||||
actual_start: start,
|
||||
ranges: merged,
|
||||
prefix,
|
||||
serial_itr: None,
|
||||
serial_seen: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct a serial iterator that yields ports in original input order,
|
||||
/// skipping duplicates. The deduplication is done on the fly with a seen-table.
|
||||
///
|
||||
/// Preconditions:
|
||||
/// - `input` must contain at least one `(u16,u16)` and each pair must satisfy `start <= end`.
|
||||
pub fn new_serial(input: &[(u16, u16)]) -> Self {
|
||||
// Build a serial iterator that yields ports in *input order* (start..=end).
|
||||
// We keep the merged ranges/prefix empty here (they are not needed for serial mode).
|
||||
let input = input.to_vec();
|
||||
let serial_itr = input.into_iter().flat_map(|(start, end)| start..=end);
|
||||
|
||||
let serial_itr_boxed: Box<dyn Iterator<Item = u16>> = Box::new(serial_itr);
|
||||
// One entry per possible port (0..=65535).
|
||||
let seen = vec![false; usize::from(u16::MAX) + 1];
|
||||
|
||||
Self {
|
||||
active: true,
|
||||
total: 0,
|
||||
normalized_first_pick: 0,
|
||||
normalized_pick: 0,
|
||||
step: 0,
|
||||
ranges: Vec::new(),
|
||||
prefix: Vec::new(),
|
||||
serial_itr: Some(serial_itr_boxed),
|
||||
serial_seen: Some(seen),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for RangeIterator {
|
||||
type Item = u16;
|
||||
|
||||
// The next step is always bound by the formula: N+1 = (N + STEP) % TOP_OF_THE_RANGE
|
||||
// It will only stop once we generate a number equal to the first generated number.
|
||||
/// Advance the iterator by one port.
|
||||
///
|
||||
/// 1. Read the current normalized index `cur`.
|
||||
/// 2. Compute `next = (cur + step) % total` and update `normalized_pick`.
|
||||
/// 3. If `next == normalized_first_pick` mark `active = false` (we completed the cycle).
|
||||
/// 4. Map the returned index `cur` into the merged ranges via the prefix array:
|
||||
/// - find the range index `idx` where `prefix[idx] <= cur < prefix[idx+1]`,
|
||||
/// - offset = `cur - prefix[idx]`,
|
||||
/// - port = `ranges[idx].0 + offset`.
|
||||
/// 5. Return `port as u16`.
|
||||
///
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if !self.active {
|
||||
return None;
|
||||
}
|
||||
|
||||
let current_pick = self.normalized_pick;
|
||||
let next_pick = (current_pick + self.step) % self.normalized_end;
|
||||
// SERIAL iterator fast-path: preserve original input order but skip duplicates.
|
||||
if let (Some(it), Some(seen)) = (self.serial_itr.as_mut(), self.serial_seen.as_mut()) {
|
||||
for p in it.by_ref() {
|
||||
// Yield each port only the first time it is seen.
|
||||
if !std::mem::replace(&mut seen[usize::from(p)], true) {
|
||||
return Some(p);
|
||||
}
|
||||
// otherwise skip duplicate and continue
|
||||
}
|
||||
// serial iterator exhausted: drop it and mark inactive
|
||||
self.serial_itr = None;
|
||||
self.serial_seen = None;
|
||||
self.active = false;
|
||||
return None;
|
||||
}
|
||||
|
||||
// If the next pick is equal to the first pick this means that
|
||||
// we have iterated through the entire range.
|
||||
if next_pick == self.normalized_first_pick {
|
||||
// RANDOMIZED (LCG) path
|
||||
let cur = self.normalized_pick;
|
||||
let next = (cur + self.step) % self.total;
|
||||
|
||||
// if next equals the original seed we finished the cycle after returning cur
|
||||
if next == self.normalized_first_pick {
|
||||
self.active = false;
|
||||
}
|
||||
|
||||
self.normalized_pick = next_pick;
|
||||
Some((self.actual_start + current_pick) as u16)
|
||||
self.normalized_pick = next;
|
||||
|
||||
// Map cur -> port using prefix + ranges (binary search)
|
||||
let mut lo: usize = 0;
|
||||
let mut hi: usize = self.ranges.len();
|
||||
while lo < hi {
|
||||
let mid = (lo + hi) / 2;
|
||||
if self.prefix[mid + 1] > cur {
|
||||
hi = mid;
|
||||
} else {
|
||||
lo = mid + 1;
|
||||
}
|
||||
}
|
||||
let idx = lo;
|
||||
let offset = cur - self.prefix[idx];
|
||||
let (start, _len) = self.ranges[idx];
|
||||
let port = (start + offset)
|
||||
.try_into()
|
||||
.expect("Could not convert u32 to u16");
|
||||
Some(port)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,15 +208,14 @@ fn pick_random_coprime(end: u32) -> u32 {
|
||||
let range_boundary = end / 4;
|
||||
let lower_range = range_boundary;
|
||||
let upper_range = end - range_boundary;
|
||||
let mut rng = rand::thread_rng();
|
||||
let mut candidate = rng.gen_range(lower_range, upper_range);
|
||||
let mut rng = rand::rng();
|
||||
let mut candidate = rng.random_range(lower_range..upper_range);
|
||||
|
||||
for _ in 0..10 {
|
||||
if end.gcd(candidate) == 1 {
|
||||
return candidate;
|
||||
} else {
|
||||
candidate = rng.gen_range(lower_range, upper_range);
|
||||
}
|
||||
candidate = rng.random_range(lower_range..upper_range);
|
||||
}
|
||||
|
||||
end - 1
|
||||
@@ -94,36 +223,105 @@ fn pick_random_coprime(end: u32) -> u32 {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::RangeIterator;
|
||||
use super::*;
|
||||
use std::collections::HashSet;
|
||||
|
||||
// Helper: collect, sort and return ports produced by randomized RangeIterator
|
||||
fn generate_sorted_from_ranges_random(input: &[(u16, u16)]) -> Vec<u16> {
|
||||
let mut it = RangeIterator::new_random(input);
|
||||
let mut v: Vec<u16> = it.by_ref().collect();
|
||||
v.sort_unstable();
|
||||
v
|
||||
}
|
||||
|
||||
// Helper: collect, sort and return ports produced by serial RangeIterator
|
||||
fn generate_sorted_from_ranges_serial(input: &[(u16, u16)]) -> Vec<u16> {
|
||||
let mut it = RangeIterator::new_serial(input);
|
||||
let mut v: Vec<u16> = it.by_ref().collect();
|
||||
v.sort_unstable();
|
||||
v
|
||||
}
|
||||
|
||||
// Build expected sorted unique ports from input ranges (inclusive)
|
||||
fn expected_ports_from_ranges(input: &[(u16, u16)]) -> Vec<u16> {
|
||||
let mut s = HashSet::new();
|
||||
for &(start, end) in input {
|
||||
for p in start..=end {
|
||||
s.insert(p);
|
||||
}
|
||||
}
|
||||
let mut v: Vec<u16> = s.into_iter().collect();
|
||||
v.sort_unstable();
|
||||
v
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_iterator_iterates_through_the_entire_range() {
|
||||
let result = generate_sorted_range(1, 10);
|
||||
let expected_range = (1..10).into_iter().collect::<Vec<u16>>();
|
||||
assert_eq!(expected_range, result);
|
||||
fn random_range_iterator_test() {
|
||||
// small disjoint ranges
|
||||
let input = &[(1u16, 10u16), (20u16, 30u16), (100u16, 110u16)];
|
||||
let result = generate_sorted_from_ranges_random(input);
|
||||
let expected = expected_ports_from_ranges(input);
|
||||
assert_eq!(expected, result);
|
||||
|
||||
let result = generate_sorted_range(1, 100);
|
||||
let expected_range = (1..100).into_iter().collect::<Vec<u16>>();
|
||||
assert_eq!(expected_range, result);
|
||||
// larger disjoint ranges
|
||||
let input = &[(1u16, 100u16), (200u16, 500u16)];
|
||||
let result = generate_sorted_from_ranges_random(input);
|
||||
let expected = expected_ports_from_ranges(input);
|
||||
assert_eq!(expected, result);
|
||||
|
||||
let result = generate_sorted_range(1, 1000);
|
||||
let expected_range = (1..1000).into_iter().collect::<Vec<u16>>();
|
||||
assert_eq!(expected_range, result);
|
||||
// overlapping and adjacent
|
||||
let input = &[(10u16, 20u16), (15u16, 25u16), (26u16, 30u16)];
|
||||
let result = generate_sorted_from_ranges_random(input);
|
||||
let expected = expected_ports_from_ranges(input);
|
||||
assert_eq!(expected, result);
|
||||
|
||||
let result = generate_sorted_range(1, 65_535);
|
||||
let expected_range = (1..65_535).into_iter().collect::<Vec<u16>>();
|
||||
assert_eq!(expected_range, result);
|
||||
// near-full domain (heavy): we only assert lengths & equality
|
||||
let input = &[(1u16, 65_535u16)];
|
||||
let result = generate_sorted_from_ranges_random(input);
|
||||
let expected = expected_ports_from_ranges(input);
|
||||
assert_eq!(expected.len(), result.len());
|
||||
assert_eq!(expected, result);
|
||||
|
||||
let result = generate_sorted_range(1000, 2000);
|
||||
let expected_range = (1000..2000).into_iter().collect::<Vec<u16>>();
|
||||
assert_eq!(expected_range, result);
|
||||
// multiple disjoint ranges - check dedupe & coverage
|
||||
let input = &[(50u16, 100u16), (1000u16, 2000u16), (30000u16, 30010u16)];
|
||||
let result = generate_sorted_from_ranges_random(input);
|
||||
let set_len = result.iter().copied().collect::<HashSet<u16>>().len();
|
||||
assert_eq!(set_len, result.len());
|
||||
let expected = expected_ports_from_ranges(input);
|
||||
assert_eq!(expected, result);
|
||||
}
|
||||
|
||||
fn generate_sorted_range(start: u32, end: u32) -> Vec<u16> {
|
||||
let range = RangeIterator::new(start, end);
|
||||
let mut result = range.into_iter().collect::<Vec<u16>>();
|
||||
result.sort();
|
||||
#[test]
|
||||
fn serial_range_iterator_test() {
|
||||
// serial should preserve input-order semantics but here we only assert
|
||||
// coverage (no duplicates) by sorting results and comparing expected set.
|
||||
|
||||
result
|
||||
// small disjoint ranges
|
||||
let input = &[(1u16, 10u16), (20u16, 30u16), (100u16, 110u16)];
|
||||
let result = generate_sorted_from_ranges_serial(input);
|
||||
let expected = expected_ports_from_ranges(input);
|
||||
assert_eq!(expected, result);
|
||||
|
||||
// overlapping and adjacent
|
||||
let input = &[(10u16, 20u16), (15u16, 25u16), (26u16, 30u16)];
|
||||
let result = generate_sorted_from_ranges_serial(input);
|
||||
let expected = expected_ports_from_ranges(input);
|
||||
assert_eq!(expected, result);
|
||||
|
||||
// multiple disjoint ranges
|
||||
let input = &[(50u16, 100u16), (1000u16, 2000u16), (30000u16, 30010u16)];
|
||||
let result = generate_sorted_from_ranges_serial(input);
|
||||
let set_len = result.iter().copied().collect::<HashSet<u16>>().len();
|
||||
assert_eq!(set_len, result.len());
|
||||
let expected = expected_ports_from_ranges(input);
|
||||
assert_eq!(expected, result);
|
||||
|
||||
// all possible inputs
|
||||
let input = &[(u16::MIN, u16::MAX)];
|
||||
let result = generate_sorted_from_ranges_serial(input);
|
||||
let set_len = result.iter().copied().collect::<HashSet<u16>>().len();
|
||||
assert_eq!(set_len, result.len());
|
||||
let expected = expected_ports_from_ranges(input);
|
||||
assert_eq!(expected, result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
use std::{collections::HashSet, fmt, io, net::IpAddr};
|
||||
|
||||
/// Keep the OS error and its target intact until diagnostics need formatting.
|
||||
#[derive(Debug)]
|
||||
struct ScanError {
|
||||
ip: IpAddr,
|
||||
error: io::Error,
|
||||
}
|
||||
|
||||
impl fmt::Display for ScanError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{} {}", self.error, self.ip)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ScanError {}
|
||||
|
||||
/// Preserve the normal I/O result layout; only diagnostics need target context.
|
||||
pub(super) fn diagnostic_error(error: io::Error, ip: IpAddr, enabled: bool) -> io::Error {
|
||||
if enabled {
|
||||
io::Error::other(ScanError { ip, error })
|
||||
} else {
|
||||
error
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct ScanErrors {
|
||||
enabled: bool,
|
||||
limit: usize,
|
||||
messages: HashSet<String>,
|
||||
}
|
||||
|
||||
impl ScanErrors {
|
||||
pub(super) fn new(enabled: bool, limit: usize) -> Self {
|
||||
Self {
|
||||
enabled,
|
||||
limit,
|
||||
messages: HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn record(&mut self, failure: io::Error) {
|
||||
if self.enabled && self.messages.len() < self.limit {
|
||||
self.messages.insert(failure.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn messages(&self) -> &HashSet<String> {
|
||||
&self.messages
|
||||
}
|
||||
}
|
||||
|
||||
/// Detect descriptor exhaustion without allocating or interpreting OS messages.
|
||||
#[cfg(unix)]
|
||||
pub(super) fn is_descriptor_exhaustion(error: &io::Error) -> bool {
|
||||
matches!(error.raw_os_error(), Some(libc::EMFILE | libc::ENFILE))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub(super) fn is_descriptor_exhaustion(error: &io::Error) -> bool {
|
||||
error.raw_os_error() == Some(windows_sys::Win32::Networking::WinSock::WSAEMFILE)
|
||||
}
|
||||
|
||||
// Retain the previous fallback for targets without Unix or Winsock error codes.
|
||||
#[cfg(not(any(unix, windows)))]
|
||||
pub(super) fn is_descriptor_exhaustion(error: &io::Error) -> bool {
|
||||
error
|
||||
.to_string()
|
||||
.to_lowercase()
|
||||
.contains("too many open files")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::{fmt, net::SocketAddr};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MustNotFormat;
|
||||
|
||||
impl fmt::Display for MustNotFormat {
|
||||
fn fmt(&self, _: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
panic!("discarded diagnostics must not format errors")
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for MustNotFormat {}
|
||||
|
||||
fn failure(error: io::Error) -> io::Error {
|
||||
diagnostic_error(error, "127.0.0.1".parse().unwrap(), true)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabled_diagnostics_do_not_format_errors() {
|
||||
let mut errors = ScanErrors::new(false, 1000);
|
||||
errors.record(diagnostic_error(
|
||||
io::Error::other(MustNotFormat),
|
||||
"127.0.0.1".parse().unwrap(),
|
||||
false,
|
||||
));
|
||||
assert!(errors.messages().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabled_diagnostics_preserve_the_original_os_error() {
|
||||
let original = io::Error::from_raw_os_error(123);
|
||||
let error = diagnostic_error(original, "127.0.0.1".parse().unwrap(), false);
|
||||
assert_eq!(error.raw_os_error(), Some(123));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_diagnostics_do_not_format_more_errors() {
|
||||
let mut errors = ScanErrors::new(true, 1);
|
||||
errors.record(failure(io::ErrorKind::ConnectionRefused.into()));
|
||||
errors.record(failure(io::Error::other(MustNotFormat)));
|
||||
assert_eq!(errors.messages().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enabled_diagnostics_keep_error_details_and_deduplicate_per_ip() {
|
||||
let mut errors = ScanErrors::new(true, 1000);
|
||||
for port in [80, 443] {
|
||||
errors.record(diagnostic_error(
|
||||
io::Error::other("test socket error"),
|
||||
SocketAddr::from(([127, 0, 0, 1], port)).ip(),
|
||||
true,
|
||||
));
|
||||
}
|
||||
assert_eq!(errors.messages().len(), 1);
|
||||
assert!(errors.messages().contains("test socket error 127.0.0.1"));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn detects_unix_descriptor_limits_by_code() {
|
||||
for code in [libc::EMFILE, libc::ENFILE] {
|
||||
assert!(is_descriptor_exhaustion(&io::Error::from_raw_os_error(
|
||||
code
|
||||
)));
|
||||
}
|
||||
assert!(!is_descriptor_exhaustion(&io::Error::from_raw_os_error(
|
||||
libc::ECONNREFUSED
|
||||
)));
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn detects_winsock_descriptor_limits_by_code() {
|
||||
use windows_sys::Win32::Networking::WinSock::{WSAECONNREFUSED, WSAEMFILE};
|
||||
assert!(is_descriptor_exhaustion(&io::Error::from_raw_os_error(
|
||||
WSAEMFILE
|
||||
)));
|
||||
assert!(!is_descriptor_exhaustion(&io::Error::from_raw_os_error(
|
||||
WSAECONNREFUSED
|
||||
)));
|
||||
}
|
||||
|
||||
#[cfg(any(unix, windows))]
|
||||
#[test]
|
||||
fn descriptor_check_never_formats_an_error() {
|
||||
assert!(!is_descriptor_exhaustion(&io::Error::other(MustNotFormat)));
|
||||
}
|
||||
}
|
||||
@@ -1,193 +1,778 @@
|
||||
use super::PortStrategy;
|
||||
//! Core functionality for actual scanning behaviour.
|
||||
use crate::generated::get_parsed_data;
|
||||
use crate::port_strategy::PortStrategy;
|
||||
use crate::tui::println_safe;
|
||||
use log::debug;
|
||||
|
||||
use async_std::io;
|
||||
use async_std::net::TcpStream;
|
||||
use async_std::prelude::*;
|
||||
use colored::*;
|
||||
use futures::stream::FuturesUnordered;
|
||||
use std::time::Duration;
|
||||
mod socket_iterator;
|
||||
use socket_iterator::SocketIterator;
|
||||
|
||||
mod errors;
|
||||
use errors::{diagnostic_error, is_descriptor_exhaustion, ScanErrors};
|
||||
|
||||
use colored::Colorize;
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use std::collections::BTreeMap;
|
||||
use std::future::poll_fn;
|
||||
use std::task::Poll;
|
||||
use std::{
|
||||
io::ErrorKind,
|
||||
net::{IpAddr, Shutdown, SocketAddr},
|
||||
collections::HashMap,
|
||||
io,
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr, Shutdown, SocketAddr},
|
||||
num::NonZeroU8,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::io::Interest;
|
||||
use tokio::net::{TcpStream, UdpSocket};
|
||||
use tokio::time::{sleep, timeout};
|
||||
|
||||
/// How many sockets the scan starts or finishes (counted together) between
|
||||
/// two polls of the runtime's I/O driver; see `Scanner::scan_sockets`.
|
||||
///
|
||||
/// Every round ends with a non-blocking poll of the OS selector, so this
|
||||
/// keeps the overhead far below 1% while bounding the time between two
|
||||
/// polls to about a millisecond on Linux.
|
||||
const WORK_PER_TURN: usize = 128;
|
||||
|
||||
/// Keeps the generated order, including duplicate ports, while removing exclusions.
|
||||
// Keep the bitmap out of the async polling frame, which runs on every wake.
|
||||
#[inline(never)]
|
||||
fn filter_excluded_ports(mut ports: Vec<u16>, excluded: &[u16]) -> Vec<u16> {
|
||||
if excluded.is_empty() {
|
||||
return ports;
|
||||
}
|
||||
|
||||
// Bitmap setup costs more than membership checks on short port lists.
|
||||
if ports.len() < 64 {
|
||||
ports.retain(|port| !excluded.contains(port));
|
||||
return ports;
|
||||
}
|
||||
|
||||
// The complete u16 port space fits in an 8 KiB bitmap.
|
||||
let mut excluded_bits = [0_u64; 1024];
|
||||
for &port in excluded {
|
||||
excluded_bits[usize::from(port) / 64] |= 1 << (port % 64);
|
||||
}
|
||||
ports.retain(|&port| excluded_bits[usize::from(port) / 64] & (1 << (port % 64)) == 0);
|
||||
ports
|
||||
}
|
||||
|
||||
/// UDP payload lookup: port -> payload bytes
|
||||
///
|
||||
/// `get_parsed_data()` returns a `&'static BTreeMap<...>`, so we can store
|
||||
/// references to the payload bytes without cloning them.
|
||||
#[doc(hidden)]
|
||||
pub type UdpPayloadLookup = HashMap<u16, &'static [u8]>;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn build_udp_payload_lookup(udp_map: &'static BTreeMap<Vec<u16>, Vec<u8>>) -> UdpPayloadLookup {
|
||||
let mut lookup: UdpPayloadLookup = HashMap::new();
|
||||
|
||||
for (ports, payload_vec) in udp_map.iter() {
|
||||
let payload: &'static [u8] = payload_vec.as_slice();
|
||||
for &port in ports.iter() {
|
||||
// Preserve existing behavior: if duplicates exist, last insert wins.
|
||||
lookup.insert(port, payload);
|
||||
}
|
||||
}
|
||||
|
||||
lookup
|
||||
}
|
||||
|
||||
/// The class for the scanner
|
||||
/// Host is data type IpAddr and is the host address
|
||||
/// IP is data type IpAddr and is the IP address
|
||||
/// start & end is where the port scan starts and ends
|
||||
/// batch_size is how many ports at a time should be scanned
|
||||
/// Timeout is the time RustScan should wait before declaring a port closed. As datatype Duration.
|
||||
/// Quiet is whether or not RustScan should print things, or wait until the end to print only open ports.
|
||||
/// greppable is whether or not RustScan should print things, or wait until the end to print only the ip and open ports.
|
||||
///
|
||||
/// # Runtime
|
||||
///
|
||||
/// The futures returned by [`Scanner::run`] and [`Scanner::run_with_status`]
|
||||
/// use Tokio sockets and timers, so they must be polled from within a
|
||||
/// [Tokio](https://docs.rs/tokio) runtime that has both the I/O and the time
|
||||
/// drivers enabled (for example `#[tokio::main]`, or a runtime built with
|
||||
/// `enable_all()`). Polling them from another executor panics.
|
||||
///
|
||||
/// The scan never spawns tasks: every socket is driven from the one future
|
||||
/// you await, so a current-thread runtime is enough (it is what the
|
||||
/// `rustscan` binary uses), and the future is `Send` if you prefer to spawn
|
||||
/// it on a multi-threaded runtime.
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
#[derive(Debug)]
|
||||
pub struct Scanner {
|
||||
hosts: Vec<IpAddr>,
|
||||
batch_size: u16,
|
||||
ips: Vec<IpAddr>,
|
||||
batch_size: usize,
|
||||
timeout: Duration,
|
||||
quiet: bool,
|
||||
tries: NonZeroU8,
|
||||
greppable: bool,
|
||||
port_strategy: PortStrategy,
|
||||
accessible: bool,
|
||||
exclude_ports: Vec<u16>,
|
||||
udp: bool,
|
||||
print_open_ports: bool,
|
||||
report_closed: bool,
|
||||
interval: Duration,
|
||||
}
|
||||
|
||||
/// The outcome for a single socket, as returned by [`Scanner::run_with_status`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PortStatus {
|
||||
/// The TCP connection succeeded, or the UDP target answered.
|
||||
Open(SocketAddr),
|
||||
/// The target actively refused the TCP connection (for example with a
|
||||
/// RST). Only reported when [`Scanner::with_closed_ports`] is enabled.
|
||||
Closed(SocketAddr),
|
||||
}
|
||||
|
||||
// Allowing too many arguments for clippy.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
impl Scanner {
|
||||
pub fn new(
|
||||
hosts: &[IpAddr],
|
||||
batch_size: u16,
|
||||
ips: &[IpAddr],
|
||||
batch_size: usize,
|
||||
timeout: Duration,
|
||||
quiet: bool,
|
||||
tries: u8,
|
||||
greppable: bool,
|
||||
port_strategy: PortStrategy,
|
||||
accessible: bool,
|
||||
exclude_ports: Vec<u16>,
|
||||
udp: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
batch_size,
|
||||
timeout,
|
||||
quiet,
|
||||
tries: NonZeroU8::new(std::cmp::max(tries, 1)).unwrap(),
|
||||
greppable,
|
||||
port_strategy,
|
||||
hosts: hosts.iter().map(|host| host.to_owned()).collect(),
|
||||
ips: ips.iter().map(ToOwned::to_owned).collect(),
|
||||
accessible,
|
||||
exclude_ports,
|
||||
udp,
|
||||
print_open_ports: false,
|
||||
report_closed: false,
|
||||
interval: Duration::ZERO,
|
||||
}
|
||||
}
|
||||
|
||||
/// Enables the CLI's incremental open-port output.
|
||||
///
|
||||
/// Library callers are quiet by default and can inspect the sockets returned by [`Self::run`].
|
||||
#[must_use]
|
||||
pub fn with_open_port_output(mut self) -> Self {
|
||||
self.print_open_ports = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Also reports TCP ports that actively refuse the connection, as
|
||||
/// [`PortStatus::Closed`] from [`Self::run_with_status`].
|
||||
///
|
||||
/// Ports that time out are still treated as filtered and are not reported.
|
||||
/// UDP scans never report closed ports.
|
||||
#[must_use]
|
||||
pub fn with_closed_ports(mut self) -> Self {
|
||||
self.report_closed = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Waits `interval` after scanning one port (on every address) before
|
||||
/// scanning the next port, for slow, low-noise scans.
|
||||
///
|
||||
/// Within a port, up to `batch_size` addresses are still scanned
|
||||
/// concurrently. A zero interval (the default) scans all sockets in
|
||||
/// batches without any delay.
|
||||
#[must_use]
|
||||
pub fn with_interval(mut self, interval: Duration) -> Self {
|
||||
self.interval = interval;
|
||||
self
|
||||
}
|
||||
|
||||
/// Runs scan_range with chunk sizes
|
||||
/// If you want to run RustScan normally, this is the entry point used
|
||||
/// Returns all open ports as Vec<u16>
|
||||
/// Returns all open sockets.
|
||||
pub async fn run(&self) -> Vec<SocketAddr> {
|
||||
let ports: Vec<u16> = self.port_strategy.order();
|
||||
let batch_per_host: usize = self.batch_size as usize / self.hosts.len();
|
||||
let mut open_sockets: Vec<SocketAddr> = Vec::new();
|
||||
|
||||
for batch in ports.chunks(batch_per_host) {
|
||||
let mut sockets = self.scan_ports(batch).await;
|
||||
open_sockets.append(&mut sockets);
|
||||
}
|
||||
|
||||
open_sockets
|
||||
self.run_with_status()
|
||||
.await
|
||||
.into_iter()
|
||||
.filter_map(|status| match status {
|
||||
PortStatus::Open(socket) => Some(socket),
|
||||
PortStatus::Closed(_) => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Given a slice of sockets, scan them all.
|
||||
/// Returns a vector of open sockets.
|
||||
async fn scan_ports(&self, ports: &[u16]) -> Vec<SocketAddr> {
|
||||
/// Like [`Self::run`], but returns the status of every socket that gave a
|
||||
/// definitive answer: open sockets, plus closed sockets when
|
||||
/// [`Self::with_closed_ports`] is enabled.
|
||||
pub async fn run_with_status(&self) -> Vec<PortStatus> {
|
||||
// Every in-flight socket is polled from this single future through a
|
||||
// `FuturesUnordered`. Under Tokio's cooperative budget the future
|
||||
// would be forced to yield after ~128 sockets made progress, and the
|
||||
// `FuturesUnordered` would then re-poll every other ready socket just
|
||||
// to have it return `Pending` again. Opt out; `scan_sockets` yields to
|
||||
// the runtime by itself, in rounds of `WORK_PER_TURN` sockets.
|
||||
tokio::task::unconstrained(self.scan()).await
|
||||
}
|
||||
|
||||
async fn scan(&self) -> Vec<PortStatus> {
|
||||
let ports = filter_excluded_ports(self.port_strategy.order(), &self.exclude_ports);
|
||||
let mut found_sockets: Vec<PortStatus> = Vec::new();
|
||||
let mut errors =
|
||||
ScanErrors::new(log::log_enabled!(log::Level::Debug), self.ips.len() * 1000);
|
||||
|
||||
// Build UDP payload lookup once (only if we are scanning UDP).
|
||||
// This avoids cloning a big map into every spawned future and turns
|
||||
// payload selection from O(n) to O(1).
|
||||
let udp_payloads: Option<Arc<UdpPayloadLookup>> = if self.udp {
|
||||
Some(Arc::new(build_udp_payload_lookup(get_parsed_data())))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
debug!("Start scanning sockets. \nBatch size {}\nNumber of ip-s {}\nNumber of ports {}\nTargets all together {}\nInterval between ports {:?}",
|
||||
self.batch_size,
|
||||
self.ips.len(),
|
||||
ports.len(),
|
||||
(self.ips.len() * ports.len()),
|
||||
self.interval);
|
||||
|
||||
if self.interval.is_zero() {
|
||||
let sockets = SocketIterator::new(&self.ips, &ports);
|
||||
self.scan_sockets(sockets, &udp_payloads, &mut found_sockets, &mut errors)
|
||||
.await;
|
||||
} else {
|
||||
// Scan one port (on every address) at a time and wait `interval`
|
||||
// before moving on to the next port.
|
||||
for (i, port) in ports.iter().enumerate() {
|
||||
if i > 0 {
|
||||
sleep(self.interval).await;
|
||||
}
|
||||
let sockets = SocketIterator::new(&self.ips, std::slice::from_ref(port));
|
||||
self.scan_sockets(sockets, &udp_payloads, &mut found_sockets, &mut errors)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
debug!("Typical socket connection errors {:?}", errors.messages());
|
||||
debug!("Sockets found: {:?}", found_sockets);
|
||||
found_sockets
|
||||
}
|
||||
|
||||
/// Scans every socket yielded by `sockets`, keeping at most `batch_size`
|
||||
/// connection attempts in flight.
|
||||
///
|
||||
/// Starts sockets and handles finished ones in rounds of at most
|
||||
/// [`WORK_PER_TURN`] of either, and lets the runtime poll for I/O and fire
|
||||
/// timers between rounds. One poll of the scan future otherwise lasts
|
||||
/// until no socket is ready, which can take long: starting a whole large
|
||||
/// batch at once, or a run of UDP probes that all finish straight away.
|
||||
/// Sockets that finish in the meantime are only noticed once the poll
|
||||
/// ends, so their results come late, and if the poll outlasts the timeout
|
||||
/// their timers fire before their answers are seen.
|
||||
async fn scan_sockets(
|
||||
&self,
|
||||
mut sockets: SocketIterator<'_>,
|
||||
udp_payloads: &Option<Arc<UdpPayloadLookup>>,
|
||||
found_sockets: &mut Vec<PortStatus>,
|
||||
errors: &mut ScanErrors,
|
||||
) {
|
||||
let mut ftrs = FuturesUnordered::new();
|
||||
for port in ports {
|
||||
for host in &self.hosts {
|
||||
ftrs.push(self.scan_socket(SocketAddr::new(*host, *port)));
|
||||
|
||||
loop {
|
||||
let mut work = 0;
|
||||
while work < WORK_PER_TURN {
|
||||
let mut started = false;
|
||||
if ftrs.len() < self.batch_size {
|
||||
if let Some(socket) = sockets.next() {
|
||||
ftrs.push(self.scan_socket(socket, udp_payloads.clone()));
|
||||
started = true;
|
||||
work += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Poll once without waiting; this starts the new socket.
|
||||
match poll_fn(|cx| Poll::Ready(ftrs.poll_next_unpin(cx))).await {
|
||||
Poll::Ready(Some(result)) => {
|
||||
self.record(result, found_sockets, errors);
|
||||
work += 1;
|
||||
}
|
||||
// Nothing in flight and nothing left to start.
|
||||
Poll::Ready(None) => return,
|
||||
// Nothing has finished; keep starting sockets while there is room.
|
||||
Poll::Pending if started => {}
|
||||
Poll::Pending => break,
|
||||
}
|
||||
}
|
||||
|
||||
if work >= WORK_PER_TURN {
|
||||
tokio::task::yield_now().await;
|
||||
} else {
|
||||
// The batch is full (or complete) and every socket in it is
|
||||
// waiting on the network.
|
||||
match ftrs.next().await {
|
||||
Some(result) => self.record(result, found_sockets, errors),
|
||||
None => return,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut open_sockets: Vec<SocketAddr> = Vec::new();
|
||||
while let Some(result) = ftrs.next().await {
|
||||
match result {
|
||||
Ok(socket) => open_sockets.push(socket),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
open_sockets
|
||||
}
|
||||
|
||||
/// Given a port, scan it.
|
||||
/// Records the outcome of one socket.
|
||||
fn record(
|
||||
&self,
|
||||
result: io::Result<PortStatus>,
|
||||
found_sockets: &mut Vec<PortStatus>,
|
||||
errors: &mut ScanErrors,
|
||||
) {
|
||||
match result {
|
||||
Ok(status) => found_sockets.push(status),
|
||||
Err(error) => errors.record(error),
|
||||
}
|
||||
}
|
||||
|
||||
/// Given a socket, scan it self.tries times.
|
||||
/// Turns the address into a SocketAddr
|
||||
/// Deals with the <result> type
|
||||
/// If it experiences error ErrorKind::Other then too many files are open and it Panics!
|
||||
/// ese any other error, it returns the error in Result as a string
|
||||
/// If no errors occur, it returns the port number in Result to signify the port is open.
|
||||
/// Deals with the `<result>` type
|
||||
/// Panics if the OS reports descriptor exhaustion.
|
||||
/// Other failures retain their I/O error, with target context for debug diagnostics.
|
||||
/// If no errors occur, it returns the port number in Result to signify the port is open.
|
||||
/// This function mainly deals with the logic of Results handling.
|
||||
/// # Example
|
||||
///
|
||||
/// self.scan_port(10:u16)
|
||||
/// ```compile_fail
|
||||
/// scanner.scan_socket(socket)
|
||||
/// ```
|
||||
///
|
||||
/// Note: `self` must contain `self.host`.
|
||||
async fn scan_socket(&self, socket: SocketAddr) -> io::Result<SocketAddr> {
|
||||
match self.connect(socket).await {
|
||||
Ok(x) => {
|
||||
// match stream_result.shutdown(Shutdown::Both)
|
||||
info!("Shutting down stream");
|
||||
match x.shutdown(Shutdown::Both) {
|
||||
_ => {}
|
||||
}
|
||||
if !self.quiet {
|
||||
println!("Open {}", socket.to_string().purple());
|
||||
}
|
||||
|
||||
Ok(socket)
|
||||
}
|
||||
Err(e) => match e.kind() {
|
||||
ErrorKind::Other => {
|
||||
panic!("Too many open files. Please reduce batch size. The default is 5000. Try -b 2500.");
|
||||
}
|
||||
_ => Err(io::Error::new(io::ErrorKind::Other, e.to_string())),
|
||||
},
|
||||
/// Note: `self` must contain `self.ip`.
|
||||
async fn scan_socket(
|
||||
&self,
|
||||
socket: SocketAddr,
|
||||
udp_payloads: Option<Arc<UdpPayloadLookup>>,
|
||||
) -> io::Result<PortStatus> {
|
||||
if self.udp {
|
||||
return self.scan_udp_socket(socket, udp_payloads).await;
|
||||
}
|
||||
|
||||
let tries = self.tries.get();
|
||||
for nr_try in 1..=tries {
|
||||
match self.connect(socket).await {
|
||||
Ok(tcp_stream) => {
|
||||
debug!("Connection was successful, shutting down stream {}", socket);
|
||||
if let Err(e) = shutdown_both(tcp_stream) {
|
||||
debug!("Shutdown stream error {}", e);
|
||||
}
|
||||
self.fmt_ports(socket);
|
||||
|
||||
debug!("Return Ok after {nr_try} tries");
|
||||
return Ok(PortStatus::Open(socket));
|
||||
}
|
||||
Err(e) => {
|
||||
// A refused connection is a definitive answer, so there is
|
||||
// no point in retrying it.
|
||||
if self.report_closed && e.kind() == io::ErrorKind::ConnectionRefused {
|
||||
self.fmt_closed_port(socket);
|
||||
return Ok(PortStatus::Closed(socket));
|
||||
}
|
||||
|
||||
assert!(!is_descriptor_exhaustion(&e), "Too many open files. Please reduce batch size. The default is 5000. Try -b 2500.");
|
||||
|
||||
if nr_try == tries {
|
||||
return Err(diagnostic_error(
|
||||
e,
|
||||
socket.ip(),
|
||||
log::log_enabled!(log::Level::Debug),
|
||||
));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
unreachable!();
|
||||
}
|
||||
|
||||
async fn scan_udp_socket(
|
||||
&self,
|
||||
socket: SocketAddr,
|
||||
udp_payloads: Option<Arc<UdpPayloadLookup>>,
|
||||
) -> io::Result<PortStatus> {
|
||||
let payload: &[u8] = udp_payloads
|
||||
.as_ref()
|
||||
.and_then(|m| m.get(&socket.port()).copied())
|
||||
.unwrap_or(b"");
|
||||
|
||||
let tries = self.tries.get();
|
||||
for _ in 1..=tries {
|
||||
match self.udp_scan(socket, payload, self.timeout).await {
|
||||
Ok(true) => return Ok(PortStatus::Open(socket)),
|
||||
Ok(false) => continue,
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
Err(io::Error::other(format!(
|
||||
"UDP scan timed-out for all tries on socket {socket}"
|
||||
)))
|
||||
}
|
||||
|
||||
/// Performs the connection to the socket with timeout
|
||||
/// # Example
|
||||
///
|
||||
/// let port: u16 = 80
|
||||
/// // Host is an IpAddr type
|
||||
/// let host = IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1));
|
||||
/// let socket = SocketAddr::new(host, port);
|
||||
/// self.connect(socket)
|
||||
/// // returns Result which is either Ok(stream) for port is open, or Er for port is closed.
|
||||
/// // Timeout occurs after self.timeout seconds
|
||||
/// ```compile_fail
|
||||
/// # use std::net::{IpAddr, Ipv6Addr, SocketAddr};
|
||||
/// let port: u16 = 80;
|
||||
/// // ip is an IpAddr type
|
||||
/// let ip = IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1));
|
||||
/// let socket = SocketAddr::new(ip, port);
|
||||
/// scanner.connect(socket);
|
||||
/// // returns Result which is either Ok(stream) for port is open, or Er for port is closed.
|
||||
/// // Timeout occurs after self.timeout seconds
|
||||
/// ```
|
||||
///
|
||||
async fn connect(&self, socket: SocketAddr) -> io::Result<TcpStream> {
|
||||
let stream = io::timeout(
|
||||
self.timeout,
|
||||
async move { TcpStream::connect(socket).await },
|
||||
)
|
||||
.await?;
|
||||
Ok(stream)
|
||||
timeout(self.timeout, TcpStream::connect(socket))
|
||||
.await
|
||||
.unwrap_or_else(|_elapsed| Err(timed_out()))
|
||||
}
|
||||
|
||||
/// Binds a non-blocking UDP socket on the unspecified address of the
|
||||
/// target's address family, so we can send and receive packets.
|
||||
fn udp_bind(socket: SocketAddr) -> io::Result<std::net::UdpSocket> {
|
||||
let local_addr = match socket {
|
||||
SocketAddr::V4(_) => SocketAddr::from((Ipv4Addr::UNSPECIFIED, 0)),
|
||||
SocketAddr::V6(_) => SocketAddr::from((Ipv6Addr::UNSPECIFIED, 0)),
|
||||
};
|
||||
|
||||
let udp_socket = std::net::UdpSocket::bind(local_addr)?;
|
||||
udp_socket.set_nonblocking(true)?;
|
||||
Ok(udp_socket)
|
||||
}
|
||||
|
||||
/// Performs a UDP scan on the specified socket with a payload and wait duration
|
||||
/// # Example
|
||||
///
|
||||
/// ```compile_fail
|
||||
/// # use std::net::{IpAddr, Ipv6Addr, SocketAddr};
|
||||
/// # use std::time::Duration;
|
||||
/// let port: u16 = 123;
|
||||
/// // ip is an IpAddr type
|
||||
/// let ip = IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1));
|
||||
/// let socket = SocketAddr::new(ip, port);
|
||||
/// let payload = vec![0, 1, 2, 3];
|
||||
/// let wait = Duration::from_secs(1);
|
||||
/// let result = scanner.udp_scan(socket, payload, wait).await;
|
||||
/// // returns Result which is either Ok(true) if response received, or Ok(false) if timed out.
|
||||
/// // Err is returned for other I/O errors.
|
||||
async fn udp_scan(
|
||||
&self,
|
||||
socket: SocketAddr,
|
||||
payload: &[u8],
|
||||
wait: Duration,
|
||||
) -> io::Result<bool> {
|
||||
let udp_socket = match Self::udp_bind(socket) {
|
||||
Ok(udp_socket) => udp_socket,
|
||||
Err(e) => {
|
||||
debug!("Error binding UDP socket: {e:?}");
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
let mut buf = [0u8; 1024];
|
||||
|
||||
udp_socket.connect(socket)?;
|
||||
|
||||
// Send the probe and try the first receive straight away, as
|
||||
// async-std did. Tokio's readiness-based I/O would first wait for the
|
||||
// reactor to report the socket ready, costing every probe extra trips
|
||||
// through the event loop, while the probe can almost always be sent
|
||||
// immediately and, on the local host, the answer (often an ICMP "port
|
||||
// unreachable", seen as a refused connection) is usually already
|
||||
// there when the send returns. The socket is only registered with
|
||||
// Tokio when we really have to wait.
|
||||
let sent = match udp_socket.send(payload) {
|
||||
Ok(_) => true,
|
||||
Err(e) if e.kind() == io::ErrorKind::WouldBlock => false,
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
let early = if sent {
|
||||
udp_socket.recv(&mut buf)
|
||||
} else {
|
||||
Err(io::ErrorKind::WouldBlock.into())
|
||||
};
|
||||
|
||||
let received = match early {
|
||||
Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
|
||||
let udp_socket = UdpSocket::from_std(udp_socket)?;
|
||||
if !sent {
|
||||
udp_socket.send(payload).await?;
|
||||
}
|
||||
match timeout(wait, recv_or_error(&udp_socket, &mut buf)).await {
|
||||
Ok(received) => received,
|
||||
// Nothing came back in time.
|
||||
Err(_elapsed) => return Ok(false),
|
||||
}
|
||||
}
|
||||
early => early,
|
||||
};
|
||||
|
||||
match received {
|
||||
Ok(size) => {
|
||||
debug!("Received {size} bytes");
|
||||
self.fmt_ports(socket);
|
||||
Ok(true)
|
||||
}
|
||||
Err(e) if e.kind() == io::ErrorKind::TimedOut => Ok(false),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats and prints the port status
|
||||
fn fmt_ports(&self, socket: SocketAddr) {
|
||||
if self.print_open_ports && !self.greppable {
|
||||
if self.accessible {
|
||||
println_safe(format_args!("Open {socket}"));
|
||||
} else {
|
||||
println_safe(format_args!("Open {}", socket.to_string().purple()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Prints a closed port (CLI output only, never in greppable mode).
|
||||
fn fmt_closed_port(&self, socket: SocketAddr) {
|
||||
if self.print_open_ports && !self.greppable {
|
||||
if self.accessible {
|
||||
println_safe(format_args!("Closed {socket}"));
|
||||
} else {
|
||||
println_safe(format_args!("Closed {}", socket.to_string().red()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Shuts down both halves of a connected stream before it is closed, as the
|
||||
/// async-std implementation did (`TcpStream::shutdown(Shutdown::Both)`).
|
||||
///
|
||||
/// Tokio only offers an asynchronous write-side shutdown, so take the socket
|
||||
/// back from the reactor and shut it down synchronously instead.
|
||||
fn shutdown_both(stream: TcpStream) -> io::Result<()> {
|
||||
stream.into_std()?.shutdown(Shutdown::Both)
|
||||
}
|
||||
|
||||
/// Waits for a datagram on a connected UDP socket, or for the error the
|
||||
/// system queued for it: an ICMP "port unreachable" is reported as a refused
|
||||
/// connection, which is how closed UDP ports are told apart from filtered
|
||||
/// ones.
|
||||
///
|
||||
/// `UdpSocket::recv` alone does not do this on Linux: a queued ICMP error
|
||||
/// only raises `EPOLLERR`, which Tokio does not count as readable, so `recv`
|
||||
/// would sleep until the timeout. async-io treated `EPOLLERR` as readable.
|
||||
async fn recv_or_error(socket: &UdpSocket, buf: &mut [u8]) -> io::Result<usize> {
|
||||
loop {
|
||||
let ready = socket.ready(Interest::READABLE | Interest::ERROR).await?;
|
||||
|
||||
if ready.is_readable() {
|
||||
match socket.try_recv(buf) {
|
||||
// A spurious wake-up; wait again (unless the socket is closed
|
||||
// for reading, which would wake us up forever).
|
||||
Err(e) if e.kind() == io::ErrorKind::WouldBlock && !ready.is_read_closed() => {}
|
||||
received => return received,
|
||||
}
|
||||
}
|
||||
|
||||
if ready.is_error() {
|
||||
// Take (and clear) the queued error. `WouldBlock` when there is
|
||||
// none makes Tokio clear the error readiness, so this cannot spin.
|
||||
let queued = socket.try_io(Interest::ERROR, || {
|
||||
socket
|
||||
.take_error()?
|
||||
.map_or_else(|| Err(io::ErrorKind::WouldBlock.into()), Ok)
|
||||
});
|
||||
match queued {
|
||||
Ok(error) => return Err(error),
|
||||
Err(e) if e.kind() == io::ErrorKind::WouldBlock => {}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The error reported for a connection attempt that hit the timeout; the same
|
||||
/// kind and message async-std's `io::timeout` used.
|
||||
fn timed_out() -> io::Error {
|
||||
io::Error::new(io::ErrorKind::TimedOut, "future timed out")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ScanOrder;
|
||||
use async_std::task::block_on;
|
||||
use std::{net::IpAddr, time::Duration};
|
||||
|
||||
#[test]
|
||||
fn scanner_runs() {
|
||||
// Makes sure the program still runs and doesn't panic
|
||||
use crate::input::{PortRanges, ScanOrder};
|
||||
use std::collections::HashSet;
|
||||
|
||||
// These tests never open sockets: they only build a `Scanner` (and its
|
||||
// futures), run a scan that has no socket to scan, or inspect the payload
|
||||
// table generated by build.rs.
|
||||
|
||||
fn test_scanner() -> Scanner {
|
||||
let addrs = vec!["127.0.0.1".parse::<IpAddr>().unwrap()];
|
||||
let strategy = PortStrategy::pick(1, 1_000, ScanOrder::Random);
|
||||
let scanner = Scanner::new(&addrs, 10, Duration::from_millis(100), true, strategy);
|
||||
block_on(scanner.run());
|
||||
// if the scan fails, it wouldn't be able to assert_eq! as it panicked!
|
||||
assert_eq!(1, 1);
|
||||
let strategy = PortStrategy::pick(&Some(PortRanges(vec![(1, 1)])), None, ScanOrder::Serial);
|
||||
Scanner::new(
|
||||
&addrs,
|
||||
1,
|
||||
Duration::from_millis(100),
|
||||
1,
|
||||
false,
|
||||
strategy,
|
||||
false,
|
||||
Vec::new(),
|
||||
false,
|
||||
)
|
||||
}
|
||||
#[test]
|
||||
fn ipv6_scanner_runs() {
|
||||
// Makes sure the program still runs and doesn't panic
|
||||
let addrs = vec!["::1".parse::<IpAddr>().unwrap()];
|
||||
let strategy = PortStrategy::pick(1, 1_000, ScanOrder::Random);
|
||||
let scanner = Scanner::new(&addrs, 10, Duration::from_millis(100), true, strategy);
|
||||
block_on(scanner.run());
|
||||
// if the scan fails, it wouldn't be able to assert_eq! as it panicked!
|
||||
assert_eq!(1, 1);
|
||||
}
|
||||
#[test]
|
||||
fn quad_zero_scanner_runs() {
|
||||
let addrs = vec!["0.0.0.0".parse::<IpAddr>().unwrap()];
|
||||
let strategy = PortStrategy::pick(1, 1_000, ScanOrder::Random);
|
||||
let scanner = Scanner::new(&addrs, 10, Duration::from_millis(100), true, strategy);
|
||||
block_on(scanner.run());
|
||||
assert_eq!(1, 1);
|
||||
}
|
||||
#[test]
|
||||
fn google_dns_runs() {
|
||||
let addrs = vec!["8.8.8.8".parse::<IpAddr>().unwrap()];
|
||||
let strategy = PortStrategy::pick(400, 445, ScanOrder::Random);
|
||||
let scanner = Scanner::new(&addrs, 10, Duration::from_millis(100), true, strategy);
|
||||
block_on(scanner.run());
|
||||
assert_eq!(1, 1);
|
||||
}
|
||||
#[test]
|
||||
fn infer_ulimit_lowering_no_panic() {
|
||||
// Test behaviour on MacOS where ulimit is not automatically lowered
|
||||
let addrs = vec!["8.8.8.8".parse::<IpAddr>().unwrap()];
|
||||
|
||||
// mac should have this automatically scaled down
|
||||
let strategy = PortStrategy::pick(400, 600, ScanOrder::Random);
|
||||
let scanner = Scanner::new(&addrs, 10, Duration::from_millis(100), true, strategy);
|
||||
block_on(scanner.run());
|
||||
assert_eq!(1, 1);
|
||||
#[test]
|
||||
fn port_exclusions_preserve_order_duplicates_and_boundaries() {
|
||||
let inputs = [
|
||||
Vec::new(),
|
||||
vec![65535, 0, 80, 443, 80, 65535, 1],
|
||||
(0..=65535).collect(),
|
||||
PortStrategy::pick(&Some(PortRanges(vec![(0, 1023)])), None, ScanOrder::Random).order(),
|
||||
];
|
||||
let exclusions = [
|
||||
Vec::new(),
|
||||
vec![0],
|
||||
vec![0, 65535, 0, 443],
|
||||
(0..1024).collect(),
|
||||
(0..=65535).collect(),
|
||||
];
|
||||
|
||||
for ports in inputs {
|
||||
for excluded in &exclusions {
|
||||
let excluded_set: HashSet<_> = excluded.iter().copied().collect();
|
||||
let expected: Vec<u16> = ports
|
||||
.iter()
|
||||
.filter(|&port| !excluded_set.contains(port))
|
||||
.copied()
|
||||
.collect();
|
||||
assert_eq!(filter_excluded_ports(ports.clone(), excluded), expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn library_scanner_is_quiet_by_default() {
|
||||
let scanner = test_scanner();
|
||||
|
||||
assert!(!scanner.print_open_ports);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_can_enable_open_port_output() {
|
||||
let scanner = test_scanner().with_open_port_output();
|
||||
|
||||
assert!(scanner.print_open_ports);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closed_ports_are_not_reported_by_default() {
|
||||
assert!(!test_scanner().report_closed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closed_port_reporting_is_opt_in() {
|
||||
assert!(test_scanner().with_closed_ports().report_closed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_interval_by_default() {
|
||||
assert!(test_scanner().interval.is_zero());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_interval_sets_the_delay_between_ports() {
|
||||
let scanner = test_scanner().with_interval(Duration::from_millis(250));
|
||||
|
||||
assert_eq!(scanner.interval, Duration::from_millis(250));
|
||||
}
|
||||
|
||||
/// Embedders may spawn the scan on a multi-threaded runtime, which needs
|
||||
/// `Send` futures. The futures are only created here, never polled, so
|
||||
/// no socket is opened.
|
||||
#[test]
|
||||
fn scan_futures_are_send() {
|
||||
fn assert_send<T: Send>(_: &T) {}
|
||||
|
||||
let scanner = test_scanner();
|
||||
assert_send(&scanner.run());
|
||||
assert_send(&scanner.run_with_status());
|
||||
}
|
||||
|
||||
/// Drives a scan on the same kind of runtime the CLI builds. Every port
|
||||
/// is excluded, so the scan has no socket to open and returns at once.
|
||||
#[test]
|
||||
fn scan_without_sockets_completes_on_a_current_thread_runtime() {
|
||||
let addrs = vec!["127.0.0.1".parse::<IpAddr>().unwrap()];
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_io()
|
||||
.enable_time()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
for interval in [Duration::ZERO, Duration::from_millis(10)] {
|
||||
let scanner = Scanner::new(
|
||||
&addrs,
|
||||
10,
|
||||
Duration::from_millis(100),
|
||||
1,
|
||||
true,
|
||||
PortStrategy::Manual(vec![1, 2]),
|
||||
true,
|
||||
vec![1, 2],
|
||||
false,
|
||||
)
|
||||
.with_interval(interval);
|
||||
|
||||
assert!(runtime.block_on(scanner.run_with_status()).is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timeouts_are_reported_as_timed_out_errors() {
|
||||
let error = timed_out();
|
||||
|
||||
assert_eq!(error.kind(), io::ErrorKind::TimedOut);
|
||||
assert_eq!(error.to_string(), "future timed out");
|
||||
}
|
||||
|
||||
/// Regression test for https://github.com/bee-san/RustScan/issues/933:
|
||||
/// the SNMP public-walk probe must be the exact 33-byte BER packet, with
|
||||
/// the literal `public` community string intact. The old hexdigits-only
|
||||
/// decoding mangled it into a 28-byte probe that agents never answered.
|
||||
#[test]
|
||||
fn udp_snmp_probe_bytes_match_nmap() {
|
||||
let payload = get_parsed_data()
|
||||
.iter()
|
||||
.find(|(ports, _)| ports.contains(&161))
|
||||
.map(|(_, payload)| payload)
|
||||
.expect("no UDP payload registered for port 161");
|
||||
let expected: Vec<u8> = vec![
|
||||
0x30, 0x1f, 0x02, 0x01, 0x00, 0x04, 0x06, b'p', b'u', b'b', b'l', b'i', b'c', 0xa1,
|
||||
0x12, 0x02, 0x01, 0x00, 0x02, 0x01, 0x00, 0x02, 0x01, 0x00, 0x30, 0x07, 0x30, 0x05,
|
||||
0x06, 0x01, 0x00, 0x05, 0x00,
|
||||
];
|
||||
assert_eq!(*payload, expected);
|
||||
}
|
||||
|
||||
/// The SSDP probe mixes `\xNN` escapes, `\"` escapes and literal text
|
||||
/// across two quoted segments: segments must decode and concatenate
|
||||
/// with no separators.
|
||||
#[test]
|
||||
fn udp_ssdp_probe_decodes_escapes_and_literal_text() {
|
||||
let payload = get_parsed_data()
|
||||
.iter()
|
||||
.find(|(ports, _)| ports.contains(&1900))
|
||||
.map(|(_, payload)| payload)
|
||||
.expect("no UDP payload registered for port 1900");
|
||||
let expected =
|
||||
b"M-SEARCH * HTTP/1.1\r\nHost: 239.255.255.250:1900\r\nMan: \"ssdp:discover\"\r\nMX: 5\r\nST: ssdp:all\r\n\r\n"
|
||||
.to_vec();
|
||||
assert_eq!(*payload, expected);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
use itertools::{iproduct, Product};
|
||||
use std::iter::FusedIterator;
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::slice;
|
||||
|
||||
/// An iterator that receives a slice of IPs and ports and returns a
|
||||
/// [`SocketAddr`] for each IP/port pair until all combinations are exhausted.
|
||||
///
|
||||
/// The goal of this iterator is to walk every IP and port combination
|
||||
/// *without* allocating a big intermediate buffer — the alternative would be
|
||||
/// materialising a `Vec<SocketAddr>` of every pair up front.
|
||||
///
|
||||
/// # Ordering
|
||||
///
|
||||
/// The IP/port order is intentionally reversed inside `product_it`: we want
|
||||
/// `iproduct!` to iterate *all IPs for one port* before advancing to the next
|
||||
/// port ("hold the port, go through all the IPs, then advance the port").
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// `SocketIterator` lives in a private module, so this example is not run as
|
||||
/// a doctest; `goes_through_every_ip_port_combination` checks the same order.
|
||||
///
|
||||
/// ```ignore
|
||||
/// # use std::net::IpAddr;
|
||||
/// let ips = [
|
||||
/// "127.0.0.1".parse::<IpAddr>().unwrap(),
|
||||
/// "192.168.0.1".parse::<IpAddr>().unwrap(),
|
||||
/// ];
|
||||
/// let ports = [80u16, 443];
|
||||
///
|
||||
/// let mut it = SocketIterator::new(&ips, &ports);
|
||||
/// assert_eq!(it.next(), Some("127.0.0.1:80".parse().unwrap()));
|
||||
/// assert_eq!(it.next(), Some("192.168.0.1:80".parse().unwrap()));
|
||||
/// assert_eq!(it.next(), Some("127.0.0.1:443".parse().unwrap()));
|
||||
/// assert_eq!(it.next(), Some("192.168.0.1:443".parse().unwrap()));
|
||||
/// assert_eq!(it.next(), None);
|
||||
/// ```
|
||||
#[derive(Clone)]
|
||||
pub struct SocketIterator<'s> {
|
||||
// No boxing: the iterator owns the concrete slice iterators directly.
|
||||
// This keeps `SocketIterator` allocation-free and makes `Clone` free too.
|
||||
product_it: Product<slice::Iter<'s, u16>, slice::Iter<'s, IpAddr>>,
|
||||
}
|
||||
|
||||
impl<'s> SocketIterator<'s> {
|
||||
pub fn new(ips: &'s [IpAddr], ports: &'s [u16]) -> Self {
|
||||
Self {
|
||||
// `iproduct!` calls `.into_iter()` on each argument; `slice::Iter`
|
||||
// is already an iterator, so this is a no-op (no allocation).
|
||||
product_it: iproduct!(ports.iter(), ips.iter()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for SocketIterator<'_> {
|
||||
type Item = SocketAddr;
|
||||
|
||||
/// Returns the next socket, or `None` when all combinations are exhausted.
|
||||
/// Every IP is paired with the same port until the port advances.
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.product_it
|
||||
.next()
|
||||
.map(|(port, ip)| SocketAddr::new(*ip, *port))
|
||||
}
|
||||
|
||||
// Delegate the size hint so callers like `collect()` can pre-size buffers.
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
self.product_it.size_hint()
|
||||
}
|
||||
}
|
||||
|
||||
// `Product` and `slice::Iter` are both fused, so we are too.
|
||||
impl FusedIterator for SocketIterator<'_> {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::SocketIterator;
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
|
||||
#[test]
|
||||
fn goes_through_every_ip_port_combination() {
|
||||
let addrs = vec![
|
||||
"127.0.0.1".parse::<IpAddr>().unwrap(),
|
||||
"192.168.0.1".parse::<IpAddr>().unwrap(),
|
||||
];
|
||||
let ports: Vec<u16> = vec![22, 80, 443];
|
||||
let mut it = SocketIterator::new(&addrs, &ports);
|
||||
|
||||
assert_eq!(Some(SocketAddr::new(addrs[0], ports[0])), it.next());
|
||||
assert_eq!(Some(SocketAddr::new(addrs[1], ports[0])), it.next());
|
||||
assert_eq!(Some(SocketAddr::new(addrs[0], ports[1])), it.next());
|
||||
assert_eq!(Some(SocketAddr::new(addrs[1], ports[1])), it.next());
|
||||
assert_eq!(Some(SocketAddr::new(addrs[0], ports[2])), it.next());
|
||||
assert_eq!(Some(SocketAddr::new(addrs[1], ports[2])), it.next());
|
||||
assert_eq!(None, it.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn size_hint_is_exact() {
|
||||
let addrs = ["127.0.0.1".parse::<IpAddr>().unwrap()];
|
||||
let ports: Vec<u16> = vec![22, 80, 443];
|
||||
let mut it = SocketIterator::new(&addrs, &ports);
|
||||
|
||||
assert_eq!(it.size_hint(), (3, Some(3)));
|
||||
it.next();
|
||||
assert_eq!(it.size_hint(), (2, Some(2)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clone_resumes_independently() {
|
||||
let addrs = [
|
||||
"127.0.0.1".parse::<IpAddr>().unwrap(),
|
||||
"192.168.0.1".parse::<IpAddr>().unwrap(),
|
||||
];
|
||||
let ports: Vec<u16> = vec![22, 80];
|
||||
|
||||
let mut a = SocketIterator::new(&addrs, &ports);
|
||||
assert_eq!(a.next(), Some(SocketAddr::new(addrs[0], ports[0])));
|
||||
let b = a.clone(); // b starts where a is now
|
||||
|
||||
// 2 IPs x 2 ports = 4 sockets, one of which was consumed before cloning.
|
||||
let rest = [
|
||||
SocketAddr::new(addrs[1], ports[0]),
|
||||
SocketAddr::new(addrs[0], ports[1]),
|
||||
SocketAddr::new(addrs[1], ports[1]),
|
||||
];
|
||||
assert_eq!(a.collect::<Vec<_>>(), rest);
|
||||
assert_eq!(b.collect::<Vec<_>>(), rest);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_inputs_yield_nothing() {
|
||||
let addrs = ["127.0.0.1".parse::<IpAddr>().unwrap()];
|
||||
let ports = [22u16];
|
||||
|
||||
for mut it in [
|
||||
SocketIterator::new(&[], &ports),
|
||||
SocketIterator::new(&addrs, &[]),
|
||||
] {
|
||||
assert_eq!(it.size_hint(), (0, Some(0)));
|
||||
assert_eq!(it.next(), None);
|
||||
// Stays exhausted, as promised by the `FusedIterator` impl.
|
||||
assert_eq!(it.next(), None);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,617 @@
|
||||
//! Scripting Engine to run scripts based on tags.
|
||||
//!
|
||||
//! This module serves to filter and run the scripts selected by the user.
|
||||
//!
|
||||
//! A new commandline and configuration file option was added.
|
||||
//!
|
||||
//! ## `--scripts`
|
||||
//!
|
||||
//! ### `default`
|
||||
//!
|
||||
//! This is the default behavior, like as it was from the beginning of RustScan.
|
||||
//!
|
||||
//! The user do not have to chose anything for this. This is the only script
|
||||
//! embedded in RustScan running as default.
|
||||
//!
|
||||
//! ### `none`
|
||||
//!
|
||||
//! The user have to use the `--scripts none` commandline argument or `scripts =
|
||||
//! "none"` in the config file.
|
||||
//!
|
||||
//! None of the scripts will run, this replaces the removed `--no-nmap` option.
|
||||
//!
|
||||
//! ### `custom`
|
||||
//!
|
||||
//! The user have to use the `--scripts custom` commandline argument or
|
||||
//! `scripts = "custom"` in the config file.
|
||||
//!
|
||||
//! RustScan will look for the script configuration file in the user's home
|
||||
//! dir: `home_dir/.rustscan_scripts.toml`
|
||||
//!
|
||||
//! The config file have 3 optional fields: `tag`, `developer` and `port`. Just
|
||||
//! the `tag` field will be used forther in the process.
|
||||
//!
|
||||
//! RustScan will also look for available scripts in the user's home dir:
|
||||
//! `home_dir/.rustscan_scripts` and will try to read all the files, and parse
|
||||
//! them into a vector of [`ScriptFile`].
|
||||
//!
|
||||
//! Filtering on tags means the tags found in the `rustscan_scripts.toml` file
|
||||
//! will also have to be present in the [`ScriptFile`], otherwise the script
|
||||
//! will not be selected.
|
||||
//!
|
||||
//! All of the `rustscan_script.toml` tags have to be present at minimum in a
|
||||
//! [`ScriptFile`] to get selected, but can be also more.
|
||||
//!
|
||||
//! Config file example:
|
||||
//!
|
||||
//! - `fixtures/test_rustscan_scripts.toml`
|
||||
//!
|
||||
//! Metadata-only test fixture:
|
||||
//!
|
||||
//! - `fixtures/.rustscan_scripts/test_script.txt`
|
||||
//!
|
||||
//! `call_format` in script files can be of 2 variants:
|
||||
//!
|
||||
//! One is where all of the possible tags `{{script}}`, `{{ip}}` and `{{port}}`
|
||||
//! are there.
|
||||
//!
|
||||
//! - The `{{script}}` part will be replaced with the scriptfile full path
|
||||
//! gathered while parsing available scripts.
|
||||
//! - The `{{ip}}` part will be replaced with the ip we got from the scan.
|
||||
//! - The `{{port}}` part will be reaplced with the ports separated with the
|
||||
//! `ports_separator` found in the script file
|
||||
//!
|
||||
//! And when there is only `{{ip}}` and `{{port}}` is in the format, only those
|
||||
//! will be replaced with the arguments from the scan.
|
||||
//!
|
||||
//! This makes it easy to run a system installed command like `nmap`, and give
|
||||
//! any kind of arguments to it.
|
||||
//!
|
||||
//! If the format is different, the script will be silently discarded and will
|
||||
//! not run. With the `Debug` option it's possible to see where it goes wrong.
|
||||
|
||||
#![allow(clippy::module_name_repetitions)]
|
||||
|
||||
use crate::input::ScriptsRequired;
|
||||
use anyhow::{anyhow, Result};
|
||||
use log::debug;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::fs::{self, File};
|
||||
use std::io::{self, prelude::*};
|
||||
use std::net::IpAddr;
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::string::ToString;
|
||||
use text_placeholder::Template;
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::process::ExitStatusExt;
|
||||
|
||||
static DEFAULT: &str = r#"tags = ["core_approved", "RustScan", "default"]
|
||||
developer = [ "RustScan", "https://github.com/RustScan" ]
|
||||
ports_separator = ","
|
||||
call_format = "nmap -vvv -p {{port}} -{{ipversion}} {{ip}}"
|
||||
"#;
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
pub fn init_scripts(scripts: &ScriptsRequired) -> Result<Vec<ScriptFile>> {
|
||||
let mut scripts_to_run: Vec<ScriptFile> = Vec::new();
|
||||
|
||||
match scripts {
|
||||
ScriptsRequired::None => {}
|
||||
ScriptsRequired::Default => {
|
||||
let default_script =
|
||||
toml::from_str::<ScriptFile>(DEFAULT).expect("Failed to parse Script file.");
|
||||
scripts_to_run.push(default_script);
|
||||
}
|
||||
ScriptsRequired::Custom => {
|
||||
let script_config = ScriptConfig::read_config()?;
|
||||
debug!("Script config \n{script_config:?}");
|
||||
|
||||
let script_dir_base = if let Some(config_directory) = &script_config.directory {
|
||||
PathBuf::from(config_directory)
|
||||
} else {
|
||||
dirs::home_dir().ok_or_else(|| anyhow!("Could not infer scripts path."))?
|
||||
};
|
||||
|
||||
let script_paths = find_scripts(script_dir_base)?;
|
||||
debug!("Scripts paths \n{script_paths:?}");
|
||||
|
||||
let parsed_scripts = parse_scripts(script_paths);
|
||||
debug!("Scripts parsed \n{parsed_scripts:?}");
|
||||
|
||||
// Only Scripts that contain all the tags found in ScriptConfig will be selected.
|
||||
if let Some(config_hashset) = script_config.tags {
|
||||
for script in parsed_scripts {
|
||||
if let Some(script_hashset) = &script.tags {
|
||||
if script_hashset
|
||||
.iter()
|
||||
.all(|tag| config_hashset.contains(tag))
|
||||
{
|
||||
scripts_to_run.push(script);
|
||||
} else {
|
||||
debug!(
|
||||
"\nScript tags does not match config tags {:?} {}",
|
||||
script_hashset,
|
||||
script.path.unwrap().display()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
debug!("\nScript(s) to run {scripts_to_run:?}");
|
||||
}
|
||||
}
|
||||
|
||||
verify_script_programs(&scripts_to_run)?;
|
||||
|
||||
Ok(scripts_to_run)
|
||||
}
|
||||
|
||||
/// Whether `path/program` is something the shell could run.
|
||||
#[cfg(not(windows))]
|
||||
fn program_exists_in(dir: &std::path::Path, program: &str) -> bool {
|
||||
dir.join(program).is_file()
|
||||
}
|
||||
|
||||
/// Whether `path/program` is something `cmd.exe` could run, which appends
|
||||
/// entries from `PATHEXT` to a bare name.
|
||||
#[cfg(windows)]
|
||||
fn program_exists_in(dir: &std::path::Path, program: &str) -> bool {
|
||||
if dir.join(program).is_file() {
|
||||
return true;
|
||||
}
|
||||
let extensions = std::env::var("PATHEXT").unwrap_or_default();
|
||||
extensions
|
||||
.split(';')
|
||||
.filter(|extension| !extension.is_empty())
|
||||
.any(|extension| dir.join(format!("{program}{extension}")).is_file())
|
||||
}
|
||||
|
||||
fn program_is_available(program: &str) -> bool {
|
||||
match std::env::var_os("PATH") {
|
||||
Some(path) => std::env::split_paths(&path).any(|dir| program_exists_in(&dir, program)),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check that the program each selected script starts with is available.
|
||||
///
|
||||
/// Scripts run through `sh -c`, so a missing binary surfaces only as a bare
|
||||
/// "Exit code = 127" once the scan has already finished. Report it up front
|
||||
/// instead, before a long scan has been wasted.
|
||||
fn verify_script_programs(scripts_to_run: &[ScriptFile]) -> Result<()> {
|
||||
for script in scripts_to_run {
|
||||
let call_format = match &script.call_format {
|
||||
Some(call_format) => call_format,
|
||||
None => continue,
|
||||
};
|
||||
let program = match call_format.split_whitespace().next() {
|
||||
Some(program) => program,
|
||||
None => continue,
|
||||
};
|
||||
// A leading assignment (`FOO=bar nmap ...`) or an absolute path is not
|
||||
// a bare program name, and `{{...}}` placeholders are only filled in
|
||||
// when the script actually runs, so leave those formats alone.
|
||||
if program.contains('=')
|
||||
|| program.contains('/')
|
||||
|| program.contains('$')
|
||||
|| program.contains("{{")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if !program_is_available(program) {
|
||||
return Err(anyhow!(
|
||||
"The script `{}` runs `{}`, which is not installed or not in PATH. \
|
||||
Install it, or skip scripts with `--scripts none`.",
|
||||
script
|
||||
.path
|
||||
.as_ref()
|
||||
.map_or_else(|| "default".to_string(), |path| path.display().to_string()),
|
||||
program
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn parse_scripts(scripts: Vec<PathBuf>) -> Vec<ScriptFile> {
|
||||
let mut parsed_scripts: Vec<ScriptFile> = Vec::with_capacity(scripts.len());
|
||||
for script in scripts {
|
||||
debug!("Parsing script {}", script.display());
|
||||
if let Some(script_file) = ScriptFile::new(script) {
|
||||
parsed_scripts.push(script_file);
|
||||
}
|
||||
}
|
||||
parsed_scripts
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub struct Script {
|
||||
// Path to the script itself.
|
||||
path: Option<PathBuf>,
|
||||
|
||||
// Ip got from scanner.
|
||||
ip: IpAddr,
|
||||
|
||||
// Ports found with portscan.
|
||||
open_ports: Vec<u16>,
|
||||
|
||||
// Port found in ScriptFile, if defined only this will run with the ip.
|
||||
trigger_port: Option<String>,
|
||||
|
||||
// Character to join ports in case we want to use a string format of them, for example nmap -p.
|
||||
ports_separator: Option<String>,
|
||||
|
||||
// Tags found in ScriptFile.
|
||||
tags: Option<Vec<String>>,
|
||||
|
||||
// The format how we want the script to run.
|
||||
call_format: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ExecPartsScript {
|
||||
script: String,
|
||||
ip: String,
|
||||
port: String,
|
||||
ipversion: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ExecParts {
|
||||
ip: String,
|
||||
port: String,
|
||||
ipversion: String,
|
||||
}
|
||||
|
||||
impl Script {
|
||||
pub fn build(
|
||||
path: Option<PathBuf>,
|
||||
ip: IpAddr,
|
||||
open_ports: Vec<u16>,
|
||||
trigger_port: Option<String>,
|
||||
ports_separator: Option<String>,
|
||||
tags: Option<Vec<String>>,
|
||||
call_format: Option<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
path,
|
||||
ip,
|
||||
open_ports,
|
||||
trigger_port,
|
||||
ports_separator,
|
||||
tags,
|
||||
call_format,
|
||||
}
|
||||
}
|
||||
|
||||
// Some variables get changed before read, and compiler throws warning on warn(unused_assignments)
|
||||
#[allow(unused_assignments)]
|
||||
pub fn run(self) -> Result<String> {
|
||||
debug!("run self {:?}", self);
|
||||
|
||||
let separator = self.ports_separator.unwrap_or_else(|| ",".into());
|
||||
|
||||
let mut ports_str = self
|
||||
.open_ports
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<String>>()
|
||||
.join(&separator);
|
||||
if let Some(port) = self.trigger_port {
|
||||
ports_str = port;
|
||||
}
|
||||
|
||||
let mut final_call_format = String::new();
|
||||
if let Some(call_format) = self.call_format {
|
||||
final_call_format = call_format;
|
||||
} else {
|
||||
return Err(anyhow!("Failed to parse execution format."));
|
||||
}
|
||||
let default_template: Template = Template::new(&final_call_format);
|
||||
let mut to_run = String::new();
|
||||
|
||||
if final_call_format.contains("{{script}}") {
|
||||
let exec_parts_script: ExecPartsScript = ExecPartsScript {
|
||||
script: self.path.unwrap().to_str().unwrap().to_string(),
|
||||
ip: self.ip.to_string(),
|
||||
port: ports_str,
|
||||
ipversion: match &self.ip {
|
||||
IpAddr::V4(_) => String::from("4"),
|
||||
IpAddr::V6(_) => String::from("6"),
|
||||
},
|
||||
};
|
||||
to_run = default_template.fill_with_struct(&exec_parts_script)?;
|
||||
} else {
|
||||
let exec_parts: ExecParts = ExecParts {
|
||||
ip: self.ip.to_string(),
|
||||
port: ports_str,
|
||||
ipversion: match &self.ip {
|
||||
IpAddr::V4(_) => String::from("4"),
|
||||
IpAddr::V6(_) => String::from("6"),
|
||||
},
|
||||
};
|
||||
to_run = default_template.fill_with_struct(&exec_parts)?;
|
||||
}
|
||||
debug!("\nScript format to run {to_run}");
|
||||
execute_script(&to_run)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
fn execute_script(script: &str) -> Result<String> {
|
||||
debug!("\nScript arguments {script}");
|
||||
|
||||
let (cmd, arg) = if cfg!(unix) {
|
||||
("sh", "-c")
|
||||
} else {
|
||||
("cmd.exe", "/c")
|
||||
};
|
||||
|
||||
match Command::new(cmd)
|
||||
.args([arg, script])
|
||||
.stdin(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
{
|
||||
Ok(output) => {
|
||||
let status = output.status;
|
||||
|
||||
let es = match status.code() {
|
||||
Some(code) => code,
|
||||
_ => {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
status.signal().unwrap()
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
return Err(anyhow!("Unknown exit status"));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if es != 0 {
|
||||
return Err(anyhow!("Exit code = {}", es));
|
||||
}
|
||||
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
|
||||
}
|
||||
Err(error) => {
|
||||
debug!("Command error {error}",);
|
||||
Err(anyhow!(error.to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_scripts(path: PathBuf) -> Result<Vec<PathBuf>> {
|
||||
if path.is_dir() {
|
||||
debug!("Scripts folder found {}", path.display());
|
||||
let mut files_vec: Vec<PathBuf> = Vec::new();
|
||||
for entry in fs::read_dir(path)? {
|
||||
let entry = entry?;
|
||||
files_vec.push(entry.path());
|
||||
}
|
||||
Ok(files_vec)
|
||||
} else {
|
||||
Err(anyhow!("Can't find scripts folder {}", path.display()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct ScriptFile {
|
||||
pub path: Option<PathBuf>,
|
||||
pub tags: Option<Vec<String>>,
|
||||
pub developer: Option<Vec<String>>,
|
||||
pub port: Option<String>,
|
||||
pub ports_separator: Option<String>,
|
||||
pub call_format: Option<String>,
|
||||
}
|
||||
|
||||
impl ScriptFile {
|
||||
fn new(script: PathBuf) -> Option<ScriptFile> {
|
||||
let real_path = script.clone();
|
||||
let mut lines_buf = String::new();
|
||||
if let Ok(file) = File::open(script) {
|
||||
for mut line in io::BufReader::new(file).lines().skip(1).flatten() {
|
||||
if line.starts_with('#') {
|
||||
line = line[1..].trim().to_string();
|
||||
line.push('\n');
|
||||
lines_buf.push_str(&line);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debug!("Failed to read file: {}", real_path.display());
|
||||
return None;
|
||||
}
|
||||
debug!("ScriptFile {} lines\n{}", real_path.display(), lines_buf);
|
||||
|
||||
match toml::from_str::<ScriptFile>(&lines_buf) {
|
||||
Ok(mut parsed) => {
|
||||
debug!("Parsed ScriptFile{} \n{:?}", real_path.display(), parsed);
|
||||
parsed.path = Some(real_path);
|
||||
// parsed_scripts.push(parsed);
|
||||
Some(parsed)
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("Failed to parse ScriptFile headers {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct ScriptConfig {
|
||||
pub tags: Option<Vec<String>>,
|
||||
pub ports: Option<Vec<String>>,
|
||||
pub developer: Option<Vec<String>>,
|
||||
pub directory: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl ScriptConfig {
|
||||
pub fn read_config() -> Result<ScriptConfig> {
|
||||
let Some(mut home_dir) = dirs::home_dir() else {
|
||||
return Err(anyhow!("Could not infer ScriptConfig path."));
|
||||
};
|
||||
home_dir.push(".rustscan_scripts.toml");
|
||||
|
||||
let content = fs::read_to_string(home_dir)?;
|
||||
let config = toml::from_str::<ScriptConfig>(&content)?;
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn script_headers_preserve_literal_hash_characters() {
|
||||
let fixture =
|
||||
std::env::temp_dir().join(format!("rustscan-literal-hash-{}.txt", std::process::id()));
|
||||
fs::write(&fixture, "#!/usr/bin/env python3\n#tags = [\"C#\"]\n#call_format = \"fixture {{ip}} {{port}} https://example.invalid/#section\"\n\n").unwrap();
|
||||
let parsed = ScriptFile::new(fixture.clone());
|
||||
fs::remove_file(fixture).unwrap();
|
||||
let parsed = parsed.expect("valid TOML metadata");
|
||||
assert_eq!(parsed.tags, Some(vec!["C#".to_owned()]));
|
||||
assert_eq!(
|
||||
parsed.call_format.as_deref(),
|
||||
Some("fixture {{ip}} {{port}} https://example.invalid/#section")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_and_parse_scripts() {
|
||||
let scripts = find_scripts("fixtures/.rustscan_scripts".into()).unwrap();
|
||||
let scripts = parse_scripts(scripts);
|
||||
assert_eq!(scripts.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn find_invalid_folder() {
|
||||
let _scripts = find_scripts("Cargo.toml".into()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn open_script_file_invalid_headers() {
|
||||
ScriptFile::new("fixtures/.rustscan_scripts/test_script_invalid_headers.txt".into())
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn open_nonexisting_script_file() {
|
||||
ScriptFile::new("qwertyuiop.txt".into()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_txt_script() {
|
||||
let script_f =
|
||||
ScriptFile::new("fixtures/.rustscan_scripts/test_script.txt".into()).unwrap();
|
||||
assert_eq!(
|
||||
script_f.tags,
|
||||
Some(vec!["core_approved".to_string(), "example".to_string()])
|
||||
);
|
||||
assert_eq!(
|
||||
script_f.developer,
|
||||
Some(vec![
|
||||
"example".to_string(),
|
||||
"https://example.org".to_string()
|
||||
])
|
||||
);
|
||||
assert_eq!(script_f.ports_separator, Some(",".to_string()));
|
||||
assert_eq!(
|
||||
script_f.call_format,
|
||||
Some("fixture {{ip}} {{port}}".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_directory_config() {
|
||||
// Create test config
|
||||
let config_str = r#"
|
||||
tags = ["core_approved", "example"]
|
||||
directory = "fixtures/.rustscan_scripts"
|
||||
"#;
|
||||
|
||||
let config: ScriptConfig = toml::from_str(config_str).unwrap();
|
||||
assert_eq!(
|
||||
config.directory,
|
||||
Some("fixtures/.rustscan_scripts".to_string())
|
||||
);
|
||||
|
||||
// Test that the directory is actually used
|
||||
let script_dir_base = PathBuf::from(config.directory.unwrap());
|
||||
let scripts = find_scripts(script_dir_base).unwrap();
|
||||
|
||||
// Verify we found the test script
|
||||
assert!(scripts.iter().any(|p| p
|
||||
.file_name()
|
||||
.and_then(|f| f.to_str())
|
||||
.map(|s| s == "test_script.txt")
|
||||
.unwrap_or(false)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_directory_fallback() {
|
||||
let config_str = r#"
|
||||
tags = ["core_approved", "example"]
|
||||
"#;
|
||||
|
||||
let config: ScriptConfig = toml::from_str(config_str).unwrap();
|
||||
assert_eq!(config.directory, None);
|
||||
|
||||
// Test fallback to home directory
|
||||
let script_dir_base = if let Some(config_directory) = &config.directory {
|
||||
PathBuf::from(config_directory)
|
||||
} else {
|
||||
dirs::home_dir().unwrap()
|
||||
};
|
||||
|
||||
assert_eq!(script_dir_base, dirs::home_dir().unwrap());
|
||||
}
|
||||
|
||||
fn script_with_call_format(call_format: &str) -> ScriptFile {
|
||||
ScriptFile {
|
||||
path: Some(PathBuf::from("/tmp/script.txt")),
|
||||
tags: None,
|
||||
developer: None,
|
||||
port: None,
|
||||
ports_separator: None,
|
||||
call_format: Some(call_format.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_script_program_is_reported() {
|
||||
let scripts = vec![script_with_call_format("definitely-not-a-real-binary {{ip}}")];
|
||||
let err = verify_script_programs(&scripts).unwrap_err();
|
||||
assert!(err.to_string().contains("definitely-not-a-real-binary"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn available_script_program_is_accepted() {
|
||||
// `sh` is what the scripts are actually run through, so it is present
|
||||
// on every platform that can execute them.
|
||||
let scripts = vec![script_with_call_format("sh -c true {{ip}}")];
|
||||
assert!(verify_script_programs(&scripts).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn placeholder_and_assignment_prefixes_are_skipped() {
|
||||
let scripts = vec![
|
||||
script_with_call_format("{{script}} {{ip}} {{port}}"),
|
||||
script_with_call_format("FOO=bar definitely-not-a-real-binary"),
|
||||
script_with_call_format("/opt/custom/nmap {{ip}}"),
|
||||
];
|
||||
assert!(verify_script_programs(&scripts).is_ok());
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,56 @@
|
||||
//! Utilities for terminal output during scanning.
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
/// Prints a single line to stdout without ever panicking.
|
||||
///
|
||||
/// `println!` panics when writing to stdout fails, which is fatal under the
|
||||
/// `panic = "abort"` release profile. That includes the `BrokenPipe` error
|
||||
/// produced when a downstream consumer (e.g. `rustscan -g ... | head`)
|
||||
/// closes the pipe, so instead the process exits quietly when that happens.
|
||||
/// Other output errors are reported on stderr before exiting.
|
||||
///
|
||||
/// All user-facing output should go through this function (the `warning!`,
|
||||
/// `detail!`, `output!` and `funny_opening!` macros already do) instead of
|
||||
/// `println!`.
|
||||
pub fn println_safe(args: std::fmt::Arguments<'_>) {
|
||||
match writeln!(std::io::stdout().lock(), "{args}") {
|
||||
Ok(()) => {}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe => {
|
||||
// the reader end of the pipe is gone; nothing left to print to
|
||||
std::process::exit(0);
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = writeln!(std::io::stderr(), "rustscan: failed writing to stdout: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Terminal User Interface Module for RustScan
|
||||
/// Defines macros to use
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! warning {
|
||||
($name:expr) => {
|
||||
use ansi_term::Colour::Red;
|
||||
println!("{} {}", Red.bold().paint("[!]"), $name);
|
||||
$crate::tui::println_safe(format_args!(
|
||||
"{} {}",
|
||||
ansi_term::Colour::Red.bold().paint("[!]"),
|
||||
$name
|
||||
));
|
||||
};
|
||||
// quiet mode
|
||||
($name:expr, $quiet:expr) => {
|
||||
use ansi_term::Colour::Red;
|
||||
if !$quiet {
|
||||
println!("{} {}", Red.bold().paint("[!]"), $name);
|
||||
($name:expr, $greppable:expr, $accessible:expr) => {
|
||||
// if not greppable then print, otherwise no else statement so do not print.
|
||||
if !$greppable {
|
||||
if $accessible {
|
||||
// Don't print the ascii art
|
||||
$crate::tui::println_safe(format_args!("{}", $name));
|
||||
} else {
|
||||
$crate::tui::println_safe(format_args!(
|
||||
"{} {}",
|
||||
ansi_term::Colour::Red.bold().paint("[!]"),
|
||||
$name
|
||||
));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -19,28 +58,52 @@ macro_rules! warning {
|
||||
#[macro_export]
|
||||
macro_rules! detail {
|
||||
($name:expr) => {
|
||||
use ansi_term::Colour::Blue;
|
||||
println!("{} {}", Blue.bold().paint("[~]"), $name);
|
||||
$crate::tui::println_safe(format_args!(
|
||||
"{} {}",
|
||||
ansi_term::Colour::Blue.bold().paint("[~]"),
|
||||
$name
|
||||
));
|
||||
};
|
||||
($name:expr, $quiet:expr) => {
|
||||
use ansi_term::Colour::Blue;
|
||||
if !$quiet {
|
||||
println!("{} {}", Blue.bold().paint("[~]"), $name);
|
||||
($name:expr, $greppable:expr, $accessible:expr) => {
|
||||
// if not greppable then print, otherwise no else statement so do not print.
|
||||
if !$greppable {
|
||||
if $accessible {
|
||||
// Don't print the ascii art
|
||||
$crate::tui::println_safe(format_args!("{}", $name));
|
||||
} else {
|
||||
$crate::tui::println_safe(format_args!(
|
||||
"{} {}",
|
||||
ansi_term::Colour::Blue.bold().paint("[~]"),
|
||||
$name
|
||||
));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! output {
|
||||
($name:expr, $quiet:expr) => {
|
||||
use ansi_term::Colour::RGB;
|
||||
if !$quiet {
|
||||
println!("{} {}", RGB(0, 255, 9).bold().paint("[>]"), $name);
|
||||
}
|
||||
};
|
||||
($name:expr) => {
|
||||
use ansi_term::Colour::RGB;
|
||||
println!("{} {}", RGB(0, 255, 9).bold().paint("[>]"), $name);
|
||||
$crate::tui::println_safe(format_args!(
|
||||
"{} {}",
|
||||
ansi_term::Colour::RGB(0, 255, 9).bold().paint("[>]"),
|
||||
$name
|
||||
));
|
||||
};
|
||||
($name:expr, $greppable:expr, $accessible:expr) => {
|
||||
// if not greppable then print, otherwise no else statement so do not print.
|
||||
if !$greppable {
|
||||
if $accessible {
|
||||
// Don't print the ascii art
|
||||
$crate::tui::println_safe(format_args!("{}", $name));
|
||||
} else {
|
||||
$crate::tui::println_safe(format_args!(
|
||||
"{} {}",
|
||||
ansi_term::Colour::RGB(0, 255, 9).bold().paint("[>]"),
|
||||
$name
|
||||
));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -48,17 +111,39 @@ macro_rules! output {
|
||||
macro_rules! funny_opening {
|
||||
// prints a funny quote / opening
|
||||
() => {
|
||||
use rand::seq::SliceRandom;
|
||||
use rand::seq::IndexedRandom;
|
||||
let quotes = vec![
|
||||
"Nmap? More like slowmap.🐢",
|
||||
"🌍HACK THE PLANET🌍",
|
||||
"Real hackers hack time ⌛",
|
||||
"Please contribute more quotes to our GitHub https://github.com/rustscan/rustscan",
|
||||
"😵 https://admin.tryhackme.com",
|
||||
"0day was here ♥",
|
||||
"I don't always scan ports, but when I do, I prefer RustScan.",
|
||||
"RustScan: Where scanning meets swagging. 😎",
|
||||
"To scan or not to scan? That is the question.",
|
||||
"RustScan: Because guessing isn't hacking.",
|
||||
"Scanning ports like it's my full-time job. Wait, it is.",
|
||||
"Open ports, closed hearts.",
|
||||
"I scanned my computer so many times, it thinks we're dating.",
|
||||
"Port scanning: Making networking exciting since... whenever.",
|
||||
"You miss 100% of the ports you don't scan. - RustScan",
|
||||
"Breaking and entering... into the world of open ports.",
|
||||
"TCP handshake? More like a friendly high-five!",
|
||||
"Scanning ports: The virtual equivalent of knocking on doors.",
|
||||
"RustScan: Making sure 'closed' isn't just a state of mind.",
|
||||
"RustScan: allowing you to send UDP packets into the void 1200x faster than NMAP",
|
||||
"Port scanning: Because every port has a story to tell.",
|
||||
"I scanned ports so fast, even my computer was surprised.",
|
||||
"Scanning ports faster than you can say 'SYN ACK'",
|
||||
"RustScan: Where '404 Not Found' meets '200 OK'.",
|
||||
"RustScan: Exploring the digital landscape, one IP at a time.",
|
||||
"TreadStone was here 🚀",
|
||||
"With RustScan, I scan ports so fast, even my firewall gets whiplash 💨",
|
||||
"Scanning ports so fast, even the internet got a speeding ticket!",
|
||||
];
|
||||
let random_quote = quotes.choose(&mut rand::thread_rng()).unwrap();
|
||||
let random_quote = quotes.choose(&mut rand::rng()).unwrap();
|
||||
|
||||
println!("{}\n", random_quote);
|
||||
// println!("{} {}", RGB(0, 255, 9).bold().paint("[>]"), $name);
|
||||
$crate::tui::println_safe(format_args!("{}\n", random_quote));
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
use rustscan::generated::get_parsed_data;
|
||||
use rustscan::scanner::build_udp_payload_lookup;
|
||||
|
||||
#[test]
|
||||
fn udp_payload_lookup_contains_common_udp_ports() {
|
||||
let udp_map = get_parsed_data();
|
||||
let lookup = build_udp_payload_lookup(udp_map);
|
||||
|
||||
// These are common UDP services; the payload database should include them.
|
||||
assert!(
|
||||
lookup.contains_key(&53),
|
||||
"expected UDP payload for DNS (53)"
|
||||
);
|
||||
assert!(
|
||||
lookup.contains_key(&123),
|
||||
"expected UDP payload for NTP (123)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn udp_payload_lookup_payloads_are_non_empty_for_known_ports() {
|
||||
let udp_map = get_parsed_data();
|
||||
let lookup = build_udp_payload_lookup(udp_map);
|
||||
|
||||
// Don't assert exact bytes (the generated payload set may evolve),
|
||||
// but it should not be empty for these well-known protocols.
|
||||
let dns = lookup.get(&53).expect("missing DNS payload");
|
||||
assert!(!dns.is_empty(), "DNS payload should not be empty");
|
||||
|
||||
let ntp = lookup.get(&123).expect("missing NTP payload");
|
||||
assert!(!ntp.is_empty(), "NTP payload should not be empty");
|
||||
}
|
||||