Repository: home-assistant/core
Pull request: Fix network starting with no configured IPv4 addresses
<!--
  You are amazing! Thanks for contributing to our project!
  Please, DO NOT DELETE ANY TEXT from this template! (unless instructed).
-->

## Proposed change
A previous fix for a bug that happens when no IPv4 addresses are available on the system added an explicit error when this happens. However, Home Assistant can run just fine in a suitably configured IPv6-only environment, and this error can also happen if homeassistant is started before the system IP configuration has finished. Fix the error by falling back to the loopback address if async_get_source_ip() is called without an explicit target IP argument, as suggested by @bdraco.



## Type of change
<!--
  What type of change does your PR introduce to Home Assistant?
  NOTE: Please, check only 1! box!
  If your PR requires multiple boxes to be checked, you'll most likely need to
  split it into multiple PRs. This makes things easier and faster to code review.
-->

- [ ] Dependency upgrade
- [x] Bugfix (non-breaking change which fixes an issue)
- [ ] New integration (thank you!)
- [ ] New feature (which adds functionality to an existing integration)
- [ ] Breaking change (fix/feature causing existing functionality to break)
- [ ] Code quality improvements to existing code or addition of tests

## Additional information
<!--
  Details are important, and help maintainers processing your PR.
  Please be sure to fill out additional details, if applicable.
-->

- This PR fixes or closes issue: fixes #61635
- This PR is related to issue: #63416
- Link to documentation pull request: 

## Checklist
<!--
  Put an `x` in the boxes that apply. You can also fill these out after
  creating the PR. If you're unsure about any of them, don't hesitate to ask.
  We're here to help! This is simply a reminder of what we are going to look
  for before merging your code.
-->

- [x] The code change is tested and works locally.
- [x] Local tests pass. **Your PR cannot be merged unless tests pass**
- [x] There is no commented out code in this PR.
- [x] I have followed the [development checklist][dev-checklist]
- [x] The code has been formatted using Black (`black --fast homeassistant tests`)
- [x] Tests have been added to verify that the new code works.

If user exposed functionality or configuration variables are added/changed:

- [ ] Documentation added/updated for [www.home-assistant.io][docs-repository]

If the code communicates with devices, web services, or third-party tools:

- [ ] The [manifest file][manifest-docs] has all fields filled out correctly.  
      Updated and included derived files by running: `python3 -m script.hassfest`.
- [ ] New or updated dependencies have been added to `requirements_all.txt`.  
      Updated by running `python3 -m script.gen_requirements_all`.
- [ ] For the updated dependencies - a link to the changelog, or at minimum a diff between library versions is added to the PR description.
- [ ] Untested files have been added to `.coveragerc`.

The integration reached or maintains the following [Integration Quality Scale][quality-scale]:
<!--
  The Integration Quality Scale scores an integration on the code quality
  and user experience. Each level of the quality scale consists of a list
  of requirements. We highly recommend getting your integration scored!
-->

- [ ] No score or internal
- [ ] 🥈 Silver
- [ ] 🥇 Gold
- [ ] 🏆 Platinum

<!--
  This project is very active and we have a high turnover of pull requests.

  Unfortunately, the number of incoming pull requests is higher than what our
  reviewers can review and merge so there is a long backlog of pull requests
  waiting for review. You can help here!
  
  By reviewing another pull request, you will help raise the code quality of
  that pull request and the final review will be faster. This way the general
  pace of pull request reviews will go up and your wait time will go down.
  
  When picking a pull request to review, try to choose one that hasn't yet
  been reviewed.

  Thanks for helping out!
-->

To help with the load of incoming pull requests:

- [ ] I have reviewed two other [open pull requests][prs] in this repository.

[prs]: https://github.com/home-assistant/core/pulls?q=is%3Aopen+is%3Apr+-author%3A%40me+-draft%3Atrue+-label%3Awaiting-for-upstream+sort%3Acreated-desc+review%3Anone+-status%3Afailure

<!--
  Thank you for contributing <3

  Below, some useful links you could explore:
-->
[dev-checklist]: https://developers.home-assistant.io/docs/en/development_checklist.html
[manifest-docs]: https://developers.home-assistant.io/docs/en/creating_integration_manifest.html
[quality-scale]: https://developers.home-assistant.io/docs/en/next/integration_quality_scale_index.html
[docs-repository]: https://github.com/home-assistant/home-assistant.io


Problem statement:
Fails to start on ipv6 only host
### The problem

Homeassistant is being run as a container on kubernetes w/ an ipv6 only network stack. The http server startup is failing due to not having any ipv4 addresses. This is seen on first startup, with no custom configurations or integrations enabled.

### What version of Home Assistant Core has the issue?

2021.12.0

### What was the last working version of Home Assistant Core?

_No response_

### What type of installation are you running?

Home Assistant Container

### Integration causing the issue

_No response_

### Link to integration documentation on our website

_No response_

### Example YAML snippet

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: homeassistant-deploy
  labels:
    app: homeassistant
