Skip to content

Commit d99bfc9

Browse files
committed
Poll for address of Supervisor default interface for mDNS URL
Outstanding comment mentions the hacky way for obtaining the outbound IP address should be replaced by API from home-assistant/supervisor#2115. Poll this API periodically with 5s retry interval in case the IP can't be determined or parsed from the response. This still ignores IPv6-only deployment of HA, however, in that case the landing page should still be reachable, just not discoverable through mDNS (combined with #164).
1 parent 9248f93 commit d99bfc9

File tree

3 files changed

+102
-18
lines changed

3 files changed

+102
-18
lines changed

http.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,7 @@ func httpSupervisorProxy(w http.ResponseWriter, r *http.Request) {
7070
log.Printf("Proxy request: %s", r.URL.Path)
7171

7272
// Base Supervisor URL
73-
supervisorHost := "supervisor"
74-
75-
if development && os.Getenv("SUPERVISOR_HOST") != "" {
76-
supervisorHost = os.Getenv("SUPERVISOR_HOST")
77-
}
73+
supervisorHost := getSupervisorHost()
7874

7975
u, err := url.Parse("http://" + supervisorHost + "/")
8076
if err != nil {

main.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,16 @@ var mdns *zeroconf.Server
1414
var wwwRoot string
1515
var development bool
1616

17+
func getSupervisorHost() string {
18+
supervisorHost := "supervisor"
19+
20+
if development && os.Getenv("SUPERVISOR_HOST") != "" {
21+
supervisorHost = os.Getenv("SUPERVISOR_HOST")
22+
}
23+
24+
return supervisorHost
25+
}
26+
1727
func main() {
1828
development = (os.Getenv("DEVELOPMENT") == "True")
1929

mdns.go

Lines changed: 91 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,41 @@
11
package main
22

33
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
47
"log"
58
"net"
9+
"net/http"
10+
"os"
11+
"time"
612

713
"github.com/grandcat/zeroconf"
814
"github.com/rs/xid"
915
)
1016

17+
type Response struct {
18+
Result string `json:"result"`
19+
Message string `json:"message,omitempty"`
20+
Data map[string]any `json:"data,omitempty"`
21+
}
22+
1123
func publishHomeAssistant() {
1224
var err error
1325

26+
var outboundIP net.IP
27+
28+
for {
29+
outboundIP, err = getOutboundIP()
30+
if outboundIP != nil {
31+
break
32+
}
33+
log.Printf("Failed to get outbound IP, retrying in 5s: %s", err)
34+
time.Sleep(5 * time.Second)
35+
}
36+
1437
unique := xid.New()
15-
hostURL := "http://" + getOutboundIP().String() + ":8123"
38+
hostURL := "http://" + outboundIP.String() + ":8123"
1639
params := []string{
1740
"location_name=Home Assistant",
1841
"uuid=",
@@ -31,21 +54,76 @@ func publishHomeAssistant() {
3154
}
3255
}
3356

34-
// Get preferred outbound ip of this machine
35-
// https://stackoverflow.com/a/37382208
36-
// Can be removed after Supervisor v248 with new lookup
37-
func getOutboundIP() net.IP {
38-
conn, err := net.Dial("udp", "8.8.8.8:80")
57+
// Get the first IPv4 address from the interface information
58+
func getFirstIPv4Address(iface map[string]any) (net.IP, error) {
59+
ipv4Data, ok := iface["ipv4"].(map[string]any)
60+
if !ok {
61+
return nil, fmt.Errorf("no ipv4 addresses found")
62+
}
63+
64+
addresses, ok := ipv4Data["address"].([]any)
65+
if !ok || len(addresses) == 0 {
66+
return nil, fmt.Errorf("empty ipv4 address list")
67+
}
68+
69+
addr, ok := addresses[0].(string)
70+
if !ok {
71+
return nil, fmt.Errorf("can't parse first ipv4 address")
72+
}
73+
74+
ip, _, err := net.ParseCIDR(addr)
75+
3976
if err != nil {
40-
log.Fatal(err)
77+
return nil, fmt.Errorf("invalid IPv4 address: %s", err)
4178
}
42-
defer func() {
43-
if err := conn.Close(); err != nil {
44-
log.Printf("error closing mdns connection: %v", err)
79+
80+
return ip, nil
81+
}
82+
83+
// Get IP address of the default interface from Supervisor
84+
func getOutboundIP() (net.IP, error) {
85+
supervisorHost := getSupervisorHost()
86+
87+
client := &http.Client{}
88+
req, err := http.NewRequest("GET", "http://"+supervisorHost+"/network/interface/default/info", nil)
89+
90+
if err != nil {
91+
return nil, fmt.Errorf("can't create request to Supervisor: %s", err)
92+
}
93+
94+
req.Header.Add("Authorization", "Bearer "+os.Getenv("SUPERVISOR_TOKEN"))
95+
96+
response, err := client.Do(req)
97+
if err == nil {
98+
defer response.Body.Close()
99+
}
100+
101+
if err != nil || response.StatusCode >= 300 {
102+
var errorMsg string
103+
104+
if err == nil {
105+
if data, err := io.ReadAll(response.Body); err == nil {
106+
errorMsg = string(data)
107+
} else {
108+
errorMsg = err.Error()
109+
}
110+
} else {
111+
errorMsg = err.Error()
45112
}
46-
}()
47113

48-
localAddr := conn.LocalAddr().(*net.UDPAddr)
114+
return nil, fmt.Errorf("can't get default interface from Supervisor: %s", errorMsg)
115+
}
116+
117+
var responseData Response
118+
119+
if err := json.NewDecoder(response.Body).Decode(&responseData); err != nil {
120+
return nil, fmt.Errorf("can't parse default interface data: %s", err)
121+
}
122+
123+
ipv4Addr, err := getFirstIPv4Address(responseData.Data)
124+
if err != nil {
125+
return nil, err
126+
}
49127

50-
return localAddr.IP
128+
return ipv4Addr, nil
51129
}

0 commit comments

Comments
 (0)