11use std:: fs;
22use std:: io:: Write ;
3- use std:: os :: unix :: process :: ExitStatusExt as _ ;
4- use std:: process :: Command ;
3+ use std:: net :: { UdpSocket , Ipv4Addr } ;
4+ use std:: time :: Duration ;
55
6- use anyhow:: { anyhow, Context , Result } ;
7- use log:: debug;
6+ use anyhow:: { Context , Result } ;
87use rustix:: system:: sethostname;
98
10- use crate :: utils:: env:: find_in_path;
11- use crate :: utils:: fs:: find_executable;
9+ use neli:: {
10+ consts:: {
11+ nl:: NlmF ,
12+ rtnl:: { Arphrd , Ifa , IfaF , Iff , Rta , RtAddrFamily , Rtm , RtmF , Rtn ,
13+ Rtprot , RtScope , RtTable } ,
14+ socket:: NlFamily ,
15+ } ,
16+ nl:: { NlPayload , Nlmsghdr } ,
17+ router:: synchronous:: { NlRouter , NlRouterReceiverHandle } ,
18+ rtnl:: { Ifaddrmsg , IfaddrmsgBuilder , Ifinfomsg , IfinfomsgBuilder ,
19+ RtattrBuilder , Rtmsg , RtmsgBuilder } ,
20+ utils:: Groups ,
21+ types:: RtBuffer ,
22+ } ;
23+
24+ /// Set interface flags for eth0 (interface index 2) with a given mask
25+ fn flags_eth0 ( rtnl : & NlRouter , mask : Iff , set : Iff ) -> Result < ( ) > {
26+ let ifinfomsg = IfinfomsgBuilder :: default ( )
27+ . ifi_family ( RtAddrFamily :: Unspecified )
28+ . ifi_type ( Arphrd :: Ether ) . ifi_index ( 2 )
29+ . ifi_change ( mask) . ifi_flags ( set)
30+ . build ( ) ?;
31+
32+ let _: NlRouterReceiverHandle < Rtm , Ifinfomsg > =
33+ rtnl. send ( Rtm :: Newlink , NlmF :: REQUEST , NlPayload :: Payload ( ifinfomsg) ) ?;
34+
35+ Ok ( ( ) )
36+ }
37+
38+ /// Add or delete IPv4 routes for eth0 (interface index 2)
39+ fn route4_eth0 ( rtnl : & NlRouter , what : Rtm , gw : Ipv4Addr ) -> Result < ( ) > {
40+ let rtmsg = RtmsgBuilder :: default ( )
41+ . rtm_family ( RtAddrFamily :: Inet )
42+ . rtm_dst_len ( 0 ) . rtm_src_len ( 0 ) . rtm_tos ( 0 )
43+ . rtm_table ( RtTable :: Main ) . rtm_protocol ( Rtprot :: Boot )
44+ . rtm_scope ( RtScope :: Universe ) . rtm_type ( Rtn :: Unicast )
45+ . rtm_flags ( RtmF :: empty ( ) )
46+ . rtattrs ( RtBuffer :: from_iter ( [
47+ RtattrBuilder :: default ( )
48+ . rta_type ( Rta :: Oif )
49+ . rta_payload ( 2 )
50+ . build ( ) ?,
51+ RtattrBuilder :: default ( )
52+ . rta_type ( Rta :: Dst )
53+ . rta_payload ( Ipv4Addr :: UNSPECIFIED . octets ( ) . to_vec ( ) )
54+ . build ( ) ?,
55+ RtattrBuilder :: default ( )
56+ . rta_type ( Rta :: Gateway )
57+ . rta_payload ( gw. octets ( ) . to_vec ( ) )
58+ . build ( ) ?
59+ ] ) )
60+ . build ( ) ?;
61+
62+ let _: NlRouterReceiverHandle < Rtm , Rtmsg > =
63+ rtnl. send ( what, NlmF :: CREATE | NlmF :: REQUEST ,
64+ NlPayload :: Payload ( rtmsg) ) ?;
65+
66+ Ok ( ( ) )
67+ }
68+
69+ /// Add or delete IPv4 addresses for eth0 (interface index 2)
70+ fn addr4_eth0 ( rtnl : & NlRouter , what : Rtm , addr : Ipv4Addr , prefix_len : u8 )
71+ -> Result < ( ) > {
72+ let ifaddrmsg = IfaddrmsgBuilder :: default ( )
73+ . ifa_family ( RtAddrFamily :: Inet )
74+ . ifa_prefixlen ( prefix_len)
75+ . ifa_scope ( RtScope :: Universe )
76+ . ifa_index ( 2 )
77+ . rtattrs ( RtBuffer :: from_iter ( [
78+ RtattrBuilder :: default ( )
79+ . rta_type ( Ifa :: Local )
80+ . rta_payload ( addr. octets ( ) . to_vec ( ) )
81+ . build ( ) ?,
82+ RtattrBuilder :: default ( )
83+ . rta_type ( Ifa :: Address )
84+ . rta_payload ( addr. octets ( ) . to_vec ( ) )
85+ . build ( ) ?,
86+ ] ) )
87+ . build ( ) ?;
88+
89+ let _: NlRouterReceiverHandle < Rtm , Ifaddrmsg > =
90+ rtnl. send ( what, NlmF :: CREATE | NlmF :: REQUEST ,
91+ NlPayload :: Payload ( ifaddrmsg) ) ?;
92+
93+ Ok ( ( ) )
94+ }
95+
96+ /// Send DISCOVER with Rapid Commit, process ACK, configure address and route
97+ fn do_dhcp ( rtnl : & NlRouter ) -> Result < ( ) > {
98+ // Temporary link-local address and route avoid the need for raw sockets
99+ route4_eth0 ( rtnl, Rtm :: Newroute , Ipv4Addr :: UNSPECIFIED ) ?;
100+ addr4_eth0 ( rtnl, Rtm :: Newaddr , Ipv4Addr :: new ( 169 , 254 , 1 , 1 ) , 16 ) ?;
101+
102+ // Send request (DHCPDISCOVER)
103+ let socket = UdpSocket :: bind ( "0.0.0.0:68" ) . expect ( "Failed to bind" ) ;
104+ let mut buf = [ 0 ; 576 /* RFC 2131, Section 2 */ ] ;
105+
106+ const REQUEST : [ u8 ; 300 /* From RFC 951: >= 60 B of options */ ] = [
107+ 1 /* REQUEST */ , 0x1 /* Ethernet */ , 6 /* hlen */ , 0 /* Hops */ ,
108+ 1 , 2 , 3 , 4 /* XID */ , 0 , 0 /* Seconds */ , 0x80 , 0x0 /* Flags */ ,
109+ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , /* All-zero (four) addresses */
110+ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , /* 16B HW address: who cares */
111+ /* 32 bytes per row: 64B 'sname', plus 128B 'file' (RFC 1531) */
112+ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ,
113+ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ,
114+ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ,
115+ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ,
116+ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ,
117+ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ,
118+ 0x63 , 0x82 , 0x53 , 0x63 , /* DHCP (magic) cookie, then options: */
119+ 53 , 1 , 1 /* DISCOVER */ , 80 , 0 /* Rapid commit */ , 0xff , // Done
120+ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ,
121+ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 /* 54B paaaadding */
122+ ] ;
123+
124+ socket. set_broadcast ( true ) ?;
125+ socket. send_to ( & REQUEST , "255.255.255.255:67" ) ?;
126+
127+ // Keep IPv6-only fast
128+ let _ = socket. set_read_timeout ( Some ( Duration :: from_millis ( 100 ) ) ) ;
129+
130+ // Get and process response (DHCPACK) if any
131+ if let Ok ( ( len, _) ) = socket. recv_from ( & mut buf) {
132+ let msg = & mut buf[ ..len] ;
133+
134+ let addr = Ipv4Addr :: new ( msg[ 16 ] , msg[ 17 ] , msg[ 18 ] , msg[ 19 ] ) ;
135+ let mut netmask = Ipv4Addr :: UNSPECIFIED ;
136+ let mut router = Ipv4Addr :: UNSPECIFIED ;
137+ let mut p: usize = 240 ;
138+
139+ while p < len {
140+ let o = msg[ p] ;
141+ let mut l: u8 ;
142+ l = msg[ p + 1 ] ;
143+
144+ if o == 1 { // Option 1: Subnet Mask
145+ netmask = Ipv4Addr :: new ( msg[ p + 2 ] , msg[ p + 3 ] ,
146+ msg[ p + 4 ] , msg[ p + 5 ] ) ;
147+ } else if o == 3 { // Option 3: Router
148+ router = Ipv4Addr :: new ( msg[ p + 2 ] , msg[ p + 3 ] ,
149+ msg[ p + 4 ] , msg[ p + 5 ] ) ;
150+ } else if o == 0xff { // Option 255: End (of options)
151+ break ;
152+ }
153+
154+ l += 2 ; // Length doesn't include code and length field itself
155+ p += l as usize ;
156+ }
157+
158+ let prefix_len : u8 = netmask. to_bits ( ) . leading_ones ( ) as u8 ;
159+
160+ // Drop temporary address and route, configure what we got instead
161+ route4_eth0 ( rtnl, Rtm :: Delroute , Ipv4Addr :: UNSPECIFIED ) ?;
162+ addr4_eth0 ( rtnl, Rtm :: Deladdr , Ipv4Addr :: new ( 169 , 254 , 1 , 1 ) , 16 ) ?;
163+
164+ addr4_eth0 ( rtnl, Rtm :: Newaddr , addr, prefix_len) ?;
165+ route4_eth0 ( rtnl, Rtm :: Newroute , router) ?;
166+ } else {
167+ // Clean up: we're clearly too cool for IPv4
168+ route4_eth0 ( rtnl, Rtm :: Delroute , Ipv4Addr :: UNSPECIFIED ) ?;
169+ addr4_eth0 ( rtnl, Rtm :: Deladdr , Ipv4Addr :: new ( 169 , 254 , 1 , 1 ) , 16 ) ?;
170+ }
171+
172+ Ok ( ( ) )
173+ }
174+
175+ /// Wait for SLAAC to complete or fail
176+ fn wait_for_slaac ( rtnl : & NlRouter ) -> Result < ( ) > {
177+ let mut global_seen = false ;
178+ let mut global_wait = true ;
179+ let mut ll_seen = false ;
180+
181+ // Busy-netlink-loop until we see a link-local address, and a global unicast
182+ // address as long as we might expect one (see below)
183+ while !ll_seen || ( global_wait && !global_seen) {
184+ let ifaddrmsg = IfaddrmsgBuilder :: default ( )
185+ . ifa_family ( RtAddrFamily :: Inet6 )
186+ . ifa_prefixlen ( 0 ) . ifa_scope ( RtScope :: Universe ) . ifa_index ( 2 )
187+ . build ( ) ?;
188+
189+ let recv = rtnl. send ( Rtm :: Getaddr , NlmF :: ROOT ,
190+ NlPayload :: Payload ( ifaddrmsg) ) ?;
191+
192+ for response in recv {
193+ let header: Nlmsghdr < Rtm , Ifaddrmsg > = response?;
194+ if let NlPayload :: Payload ( p) = header. nl_payload ( ) {
195+ if p. ifa_scope ( ) == & RtScope :: Link {
196+ // A non-tentative link-local address implies we sent a
197+ // router solicitation that didn't get any response
198+ // (IPv4-only)? Stop waiting for the router in that case
199+ if * p. ifa_flags ( ) & IfaF :: TENTATIVE != IfaF :: TENTATIVE {
200+ global_wait = false ;
201+ }
202+
203+ ll_seen = true ;
204+ } else if p. ifa_scope ( ) == & RtScope :: Universe {
205+ global_seen = true ;
206+ }
207+ }
208+ }
209+ }
210+
211+ Ok ( ( ) )
212+ }
12213
13214pub fn configure_network ( ) -> Result < ( ) > {
14215 // Allow unprivileged users to use ping, as most distros do by default.
@@ -33,63 +234,29 @@ pub fn configure_network() -> Result<()> {
33234 sethostname ( hostname. as_bytes ( ) ) . context ( "Failed to set hostname" ) ?;
34235 }
35236
36- let dhcpcd_path = find_in_path ( "dhcpcd" ) . context ( "Failed to check existence of `dhcpcd`" ) ?;
37- let dhcpcd_path = if let Some ( dhcpcd_path) = dhcpcd_path {
38- Some ( dhcpcd_path)
39- } else {
40- find_executable ( "/sbin/dhcpcd" ) . context ( "Failed to check existence of `/sbin/dhcpcd`" ) ?
41- } ;
42- if let Some ( dhcpcd_path) = dhcpcd_path {
43- let output = Command :: new ( dhcpcd_path)
44- . args ( [ "-M" , "--nodev" , "eth0" ] )
45- . output ( )
46- . context ( "Failed to execute `dhcpcd` as child process" ) ?;
47- debug ! ( output: ?; "dhcpcd output" ) ;
48- if !output. status . success ( ) {
49- let err = if let Some ( code) = output. status . code ( ) {
50- anyhow ! ( "`dhcpcd` process exited with status code: {code}" )
51- } else {
52- anyhow ! (
53- "`dhcpcd` process terminated by signal: {}" ,
54- output
55- . status
56- . signal( )
57- . expect( "either one of status code or signal should be set" )
58- )
59- } ;
60- Err ( err) ?;
61- }
237+ let ( rtnl, _) = NlRouter :: connect ( NlFamily :: Route , None , Groups :: empty ( ) ) ?;
238+ rtnl. enable_strict_checking ( true ) ?;
62239
63- return Ok ( ( ) ) ;
240+ // Disable neighbour solicitations (dodge DAD), bring up link to start SLAAC
241+ {
242+ // IFF_NOARP | IFF_UP in one shot delays router solicitations, avoid it
243+ flags_eth0 ( & rtnl, Iff :: NOARP , Iff :: NOARP ) ?;
244+ flags_eth0 ( & rtnl, Iff :: UP , Iff :: UP ) ?;
64245 }
65246
66- let dhclient_path =
67- find_in_path ( "dhclient" ) . context ( "Failed to check existence of `dhclient`" ) ?;
68- let dhclient_path = if let Some ( dhclient_path) = dhclient_path {
69- Some ( dhclient_path)
70- } else {
71- find_executable ( "/sbin/dhclient" )
72- . context ( "Failed to check existence of `/sbin/dhclient`" ) ?
73- } ;
74- let dhclient_path =
75- dhclient_path. ok_or_else ( || anyhow ! ( "could not find required `dhcpcd` or `dhclient`" ) ) ?;
76- let output = Command :: new ( dhclient_path)
77- . output ( )
78- . context ( "Failed to execute `dhclient` as child process" ) ?;
79- debug ! ( output: ?; "dhclient output" ) ;
80- if !output. status . success ( ) {
81- let err = if let Some ( code) = output. status . code ( ) {
82- anyhow ! ( "`dhclient` process exited with status code: {code}" )
83- } else {
84- anyhow ! (
85- "`dhclient` process terminated by signal: {}" ,
86- output
87- . status
88- . signal( )
89- . expect( "either one of status code or signal should be set" )
90- )
91- } ;
92- Err ( err) ?;
247+ // Configure IPv4
248+ {
249+ do_dhcp ( & rtnl) ?;
250+ }
251+
252+ // Ensure IPv6 setup is done, if available
253+ {
254+ wait_for_slaac ( & rtnl) ?;
255+ }
256+
257+ // Re-enable neighbour solicitations and ARP requests
258+ {
259+ flags_eth0 ( & rtnl, Iff :: NOARP , Iff :: empty ( ) ) ?;
93260 }
94261
95262 Ok ( ( ) )
0 commit comments