spec:
  replicas: 1
  selector:
    matchLabels:
      app: homeassistant
  template:
    metadata:
      labels:
        app: homeassistant
    spec:
      containers:
      - name: homeassistant
        image: ghcr.io/home-assistant/home-assistant:stable
        volumeMounts:
        - mountPath: "/config"
          name: cephfs-config
        env:
          - name: TZ
            value: "America/Detroit"
      volumes:
      - name: cephfs-config
        hostPath:
          path: redacted
```


### Anything in the logs that might be useful for us?

```txt
[s6-init] making user provided files available at /var/run/s6/etc...exited 0.
[s6-init] ensuring user provided files have correct perms...exited 0.
[fix-attrs.d] applying ownership & permissions fixes...
[fix-attrs.d] done.
[cont-init.d] executing container initialization scripts...
[cont-init.d] done.
[services.d] starting services
[services.d] done.
2021-12-12 18:44:36 ERROR (MainThread) [homeassistant.setup] Error during setup of component http
Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/setup.py", line 229, in _async_setup_component
    result = await task
  File "/usr/src/homeassistant/homeassistant/components/http/__init__.py", line 192, in async_setup
    local_ip = await async_get_source_ip(hass)
  File "/usr/src/homeassistant/homeassistant/components/network/__init__.py", line 35, in async_get_source_ip
    return source_ip if source_ip in all_ipv4s else all_ipv4s[0]
