Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f17a6de63f | ||
|
|
fd34fae8ef | ||
|
|
2400c16ea9 |
No files matched your search
+47
-44
@@ -1,43 +1,76 @@
|
|||||||
{
|
{
|
||||||
"version": "1.0.0b1",
|
"version": "1.0.0b1",
|
||||||
"files": {
|
"files": {
|
||||||
"install.py": "f97f925d07c5589e24332b127a172c7fe1571cf66692d3611cae888553291888",
|
|
||||||
"pyproject.toml": "98da2b2cc6af9c2d5cf6bf1bdd6513585b5c49dfa04c3ca2dc63d0031a5dcea2",
|
|
||||||
"vpsmaster.sh": "7b5ccc2c96e36b4f6d53dd54915bc90416aba9ace06738e0c1913cf887b92972",
|
|
||||||
"proxydoctor.sh": "7b5ccc2c96e36b4f6d53dd54915bc90416aba9ace06738e0c1913cf887b92972",
|
|
||||||
"LICENSE": "07cace8b56becb2cd64969414c59082df9aa7453eeed8caab8378aaff38f6820",
|
|
||||||
".gitattributes": "1cbf58d18ccb72113d6d1bb3996400d557ccc83cda00a11cd63ef5ca38bbf199",
|
".gitattributes": "1cbf58d18ccb72113d6d1bb3996400d557ccc83cda00a11cd63ef5ca38bbf199",
|
||||||
|
".github/CONTRIBUTING.md": "53d976a3571d70cabfa32e98185a905b25a91a1d9694f92502f037503b3d0076",
|
||||||
|
".github/ISSUE_TEMPLATE/bug_report.md": "b5eb7883641a2cad94b1770f47f946aecda05917d0961eb1d8c639ce4da4f64e",
|
||||||
|
".github/ISSUE_TEMPLATE/feature_request.md": "e4efabb0a5961e03344f59985797f4a349989d94ce3bd9edabf5f4d5a656bc68",
|
||||||
|
".github/SECURITY.md": "e13fd5de68bce030755b1c93c47c31e7a91a866d26dcdbc762f6ea84a3f6ac7b",
|
||||||
|
".github/workflows/tests.yml": "a0d3002520e97206d94aa3ff9ee02be81f062c0dbb280c579af29e9d95846a58",
|
||||||
".gitignore": "7acff1fc699456d32b8d2a69205704d48193320644ed6d97c0f28612db75e87e",
|
".gitignore": "7acff1fc699456d32b8d2a69205704d48193320644ed6d97c0f28612db75e87e",
|
||||||
"README.en.md": "8af18dd3ffc5b49209bea79c0f931174feda6b9476e62e8d56ab2c0d1905df1d",
|
"LICENSE": "07cace8b56becb2cd64969414c59082df9aa7453eeed8caab8378aaff38f6820",
|
||||||
"README.md": "b34606732b22930a29cd7cf3626c8ab7671738983cc164caae1070256043202d",
|
"README.en.md": "6f86280e2a03baded9544126243f75606be7cf10a83471b881759e514a5ec7de",
|
||||||
|
"README.md": "c5a74c954959fac96b17097dd194310d3d40c9a2d46f8634234c67f8ea78ea35",
|
||||||
"THIRD_PARTY_NOTICES.md": "5049c589e832f752d9561240facccd23456cb41123820e0a93bac1532b416afe",
|
"THIRD_PARTY_NOTICES.md": "5049c589e832f752d9561240facccd23456cb41123820e0a93bac1532b416afe",
|
||||||
|
"docs/README.md": "fc451f854a312ebf13fab73b1e2215105b2f97c5dd31cf832e7ea50d9a6b8fea",
|
||||||
|
"docs/archive/ALPHA_RELEASE_REPORT.md": "02c4e902c402918ccc93e5519d61f46c20be741d2177b5b2566f5022d281b231",
|
||||||
|
"docs/archive/ALPHA_TASK_STATUS.md": "d34694421c89ac6bdd8ec8d7f934e062f05ebb667b099e5647655c8614b0df01",
|
||||||
|
"docs/archive/ARCHITECTURE-alpha.md": "c10fd83a73613f93d95a8c024a4827ed682f2282efe5b26474a9935d7db6dc1b",
|
||||||
|
"docs/archive/OPEN_SOURCE_RESEARCH.md": "3c8d475c43cfb1f887ef3a3a52e5c828f684e9dca8f859d0fef73193f00ca19b",
|
||||||
|
"docs/archive/PUBLIC_BETA_CHANGE_REPORT.md": "d08880333fea862cb4c6c83cd7a25c7d314b7ccc0ed96dc695561aef29b39f7e",
|
||||||
|
"docs/archive/README.md": "0d004615164da565a4c3166441d8814ef8ba7cc82c7d5be057c9bc8aab56ff8a",
|
||||||
|
"docs/archive/TEST_PLAN-alpha.md": "13d21028725cc7259c7de9d52169f370a87f5df1326995a4c608b062f961d826",
|
||||||
|
"docs/archive/history-beta-before-p0-20261101.md": "b653320e061bc233725c516f4af197dae064c98e907e7c2964d1d2d1a64e4660",
|
||||||
|
"docs/archive/pre-alpha-README.en.md": "b2f1d1d8fbaf36340330b465c8e5b7ba82dbacebab2558a994753a1ccafe5874",
|
||||||
|
"docs/archive/pre-alpha-README.md": "b78ebddbe4d6ad6882a9d9a7f888721daf672a4e2886313af27993fc6dca0aca",
|
||||||
|
"docs/archive/status-history-before-2026-11-01.md": "8d16d9d40292c2a157d445007c0ba1b9fa64c2cae5235be3025e0d33e799be4b",
|
||||||
|
"docs/development/ARCHITECTURE.md": "affd2a73cf821824a05ace6b06d4629aa5d4b8af21c349063a99195b623a5b91",
|
||||||
|
"docs/development/CI.md": "f5561af16a10d8de0ce58f200d3e21d76c4a8e3745c401d0b2e137450c11a4c3",
|
||||||
|
"docs/development/CONTRIBUTING.md": "ff542d9f3161d4034a9b1eb24c48fdbcf2834813e728a348393df8787825999d",
|
||||||
|
"docs/development/REFERENCES.md": "497d430e272e3fb1466980baae2af6e6d38a0e26ef47166b7eafc0e6fb59a192",
|
||||||
|
"docs/development/TESTING.md": "4217a35c53bf77404badc7145e5ad4a69bfb6a2803f23183d1d186cb6f2a241a",
|
||||||
|
"docs/guide/DATA_SERVICES.md": "d91be04176e6dda22c831cb9cd24f968ad82c1851c53bb6cbf356c51141b4a4c",
|
||||||
|
"docs/guide/INSTALL.md": "2099bbdd3aff80ae4da8afcd7d77b0d8e1f47262a6589a5ca22e5762c4a18ef9",
|
||||||
|
"docs/guide/IPQUALITY_COVERAGE.md": "c83c14ae7a00700e484fa707546876fba512d5080adf9ec3827b941df4d3e4c1",
|
||||||
|
"docs/guide/USAGE.md": "bf94cc664b7aae5508ee3e5c5d3a23449bf00a5db950551251d5b7ca677742e6",
|
||||||
|
"docs/release/CHANGELOG.md": "a999eb3a33694afe4099a3ada9d69766c7584533dacfc0ae8a249163d9524ab9",
|
||||||
|
"docs/release/KNOWN_LIMITATIONS.md": "08205fcf50cf2cd6191d203912d32e83bccdb4b325abfedb541fda7d660b740d",
|
||||||
|
"docs/release/RELEASE_NOTES.md": "80ec63cc949e8bf5944a285ca27c4a0f8108b2c30f22dfe54bc67c5639424815",
|
||||||
|
"docs/security/ROADMAP.md": "1c669b57bc08e05cba7a93f09b87c63286cd76bf98bf9ec30d584aa3dcc7700f",
|
||||||
|
"docs/security/SECURITY.md": "24dd0d7306bd3643f9d9773db51ea777d9049337afdd92d5ce3c6dfd38b2c81f",
|
||||||
|
"install.py": "f97f925d07c5589e24332b127a172c7fe1571cf66692d3611cae888553291888",
|
||||||
|
"proxydoctor.sh": "7b5ccc2c96e36b4f6d53dd54915bc90416aba9ace06738e0c1913cf887b92972",
|
||||||
"proxydoctor/__init__.py": "78648a703705f6f05484eabdd61c9cc302943dd96c25c8448666d07a1eaa713b",
|
"proxydoctor/__init__.py": "78648a703705f6f05484eabdd61c9cc302943dd96c25c8448666d07a1eaa713b",
|
||||||
"proxydoctor/__main__.py": "f21b83731acbaa16519e17fa795601ccd3543400cd5c59bb40098a488c225d84",
|
"proxydoctor/__main__.py": "f21b83731acbaa16519e17fa795601ccd3543400cd5c59bb40098a488c225d84",
|
||||||
"proxydoctor/bandwidth.py": "822392dd28872e55f4c67253983c69c4545a89dfbacc68d805c45d16c3283130",
|
"proxydoctor/bandwidth.py": "822392dd28872e55f4c67253983c69c4545a89dfbacc68d805c45d16c3283130",
|
||||||
"proxydoctor/bgp.py": "4c9fea00e616058700f73335fe1995764b85d610a89b22936b4e5dd9f563c0c6",
|
"proxydoctor/bgp.py": "4c9fea00e616058700f73335fe1995764b85d610a89b22936b4e5dd9f563c0c6",
|
||||||
"proxydoctor/carriers.py": "11506fef02973552ab74367f3c1f91c7a7ec484ac574c71eada6ab82fc5448aa",
|
"proxydoctor/carriers.py": "11506fef02973552ab74367f3c1f91c7a7ec484ac574c71eada6ab82fc5448aa",
|
||||||
"proxydoctor/checks.py": "f010c09c834af2f4ac48128113bb5aebef9d1fb0c796891623d02232ccf09e31",
|
"proxydoctor/checks.py": "f010c09c834af2f4ac48128113bb5aebef9d1fb0c796891623d02232ccf09e31",
|
||||||
"proxydoctor/cli.py": "c88708f6bcfefdfe1f2b5eebe9651aec4c25d56b4658d987b5d502d61734a48b",
|
"proxydoctor/cli.py": "53b1669c1dc46e7ffaef87a44b8ddabbaac25484fd1980858c0f6aef6009ad13",
|
||||||
"proxydoctor/connectivity.py": "2b6d4cfe69271ad582c3b0eb81bc69d38a9395c80cc12df2804bd01854f22385",
|
"proxydoctor/connectivity.py": "2b6d4cfe69271ad582c3b0eb81bc69d38a9395c80cc12df2804bd01854f22385",
|
||||||
"proxydoctor/console.py": "f6d29e9483e28d3252bba182f8152dc39ba28767afe8e8323fa50fdda88d6d2b",
|
"proxydoctor/console.py": "d85d6bbb75698fa0851a5c00a90c292653e5052771665a6dad6b0404d9f49837",
|
||||||
"proxydoctor/geo.py": "e4dde1452488743a0fc9a74e65bca65cad894ee357bc09c1d2b6dbdc2f0cfd04",
|
"proxydoctor/geo.py": "e4dde1452488743a0fc9a74e65bca65cad894ee357bc09c1d2b6dbdc2f0cfd04",
|
||||||
"proxydoctor/i18n.py": "a7a72f7ce2d64d20564828438eba9becf68ee3ef33c7e39dc90efe767d636d4d",
|
"proxydoctor/i18n.py": "a7a72f7ce2d64d20564828438eba9becf68ee3ef33c7e39dc90efe767d636d4d",
|
||||||
"proxydoctor/ipquality.py": "4dd1fc1421d11d03d8c803626393ca02d7be81527d4984da57c9ef2140c395bb",
|
"proxydoctor/ipquality.py": "4dd1fc1421d11d03d8c803626393ca02d7be81527d4984da57c9ef2140c395bb",
|
||||||
|
"proxydoctor/limits.py": "8c9e44bab967e8c62eb791e58e10584150fc804529731ca67aae1f7b1868f5cd",
|
||||||
"proxydoctor/mailquality.py": "ae1ad2e071bd7038b6ea7fbcbd63b7128eac9e5477f79f43c22172d6a5bb5071",
|
"proxydoctor/mailquality.py": "ae1ad2e071bd7038b6ea7fbcbd63b7128eac9e5477f79f43c22172d6a5bb5071",
|
||||||
"proxydoctor/media.py": "d7920b5d9d946ec40532a756df2329d37f6ede07f1d6c73f6ef63f43b1a95717",
|
"proxydoctor/media.py": "d7920b5d9d946ec40532a756df2329d37f6ede07f1d6c73f6ef63f43b1a95717",
|
||||||
"proxydoctor/net.py": "4ea5a7e722dfa2adc97dacf49e5ba28a5980e91e165382fce140b75f206db4c4",
|
"proxydoctor/net.py": "4ea5a7e722dfa2adc97dacf49e5ba28a5980e91e165382fce140b75f206db4c4",
|
||||||
|
"proxydoctor/overview.py": "c307332872d3d25357058e72003c167b1401bcb55c3bf4d97e5cbd145075fe83",
|
||||||
"proxydoctor/quality.py": "2e06f0d43cb95909fe4f14f08b39567b94cc1ee4ee03338fb8885fe1ec9fc878",
|
"proxydoctor/quality.py": "2e06f0d43cb95909fe4f14f08b39567b94cc1ee4ee03338fb8885fe1ec9fc878",
|
||||||
"proxydoctor/reporting.py": "3037733647a6f9bc6753d144bd79e8b46e975f26f9a6b305080c55d87ee97b99",
|
"proxydoctor/reporting.py": "3037733647a6f9bc6753d144bd79e8b46e975f26f9a6b305080c55d87ee97b99",
|
||||||
"proxydoctor/reports.py": "09bd0952c89397ef601a3421e563bdad1e74b17d07976dfcb4066a0353cea4ba",
|
"proxydoctor/reports.py": "09bd0952c89397ef601a3421e563bdad1e74b17d07976dfcb4066a0353cea4ba",
|
||||||
"proxydoctor/resources.py": "b2b7355ef8f09a16d28956e292dfb848c2ddc927c3773730c437cf53de4a09a4",
|
"proxydoctor/resources.py": "b2b7355ef8f09a16d28956e292dfb848c2ddc927c3773730c437cf53de4a09a4",
|
||||||
"proxydoctor/routes.py": "c266cd3768b0c17add3a7c073d721556d7e31b3a07ef082967d949eb33bd65d3",
|
"proxydoctor/routes.py": "c266cd3768b0c17add3a7c073d721556d7e31b3a07ef082967d949eb33bd65d3",
|
||||||
"proxydoctor/runner.py": "84462cb56bf335140a3aec08ace9c80dd745a9aeea99be618ac4483cb3430c8e",
|
"proxydoctor/runner.py": "93d3c0ea46889200199527e45c080cebe4a117f032772ba7f0ebd41f0295c5b9",
|
||||||
"proxydoctor/schema.py": "6ae0e99c696fae778e88f96132142ac8eecb51a6a5df91ba38b816895aa1ca88",
|
"proxydoctor/schema.py": "c7e0e5116b0bbe80dfcb6d895c1bb5e43ca7c9ae3662697000564c9735c43f77",
|
||||||
"proxydoctor/security.py": "363327e43ab13dc8ebb9bb88f49fd14cea4aedca0952e9733159f99a195bb019",
|
"proxydoctor/security.py": "363327e43ab13dc8ebb9bb88f49fd14cea4aedca0952e9733159f99a195bb019",
|
||||||
"proxydoctor/session.py": "3302f8aa1f031ca13c863ad0b464d94b11a41ee944e97cdb2f4d6ad2e7990737",
|
"proxydoctor/session.py": "3302f8aa1f031ca13c863ad0b464d94b11a41ee944e97cdb2f4d6ad2e7990737",
|
||||||
"proxydoctor/suite.py": "631a9b050c17a44abfd2b89630d362810743e51a83c2e503d03012a8f233d958",
|
"proxydoctor/suite.py": "cd69adeb081080e2260a95e151ab3f60e8532cd5c9416945f398c8ae6b9a8539",
|
||||||
"proxydoctor/worker.py": "c4745ba21aa68d4ce57c9bb8727841f8839d1c1c2d820e0e8e7ff071c88b0e06",
|
"proxydoctor/worker.py": "105eec8473d80b8a0118703328f797911b0aa269ce94f014e259b3cce2ff3d83",
|
||||||
|
"pyproject.toml": "98da2b2cc6af9c2d5cf6bf1bdd6513585b5c49dfa04c3ca2dc63d0031a5dcea2",
|
||||||
"tests/conftest.py": "9ba6f678d6297f2dc85f42121790246cd86b120dd52e537ee09d4c19f2cdf1b9",
|
"tests/conftest.py": "9ba6f678d6297f2dc85f42121790246cd86b120dd52e537ee09d4c19f2cdf1b9",
|
||||||
|
"tests/test_all.py": "4950f98bb42576cb388580a1c30fb9e494cb7c423d6d083e67c2a20fffd93b22",
|
||||||
"tests/test_alpha_quality.py": "a833be4ec7f0600383495b838ea1e95f50112003d47b2cc2cebbec50d0eba706",
|
"tests/test_alpha_quality.py": "a833be4ec7f0600383495b838ea1e95f50112003d47b2cc2cebbec50d0eba706",
|
||||||
"tests/test_bandwidth_sources.py": "c9e41f7f3d25078feef335a549fb8a3229662d552b552f0579369ae61cfc659b",
|
"tests/test_bandwidth_sources.py": "c9e41f7f3d25078feef335a549fb8a3229662d552b552f0579369ae61cfc659b",
|
||||||
"tests/test_branding.py": "d9cf33286f9591564e722dd5c5be038eed85a4a3643f0348fb00f7d3db475ef6",
|
"tests/test_branding.py": "d9cf33286f9591564e722dd5c5be038eed85a4a3643f0348fb00f7d3db475ef6",
|
||||||
@@ -65,36 +98,6 @@
|
|||||||
"tests/test_server_info.py": "52bd0f409cc01ffaabb3fde25a0781dab286065d475f2c639ed586b89b5608c2",
|
"tests/test_server_info.py": "52bd0f409cc01ffaabb3fde25a0781dab286065d475f2c639ed586b89b5608c2",
|
||||||
"tests/test_stage1.py": "9413983d55805a663d5b831b64146f191846192788ff014f4db256c5f68d3b9a",
|
"tests/test_stage1.py": "9413983d55805a663d5b831b64146f191846192788ff014f4db256c5f68d3b9a",
|
||||||
"tests/test_suite.py": "060496ba254109852a8a47669b4d0f0236b5e8ce1755b9889f7717148d5ad436",
|
"tests/test_suite.py": "060496ba254109852a8a47669b4d0f0236b5e8ce1755b9889f7717148d5ad436",
|
||||||
"docs/archive/ALPHA_RELEASE_REPORT.md": "02c4e902c402918ccc93e5519d61f46c20be741d2177b5b2566f5022d281b231",
|
"vpsmaster.sh": "7b5ccc2c96e36b4f6d53dd54915bc90416aba9ace06738e0c1913cf887b92972"
|
||||||
"docs/archive/ALPHA_TASK_STATUS.md": "d34694421c89ac6bdd8ec8d7f934e062f05ebb667b099e5647655c8614b0df01",
|
|
||||||
"docs/archive/ARCHITECTURE-alpha.md": "c10fd83a73613f93d95a8c024a4827ed682f2282efe5b26474a9935d7db6dc1b",
|
|
||||||
"docs/archive/history-beta-before-p0-20261101.md": "b653320e061bc233725c516f4af197dae064c98e907e7c2964d1d2d1a64e4660",
|
|
||||||
"docs/archive/OPEN_SOURCE_RESEARCH.md": "3c8d475c43cfb1f887ef3a3a52e5c828f684e9dca8f859d0fef73193f00ca19b",
|
|
||||||
"docs/archive/pre-alpha-README.en.md": "b2f1d1d8fbaf36340330b465c8e5b7ba82dbacebab2558a994753a1ccafe5874",
|
|
||||||
"docs/archive/pre-alpha-README.md": "b78ebddbe4d6ad6882a9d9a7f888721daf672a4e2886313af27993fc6dca0aca",
|
|
||||||
"docs/archive/PUBLIC_BETA_CHANGE_REPORT.md": "d08880333fea862cb4c6c83cd7a25c7d314b7ccc0ed96dc695561aef29b39f7e",
|
|
||||||
"docs/archive/README.md": "0d004615164da565a4c3166441d8814ef8ba7cc82c7d5be057c9bc8aab56ff8a",
|
|
||||||
"docs/archive/status-history-before-2026-11-01.md": "8d16d9d40292c2a157d445007c0ba1b9fa64c2cae5235be3025e0d33e799be4b",
|
|
||||||
"docs/archive/TEST_PLAN-alpha.md": "13d21028725cc7259c7de9d52169f370a87f5df1326995a4c608b062f961d826",
|
|
||||||
"docs/development/ARCHITECTURE.md": "affd2a73cf821824a05ace6b06d4629aa5d4b8af21c349063a99195b623a5b91",
|
|
||||||
"docs/development/CI.md": "b0a391418bb98d077eb46c1ca4f4ba4cedf6aa7b45ce8cef682e20c88576ba3b",
|
|
||||||
"docs/development/CONTRIBUTING.md": "ff542d9f3161d4034a9b1eb24c48fdbcf2834813e728a348393df8787825999d",
|
|
||||||
"docs/development/REFERENCES.md": "497d430e272e3fb1466980baae2af6e6d38a0e26ef47166b7eafc0e6fb59a192",
|
|
||||||
"docs/development/TESTING.md": "4217a35c53bf77404badc7145e5ad4a69bfb6a2803f23183d1d186cb6f2a241a",
|
|
||||||
"docs/guide/DATA_SERVICES.md": "d91be04176e6dda22c831cb9cd24f968ad82c1851c53bb6cbf356c51141b4a4c",
|
|
||||||
"docs/guide/INSTALL.md": "320a91fdf7f97f27153dd383a05c5db1013aae1400e4f3a9e603b1579c21581a",
|
|
||||||
"docs/guide/IPQUALITY_COVERAGE.md": "c83c14ae7a00700e484fa707546876fba512d5080adf9ec3827b941df4d3e4c1",
|
|
||||||
"docs/guide/USAGE.md": "7ede2dfa108b8b8a7563c7da6fd25525e20b1f4e25a4389ad1058573fc8a896d",
|
|
||||||
"docs/README.md": "fc451f854a312ebf13fab73b1e2215105b2f97c5dd31cf832e7ea50d9a6b8fea",
|
|
||||||
"docs/release/CHANGELOG.md": "a999eb3a33694afe4099a3ada9d69766c7584533dacfc0ae8a249163d9524ab9",
|
|
||||||
"docs/release/KNOWN_LIMITATIONS.md": "37abd811206a4b2cefccacc445d48d08a70ebde364fc2e5a4050ec353cc3500a",
|
|
||||||
"docs/release/RELEASE_NOTES.md": "3118790651074d98c2a13779ae613b1d7538795d12834d7c85a9662d91a8717c",
|
|
||||||
"docs/security/ROADMAP.md": "bdec671b5690927df12fdc48ed54b55afdfa51fa573213944236630a7da0a497",
|
|
||||||
"docs/security/SECURITY.md": "24dd0d7306bd3643f9d9773db51ea777d9049337afdd92d5ce3c6dfd38b2c81f",
|
|
||||||
".github/workflows/tests.yml": "a0d3002520e97206d94aa3ff9ee02be81f062c0dbb280c579af29e9d95846a58",
|
|
||||||
".github/CONTRIBUTING.md": "53d976a3571d70cabfa32e98185a905b25a91a1d9694f92502f037503b3d0076",
|
|
||||||
".github/ISSUE_TEMPLATE/bug_report.md": "b5eb7883641a2cad94b1770f47f946aecda05917d0961eb1d8c639ce4da4f64e",
|
|
||||||
".github/ISSUE_TEMPLATE/feature_request.md": "e4efabb0a5961e03344f59985797f4a349989d94ce3bd9edabf5f4d5a656bc68",
|
|
||||||
".github/SECURITY.md": "e13fd5de68bce030755b1c93c47c31e7a91a866d26dcdbc762f6ea84a3f6ac7b"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+63
-31
@@ -10,9 +10,9 @@ VPS Diagnostics & Security Toolkit — an open-source Linux VPS diagnostics and
|
|||||||
|
|
||||||
## About
|
## About
|
||||||
|
|
||||||
Built with Python for VPS beginners and Linux users, VPSMaster helps explain server resources, connectivity, IP information and basic security risks. Beta.1 provides CLI diagnostics and local reports. Default diagnostics are read-only and do not modify SSH, firewall, routes or system DNS.
|
VPSMaster helps Linux users and VPS beginners understand server resources, connectivity, IP information and basic security risks. **v1.0.0-beta.1** provides CLI diagnostics and local reports. Default checks are read-only and do not change SSH, firewall, routes or system DNS.
|
||||||
|
|
||||||
The project aims to grow into a Linux security diagnostics and hardening assistant, with SSH/firewall hardening, configuration backup and rollback, continuous security monitoring and a local Web UI as future priorities.
|
The project will grow toward server management: improved diagnostics, configuration backup and recovery, optional proxy deployment, security hardening and a local Web UI. These management capabilities are future plans, not current Beta features.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -20,67 +20,99 @@ The project aims to grow into a Linux security diagnostics and hardening assista
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Server | Linux, CPU, memory, swap, disk capacity, virtualization and resource observations |
|
| Server | Linux, CPU, memory, swap, disk capacity, virtualization and resource observations |
|
||||||
| Network | DNS, TCP/TLS, IPv4/IPv6 and UDP/ICMP/TCP traceroute |
|
| Network | DNS, TCP/TLS, IPv4/IPv6 and UDP/ICMP/TCP traceroute |
|
||||||
| Routing & IP | BGP, ASN, ownership, network types and provider-specific risk observations |
|
| Routing & IP | BGP, ASN, ownership, network types and provider-specific risk information |
|
||||||
| Services | Basic public-page reachability for selected media services |
|
| Services | Public-page reachability for selected media services |
|
||||||
| Security | Read-only SSH, port, firewall and file-permission checks |
|
| Security | Read-only SSH, port, firewall and file-permission checks |
|
||||||
| Reports | Chinese/English CLI, local JSON/Markdown, redaction and interruption recovery |
|
| Reports | Chinese/English CLI, JSON/Markdown, redaction and interruption recovery |
|
||||||
|
|
||||||
## Install
|
## Install from public GitHub
|
||||||
|
|
||||||
Requires **Linux, Python 3.10+ and standard-library venv**. Download the ZIP and matching `SHA256SUMS.txt` from [Releases](https://github.com/griffinmx/VPSMaster/releases) into the same directory. During private testing, access is restricted; a release draft is not a generally available published download.
|
Requires **Linux, Python 3.10+, standard-library venv and Git**. Ubuntu 22.04 / 24.04 is recommended.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sha256sum -c SHA256SUMS.txt
|
git clone https://github.com/griffinmx/VPSMaster.git
|
||||||
python3 -m zipfile -e VPSMaster-v1.0.0-beta.1.zip .
|
cd VPSMaster
|
||||||
cd VPSMaster-v1.0.0-beta.1
|
|
||||||
python3 -I -B install.py --run
|
python3 -I -B install.py --run
|
||||||
```
|
```
|
||||||
|
|
||||||
The installer creates an isolated user environment and prints the launcher path. The first scan is offline. Root is not required; the installer invokes neither pip, sudo nor a system package manager. Missing dependencies produce an error without changing the system. [Installation guide](docs/guide/INSTALL.md)
|
The installer verifies the manifest, creates an isolated user environment and runs an **offline** check. It requires no root and invokes neither pip, sudo nor a package manager. Default location: `~/.local/share/vpsmaster/1.0.0b1/`.
|
||||||
|
|
||||||
|
Start the online scan separately:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
~/.local/share/vpsmaster/1.0.0b1/bin/vpsmaster all --lang en
|
||||||
|
```
|
||||||
|
|
||||||
|
The online scan saves local reports and has a default 300-second budget. Missing permissions, tools or reachable targets can yield unknown/skipped/incomplete results without a program crash. [Install/update/uninstall](docs/guide/INSTALL.md)
|
||||||
|
|
||||||
|
## One-command diagnostics
|
||||||
|
|
||||||
|
Optionally define an alias in your current shell:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
alias vpsmaster="$HOME/.local/share/vpsmaster/1.0.0b1/bin/vpsmaster"
|
||||||
|
vpsmaster all --lang zh
|
||||||
|
vpsmaster all --lang en
|
||||||
|
vpsmaster all --offline --lang en
|
||||||
|
vpsmaster all --full --lang en
|
||||||
|
```
|
||||||
|
|
||||||
|
`all` reuses existing modules and shows live progress and a final summary. It covers resources/interfaces, DNS/TCP/TLS, UDP and available ICMP/TCP, IP/ASN/BGP, pages and basic security. Unknown is not success. Default limits: 300 seconds total, 60 per module; full mode allows 600 total.
|
||||||
|
|
||||||
|
`--full` adds PTR/TCP25 and bounded China-destination traces. Bandwidth needs separate `--allow-bandwidth` consent, with a 64 MiB test-payload cap. Mainland speed candidates additionally need experimental `--china-speed`. No bandwidth, CPU or disk benchmark runs by default. [Full usage](docs/guide/USAGE.md)
|
||||||
|
|
||||||
## Common commands
|
## Common commands
|
||||||
|
|
||||||
Use the printed launcher path for custom installations; the default is:
|
Use the printed launcher path for custom installations.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
VM="$HOME/.local/share/vpsmaster/1.0.0b1/bin/vpsmaster"
|
VM="$HOME/.local/share/vpsmaster/1.0.0b1/bin/vpsmaster"
|
||||||
|
"$VM" all --lang en
|
||||||
|
"$VM" all --offline --lang en
|
||||||
"$VM" check --lang en
|
"$VM" check --lang en
|
||||||
"$VM" suite --offline --lang en
|
|
||||||
"$VM" quality --lang en
|
"$VM" quality --lang en
|
||||||
"$VM" trace --target 1.1.1.1 --method udp
|
"$VM" trace --target 1.1.1.1 --method udp
|
||||||
"$VM" ipquality
|
"$VM" ipquality
|
||||||
|
"$VM" bgp --target 1.1.1.1
|
||||||
"$VM" suite --json-output report.json
|
"$VM" suite --json-output report.json
|
||||||
"$VM" report --input report.json --output report.md
|
"$VM" report --input report.json --output report.md
|
||||||
"$VM" --help
|
"$VM" --help
|
||||||
```
|
```
|
||||||
|
|
||||||
The legacy `proxydoctor` command remains compatible. See [usage](docs/guide/USAGE.md) for full options and opt-in bandwidth tests. Remote services see your source IP; review redacted reports before sharing.
|
The legacy `proxydoctor` command remains supported. External services see your source IP; inspect reports before sharing.
|
||||||
|
|
||||||
## Beta limitations
|
## FAQ and limits
|
||||||
|
|
||||||
- Media checks test page reachability, not unlocking or playback. Mainland three-carrier bandwidth tests are experimental and off by default.
|
- A Git proxy is not automatically used by VPSMaster; some probes connect directly. DNS success does not imply TCP/TLS success.
|
||||||
- ICMP/TCP may require system `traceroute` and permissions. BGP, geography and risk observations depend on third-party data.
|
- A Linux text console may lack Chinese fonts. Use a suitable UTF-8 SSH terminal or `--lang en`.
|
||||||
- Lab coverage: Ubuntu 22.04 Desktop and 24.04 Server, x86_64. Overseas VPS, ARM, public IPv6 and a fresh Ubuntu 22.04 Server image are not comprehensively validated.
|
- Media checks mean public-page reachability, not unlocking or playback. Mainland carrier tests remain experimental/off by default.
|
||||||
- No one-click hardening, automatic proxy deployment or Web UI. Beta does not imply production stability or independent security certification.
|
- ICMP/TCP may need system `traceroute` and permissions. BGP, location and risk data depend on third parties.
|
||||||
|
- Main lab coverage is Ubuntu22 Desktop/Ubuntu24 Server, x86_64. Overseas VPS, ARM, public IPv6 and more fresh images are not comprehensively validated.
|
||||||
|
- No automatic proxy deployment, hardening, configuration rollback or Web UI exists in this Beta. It is not production stability or independent security certification.
|
||||||
|
- Redaction is not an arbitrary-text anonymizer; review IP/topology and other sensitive information before sharing.
|
||||||
|
|
||||||
[Detailed limitations](docs/release/KNOWN_LIMITATIONS.md) · [Release notes](docs/release/RELEASE_NOTES.md)
|
[Detailed limits](docs/release/KNOWN_LIMITATIONS.md) · [Release notes](docs/release/RELEASE_NOTES.md)
|
||||||
|
|
||||||
## Roadmap
|
## Five-phase roadmap
|
||||||
|
|
||||||
| Phase | Direction | Status |
|
| Phase | Direction | Status |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| 1 · VPS diagnostics | Resources, connectivity, routes, BGP/IP, basic security and recoverable reports | Current Beta foundation |
|
| 1 · Comprehensive VPS diagnostics | Resources, connectivity, routes, BGP/IP, basic security and local recoverable reports | Current Beta foundation |
|
||||||
| 2 · Improve diagnostics | CPU/disk benchmarks, mainland carrier tests, IP cross-checking, routing advice, security baselines and purchase acceptance | Planned |
|
| 2 · Better diagnostics and backup/rollback | Clearer CLI reports, CPU/disk benchmarks, carrier tests, IP cross-checking, routing advice, security baselines, snapshots/diffs and basic recovery | Planned |
|
||||||
| 3 · Security hardening | SSH/key policies, UFW/nftables, Fail2ban, updates and permissions, with selectable plans and previews | Planned; future priority |
|
| 3 · One-click proxy deployment | Common protocols, installation/configuration, health, updates/uninstall, then domains/certificates/subscriptions | Planned |
|
||||||
| 4 · Backup, rollback & monitoring | Configuration backups/diffs, SSH access protection, verification, conditional rollback, auditing and monitoring | Planned |
|
| 4 · One-click security hardening | SSH/key policies, UFW/nftables, Fail2ban, updates/permissions/ports; plans, previews, verification and conditional rollback | Future priority |
|
||||||
| 5 · Visual management | Local Web UI, risks/tasks, recovery, multiple VPS and optional service management | Long-term plan |
|
| 5 · Web UI and server management | Local dashboard, reports/risks, backups/recovery, multiple VPS and optional services | Long-term plan |
|
||||||
|
|
||||||
Future hardening requires explicit confirmation: **detect → explain → preview → confirm → back up → execute → verify → roll back**. Protecting SSH access is essential; rollback cannot cover every outage. A future management UI must not expose its entry point publicly by default.
|
Phase 2 builds verifiable backup/recovery foundations alongside diagnostic improvements. Phase 3 prioritizes basic deployments without a domain before domain/certificate-based configurations, with secure configuration and service lifecycle handling.
|
||||||
|
|
||||||
Future capabilities are not implemented or included in this Beta, and have no fixed delivery dates. [Detailed roadmap](docs/security/ROADMAP.md)
|
Phase 4 requires explicit confirmation for changes affecting SSH, firewall or access: **detect → explain → preview → confirm → back up → execute → verify → roll back**. Protect management connections and business ports; rollback cannot cover every outage, cloud firewall, reboot or host failure.
|
||||||
|
|
||||||
## Documentation & contributing
|
A future local Web UI must not expose its management entry point publicly by default; authentication, authorization and credential protection are essential. **Phases 2–5 are plans, not implemented Beta.1 capabilities, and have no fixed release dates.** [Detailed roadmap](docs/security/ROADMAP.md)
|
||||||
|
|
||||||
[Usage](docs/guide/USAGE.md) · [中文首页](README.md) · [Installation](docs/guide/INSTALL.md) · [Limitations](docs/release/KNOWN_LIMITATIONS.md) · [Roadmap](docs/security/ROADMAP.md)
|
## Documentation and contributing
|
||||||
|
|
||||||
[Documentation](docs/README.md) · [Security/privacy](docs/security/SECURITY.md) · [Issues](https://github.com/griffinmx/VPSMaster/issues) · [Contributing](docs/development/CONTRIBUTING.md) · [MIT License](LICENSE) · [Third-party notices](THIRD_PARTY_NOTICES.md)
|
[Usage](docs/guide/USAGE.md) · [中文首页](README.md) · [Installation](docs/guide/INSTALL.md) · [Limits](docs/release/KNOWN_LIMITATIONS.md) · [Roadmap](docs/security/ROADMAP.md)
|
||||||
|
|
||||||
|
[Docs](docs/README.md) · [Security/privacy](docs/security/SECURITY.md) · [Issues](https://github.com/griffinmx/VPSMaster/issues) · [Contributing](docs/development/CONTRIBUTING.md) · [MIT License](LICENSE) · [Third-party notices](THIRD_PARTY_NOTICES.md)
|
||||||
|
|
||||||
|
Please share reproducible, redacted feedback. Never upload passwords, tokens, SSH private keys or unchecked raw reports to public Issues.
|
||||||
@@ -10,9 +10,19 @@ VPS Diagnostics & Security Toolkit — 面向 VPS 新手和 Linux 用户的开
|
|||||||
|
|
||||||
## 项目简介
|
## 项目简介
|
||||||
|
|
||||||
VPSMaster 基于 Python,帮助你快速了解 Linux VPS 的硬件资源、网络质量、IP 信息及基础安全风险。当前 Beta.1 提供 CLI 检测与本地报告,普通检测默认只读,不修改 SSH、防火墙、路由或系统 DNS。
|
VPSMaster 基于 Python,帮助用户快速了解 Linux VPS 的硬件资源、网络质量、IP 信息及基础安全风险。
|
||||||
|
|
||||||
项目将从 VPS 检测工具逐步发展为 Linux 服务器安全诊断与加固助手,未来重点是 SSH 与防火墙安全加固、配置备份与回滚、持续安全监控及 Web UI 管理。
|
当前 **v1.0.0-beta.1** 主要提供 CLI 综合检测与本地报告。普通检测默认只读,不修改 SSH、防火墙、路由或系统 DNS。
|
||||||
|
|
||||||
|
项目将逐步从 VPS 检测工具发展为 Linux 服务器综合管理助手,未来计划加入:
|
||||||
|
|
||||||
|
- 更完整的硬件与网络质量检测
|
||||||
|
- 配置备份、差异对比及恢复机制
|
||||||
|
- 常见代理协议的一键部署与管理
|
||||||
|
- SSH、防火墙和系统安全加固
|
||||||
|
- 本地 Web UI 与多 VPS 管理
|
||||||
|
|
||||||
|
**当前 Beta 仅提供已经实现的检测功能,其余功能属于后续规划。**
|
||||||
|
|
||||||
## 核心功能
|
## 核心功能
|
||||||
|
|
||||||
@@ -27,60 +37,209 @@ VPSMaster 基于 Python,帮助你快速了解 Linux VPS 的硬件资源、网
|
|||||||
|
|
||||||
## 快速安装
|
## 快速安装
|
||||||
|
|
||||||
需要 **Linux、Python 3.10+ 和标准库 venv**。从 [GitHub Releases](https://github.com/griffinmx/VPSMaster/releases) 获取 ZIP 与配套 `SHA256SUMS.txt`,放到同一目录。仓库仍为 Private 时,仅获授权用户可访问;Draft 是发行草稿,并非所有用户均可下载的正式版本。
|
需要 **Linux、Python 3.10+、标准库 venv 和 Git**,推荐使用 Ubuntu 22.04 / 24.04。
|
||||||
|
|
||||||
|
### 1. 下载 VPSMaster
|
||||||
|
|
||||||
|
打开 Linux 终端,执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/griffinmx/VPSMaster.git
|
||||||
|
cd VPSMaster
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 安装 VPSMaster
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sha256sum -c SHA256SUMS.txt
|
|
||||||
python3 -m zipfile -e VPSMaster-v1.0.0-beta.1.zip .
|
|
||||||
cd VPSMaster-v1.0.0-beta.1
|
|
||||||
python3 -I -B install.py --run
|
python3 -I -B install.py --run
|
||||||
```
|
```
|
||||||
|
|
||||||
安装器在用户目录创建隔离环境,输出可执行入口路径;首次运行离线体检,不要求 root,也不调用 pip、sudo 或系统包管理器。缺少依赖时明确退出,不擅自修改系统。[安装、升级与卸载](docs/guide/INSTALL.md)
|
安装器会验证文件完整性,在用户目录创建独立运行环境,并自动执行一次**离线体检**。
|
||||||
|
|
||||||
|
首次安装不要求 root 权限,不调用 pip、sudo 或系统包管理器,也不会自动修改系统配置。
|
||||||
|
|
||||||
|
默认安装目录:
|
||||||
|
|
||||||
|
```text
|
||||||
|
~/.local/share/vpsmaster/1.0.0b1/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 开始中文联网检测
|
||||||
|
|
||||||
|
```bash
|
||||||
|
~/.local/share/vpsmaster/1.0.0b1/bin/vpsmaster all --lang zh
|
||||||
|
```
|
||||||
|
|
||||||
|
程序将自动检测服务器资源、网络连通性、IP/ASN/BGP、路由、部分公开网页可达性及基础安全状态,并保存本地报告。
|
||||||
|
|
||||||
|
默认检测总时间预算为 **300 秒**。
|
||||||
|
|
||||||
|
**注意:** 安装命令中的 `--run` 执行首次离线体检;`vpsmaster all --lang zh` 才会执行综合联网检测。
|
||||||
|
|
||||||
|
网络异常、缺少依赖或权限不足可能导致部分检测结果显示未知或跳过,并不一定代表软件运行失败。
|
||||||
|
|
||||||
|
更多安装方式请查看 [安装、更新与卸载指南](docs/guide/INSTALL.md)。
|
||||||
|
|
||||||
|
## 一键综合检测
|
||||||
|
|
||||||
|
新用户推荐入口:**`vpsmaster all`**。
|
||||||
|
|
||||||
|
VPSMaster 复用现有检测模块,实时显示检测进度、执行结果和最终汇总,并自动保存本地报告。
|
||||||
|
|
||||||
|
为了方便后续使用,可以在当前终端设置别名:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
alias vpsmaster="$HOME/.local/share/vpsmaster/1.0.0b1/bin/vpsmaster"
|
||||||
|
```
|
||||||
|
|
||||||
|
然后执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 中文综合联网检测
|
||||||
|
vpsmaster all --lang zh
|
||||||
|
|
||||||
|
# 英文综合联网检测
|
||||||
|
vpsmaster all --lang en
|
||||||
|
|
||||||
|
# 中文离线检测
|
||||||
|
vpsmaster all --offline --lang zh
|
||||||
|
|
||||||
|
# 完整模式
|
||||||
|
vpsmaster all --full --lang zh
|
||||||
|
```
|
||||||
|
|
||||||
|
默认检测包含系统与资源、网络接口、DNS/TCP/TLS、UDP 与条件允许时的 ICMP/TCP 路由、IP/ASN/BGP、网页可达性和基础安全体检。
|
||||||
|
|
||||||
|
缺少依赖、权限不足或超时时,程序会保留相应原因,不会将未知结果直接标记为成功。
|
||||||
|
|
||||||
|
默认总时间预算为 300 秒,每模块 60 秒;完整模式默认总预算为 600 秒。
|
||||||
|
|
||||||
|
`--full` 增加 PTR/TCP25 和中国目标方向的有限路由检测。
|
||||||
|
|
||||||
|
带宽测试需要额外指定 `--allow-bandwidth`,测试载荷预算上限为 64 MiB。中国大陆测速候选另需实验性 `--china-speed` 参数。
|
||||||
|
|
||||||
|
默认不执行带宽测速、CPU 或磁盘性能基准测试。
|
||||||
|
|
||||||
|
[完整参数与检测说明](docs/guide/USAGE.md)
|
||||||
|
|
||||||
## 常用命令
|
## 常用命令
|
||||||
|
|
||||||
默认安装使用下面的入口;自定义目录请使用安装器打印的路径。
|
默认安装使用以下入口,自定义安装目录请使用安装器实际输出的路径。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
VM="$HOME/.local/share/vpsmaster/1.0.0b1/bin/vpsmaster"
|
VM="$HOME/.local/share/vpsmaster/1.0.0b1/bin/vpsmaster"
|
||||||
"$VM" check --lang zh # 本机体检
|
|
||||||
"$VM" suite --offline --lang zh # 离线综合检测
|
"$VM" all --lang zh # 综合联网检测
|
||||||
"$VM" quality --lang zh # DNS / TCP / TLS 网络检测
|
"$VM" all --offline --lang zh # 离线综合检测
|
||||||
"$VM" trace --target 1.1.1.1 --method udp # 逐跳追踪
|
"$VM" check --lang zh # 本机基础体检
|
||||||
"$VM" ipquality # IP 归属与风险信息
|
"$VM" quality --lang zh # DNS / TCP / TLS 检测
|
||||||
"$VM" suite --json-output report.json # 联网综合检测并保存报告
|
"$VM" trace --target 1.1.1.1 --method udp # UDP 路由追踪
|
||||||
|
"$VM" ipquality # IP 归属与风险信息
|
||||||
|
"$VM" bgp --target 1.1.1.1 # BGP 信息
|
||||||
|
"$VM" suite --json-output report.json # 保存指定 JSON 报告
|
||||||
"$VM" report --input report.json --output report.md
|
"$VM" report --input report.json --output report.md
|
||||||
"$VM" --help
|
"$VM" --help
|
||||||
```
|
```
|
||||||
|
|
||||||
`proxydoctor` 兼容命令仍可用。完整参数与可选测速见[使用指南](docs/guide/USAGE.md)。联网服务会看到来源 IP;分享报告前仍需人工复核脱敏结果。
|
`proxydoctor` 兼容命令仍然可用。
|
||||||
|
|
||||||
|
联网检测涉及第三方服务时,对方可能看到请求来源 IP。分享报告前请人工检查脱敏结果。
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
**为什么 GitHub 下载成功,但 TCP/TLS 检测失败?**
|
||||||
|
|
||||||
|
Git 使用的代理配置不会自动应用到 VPSMaster。当前部分网络检测明确使用直连方式,DNS 成功也不代表 TCP/TLS 一定成功。
|
||||||
|
|
||||||
|
**为什么 Ubuntu Server 中文显示乱码?**
|
||||||
|
|
||||||
|
部分 Linux 纯文本控制台不支持中文字体。建议通过支持 UTF-8 和中文字体的 SSH 终端运行,或者使用 `--lang en`。
|
||||||
|
|
||||||
|
**为什么检测结果显示 `unknown`、`skipped` 或 `incomplete`?**
|
||||||
|
|
||||||
|
可能由网络限制、权限不足、缺少系统工具、请求超时或第三方数据源不可用引起。模块执行完成不代表所有目标均已成功检测。
|
||||||
|
|
||||||
|
**安全体检会修改服务器吗?**
|
||||||
|
|
||||||
|
不会。当前基础安全体检只读,不自动修改 SSH、防火墙、系统 DNS 或路由,也不会自动执行安全加固。
|
||||||
|
|
||||||
|
**检测报告可以公开分享吗?**
|
||||||
|
|
||||||
|
建议先人工检查。脱敏不能保证覆盖任意敏感信息,报告仍可能包含公网 IP、网络拓扑等内容。
|
||||||
|
|
||||||
## Beta 已知限制
|
## Beta 已知限制
|
||||||
|
|
||||||
- 流媒体仅网页可达性,不代表解锁或播放;大陆三网测速仍为实验性,默认关闭。
|
- 流媒体仅检测公开网页可达性,不代表实际解锁或播放。
|
||||||
- ICMP/TCP 可能需要系统 `traceroute` 和相应权限;BGP、IP 地理位置及风险信息受第三方数据限制。
|
- 中国大陆三网测速仍为实验性功能,默认关闭,不保证完整节点覆盖。
|
||||||
- 已验证实验环境为 Ubuntu 22.04 Desktop、24.04 Server(x86_64);海外 VPS、ARM、公网 IPv6、全新 Ubuntu 22.04 Server 镜像尚未全面验证。
|
- ICMP/TCP 路由可能需要系统 `traceroute` 和相应权限;BGP、IP 地理位置及风险数据受第三方来源限制。
|
||||||
- 当前没有一键安全加固、自动代理部署和 Web UI;Beta 不代表稳定生产版本或独立安全认证。
|
- 当前主要验证环境为 Ubuntu 22.04 Desktop、24.04 Server(x86_64);海外 VPS、ARM、公网 IPv6 及更多全新系统镜像尚未全面验证。
|
||||||
|
- 当前没有一键代理部署、自动安全加固、配置自动回滚或 Web UI。
|
||||||
|
- Beta 不代表生产环境稳定性或独立安全认证。
|
||||||
|
|
||||||
[完整技术限制](docs/release/KNOWN_LIMITATIONS.md) · [Beta 发行说明](docs/release/RELEASE_NOTES.md)
|
[完整技术限制](docs/release/KNOWN_LIMITATIONS.md) · [Beta 发行说明](docs/release/RELEASE_NOTES.md)
|
||||||
|
|
||||||
## 后续开发方向
|
## 后续开发方向
|
||||||
|
|
||||||
|
VPSMaster 计划分为五个阶段,逐步从服务器检测发展为综合部署与安全管理工具。
|
||||||
|
|
||||||
| 阶段 | 方向与目标 | 状态 |
|
| 阶段 | 方向与目标 | 状态 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Phase 1 · VPS 综合检测 | 系统与资源、网络连通性、逐跳追踪、BGP/IP、基础安全及报告恢复 | 当前 Beta 基础能力 |
|
| **Phase 1 · VPS 综合检测** | 系统与资源、网络连通性、逐跳追踪、BGP/IP、基础安全体检及本地报告恢复 | 当前 Beta 基础能力 |
|
||||||
| Phase 2 · 检测能力完善 | CPU/磁盘基准、三网测速完善、IP 情报交叉验证、线路建议、安全基线与购买验收 | Planned / 规划中 |
|
| **Phase 2 · 检测能力完善与备份回滚** | CLI 彩色综合报表、CPU/磁盘性能基准、三网测速、IP 情报交叉验证、线路建议、安全基线,以及配置备份、差异对比和基础恢复机制 | 规划中 |
|
||||||
| Phase 3 · 一键安全加固 | SSH 与密钥策略、UFW/nftables、Fail2ban、更新及权限加固;可选方案、风险解释和修改预览 | Planned / 未来重点 |
|
| **Phase 3 · 一键代理部署** | 常见代理协议的一键安装、服务配置、状态检测、更新与卸载;逐步支持域名、证书及订阅配置管理 | 规划中 |
|
||||||
| Phase 4 · 备份、回滚与监控 | 配置备份与差异、保留 SSH 通道、执行后复验、有条件回滚、审计及持续安全监控 | Planned / 规划中 |
|
| **Phase 4 · 一键安全加固** | SSH 与密钥策略、UFW/nftables、Fail2ban、系统更新、权限和端口加固;提供方案选择、风险解释、修改预览、执行验证及有条件回滚 | 未来重点 |
|
||||||
| Phase 5 · Web UI 与管理 | 本地面板、风险与任务汇总、备份恢复、多 VPS 和可选服务管理 | 长期规划 |
|
| **Phase 5 · Web UI 与服务器管理** | 本地 Web 面板、检测报告展示、安全风险管理、备份恢复、多 VPS 管理及可选服务管理 | 长期规划 |
|
||||||
|
|
||||||
未来安全加固必须由用户明确确认,遵循 **检测 → 解释 → 预览 → 确认 → 备份 → 执行 → 验证 → 回滚**。重点保护 SSH 管理访问,不承诺所有故障都能自动恢复;Web 管理入口默认不向公网开放。
|
### Phase 2 · 检测能力完善与备份回滚
|
||||||
|
|
||||||
以上未来能力尚未实现,不属于当前 Beta,不承诺固定交付时间。[完整开发路线](docs/security/ROADMAP.md)
|
在现有检测基础上,逐步完善硬件性能、网络质量和 IP 情报分析,并优化 CLI 最终报告的可读性。
|
||||||
|
|
||||||
|
同时建设配置备份与恢复基础能力,包括配置快照、差异比较及可验证的恢复流程,为后续服务部署和安全加固提供基础保障。
|
||||||
|
|
||||||
|
### Phase 3 · 一键代理部署
|
||||||
|
|
||||||
|
面向 VPS 新手,计划提供常见代理协议的简化部署和服务管理功能。
|
||||||
|
|
||||||
|
初期优先考虑无需域名的基础部署方式,随后逐步增加需要域名及证书的配置方案。
|
||||||
|
|
||||||
|
重点关注安装流程、服务状态、更新、卸载、配置安全及故障诊断。
|
||||||
|
|
||||||
|
### Phase 4 · 一键安全加固
|
||||||
|
|
||||||
|
未来计划提供可选择的 Linux 服务器安全加固方案,包括 SSH 登录保护、防火墙、Fail2ban、系统更新和权限管理等。
|
||||||
|
|
||||||
|
涉及 SSH、防火墙或其他可能影响服务器连接的操作时,必须由用户明确确认。
|
||||||
|
|
||||||
|
安全加固遵循:
|
||||||
|
|
||||||
|
**检测 → 解释 → 预览 → 确认 → 备份 → 执行 → 验证 → 回滚**
|
||||||
|
|
||||||
|
重点保护 SSH 管理连接和现有业务端口,尽可能降低因错误配置导致服务器失联的风险。
|
||||||
|
|
||||||
|
回滚功能需要满足相应前提,不承诺自动恢复全部断网、云安全组、重启或宿主机故障。
|
||||||
|
|
||||||
|
### Phase 5 · Web UI 与服务器管理
|
||||||
|
|
||||||
|
长期计划提供本地 Web 管理面板,集中展示服务器资源、网络质量、安全风险、部署服务和备份状态。
|
||||||
|
|
||||||
|
逐步探索多 VPS 管理与可选服务维护能力。
|
||||||
|
|
||||||
|
未来 Web 管理入口默认不向公网开放,认证、权限及凭据保护将作为基础安全要求。
|
||||||
|
|
||||||
|
**以上 Phase 2–5 功能均为后续规划,不属于当前 Beta.1 已实现能力,也不承诺固定发布时间。**
|
||||||
|
|
||||||
|
[完整开发路线](docs/security/ROADMAP.md)
|
||||||
|
|
||||||
## 文档与贡献
|
## 文档与贡献
|
||||||
|
|
||||||
[中文使用文档](docs/guide/USAGE.md) · [English README](README.en.md) · [安装指南](docs/guide/INSTALL.md) · [已知限制](docs/release/KNOWN_LIMITATIONS.md) · [安全加固路线](docs/security/ROADMAP.md)
|
[中文使用文档](docs/guide/USAGE.md) · [English README](README.en.md) · [安装指南](docs/guide/INSTALL.md) · [已知限制](docs/release/KNOWN_LIMITATIONS.md) · [安全加固路线](docs/security/ROADMAP.md)
|
||||||
|
|
||||||
[文档目录](docs/README.md) · [安全与隐私](docs/security/SECURITY.md) · [Issues](https://github.com/griffinmx/VPSMaster/issues) · [贡献指南](docs/development/CONTRIBUTING.md) · [MIT LICENSE](LICENSE) · [第三方来源及许可声明](THIRD_PARTY_NOTICES.md)
|
[文档目录](docs/README.md) · [安全与隐私](docs/security/SECURITY.md) · [Issues](https://github.com/griffinmx/VPSMaster/issues) · [贡献指南](docs/development/CONTRIBUTING.md) · [MIT LICENSE](LICENSE) · [第三方来源及许可声明](THIRD_PARTY_NOTICES.md)
|
||||||
|
|
||||||
|
欢迎提交 Bug、功能建议及经过脱敏的测试反馈。
|
||||||
|
|
||||||
|
请勿在公开 Issue 中上传密码、Token、SSH 私钥或未经检查的原始报告。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**VPSMaster — 看清你的 VPS,让服务器管理更安全。**
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
# Private Beta CI
|
# Beta CI
|
||||||
|
|
||||||
此工作流仅workflow_dispatch手动触发,contents:read,不批准PR,不上传制品/缓存,不运行部署/加固。
|
此工作流仅workflow_dispatch手动触发,contents:read,不批准PR,不上传制品/缓存,不运行部署/加固。
|
||||||
checkout固定至官方v7.0.1提交3d3c42e5aac5ba805825da76410c181273ba90b1,关闭凭据持久化。
|
checkout固定至官方v7.0.1提交3d3c42e5aac5ba805825da76410c181273ba90b1,关闭凭据持久化。
|
||||||
开发测试在用户venv里使用pytest、setuptools和wheel;这些不是产品运行依赖,不打入发行ZIP。
|
开发测试在用户venv里使用pytest、setuptools和wheel;这些不是产品运行依赖,不打入发行ZIP。
|
||||||
|
|
||||||
使用self-hosted Linux x64 runner标签vpsmaster-ubuntu22 / vpsmaster-ubuntu24,分别要求Python3.10/3.12。
|
使用self-hosted Linux x64 runner标签vpsmaster-ubuntu22 / vpsmaster-ubuntu24,分别要求Python3.10/3.12。
|
||||||
本轮私有验收会临时注册已授权实验VM为ephemeral runner,完成后移除,不安装后台服务、不使用GitHub托管计费额度。
|
维护者验收可临时注册已授权实验VM为ephemeral runner,完成后移除,不安装后台服务、不使用GitHub托管计费额度。
|
||||||
没有临时runner时手动执行会排队;不能把排队或本地测试当作远端CI通过。未来更换CI方式须单独核实费用、安全边界和凭据。
|
没有临时runner时手动执行会排队;不能把排队或本地测试当作远端CI通过。未来更换CI方式须单独核实费用、安全边界和凭据。
|
||||||
仅对受信任已审代码使用本地runner;不得把持久自托管runner直接暴露给不受信任公开PR。
|
仅对受信任已审代码使用本地runner;不得把持久自托管runner直接暴露给不受信任公开PR。
|
||||||
|
|
||||||
CI原始系统报告不上传;只在临时runner保存并按本轮清理。GitHub原生步骤日志由私有仓库访问权限保护。
|
CI原始系统报告不上传;只在临时runner保存并按本轮清理。公开仓库中的GitHub原生步骤日志可能公开,禁止打印主机私密信息或凭据。
|
||||||
Self-hosted免费政策参考:https://docs.github.com/en/billing/concepts/product-billing/github-actions ,执行时须核对当前政策。
|
Self-hosted免费政策参考:https://docs.github.com/en/billing/concepts/product-billing/github-actions ,执行时须核对当前政策。
|
||||||
+37
-26
@@ -1,10 +1,39 @@
|
|||||||
# 安装、更新与卸载 / Installation
|
# 安装、更新与卸载 / Installation
|
||||||
|
|
||||||
需要 Linux、Python 3.10+ 和标准库 venv;Ubuntu 22.04+ 是主要目标。安装器不下载依赖、不调用 pip/sudo/系统包管理器。缺少 Python 或 venv 时明确退出,由用户自行选择系统依赖安装方式。
|
需要 Linux、Python 3.10+、标准库venv与Git。主要目标为Ubuntu22.04+。安装器不下载依赖,不调用pip、sudo或系统包管理器;缺少依赖时明确退出。
|
||||||
|
|
||||||
## ZIP 安装
|
## 从公开GitHub安装(推荐)
|
||||||
|
|
||||||
从可信的 [Releases](https://github.com/griffinmx/VPSMaster/releases) 渠道取得同一构建的 ZIP 与 `SHA256SUMS.txt`。校验只能验证内容一致性,不能独立证明发布者身份。私有测试期间需访问授权。
|
```bash
|
||||||
|
git clone https://github.com/griffinmx/VPSMaster.git
|
||||||
|
cd VPSMaster
|
||||||
|
python3 -I -B install.py --run
|
||||||
|
```
|
||||||
|
|
||||||
|
公开仓库克隆不需要GitHub登录。安装器检查根目录MANIFEST.sha256.json列出的文件与哈希,在用户目录创建不含pip的隔离环境。默认位置:`~/.local/share/vpsmaster/1.0.0b1/`。
|
||||||
|
|
||||||
|
`--run`安装后执行离线suite,不是联网all。看到Installed表示安装阶段完成;其后返回1可能表示体检发现问题或结果不完整,应分别查看输出。参数/完整性/安装失败通常返回2。不要跳过校验来解决文档与清单不一致问题。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
~/.local/share/vpsmaster/1.0.0b1/bin/vpsmaster --version
|
||||||
|
~/.local/share/vpsmaster/1.0.0b1/bin/vpsmaster all --lang zh
|
||||||
|
~/.local/share/vpsmaster/1.0.0b1/bin/vpsmaster all --offline --lang en
|
||||||
|
```
|
||||||
|
|
||||||
|
联网默认300秒、每模块60秒。Server纯文字控制台中文需要字体支持,可以使用英文或支持中文的SSH终端。Git代理不会自动应用到所有诊断探测,下载成功不证明检测目标TCP/TLS可达。
|
||||||
|
|
||||||
|
## 新目录安装与重复安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -I -B install.py --prefix "$HOME/vpsmaster-beta-test" --run
|
||||||
|
"$HOME/vpsmaster-beta-test/bin/vpsmaster" all --lang en
|
||||||
|
```
|
||||||
|
|
||||||
|
目标必须事先不存在,重复安装拒绝覆盖,不会擅自删除旧目录。安装器会打印实际入口路径。测试首次安装时可使用独立HOME或显式prefix,不能覆盖已有安装。
|
||||||
|
|
||||||
|
## ZIP安装(仅在取得对应发行文件时)
|
||||||
|
|
||||||
|
从可信发行渠道取得同一构建ZIP和SHA256SUMS.txt;[Releases](https://github.com/griffinmx/VPSMaster/releases)中Draft只对有权限的协作者可见,不等于公开发行文件。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sha256sum -c SHA256SUMS.txt
|
sha256sum -c SHA256SUMS.txt
|
||||||
@@ -13,30 +42,12 @@ cd VPSMaster-v1.0.0-beta.1
|
|||||||
python3 -I -B install.py --run
|
python3 -I -B install.py --run
|
||||||
```
|
```
|
||||||
|
|
||||||
默认目录为 `~/.local/share/vpsmaster/1.0.0b1`。`--run` 安装后运行离线 suite;检测有发现时可能返回 1,先查看安装器和检测各自的输出。
|
GitHub main的文档可能比旧Beta.1 ZIP更新,必须核对具体构建。校验和只验证内容一致性,不能独立证明发布者身份。
|
||||||
|
|
||||||
安装器检查根目录 `MANIFEST.sha256.json`,创建不含 pip 的隔离 venv,复制 Python 包并提供 `bin/vpsmaster` 与 `bin/proxydoctor`。不要自行移动清单中的文件。
|
## 更新、卸载与兼容
|
||||||
|
|
||||||
## 自定义目录与重复安装
|
更新时使用新源码目录和新的安装prefix,验证后再切换入口;旧报告不自动迁移。卸载前备份报告并核对安装路径,只删除自己确认的安装目录,不要删除整个~/.local/share。
|
||||||
|
|
||||||
```bash
|
vpsmaster与proxydoctor命令指向同一实现;内部Python包、JSON schema和PROXYDOCTOR_*环境变量保留。旧~/.local/share/proxydoctor/不自动迁移。源码运行仍支持`bash vpsmaster.sh ...`和`bash proxydoctor.sh ...`。
|
||||||
python3 -I -B install.py --prefix "$HOME/vpsmaster-beta-test"
|
|
||||||
"$HOME/vpsmaster-beta-test/bin/vpsmaster" --version
|
|
||||||
```
|
|
||||||
|
|
||||||
必须指定尚不存在的目录;重复安装拒绝覆盖。安装失败会清理本次新建的安装目录,保留既有安装。
|
[使用指南](USAGE.md) · [首页](../../README.md)
|
||||||
|
|
||||||
## 更新和卸载
|
|
||||||
|
|
||||||
更新时先核对新包校验和,安装到新的目录并验证,再切换自己使用的启动器路径。没有自动更新、旧报告迁移或自动卸载。卸载前备份报告、核对实际安装路径,然后仅删除自己确认的安装目录;不要删除整个 `~/.local/share`。
|
|
||||||
|
|
||||||
## 源码运行与兼容
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash vpsmaster.sh check --lang zh
|
|
||||||
bash proxydoctor.sh check --lang en
|
|
||||||
```
|
|
||||||
|
|
||||||
内部 Python 包与发行元数据仍名为 `proxydoctor`;现有 JSON schema、`PROXYDOCTOR_*` 环境变量保留。没有新增 `VPSMASTER_*` 环境变量别名。旧 `~/.local/share/proxydoctor/` 不自动迁移。
|
|
||||||
|
|
||||||
[使用指南](USAGE.md) · [返回首页](../../README.md)
|
|
||||||
@@ -49,3 +49,16 @@
|
|||||||
- **这些 CPU/内存是独享吗?** 只反映来宾可见资源及可见配额,不能证明宿主硬件独享。
|
- **这些 CPU/内存是独享吗?** 只反映来宾可见资源及可见配额,不能证明宿主硬件独享。
|
||||||
|
|
||||||
[安装指南](INSTALL.md) · [已知限制](../release/KNOWN_LIMITATIONS.md)
|
[安装指南](INSTALL.md) · [已知限制](../release/KNOWN_LIMITATIONS.md)
|
||||||
|
|
||||||
|
|
||||||
|
## all:面向初学者的一键入口
|
||||||
|
|
||||||
|
`all` 与 `suite` 共用参数、调度、worker、逐模块日志和报告;`suite` 保持原有执行范围。`all` 默认额外选择 BGP 与 ICMP/TCP(系统缺 traceroute 或权限时保留原因,不自动安装或 sudo)。`--skip-route` 同时跳过三种路由。`--offline` 不发起外部探测,所有联网模块明确跳过。
|
||||||
|
|
||||||
|
进度显示已完成模块数量,不虚构百分比。执行成功只表示模块运行结束,不能证明网络目标成功、安全、解锁或播放。`--show-unavailable` 可显示更多无数据条目;默认简洁展示仍保留每模块跳过/失败原因。
|
||||||
|
|
||||||
|
`--total-timeout 30..900` 设置总秒数,默认300、full600;`--module-timeout 5..120` 默认60。Linux CLI 主线程使用可恢复的作用域计时器取消阻塞操作;已有活动计时器、不支持计时器的平台或非主线程不能保证该边界时,明确标记不支持。到总预算后剩余模块记为跳过,总结果不完整。单模块超时记未知并尝试继续,其已落盘数据保留。计时预算不保证故障存储设备写入或外部服务端传输的严格实时时限。
|
||||||
|
|
||||||
|
默认串行、固定来源、有限响应和请求超时;UDP 最多64跳×3轮、位置查询上限12个公网地址。普通模式默认30跳;ICMP/TCP 不对无响应跳点作转发丢包结论。`--full` 启用 PTR/单目标TCP25与有界中国方向追踪,没有完整三网线路保证。
|
||||||
|
|
||||||
|
带宽须同时使用 `--full --allow-bandwidth`。按来源数×轮数×每方向样本×2进行保守测试载荷预算,最大64 MiB,超限在开始前拒绝;协议开销不计入载荷上限。大陆测速另需 `--china-speed`,可能需要降低 `--sample-mib` 以满足总预算。不会自动执行 CPU/磁盘基准。脱敏、独占保存、Ctrl+C恢复与旧命令一致。
|
||||||
@@ -26,4 +26,4 @@
|
|||||||
- Bandwidth `attempted` counts scheduled sample slots, including automatically
|
- Bandwidth `attempted` counts scheduled sample slots, including automatically
|
||||||
skipped slots; it is not an actual network-request counter. Inspect sample errors.
|
skipped slots; it is not an actual network-request counter. Inspect sample errors.
|
||||||
|
|
||||||
当前不具备一键安全加固或自动修复。未来遵循检测→解释→预览→确认→备份→执行→验证→回滚;回滚不能覆盖所有断网/重启/云安全组/宿主机故障。本轮只更新路线与私有发布准备。
|
当前不具备一键安全加固或自动修复。未来遵循检测→解释→预览→确认→备份→执行→验证→回滚;回滚不能覆盖所有断网/重启/云安全组/宿主机故障。未来阶段以[当前五阶段路线](../security/ROADMAP.md)为准。
|
||||||
@@ -28,8 +28,12 @@
|
|||||||
|
|
||||||
MIT许可,保留历史版权和第三方来源说明。仅准备 Beta 候选;不代表已经公开发布。
|
MIT许可,保留历史版权和第三方来源说明。仅准备 Beta 候选;不代表已经公开发布。
|
||||||
|
|
||||||
当前不具备一键安全加固或自动修复。未来遵循检测→解释→预览→确认→备份→执行→验证→回滚;回滚不能覆盖所有断网/重启/云安全组/宿主机故障。本轮只更新路线与私有发布准备。
|
当前不具备一键安全加固或自动修复。未来遵循检测→解释→预览→确认→备份→执行→验证→回滚;回滚不能覆盖所有断网/重启/云安全组/宿主机故障。未来阶段以[当前五阶段路线](../security/ROADMAP.md)为准。
|
||||||
|
|
||||||
## 仓库目录整理
|
## 仓库目录整理
|
||||||
|
|
||||||
README 中英文首页精简;指南、安全、开发、发行和历史材料分别归档。内部过程文档保留在本地备份,Git 历史不重写。Python 包、CLI 兼容入口与安装路径保持一致。目录和截图属于包内变更,因此另行构建、校验并从最终 ZIP 验收;原验收包保持不变。
|
README 中英文首页精简;指南、安全、开发、发行和历史材料分别归档。内部过程文档保留在本地备份,Git 历史不重写。Python 包、CLI 兼容入口与安装路径保持一致。目录和截图属于包内变更,因此另行构建、校验并从最终 ZIP 验收;原验收包保持不变。
|
||||||
|
|
||||||
|
## Beta.1 独立重建:all 综合入口
|
||||||
|
|
||||||
|
按所有者要求保持1.0.0b1版本标识。新增all复用suite机制、模块进度、时间预算、ICMP/TCP和BGP默认检测、最终汇总;full带宽仍需明确授权。与早期同名Beta.1 ZIP内容不同,必须用本次独立目录的SHA256核对;旧包保留。公开发行前需以最终包新验收为准。
|
||||||
+17
-31
@@ -1,45 +1,31 @@
|
|||||||
# Linux VPS 安全诊断与加固路线 / Roadmap
|
# VPSMaster 五阶段开发路线
|
||||||
|
|
||||||
项目从可信检测逐步发展为 Linux 服务器安全诊断与加固助手。Phase 1 是当前基础,Phase 2–5 均为 Planned,不属于当前 Beta,不承诺固定交付时间。
|
路线以[中文README](../../README.md)为准。Phase 1为当前Beta基础;Phase 2–5均为后续规划,尚未实现的能力不属于Beta.1,不承诺固定发布时间。
|
||||||
|
|
||||||
## Phase 1 — VPS 综合检测(当前 Beta)
|
## Phase 1 — VPS 综合检测
|
||||||
|
|
||||||
系统与可见资源、DNS/IPv4/IPv6/TCP/TLS、原生 UDP 与可选系统 ICMP/TCP 路由、BGP/ASN/IP 信息、基础只读安全体检、本地报告与恢复。系统依赖、权限和第三方数据决定实际覆盖;媒体仅公开网页可达性。[当前限制](../release/KNOWN_LIMITATIONS.md)
|
当前:系统与可见资源、接口/连通性/DNS/TCP/TLS、UDP及条件允许的ICMP/TCP、IP/ASN/BGP、公开网页可达性、只读安全体检与本地JSON/Markdown/恢复日志。实际覆盖取决于工具、权限及第三方来源,网页可达不等于解锁或播放。
|
||||||
|
|
||||||
## Phase 2 — 检测能力完善(Planned)
|
## Phase 2 — 检测能力完善与备份回滚(Planned)
|
||||||
|
|
||||||
- CPU、磁盘性能基准,独立明确授权和负载预算。
|
完善CLI彩色综合报表、CPU/磁盘性能基准、三网测速、IP情报交叉验证、线路建议与安全基线。建立配置快照、差异对比、基础恢复和可验证的恢复流程,为部署与加固提供保障。负载测试仍需单独明确授权和资源预算。
|
||||||
- 中国大陆电信、联通、移动测速覆盖和可靠性完善。
|
|
||||||
- 扩展 IP 情报交叉验证、网络线路分析与诊断建议。
|
|
||||||
- 更完整的 Linux 安全基线、VPS 购买验收和性能分析。
|
|
||||||
|
|
||||||
## Phase 3 — 一键安全加固(Planned,未来重点)
|
## Phase 3 — 一键代理部署(Planned)
|
||||||
|
|
||||||
- SSH 配置、登录保护、密钥认证策略。
|
常见代理协议的一键安装、服务配置、状态检测、更新与卸载。初期优先无需域名的基础方式,再逐步支持域名、证书及订阅管理。强调配置和凭据安全、服务生命周期及故障诊断,不用公网明文传输暴露订阅凭据。
|
||||||
- UFW/nftables 配置及 Fail2ban 暴力破解防护。
|
|
||||||
- 系统更新/补丁检查,用户权限与文件权限加固。
|
|
||||||
- 不必要服务与开放端口管理,Linux 安全基线。
|
|
||||||
- 可选择的加固方案、风险解释与修改预览。
|
|
||||||
|
|
||||||
不会默认自动修改服务器配置。执行必须经过用户明确确认;检测到风险不等于获得修改授权。不通过关闭安全保护或放宽权限“修复”。
|
## Phase 4 — 一键安全加固(Planned,未来重点)
|
||||||
|
|
||||||
## Phase 4 — 安全备份、回滚与持续监控(Planned)
|
SSH登录保护与密钥策略、UFW/nftables、Fail2ban、系统更新、权限和端口管理。提供方案选择、风险解释、修改预览、执行验证和有条件回滚。
|
||||||
|
|
||||||
- 修改前备份 SSH、防火墙及相关配置,展示变更差异。
|
涉及SSH、防火墙或其他可能影响连接的变更,必须经用户明确确认。遵循:
|
||||||
- 验证并保留 SSH 管理通道,考虑业务端口和云安全组,执行后重新验证连接。
|
|
||||||
- 在具备可靠机制时提供超时回滚、操作审计及恢复向导。
|
|
||||||
- 定期安全体检、配置漂移、异常登录提醒、网络稳定性与历史报告对比。
|
|
||||||
|
|
||||||
重点避免用户加固 SSH/防火墙时失去管理访问;不能承诺自动恢复全部断网、重启、云安全组或宿主故障。涉及高风险变更不得无人确认执行,必须说明回滚前提与人工恢复入口。
|
|
||||||
|
|
||||||
## Phase 5 — Web UI 与可视化管理(长期规划)
|
|
||||||
|
|
||||||
本地 Web 面板、服务器状态、安全风险和加固任务、备份/恢复界面、多 VPS 及可选服务部署维护。认证、权限、敏感凭据及访问安全是前提,默认不向公网开放管理入口。
|
|
||||||
|
|
||||||
## 长期安全原则
|
|
||||||
|
|
||||||
**检测 → 解释 → 预览 → 确认 → 备份 → 执行 → 验证 → 回滚**
|
**检测 → 解释 → 预览 → 确认 → 备份 → 执行 → 验证 → 回滚**
|
||||||
|
|
||||||
默认只读、显式授权、幂等与审计、管理连接及业务端口保护。未知不能视为安全;有依据地描述风险,不提供无法解释的绝对安全评分。
|
保护SSH管理连接和既有业务端口;不默认执行高风险无人确认变更,不通过关闭安全保护“修复”。回滚需可靠前提,不承诺恢复全部断网、云安全组、重启或宿主故障。
|
||||||
|
|
||||||
[首页](../../README.md) · [安全反馈](SECURITY.md)
|
## Phase 5 — Web UI 与服务器管理(Planned,长期规划)
|
||||||
|
|
||||||
|
本地Web面板、检测报告展示、安全风险、部署服务、备份恢复、多VPS与可选服务管理。默认不向公网开放管理入口,认证、权限与凭据保护是基础要求。
|
||||||
|
|
||||||
|
[安全反馈](SECURITY.md) · [当前限制](../release/KNOWN_LIMITATIONS.md)
|
||||||
+17
-3
@@ -121,7 +121,10 @@ def _main():
|
|||||||
media.add_argument('--lang',choices=('zh','en'),default=lang)
|
media.add_argument('--lang',choices=('zh','en'),default=lang)
|
||||||
media.add_argument('--color',choices=('auto','always','never'),default='auto')
|
media.add_argument('--color',choices=('auto','always','never'),default='auto')
|
||||||
media.add_argument('--json-output',type=Path,help=st('保存脱敏检测依据,不覆盖文件','Save redacted evidence; never overwrite'))
|
media.add_argument('--json-output',type=Path,help=st('保存脱敏检测依据,不覆盖文件','Save redacted evidence; never overwrite'))
|
||||||
suite = sub.add_parser('suite',lang=lang,help=st('VPS / 网络质量 / 路由 / 流媒体','VPS / network / routes / streaming'))
|
suite = sub.add_parser('suite',aliases=['all'],lang=lang,help=st('一键综合检测:all(suite 兼容入口)','One-command diagnostics: all (suite compatible)'))
|
||||||
|
suite.add_argument('--full',action='store_true',help=st('all 扩展模式;带宽仍需独立授权','Extended all mode; bandwidth still requires consent'))
|
||||||
|
suite.add_argument('--total-timeout',type=int,choices=range(30,901),metavar='30..900',help=st('all 总秒数预算,默认300/full600','all total seconds; default 300/full600'))
|
||||||
|
suite.add_argument('--module-timeout',type=int,choices=range(5,121),default=60,metavar='5..120',help=st('all 每模块秒数预算,默认60','all per-module seconds, default 60'))
|
||||||
suite.add_argument('--lang',choices=('zh','en'),default=lang)
|
suite.add_argument('--lang',choices=('zh','en'),default=lang)
|
||||||
suite.add_argument('--allow-bandwidth',action='store_true',help=st('明确允许带宽测速(默认跳过)','Consent to bandwidth tests (skipped by default)'))
|
suite.add_argument('--allow-bandwidth',action='store_true',help=st('明确允许带宽测速(默认跳过)','Consent to bandwidth tests (skipped by default)'))
|
||||||
suite.add_argument('--offline',action='store_true',help=st('跳过联网检测','Skip outbound requests'))
|
suite.add_argument('--offline',action='store_true',help=st('跳过联网检测','Skip outbound requests'))
|
||||||
@@ -259,7 +262,17 @@ def _main():
|
|||||||
incomplete=any(item.get('state')!='reachable' for item in scan.report['media'].values())
|
incomplete=any(item.get('state')!='reachable' for item in scan.report['media'].values())
|
||||||
if incomplete:scan.report['result']='incomplete'
|
if incomplete:scan.report['result']='incomplete'
|
||||||
sys.exit(1 if incomplete or scan.failed else 0)
|
sys.exit(1 if incomplete or scan.failed else 0)
|
||||||
if args.command == 'suite':
|
if args.command in ('suite','all'):
|
||||||
|
is_all=args.command=='all'
|
||||||
|
if args.full and not is_all:parser.error('--full requires all')
|
||||||
|
if is_all and args.allow_bandwidth and not args.full:parser.error('all bandwidth requires --full --allow-bandwidth')
|
||||||
|
if is_all and args.full:
|
||||||
|
args.mail_probes=True
|
||||||
|
args.include_return_routes=True
|
||||||
|
if is_all and args.allow_bandwidth:
|
||||||
|
selected=args.bandwidth_source or (CHINA_SOURCES if args.china_speed else ('cloudflare','ovh'))
|
||||||
|
if args.sample_mib*args.bandwidth_rounds*len(set(selected))*2>64:
|
||||||
|
parser.error('all bandwidth payload budget is at most 64 MiB; reduce sources/sample size/rounds')
|
||||||
if not 1<=args.tcp_port<=65535:parser.error(t('端口必须在 1~65535 之间'))
|
if not 1<=args.tcp_port<=65535:parser.error(t('端口必须在 1~65535 之间'))
|
||||||
if args.china_speed and not args.allow_bandwidth:
|
if args.china_speed and not args.allow_bandwidth:
|
||||||
parser.error(st('--china-speed 需要 --allow-bandwidth','--china-speed requires --allow-bandwidth'))
|
parser.error(st('--china-speed 需要 --allow-bandwidth','--china-speed requires --allow-bandwidth'))
|
||||||
@@ -280,7 +293,8 @@ def _main():
|
|||||||
max_hops=args.max_hops,geo=not args.no_geo,color=args.color,
|
max_hops=args.max_hops,geo=not args.no_geo,color=args.color,
|
||||||
bandwidth_rounds=args.bandwidth_rounds,skip_route=args.skip_route,json_output=args.json_output,
|
bandwidth_rounds=args.bandwidth_rounds,skip_route=args.skip_route,json_output=args.json_output,
|
||||||
bandwidth_sources=args.bandwidth_source or (CHINA_SOURCES if args.china_speed else None),port=args.port,show_ip=args.show_ip,ip_enrichment=not args.no_ip_enrichment,allow_bandwidth=args.allow_bandwidth,mail_probes=args.mail_probes,route_probes=args.route_probes,theme=args.theme,
|
bandwidth_sources=args.bandwidth_source or (CHINA_SOURCES if args.china_speed else None),port=args.port,show_ip=args.show_ip,ip_enrichment=not args.no_ip_enrichment,allow_bandwidth=args.allow_bandwidth,mail_probes=args.mail_probes,route_probes=args.route_probes,theme=args.theme,
|
||||||
route_method=args.route_method,tcp_port=args.tcp_port,include_return_routes=args.include_return_routes,include_bgp=args.include_bgp,show_unavailable=args.show_unavailable)
|
route_method='udp' if is_all else args.route_method,tcp_port=args.tcp_port,include_return_routes=args.include_return_routes,include_bgp=args.include_bgp or is_all,show_unavailable=args.show_unavailable,
|
||||||
|
comprehensive=is_all,total_timeout=args.total_timeout or (600 if args.full else 300),module_timeout=args.module_timeout)
|
||||||
except OSError:
|
except OSError:
|
||||||
parser.error(st('无法保存报告,请检查输出目录和文件权限','Unable to save report; check the output directory and file permissions'))
|
parser.error(st('无法保存报告,请检查输出目录和文件权限','Unable to save report; check the output directory and file permissions'))
|
||||||
sys.exit(code)
|
sys.exit(code)
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ class Console:
|
|||||||
def __init__(self, lang='zh', color='auto', hide_unavailable=False):
|
def __init__(self, lang='zh', color='auto', hide_unavailable=False):
|
||||||
self.lang = lang
|
self.lang = lang
|
||||||
self.hide_unavailable = hide_unavailable
|
self.hide_unavailable = hide_unavailable
|
||||||
|
self.wrap_output = False
|
||||||
self.enabled = color == 'always' or (color == 'auto' and sys.stdout.isatty() and 'NO_COLOR' not in os.environ)
|
self.enabled = color == 'always' or (color == 'auto' and sys.stdout.isatty() and 'NO_COLOR' not in os.environ)
|
||||||
self._framed = False # when True, row/keyvalue/section render inside a box
|
self._framed = False # when True, row/keyvalue/section render inside a box
|
||||||
|
|
||||||
@@ -102,13 +103,20 @@ class Console:
|
|||||||
return
|
return
|
||||||
label = clean(label)
|
label = clean(label)
|
||||||
w = width(label)
|
w = width(label)
|
||||||
|
if self.wrap_output:
|
||||||
|
for piece in wrap_columns(label+' '+clean(value),max(12,shutil.get_terminal_size().columns)):
|
||||||
|
print(self.paint(piece,status),flush=True)
|
||||||
|
return
|
||||||
print(label + ' ' * max(1, 19 - w) + self.paint(value, status), flush=True)
|
print(label + ' ' * max(1, 19 - w) + self.paint(value, status), flush=True)
|
||||||
|
|
||||||
def line(self, value='', status='ok'):
|
def line(self, value='', status='ok'):
|
||||||
if self._framed:
|
if self._framed:
|
||||||
self.frame_line(value, status)
|
self.frame_line(value, status)
|
||||||
else:
|
else:
|
||||||
print(self.paint(value, status, preserve_space=True), flush=True)
|
if self.wrap_output:
|
||||||
|
for piece in wrap_columns(value,max(12,shutil.get_terminal_size().columns)):
|
||||||
|
print(self.paint(piece,status),flush=True)
|
||||||
|
else:print(self.paint(value, status, preserve_space=True), flush=True)
|
||||||
|
|
||||||
def section(self, index, zh, en):
|
def section(self, index, zh, en):
|
||||||
if self._framed:
|
if self._framed:
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
"""Scoped Linux main-thread deadlines; no background thread left doing work."""
|
||||||
|
from contextlib import contextmanager
|
||||||
|
import signal
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleTimeout(BaseException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DeadlineUnsupported(BaseException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def bounded(seconds):
|
||||||
|
if seconds is None:
|
||||||
|
yield
|
||||||
|
return
|
||||||
|
if not hasattr(signal, 'setitimer') or threading.current_thread() is not threading.main_thread():
|
||||||
|
raise DeadlineUnsupported()
|
||||||
|
previous_handler = signal.getsignal(signal.SIGALRM)
|
||||||
|
previous_timer = signal.getitimer(signal.ITIMER_REAL)
|
||||||
|
# Do not displace a caller's active alarm.
|
||||||
|
if previous_timer[0]:
|
||||||
|
raise DeadlineUnsupported()
|
||||||
|
def expired(*_):
|
||||||
|
raise ModuleTimeout()
|
||||||
|
signal.signal(signal.SIGALRM, expired)
|
||||||
|
signal.setitimer(signal.ITIMER_REAL, max(0.001, seconds))
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
signal.setitimer(signal.ITIMER_REAL, 0)
|
||||||
|
signal.signal(signal.SIGALRM, previous_handler)
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
"""User-facing summary of existing evidence; never invent a safety score."""
|
||||||
|
def render_summary(report,out):
|
||||||
|
tr=out.text
|
||||||
|
out.line(tr('综合汇总(执行完成不代表目标全部可用)','Summary (execution is not target availability)'),'title')
|
||||||
|
checks={r['id']:r for r in (report.get('system') or {}).get('checks',[])}
|
||||||
|
system=checks.get('system',{})
|
||||||
|
out.row(tr('系统','System'),system.get('evidence',tr('未确认','Unconfirmed')))
|
||||||
|
allocation=(report.get('resources') or {}).get('allocation',{})
|
||||||
|
out.row(tr('资源','Resources'),f"CPU {allocation.get('cpu_limit_cores','?')} vCPU; RAM {allocation.get('memory_limit_bytes','?')} B",'unknown')
|
||||||
|
network=report.get('network') or {}
|
||||||
|
dns=network.get('dns',[]);transport=network.get('transports',[])
|
||||||
|
out.row('DNS / TCP / TLS',f"DNS {sum(r.get('status')=='success' for r in dns)}/{len(dns)}; TCP {sum(r.get('tcp',{}).get('status')=='success' for r in transport)}/{len(transport)}; TLS {sum(r.get('tls',{}).get('status')=='success' for r in transport)}/{len(transport)}",'unknown')
|
||||||
|
ip=report.get('ip_quality') or {}
|
||||||
|
values=ip.get('values') or {}
|
||||||
|
out.row('IP / ASN',str(values.get('asn') or 'unknown')+' · '+str(values.get('country') or 'unknown'),'unknown')
|
||||||
|
routes={'udp':report.get('route') or {},**report.get('additional_routes',{})}
|
||||||
|
for method,result in routes.items():out.row(method.upper(),result.get('state','not_run'),'unknown')
|
||||||
|
media=report.get('summary',{}).get('media',{})
|
||||||
|
out.row(tr('网页可达','Pages reachable'),f"{media.get('reachable',0)}/{len(report.get('media',{}))} · "+tr('不代表解锁或播放','Not unlocking/playback'),'unknown')
|
||||||
|
security=report.get('security') or {}
|
||||||
|
high=sum(bool(set(row.get('reasons',[])) & {'WORLD_WRITABLE','SENSITIVE_WORLD_READABLE'}) for row in security.get('files',[]))
|
||||||
|
out.row(tr('权限风险提醒','Permission risk advisory'),f'high {high}; review {security.get("review_count","?")}', 'bad' if high else 'unknown')
|
||||||
|
out.row(tr('安全审阅','Security review'),str(security.get('review_count','unknown'))+' · '+tr('只读发现需人工评估,非漏洞严重度认证','Read-only findings need review, not certified vulnerability severity'),'unknown')
|
||||||
|
counts=report.get('summary',{}).get('module_counts',{})
|
||||||
|
out.row(tr('模块统计','Module counts'),'; '.join(tr(zh,en)+' '+str(counts.get(key,0)) for key,zh,en in
|
||||||
|
[('success','完成','completed'),('failure','失败','failed'),('skipped','跳过','skipped'),('unknown','未知','unknown'),('unsupported','不支持','unsupported'),('cancelled','中断','cancelled')]),'title')
|
||||||
|
out.line(tr('关注:未知/超时不等于安全;检查报告中的错误与权限,再决定修复。','Action: unknown/timeouts are not safe; review errors and permissions before repair.'),'unknown')
|
||||||
+24
-2
@@ -1,27 +1,48 @@
|
|||||||
"""Small synchronous module boundary; no global process or signal changes."""
|
"""Small synchronous module boundary; no global process or signal changes."""
|
||||||
from .reports import utc_now
|
from .reports import utc_now
|
||||||
|
import time
|
||||||
|
from .limits import bounded, ModuleTimeout, DeadlineUnsupported
|
||||||
|
|
||||||
|
|
||||||
class ModuleRunner:
|
class ModuleRunner:
|
||||||
def __init__(self, report, checkpoint, notify):
|
def __init__(self, report, checkpoint, notify, observe=None, deadline=None, module_timeout=None):
|
||||||
self.report = report
|
self.report = report
|
||||||
self.checkpoint = checkpoint
|
self.checkpoint = checkpoint
|
||||||
self.notify = notify
|
self.notify = notify
|
||||||
|
self.observe = observe
|
||||||
|
self.deadline = deadline
|
||||||
|
self.module_timeout = module_timeout
|
||||||
report['modules'] = {}
|
report['modules'] = {}
|
||||||
|
|
||||||
def run(self, name, operation=None, skip_reason=None):
|
def run(self, name, operation=None, skip_reason=None):
|
||||||
|
if not skip_reason and self.deadline is not None and time.monotonic() >= self.deadline:
|
||||||
|
skip_reason = 'TOTAL_TIME_BUDGET'
|
||||||
state = {'status': 'skipped' if skip_reason else 'running',
|
state = {'status': 'skipped' if skip_reason else 'running',
|
||||||
'started_at': utc_now(), 'error_code': None}
|
'started_at': utc_now(), 'error_code': None}
|
||||||
self.report['modules'][name] = state
|
self.report['modules'][name] = state
|
||||||
|
if self.observe:self.observe(name, state, False)
|
||||||
if skip_reason:
|
if skip_reason:
|
||||||
state['reason'] = skip_reason
|
state['reason'] = skip_reason
|
||||||
state['finished_at'] = utc_now()
|
state['finished_at'] = utc_now()
|
||||||
self.checkpoint()
|
self.checkpoint()
|
||||||
|
if self.observe:self.observe(name, state, True)
|
||||||
return
|
return
|
||||||
self.checkpoint()
|
self.checkpoint()
|
||||||
try:
|
try:
|
||||||
operation()
|
seconds = self.module_timeout
|
||||||
|
if self.deadline is not None:
|
||||||
|
seconds = min(seconds, max(0.001, self.deadline-time.monotonic()))
|
||||||
|
with bounded(seconds):
|
||||||
|
result = operation()
|
||||||
state['status'] = 'success'
|
state['status'] = 'success'
|
||||||
|
if isinstance(result,dict) and result.get('status') in ('unknown','skipped','unsupported'):
|
||||||
|
state.update(result)
|
||||||
|
except ModuleTimeout:
|
||||||
|
from .worker import cancel_all, reset
|
||||||
|
cancel_all();reset()
|
||||||
|
state.update(status='unknown', error_code='MODULE_TIMEOUT')
|
||||||
|
except DeadlineUnsupported:
|
||||||
|
state.update(status='unsupported', error_code='DEADLINE_UNSUPPORTED')
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
state.update(status='cancelled', error_code='CANCELLED')
|
state.update(status='cancelled', error_code='CANCELLED')
|
||||||
raise
|
raise
|
||||||
@@ -33,3 +54,4 @@ class ModuleRunner:
|
|||||||
finally:
|
finally:
|
||||||
state['finished_at'] = utc_now()
|
state['finished_at'] = utc_now()
|
||||||
self.checkpoint()
|
self.checkpoint()
|
||||||
|
if self.observe:self.observe(name, state, True)
|
||||||
@@ -39,6 +39,9 @@ def normalize_report(report):
|
|||||||
for i,item in enumerate(route.get('hops',[])):
|
for i,item in enumerate(route.get('hops',[])):
|
||||||
add(f'route.hop.{i}','route','success' if item.get('state') in ('reply','reached') else 'unknown',
|
add(f'route.hop.{i}','route','success' if item.get('state') in ('reply','reached') else 'unknown',
|
||||||
'single_'+route.get('method','udp')+'_probe',item.get('ip'),item,limitations=['One probe per TTL; response loss is not forwarding loss.'])
|
'single_'+route.get('method','udp')+'_probe',item.get('ip'),item,limitations=['One probe per TTL; response loss is not forwarding loss.'])
|
||||||
|
for method,item in report.get('additional_routes',{}).items():
|
||||||
|
add('route.'+method,'route','success' if item.get('state')=='reached' else 'skipped' if item.get('state')=='dependency_missing' else 'unknown',
|
||||||
|
method+'_trace',item.get('target'),item,item.get('error_code'),['Router silence is not forwarding loss.'])
|
||||||
for name in ('bgp','carrier_nodes','return_routes'):
|
for name in ('bgp','carrier_nodes','return_routes'):
|
||||||
item=report.get(name)
|
item=report.get(name)
|
||||||
if item:
|
if item:
|
||||||
|
|||||||
+64
-4
@@ -25,7 +25,8 @@ def execute_provider(argv, timeout=180):
|
|||||||
|
|
||||||
def run_suite(lang='zh',offline=False,sample_mib=4,route_target='1.1.1.1',besttrace=None,
|
def run_suite(lang='zh',offline=False,sample_mib=4,route_target='1.1.1.1',besttrace=None,
|
||||||
region_script=None,max_hops=30,geo=True,color='auto',bandwidth_rounds=3,skip_route=False,json_output=None,bandwidth_sources=None,port=8388,show_ip=False,ip_enrichment=True,allow_bandwidth=False,mail_probes=False,route_probes=3,theme='default',
|
region_script=None,max_hops=30,geo=True,color='auto',bandwidth_rounds=3,skip_route=False,json_output=None,bandwidth_sources=None,port=8388,show_ip=False,ip_enrichment=True,allow_bandwidth=False,mail_probes=False,route_probes=3,theme='default',
|
||||||
route_method='udp',tcp_port=443,include_return_routes=False,include_bgp=False,show_unavailable=False):
|
route_method='udp',tcp_port=443,include_return_routes=False,include_bgp=False,show_unavailable=False,
|
||||||
|
comprehensive=False,total_timeout=300,module_timeout=60):
|
||||||
from .cli import redact
|
from .cli import redact
|
||||||
from .console import Console
|
from .console import Console
|
||||||
import re
|
import re
|
||||||
@@ -37,6 +38,10 @@ def run_suite(lang='zh',offline=False,sample_mib=4,route_target='1.1.1.1',besttr
|
|||||||
if route_method not in ('udp','icmp','tcp') or not 1<=tcp_port<=65535 or theme not in ('default','iprisk','framed'):
|
if route_method not in ('udp','icmp','tcp') or not 1<=tcp_port<=65535 or theme not in ('default','iprisk','framed'):
|
||||||
raise ValueError('Invalid route or theme options')
|
raise ValueError('Invalid route or theme options')
|
||||||
out=Console(lang,color,hide_unavailable=not show_unavailable); tr=out.text
|
out=Console(lang,color,hide_unavailable=not show_unavailable); tr=out.text
|
||||||
|
out.wrap_output=comprehensive
|
||||||
|
started=time.monotonic()
|
||||||
|
if comprehensive:include_bgp=True
|
||||||
|
if comprehensive and not (30<=total_timeout<=900 and 5<=module_timeout<=120):raise ValueError('Invalid all time budget')
|
||||||
iprisk = theme == 'iprisk'
|
iprisk = theme == 'iprisk'
|
||||||
framed = theme == 'framed'
|
framed = theme == 'framed'
|
||||||
from datetime import datetime,timezone
|
from datetime import datetime,timezone
|
||||||
@@ -82,7 +87,30 @@ def run_suite(lang='zh',offline=False,sample_mib=4,route_target='1.1.1.1',besttr
|
|||||||
def module_error(name):
|
def module_error(name):
|
||||||
out.row(name, tr('本模块异常,继续其他检测(MODULE_ERROR)','Module failed; continuing other checks (MODULE_ERROR)'),'bad')
|
out.row(name, tr('本模块异常,继续其他检测(MODULE_ERROR)','Module failed; continuing other checks (MODULE_ERROR)'),'bad')
|
||||||
|
|
||||||
runner = ModuleRunner(report, checkpoint, module_error)
|
labels={'system':('系统与接口','System and interfaces'),'resources':('资源状态','Resources'),
|
||||||
|
'security':('安全体检','Security'),'bandwidth':('带宽','Bandwidth'),'network':('DNS / TCP / TLS','DNS / TCP / TLS'),
|
||||||
|
'ip_quality':('IP 信息','IP information'),'route':('UDP 路由','UDP route'),
|
||||||
|
'route_icmp':('ICMP 路由','ICMP route'),'route_tcp':('TCP 路由','TCP route'),
|
||||||
|
'media':('网页可达性','Page reachability'),'bgp':('ASN / BGP','ASN / BGP'),'return_routes':('中国方向追踪','China destination routes')}
|
||||||
|
planned=8+(3 if comprehensive else int(include_bgp))+int(include_return_routes)
|
||||||
|
completed=0
|
||||||
|
def progress(name,state,finished):
|
||||||
|
nonlocal completed
|
||||||
|
title=tr(*labels.get(name,(name,name)))
|
||||||
|
if finished:
|
||||||
|
completed+=1
|
||||||
|
status=state['status']
|
||||||
|
out.row(tr('进度','Progress'),f'{completed}/{planned} '+title+' · '+status+
|
||||||
|
(' · '+str(state.get('error_code') or state.get('reason')) if state.get('error_code') or state.get('reason') else ''),
|
||||||
|
'bad' if status=='failure' else 'ok' if status=='success' else 'unknown')
|
||||||
|
else:out.row(tr('正在执行','Running'),f'{completed}/{planned} · '+title,'title')
|
||||||
|
if comprehensive:
|
||||||
|
report['command']='all'
|
||||||
|
report['limits']={'total_seconds':total_timeout,'module_seconds':module_timeout,'concurrency':1,'bandwidth_payload_max_mib':64}
|
||||||
|
out.row(tr('检测预算','Time budget'),f'{total_timeout}s / module {module_timeout}s','title')
|
||||||
|
runner = ModuleRunner(report, checkpoint, module_error,observe=progress if comprehensive else None,
|
||||||
|
deadline=started+total_timeout if comprehensive else None,
|
||||||
|
module_timeout=module_timeout if comprehensive else None)
|
||||||
|
|
||||||
def heading(number, zh, en, kind='b_cyan'):
|
def heading(number, zh, en, kind='b_cyan'):
|
||||||
if iprisk:
|
if iprisk:
|
||||||
@@ -122,6 +150,15 @@ def run_suite(lang='zh',offline=False,sample_mib=4,route_target='1.1.1.1',besttr
|
|||||||
if key=='ssh' and item['status']=='warn': value=tr('允许 root 或密码认证','Root or password auth allowed')
|
if key=='ssh' and item['status']=='warn': value=tr('允许 root 或密码认证','Root or password auth allowed')
|
||||||
out.row(tr(zh,en),value,'bad' if item['status'] in ('warn','fail') else 'ok' if item['status']=='pass' else 'unknown')
|
out.row(tr(zh,en),value,'bad' if item['status'] in ('warn','fail') else 'ok' if item['status']=='pass' else 'unknown')
|
||||||
out.row('CPU benchmark',tr('未运行:常规检测不执行性能基准','Not run: routine checks do not benchmark CPU'),'unknown')
|
out.row('CPU benchmark',tr('未运行:常规检测不执行性能基准','Not run: routine checks do not benchmark CPU'),'unknown')
|
||||||
|
if comprehensive:
|
||||||
|
for key in ('interfaces','addresses'):
|
||||||
|
item=checks.get(key)
|
||||||
|
if item:out.row(tr('网络接口','Interfaces'),item.get('evidence',''),'unknown')
|
||||||
|
from .worker import local_command
|
||||||
|
tool=shutil.which('systemd-detect-virt',path='/usr/bin:/bin')
|
||||||
|
code,value,error=local_command([tool],timeout=3) if tool else (None,'','COMMAND_UNAVAILABLE')
|
||||||
|
report['virtualization']={'value':value.strip() if code==0 else None,'method':'systemd-detect-virt','error_code':error if code else None}
|
||||||
|
out.row(tr('虚拟化','Virtualization'),value.strip() if code==0 else tr('未确认','Unconfirmed'),'ok' if code==0 else 'unknown')
|
||||||
|
|
||||||
def collect_bandwidth():
|
def collect_bandwidth():
|
||||||
nonlocal data, incomplete
|
nonlocal data, incomplete
|
||||||
@@ -243,11 +280,24 @@ def run_suite(lang='zh',offline=False,sample_mib=4,route_target='1.1.1.1',besttr
|
|||||||
if not address:
|
if not address:
|
||||||
report['bgp']={'status':'unknown','error_code':'EGRESS_IP_UNAVAILABLE'}
|
report['bgp']={'status':'unknown','error_code':'EGRESS_IP_UNAVAILABLE'}
|
||||||
out.row('BGP','EGRESS_IP_UNAVAILABLE','unknown');incomplete=True
|
out.row('BGP','EGRESS_IP_UNAVAILABLE','unknown');incomplete=True
|
||||||
return
|
return {'status':'unknown','error_code':'EGRESS_IP_UNAVAILABLE'}
|
||||||
report['bgp']={}
|
report['bgp']={}
|
||||||
collect(address,report['bgp'],checkpoint);render(report['bgp'],out)
|
collect(address,report['bgp'],checkpoint);render(report['bgp'],out)
|
||||||
incomplete=report['bgp']['status']!='success' or incomplete
|
incomplete=report['bgp']['status']!='success' or incomplete
|
||||||
|
|
||||||
|
def additional_route(method):
|
||||||
|
nonlocal incomplete
|
||||||
|
route={'target':route_target,'method':method,'hops':[]}
|
||||||
|
report.setdefault('additional_routes',{})[method]=route
|
||||||
|
protocol_trace(route_target,method=method,tcp_port=tcp_port,hops=max_hops,lang=lang,
|
||||||
|
geo=False,emit=lambda text,**kw:out.line(text,'unknown'),records=route['hops'],
|
||||||
|
summary=route,on_result=checkpoint,deadline=time.monotonic()+module_timeout)
|
||||||
|
state=route.get('state')
|
||||||
|
if state!='reached':
|
||||||
|
incomplete=True
|
||||||
|
status='skipped' if state=='dependency_missing' else 'unsupported' if state=='unsupported' else 'unknown'
|
||||||
|
return {'status':status,'error_code':route.get('error_code') or 'ROUTE_INCOMPLETE'}
|
||||||
|
|
||||||
cancelled = False
|
cancelled = False
|
||||||
try:
|
try:
|
||||||
runner.run('system', collect_system)
|
runner.run('system', collect_system)
|
||||||
@@ -264,6 +314,9 @@ def run_suite(lang='zh',offline=False,sample_mib=4,route_target='1.1.1.1',besttr
|
|||||||
heading(3,'路由追踪','Route trace')
|
heading(3,'路由追踪','Route trace')
|
||||||
out.row(tr('路由','Route'),tr('已跳过','Skipped'),'unknown')
|
out.row(tr('路由','Route'),tr('已跳过','Skipped'),'unknown')
|
||||||
runner.run('route', collect_route, 'offline' if offline else 'user_skipped' if skip_route else None)
|
runner.run('route', collect_route, 'offline' if offline else 'user_skipped' if skip_route else None)
|
||||||
|
if comprehensive:
|
||||||
|
for method in ('icmp','tcp'):
|
||||||
|
runner.run('route_'+method,lambda m=method:additional_route(m),'offline' if offline else 'user_skipped' if skip_route else None)
|
||||||
if offline and show_unavailable:
|
if offline and show_unavailable:
|
||||||
heading(4,'流媒体可达性','Media reachability')
|
heading(4,'流媒体可达性','Media reachability')
|
||||||
out.row(tr('流媒体','Media'),tr('离线跳过','Skipped offline'),'unknown')
|
out.row(tr('流媒体','Media'),tr('离线跳过','Skipped offline'),'unknown')
|
||||||
@@ -276,7 +329,7 @@ def run_suite(lang='zh',offline=False,sample_mib=4,route_target='1.1.1.1',besttr
|
|||||||
cancelled = True
|
cancelled = True
|
||||||
finally:
|
finally:
|
||||||
report['finished_at'] = utc_now()
|
report['finished_at'] = utc_now()
|
||||||
failed = any(item['status'] == 'failure' for item in report['modules'].values())
|
failed = any(item['status'] in ('failure','unknown','unsupported') or item.get('reason')=='TOTAL_TIME_BUDGET' for item in report['modules'].values())
|
||||||
report['result'] = 'cancelled' if cancelled else 'incomplete' if incomplete or failed else 'local_execution_complete'
|
report['result'] = 'cancelled' if cancelled else 'incomplete' if incomplete or failed else 'local_execution_complete'
|
||||||
report['summary'] = {
|
report['summary'] = {
|
||||||
'playback': 'not_tested',
|
'playback': 'not_tested',
|
||||||
@@ -287,11 +340,18 @@ def run_suite(lang='zh',offline=False,sample_mib=4,route_target='1.1.1.1',besttr
|
|||||||
for item in report['media'].values())
|
for item in report['media'].values())
|
||||||
for state in ('reachable','access_denied','rate_limited','http_error','unknown')},
|
for state in ('reachable','access_denied','rate_limited','http_error','unknown')},
|
||||||
}
|
}
|
||||||
|
if comprehensive:
|
||||||
|
from collections import Counter
|
||||||
|
report['summary']['module_counts']=dict(Counter(item['status'] for item in report['modules'].values()))
|
||||||
|
report['summary']['module_count_note']='Execution success does not imply target availability or safety.'
|
||||||
from .schema import normalize_report
|
from .schema import normalize_report
|
||||||
normalize_report(report)
|
normalize_report(report)
|
||||||
saved = store.finish(report)
|
saved = store.finish(report)
|
||||||
store.close()
|
store.close()
|
||||||
out.row(tr('体检结果','Results'), report['result'], 'unknown' if cancelled or incomplete or failed else 'ok')
|
out.row(tr('体检结果','Results'), report['result'], 'unknown' if cancelled or incomplete or failed else 'ok')
|
||||||
|
if comprehensive:
|
||||||
|
from .overview import render_summary
|
||||||
|
render_summary(report,out)
|
||||||
if saved and not store.failed:
|
if saved and not store.failed:
|
||||||
out.row(tr('脱敏报告','Redacted report'), str(store.path))
|
out.row(tr('脱敏报告','Redacted report'), str(store.path))
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -83,6 +83,9 @@ def invoke(action,options,timeout=20):
|
|||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
cancel_all();process.communicate()
|
cancel_all();process.communicate()
|
||||||
raise
|
raise
|
||||||
|
except BaseException:
|
||||||
|
process.kill();process.communicate()
|
||||||
|
raise
|
||||||
finally:
|
finally:
|
||||||
with _lock:_active.discard(process)
|
with _lock:_active.discard(process)
|
||||||
if process.returncode or len(output)>2*1024**2:
|
if process.returncode or len(output)>2*1024**2:
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import contextlib
|
||||||
|
import json
|
||||||
|
import signal
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
import pytest
|
||||||
|
from proxydoctor import cli,suite,worker
|
||||||
|
from proxydoctor.runner import ModuleRunner
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_all_reuses_suite(monkeypatch):
|
||||||
|
captured={}
|
||||||
|
monkeypatch.setattr(sys,'argv',['vpsmaster','all','--offline','--lang','en'])
|
||||||
|
monkeypatch.setattr(cli,'run_suite',lambda **kw:captured.update(kw) or 0)
|
||||||
|
with pytest.raises(SystemExit) as ex:cli._main()
|
||||||
|
assert ex.value.code==0
|
||||||
|
assert captured['comprehensive'] and captured['include_bgp'] and captured['offline']
|
||||||
|
assert not captured['allow_bandwidth'] and not captured['include_return_routes']
|
||||||
|
|
||||||
|
|
||||||
|
def test_full_requires_bandwidth_consent_and_no_implicit_china(monkeypatch):
|
||||||
|
captured={}
|
||||||
|
monkeypatch.setattr(cli,'run_suite',lambda **kw:captured.update(kw) or 0)
|
||||||
|
monkeypatch.setattr(sys,'argv',['vpsmaster','all','--full'])
|
||||||
|
with pytest.raises(SystemExit) as ex:cli._main()
|
||||||
|
assert ex.value.code==0 and not captured['allow_bandwidth']
|
||||||
|
assert captured['include_return_routes'] and captured['mail_probes']
|
||||||
|
assert captured['bandwidth_sources'] is None
|
||||||
|
monkeypatch.setattr(sys,'argv',['vpsmaster','all','--allow-bandwidth'])
|
||||||
|
with pytest.raises(SystemExit) as ex:cli._main()
|
||||||
|
assert ex.value.code==2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('lang',['zh','en'])
|
||||||
|
def test_all_offline_keeps_skips_without_network(tmp_path,monkeypatch,capsys,lang):
|
||||||
|
import proxydoctor.runner as runner
|
||||||
|
monkeypatch.setattr(runner,'bounded',lambda n:contextlib.nullcontext())
|
||||||
|
monkeypatch.chdir(tmp_path)
|
||||||
|
def blocked(*a,**kw):raise AssertionError('Offline initiated network')
|
||||||
|
for name in ('collect_ipquality','run_media','protocol_trace','native_trace','measure_bandwidth'):
|
||||||
|
monkeypatch.setattr(suite,name,blocked)
|
||||||
|
monkeypatch.setattr('proxydoctor.connectivity.run_console',blocked)
|
||||||
|
monkeypatch.setattr('proxydoctor.bgp.collect',blocked)
|
||||||
|
path=tmp_path/'offline.json'
|
||||||
|
assert suite.run_suite(comprehensive=True,offline=True,lang=lang,color='never',json_output=path) in (0,1)
|
||||||
|
report=json.loads(path.read_text(encoding='utf-8'))
|
||||||
|
assert report['command']=='all' and len(report['modules'])==11
|
||||||
|
for name in ('network','ip_quality','route','route_icmp','route_tcp','bgp','media'):
|
||||||
|
assert report['modules'][name]['status']=='skipped'
|
||||||
|
assert sum(report['summary']['module_counts'].values())==11
|
||||||
|
text=capsys.readouterr().out
|
||||||
|
assert '11/11' in text and '\x1b[' not in text
|
||||||
|
|
||||||
|
|
||||||
|
def test_module_failure_and_budget_continue_without_fake_success():
|
||||||
|
report={};events=[]
|
||||||
|
runner=ModuleRunner(report,lambda:None,lambda _:None,observe=lambda n,s,end:events.append((n,end)))
|
||||||
|
def bad():raise ValueError('secret must not be displayed')
|
||||||
|
runner.run('bad',bad);runner.run('good',lambda:None)
|
||||||
|
runner.deadline=time.monotonic()-1
|
||||||
|
runner.run('budget',lambda:pytest.fail('Expired budget executed operation'))
|
||||||
|
assert report['modules']['bad']['status']=='failure'
|
||||||
|
assert report['modules']['good']['status']=='success'
|
||||||
|
assert report['modules']['budget']['reason']=='TOTAL_TIME_BUDGET'
|
||||||
|
assert len(events)==6
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not hasattr(signal,'setitimer'),reason='Linux scoped deadline test')
|
||||||
|
def test_module_timeout_reaps_child_and_next_module_runs():
|
||||||
|
report={};worker.reset();started=time.monotonic()
|
||||||
|
runner=ModuleRunner(report,lambda:None,lambda _:None,module_timeout=.15)
|
||||||
|
runner.run('timeout',lambda:worker.local_command([sys.executable,'-c','import time;time.sleep(30)'],timeout=35))
|
||||||
|
runner.run('next',lambda:None)
|
||||||
|
assert time.monotonic()-started<3
|
||||||
|
assert report['modules']['timeout']['error_code']=='MODULE_TIMEOUT'
|
||||||
|
assert report['modules']['next']['status']=='success'
|
||||||
|
assert not worker._active
|
||||||
|
assert signal.getitimer(signal.ITIMER_REAL)[0]==0
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_trace_dependency_has_structured_reason(tmp_path,monkeypatch,capsys):
|
||||||
|
import proxydoctor.runner as runner
|
||||||
|
from test_suite import unavailable_ip
|
||||||
|
monkeypatch.setattr(runner,'bounded',lambda n:contextlib.nullcontext())
|
||||||
|
monkeypatch.chdir(tmp_path)
|
||||||
|
monkeypatch.setattr(suite,'collect_ipquality',unavailable_ip)
|
||||||
|
monkeypatch.setattr('proxydoctor.connectivity.run_console',lambda *a,**kw:1)
|
||||||
|
monkeypatch.setattr(suite,'native_trace',lambda *a,**kw:1)
|
||||||
|
monkeypatch.setattr(suite,'protocol_trace',lambda *a,**kw:kw['summary'].update(state='dependency_missing',error_code='TRACEROUTE_DEPENDENCY_MISSING') or 1)
|
||||||
|
monkeypatch.setattr(suite,'run_media',lambda *a,**kw:kw['results'].update(Netflix={'state':'unknown'}))
|
||||||
|
path=tmp_path/'online.json'
|
||||||
|
assert suite.run_suite(comprehensive=True,json_output=path,color='never')==1
|
||||||
|
report=json.loads(path.read_text())
|
||||||
|
assert report['modules']['route_icmp']['status']=='skipped'
|
||||||
|
assert report['modules']['route_tcp']['error_code']=='TRACEROUTE_DEPENDENCY_MISSING'
|
||||||
|
assert report['modules']['bgp']['status']=='unknown'
|
||||||
|
assert report['summary']['failed_modules']==0
|
||||||
|
assert '11/11' in capsys.readouterr().out
|
||||||
Reference in new issue
Block a user