<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Networking on Lessons learned</title><link>https://eghuro.cz/tags/networking/</link><description>Recent content in Networking on Lessons learned</description><generator>Hugo</generator><language>en-us</language><copyright>© Alexandr Mansurov · Licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/)</copyright><lastBuildDate>Sat, 17 Oct 2026 22:30:00 +0200</lastBuildDate><atom:link href="https://eghuro.cz/tags/networking/index.xml" rel="self" type="application/rss+xml"/><item><title>"No route to host" only for the AI agent: macOS Local Network privacy</title><link>https://eghuro.cz/posts/macos-local-network-privacy-agent-no-route-to-host/</link><pubDate>Sat, 17 Oct 2026 22:30:00 +0200</pubDate><guid>https://eghuro.cz/posts/macos-local-network-privacy-agent-no-route-to-host/</guid><description>&lt;p&gt;My coding agent couldn&amp;rsquo;t reach anything on my home network:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nc: connectx to 192.168.0.2 port 22 (tcp) failed: No route to host&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ping: sendto: No route to host&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The router answered, and &lt;code&gt;arp -an&lt;/code&gt; listed the server&amp;rsquo;s MAC address. In my own Terminal, the same &lt;code&gt;ssh&lt;/code&gt; command connected. If you see this on a Mac and only your agent sees it, your network is fine. macOS is blocking the app the agent runs in because that app doesn&amp;rsquo;t have the Local Network permission.&lt;/p&gt;&#10;&#10;&lt;h2 id="the-setup"&gt;The setup &lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;macOS 27 on Wi-Fi&lt;/li&gt;&#10;&lt;li&gt;Claude Code in its VS Code extension, so every command the agent runs is a child process of VS Code&lt;/li&gt;&#10;&lt;li&gt;Targets on the Mac&amp;rsquo;s own subnet: a Linux server, a NAS, a second router&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&#10;&lt;h2 id="what-went-wrong"&gt;What went wrong &lt;/h2&gt;&#10;&lt;p&gt;The agent was helping me with a server that really was down (&lt;a&#10; href="https://eghuro.cz/posts/systemd-run-rollback-killed-dhclient"&#10; &#10; data-rel="prefetch"&#10; &#10; &#10;&gt;that story is here&lt;/a&gt;&#10;). It probed the subnet from the Mac, found that only the router answered, and reported with confidence that &amp;ldquo;the Mac reaching only [the router] looks like client isolation on the Wi-Fi. That&amp;rsquo;s usually a guest network.&amp;rdquo; It told me to join a different network.&lt;/p&gt;&#10;&lt;p&gt;I wasn&amp;rsquo;t on a guest network, and I said so. The agent looked again and noticed what rules the network out: &lt;code&gt;ping&lt;/code&gt; itself failed with &lt;code&gt;sendto: No route to host&lt;/code&gt;. That message comes from the local &lt;code&gt;sendto()&lt;/code&gt; call, so the Mac refused to send the packet at all. Client isolation drops packets in the access point, and then &lt;code&gt;ping&lt;/code&gt; reports &lt;code&gt;Request timeout&lt;/code&gt;, not a send error. The target&amp;rsquo;s MAC was already in the ARP table, so the kernel knew exactly where to send it.&lt;/p&gt;&#10;&lt;p&gt;So the block was at the MAC, and every TCP test the agent ran from there was worthless.&lt;/p&gt;&#10;&lt;p&gt;Later that evening, in a new session, it happened again with a different host on the same subnet. This time the agent suspected its own sandbox and retried with the sandbox off. Same error. What settled it was my reply: the same &lt;code&gt;ssh&lt;/code&gt; command works in my Terminal.&lt;/p&gt;&#10;&#10;&lt;h2 id="fix"&gt;Fix &lt;/h2&gt;&#10;&lt;p&gt;Open System Settings → Privacy &amp;amp; Security → Local Network and turn on Visual Studio Code (or whichever app hosts your agent). The agent&amp;rsquo;s next &lt;code&gt;ssh&lt;/code&gt; went through. If yours still fails, restart the app.&lt;/p&gt;&#10;&lt;p&gt;The app can be missing from the list if macOS never showed it the permission prompt. Apple&amp;rsquo;s &lt;a&#10; href="https://developer.apple.com/documentation/technotes/tn3179-understanding-local-network-privacy"&#10; &#10; target="_blank" rel="noopener"&#10; &#10; &#10;&gt;TN3179&lt;/a&gt;&#10; notes that the prompt doesn&amp;rsquo;t appear when a very short-lived process makes the request, and most agent commands exit right after they fail. Since macOS 27.2, the list includes a + button to add an app yourself.&lt;/p&gt;&#10;&#10;&lt;h2 id="why-only-the-agent"&gt;Why only the agent &lt;/h2&gt;&#10;&lt;p&gt;Local Network privacy covers traffic to addresses on a Wi-Fi or Ethernet network that goes directly to the host, not through a router. TN3179 lists who gets access without asking:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;daemons started by &lt;code&gt;launchd&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;programs running as root&lt;/li&gt;&#10;&lt;li&gt;&amp;ldquo;command-line tools run from Terminal or over SSH, including any child processes they spawn&amp;rdquo;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Everything else is charged to its &amp;ldquo;responsible code&amp;rdquo;. If an app spawns a helper and the helper connects to a local address, macOS checks the app&amp;rsquo;s permission. The agent&amp;rsquo;s shell is a child of VS Code, so &lt;code&gt;ssh&lt;/code&gt;, &lt;code&gt;nc&lt;/code&gt;, and &lt;code&gt;ping&lt;/code&gt; run there under VS Code&amp;rsquo;s permission. The same binary, run by the same user from Terminal, is allowed.&lt;/p&gt;&#10;&lt;p&gt;That explains the other half of the symptom. Internet traffic goes through the router, so it isn&amp;rsquo;t local and works fine. TN3179 also exempts the device&amp;rsquo;s DNS server when it sits on the local network, and a home router is usually the DNS server it hands out by DHCP. That is probably why the router was the one host still reachable, and it is exactly how client isolation looks from the inside.&lt;/p&gt;&#10;&lt;p&gt;The same trap waits for any agent or tool that an app such as an IDE starts on macOS.&lt;/p&gt;&#10;&#10;&lt;h2 id="check-before-you-blame-the-network"&gt;Check before you blame the network &lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Run &lt;code&gt;ping&lt;/code&gt; from the agent&amp;rsquo;s shell. &lt;code&gt;sendto: No route to host&lt;/code&gt; for a host on your subnet whose MAC is in &lt;code&gt;arp -an&lt;/code&gt; means the Mac refused to send it.&lt;/li&gt;&#10;&lt;li&gt;Run the same command in Terminal. If it works there, check the app&amp;rsquo;s Local Network permission.&lt;/li&gt;&#10;&lt;li&gt;If it fails in Terminal too, the cause is the network or the routing table. Later that same evening, another VPN came up as &lt;code&gt;utun6&lt;/code&gt; and took the default route, which cut the whole Mac off from the subnet. This time the agent caught it in &lt;code&gt;netstat -rn -f inet&lt;/code&gt;.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&#10;&lt;h2 id="takeaway"&gt;Takeaway &lt;/h2&gt;&#10;&lt;p&gt;On macOS, an app without the Local Network permission gets &lt;code&gt;No route to host&lt;/code&gt; for hosts on your subnet, and so does every tool it spawns. An agent running inside that app will diagnose a working network as broken.&lt;/p&gt;&#10;&lt;p&gt;If your agent runs inside an app on macOS, open Privacy &amp;amp; Security → Local Network now and check that the app is on the list and switched on.&lt;/p&gt;&#10;</description></item><item><title>The rollback that saved the host, and killed it 24 hours later</title><link>https://eghuro.cz/posts/systemd-run-rollback-killed-dhclient/</link><pubDate>Sat, 17 Oct 2026 21:51:00 +0200</pubDate><guid>https://eghuro.cz/posts/systemd-run-rollback-killed-dhclient/</guid><description>&lt;p&gt;The rollback timer fired, the remote host got its address back, and the journal said all was well:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;dhclient[1796120]: bound to 192.168.0.2 -- renewal in 34318 seconds.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemd[1]: lxc-uplink-rollback.service: Deactivated successfully.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Exactly 24 hours later, to the second, the host dropped off the network and took SSH, the port forwards, and the VPN with it.&lt;/p&gt;&#10;&lt;p&gt;When a transient &lt;code&gt;systemd-run&lt;/code&gt; unit finishes, systemd kills everything its command left running. If your rollback, or any other command run that way, starts a daemon, the daemon dies with the unit. Here it was &lt;code&gt;dhclient&lt;/code&gt;, and the damage only showed when the DHCP lease expired.&lt;/p&gt;&#10;&#10;&lt;h2 id="the-setup"&gt;The setup &lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Debian 13, systemd 257, ifupdown with ISC dhclient 4.4.3-P1&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;allow-hotplug enp0s25&lt;/code&gt; / &lt;code&gt;iface enp0s25 inet dhcp&lt;/code&gt;, with a 24-hour lease from the router&lt;/li&gt;&#10;&lt;li&gt;The only way in is SSH through a port forward to the host&amp;rsquo;s address; the console is days away&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&#10;&lt;h2 id="the-dead-mans-switch-worked"&gt;The dead man&amp;rsquo;s switch worked &lt;/h2&gt;&#10;&lt;p&gt;Before a risky network change, an Ansible play arms a timer that undoes it, applies the change, and disarms the timer only if a fresh SSH login succeeds:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Arm rollback timer&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ansible.builtin.command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;gt;-&lt;/span&gt;&lt;span class="sd"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; systemd-run --collect --unit=lxc-uplink-rollback --on-active=5min&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; /bin/sh -c &amp;#39;ifdown -a --exclude=lo; ip link del br0;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; cp /etc/network/interfaces.rollback /etc/network/interfaces;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; ifup -a; ifup -a --allow=hotplug&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# ... apply the change ...&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Drop the cached SSH connection&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ansible.builtin.meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;reset_connection&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Prove a fresh SSH login still works&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ansible.builtin.wait_for_connection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;420&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Disarm rollback timer&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ansible.builtin.command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;systemctl stop lxc-uplink-rollback.timer&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Without &lt;code&gt;meta: reset_connection&lt;/code&gt;, Ansible reuses its open SSH connection and a broken change looks fine.&lt;/p&gt;&#10;&lt;p&gt;The change moved the NIC into a bridge, &lt;code&gt;br0&lt;/code&gt;. The bridge got the old address back, but forwarded SSH from the internet stopped reaching the host. (I never found out why, and it isn&amp;rsquo;t the point here.) Five minutes later the timer restored the old config, the host came back, and I moved on.&lt;/p&gt;&#10;&#10;&lt;h2 id="what-went-wrong"&gt;What went wrong &lt;/h2&gt;&#10;&lt;p&gt;Look at the rollback unit again. Its main process is &lt;code&gt;/bin/sh&lt;/code&gt;. &lt;code&gt;ifup&lt;/code&gt; starts &lt;code&gt;dhclient&lt;/code&gt;, which forks into the background, and then &lt;code&gt;sh&lt;/code&gt; exits. The unit is now done, and a finished service gets its cgroup cleaned up: by default (&lt;code&gt;KillMode=control-group&lt;/code&gt;), systemd kills every process still in it. That includes the &lt;code&gt;dhclient&lt;/code&gt; that had just been &amp;ldquo;bound&amp;rdquo;.&lt;/p&gt;&#10;&lt;p&gt;A killed &lt;code&gt;dhclient&lt;/code&gt; doesn&amp;rsquo;t release the lease, so the address stays on the interface with a lifetime of 86400 seconds. Nothing renews it. When the lifetime runs out, the kernel removes the address, and avahi is the first to notice:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Oct 17 21:08:42 avahi-daemon[936]: Withdrawing address record for 192.168.0.2 on enp0s25.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The clue was in the journal on day one. When the rollback started, its &lt;code&gt;dhclient&lt;/code&gt; logged:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;dhclient[1796009]: Removed stale PID file&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That stale PID file belonged to the &lt;code&gt;dhclient&lt;/code&gt; on &lt;code&gt;br0&lt;/code&gt;. Ansible had started it five minutes earlier from another &lt;code&gt;systemd-run&lt;/code&gt; unit, and systemd had killed it the same way, but the rollback replaced it before anyone noticed.&lt;/p&gt;&#10;&lt;p&gt;At boot, &lt;code&gt;ifup@enp0s25.service&lt;/code&gt; starts the same &lt;code&gt;dhclient&lt;/code&gt;. That unit stays &lt;code&gt;active (exited)&lt;/code&gt;, so its cgroup is never cleaned up, which is how the boot-time &lt;code&gt;dhclient&lt;/code&gt; had kept renewing for about six weeks.&lt;/p&gt;&#10;&lt;p&gt;You can see the behavior in two lines:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemd-run --unit&lt;span class="o"&gt;=&lt;/span&gt;demo /bin/sh -c &lt;span class="s1"&gt;&amp;#39;sleep 600 &amp;amp;&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sleep 1&lt;span class="p"&gt;;&lt;/span&gt; pgrep -a sleep &lt;span class="c1"&gt;# nothing: systemd killed it when sh exited&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#10;&lt;h2 id="the-fix"&gt;The fix &lt;/h2&gt;&#10;&lt;p&gt;To get back in, I went through a VM that had its own macvlan leg on the same NIC and ran &lt;code&gt;sudo dhclient enp0s25&lt;/code&gt;. With &lt;code&gt;ssh -J&lt;/code&gt;, &lt;code&gt;-i&lt;/code&gt; applies only to the last hop, so give the jump host its key through &lt;code&gt;ProxyCommand&lt;/code&gt;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ssh -i ~/.ssh/key -o &lt;span class="nv"&gt;IdentitiesOnly&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;yes &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -o &lt;span class="nv"&gt;ProxyCommand&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;ssh -i ~/.ssh/key -o IdentitiesOnly=yes -W %h:%p user@jump.example.com&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; user@target.example.com&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The real fix was to stop depending on a long-running DHCP client. The router reserves the address for the host anyway, so the host now has a static stanza. Ansible applies it without restarting networking, because that would cut off the run and the only way in:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ip address replace 192.168.0.2/24 dev enp0s25 valid_lft forever preferred_lft forever&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ip route replace default via 192.168.0.1 dev enp0s25&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pkill --signal KILL -f &lt;span class="s1"&gt;&amp;#39;(^|/)dhclient .*enp0s25$&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;SIGKILL&lt;/code&gt; is deliberate: a &lt;code&gt;dhclient&lt;/code&gt; that stops cleanly removes the address it holds, the one this very run is connected through.&lt;/p&gt;&#10;&lt;p&gt;If the host has to stay on DHCP, keep daemons out of transient units. Start the daemon through the unit that owns it at boot (here &lt;code&gt;ifup@enp0s25.service&lt;/code&gt;), or let the transient unit leave its children alone with &lt;code&gt;systemd-run -p KillMode=process&lt;/code&gt;. I didn&amp;rsquo;t retry the bridge, so I haven&amp;rsquo;t tested either variant in this rollback.&lt;/p&gt;&#10;&lt;p&gt;To check which unit a daemon really belongs to:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat /proc/&lt;span class="k"&gt;$(&lt;/span&gt;pgrep -o dhclient&lt;span class="k"&gt;)&lt;/span&gt;/cgroup&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#10;&lt;h2 id="takeaway"&gt;Takeaway &lt;/h2&gt;&#10;&lt;p&gt;&lt;code&gt;Deactivated successfully&lt;/code&gt; only says the unit finished; whatever it started may have died with it. A killed DHCP client fails a whole lease later, long after you have stopped looking.&lt;/p&gt;&#10;&lt;p&gt;Search your playbooks and scripts for &lt;code&gt;systemd-run&lt;/code&gt; calls that run &lt;code&gt;ifup&lt;/code&gt;, &lt;code&gt;dhclient&lt;/code&gt; or anything else that forks into the background. Then check which cgroup that process ends up in.&lt;/p&gt;&#10;</description></item></channel></rss>