IndexError: list index out of range
2021-12-12 18:44:36 ERROR (MainThread) [homeassistant.setup] Unable to set up dependencies of system_log. Setup failed for dependencies: http
2021-12-12 18:44:36 ERROR (MainThread) [homeassistant.setup] Setup failed for system_log: Could not set up all dependencies.
2021-12-12 18:44:37 ERROR (MainThread) [homeassistant.setup] Unable to set up dependencies of websocket_api. Setup failed for dependencies: http
2021-12-12 18:44:37 ERROR (MainThread) [homeassistant.setup] Setup failed for websocket_api: Could not set up all dependencies.
```


### Additional information

_No response_

Hints from issue discussion:
[http documentation](https://www.home-assistant.io/integrations/http)
[http source](https://github.com/home-assistant/core/tree/dev/homeassistant/components/http)
<sub><sup>(message by IssueLinks)</sup></sub>
Hey there @home-assistant/core, mind taking a look at this issue as it has been labeled with an integration (`http`) you are listed as a [code owner](https://github.com/home-assistant/core/blob/dev/CODEOWNERS#L397) for? Thanks!
<sub><sup>(message by CodeOwnersMention)</sup></sub>
I am not sure home assistant is designed to work on IPv6 only stack, many integrations will also fail
@thecode It worked just fine on earlier versions. It broke for me when upgrading from 2021.9.3 to 2022.2.6 today. The immediate culprit is https://github.com/home-assistant/core/pull/63416 which added an explicit refusal to start if it can't find an IPv4 address. I think it might have worked before because the `all_ipvs` array happened to contain `None` and so the function returned `None`, which is totally fine.

And yeah, some integrations may break, but with a properly configured NAT64 gateway it's quite possible to set things up so they continue to work. Explicitly refusing to run in this case seems to be the wrong solution; I'm not really sure why the PR above both prints a warning and raises and exception; the warning ought to be good enough...

I'm running with the patch below, and that works fine :)

```patch
diff --git a/homeassistant/components/network/__init__.py b/homeassistant/components/network/__init__.py
index b3ef88e7ab27..b822978ce188 100644
--- a/homeassistant/components/network/__init__.py
+++ b/homeassistant/components/network/__init__.py
@@ -40,10 +40,6 @@ async def async_get_source_ip(
         _LOGGER.warning(
             "Because the system does not have any enabled IPv4 addresses, source address detection may be inaccurate"
         )
-        if source_ip is None:
-            raise HomeAssistantError(
-                "Could not determine source ip because the system does not have any enabled IPv4 addresses and creating a socket failed"
-            )
         return source_ip
 
     return source_ip if source_ip in all_ipv4s else all_ipv4s[0]
```

Commit message: network: fix starting with no configured IPv4 addresses

A previous fix for a bug that happens when no IPv4 addresses are available
on the system added an explicit error when this happens. However, Home
Assistant can run just fine in a suitably configured IPv6-only environment,
and this error can also happen if homeassistant is started before the
system IP configuration has finished. Fix the error by falling back to the
loopback address if async_get_source_ip() is called without an explicit
target IP argument, as suggested by @bdraco.

Fixes #61635.

Signed-off-by: Toke Høiland-Jørgensen <toke@toke.dk>

Patch to review:
diff --git a/homeassistant/components/network/__init__.py b/homeassistant/components/network/__init__.py
index 8c1a0020cfb4ff..1ad975e1d85d02 100644
--- a/homeassistant/components/network/__init__.py
+++ b/homeassistant/components/network/__init__.py
@@ -6,11 +6,16 @@
 
 from homeassistant.core import HomeAssistant, callback
 from homeassistant.exceptions import HomeAssistantError
-from homeassistant.helpers.typing import ConfigType
+from homeassistant.helpers.typing import UNDEFINED, ConfigType, UndefinedType
 from homeassistant.loader import bind_hass
 
 from . import util
-from .const import IPV4_BROADCAST_ADDR, PUBLIC_TARGET_IP
+from .const import (
+    IPV4_BROADCAST_ADDR,
+    LOOPBACK_TARGET_IP,
+    MDNS_TARGET_IP,
+    PUBLIC_TARGET_IP,
+)
 from .models import Adapter
 from .network import Network, async_get_network
 
@@ -26,7 +31,7 @@ async def async_get_adapters(hass: HomeAssistant) -> list[Adapter]:
 
 @bind_hass
 async def async_get_source_ip(
-    hass: HomeAssistant, target_ip: str = PUBLIC_TARGET_IP
+    hass: HomeAssistant, target_ip: str | UndefinedType = UNDEFINED
 ) -> str:
     """Get the source ip for a target ip."""
     adapters = await async_get_adapters(hass)
@@ -35,7 +40,15 @@ async def async_get_source_ip(
         if adapter["enabled"] and (ipv4s := adapter["ipv4"]):
             all_ipv4s.extend([ipv4["address"] for ipv4 in ipv4s])
 
-    source_ip = util.async_get_source_ip(target_ip)
+    if target_ip is UNDEFINED:
+        source_ip = (
+            util.async_get_source_ip(PUBLIC_TARGET_IP)
+            or util.async_get_source_ip(MDNS_TARGET_IP)
+            or util.async_get_source_ip(LOOPBACK_TARGET_IP)
+        )
+    else:
+        source_ip = util.async_get_source_ip(target_ip)
+
     if not all_ipv4s:
         _LOGGER.warning(
             "Because the system does not have any enabled IPv4 addresses, source address detection may be inaccurate"
diff --git a/homeassistant/components/network/const.py b/homeassistant/components/network/const.py
index 07c12e63a10c5d..3a166189b85c41 100644
--- a/homeassistant/components/network/const.py
+++ b/homeassistant/components/network/const.py
@@ -17,6 +17,7 @@
 ATTR_CONFIGURED_ADAPTERS: Final = "configured_adapters"
 DEFAULT_CONFIGURED_ADAPTERS: list[str] = []
 
+LOOPBACK_TARGET_IP: Final = "127.0.0.1"
 MDNS_TARGET_IP: Final = "224.0.0.251"
 PUBLIC_TARGET_IP: Final = "8.8.8.8"
 IPV4_BROADCAST_ADDR: Final = "255.255.255.255"
diff --git a/tests/components/network/test_init.py b/tests/components/network/test_init.py
index 1103c6fa85021e..97dd46b532569e 100644
--- a/tests/components/network/test_init.py
+++ b/tests/components/network/test_init.py
@@ -27,6 +27,21 @@ def _mock_socket(sockname):
     return mock_socket
 
 
+def _mock_cond_socket(sockname):
+    class CondMockSock(MagicMock):
+        def connect(self, addr):
+            """Mock connect that stores addr."""
+            self._addr = addr[0]
+
+        def getsockname(self):
+            """Return addr iif it matches the mock sockname."""
+            if self._addr == sockname:
+                return [sockname]
+            raise AttributeError()
+
+    return CondMockSock()
+
+
 def _mock_socket_exception(exc):
     mock_socket = MagicMock()
     mock_socket.getsockname = Mock(side_effect=exc)
@@ -650,3 +665,24 @@ async def test_async_get_source_ip_cannot_be_determined_and_no_enabled_addresses
         await hass.async_block_till_done()
         with pytest.raises(HomeAssistantError):
             await network.async_get_source_ip(hass, MDNS_TARGET_IP)
+
+
+async def test_async_get_source_ip_no_ip_loopback(hass, hass_storage, caplog):
+    """Test getting the source ip address when all adapters are disabled no target is specified."""
+    hass_storage[STORAGE_KEY] = {
+        "version": STORAGE_VERSION,
+        "key": STORAGE_KEY,
+        "data": {ATTR_CONFIGURED_ADAPTERS: ["eth1"]},
+    }
+
+    with patch(
+        "homeassistant.components.network.util.ifaddr.get_adapters",
+        return_value=[],
+    ), patch(
+        "homeassistant.components.network.util.socket.socket",
+        return_value=_mock_cond_socket(_LOOPBACK_IPADDR),
+    ):
+        assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
+        await hass.async_block_till_done()
+
+        assert await network.async_get_source_ip(hass) == "127.0.0.1"


Code review comments:
diff_hunk: @@ -27,6 +27,21 @@ def _mock_socket(sockname):
     return mock_socket
 
 
+def _mock_cond_socket(sockname):
+    class CondMockSock(MagicMock):
+        def connect(self, addr):
+            """Mock connect that stores addr."""
+            self._addr = addr[0]
+
+        def getsockname(self):
+            """Return addr iif it matches the mock sockname."""
path: tests/components/network/test_init.py
text: @user1:
```suggestion
            """Return addr if it matches the mock sockname."""
```