├── .gitattributes ├── .prettierignore ├── .husky ├── commit-msg └── pre-commit ├── src ├── index.ts ├── getMac.ts ├── __test__ │ ├── getMac.spec.ts │ ├── arpTable.spec.ts │ ├── utils.spec.ts │ ├── getNetworkInteraces.spec.ts │ └── testData.mock.ts ├── getIFacesByExec.ts ├── arpTable.ts ├── utils.ts ├── getIFacesByIpconfig.ts └── getNetworkInteraces.ts ├── tsconfig.eslint.json ├── .editorconfig ├── tsconfig.module.json ├── tsconfig.cjs.json ├── .prettierrc ├── .gitignore ├── scripts └── rmdir.mjs ├── .flh.config.js ├── .npmignore ├── tsconfig.json ├── jest.config.ts ├── .github ├── workflows │ ├── node-ci.yml │ └── npm-publish.yml └── README_zh-CN.md ├── bin └── cli.js ├── LICENSE ├── CHANGELOG.md ├── .eslintrc.js ├── package.json └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | *.ts text eol=lf 3 | *.bat text eol=crlf 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # package.json is formatted by package managers, so we ignore it here 2 | package.json -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install flh --commitlint 5 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './utils'; 2 | export * from './getNetworkInteraces'; 3 | export * from './getMac'; 4 | export * from './arpTable'; 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run cov 5 | pnpm exec flh --eslint --tscheck --only-changes --fix 6 | pnpm exec flh --prettier --only-changes --fix 7 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "build/module", 5 | "target": "esnext", 6 | "module": "esnext" 7 | }, 8 | "include": ["**/*.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | max_line_length = 140 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | max_line_length = 0 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /tsconfig.module.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "declarationDir": "types", 6 | "emitDeclarationOnly": false, 7 | "outDir": "esm" 8 | }, 9 | "include": ["src/**/*.ts"], 10 | "exclude": ["node_modules/**", "src/**/*.mock.ts", "src/**/*.spec.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "outDir": "cjs", 6 | "target": "es2018", 7 | "module": "commonjs" 8 | }, 9 | "include": ["src/**/*.ts"], 10 | "exclude": [ 11 | "node_modules/**", 12 | "src/**/*.mock.ts", 13 | "src/**/*.spec.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 140, 3 | "singleQuote": true, 4 | "trailingComma": "es5", 5 | "proseWrap": "preserve", 6 | "semi": true, 7 | "bracketSpacing": true, 8 | "arrowParens": "avoid", 9 | "overrides": [ 10 | { 11 | "files": ".prettierrc", 12 | "options": { 13 | "parser": "json" 14 | } 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # build dist 3 | .DS_Store 4 | .nyc_output 5 | *.log 6 | /release/ 7 | /debug/ 8 | /dist/ 9 | coverage 10 | build 11 | esm 12 | cjs 13 | docs 14 | types 15 | 16 | # manager 17 | package-lock.json 18 | yarn.lock 19 | yarn-error.log 20 | pnpm-lock.yaml 21 | .pnpm-debug.log 22 | 23 | node_modules 24 | .husky/post-commit 25 | # .flh.config.js 26 | .grs.config.js 27 | -------------------------------------------------------------------------------- /scripts/rmdir.mjs: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | 4 | const start = () => { 5 | const dest = path.resolve((process.argv[2] || './dist').trim()); 6 | 7 | if (!fs.existsSync(dest)) { 8 | console.error('指定的目录不存在:', dest); 9 | return false; 10 | } 11 | 12 | fs.rmSync(dest, { recursive: true }); 13 | 14 | console.log('目录已删除:', dest); 15 | return true; 16 | } 17 | 18 | start(); 19 | -------------------------------------------------------------------------------- /.flh.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('@lzwme/fed-lint-helper').FlhConfig} 3 | */ 4 | module.exports = { 5 | src: ['src'], 6 | debug: false, 7 | silent: false, 8 | printDetail: true, 9 | exitOnError: true, 10 | cache: true, 11 | tscheck: { 12 | whiteListFilePath: 'scripts/config/tsCheckWhiteList.json', 13 | }, 14 | eslint: { 15 | whiteListFilePath: 'scripts/config/eslintWhitelist.json', 16 | }, 17 | jest: { 18 | // silent: true, 19 | // fileList: glob.sync('src/**/**.spec.ts'), 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # source 2 | src 3 | **/*.spec.* 4 | test 5 | test-cases 6 | scripts 7 | README_zh-CN.MD 8 | CHANGELOG.md 9 | 10 | # build dist 11 | docs 12 | coverage 13 | .nyc_output 14 | *.log 15 | npm-debug.log 16 | 17 | # manager 18 | package-lock.json 19 | yarn.lock 20 | yarn-error.log 21 | pnpm-lock.yaml 22 | .pnpm-debug.log 23 | 24 | # config 25 | .gitattributes 26 | publish.bat 27 | .github 28 | .vscode 29 | .husky 30 | .circleci 31 | tsconfig.* 32 | jest.config.* 33 | .travis.yml 34 | .gitlab-ci.yml 35 | .editorconfig 36 | .eslintrc.js 37 | .eslintrc.json 38 | .prettierignore 39 | .prettierrc 40 | .grs.config.js 41 | .flh.config.js 42 | -------------------------------------------------------------------------------- /src/getMac.ts: -------------------------------------------------------------------------------- 1 | import { getNetworkIFaces, getNetworkIFaceOne, getAllNetworkIFaces } from './getNetworkInteraces'; 2 | import { isVirtualMac, isZeroMac } from './utils'; 3 | 4 | export function getAllMac() { 5 | const list = getAllNetworkIFaces(); 6 | const macSet = new Set(); 7 | 8 | for (const item of list) { 9 | if (!item.internal && !isZeroMac(item.mac)) macSet.add(item.mac); 10 | } 11 | 12 | return [...macSet]; 13 | } 14 | 15 | export function getAllPhysicsMac(family?: 'IPv4' | 'IPv6') { 16 | return getNetworkIFaces('', family).then(d => { 17 | d = d.filter(m => !isVirtualMac(m.mac, m.desc)); 18 | return [...new Set(d.map(m => m.mac))]; 19 | }); 20 | } 21 | 22 | export function getMac(iface?: string) { 23 | return getNetworkIFaceOne(iface).then(item => item?.mac); 24 | } 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "ts-node": { 3 | "transpileOnly": true 4 | }, 5 | "compilerOptions": { 6 | "alwaysStrict": true, 7 | "baseUrl": ".", 8 | "rootDir": "src", 9 | "skipLibCheck": true, 10 | "outDir": "cjs", 11 | "sourceMap": false, 12 | "noImplicitThis": true, 13 | "noImplicitAny": false, 14 | "module": "ESNext", 15 | "target": "ESNext", 16 | "moduleResolution": "node", 17 | "esModuleInterop": true, 18 | "experimentalDecorators": true, 19 | "emitDecoratorMetadata": true, 20 | "allowSyntheticDefaultImports": true, 21 | "allowJs": false, 22 | "noUnusedLocals": true, 23 | "noUnusedParameters": true, 24 | "resolveJsonModule": true, 25 | "pretty": true, 26 | "lib": ["ESNext"], 27 | "typeRoots": ["./node_modules/@types"] 28 | }, 29 | "exclude": ["**/node_modules/*"], 30 | "include": ["src/**/*.ts"], 31 | "compileOnSave": false 32 | } 33 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** @type {import('@jest/types').Config.InitialOptions } */ 4 | const config = { 5 | cache: true, 6 | // preset: 'ts-jest', 7 | // globals: { 8 | // 'ts-jest': { 9 | // tsconfig: 'tsconfig.module.json', 10 | // }, 11 | // }, 12 | transform: { 13 | '^.+\\.(t|j)sx?$': [ 14 | '@swc/jest', 15 | { 16 | jsc: { 17 | target: 'es2022', 18 | }, 19 | }, 20 | ], 21 | }, 22 | extensionsToTreatAsEsm: ['.ts'], 23 | 24 | testEnvironment: 'node', 25 | testMatch: ['/src/__test__/*.spec.ts'], 26 | coveragePathIgnorePatterns: ['/node_modules/', 'src/cli.ts', 'src/index.ts', 'src/__test__'], 27 | collectCoverageFrom: ['src/**/!(*.d).ts'], 28 | // eslint-disable-next-line @typescript-eslint/no-var-requires 29 | maxWorkers: require('os').cpus().length, 30 | // watchPlugins: ['jest-watch-typeahead/filename', 'jest-watch-typeahead/testname'], 31 | }; 32 | // module.exports = config; 33 | export default config; 34 | -------------------------------------------------------------------------------- /.github/workflows/node-ci.yml: -------------------------------------------------------------------------------- 1 | name: 'Tests' 2 | on: 3 | push: 4 | branches: 5 | - '**' 6 | paths-ignore: 7 | - README.md 8 | - CONTRIBUTING.md 9 | pull_request: 10 | branches: 11 | - '**' 12 | jobs: 13 | test: 14 | runs-on: ${{ matrix.os }} 15 | continue-on-error: ${{ matrix.os == 'windows-latest' }} 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | os: 20 | - ubuntu-latest 21 | - macos-latest 22 | - windows-latest 23 | node-version: 24 | - 16 25 | include: 26 | - node-version: 18 27 | os: ubuntu-latest 28 | name: Node ${{ matrix.node-version }} on ${{ matrix.os }} 29 | steps: 30 | - uses: actions/checkout@v3 31 | with: 32 | submodules: 'recursive' 33 | - uses: pnpm/action-setup@v2.2.1 34 | with: 35 | version: 8 36 | - uses: actions/setup-node@v3 37 | with: 38 | node-version: ${{ matrix.node-version }} 39 | # cache: 'pnpm' 40 | - run: pnpm install 41 | - run: pnpm test 42 | -------------------------------------------------------------------------------- /bin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | "use strict"; 3 | // @ts-check 4 | const { getAllMac, getAllPhysicsMac, getMac } = require("../build/main"); 5 | function help() { 6 | console.log(['USEAGE:', `\tgmac <--all> <--one [iface]>`, `\nexample:`, `\tgmac --all`, `\tgmac --one`, `\tgmac --one en0`].join('\n')); 7 | } 8 | async function start(argv = process.argv.slice(2)) { 9 | try { 10 | if (argv.includes('--debug')) 11 | process.env.DEBUG = '*'; 12 | if (argv.includes('--all')) { 13 | console.log('getAllMac:', getAllMac()); 14 | getAllPhysicsMac().then(list => console.log('getAllPhysicsMac:', list)); 15 | return; 16 | } 17 | const index = argv.indexOf('--one'); 18 | if (index > -1) { 19 | const iface = argv[index + 1]; 20 | return getMac(iface).then(mac => { 21 | console.log(`Mac address${iface ? ` for ${iface}` : ''}:`, mac); 22 | }) 23 | } 24 | help(); 25 | } 26 | catch (error) { 27 | console.error((0, require('os').networkInterfaces)()); 28 | throw error; 29 | } 30 | } 31 | start(); 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 renxia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## [1.1.0](https://github.com/lzwme/get-physical-address/compare/v1.0.5...v1.1.0) (2023-12-25) 6 | 7 | 8 | ### Features 9 | 10 | * 新增 getArpTable 方法 ([02fac9b](https://github.com/lzwme/get-physical-address/commit/02fac9bce34fadf4b503d6236a5a7c93e0c40d0c)) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * 新增 vEthernet 虚拟网卡关键字 ([0973735](https://github.com/lzwme/get-physical-address/commit/0973735a49d35ff32f6541c3ed6d950433273832)) 16 | 17 | ### [1.0.5](https://github.com/lzwme/get-physical-address/compare/v1.0.4...v1.0.5) (2023-07-12) 18 | 19 | ### Bug Fixes 20 | 21 | - 修复 wmic 获取的 mac 地址未格式化导致的描述过滤失效问题(close [#1](https://github.com/lzwme/get-physical-address/issues/1)) ([3b618ff](https://github.com/lzwme/get-physical-address/commit/3b618ff89c943f4eb461f05bb44102572a53c330)) 22 | 23 | ### [1.0.4](https://github.com/lzwme/get-physical-address/compare/v1.0.3...v1.0.4) (2023-07-03) 24 | 25 | ### [1.0.3](https://github.com/lzwme/get-physical-address/compare/v1.0.2...v1.0.3) (2022-10-25) 26 | 27 | ### Bug Fixes 28 | 29 | - 新增 Sangfor VPN 虚拟网卡识别规则 ([3724712](https://github.com/lzwme/get-physical-address/commit/3724712356c994afb487843eb189e4555f61bd70)) 30 | 31 | ### [1.0.2](https://github.com/lzwme/get-physical-address/compare/v1.0.1...v1.0.2) (2022-10-13) 32 | 33 | ### Bug Fixes 34 | 35 | - 更新通过 wmic 获取描述信息的处理逻辑 ([1ec85a2](https://github.com/lzwme/get-physical-address/commit/1ec85a2a47e3d6031858c37396952e46ad669461)) 36 | 37 | ### [1.0.1](https://github.com/lzwme/get-physical-address/compare/v1.0.0...v1.0.1) (2022-05-09) 38 | 39 | ### Bug Fixes 40 | 41 | - fix for husky install scripts ([3810f3b](https://github.com/lzwme/get-physical-address/commit/3810f3b6360e46c551fa2d47573ce0c518a36201)) 42 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const path = require('path'); 3 | const rootDir = path.resolve(__dirname); 4 | 5 | /** @type {import('eslint').Linter.Config} */ 6 | module.exports = { 7 | extends: [ 8 | 'eslint:recommended', 9 | // https://github.com/typescript-eslint/typescript-eslint/blob/HEAD/docs/getting-started/linting/README.md 10 | 'plugin:@typescript-eslint/recommended', 11 | // 'plugin:@typescript-eslint/recommended-requiring-type-checking', 12 | 'plugin:prettier/recommended', 13 | 'plugin:jest/recommended', 14 | 'plugin:unicorn/recommended', 15 | ], 16 | env: { 17 | browser: true, 18 | es2021: true, 19 | node: true, 20 | }, 21 | globals: { 22 | SharedArrayBuffer: 'readonly', 23 | }, 24 | parser: '@typescript-eslint/parser', 25 | parserOptions: { 26 | tsconfigRootDir: rootDir, 27 | project: path.resolve(rootDir, './tsconfig.eslint.json'), 28 | projectFolderIgnoreList: ['**/node_modules/**', '**/dist/**', '**/dist-admin/**'], 29 | warnOnUnsupportedTypeScriptVersion: false, 30 | // createDefaultProgram: true, 31 | ecmaFeatures: { 32 | jsx: true, 33 | }, 34 | ecmaVersion: 2020, 35 | sourceType: 'module', 36 | }, 37 | plugins: ['@typescript-eslint'], 38 | ignorePatterns: ['**/node_modules/**', 'dist/**', 'build/**', 'mock/**', '**/*.js', '**/*.d.ts'], 39 | rules: { 40 | 'prettier/prettier': 'warn', 41 | // 关闭 eslint 的 indent,使用 prettier 格式化格式 42 | indent: ['off', 2], 43 | '@typescript-eslint/explicit-module-boundary-types': 'off', 44 | '@typescript-eslint/no-unused-vars': ['warn', { varsIgnorePattern: '^_', argsIgnorePattern: '^_', ignoreRestSiblings: true }], 45 | '@typescript-eslint/ban-ts-comment': 'off', 46 | 'unicorn/prefer-module': 'off', 47 | 'unicorn/prefer-node-protocol': 'off', 48 | 'unicorn/filename-case': 'off', 49 | 'unicorn/prefer-string-replace-all': 'off', 50 | 'prefer-const': [ 51 | 'error', 52 | { 53 | destructuring: 'all', 54 | // "ignoreReadBeforeAssign": true, 55 | }, 56 | ], 57 | }, 58 | }; 59 | -------------------------------------------------------------------------------- /src/__test__/getMac.spec.ts: -------------------------------------------------------------------------------- 1 | import { getAllMac, getAllPhysicsMac, getMac } from '../getMac'; 2 | import { isValidMac } from '../utils'; 3 | import { ifacesMock, wmicNicStdout, ipconfigStdout } from './testData.mock'; 4 | 5 | jest.mock('os', () => ({ 6 | networkInterfaces: () => ifacesMock, 7 | })); 8 | jest.mock('child_process', () => ({ 9 | exec(cmd, _options, callback: (...a) => void) { 10 | callback('error for test', cmd.startsWith('wmic') ? wmicNicStdout : Buffer.from(ipconfigStdout)); 11 | }, 12 | execSync(cmd: string) { 13 | return cmd.startsWith('wmic') ? wmicNicStdout : ipconfigStdout; 14 | }, 15 | })); 16 | 17 | jest.mock('process', () => { 18 | return { 19 | get platform() { 20 | return 'win32'; 21 | }, 22 | }; 23 | }); 24 | 25 | describe('getMac.ts', () => { 26 | beforeAll(() => { 27 | console.debug = () => void 0; 28 | console.error = () => void 0; 29 | process.env.DEBUG = '*'; 30 | }); 31 | 32 | it('getAllMac', () => { 33 | const list = getAllMac(); 34 | expect(Array.isArray(list)).toBeTruthy(); 35 | // it is not filtered by visual mac prefix 36 | expect(list.includes(ifacesMock.vmware[0].mac)).toBeTruthy(); 37 | }); 38 | 39 | it('getAllPhysicsMac', async () => { 40 | let list = await getAllPhysicsMac(); 41 | expect(Array.isArray(list)).toBeTruthy(); 42 | // it is filtered by visual mac prefix 43 | expect(list.includes(ifacesMock.vmware[0].mac)).toBeFalsy(); 44 | 45 | list = await getAllPhysicsMac('IPv4'); 46 | expect(list.length > 0).toBeTruthy(); 47 | expect(list.includes(ifacesMock.en0[1].mac)).toBeTruthy(); 48 | 49 | list = await getAllPhysicsMac('IPv6'); 50 | expect(list.includes(ifacesMock.en0[0].mac)).toBeTruthy(); 51 | }); 52 | 53 | it('getMac', async () => { 54 | let mac = await getMac(); 55 | expect(typeof mac === 'string' && isValidMac(mac)).toBeTruthy(); 56 | 57 | // return the ipv4 mac address 58 | mac = await getMac('en0'); 59 | expect(mac).toBe(ifacesMock.en0[1].mac); 60 | 61 | mac = await getMac('abc'); 62 | expect(mac).toBeUndefined(); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /src/__test__/arpTable.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: renxia 3 | * @Date: 2023-12-08 10:11:08 4 | * @LastEditors: renxia 5 | * @LastEditTime: 2023-12-08 11:59:50 6 | * @Description: 7 | */ 8 | import { getArpIpByMac, getArpMacByIp, getArpTable } from '../arpTable'; 9 | import { ifacesMock, arpANStdout } from './testData.mock'; 10 | 11 | let platform: keyof typeof arpANStdout = 'win32'; 12 | 13 | jest.mock('os', () => ({ 14 | networkInterfaces: () => ifacesMock, 15 | })); 16 | jest.mock('child_process', () => ({ 17 | execSync() { 18 | return arpANStdout[platform]; 19 | }, 20 | })); 21 | 22 | jest.mock('process', () => { 23 | return { 24 | get platform() { 25 | return platform; 26 | }, 27 | }; 28 | }); 29 | 30 | describe('getArpTable.ts', () => { 31 | beforeAll(() => { 32 | console.debug = () => void 0; 33 | console.error = () => void 0; 34 | process.env.DEBUG = '*'; 35 | }); 36 | 37 | it('getArpTable', async () => { 38 | platform = 'win32'; 39 | let info = await getArpTable(); 40 | expect(Array.isArray(info.table)).toBeTruthy(); 41 | 42 | platform = 'mac'; 43 | info = await getArpTable(); 44 | expect(Array.isArray(info.table)).toBeTruthy(); 45 | expect(info.table.every(d => d.type === 'unknown')).toBeTruthy(); 46 | 47 | platform = 'linux'; 48 | info = await getArpTable(); 49 | expect(Array.isArray(info.table)).toBeTruthy(); 50 | expect(info.table.some(d => d.mac === '')).toBeTruthy(); 51 | expect(info.table.every(d => d.type === 'unknown')).toBeTruthy(); 52 | 53 | info = await getArpTable(arpANStdout.mac); 54 | expect(info.table.some(d => d.mac === '')).toBeFalsy(); 55 | }); 56 | 57 | it('getArpMacByIp', async () => { 58 | platform = 'win32'; 59 | let mac = await getArpMacByIp('10.10.1.1'); 60 | expect(mac).toBe('dc:ef:80:37:aa:ff'); 61 | 62 | mac = await getArpMacByIp('10.10.1.0'); 63 | expect(mac).toBe(''); 64 | }); 65 | 66 | it('getArpIpByMac', async () => { 67 | platform = 'win32'; 68 | let ip = await getArpIpByMac('dc-ef-80-37-aa-ff'); 69 | expect(ip[0]).toBe('10.10.1.1'); 70 | 71 | ip = await getArpIpByMac('dc-ef-80-37'); 72 | expect(ip.length > 1).toBeTruthy(); 73 | 74 | ip = await getArpIpByMac('dc:ef:80:37'); 75 | expect(ip[0]).toBe('10.10.1.1'); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 2 | 3 | name: Publish Package to npmjs 4 | 5 | on: 6 | push: 7 | tags: 8 | - "v*.*.*" 9 | # release: 10 | # types: [created] 11 | 12 | jobs: 13 | npm-publish: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: write 17 | concurrency: 18 | group: ${{ github.workflow }}-${{ github.ref }} 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v3 22 | with: 23 | submodules: 'recursive' 24 | 25 | - name: Install Node.js 26 | uses: actions/setup-node@v3 27 | with: 28 | node-version: 18 29 | registry-url: https://registry.npmjs.com 30 | 31 | - uses: pnpm/action-setup@v2 32 | name: Install pnpm 33 | id: pnpm-install 34 | with: 35 | version: 8 36 | run_install: false 37 | 38 | - name: Get pnpm store directory 39 | id: pnpm-cache 40 | shell: bash 41 | run: | 42 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT 43 | 44 | - uses: actions/cache@v3 45 | name: Setup pnpm cache 46 | with: 47 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} 48 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 49 | restore-keys: | 50 | ${{ runner.os }}-pnpm-store- 51 | 52 | - name: Install dependencies 53 | run: pnpm install --no-frozen-lockfile --ignore-scripts 54 | 55 | - name: Build and npm-publish 56 | run: npm run release 57 | 58 | - run: npm publish 59 | env: 60 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 61 | 62 | # - name: GitHub Pages action 63 | # uses: peaceiris/actions-gh-pages@v3 64 | # with: 65 | # github_token: ${{ secrets.GITHUB_TOKEN }} 66 | # publish_dir: ./docs 67 | 68 | - name: Github Release 69 | uses: softprops/action-gh-release@v1 70 | if: startsWith(github.ref, 'refs/tags/') 71 | with: 72 | draft: false 73 | prerelease: false 74 | # tag_name: ${{ github.ref }} 75 | # name: Release ${{ github.ref }} 76 | # body: TODO New Release. 77 | # files: | 78 | # ${{ secrets.ReleaseZipName }}.zip 79 | # LICENSE 80 | -------------------------------------------------------------------------------- /src/getIFacesByExec.ts: -------------------------------------------------------------------------------- 1 | import { exec, type ExecException, type ExecOptions } from 'child_process'; 2 | import { ObjectEncodingOptions } from 'fs'; 3 | import process from 'process'; 4 | import { formatMac, isVirtualMac, logDebug } from './utils'; 5 | 6 | export interface IpconfigNIFItem { 7 | desc?: string; 8 | ipv4?: string; 9 | ipv6?: string; 10 | ipv6LinkLocal?: string; 11 | mac?: string; 12 | network?: string; 13 | dnsSuffix?: string; 14 | dns?: string[]; 15 | subnetmask?: string; 16 | dhcpEnabled?: boolean; 17 | dhcpServer?: string; 18 | dhcpv6IAID?: string; 19 | dhcpv6DUID?: string; 20 | isMain?: boolean; 21 | } 22 | 23 | function execPromisfy(cmd: string, options: ObjectEncodingOptions & ExecOptions = {}, trimEmptyLine = false) { 24 | return new Promise<{ error: ExecException; stdout: string; stderr: string }>(resolve => { 25 | exec(cmd, { windowsHide: true, ...options }, (error, stdout, stderr) => { 26 | if (error) console.error('exec error:', `cmd: ${cmd}\n`, error.message || error); 27 | stdout = stdout.replace(/\r+\n/g, '\n').trim(); 28 | if (trimEmptyLine) stdout = stdout.replace(/\n{2,}/g, '\n'); 29 | resolve({ error, stdout, stderr }); 30 | }); 31 | }); 32 | } 33 | 34 | export async function getNetworkIFacesInfoByWmic() { 35 | const config: { [mac: string]: IpconfigNIFItem } = {}; 36 | let stdout = ''; 37 | 38 | if (process.platform === 'win32') { 39 | const keyMap = { MACAddress: 'mac', Description: 'desc' }; 40 | const cmd = `wmic nic get MACAddress,Description /format:list`; 41 | const info = await execPromisfy(cmd); 42 | const lines = info.stdout.split('\n').filter(d => d.includes('=')); 43 | 44 | stdout = info.stdout; 45 | if (lines[0]) { 46 | let item: Record = {}; 47 | const setToConfig = () => { 48 | if (item.mac) { 49 | item.mac = formatMac(item.mac); 50 | if (!config[item.mac] || !isVirtualMac('', item.desc)) config[item.mac] = item; 51 | } 52 | item = {}; 53 | }; 54 | 55 | for (const line of lines) { 56 | let [key, value] = line.split('=').map(d => d.trim()); 57 | key = keyMap[key] || key.toLowerCase(); 58 | 59 | if (item[key]) setToConfig(); 60 | 61 | item[key] = value; 62 | } 63 | setToConfig(); 64 | } 65 | 66 | if (stdout) logDebug(`[getNetworkIFacesInfoByWmic]`, stdout, config); 67 | } 68 | 69 | return { stdout, config }; 70 | } 71 | -------------------------------------------------------------------------------- /src/arpTable.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: renxia 3 | * @Date: 2023-12-08 09:31:39 4 | * @LastEditors: renxia 5 | * @LastEditTime: 2023-12-08 11:58:38 6 | * @Description: 7 | */ 8 | import { execSync } from 'child_process'; 9 | import { isIP } from 'net'; 10 | import process from 'process'; 11 | import { formatMac } from './utils'; 12 | import { getNetworkIFaceOne } from './getNetworkInteraces'; 13 | 14 | export type ArpTableItem = { 15 | /** interface ip address */ 16 | iip: string; 17 | ip: string; 18 | mac: string; 19 | type: 'static' | 'dynamic' | 'unknown'; 20 | }; 21 | 22 | export async function getArpTable(stdout = '') { 23 | const isWindows = process.platform === 'win32'; 24 | if (!stdout) { 25 | if (isWindows) { 26 | const { default: iconv } = await import('iconv-lite'); 27 | stdout = iconv.decode(execSync('arp -a', { windowsHide: true }), 'gbk').trim(); 28 | } else { 29 | stdout = execSync('arp -an', { windowsHide: true }).toString('utf8').trim(); 30 | } 31 | } 32 | 33 | const table: ArpTableItem[] = []; 34 | let iip = ''; 35 | let iface = ''; 36 | 37 | for (const line of stdout.split('\n').map(d => d.trim())) { 38 | if (!line) continue; 39 | let ip = ''; 40 | let mac = ''; 41 | let type = 'unknown'; 42 | 43 | if (isWindows) { 44 | [ip, mac, type = 'unknown'] = line.split(/\s+/); 45 | 46 | if (isIP(mac)) { 47 | iip = mac; 48 | continue; 49 | } 50 | 51 | if (type.includes('动态')) type = 'dynamic'; 52 | else if (type.includes('静态')) type = 'static'; 53 | } else { 54 | const match = line.match(/\(([\d.]+)\) at (\S+) .*on ([\da-z]+)/); 55 | if (!match) continue; 56 | 57 | const preIface = iface; 58 | [, ip, mac, iface] = match; 59 | 60 | if (preIface !== iface) { 61 | const item = await getNetworkIFaceOne(iface); 62 | if (item) iip = item.address; 63 | } 64 | } 65 | 66 | if (isIP(ip)) { 67 | table.push({ iip, ip, mac: formatMac(mac), type: (type || 'unknown') as never }); 68 | } 69 | } 70 | 71 | return { stdout, table }; 72 | } 73 | 74 | export async function getArpMacByIp(ip: string) { 75 | if (!ip) return ''; 76 | const { table } = await getArpTable(); 77 | const item = table.find(d => d.ip === ip); 78 | return item ? item.mac : ''; 79 | } 80 | 81 | export async function getArpIpByMac(mac: string) { 82 | mac = formatMac(mac); 83 | if (!mac) return []; 84 | const { table } = await getArpTable(); 85 | return table.filter(d => d.mac.includes(mac)).map(d => d.ip); 86 | } 87 | 88 | // getArpTable().then(d => console.log(d.table)); 89 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import type { NetworkInterfaceInfo } from 'os'; 2 | 3 | export function isDebug() { 4 | return process.env.DEBUG === '*' || process.env.DEBUG === 'GPMA' || process.env.GPA_DEBUG === '1'; 5 | } 6 | 7 | export function logDebug(...argument) { 8 | if (!isDebug()) return; 9 | if (globalThis.logger?.debug) globalThis.logger.debug(...argument); 10 | else console.debug(...argument); 11 | } 12 | 13 | export function isMac(mac: string) { 14 | return /^([\da-f]{1,2}[:-]){5}([\da-f]{1,2})$/i.test(mac); 15 | } 16 | 17 | export function hasMac(content: string) { 18 | return /([\da-f]{1,2}[:-]){5}([\da-f]{1,2})/gi.test(content); 19 | } 20 | 21 | export function isZeroMac(mac: string) { 22 | return /^(0{1,2}[:-]){5}0{1,2}$/.test(mac); 23 | } 24 | 25 | const invalidMacAddresses = new Set(['00:00:00:00:00:00', 'ff:ff:ff:ff:ff:ff', 'ac:de:48:00:11:22']); 26 | 27 | // see https://standards-oui.ieee.org/oui/oui.txt 28 | const virtualMacPrefix = new Set([ 29 | '00:05:69', // vmware1 30 | '00:0c:29', // vmware2 31 | '00:50:56', // vmware3 32 | '00:1c:14', // vmware 33 | '00:1c:42', // parallels1 34 | '02:00:4c', // Microsoft Loopback Adapter (微软回环网卡) 35 | '00:03:ff', // microsoft virtual pc 36 | '00:0f:4b', // virtual iron 4 37 | '00:16:3e', // red hat xen , oracle vm , xen source, novell xen 38 | '08:00:27', // virtualbox 39 | '0a:00:27', // virtualbox 40 | '00:ff:78', // Sangfor 41 | '00:ff:9d', // Sangfor 42 | ]); 43 | 44 | export function isVirtualMac(mac: string, desc?: string) { 45 | let isVirtual = false; 46 | 47 | if (mac) { 48 | isVirtual = isMac(mac) && virtualMacPrefix.has(formatMac(mac).slice(0, 8)); 49 | } 50 | 51 | if (desc && !isVirtual) { 52 | const virtualDescList = [ 53 | 'virtual', 54 | ' vpn ', 55 | ' ssl ', 56 | 'tap-windows', 57 | 'hyper-v', 58 | 'vEthernet', // vEthernet (Default Switch) 59 | 'km-test', 60 | 'microsoft loopback', 61 | 'sangfor ', 62 | ]; 63 | desc = String(desc).toLowerCase(); 64 | isVirtual = virtualDescList.some(d => desc.includes(d)); 65 | } 66 | 67 | return isVirtual; 68 | } 69 | 70 | export function isValidMac(mac: string) { 71 | return !invalidMacAddresses.has(formatMac(mac)) && isMac(mac); 72 | } 73 | 74 | export function formatMac(mac: string) { 75 | return String(mac).trim().toLowerCase().replace(/-/g, ':'); 76 | } 77 | 78 | export function hasMutiMac(list: NetworkInterfaceInfo[], filter?: (mac: string) => boolean) { 79 | if (!list || list.length === 0) return false; 80 | if (typeof filter !== 'function') filter = isMac; 81 | return new Set(list.map(d => d.mac).filter(mac => filter(mac))).size > 1; 82 | } 83 | -------------------------------------------------------------------------------- /src/__test__/utils.spec.ts: -------------------------------------------------------------------------------- 1 | import { formatMac, hasMac, hasMutiMac, isDebug, isMac, isValidMac, isVirtualMac, isZeroMac, logDebug } from '../utils'; 2 | import { ifacesMock } from './testData.mock'; 3 | 4 | describe('utils', () => { 5 | it('isDebug', () => { 6 | process.env.DEBUG = '*'; 7 | expect(isDebug()).toBeTruthy(); 8 | process.env.DEBUG = 'GPMA'; 9 | expect(isDebug()).toBeTruthy(); 10 | process.env.DEBUG = ''; 11 | expect(isDebug()).toBeFalsy(); 12 | }); 13 | 14 | it('logDebug', () => { 15 | let debugCount = 0; 16 | globalThis.logger = { 17 | debug: () => debugCount++, 18 | }; 19 | logDebug('test'); 20 | expect(debugCount === 0).toBeTruthy(); 21 | 22 | process.env.GPA_DEBUG = '1'; 23 | logDebug('test'); 24 | expect(debugCount > 0).toBeTruthy(); 25 | }); 26 | 27 | it('isMac', () => { 28 | const list = ['00:00:00:00:00:00', 'ab:34:56:78:90:01', 'AB:34:56:78:90:01', '00-00-00-00-00-00', '12-34-56-78-90-01']; 29 | for (const mac of list) expect(isMac(mac)).toBeTruthy(); 30 | }); 31 | 32 | it('hasMac', () => { 33 | const list = [ 34 | '00:00:00:00:00:00', 35 | 'ab:34:56:78:90:01', 36 | 'AB:34:56:78:90:01', 37 | '00-00-00-00-00-00', 38 | 'the mac address is 12-34-56-78-90-01.', 39 | ]; 40 | for (const mac of list) expect(hasMac(mac)).toBeTruthy(); 41 | }); 42 | 43 | it('isZeroMac', () => { 44 | for (const mac of ['00:00:00:00:00:00', '00-00-00-00-00-00']) expect(isZeroMac(mac)).toBeTruthy(); 45 | for (const mac of ['', undefined, void 0, 'AB:34:56:78:90:01', '12-34-56-78-90-01']) expect(isZeroMac(mac)).toBeFalsy(); 46 | }); 47 | 48 | it('isVirtualMac', () => { 49 | for (const mac of ['08:00:27:78:90:01', '00-1C-14-78-90-01']) expect(isVirtualMac(mac)).toBeTruthy(); 50 | for (const mac of ['', undefined, '08:00:27:78', 'a0:01', 'AB:34:56:78:90:01', '00-00-00-00-00-00']) { 51 | expect(isVirtualMac(mac)).toBeFalsy(); 52 | } 53 | }); 54 | 55 | it('isValidMac', () => { 56 | for (const mac of ['AB:34:56:78:90:01', '12-34-56-78-90-01']) expect(isValidMac(mac)).toBeTruthy(); 57 | for (const mac of ['', undefined, 'a0:01', '00:00:00:00:00:00', '00-00-00-00-00-00']) expect(isValidMac(mac)).toBeFalsy(); 58 | }); 59 | 60 | it('formatMac', () => { 61 | for (const mac of ['AB:34:56:78:90:01', '12-34-56-78-90-01', '00-00-00-00-00-00']) 62 | expect(/^[\d:a-z]+$/.test(formatMac(mac))).toBeTruthy(); 63 | }); 64 | 65 | it('hasMutiMac', () => { 66 | expect(hasMutiMac([])).toBeFalsy(); 67 | expect(hasMutiMac(void 0)).toBeFalsy(); 68 | expect(hasMutiMac(ifacesMock.en0 as never)).toBeTruthy(); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /src/__test__/getNetworkInteraces.spec.ts: -------------------------------------------------------------------------------- 1 | import { isValidMac } from '../utils'; 2 | import { ifacesMock, wmicNicStdout, ipconfigStdout } from './testData.mock'; 3 | import { getNetworkIFaceOne, getNetworkIFaces } from '../getNetworkInteraces'; 4 | import { getNetworkIFacesInfoByWmic } from '../getIFacesByExec'; 5 | import { getNetworkIFacesInfoByIpconfig } from '../getIFacesByIpconfig'; 6 | 7 | jest.mock('os', () => ({ 8 | networkInterfaces: () => ifacesMock, 9 | })); 10 | jest.mock('child_process', () => ({ 11 | exec(cmd, _options, callback: (...a) => void) { 12 | callback('error for test', cmd.startsWith('wmic') ? wmicNicStdout : Buffer.from(ipconfigStdout)); 13 | }, 14 | execSync(cmd: string) { 15 | return cmd.startsWith('wmic') ? wmicNicStdout : ipconfigStdout; 16 | }, 17 | })); 18 | 19 | let platform = 'win32'; 20 | jest.mock('process', () => { 21 | return { 22 | get platform() { 23 | return platform; 24 | }, 25 | }; 26 | }); 27 | 28 | describe('getIFacesByExec', () => { 29 | beforeAll(() => { 30 | console.debug = () => void 0; 31 | console.error = () => void 0; 32 | process.env.DEBUG = '*'; 33 | }); 34 | 35 | it('getNetworkIFacesInfoByIpconfig', async () => { 36 | platform = 'linux'; 37 | let info = await getNetworkIFacesInfoByIpconfig(); 38 | expect(Object.keys(info.config).length).toBe(0); 39 | 40 | platform = 'win32'; 41 | info = await getNetworkIFacesInfoByIpconfig(); 42 | 43 | expect(Object.keys(info.config).length > 0).toBeTruthy(); 44 | // 应当转换为小写 45 | expect('00:1d:7d:71:a8:d6' in info.config).toBeTruthy(); 46 | expect(info.stdout.includes('00-1D-7D-71-A8-D6')).toBeTruthy(); 47 | }); 48 | 49 | it('getNetworkIFacesInfoByWmic', async () => { 50 | platform = 'linux'; 51 | let info = await getNetworkIFacesInfoByWmic(); 52 | expect(Object.keys(info.config).length).toBe(0); 53 | 54 | platform = 'win32'; 55 | info = await getNetworkIFacesInfoByWmic(); 56 | expect(Object.keys(info.config).includes(ifacesMock.en0[1].mac)).toBeTruthy(); 57 | // 应当转换为小写 58 | expect('1c:1b:b5:9b:ff:cc' in info.config).toBeTruthy(); 59 | expect(info.stdout.includes('1C:1B:B5:9B:FF:CC')).toBeTruthy(); 60 | }); 61 | }); 62 | 63 | describe('getNetworkInfo.ts', () => { 64 | it('getNetworkIFaces', async () => { 65 | let list = await getNetworkIFaces(); 66 | expect(Array.isArray(list)).toBeTruthy(); 67 | // en0 have high highPriority 68 | expect(ifacesMock.en0.some(d => d.mac === list[0].mac)).toBeTruthy(); 69 | 70 | list = await getNetworkIFaces('', 'IPv4'); 71 | expect(list.length > 0).toBeTruthy(); 72 | expect(list.some(d => d.mac === ifacesMock.en0[1].mac)).toBeTruthy(); 73 | expect(list.every(d => d.family === 'IPv4')).toBeTruthy(); 74 | 75 | list = await getNetworkIFaces('', 'IPv6'); 76 | expect(list.some(d => d.mac === ifacesMock.en0[0].mac)).toBeTruthy(); 77 | expect(list.every(d => d.family === 'IPv6')).toBeTruthy(); 78 | 79 | list = await getNetworkIFaces('abc'); 80 | expect(list.length === 0).toBeTruthy(); 81 | 82 | list = await getNetworkIFaces('en0', 'IPv4'); 83 | expect(list.length > 0).toBeTruthy(); 84 | }); 85 | 86 | it('getNetworkIFaceOne', async () => { 87 | let item = await getNetworkIFaceOne(); 88 | expect(isValidMac(item.mac)).toBeTruthy(); 89 | expect(item.mac).toBe(ifacesMock.en0[1].mac); 90 | 91 | // return the ipv4 mac address 92 | item = await getNetworkIFaceOne('en0'); 93 | expect(item.mac).toBe(ifacesMock.en0[1].mac); 94 | 95 | item = await getNetworkIFaceOne('abc'); 96 | expect(item).toBeUndefined(); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /src/getIFacesByIpconfig.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: renxia 3 | * @Date: 2023-03-23 23:02:16 4 | * @LastEditors: renxia 5 | * @LastEditTime: 2023-12-08 09:43:26 6 | * @Description: 7 | */ 8 | import { execSync } from 'child_process'; 9 | import process from 'process'; 10 | import { formatMac, isVirtualMac, logDebug } from './utils'; 11 | import { IpconfigNIFItem } from './getIFacesByExec'; 12 | 13 | /** 14 | * get by ipconfig /all: only for test 15 | */ 16 | export async function getNetworkIFacesInfoByIpconfig() { 17 | const config: { [mac: string]: IpconfigNIFItem } = {}; 18 | let stdout = ''; 19 | 20 | if (process.platform === 'win32') { 21 | // https://docs.microsoft.com/zh-cn/windows-server/administration/windows-commands/ipconfig 22 | const { default: iconv } = await import('iconv-lite'); 23 | const cmd = 'ipconfig /all'; 24 | // const info = await execPromisfy(cmd, { encoding: 'binary' }); 25 | // stdout = iconv.decode(Buffer.from(info.stdout, 'binary'), 'gbk').trim(); 26 | stdout = iconv.decode(execSync(cmd, { windowsHide: true }) as never, 'gbk').trim(); 27 | 28 | const keyMap = { 29 | '连接特定的 DNS 后缀': 'dnsSuffix', 30 | 'Connection-specific DNS Suffix': 'dnsSuffix', 31 | 描述: 'desc', 32 | Description: 'desc', 33 | 物理地址: 'mac', 34 | 'Physical Address': 'mac', 35 | 'IPv6 地址': 'ipv6', 36 | 'IPv6 Address': 'ipv6', 37 | '本地链接 IPv6 地址': 'ipv6LinkLocal', 38 | 'Link-local IPv6 Address': 'ipv6LinkLocal', 39 | 'IPv4 地址': 'ipv4', 40 | 'IPv4 Address': 'ipv4', 41 | 'IP Address': 'ipv4', 42 | 子网掩码: 'subnetmask', 43 | 'Subnet Mask': 'subnetmask', 44 | 默认网关: 'gateway', 45 | 'Default Gateway': 'gateway', 46 | 'DHCP 服务器': 'dhcpServer', 47 | 'DHCP Server': 'dhcpServer', 48 | 'DHCPv6 IAID': 'dhcpv6IAID', 49 | 'DHCPv6 客户端 DUID': 'dhcpv6DUID', 50 | 'DNS 服务器': 'dns', 51 | 'DNS Servers': 'dns', 52 | 'DHCP 已启用': 'dhcpEnabled', 53 | 'DHCP Enabled': 'dhcpEnabled', 54 | // '自动配置已启用': 'dhcpEnabled', 55 | 获得租约的时间: 'leaseObtained', 56 | 'Lease Obtained': 'leaseObtained', 57 | 租约过期的时间: 'leaseExpires', 58 | 'Lease Expires': 'leaseExpires', 59 | }; 60 | const lines = stdout.replace(/(\. )+:/g, ':').split('\n'); 61 | let item: IpconfigNIFItem = {}; 62 | let key = ''; 63 | let value: string | boolean = ''; 64 | const setToConfig = () => { 65 | if (item.mac) { 66 | item.mac = formatMac(item.mac); 67 | if (!config[item.mac] || !isVirtualMac('', item.desc)) config[item.mac] = item; 68 | } 69 | item = {}; 70 | }; 71 | 72 | for (let line of lines) { 73 | if (!line) continue; 74 | if (line.startsWith(' ')) { 75 | if (line.includes(':')) { 76 | [key, value] = line.split(':').map(d => d.trim()); 77 | 78 | if (keyMap[key]) key = keyMap[key]; 79 | if (value.includes('首选') || value.includes('Preferred')) { 80 | value = value.replace(/[^\d.]/g, ''); 81 | item.isMain = true; 82 | } else if (['Yes', 'No'].includes(value)) { 83 | value = value === 'Yes'; 84 | } 85 | 86 | item[key] = key === 'dns' ? [value] : value; 87 | } else { 88 | line = line.trim(); 89 | if (key === 'dns' && item.dns && /^(\d+\.)+\d+$/.test(line)) item.dns.push(line); 90 | } 91 | } else { 92 | setToConfig(); 93 | } 94 | } 95 | setToConfig(); 96 | 97 | logDebug(`[exec]cmd: ${cmd}. getNetworkIFacesInfo:`, stdout, config); 98 | } 99 | 100 | return { stdout, config }; 101 | } 102 | -------------------------------------------------------------------------------- /src/getNetworkInteraces.ts: -------------------------------------------------------------------------------- 1 | import { networkInterfaces, type NetworkInterfaceInfo } from 'os'; 2 | import { getNetworkIFacesInfoByWmic } from './getIFacesByExec'; 3 | import { isZeroMac, hasMutiMac, logDebug, isVirtualMac } from './utils'; 4 | 5 | type NIFInfo = NetworkInterfaceInfo & { desc?: string }; 6 | 7 | /** sort by: !internal > !zeroMac(mac) > desc for visual > family=IPv4 */ 8 | function ifacesSort(list: NIFInfo[]) { 9 | return list.sort((a, b) => { 10 | if (a.internal !== b.internal) return a.internal ? 1 : -1; 11 | if (isZeroMac(a.mac) !== isZeroMac(b.mac)) return isZeroMac(a.mac) ? 1 : -1; 12 | 13 | const isVirtualA = isVirtualMac(a.mac); 14 | const isVirtualB = isVirtualMac(b.mac); 15 | if (isVirtualA !== isVirtualB) return isVirtualA ? 1 : -1; 16 | 17 | if (!a.address || !b.address) return a.address ? -1 : 1; 18 | 19 | if (a.family !== b.family) return a.family === 'IPv6' ? 1 : -1; 20 | }); 21 | } 22 | 23 | function getNif() { 24 | const nif = networkInterfaces(); 25 | 26 | for (const key in nif) { 27 | if (key.includes('以太网')) { 28 | nif[key.replace('以太网', 'ethernet')] = nif[key]; 29 | delete nif[key]; 30 | } 31 | } 32 | return nif; 33 | } 34 | 35 | /** get all networkInterfaces and sort by some rules */ 36 | export function getAllNetworkIFaces() { 37 | const nif = getNif(); 38 | const list: NetworkInterfaceInfo[] = []; 39 | 40 | // en0 - mac, eth3 - linux, ethernet - windows 41 | const highPriorityIfaces = /^((en|eth)\d+|ethernet)$/i; 42 | const lowPriorityIfaces = /^((lo|vboxnet)\d+)$/i; 43 | const ifaces = Object.keys(nif).sort((a, b) => { 44 | if (highPriorityIfaces.test(a)) { 45 | if (highPriorityIfaces.test(b)) return a.localeCompare(b); 46 | return -1; 47 | } 48 | if (lowPriorityIfaces.test(a)) { 49 | if (lowPriorityIfaces.test(b)) return a.localeCompare(b); 50 | return 1; 51 | } 52 | if (highPriorityIfaces.test(b)) return 1; 53 | if (lowPriorityIfaces.test(b)) return -1; 54 | return a.localeCompare(b); 55 | }); 56 | 57 | for (const key of ifaces) { 58 | for (const item of nif[key]) list.push(item); 59 | } 60 | 61 | return ifacesSort(list); 62 | } 63 | 64 | export async function getNetworkIFaces(iface?: string, family?: 'IPv4' | 'IPv6') { 65 | let list: NIFInfo[] = []; 66 | 67 | if (iface) { 68 | const nif = getNif(); 69 | 70 | if (nif[iface]) { 71 | list = nif[iface]; 72 | if (family) list = list.filter(d => d.family === family); 73 | if (list.length > 1) list = list.filter(d => !isZeroMac(d.mac)); 74 | } 75 | return ifacesSort(list); 76 | } 77 | 78 | list = getAllNetworkIFaces().filter(item => !isZeroMac(item.mac) && (!family || item.family === family)); 79 | 80 | // if (hasMutiMac(list)) list = list.filter(d => d.address); 81 | // if (hasMutiMac(list)) list = list.filter(d => !isVirtualMac(d.mac)); 82 | 83 | // filter by desc for windows 84 | if (hasMutiMac(list) && process.platform === 'win32') { 85 | const info = await getNetworkIFacesInfoByWmic(); // await getNetworkIFacesInfoByIpconfig(); 86 | 87 | if (info.stdout) { 88 | const r = list.filter(item => { 89 | if (!info.config[item.mac]) return true; 90 | const desc = info.config[item.mac].desc; 91 | item.desc = desc; 92 | return !isVirtualMac('', desc); 93 | }); 94 | 95 | if (r.length > 0) list = r; 96 | } 97 | } 98 | 99 | logDebug('[getNetworkIFaces]', list); 100 | return list; 101 | } 102 | 103 | export function getNetworkIFaceOne(iface?: string) { 104 | return getNetworkIFaces(iface).then(list => list[0]); 105 | } 106 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lzwme/get-physical-address", 3 | "version": "1.1.0", 4 | "description": "Try get the physical address(hardware MAC address) of the hosts network interfaces. Filter the virtual machine network card, VPN virtual network card, etc., and return the real MAC address information of the physical network card.", 5 | "main": "cjs/index.js", 6 | "module": "esm/index.js", 7 | "typings": "types/index.d.ts", 8 | "private": false, 9 | "license": "MIT", 10 | "repository": "https://github.com/lzwme/get-physical-address.git", 11 | "author": { 12 | "name": "renxia", 13 | "email": "lzwy0820@qq.com", 14 | "url": "https://lzw.me" 15 | }, 16 | "keywords": [ 17 | "mac", 18 | "mac-address", 19 | "physical-address", 20 | "visual-mac", 21 | "physical", 22 | "hardware", 23 | "network interfaces", 24 | "network card", 25 | "arp", 26 | "arp table", 27 | "arp lookup" 28 | ], 29 | "bin": { 30 | "gmac": "bin/cli.js" 31 | }, 32 | "scripts": { 33 | "prepare": "husky install", 34 | "start": "npm run watch", 35 | "build": "run-s clean && run-p build:*", 36 | "build:main": "tsc -p tsconfig.cjs.json", 37 | "build:module": "tsc -p tsconfig.module.json", 38 | "fix": "run-s fix:*", 39 | "fix:prettier": "prettier \"src/**/*.ts\" --write", 40 | "fix:lint": "eslint src --ext .ts --fix", 41 | "lint": "flh --eslint --tscheck --prettier", 42 | "test": "run-s test:*", 43 | "test:lint": "eslint src --ext .ts", 44 | "test:prettier": "prettier \"src/**/*.ts\" --list-different", 45 | "test:unit": "npm run cov", 46 | "watch": "run-s clean build:main && run-p \"build:main -- -w\" \"test:unit -- --watch\"", 47 | "watch:build": "tsc -p tsconfig.json -w", 48 | "watch:test": "jest --watch", 49 | "cov": "jest --coverage --silent", 50 | "cov:html": "jest --coverage --silent --reporter=html", 51 | "doc": "run-s doc:html && open-cli docs/index.html", 52 | "doc:html": "typedoc src/ --exclude **/*.spec.ts --out docs --tsconfig tsconfig.module.json", 53 | "doc:json": "typedoc src/ --exclude **/*.spec.ts --json docs/typedoc.json --tsconfig tsconfig.module.json", 54 | "version": "standard-version", 55 | "clean": "flh rm -f esm cjs types docs", 56 | "release": "run-s test build doc:html", 57 | "release-version": "run-s release version" 58 | }, 59 | "engines": { 60 | "node": ">=12" 61 | }, 62 | "publishConfig": { 63 | "access": "public", 64 | "registry": "https://registry.npmjs.com" 65 | }, 66 | "peerDependencies": { 67 | "iconv-lite": "*" 68 | }, 69 | "peerDependenciesMeta": { 70 | "iconv-lite": { 71 | "optional": true 72 | } 73 | }, 74 | "devDependencies": { 75 | "@jest/core": "^29", 76 | "@lzwme/fed-lint-helper": "^2.5.1", 77 | "@swc/core": "^1.3.101", 78 | "@swc/jest": "^0.2.29", 79 | "@types/eslint": "^8.56.0", 80 | "@types/jest": "^29.5.11", 81 | "@types/node": "^20", 82 | "@typescript-eslint/eslint-plugin": "^6.15.0", 83 | "@typescript-eslint/parser": "^6.15.0", 84 | "console-log-colors": "^0.4.0", 85 | "eslint": "^8.56.0", 86 | "eslint-config-prettier": "^9.1.0", 87 | "eslint-plugin-jest": "^27.6.0", 88 | "eslint-plugin-prettier": "^5.1.2", 89 | "eslint-plugin-unicorn": "^50.0.1", 90 | "husky": "^8.0.3", 91 | "iconv-lite": "^0.6.3", 92 | "jest": "^29", 93 | "npm-run-all": "^4.1.5", 94 | "prettier": "^3.1.1", 95 | "standard-version": "^9.5.0", 96 | "ts-node": "^10.9.2", 97 | "typedoc": "^0.25.4", 98 | "typescript": "^5.3.3" 99 | }, 100 | "packageManager": "pnpm@8.0.0" 101 | } 102 | -------------------------------------------------------------------------------- /.github/README_zh-CN.md: -------------------------------------------------------------------------------- 1 | [![@lzwme/get-physical-address](https://nodei.co/npm/@lzwme/get-physical-address.png)][npm-url] 2 | 3 | # @lzwme/get-physical-address 4 | 5 | [![build status](https://github.com/lzwme/get-physical-address/actions/workflows/node-ci.yml/badge.svg?branch=main)](https://github.com/lzwme/get-physical-address/actions/workflows/node-ci.yml) 6 | [![NPM version][npm-badge]][npm-url] 7 | [![node version][node-badge]][node-url] 8 | [![npm download][download-badge]][download-url] 9 | [![GitHub issues][issues-badge]][issues-url] 10 | [![GitHub forks][forks-badge]][forks-url] 11 | [![GitHub stars][stars-badge]][stars-url] 12 | ![license MIT](https://img.shields.io/github/license/lzwme/asmd-calc) 13 | 14 | 获取本机物理网卡 Mac 地址。过滤虚拟机网卡、VPN 虚拟网卡等,返回真实的物理网卡 Mac 地址信息。 15 | 16 | ## 背景 17 | 18 | 在 `Node.js` 或 `Electron` 应用中,可能会需要根据 Mac 地址和 IP 地址作为识别设备唯一性的重要标记,并据此作为建立用户设备黑名单/白名单机制的依据。 19 | 20 | 通过 Node.js 提供的 API `os.networkInterfaces()` 可以很方便的获得设备网卡信息。 21 | 22 | 但当用户机器上开启了虚拟机、VPN 等时,该 API 返回的网卡数量可能会非常多,此时需要识别真实的物理网卡,以保证在无论是否使用了虚拟网卡上网方式,都能准确的返回唯一物理网卡的信息,以避免识别出现误差而导致误判。 23 | 24 | 为了尽可能的获取真实物理网卡信息,可以使用的手段主要有: 25 | 26 | - 通过常见虚拟机厂商的 Mac 地址前缀过滤。厂商设备 Mac 地址前缀列表参考: https://standards-oui.ieee.org/oui/oui.txt 27 | - 通过系统命令获取网卡描述,按关键字过滤(windows) 28 | - `ipconfig /all` for windows 29 | - `wmic nic get` for windows 30 | - 按 Mac 地址、IP 地址格式排序优先级 31 | - more... 32 | 33 | ## 安装 34 | 35 | ```bash 36 | npm i @lzwme/get-physical-address 37 | # use yarn 38 | yarn add @lzwme/get-physical-address 39 | ``` 40 | 41 | ## 使用 42 | 43 | 示例: 44 | 45 | ```ts 46 | import { getNetworkIFaceOne, getMac } from '@lzwme/get-physical-address'; 47 | 48 | getNetworkIFaceOne().then(item => { 49 | console.log(`isVirtualMac: ${isVirtualMac(item.mac, item.desc)}. the MAC address is ${item.mac}, the IP address is ${item.address}`); 50 | }); 51 | 52 | getMac().then(mac => console.log(`the MAC address is ${mac}`)); 53 | getMac('en0').then(mac => console.log(`the MAC address for en0 is ${mac}`)); 54 | ``` 55 | 56 | 更多 API 使用示例: 57 | 58 | ```ts 59 | import { isMac, hasMac, isValidMac, isVirtualMac, formatMac, getAllPhysicsMac } from '@lzwme/get-physical-address'; 60 | 61 | isMac('aa-bb-cc-dd-ee-ff'); // true 62 | hasMac('The MAC address is aa-bb-cc-dd-ee-ff'); // true 63 | isMac('00:00:00:00:00:00'); // true 64 | isValidMac('00:00:00:00:00:00'); // false 65 | formatMac('AA-BB-CC-DD-EE-FF'); // aa:bb:cc:dd:ee:ff 66 | isVirtualMac('00:0c:29:ae:ce'); // true 67 | 68 | getAllMac().then(list => console.log(list)); 69 | getAllPhysicsMac('IPv4').then(list => console.log(list)); 70 | ``` 71 | 72 | ## API 73 | 74 | ### `getMac` 75 | 76 | - `getMac(iface?: string): Promise` 77 | - `getAllPhysicsMac(family?: 'IPv4' | 'IPv6'): Promise` 78 | - `getAllMac(): string[]` 仅过滤 `internal=true` 和 MAC 地址为 0 的项 79 | 80 | ### `getNetworkInterface` 81 | 82 | - `getNetworkIFaces(iface?: string, family?: 'IPv4' | 'IPv6'): Promise` 83 | - `getNetworkIFaceOne(iface?: string): Promise` 84 | 85 | ### `utils` 86 | 87 | - `isMac(mac: string): boolean` 88 | - `hasMac(str: string): boolean` 89 | - `isZeroMac(mac: string): boolean` 90 | - `isValidMac(mac: string): boolean` 91 | - `formatMac(mac: string): string` 92 | 93 | ## 开发 94 | 95 | 本地二次开发: 96 | 97 | ```bash 98 | git clone https://github.com/lzwme/get-physical-address 99 | yarn install 100 | npm link 101 | yarn dev 102 | ``` 103 | 104 | 或者 [fork](https://github.com/lzwme/get-physical-address/network) 本项目进行代码贡献。 105 | 106 | **欢迎贡献想法与代码。** 107 | 108 | ## License 109 | 110 | `@lzwme/get-physical-address` is released under the MIT license. 111 | 112 | 该插件由[志文工作室](https://lzw.me)开发和维护。 113 | 114 | [stars-badge]: https://img.shields.io/github/stars/lzwme/get-physical-address.svg 115 | [stars-url]: https://github.com/lzwme/get-physical-address/stargazers 116 | [forks-badge]: https://img.shields.io/github/forks/lzwme/get-physical-address.svg 117 | [forks-url]: https://github.com/lzwme/get-physical-address/network 118 | [issues-badge]: https://img.shields.io/github/issues/lzwme/get-physical-address.svg 119 | [issues-url]: https://github.com/lzwme/get-physical-address/issues 120 | [npm-badge]: https://img.shields.io/npm/v/@lzwme/get-physical-address.svg?style=flat-square 121 | [npm-url]: https://npmjs.org/package/@lzwme/get-physical-address 122 | [node-badge]: https://img.shields.io/badge/node.js-%3E=_12.0.0-green.svg?style=flat-square 123 | [node-url]: https://nodejs.org/download/ 124 | [download-badge]: https://img.shields.io/npm/dm/@lzwme/get-physical-address.svg?style=flat-square 125 | [download-url]: https://npmjs.org/package/@lzwme/get-physical-address 126 | [bundlephobia-url]: https://bundlephobia.com/result?p=@lzwme/get-physical-address@latest 127 | [bundlephobia-badge]: https://badgen.net/bundlephobia/minzip/@lzwme/get-physical-address@latest 128 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![@lzwme/get-physical-address](https://nodei.co/npm/@lzwme/get-physical-address.png)][npm-url] 2 | 3 | # @lzwme/get-physical-address 4 | 5 | [![build status](https://github.com/lzwme/get-physical-address/actions/workflows/node-ci.yml/badge.svg?branch=main)](https://github.com/lzwme/get-physical-address/actions/workflows/node-ci.yml) 6 | [![NPM version][npm-badge]][npm-url] 7 | [![node version][node-badge]][node-url] 8 | [![license MIT](https://img.shields.io/github/license/lzwme/get-physical-address)](https://github.com/lzwme/get-physical-address) 9 | 10 | [![npm download][download-badge]][download-url] 11 | [![GitHub issues][issues-badge]][issues-url] 12 | [![GitHub forks][forks-badge]][forks-url] 13 | [![GitHub stars][stars-badge]][stars-url] 14 | 15 | [简体中文](./.github/README_zh-CN.md) 16 | 17 | Try get the physical address(hardware MAC address) of the hosts network interfaces. Filter the virtual machine network card, VPN virtual network card, etc., and return the real MAC address information of the physical network card. 18 | 19 | ## Background 20 | 21 | In Node.js or electron applications, it may be necessary to use MAC address and IP address as important marks to identify the uniqueness of the device, and use them as the basis for establishing the blacklist / whitelist mechanism of user equipment. 22 | 23 | Via the API of `os.networkInterfaces()` you can easily get the information of the device network card. 24 | 25 | However, when the virtual machine and VPN are enabled on the user's machine, the number of network cards returned by the API may be very large. At this time, it is necessary to identify the real physical network card to ensure that the information of the only physical network card can be accurately returned no matter whether the virtual network card is used or not, so as to avoid misjudgment caused by identification error. 26 | 27 | ## Install 28 | 29 | ```bash 30 | npm i @lzwme/get-physical-address 31 | # use yarn 32 | yarn add @lzwme/get-physical-address 33 | ``` 34 | 35 | ## Usage 36 | 37 | Example: 38 | 39 | ```ts 40 | import { getNetworkIFaceOne, getMac, isVirtualMac } from '@lzwme/get-physical-address'; 41 | 42 | getNetworkIFaceOne().then(item => { 43 | console.log(`isVirtualMac: ${isVirtualMac(item.mac, item.desc)}. the MAC address is ${item.mac}, the IP address is ${item.address}`); 44 | }); 45 | 46 | getMac().then(mac => console.log(`the MAC address is ${mac}`)); 47 | getMac('en0').then(mac => console.log(`the MAC address for en0 is ${mac}`)); 48 | ``` 49 | 50 | Example for some other API: 51 | 52 | ```ts 53 | import { isMac, hasMac, isValidMac, isVirtualMac, formatMac, getAllPhysicsMac } from '@lzwme/get-physical-address'; 54 | 55 | isMac('aa-bb-cc-dd-ee-ff'); // true 56 | hasMac('The MAC address is aa-bb-cc-dd-ee-ff'); // true 57 | isMac('00:00:00:00:00:00'); // true 58 | isValidMac('00:00:00:00:00:00'); // false 59 | formatMac('AA-BB-CC-DD-EE-FF'); // aa:bb:cc:dd:ee:ff 60 | isVirtualMac('00:0c:29:ae:ce'); // true 61 | 62 | getAllMac().then(list => console.log(list)); 63 | getAllPhysicsMac('IPv4').then(list => console.log(list)); 64 | ``` 65 | 66 | ## API 67 | 68 | ### `getMac` 69 | 70 | - `getMac(iface?: string): Promise` 71 | - `getAllPhysicsMac(family?: 'IPv4' | 'IPv6'): Promise` 72 | - `getAllMac(): string[]` Filtered by `internal=true` and `isZeroMac(macAdress)` 73 | 74 | ### `getNetworkInterface` 75 | 76 | - `getNetworkIFaces(iface?: string, family?: 'IPv4' | 'IPv6'): Promise` 77 | - `getNetworkIFaceOne(iface?: string): Promise` 78 | 79 | ### `arpTable` 80 | 81 | - `getArpTable(arpSstdout?: string)` 82 | - `getArpMacByIp(ip: string)` 83 | - `getArpIpByMac(mac: string)` 84 | 85 | ### `utils` 86 | 87 | - `isMac(mac: string): boolean` 88 | - `hasMac(str: string): boolean` 89 | - `isZeroMac(mac: string): boolean` 90 | - `isValidMac(mac: string): boolean` 91 | - `formatMac(mac: string): string` 92 | 93 | ## Development 94 | 95 | ```bash 96 | git clone https://github.com/lzwme/get-physical-address 97 | yarn install 98 | npm link 99 | yarn dev 100 | ``` 101 | 102 | ## License 103 | 104 | `@lzwme/get-physical-address` is released under the MIT license. 105 | 106 | 该插件由[志文工作室](https://lzw.me)开发和维护。 107 | 108 | [stars-badge]: https://img.shields.io/github/stars/lzwme/get-physical-address.svg 109 | [stars-url]: https://github.com/lzwme/get-physical-address/stargazers 110 | [forks-badge]: https://img.shields.io/github/forks/lzwme/get-physical-address.svg 111 | [forks-url]: https://github.com/lzwme/get-physical-address/network 112 | [issues-badge]: https://img.shields.io/github/issues/lzwme/get-physical-address.svg 113 | [issues-url]: https://github.com/lzwme/get-physical-address/issues 114 | [npm-badge]: https://img.shields.io/npm/v/@lzwme/get-physical-address.svg?style=flat-square 115 | [npm-url]: https://npmjs.org/package/@lzwme/get-physical-address 116 | [node-badge]: https://img.shields.io/badge/node.js-%3E=_12.0.0-green.svg?style=flat-square 117 | [node-url]: https://nodejs.org/download/ 118 | [download-badge]: https://img.shields.io/npm/dm/@lzwme/get-physical-address.svg?style=flat-square 119 | [download-url]: https://npmjs.org/package/@lzwme/get-physical-address 120 | [bundlephobia-url]: https://bundlephobia.com/result?p=@lzwme/get-physical-address@latest 121 | [bundlephobia-badge]: https://badgen.net/bundlephobia/minzip/@lzwme/get-physical-address@latest 122 | -------------------------------------------------------------------------------- /src/__test__/testData.mock.ts: -------------------------------------------------------------------------------- 1 | export const ifacesMock = { 2 | lo0: [ 3 | { 4 | address: '127.0.0.1', 5 | netmask: '255.0.0.0', 6 | family: 'IPv4', 7 | mac: '00:00:00:00:00:00', 8 | internal: true, 9 | cidr: '127.0.0.1/8', 10 | }, 11 | { 12 | address: '::1', 13 | netmask: 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff', 14 | family: 'IPv6', 15 | mac: '00:00:00:00:00:00', 16 | internal: true, 17 | cidr: '::1/12', 18 | scopeid: 0, 19 | }, 20 | { 21 | address: 'fe80::1', 22 | netmask: 'ffff:ffff:ffff:ffff::', 23 | family: 'IPv6', 24 | mac: '00:00:00:00:00:00', 25 | internal: true, 26 | cidr: 'fe80::1/64', 27 | scopeid: 1, 28 | }, 29 | ], 30 | en1: [ 31 | { 32 | address: 'fe80::aede:48ff:fe00:1122', 33 | netmask: 'ffff:ffff:ffff:ffff::', 34 | family: 'IPv6', 35 | mac: 'ac:de:48:00:bb:cc', 36 | internal: false, 37 | cidr: 'fe80::aede:48ff:fe00:1122/64', 38 | scopeid: 4, 39 | }, 40 | ], 41 | en0: [ 42 | { 43 | address: 'fe80::ccc:d04d:ff59:aa99', 44 | netmask: 'ffff:ffff:ffff:ffff::', 45 | family: 'IPv6', 46 | mac: 'f0:18:98:9e:bb:cc', 47 | internal: false, 48 | cidr: 'fe80::ccc:d04d:ff59:aa99/64', 49 | scopeid: 6, 50 | }, 51 | { 52 | address: '192.168.31.100', 53 | netmask: '255.255.255.0', 54 | family: 'IPv4', 55 | mac: '00:1d:7d:71:a8:d6', 56 | internal: false, 57 | cidr: '192.168.31.100/24', 58 | }, 59 | ], 60 | awdl0: [ 61 | { 62 | address: 'fe80::b4af:b5ff:fe40:aa8b', 63 | netmask: 'ffff:ffff:ffff:ffff::', 64 | family: 'IPv6', 65 | mac: 'b6:af:b5:40:aa:bb', 66 | internal: false, 67 | cidr: 'fe80::b4af:b5ff:fe40:aa8b/64', 68 | scopeid: 10, 69 | }, 70 | ], 71 | vboxnet1: [ 72 | { 73 | address: '192.168.31.110', 74 | netmask: '255.255.255.0', 75 | family: 'IPv4', 76 | mac: '00:1d:7d:71:a8:ff', 77 | internal: false, 78 | cidr: '192.168.31.110/24', 79 | }, 80 | ], 81 | vmware: [ 82 | { 83 | address: '192.168.31.120', 84 | netmask: '255.255.255.0', 85 | family: 'IPv4', 86 | mac: '00:0c:29:71:a8:ff', 87 | internal: false, 88 | cidr: '192.168.31.120/24', 89 | }, 90 | { 91 | address: '192.168.31.111', 92 | netmask: '255.255.255.0', 93 | family: 'IPv4', 94 | mac: '00:00:00:00:00:00', 95 | internal: false, 96 | cidr: '192.168.31.111/24', 97 | }, 98 | { 99 | address: 'fe80::1', 100 | netmask: 'ffff:ffff:ffff:ffff::', 101 | family: 'IPv6', 102 | mac: '00:00:00:00:00:00', 103 | internal: true, 104 | cidr: 'fe80::1/64', 105 | scopeid: 1, 106 | }, 107 | ], 108 | }; 109 | 110 | export const ipconfigStdout = [ 111 | `Windows IP Configuration`, 112 | ` Host Name . . . . . . . . . . . . : PCNAME`, // 域中计算机名、主机名 113 | ` Primary Dns Suffix . . . . . . . :`, // 主 DNS 后缀 114 | ` Node Type . . . . . . . . . . . . : Unknown`, // 节点类型 115 | ` IP Routing Enabled. . . . . . . . : Yes`, // IP路由服务是否启用 116 | ` WINS Proxy Enabled. . . . . . . . : No`, // WINS代理服务是否启用 117 | ` `, 118 | `Ethernet adapter:`, // 本地连接 119 | ` Connection-specific DNS Suffix :`, 120 | ` Description . . . . . . . . . . . : Realtek RTL8168/8111 PCI-E Gigabi`, 121 | ` Physical Address. . . . . . . . . : ${ifacesMock.en0[1].mac.replace(/:/g, '-').toUpperCase()}`, 122 | ` DHCP Enabled. . . . . . . . . . . : No`, 123 | ` IP Address. . . . . . . . . . . . : ${ifacesMock.en0[1].address}(Preferred)`, 124 | ` Subnet Mask . . . . . . . . . . . : 255.255.255.0`, 125 | ` Default Gateway . . . . . . . . . : 192.168.90.254`, 126 | ` DHCP Server. . . . . . . .. . . . : 192.168.90.88`, 127 | ` DNS Servers . . . . . . . . . . . : 114.114.114.114`, 128 | ` 8.8.8.8`, 129 | ` Lease Obtained. . . . . . . . . . : 2022-5-1 8:13:54`, 130 | ` Lease Expires . . . . . . .. . . .: 2022-5-10 8:13:54`, 131 | ` abc . . . . . . .. . . .: Yes`, 132 | `以太网适配器 VMware Network Adapter VMnet1: 133 | 134 | 连接特定的 DNS 后缀 . . . . . . . : 135 | 描述. . . . . . . . . . . . . . . : VMware Virtual Ethernet Adapter for VMnet1 136 | 物理地址. . . . . . . . . . . . . : 00-50-56-C0-00-01 137 | DHCP 已启用 . . . . . . . . . . . : 否 138 | 自动配置已启用. . . . . . . . . . : 是 139 | 本地链接 IPv6 地址. . . . . . . . : fe80::1935:e4f6:3781:4d4c%19(首选) 140 | IPv4 地址 . . . . . . . . . . . . : 192.168.160.1(首选) 141 | 子网掩码 . . . . . . . . . . . . : 255.255.255.0 142 | 默认网关. . . . . . . . . . . . . : 143 | DHCPv6 IAID . . . . . . . . . . . : 83906646 144 | DHCPv6 客户端 DUID . . . . . . . : 00-01-00-01-1F-75-64-25-C8-5B-76-70-B3-98 145 | DNS 服务器 . . . . . . . . . . . : fec0:0:0:ffff::1%1 146 | fec0:0:0:ffff::2%1 147 | fec0:0:0:ffff::3%1 148 | TCPIP 上的 NetBIOS . . . . . . . : 已启用 149 | 150 | 以太网适配器 VMware Network Adapter VMnet8: 151 | 152 | 连接特定的 DNS 后缀 . . . . . . . : 153 | 描述. . . . . . . . . . . . . . . : VMware Virtual Ethernet Adapter for VMnet8 154 | 物理地址. . . . . . . . . . . . . : 00-50-56-C0-00-08 155 | DHCP 已启用 . . . . . . . . . . . : 否 156 | 自动配置已启用. . . . . . . . . . : 是 157 | 本地链接 IPv6 地址. . . . . . . . : fe80::55a5:a92f:74b4:6fec%15(首选) 158 | IPv4 地址 . . . . . . . . . . . . : 192.168.87.1(首选) 159 | 子网掩码 . . . . . . . . . . . . : 255.255.255.0 160 | 默认网关. . . . . . . . . . . . . : 161 | DHCPv6 IAID . . . . . . . . . . . : 285233238 162 | DHCPv6 客户端 DUID . . . . . . . : 00-01-00-01-1F-75-64-25-C8-5B-76-70-B3-98 163 | DNS 服务器 . . . . . . . . . . . : fec0:0:0:ffff::1%1 164 | fec0:0:0:ffff::2%1 165 | fec0:0:0:ffff::3%1 166 | TCPIP 上的 NetBIOS . . . . . . . : 已启用`, 167 | ].join('\n'); 168 | 169 | export const wmicNicStdout = [ 170 | `MACAddress=${ifacesMock.en0[1].mac} \nName=Realtek PCIe FE Family Controller `, 171 | `MACAddress=1C:1B:B5:9B:FF:CC \nName=Intel(R) Wireless-AC 9462 `, 172 | `MACAddress=${ifacesMock.vmware[0].mac} \nName=Visual Adpter`, 173 | `MACAddress=${ifacesMock.vmware[1].mac} \nName=Visual Vmware Adpter 0`, 174 | ].join('\n'); 175 | 176 | const arpAWinStdout = `接口: 10.10.1.108 --- 0xc 177 | Internet 地址 物理地址 类型 178 | 10.10.1.1 dc-ef-80-37-aa-ff 动态 179 | 10.10.1.2 dc-ef-80-37-cc-ff 动态 180 | 10.10.1.3 00-00-ff-00-ff-ff 动态 181 | 10.10.1.43 00-00-ff-00-ff-ff 动态 182 | 10.10.1.112 00-00-ff-00-ff-ff 动态 183 | 10.10.1.255 ff-ff-ff-ff-ff-ff 静态 184 | 224.0.0.2 01-00-ff-00-ff-f2 静态 185 | 224.0.0.22 01-00-ff-00-ff-f6 静态 186 | 224.0.0.251 01-00-ff-00-ff-fb 静态 187 | 224.0.0.252 01-00-ff-00-ff-fc 静态 188 | 239.255.255.250 01-00-ff-7f-ff-fa 静态 189 | 255.255.255.255 ff-ff-ff-ff-ff-ff 静态 190 | 191 | 接口: 172.29.64.1 --- 0x15 192 | Internet 地址 物理地址 类型 193 | 172.29.79.255 ff-ff-ff-ff-ff-ff 静态 194 | 224.0.0.2 01-00-ff-00-ff-f2 静态 195 | 224.0.0.22 01-00-ff-00-ff-f6 静态 196 | 224.0.0.251 01-00-ff-00-ff-fb 静态 197 | 224.0.0.252 01-00-ff-00-ff-fc 静态 198 | 239.255.255.250 01-00-ff-7f-ff-ff 静态 199 | 255.255.255.255 ff-ff-ff-ff-ff-ff 静态`; 200 | 201 | const arpANMacStdout = `? (10.10.2.2) at dc:ef:80:37:ff:ff on en1 ifscope [ethernet] 202 | ? (10.10.2.3) at 0:0:5e:0:1:ff on en1 ifscope [ethernet] 203 | ? (10.10.2.51) at a0:80:69:96:ff:ff on en1 ifscope [ethernet] 204 | ? (10.10.2.54) at a4:cf:99:96:ff:ff on en1 ifscope [ethernet] 205 | ? (10.10.2.96) at 3c:22:fb:52:ff:ff on en1 ifscope [ethernet] 206 | ? (10.10.2.121) at 62:0:34:b8:ff:ff on en1 ifscope [ethernet] 207 | ? (10.10.2.135) at 6c:b1:33:9c:ff:ff on en1 ifscope [ethernet] 208 | ? (10.10.2.182) at 3c:6:30:0:ff:ff on en1 ifscope [ethernet] 209 | ? (10.10.2.255) at ff:ff:ff:ff:ff:ff on en1 ifscope [ethernet] 210 | ? (224.0.0.251) at 1:0:5e:0:0:ff on en1 ifscope permanent [ethernet] 211 | ? (239.255.255.250) at 1:0:5e:7f:ff:ff on en1 ifscope permanent [ethernet]`; 212 | 213 | const arpANLinuxStdout = `? (10.12.11.2) at 70:79:b3:48:ff:ff [ether] on eno1 214 | ? (172.17.125.49) at on docker0 215 | ? (10.12.11.40) at 18:66:da:e8:ff:ff [ether] on eno1 216 | ? (10.12.11.54) at 1c:98:ec:27:ff:ff [ether] on eno1 217 | ? (172.17.0.2) at 02:42:ac:11:ff:ff [ether] on docker0 218 | ? (172.17.12.1) at on docker0 219 | ? (10.12.11.46) at 14:18:77:3c:ff:ff [ether] on eno1 220 | ? (10.12.11.3) at 00:00:0c:9f:ff:ff [ether] on eno1 221 | ? (10.12.11.41) at 18:66:da:e8:ff:ff [ether] on eno1 222 | ? (10.12.11.55) at 1c:98:ec:27:ff:ff [ether] on eno1 223 | ? (10.12.11.21) at 40:f2:e9:9d:ff:ff [ether] on eno1 224 | ? (172.17.0.3) at 02:42:ac:11:ff:ff [ether] on docker0 225 | ? (10.12.11.1) at 70:79:b3:48:ff:ff [ether] on eno1 226 | ? (10.12.11.47) at 14:18:77:37:ff:ff [ether] on eno1 227 | ? (10.12.11.59) at 94:18:82:6d:ff:ff [ether] on eno1 228 | ? (10.12.11.239) at 00:0c:29:44:ff:ff [ether] on eno1`; 229 | 230 | export const arpANStdout = { 231 | win32: arpAWinStdout, 232 | mac: arpANMacStdout, 233 | linux: arpANLinuxStdout, 234 | }; 235 | --------------------------------------------------------------------------------