Skip to content

Commit abe2afe

Browse files
authored
feat: add ip.link command utils (#19)
1 parent d518fa9 commit abe2afe

File tree

3 files changed

+211
-0
lines changed

3 files changed

+211
-0
lines changed

src/ip/index.ts

+33
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,31 @@
1+
import { del as linkDel } from './link';
2+
3+
// ------------------------
4+
15
export * as route from './route';
6+
export * as link from './link';
7+
8+
// ------------------------
9+
10+
/**
11+
* Drop Device
12+
*
13+
* Example:
14+
* ```javascript
15+
* await dropDevice('tun0');
16+
* ```
17+
*
18+
* @param name "DEVICE_NAME" or "dev NAME" or "group NAME"
19+
*/
20+
export async function dropDevice(name: string) {
21+
const { error } = await linkDel({
22+
name,
23+
});
24+
25+
if (error) {
26+
throw error;
27+
}
28+
}
229

330
// ------------------------
431

@@ -7,6 +34,8 @@ export type IPFamily = 'IPv4' | 'IPv6';
734
export const IPV4_REGEX = /^(\d{1,3}\.){3}\d{1,3}$/;
835
export const IPV6_REGEX = /^(::)?(((\d{1,3}\.){3}(\d{1,3}){1})?([0-9a-f]){0,4}:{0,2}){1,8}(::)?$/i;
936

37+
export const MAC_REGEX = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/;
38+
1039
export function isValidIP(ip: string) {
1140
return isValidIPv4(ip) || isValidIPv6(ip);
1241
}
@@ -150,3 +179,7 @@ export function* generateIP(cidr: string): Generator<string, void> {
150179
yield fromLong(currentIP + i);
151180
}
152181
}
182+
183+
export function isValidMacAddress(mac: string) {
184+
return MAC_REGEX.test(mac);
185+
}

src/ip/link.ts

+151
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import { execShell } from '@/utils/exec-shell';
2+
import { isValidMacAddress } from '@/ip/index';
3+
4+
const LINK_TYPES = <const>[
5+
'amt',
6+
'bareudp',
7+
'bond',
8+
'bond_slave',
9+
'bridge',
10+
'bridge_slave',
11+
'dsa',
12+
'dummy',
13+
'erspan',
14+
'geneve',
15+
'gre',
16+
'gretap',
17+
'gtp',
18+
'ifb',
19+
'ip6erspan',
20+
'ip6gre',
21+
'ip6gretap',
22+
'ip6tnl',
23+
'ipip',
24+
'ipoib',
25+
];
26+
27+
export type LinkType = (typeof LINK_TYPES)[number];
28+
29+
export type AddParams = Record<string, string> & {
30+
type?: LinkType;
31+
name?: string;
32+
txqueuelen?: number;
33+
address?: string;
34+
broadcast?: string;
35+
mtu?: number;
36+
numtxqueues?: number;
37+
numrxqueues?: number;
38+
netns?: string;
39+
};
40+
41+
export async function add(params: AddParams) {
42+
const args = [
43+
'ip',
44+
'link',
45+
'add',
46+
...Object.entries(params).map(([key, value]) => `${key.toLowerCase()} ${value}`),
47+
];
48+
return execShell(args);
49+
}
50+
51+
export type DelParams = Record<string, string> & {
52+
name?: string;
53+
type?: LinkType;
54+
};
55+
56+
export async function del(params: DelParams) {
57+
const args = [
58+
'ip',
59+
'link',
60+
'delete',
61+
...Object.entries(params).map(([key, value]) => `${key.toLowerCase()} ${value}`),
62+
];
63+
return execShell(args);
64+
}
65+
66+
export type ListParams = Record<string, string> & {
67+
name?: string;
68+
type?: LinkType;
69+
};
70+
71+
export type Link = {
72+
name: string;
73+
flags: string[];
74+
qdisc: string;
75+
state: string;
76+
mode: string;
77+
group: string;
78+
qlen?: number;
79+
mtu: number;
80+
address?: string;
81+
broadcast?: string;
82+
};
83+
84+
export async function list(params: ListParams = {}): Promise<Link[]> {
85+
const args = [
86+
'ip',
87+
'link',
88+
'show',
89+
...Object.entries(params).map(([key, value]) => `${key.toLowerCase()} ${value}`),
90+
];
91+
92+
const { error, data } = await execShell(args);
93+
94+
if (error) {
95+
throw error;
96+
}
97+
98+
const lines = data.output.match(/^\d+:(.*|(?:\n\s)+)+/gm)!;
99+
const links: Link[] = [];
100+
101+
for (const line of lines) {
102+
const link = parseLink(line);
103+
if (link) {
104+
links.push(link);
105+
}
106+
}
107+
108+
return links;
109+
}
110+
111+
function parseLink(line: string) {
112+
if (!line) {
113+
return undefined;
114+
}
115+
116+
const parts = line.replace(/\n$/, ' ').split(/\s+/).slice(1);
117+
if (parts.length < 6) {
118+
return undefined;
119+
}
120+
121+
const link: Link = {
122+
name: parts[0].slice(0, -1),
123+
flags: parts[1].slice(1, -1).split(','),
124+
qdisc: '',
125+
state: '',
126+
mode: '',
127+
group: '',
128+
qlen: undefined,
129+
mtu: -1,
130+
address: undefined,
131+
broadcast: undefined,
132+
};
133+
134+
for (const part of parts.slice(2)) {
135+
const next = parts[parts.indexOf(part) + 1];
136+
if (part in link) {
137+
// @ts-expect-error Type 'string' is not assignable to type 'Link[keyof Link]'.
138+
link[part] = next.match(/^\d+$/) ? +next : next;
139+
}
140+
141+
if (part.startsWith('link/') && isValidMacAddress(next)) {
142+
link.address = next;
143+
}
144+
145+
if (part === 'brd') {
146+
link.broadcast = next;
147+
}
148+
}
149+
150+
return link;
151+
}

tests/ip/link.test.ts

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { link } from 'node-netkit/ip';
2+
import { expect } from 'chai';
3+
4+
describe('Link', () => {
5+
it('should list links and locate loopback', async () => {
6+
const links = await link.list();
7+
8+
expect(links).to.be.an('array');
9+
expect(links.length).to.be.greaterThan(0);
10+
11+
expect(links[0]).to.have.property('name');
12+
expect(links[0]).to.have.property('flags');
13+
expect(links[0]).to.have.property('qdisc');
14+
expect(links[0]).to.have.property('state');
15+
expect(links[0]).to.have.property('mode');
16+
expect(links[0]).to.have.property('group');
17+
expect(links[0]).to.have.property('qlen');
18+
expect(links[0]).to.have.property('mtu');
19+
expect(links[0]).to.have.property('address');
20+
expect(links[0]).to.have.property('broadcast');
21+
22+
const loopback = links.find((l) => l.name === 'lo');
23+
expect(loopback).to.be.an('object');
24+
expect(loopback).to.have.property('address', '00:00:00:00:00:00');
25+
expect(loopback).to.have.property('broadcast', '00:00:00:00:00:00');
26+
});
27+
});

0 commit comments

Comments
 (0)