watcher: an IPv6 address is written in brackets before its port (closes #270)
check / check (push) Successful in 3m33s

Port and certificate keys were joined as address:port, so an IPv6
address ran into its port: 2a01:4f8:2200:fd11::1:80 is also an address
on its own. Keys are now joined with net.JoinHostPort and split with
net.SplitHostPort, giving [2a01:4f8:2200:fd11::1]:80 in the state file,
/api/v1/status, the dashboard and Port Change notifications; an IPv4
address is unchanged. A certificate key still splits at its last colon,
since a hostname has none. Tests that built IPv6 keys by hand now use
the bracketed form, and a test checks that an IPv6 address's
certificate entry is removed when its port 443 closes.

Model: opus-5-5
This commit is contained in:
2026-10-13 20:07:57 +00:00
parent 6b580a5a0f
commit 2de3c4b820
6 changed files with 115 additions and 20 deletions
+12 -13
View File
@@ -6,8 +6,10 @@ import (
"fmt"
"log/slog"
"maps"
"net"
"slices"
"sort"
"strconv"
"strings"
"time"
@@ -1000,7 +1002,7 @@ func (w *Watcher) buildPortAssociations() map[string][]string {
ips := w.collectIPs(name)
for _, ip := range ips {
for _, port := range monitoredPorts {
key := fmt.Sprintf("%s:%d", ip, port)
key := net.JoinHostPort(ip, strconv.Itoa(port))
if assoc[key] == nil {
assoc[key] = make(map[string]bool)
}
@@ -1025,18 +1027,15 @@ func (w *Watcher) buildPortAssociations() map[string][]string {
return result
}
// parsePortKey splits an "ip:port" key into its components.
// parsePortKey splits an "ip:port" key, written by net.JoinHostPort,
// into its components.
func parsePortKey(key string) (string, int) {
lastColon := strings.LastIndex(key, ":")
if lastColon < 0 {
ip, port, err := net.SplitHostPort(key)
if err != nil {
return key, 0
}
ip := key[:lastColon]
var p int
_, err := fmt.Sscanf(key[lastColon+1:], "%d", &p)
p, err := strconv.Atoi(port)
if err != nil {
return ip, 0
}
@@ -1074,7 +1073,7 @@ func (w *Watcher) cleanupStaleCertificates() {
for _, key := range w.state.GetAllCertificateKeys() {
ip, hostname := parseCertKey(key)
ps, ok := w.state.GetPortState(fmt.Sprintf("%s:%d", ip, tlsPort))
ps, ok := w.state.GetPortState(net.JoinHostPort(ip, strconv.Itoa(tlsPort)))
if ok && !ps.Open {
w.state.DeleteCertificateState(key)
@@ -1189,7 +1188,7 @@ func (w *Watcher) checkSinglePort(
return
}
key := fmt.Sprintf("%s:%d", ip, port)
key := net.JoinHostPort(ip, strconv.Itoa(port))
now := time.Now().UTC()
prev, hasPrev := w.state.GetPortState(key)
@@ -1251,7 +1250,7 @@ func (w *Watcher) checkTLSForHostname(
ips := w.collectIPs(hostname)
for _, ip := range ips {
portKey := fmt.Sprintf("%s:%d", ip, tlsPort)
portKey := net.JoinHostPort(ip, strconv.Itoa(tlsPort))
ps, ok := w.state.GetPortState(portKey)
if !ok || !ps.Open {
@@ -1275,7 +1274,7 @@ func (w *Watcher) checkTLSCert(
return
}
certKey := fmt.Sprintf("%s:%d:%s", ip, tlsPort, hostname)
certKey := net.JoinHostPort(ip, strconv.Itoa(tlsPort)) + ":" + hostname
now := time.Now().UTC()
prev, hasPrev := w.state.GetCertificateState(certKey)