buninu-linux 0.11.56 → 0.14.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,85 @@
1
+ // Best-effort one-shot DHCP for cfg.net. Each interface owns its result and
2
+ // bounded output buffer; a failed link never changes another link's status.
3
+ export function createDhcpLauncher({spawn=(argv,options)=>Bun.spawn(argv,options),log=console.log,
4
+ acquisitionSeconds=20,watchdogMs=25000,killGraceMs=2000,outputGraceMs=250}={}) {
5
+ const active=new Map();
6
+ const messageOf=error=>String(error?.message??error);
7
+ const clean=text=>String(text).replace(/\x1b\[[0-?]*[ -/]*[@-~]/g,'').replace(/[\x00-\x1f\x7f]/g,' ').trim();
8
+ const report=(name,message)=>{try{log(`[dhcp ${clean(name)}] ${clean(message)}`);}catch{}};
9
+ return function startDhcp(interfaces) {
10
+ const started=[];
11
+ try {
12
+ for(const [index,name] of interfaces.entries()) {
13
+ if(name==='lo'||active.has(name))continue;
14
+ let child,state;
15
+ try {
16
+ if(!/^[a-zA-Z0-9_.:-]{1,15}$/.test(name))throw new Error('invalid interface name');
17
+ const args=[process.execPath,`${import.meta.dir}/../bin/dhclient`,'-1','-timeout',String(acquisitionSeconds),'--metric',String(100+index)];
18
+ // Keep a single resolver owner; init already supplies fallback DNS.
19
+ if(index>0)args.push('--no-dns');
20
+ args.push(name);
21
+ child=spawn(args,{stdin:'ignore',stdout:'pipe',stderr:'pipe'});
22
+ state={child,done:false,timer:null,killTimer:null,timedOut:false,address:null,errors:[],notes:[],readers:[]};active.set(name,state);
23
+ const remember=(list,text)=>{list.push(clean(text).slice(0,512));if(list.length>4)list.shift();};
24
+ const kill=signal=>{try{child.kill(signal);}catch(e){remember(state.errors,`${signal}: ${messageOf(e)}`);}};
25
+ state.timer=setTimeout(()=>{
26
+ if(state.done)return;
27
+ state.timedOut=true;kill('SIGTERM');
28
+ state.killTimer=setTimeout(()=>{if(!state.done)kill('SIGKILL');},killGraceMs);
29
+ state.killTimer.unref?.();
30
+ },watchdogMs);state.timer.unref?.();
31
+ const line=(text,isError)=>{
32
+ text=clean(text);if(!text)return;
33
+ const bound=!isError&&/^dhclient: bound to ([^,\s]+)/.exec(text);
34
+ if(bound)state.address=bound[1];
35
+ else remember(isError?state.errors:state.notes,text.replace(/^dhclient:\s*/,''));
36
+ };
37
+ const drain=async(stream,isError)=>{
38
+ if(!stream)return;
39
+ const reader=stream.getReader();state.readers.push(reader);
40
+ const decoder=new TextDecoder();let pending='';
41
+ try {
42
+ for(;;){
43
+ const {done,value}=await reader.read();if(done)break;
44
+ pending+=decoder.decode(value,{stream:true});
45
+ let at;while((at=pending.indexOf('\n'))>=0){line(pending.slice(0,at),isError);pending=pending.slice(at+1);}
46
+ if(pending.length>4096){line(pending.slice(0,4096),isError);pending='';}
47
+ }
48
+ if(pending)line(pending,isError);
49
+ }finally{reader.releaseLock();}
50
+ };
51
+ const drains=[drain(child.stdout,false),drain(child.stderr,true)].map(p=>p.catch(e=>remember(state.errors,`output: ${messageOf(e)}`)));
52
+ const finish=async(code,error)=>{
53
+ state.done=true;clearTimeout(state.timer);clearTimeout(state.killTimer);
54
+ // exited can resolve before the last stderr/stdout chunk arrives.
55
+ // Bound the wait even if an inherited pipe never closes.
56
+ let outputTimer;
57
+ try {
58
+ await Promise.race([Promise.all(drains),new Promise(resolve=>{outputTimer=setTimeout(resolve,outputGraceMs);})]);
59
+ }finally{
60
+ clearTimeout(outputTimer);
61
+ for(const reader of state.readers){try{void reader.cancel().catch(()=>{});}catch{}}
62
+ if(active.get(name)===state)active.delete(name);
63
+ }
64
+ if(state.timedOut)report(name,`failed: timed out; other interfaces are unaffected`);
65
+ else if(error!==undefined)report(name,`failed: ${messageOf(error)}`);
66
+ else if(code===0&&state.address)report(name,`configured ${state.address}; command finished`);
67
+ else if(code===0)report(name,'finished without configuring an address');
68
+ else {
69
+ const detail=state.errors.at(-1)??state.notes.at(-1)??'no error details';
70
+ report(name,`failed: ${detail} (exit ${code}); other interfaces are unaffected`);
71
+ }
72
+ };
73
+ void Promise.resolve(child.exited).then(code=>finish(code),error=>finish(null,error??'unknown exit error')).catch(e=>report(name,`result unavailable: ${messageOf(e)}`));
74
+ started.push(name);report(name,'requesting configuration');
75
+ }catch(error){
76
+ if(state){clearTimeout(state.timer);clearTimeout(state.killTimer);active.delete(name);}
77
+ if(child){try{child.kill('SIGKILL');}catch{}}
78
+ report(name,`failed to start: ${messageOf(error)}; other interfaces are unaffected`);
79
+ }
80
+ }
81
+ }catch(error){report('setup',`failed: ${messageOf(error)}; init continues`);}
82
+ return started;
83
+ };
84
+ }
85
+ export const startDhcp=createDhcpLauncher();
@@ -0,0 +1,44 @@
1
+ import {libc,ptr,check,errno,SysError,cString,withInetSocket} from './dlopen.js';
2
+ import {ipv4,wrapIPv4,unwrapIPv4} from './dhcp.js';
3
+ export function interfaceInfo(name) {
4
+ if(!/^[a-zA-Z0-9_.:-]{1,15}$/.test(name))throw new Error('invalid interface name');
5
+ return withInetSocket(ioctl=>{
6
+ const req=Buffer.alloc(40);req.write(name);ioctl(0x8933,req,'SIOCGIFINDEX');const index=req.readInt32LE(16);
7
+ ioctl(0x8927,req,'SIOCGIFHWADDR');if(req.readUInt16LE(16)!==1)throw new Error('DHCP requires an Ethernet-compatible interface');
8
+ return {index,mac:Buffer.from(req.subarray(18,24))};
9
+ });
10
+ }
11
+ const sockaddr=(ip,port)=>{const b=Buffer.alloc(16);b.writeUInt16LE(2);b.writeUInt16BE(port,2);ipv4(ip).copy(b,4);return b;};
12
+ export function openTransport(name,{index}) {
13
+ const f=libc.symbols, descriptors=[];
14
+ try {
15
+ // Packet sockets work before the interface has any IPv4 address or route.
16
+ const raw=check('socket(AF_PACKET): requires CAP_NET_RAW',f.socket(17,2|0x800|0x80000,0x0008));descriptors.push(raw);
17
+ const link=Buffer.alloc(20);link.writeUInt16LE(17);link.writeUInt16BE(0x0800,2);link.writeInt32LE(index,4);link[11]=6;link.fill(255,12,18);
18
+ check('bind(AF_PACKET)',f.bind(raw,ptr(link),link.length));
19
+ // Reserve the client UDP port and use kernel routing/ARP for unicast renew
20
+ // and release. The packet socket receives both broadcast and unicast replies.
21
+ const udp=check('socket(UDP)',f.socket(2,2|0x800|0x80000,17));descriptors.push(udp);
22
+ const device=cString(name);check('SO_BINDTODEVICE',f.setsockopt(udp,1,25,ptr(device),device.length));
23
+ const local=sockaddr('0.0.0.0',68);check('bind UDP port 68 (another DHCP client may be running)',f.bind(udp,ptr(local),16));
24
+ const buffer=Buffer.alloc(65535);
25
+ return {
26
+ send(payload,server){
27
+ const destination=server?sockaddr(server,67):link;
28
+ const bytes=server?payload:wrapIPv4(payload,payload.subarray(12,16).join('.'));
29
+ check('sendto(DHCP)',Number(f.sendto(server?udp:raw,ptr(bytes),bytes.length,0,ptr(destination),destination.length)));
30
+ },
31
+ receive(){
32
+ // Drain the UDP copy so its receive buffer does not remain full.
33
+ for(let i=0;i<64;i++)if(f.recv(udp,ptr(buffer),buffer.length,0)<0)break;
34
+ for(let i=0;i<64;i++){
35
+ const n=f.recv(raw,ptr(buffer),buffer.length,0);
36
+ if(n<0){const code=errno();if(code===11||code===4)return null;throw new SysError('recv(DHCP)',code);}
37
+ const payload=unwrapIPv4(buffer.subarray(0,Number(n)));if(payload)return Buffer.from(payload);
38
+ }
39
+ return null;
40
+ },
41
+ close(){for(const fd of descriptors.splice(0))f.close(fd);},
42
+ };
43
+ }catch(error){for(const fd of descriptors)f.close(fd);throw error;}
44
+ }
@@ -0,0 +1,124 @@
1
+ // DHCPv4 codec and lease state machine. Network/configuration adapters are
2
+ // separate so protocol tests never change the host's addresses or resolver.
3
+ import { randomBytes } from 'node:crypto';
4
+ export const TYPE = { DISCOVER:1, OFFER:2, REQUEST:3, DECLINE:4, ACK:5, NAK:6, RELEASE:7 };
5
+ export const ipv4 = text => {
6
+ const parts=String(text).split('.');
7
+ if(parts.length!==4 || parts.some(p=>!/^\d{1,3}$/.test(p)||Number(p)>255))throw new Error(`invalid IPv4 address: ${text}`);
8
+ return Buffer.from(parts.map(Number));
9
+ };
10
+ const address=b=>[...b].join('.');
11
+ export function checksum(b) {let n=0;for(let i=0;i<b.length;i+=2)n+=(b[i]<<8)|(b[i+1]??0);while(n>>>16)n=(n&65535)+(n>>>16);return (~n)&65535;}
12
+ export function encode({type,xid,mac,requested,server,ciaddr='0.0.0.0',hostname,secs=0,broadcast=true}) {
13
+ const b=Buffer.alloc(240);b[0]=1;b[1]=1;b[2]=6;b.writeUInt32BE(xid,4);b.writeUInt16BE(Math.min(65535,secs),8);
14
+ b.writeUInt16BE(broadcast?0x8000:0,10);ipv4(ciaddr).copy(b,12);Buffer.from(mac).copy(b,28,0,6);b.writeUInt32BE(0x63825363,236);
15
+ const options=[];
16
+ const option=(code,value)=>{value=Buffer.from(value);if(value.length>255)throw new Error('DHCP option too long');options.push(Buffer.from([code,value.length]),value);};
17
+ option(53,[type]);option(61,[1,...mac]);
18
+ if(requested)option(50,ipv4(requested));if(server)option(54,ipv4(server));
19
+ if(hostname)option(12,Buffer.from(hostname));
20
+ if(type!==TYPE.RELEASE){option(55,[1,3,6,15,28,51,58,59]);option(57,[5,220]);}
21
+ const packet=Buffer.concat([b,...options,Buffer.from([255])]);
22
+ return packet.length<300?Buffer.concat([packet,Buffer.alloc(300-packet.length)]):packet;
23
+ }
24
+ export function decode(b,{xid,mac}={}) {
25
+ if(b.length<240||b[0]!==2||b[1]!==1||b[2]!==6||b.readUInt32BE(236)!==0x63825363)return null;
26
+ if(xid!==undefined&&b.readUInt32BE(4)!==xid)return null;
27
+ if(mac&&!b.subarray(28,34).equals(Buffer.from(mac)))return null;
28
+ const options=new Map();
29
+ function parse(start,end){for(let i=start;i<end;){const code=b[i++];if(code===255)return true;if(!code)continue;if(i>=end)return false;const len=b[i++];if(i+len>end)return false;options.set(code,Buffer.concat([options.get(code)??Buffer.alloc(0),b.subarray(i,i+len)]));i+=len;}return true;}
30
+ if(!parse(240,b.length))return null;
31
+ const overload=options.get(52);if(overload&&(overload.length!==1||overload[0]>3))return null;
32
+ if(overload?.[0]&1)if(!parse(108,236))return null;
33
+ if(overload?.[0]&2)if(!parse(44,108))return null;
34
+ if(options.get(53)?.length!==1)return null;
35
+ for(const code of [1,28,51,54,58,59])if(options.has(code)&&options.get(code).length!==4)return null;
36
+ for(const code of [3,6])if(options.has(code)&&(!options.get(code).length||options.get(code).length%4))return null;
37
+ const ip=code=>options.has(code)?address(options.get(code)):undefined;
38
+ const ips=code=>{const v=options.get(code),out=[];if(v)for(let i=0;i<v.length;i+=4)out.push(address(v.subarray(i,i+4)));return out;};
39
+ const number=code=>options.get(code)?.readUInt32BE(0);
40
+ return {type:options.get(53)[0],xid:b.readUInt32BE(4),address:address(b.subarray(16,20)),server:ip(54),mask:ip(1),routers:options.has(3)?ips(3):undefined,dns:options.has(6)?ips(6):undefined,lease:number(51),t1:number(58),t2:number(59)};
41
+ }
42
+ export function prefixOf(mask) {
43
+ const n=ipv4(mask).readUInt32BE(),inverse=(~n)>>>0;
44
+ if((inverse&(inverse+1))!==0)throw new Error('DHCP supplied a non-contiguous subnet mask');
45
+ return 32-Math.log2(inverse+1);
46
+ }
47
+ export function leaseFrom(reply,previous,now) {
48
+ const ip=reply.address==='0.0.0.0'?previous?.address:reply.address;
49
+ const server=reply.server,mask=reply.mask??previous?.mask;
50
+ if(!ip||ip==='0.0.0.0'||!server||!mask||!reply.lease)throw new Error('DHCP ACK is missing address, server, mask or lease duration');
51
+ const prefix=prefixOf(mask),seconds=reply.lease;
52
+ let t1=reply.t1??seconds*.5,t2=reply.t2??seconds*.875;
53
+ if(!(t1>0&&t1<t2&&t2<seconds)){t1=seconds*.5;t2=seconds*.875;}
54
+ return {address:ip,server,mask,prefix,routers:reply.routers??previous?.routers??[],dns:reply.dns??previous?.dns??[],seconds,
55
+ acquiredAt:now,renewAt:now+t1*1000,rebindAt:now+t2*1000,expiresAt:now+seconds*1000};
56
+ }
57
+ // SOCK_DGRAM AF_PACKET supplies/consumes the Ethernet header itself.
58
+ export function wrapIPv4(payload,source='0.0.0.0',destination='255.255.255.255') {
59
+ const b=Buffer.alloc(28+payload.length);b[0]=0x45;b.writeUInt16BE(b.length,2);b[8]=64;b[9]=17;
60
+ ipv4(source).copy(b,12);ipv4(destination).copy(b,16);b.writeUInt16BE(checksum(b.subarray(0,20)),10);
61
+ b.writeUInt16BE(68,20);b.writeUInt16BE(67,22);b.writeUInt16BE(8+payload.length,24);payload.copy(b,28);
62
+ // IPv4 allows zero UDP checksum; the IPv4 header is checksummed above.
63
+ return b;
64
+ }
65
+ export function unwrapIPv4(b) {
66
+ if(b.length<28||b[0]>>>4!==4||b[9]!==17)return null;
67
+ const h=(b[0]&15)*4,n=b.readUInt16BE(2);
68
+ if(h<20||n>b.length||n<h+8||(b.readUInt16BE(6)&0x3fff)||checksum(b.subarray(0,h)))return null;
69
+ const len=b.readUInt16BE(h+4);
70
+ if(b.readUInt16BE(h)!==67||b.readUInt16BE(h+2)!==68||len<8||h+len>n)return null;
71
+ if(b.readUInt16BE(h+6)){
72
+ const pseudo=Buffer.alloc(12);b.copy(pseudo,0,12,20);pseudo[9]=17;pseudo.writeUInt16BE(len,10);
73
+ if(checksum(Buffer.concat([pseudo,b.subarray(h,h+len)])))return null;
74
+ }
75
+ return b.subarray(h+8,h+len);
76
+ }
77
+ export class NakError extends Error {}
78
+ export class DhcpClient {
79
+ constructor({transport,mac,hostname,timeout=60,signal,log=()=>{},now=Date.now,sleep=ms=>Bun.sleep(ms),random=Math.random}) {
80
+ Object.assign(this,{transport,mac,hostname,timeout,signal,log,now,sleep,random});
81
+ }
82
+ get stopped(){return this.signal?.aborted;}
83
+ async exchange(fields,types,deadline,server) {
84
+ const xid=randomBytes(4).readUInt32BE(),start=this.now();let retry=0,next=start;
85
+ while(!this.stopped&&this.now()<deadline){
86
+ if(this.now()>=next){
87
+ const packet=encode({...fields,xid,mac:this.mac,hostname:this.hostname,secs:Math.floor((this.now()-start)/1000)});
88
+ this.transport.send(packet,fields.broadcast===false?server:undefined);
89
+ next=this.now()+Math.min(64000,4000*2**Math.min(retry++,4))*(.75+.5*this.random());
90
+ }
91
+ // Bound the work per iteration so unrelated traffic cannot starve timers.
92
+ for(let i=0;i<64;i++){
93
+ const packet=this.transport.receive();if(!packet)break;
94
+ const reply=decode(packet,{xid,mac:this.mac});
95
+ if(!reply||!types.includes(reply.type)||!reply.server||(server&&reply.server!==server))continue;
96
+ if(reply.type===TYPE.NAK)throw new NakError('DHCP server rejected the lease');
97
+ return {reply,sentAt:start};
98
+ }
99
+ await this.sleep(Math.min(20,Math.max(1,deadline-this.now())));
100
+ }
101
+ return null;
102
+ }
103
+ async acquire() {
104
+ const deadline=this.now()+this.timeout*1000;
105
+ while(!this.stopped&&this.now()<deadline){
106
+ this.log('DHCPDISCOVER');
107
+ const offer=await this.exchange({type:TYPE.DISCOVER},[TYPE.OFFER],deadline);
108
+ if(!offer)return null;
109
+ if(offer.reply.address==='0.0.0.0')continue;
110
+ this.log(`DHCPOFFER ${offer.reply.address} from ${offer.reply.server}`);
111
+ try {
112
+ const result=await this.exchange({type:TYPE.REQUEST,requested:offer.reply.address,server:offer.reply.server},[TYPE.ACK,TYPE.NAK],Math.min(deadline,this.now()+16000),offer.reply.server);
113
+ if(result){if(result.reply.address!==offer.reply.address)continue;return leaseFrom(result.reply,offer.reply,result.sentAt);}
114
+ }catch(error){if(!(error instanceof NakError))throw error;await this.sleep(1000);}
115
+ }
116
+ return null;
117
+ }
118
+ async renew(lease,rebind=false) {
119
+ this.log(rebind?'DHCPREQUEST broadcast (rebinding)':'DHCPREQUEST unicast (renewing)');
120
+ const result=await this.exchange({type:TYPE.REQUEST,ciaddr:lease.address,broadcast:rebind},[TYPE.ACK,TYPE.NAK],rebind?lease.expiresAt:lease.rebindAt,rebind?undefined:lease.server);
121
+ return result?leaseFrom(result.reply,lease,result.sentAt):null;
122
+ }
123
+ release(lease){this.transport.send(encode({type:TYPE.RELEASE,xid:randomBytes(4).readUInt32BE(),mac:this.mac,ciaddr:lease.address,server:lease.server,broadcast:false}),lease.server);}
124
+ }
@@ -3,8 +3,7 @@
3
3
  // init.js opens musl on its own because it runs before /proc exists and must
4
4
  // not import anything. Every other command imports this module instead, so
5
5
  // the symbol table, the errno plumbing and the error messages live in one
6
- // place. The library is located relative to this file (/lib in the guest),
7
- // which also lets the commands run from a checkout for testing.
6
+ // place. Automatic lookup follows the runtime ABI; BUNINU_LIBC overrides it.
8
7
 
9
8
  import { dlopen, FFIType, ptr, read, CString, toArrayBuffer } from "bun:ffi";
10
9
 
@@ -20,11 +19,13 @@ const architectures = {
20
19
  export const arch = architectures[process.arch]
21
20
  ?? (() => { throw new Error(`unsupported CPU architecture ${process.arch}`); })();
22
21
 
23
- // BUNINU_LIBC lets the commands run on a glibc host for testing:
24
- // BUNINU_LIBC=/lib/aarch64-linux-gnu/libc.so.6 bun initramfs/bin/ip addr
25
- export const libcPath = process.env.BUNINU_LIBC ?? `${import.meta.dir}/${arch.libc}`;
26
-
27
- export const libc = dlopen(libcPath, {
22
+ // Automatic lookup must not mix bionic, glibc and musl runtime ABIs.
23
+ // An explicit override is authoritative: report its error instead of hiding it.
24
+ const android = process.platform === "android";
25
+ const glibc = !android && !!process.report?.getReport?.().header?.glibcVersionRuntime;
26
+ export const libcFamily = android ? "bionic" : glibc ? "glibc" : "musl";
27
+ const errnoSymbol = android ? "__errno" : "__errno_location";
28
+ const definitions = {
28
29
  mount: {
29
30
  args: [FFIType.ptr, FFIType.ptr, FFIType.ptr, FFIType.u64, FFIType.ptr],
30
31
  returns: FFIType.i32,
@@ -41,7 +42,7 @@ export const libc = dlopen(libcPath, {
41
42
  args: [],
42
43
  returns: FFIType.void,
43
44
  },
44
- __errno_location: {
45
+ [errnoSymbol]: {
45
46
  args: [],
46
47
  returns: FFIType.ptr,
47
48
  },
@@ -77,6 +78,14 @@ export const libc = dlopen(libcPath, {
77
78
  args: [FFIType.i32, FFIType.i32, FFIType.i32],
78
79
  returns: FFIType.i32,
79
80
  },
81
+ bind: { args: [FFIType.i32, FFIType.ptr, FFIType.u32], returns: FFIType.i32 },
82
+ inet_pton: { args: [FFIType.i32, FFIType.ptr, FFIType.ptr], returns: FFIType.i32 },
83
+ connect: { args: [FFIType.i32, FFIType.ptr, FFIType.u32], returns: FFIType.i32 },
84
+ getsockname: { args: [FFIType.i32, FFIType.ptr, FFIType.ptr], returns: FFIType.i32 },
85
+ setsockopt: { args: [FFIType.i32, FFIType.i32, FFIType.i32, FFIType.ptr, FFIType.u32], returns: FFIType.i32 },
86
+ send: { args: [FFIType.i32, FFIType.ptr, FFIType.u64, FFIType.i32], returns: FFIType.i64 },
87
+ sendto: { args: [FFIType.i32, FFIType.ptr, FFIType.u64, FFIType.i32, FFIType.ptr, FFIType.u32], returns: FFIType.i64 },
88
+ recv: { args: [FFIType.i32, FFIType.ptr, FFIType.u64, FFIType.i32], returns: FFIType.i64 },
80
89
  syscall: {
81
90
  args: [FFIType.i64, FFIType.i64, FFIType.ptr, FFIType.i64],
82
91
  returns: FFIType.i32,
@@ -93,13 +102,51 @@ export const libc = dlopen(libcPath, {
93
102
  args: [FFIType.ptr],
94
103
  returns: FFIType.i32,
95
104
  },
96
- });
105
+ };
106
+ const override = process.env.BUNINU_LIBC;
107
+ const candidates = override !== undefined ? [override]
108
+ : android ? ["libc.so", "/apex/com.android.runtime/lib64/bionic/libc.so", "/system/lib64/libc.so"]
109
+ : glibc ? ["libc.so.6", `/lib/${process.arch === "x64" ? "x86_64" : "aarch64"}-linux-gnu/libc.so.6`, `/usr/lib/${process.arch === "x64" ? "x86_64" : "aarch64"}-linux-gnu/libc.so.6`]
110
+ : [`${import.meta.dir}/${arch.libc}`, arch.libc, `/lib/${arch.libc}`, `/lib/ld-musl-${process.arch === "x64" ? "x86_64" : "aarch64"}.so.1`, "libc.so"];
111
+ // Bun requires at least one symbol per dlopen call. Defer opening until a
112
+ // symbol is requested; unrelated missing symbols never prevent an import.
113
+ export let libcPath;
114
+ const handles = [], bound = new Map();
115
+ let closed = false;
116
+ function resolve(name) {
117
+ if (bound.has(name)) return bound.get(name);
118
+ const failures = [];
119
+ for (const path of libcPath === undefined ? candidates : [libcPath]) {
120
+ try {
121
+ const handle = dlopen(path, { [name]: definitions[name] });
122
+ libcPath = path; handles.push(handle); bound.set(name, handle.symbols[name]);
123
+ return handle.symbols[name];
124
+ } catch (error) { failures.push(`${path}: ${error.message}`); }
125
+ }
126
+ throw new Error(`Cannot load libc symbol ${name}:\n${failures.join("\n")}`);
127
+ }
128
+ export const libc = {
129
+ symbols: new Proxy({}, {
130
+ get(_target, name) {
131
+ if (closed) throw new Error("libc is closed");
132
+ if (!Object.hasOwn(definitions, name)) return undefined;
133
+ const symbol = resolve(name);
134
+ // Compile the errno accessor before the native call, so first-use FFI
135
+ // compilation cannot clobber the errno that the caller wants to inspect.
136
+ // An absent accessor only fails when errno() is actually requested.
137
+ if (name !== errnoSymbol && !bound.has(errnoSymbol)) {
138
+ try { resolve(errnoSymbol); } catch {}
139
+ }
140
+ return symbol;
141
+ },
142
+ }),
143
+ close() { if (closed) return; closed = true; for (const handle of handles) handle.close(); },
144
+ };
97
145
 
98
146
  export const cString = (value) => new TextEncoder().encode(`${value}\0`);
99
147
 
100
148
  // musl reports failure as -1 plus errno; the raw syscall() wrapper does too.
101
- const errnoLocation = libc.symbols.__errno_location();
102
- export const errno = () => read.i32(errnoLocation);
149
+ export const errno = () => read.i32(libc.symbols[errnoSymbol]());
103
150
  export const strerror = (code) => new CString(libc.symbols.strerror(code)).toString();
104
151
 
105
152
  export const Errno = {
@@ -0,0 +1,110 @@
1
+ import { isIP } from 'node:net';
2
+ import { randomBytes } from 'node:crypto';
3
+
4
+ export function parsePing(args) {
5
+ const o={count:4,interval:1,timeout:2,deadline:Infinity,size:56,ttl:64,family:0,quiet:false,timestamp:false};
6
+ const values={c:'count',i:'interval',W:'timeout',w:'deadline',s:'size',t:'ttl',I:'source'};
7
+ let positional=false;
8
+ for(let i=0;i<args.length;i++){
9
+ const arg=args[i];
10
+ if(!positional&&arg==='--'){positional=true;continue;}
11
+ if(!positional&&(arg==='--help'||arg==='-h')){o.help=true;continue;}
12
+ if(!positional&&arg.startsWith('-')&&arg!=='-'){
13
+ for(let j=1;j<arg.length;j++){
14
+ const flag=arg[j];
15
+ if(values[flag]){const value=arg.slice(j+1)||args[++i];if(value===undefined||value==='')throw Error(`-${flag} requires a value`);if(flag!=='I'&&(!Number.isFinite(Number(value))||!value.trim()))throw Error(`invalid ${values[flag]}`);o[values[flag]]=flag==='I'?value:Number(value);break;}
16
+ if(flag==='4'||flag==='6')o.family=Number(flag);
17
+ else if(flag==='q')o.quiet=true;
18
+ else if(flag==='D')o.timestamp=true;
19
+ else if(flag!=='n')throw Error(`unknown option -${flag}`);
20
+ }
21
+ }else{if(o.host)throw Error('only one destination is supported');o.host=arg;}
22
+ }
23
+ if(o.help)return o;
24
+ if(!o.host)throw Error('destination required (see ping --help)');
25
+ for(const key of ['count','size','ttl'])if((key!=='count'||o.count!==Infinity)&&(!Number.isInteger(o[key])||o[key]<(key==='size'?0:1)))throw Error(`invalid ${key}`);
26
+ if(o.size>65507||o.ttl>255||o.count>Number.MAX_SAFE_INTEGER&&o.count!==Infinity)throw Error('count, packet size or TTL out of range');
27
+ for(const key of ['interval','timeout','deadline'])if(!(o[key]>0)||(!Number.isFinite(o[key])&&!(key==='deadline'&&o[key]===Infinity)))throw Error(`invalid ${key}`);
28
+ if(o.interval<.002)throw Error('minimum interval is 0.002 seconds');
29
+ return o;
30
+ }
31
+ export function checksum(bytes) {
32
+ let sum=0;for(let i=0;i<bytes.length;i+=2)sum+=(bytes[i]<<8)|(bytes[i+1]??0);
33
+ while(sum>>>16)sum=(sum&65535)+(sum>>>16);return (~sum)&65535;
34
+ }
35
+ export function echoPacket(family,id,seq,size) {
36
+ const b=Buffer.alloc(size+8);b[0]=family===6?128:8;b.writeUInt16BE(id,4);b.writeUInt16BE(seq,6);randomBytes(size).copy(b,8);
37
+ if(family===4)b.writeUInt16BE(checksum(b),2);return b;
38
+ }
39
+ export function isEchoReply(bytes,{family,raw,id,seq,packet}) {
40
+ let offset=0;
41
+ if(raw&&family===4){if(bytes.length<20||bytes[0]>>>4!==4||bytes[9]!==1)return false;offset=(bytes[0]&15)*4;if(offset<20||offset>bytes.length-8)return false;}
42
+ const b=bytes.subarray(offset);
43
+ return b.length===packet.length&&b[0]===(family===6?129:0)&&b[1]===0&&b.readUInt16BE(4)===id&&b.readUInt16BE(6)===seq&&b.subarray(8).equals(packet.subarray(8))&&(family===6||checksum(b)===0);
44
+ }
45
+ export async function runPing(o,{log=console.log,error=console.error,forceRaw=false}={}) {
46
+ if(!['linux','android'].includes(process.platform))throw Error('ping currently supports Linux and Android; Windows needs an ICMP API backend');
47
+ const {libc,ptr,cString,errno,SysError,check}=await import('./dlopen.js');
48
+ const c=libc.symbols;
49
+ let family=o.family||isIP(o.host)||4,address=o.host;
50
+ if(isIP(address)&&isIP(address)!==family)throw Error('destination does not match selected address family');
51
+ if(!isIP(address)){
52
+ const records=await Bun.dns.lookup(address,{family});address=records.find(r=>r.family===family)?.address;
53
+ if(!address)throw Error(`no IPv${family} address for ${o.host}`);
54
+ }
55
+ const af=family===6?10:2;
56
+ const sockaddr=value=>{
57
+ const b=Buffer.alloc(family===6?28:16);b.writeUInt16LE(af,0);
58
+ if(c.inet_pton(af,ptr(cString(value)),ptr(b.subarray(family===6?8:4)))!==1)throw Error(`invalid IPv${family} address: ${value}`);
59
+ return b;
60
+ };
61
+ let raw=forceRaw,fd=c.socket(af,(raw?3:2)|0x80800,family===6?58:1);
62
+ if(fd<0&&!raw){const code=errno();if(![1,13,93,97].includes(code))throw new SysError('ICMP socket',code);raw=true;fd=c.socket(af,3|0x80800,family===6?58:1);}
63
+ if(fd<0)throw new SysError('ICMP socket (allow ping_group_range or CAP_NET_RAW)');
64
+ let stop=false;const interrupt=()=>{stop=true};let transmitted=0,received=0,min=Infinity,max=0,sum=0,sumSquares=0;
65
+ const started=performance.now(),pending=new Map();let next=started;
66
+ const prefix=()=>o.timestamp?`[${(Date.now()/1000).toFixed(6)}] `:'';
67
+ try{
68
+ if(o.source){
69
+ if(isIP(o.source)){const src=sockaddr(o.source);check('bind source address',c.bind(fd,ptr(src),src.length));}
70
+ else{const name=cString(o.source);check('bind interface',c.setsockopt(fd,1,25,ptr(name),name.length));}
71
+ }
72
+ const ttl=Buffer.alloc(4);ttl.writeInt32LE(o.ttl);check('set TTL',c.setsockopt(fd,family===6?41:0,family===6?16:2,ptr(ttl),4));
73
+ const remote=sockaddr(address);check('connect',c.connect(fd,ptr(remote),remote.length));
74
+ const local=Buffer.alloc(28),length=Buffer.alloc(4);length.writeUInt32LE(local.length);check('getsockname',c.getsockname(fd,ptr(local),ptr(length)));
75
+ const id=raw?randomBytes(2).readUInt16BE():local.readUInt16BE(2);
76
+ log(`PING ${o.host} (${address}) ${o.size} data bytes`);
77
+ process.on('SIGINT',interrupt);process.on('SIGTERM',interrupt);
78
+ const buffer=Buffer.alloc(65536);
79
+ while(!stop&&performance.now()-started<o.deadline*1000){
80
+ let now=performance.now();
81
+ if(transmitted<o.count&&now>=next){
82
+ const seq=(transmitted+1)&65535;
83
+ // Avoid confusing a sequence that wrapped while an old probe is pending.
84
+ if(!pending.has(seq)){
85
+ const packet=echoPacket(family,id,seq,o.size);const sent=Number(c.send(fd,ptr(packet),packet.length,0));
86
+ if(sent!==packet.length)throw new SysError('send');
87
+ transmitted++;pending.set(seq,{packet,start:now});
88
+ }
89
+ next=now+o.interval*1000;
90
+ }
91
+ for(let n=0;n<64;n++){
92
+ const size=Number(c.recv(fd,ptr(buffer),buffer.length,0));
93
+ if(size<0){const code=errno();if(code===11)break;if(code===4)continue;if([101,111,113].includes(code)){if(!o.quiet)error(`${prefix()}ping: ${new SysError('receive',code).message}`);break;}throw new SysError('receive',code);}
94
+ const b=buffer.subarray(0,size),offset=raw&&family===4?(b[0]&15)*4:0;
95
+ if(size<offset+8)continue;const seq=b.readUInt16BE(offset+6),p=pending.get(seq);
96
+ if(!p||!isEchoReply(b,{family,raw,id,seq,packet:p.packet}))continue;
97
+ const ms=performance.now()-p.start;pending.delete(seq);received++;min=Math.min(min,ms);max=Math.max(max,ms);sum+=ms;sumSquares+=ms*ms;
98
+ if(!o.quiet)log(`${prefix()}${p.packet.length} bytes from ${address}: icmp_seq=${seq}${raw&&family===4?` ttl=${b[8]}`:''} time=${ms.toFixed(3)} ms`);
99
+ }
100
+ now=performance.now();for(const [seq,p] of pending)if(now-p.start>=o.timeout*1000){pending.delete(seq);if(!o.quiet)log(`${prefix()}Request timeout for icmp_seq=${seq}`);}
101
+ if(transmitted>=o.count&&!pending.size)break;
102
+ const sendDelay=transmitted<o.count?next-now:10;
103
+ await Bun.sleep(Math.max(1,Math.min(10,sendDelay,started+o.deadline*1000-now)));
104
+ }
105
+ }finally{process.removeListener('SIGINT',interrupt);process.removeListener('SIGTERM',interrupt);c.close(fd);}
106
+ log(`\n--- ${o.host} ping statistics ---`);
107
+ log(`${transmitted} packets transmitted, ${received} received, ${transmitted?((transmitted-received)/transmitted*100).toFixed(0):0}% packet loss, time ${Math.round(performance.now()-started)}ms`);
108
+ if(received)log(`rtt min/avg/max/mdev = ${min.toFixed(3)}/${(sum/received).toFixed(3)}/${max.toFixed(3)}/${Math.sqrt(Math.max(0,sumSquares/received-(sum/received)**2)).toFixed(3)} ms`);
109
+ return received>0&&!(Number.isFinite(o.deadline)&&Number.isFinite(o.count)&&received<o.count)?0:1;
110
+ }
@@ -35,7 +35,7 @@ identified from the process task name and its open console descriptor under
35
35
  | --- | --- |
36
36
  | `N`, `/dev/ttyN` | The virtual console to use for graphics and the keyboard; for example, `2` means `/dev/tty2`. Naming a VT other than the active one starts a detached bunterm there, switches the display to it, and immediately returns control to the calling shell. Without this argument, bunterm uses the active VT in the foreground. |
37
37
  | `-e command [argument...]` | The program to run; everything after `-e` belongs to it. Default `/bin/sh`. |
38
- | `-s N`, `--font-size N` | Font size in pixels (default 16). The cell is 0.6 × N wide and 1.16 × N tall with DejaVu Sans Mono, so 1024×768 at 16 px gives 106 × 41 cells. |
38
+ | `-s N`, `--font-size N` | Font size in pixels (default 13). The cell is 0.6 × N wide and 1.16 × N tall with DejaVu Sans Mono, so 1024×768 at 16 px gives 106 × 41 cells. |
39
39
  | `--line-height F` | Multiplies the cell height (default 1). |
40
40
  | `--fb /dev/fbN` | The framebuffer device (default `/dev/fb0`). |
41
41
  | `--no-blink` | A steady cursor. |
@@ -0,0 +1,82 @@
1
+ # dhclient — DHCPv4 client
2
+
3
+ Use `ip link` to list available network interfaces, then choose the interface
4
+ name for the command, for example `dhclient usb0`.
5
+
6
+ Acquire an IPv4 lease on an Ethernet-compatible interface. DHCP packets are
7
+ implemented in JavaScript; Bun FFI calls the runtime libc for packet/UDP
8
+ sockets and interface identification. Address and route changes invoke the
9
+ existing Buninu `ip` command, without an external DHCP binary or dependencies.
10
+
11
+ ```sh
12
+ dhclient eth0 # acquire, configure, then exit
13
+ dhclient -v -timeout 20 eth0 # show progress, fail after 20 seconds
14
+ dhclient -r eth0 # release a saved lease after the client stops
15
+ dhclient --no-dns eth0 # leave the resolver unchanged
16
+ ```
17
+
18
+ | Option | Meaning |
19
+ | --- | --- |
20
+ | `-4` | DHCPv4 (default) |
21
+ | `-d` | Accepted for compatibility; all invocations exit after configuration |
22
+ | `-1` | Acquire once (already the default) |
23
+ | `-r` | Send DHCPRELEASE and remove the saved lease's configuration |
24
+ | `-timeout SECONDS` | Initial acquisition deadline; default 60 seconds |
25
+ | `-v`, `-q` | Verbose protocol progress, or quiet output |
26
+ | `-H HOSTNAME` | Send DHCP hostname option |
27
+ | `-lf PATH` | JSON lease file; default `/var/lib/dhclient/INTERFACE.json` |
28
+ | `--metric NUMBER` | Default route metric, 0–32766 |
29
+ | `--no-dns` | Do not write DNS configuration |
30
+ | `--resolv-conf PATH` | Resolver file; default `/etc/resolv.conf` |
31
+ | `-h`, `--help` | This manual |
32
+
33
+ Specify one interface per process. The interface is brought up first. This
34
+ client replaces its primary IPv4 address and installs the first offered
35
+ router as its default gateway. DNS server addresses replace the resolver
36
+ file; its previous content is saved and restored on explicit release only if
37
+ another writer has not modified it. `--no-dns` is useful for secondary links.
38
+ Avoid simultaneous resolver management by multiple clients.
39
+
40
+ Each invocation acquires and configures one lease, then exits. No background
41
+ renewal process is started. `-1` is an optional alias for this default behavior.
42
+ A DHCP lease is still time-limited unless the server grants an infinite lease:
43
+ run the command again before expiry, or use an appropriate static address.
44
+ After exit this command cannot renew or remove an expired address. The reported
45
+ lease duration and JSON timestamps show the server's validity period.
46
+ Ctrl-C/SIGTERM during acquisition stops the attempt; successful configuration
47
+ remains after normal exit. Use `-r` to release it explicitly. Saved JSON leases
48
+ support release and resolver restoration; their addresses are never assumed
49
+ valid on a later acquisition.
50
+
51
+ The per-interface `/run/dhclient-INTERFACE.lock` prevents duplicate Buninu
52
+ clients, including callers using different lease files. Stop the running
53
+ client before `-r`. After an unclean kill, verify it is gone before removing
54
+ the stale lock. UDP port 68 is reserved exclusively to detect other clients.
55
+
56
+ Linux/Android on x86-64 or aarch64 is supported, subject to privileges:
57
+ CAP_NET_RAW for packet sockets, CAP_NET_ADMIN for interface/route changes,
58
+ and permission to bind UDP 68 and write lease/resolver files. Ordinary
59
+ unprivileged Termux cannot configure Android networking. Root in Buninu's
60
+ VM can. Library selection follows the shared `dlopen.js` runtime ABI rules.
61
+
62
+ This is a focused client, not an ISC dhclient CLI clone. DHCPv6, ISC config/
63
+ script hooks, address-conflict ARP probing/DECLINE, classless static routes,
64
+ and automatic renewal/daemonization are not implemented. Wi-Fi must already be
65
+ associated. A failed acquisition exits nonzero; successful one-shot/release
66
+ or clean signal termination returns zero.
67
+
68
+ Protocol: [RFC 2131](https://www.rfc-editor.org/rfc/rfc2131) and
69
+ [DHCP options, RFC 2132](https://www.rfc-editor.org/rfc/rfc2132).
70
+ Run `bun test test/dhcp.test.js test/libc.test.js` for codec, state-machine,
71
+ configuration and libc regression tests; tests do not reconfigure host links.
72
+
73
+ `cfg.net` launches foreground clients as background child processes. Each has
74
+ a 20-second acquisition deadline and an independent 25-second startup watchdog
75
+ (SIGTERM, then SIGKILL after 2 seconds if necessary). Successful clients exit after configuration; none remain to renew. All child and output failures are caught and logged. Metrics start
76
+ at 100 per interface; only the first interface may write DNS. Repeated calls
77
+ reuse active clients; a later call can retry clients which have exited.
78
+
79
+ Automatic calls report one final result per interface, prefixed with
80
+ `[dhcp INTERFACE]`. A failed interface does not change another interface's
81
+ success result or configuration; output is collected before reporting exit
82
+ status so a delayed stderr message cannot appear after its final result.