2 Commits
Author SHA1 Message Date
VeillaxandClaude Sonnet 5.5 ad56fa4e3a A website on every address steps aside for websites that name an address on the same port
CI / test (push) Successful in 1m8s
Release / release (push) Successful in 3m53s
The wildcard then listens on the machine's other addresses only, in the built-in proxy and in Traefik,
and follows the machine when it gains or loses an address.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 22:48:13 -06:00
VeillaxandClaude Sonnet 5.5 246b08c090 Report update progress and log to Hammerhead while updating
CI / test (push) Successful in 2m59s
Release / release (push) Successful in 6m42s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 17:58:34 -06:00
7 changed files with 222 additions and 33 deletions

No files matched your search

+131 -14
View File
@@ -43,7 +43,7 @@ type Config struct {
// Hammerhead, by calling UpdateNow (which normally restarts the process into the new binary). // Hammerhead, by calling UpdateNow (which normally restarts the process into the new binary).
Version string Version string
AutoUpdate bool AutoUpdate bool
UpdateNow func(ctx context.Context) (newVersion string, err error) UpdateNow func(ctx context.Context, progress func(stage string, pct int, line string)) (newVersion string, err error)
// Traefik, when its Path is set, serves http/https resources instead of the built-in proxy. // Traefik, when its Path is set, serves http/https resources instead of the built-in proxy.
Traefik TraefikConfig Traefik TraefikConfig
@@ -114,8 +114,13 @@ type Blacktip struct {
logq chan model.LogEntry logq chan model.LogEntry
reportMu sync.Mutex reportMu sync.Mutex
updating atomic.Bool updating atomic.Bool
addrSig string // machine addresses a wildcard website was spread across, "" when none was
updateMsg string // outcome of the update Hammerhead asked for, reported back updateMsg string // outcome of the update Hammerhead asked for, reported back
updateID string // the request that updateMsg answers updateID string // the request that updateMsg answers
updStage string // progress of the running update, reported to Hammerhead
updPct int
updLog []string
updSent time.Time
dockerMu sync.Mutex dockerMu sync.Mutex
dockerAt time.Time dockerAt time.Time
dockerList []model.Container dockerList []model.Container
@@ -243,6 +248,50 @@ func (b *Blacktip) fetchConfig(ctx context.Context) (*model.EdgeConfig, error) {
return &cfg, json.NewDecoder(resp.Body).Decode(&cfg) return &cfg, json.NewDecoder(resp.Body).Decode(&cfg)
} }
// hostIPv4 lists every IPv4 address on this machine, loopback included: what a wildcard bind covers.
func hostIPv4() []string {
var ips []string
addrs, _ := net.InterfaceAddrs()
for _, a := range addrs {
if ipn, ok := a.(*net.IPNet); ok && ipn.IP.To4() != nil {
ips = append(ips, ipn.IP.String())
}
}
sort.Strings(ips)
return ips
}
// bindIPs says which addresses a mapping listens on. Normally that is its own BindIP. A website on
// 0.0.0.0 steps aside for websites of the same kind that name an address on the same port: it then
// listens on the machine's other addresses only, so a wildcard never takes a port from a specific IP.
func bindIPs(m model.Mapping, all []model.Mapping) []string {
if !model.Web(m.Proto) {
return []string{m.BindIP}
}
if ip := net.ParseIP(m.BindIP); ip == nil || !ip.IsUnspecified() {
return []string{m.BindIP}
}
named := map[string]bool{}
for _, o := range all {
if o.ID == m.ID || o.Proto != m.Proto || o.Port != m.Port {
continue
}
if ip := net.ParseIP(o.BindIP); ip != nil && !ip.IsUnspecified() {
named[ip.String()] = true
}
}
if len(named) == 0 {
return []string{m.BindIP}
}
var out []string
for _, ip := range hostIPv4() {
if !named[ip] {
out = append(out, ip)
}
}
return out
}
func listenKey(m model.Mapping) string { func listenKey(m model.Mapping) string {
n := "tcp" n := "tcp"
if m.Proto == model.ProtoUDP { if m.Proto == model.ProtoUDP {
@@ -307,23 +356,27 @@ func (b *Blacktip) apply(cfg *model.EdgeConfig) {
continue continue
} }
// a range opens one listener per port; each shifts the target port by its offset // a range opens one listener per port; each shifts the target port by its offset
for p := m.Port; p <= m.LastPort(); p++ { for _, ip := range bindIPs(m, cfg.Mappings) {
mm := m for p := m.Port; p <= m.LastPort(); p++ {
mm.Port = p mm := m
k := listenKey(mm) mm.BindIP, mm.Port = ip, p
if want[k] == nil { k := listenKey(mm)
want[k] = &bindSet{off: p - m.Port} if want[k] == nil {
want[k] = &bindSet{off: p - m.Port}
}
want[k].ms = append(want[k].ms, b.maps[m.ID])
} }
want[k].ms = append(want[k].ms, b.maps[m.ID])
} }
} }
// https ports can also answer on :80 with a redirect, unless something else owns that port // https ports can also answer on :80 with a redirect, unless something else owns that port
redirects := map[string][]*mapState{} redirects := map[string][]*mapState{}
for _, m := range cfg.Mappings { for _, m := range cfg.Mappings {
if b.tr == nil && m.Proto == model.ProtoHTTPS && m.RedirectHTTP { if b.tr == nil && m.Proto == model.ProtoHTTPS && m.RedirectHTTP {
k := "tcp|" + net.JoinHostPort(m.BindIP, "80") for _, ip := range bindIPs(m, cfg.Mappings) {
if _, taken := want[k]; !taken { k := "tcp|" + net.JoinHostPort(ip, "80")
redirects[k] = append(redirects[k], b.maps[m.ID]) if _, taken := want[k]; !taken {
redirects[k] = append(redirects[k], b.maps[m.ID])
}
} }
} }
} }
@@ -336,6 +389,13 @@ func (b *Blacktip) apply(cfg *model.EdgeConfig) {
delete(b.listeners, k) delete(b.listeners, k)
} }
} }
b.addrSig = ""
for _, m := range cfg.Mappings {
if ips := bindIPs(m, cfg.Mappings); len(ips) != 1 || ips[0] != m.BindIP {
b.addrSig = strings.Join(hostIPv4(), ",")
break
}
}
b.status = map[string]string{} b.status = map[string]string{}
bring := func(k string, ms []*mapState, off int, redirect bool) { bring := func(k string, ms []*mapState, off int, redirect bool) {
if l, ok := b.listeners[k]; ok { if l, ok := b.listeners[k]; ok {
@@ -345,7 +405,9 @@ func (b *Blacktip) apply(cfg *model.EdgeConfig) {
l.close() l.close()
delete(b.listeners, k) delete(b.listeners, k)
} }
l, err := b.open(ms, off, redirect) _, addr, _ := strings.Cut(k, "|")
host, _, _ := net.SplitHostPort(addr)
l, err := b.open(host, ms, off, redirect)
if err != nil { if err != nil {
log.Printf("open %s: %v", k, err) log.Printf("open %s: %v", k, err)
if !redirect { if !redirect {
@@ -372,7 +434,11 @@ func (b *Blacktip) apply(cfg *model.EdgeConfig) {
bring(k, ms, 0, true) bring(k, ms, 0, true)
} }
if b.tr != nil { if b.tr != nil {
b.tr.sync(b.ctx, webMaps, cfg.AcmeEmail, cfg.AcmeStages) binds := map[string][]string{}
for _, ms := range webMaps {
binds[ms.m.ID] = bindIPs(ms.m, cfg.Mappings)
}
b.tr.sync(b.ctx, webMaps, binds, cfg.AcmeEmail, cfg.AcmeStages)
if e := b.tr.err(); e != "" { if e := b.tr.err(); e != "" {
for _, ms := range webMaps { for _, ms := range webMaps {
b.status[ms.m.ID] = e b.status[ms.m.ID] = e
@@ -534,6 +600,7 @@ func (b *Blacktip) report() {
} }
b.mu.Lock() b.mu.Lock()
rep.UpdateMsg, rep.UpdateID = b.updateMsg, b.updateID rep.UpdateMsg, rep.UpdateID = b.updateMsg, b.updateID
rep.UpdateStage, rep.UpdatePct, rep.UpdateLog = b.updStage, b.updPct, append([]string(nil), b.updLog...)
for id, chk := range b.mailChecks { for id, chk := range b.mailChecks {
rep.MailChecks[id] = chk rep.MailChecks[id] = chk
} }
@@ -580,10 +647,25 @@ func (b *Blacktip) reportLoop(ctx context.Context) {
return return
case <-t.C: case <-t.C:
b.report() b.report()
b.recheckAddresses()
} }
} }
} }
// recheckAddresses re-applies the config when this machine gains or loses an address that a wildcard
// website is spread across, so the listeners follow the machine.
func (b *Blacktip) recheckAddresses() {
b.mu.Lock()
last, sig := b.last, b.addrSig
b.mu.Unlock()
if last == nil || sig == "" {
return
}
if strings.Join(hostIPv4(), ",") != sig {
b.apply(last)
}
}
// ---------- access logs ---------- // ---------- access logs ----------
func (b *Blacktip) logAccess(e model.LogEntry) { func (b *Blacktip) logAccess(e model.LogEntry) {
@@ -760,19 +842,25 @@ func (b *Blacktip) requestUpdate(id string) {
return // already handled, or in progress return // already handled, or in progress
} }
b.updateID, b.updateMsg = id, "" b.updateID, b.updateMsg = id, ""
b.updStage, b.updPct, b.updLog = "starting", 0, nil
b.updNote("Update requested from the dashboard")
if !b.cfg.AutoUpdate || b.cfg.UpdateNow == nil { if !b.cfg.AutoUpdate || b.cfg.UpdateNow == nil {
b.updateMsg = "auto-update is off on this machine (set BLACKTIP_AUTO_UPDATE=1 and restart it)" b.updateMsg = "auto-update is off on this machine (set BLACKTIP_AUTO_UPDATE=1 and restart it)"
b.updStage = "failed"
b.updNote("Refused: " + b.updateMsg)
go b.report() go b.report()
return return
} }
if !b.updating.CompareAndSwap(false, true) { if !b.updating.CompareAndSwap(false, true) {
b.updateMsg = "another update is already running" b.updateMsg = "another update is already running"
b.updStage = "failed"
b.updNote("Refused: " + b.updateMsg)
go b.report() go b.report()
return return
} }
go func() { go func() {
defer b.updating.Store(false) defer b.updating.Store(false)
tag, err := b.cfg.UpdateNow(b.ctx) // on success this restarts the process and does not return tag, err := b.cfg.UpdateNow(b.ctx, b.updProgress) // on success this restarts the process and does not return
b.mu.Lock() b.mu.Lock()
switch { switch {
case err != nil: case err != nil:
@@ -786,3 +874,32 @@ func (b *Blacktip) requestUpdate(id string) {
b.report() b.report()
}() }()
} }
// updNote adds a timestamped line to the update log. Caller holds b.mu.
func (b *Blacktip) updNote(line string) {
b.updLog = append(b.updLog, time.Now().Format("15:04:05")+" "+line)
if len(b.updLog) > 200 {
b.updLog = b.updLog[len(b.updLog)-200:]
}
}
// updProgress is called by the updater as it works; Hammerhead gets a fresh report at most once a
// second (and straight away for every new log line), so its progress bar follows along.
func (b *Blacktip) updProgress(stage string, pct int, line string) {
b.mu.Lock()
b.updStage = stage
if pct >= 0 {
b.updPct = pct
}
if line != "" {
b.updNote(line)
}
send := line != "" || time.Since(b.updSent) > time.Second
if send {
b.updSent = time.Now()
}
b.mu.Unlock()
if send {
go b.report()
}
}
+5 -1
View File
@@ -55,7 +55,11 @@ func main() {
Hammerhead: strings.TrimRight(*hh, "/"), Token: *token, TunnelAddr: *tun, DataDir: *data, Docker: *dockerOn, Hammerhead: strings.TrimRight(*hh, "/"), Token: *token, TunnelAddr: *tun, DataDir: *data, Docker: *dockerOn,
Traefik: blacktip.TraefikConfig{Path: *traefikPath, ACMEDNS: *acmeDNS, NoACME: *noACME}, Traefik: blacktip.TraefikConfig{Path: *traefikPath, ACMEDNS: *acmeDNS, NoACME: *noACME},
Version: version, AutoUpdate: *autoUpdate, Version: version, AutoUpdate: *autoUpdate,
UpdateNow: func(ctx context.Context) (string, error) { return selfupdate.CheckOnce(ctx, upd) }, UpdateNow: func(ctx context.Context, progress func(string, int, string)) (string, error) {
c := upd
c.Progress = func(e selfupdate.Event) { progress(e.Stage, e.Pct, e.Line) }
return selfupdate.CheckOnce(ctx, c)
},
}) })
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
+1 -1
View File
@@ -3,7 +3,7 @@ module git.veillax.com/Veillax/blacktip
go 1.27.3 go 1.27.3
require ( require (
git.veillax.com/Veillax/reef v0.1.6 git.veillax.com/Veillax/reef v0.1.7
github.com/emersion/go-msgauth v0.7.0 github.com/emersion/go-msgauth v0.7.0
golang.org/x/crypto v0.58.0 golang.org/x/crypto v0.58.0
) )
+2
View File
@@ -6,6 +6,8 @@ git.veillax.com/Veillax/reef v0.1.5 h1:BydjLJsfGgU4zEIqUsePJRjPi9NgbFdmvq0M8dvJC
git.veillax.com/Veillax/reef v0.1.5/go.mod h1:ZBUd/BARO5kkpUNTP81TTY1aQXLHFR6bKDhCxckjHso= git.veillax.com/Veillax/reef v0.1.5/go.mod h1:ZBUd/BARO5kkpUNTP81TTY1aQXLHFR6bKDhCxckjHso=
git.veillax.com/Veillax/reef v0.1.6 h1:NgZ4u2Z3hJ+BYD/zX9lov8fox4s2i8txtYNOpIsikVo= git.veillax.com/Veillax/reef v0.1.6 h1:NgZ4u2Z3hJ+BYD/zX9lov8fox4s2i8txtYNOpIsikVo=
git.veillax.com/Veillax/reef v0.1.6/go.mod h1:ZBUd/BARO5kkpUNTP81TTY1aQXLHFR6bKDhCxckjHso= git.veillax.com/Veillax/reef v0.1.6/go.mod h1:ZBUd/BARO5kkpUNTP81TTY1aQXLHFR6bKDhCxckjHso=
git.veillax.com/Veillax/reef v0.1.7 h1:5C2udonlJac2xvDOE5e+zDxej+AbZ9ttrNSE4owmwuU=
git.veillax.com/Veillax/reef v0.1.7/go.mod h1:ZBUd/BARO5kkpUNTP81TTY1aQXLHFR6bKDhCxckjHso=
github.com/Microsoft/go-winio v0.6.4 h1:kJF2pamtYCWXf0xAT8sAkwKC0pMl2HYBrRPHAqyUc+g= github.com/Microsoft/go-winio v0.6.4 h1:kJF2pamtYCWXf0xAT8sAkwKC0pMl2HYBrRPHAqyUc+g=
github.com/Microsoft/go-winio v0.6.4/go.mod h1:1xl5JXG4ze/m74GH6Dc4Mwxfc3VL5siRVQjIAlHH07U= github.com/Microsoft/go-winio v0.6.4/go.mod h1:1xl5JXG4ze/m74GH6Dc4Mwxfc3VL5siRVQjIAlHH07U=
github.com/emersion/go-msgauth v0.7.0 h1:vj2hMn6KhFtW41kshIBTXvp6KgYSqpA/ZN9Pv4g1INc= github.com/emersion/go-msgauth v0.7.0 h1:vj2hMn6KhFtW41kshIBTXvp6KgYSqpA/ZN9Pv4g1INc=
+3 -3
View File
@@ -35,12 +35,12 @@ type listener struct {
sessions map[string]*udpSession sessions map[string]*udpSession
} }
func (b *Blacktip) open(ms []*mapState, off int, redirect bool) (*listener, error) { func (b *Blacktip) open(ip string, ms []*mapState, off int, redirect bool) (*listener, error) {
m := ms[0].m m := ms[0].m
addr := net.JoinHostPort(m.BindIP, strconv.Itoa(m.Port+off)) addr := net.JoinHostPort(ip, strconv.Itoa(m.Port+off))
l := &listener{b: b, kind: m.Proto, redirect: redirect, maps: ms, off: off, done: make(chan struct{}), sessions: map[string]*udpSession{}} l := &listener{b: b, kind: m.Proto, redirect: redirect, maps: ms, off: off, done: make(chan struct{}), sessions: map[string]*udpSession{}}
if redirect { if redirect {
addr = net.JoinHostPort(m.BindIP, "80") addr = net.JoinHostPort(ip, "80")
l.kind = model.ProtoHTTP l.kind = model.ProtoHTTP
} }
if m.Proto == model.ProtoUDP { if m.Proto == model.ProtoUDP {
+43 -14
View File
@@ -49,7 +49,8 @@ type traefikMgr struct {
stop context.CancelFunc // stops the current traefik process stop context.CancelFunc // stops the current traefik process
lastErr string lastErr string
maps map[string]*mapState maps map[string]*mapState
ports map[string]int // mapping ID -> relay port, kept stable across edits so Traefik's backend URL never changes binds map[string][]string // mapping ID -> the addresses it listens on (a wildcard steps aside for named ones)
ports map[string]int // mapping ID -> relay port, kept stable across edits so Traefik's backend URL never changes
dynBytes []byte dynBytes []byte
} }
@@ -217,20 +218,23 @@ func hostRule(host string) (string, bool) {
// sync rewrites Traefik's configuration for the given web mappings and // sync rewrites Traefik's configuration for the given web mappings and
// (re)starts the process if its static part changed. Called with b.mu held. // (re)starts the process if its static part changed. Called with b.mu held.
func (t *traefikMgr) sync(ctx context.Context, maps []*mapState, acmeEmail string, staging bool) { func (t *traefikMgr) sync(ctx context.Context, maps []*mapState, binds map[string][]string, acmeEmail string, staging bool) {
t.mu.Lock() t.mu.Lock()
defer t.mu.Unlock() defer t.mu.Unlock()
t.binds = binds
t.maps = map[string]*mapState{} t.maps = map[string]*mapState{}
eps := map[string]string{} // name -> address eps := map[string]string{} // name -> address
needACME := false needACME := false
for _, ms := range maps { for _, ms := range maps {
t.maps[ms.m.ID] = ms t.maps[ms.m.ID] = ms
eps[epName(ms.m.BindIP, ms.m.Port)] = net.JoinHostPort(ms.m.BindIP, strconv.Itoa(ms.m.Port)) for _, ip := range t.bindsOf(ms.m) {
eps[epName(ip, ms.m.Port)] = net.JoinHostPort(ip, strconv.Itoa(ms.m.Port))
if ms.m.Proto == model.ProtoHTTPS && ms.m.RedirectHTTP {
eps[epName(ip, 80)] = net.JoinHostPort(ip, "80")
}
}
if ms.m.Proto == model.ProtoHTTPS { if ms.m.Proto == model.ProtoHTTPS {
needACME = true needACME = true
if ms.m.RedirectHTTP {
eps[epName(ms.m.BindIP, 80)] = net.JoinHostPort(ms.m.BindIP, "80")
}
} }
} }
routers := map[string]any{} routers := map[string]any{}
@@ -244,7 +248,11 @@ func (t *traefikMgr) sync(ctx context.Context, maps []*mapState, acmeEmail strin
continue continue
} }
rule, wildcard := hostRule(m.Hostname) rule, wildcard := hostRule(m.Hostname)
ep := epName(m.BindIP, m.Port) var mainEPs, redirEPs []string
for _, ip := range t.bindsOf(m) {
mainEPs = append(mainEPs, epName(ip, m.Port))
redirEPs = append(redirEPs, epName(ip, 80))
}
prio := 5000 prio := 5000
if wildcard { if wildcard {
prio = 2000 prio = 2000
@@ -274,7 +282,7 @@ func (t *traefikMgr) sync(ctx context.Context, maps []*mapState, acmeEmail strin
mws["host-"+m.ID] = map[string]any{"headers": map[string]any{"customRequestHeaders": map[string]string{"Host": m.RewriteHost}}} mws["host-"+m.ID] = map[string]any{"headers": map[string]any{"customRequestHeaders": map[string]string{"Host": m.RewriteHost}}}
chain = append(chain, "host-"+m.ID) chain = append(chain, "host-"+m.ID)
} }
router := map[string]any{"rule": rule, "entryPoints": []string{ep}, "service": "s-" + m.ID, "middlewares": chain, "priority": prio} router := map[string]any{"rule": rule, "entryPoints": mainEPs, "service": "s-" + m.ID, "middlewares": chain, "priority": prio}
var tlsCfg map[string]any var tlsCfg map[string]any
if m.Proto == model.ProtoHTTPS { if m.Proto == model.ProtoHTTPS {
tlsCfg = map[string]any{} tlsCfg = map[string]any{}
@@ -292,7 +300,7 @@ func (t *traefikMgr) sync(ctx context.Context, maps []*mapState, acmeEmail strin
services["g-"+m.ID] = map[string]any{"loadBalancer": map[string]any{ services["g-"+m.ID] = map[string]any{"loadBalancer": map[string]any{
"servers": []map[string]string{{"url": fmt.Sprintf("http://127.0.0.1:%d", t.gatePort)}}, "passHostHeader": true}} "servers": []map[string]string{{"url": fmt.Sprintf("http://127.0.0.1:%d", t.gatePort)}}, "passHostHeader": true}}
mws["gate-"+m.ID] = map[string]any{"headers": map[string]any{"customRequestHeaders": map[string]string{"X-Hammerhead-Mapping": m.ID}}} mws["gate-"+m.ID] = map[string]any{"headers": map[string]any{"customRequestHeaders": map[string]string{"X-Hammerhead-Mapping": m.ID}}}
g := map[string]any{"rule": rule + " && PathPrefix(`/.hammerhead/`)", "entryPoints": []string{ep}, "service": "g-" + m.ID, g := map[string]any{"rule": rule + " && PathPrefix(`/.hammerhead/`)", "entryPoints": mainEPs, "service": "g-" + m.ID,
"middlewares": []string{"gate-" + m.ID}, "priority": prio + 1000} "middlewares": []string{"gate-" + m.ID}, "priority": prio + 1000}
if tlsCfg != nil { if tlsCfg != nil {
g["tls"] = tlsCfg g["tls"] = tlsCfg
@@ -305,7 +313,7 @@ func (t *traefikMgr) sync(ctx context.Context, maps []*mapState, acmeEmail strin
redirPort = strconv.Itoa(m.Port) redirPort = strconv.Itoa(m.Port)
} }
mws["redir-"+m.ID] = map[string]any{"redirectScheme": map[string]any{"scheme": "https", "port": redirPort, "permanent": true}} mws["redir-"+m.ID] = map[string]any{"redirectScheme": map[string]any{"scheme": "https", "port": redirPort, "permanent": true}}
routers["r-"+m.ID] = map[string]any{"rule": rule, "entryPoints": []string{epName(m.BindIP, 80)}, "service": "noop@internal", routers["r-"+m.ID] = map[string]any{"rule": rule, "entryPoints": redirEPs, "service": "noop@internal",
"middlewares": []string{"redir-" + m.ID}, "priority": prio} "middlewares": []string{"redir-" + m.ID}, "priority": prio}
} }
} }
@@ -545,10 +553,7 @@ func (t *traefikMgr) probeCerts(ctx context.Context) {
t.mu.Unlock() t.mu.Unlock()
changed := false changed := false
for _, ms := range targets { for _, ms := range targets {
ip := ms.m.BindIP ip := t.probeAddr(ms.m)
if pip := net.ParseIP(ip); pip != nil && pip.IsUnspecified() {
ip = "127.0.0.1"
}
status := t.probeOne(net.JoinHostPort(ip, strconv.Itoa(ms.m.Port)), ms.m.Hostname) status := t.probeOne(net.JoinHostPort(ip, strconv.Itoa(ms.m.Port)), ms.m.Hostname)
t.b.mu.Lock() t.b.mu.Lock()
if t.b.acme.certs == nil { if t.b.acme.certs == nil {
@@ -598,3 +603,27 @@ func containsStr(list []string, s string) bool {
} }
return false return false
} }
// bindsOf is the addresses a web mapping listens on; t.mu is held, or the caller does not race sync.
func (t *traefikMgr) bindsOf(m model.Mapping) []string {
if ips := t.binds[m.ID]; len(ips) > 0 {
return ips
}
return []string{m.BindIP}
}
// probeAddr picks an address on this machine to test a mapping's certificate through.
func (t *traefikMgr) probeAddr(m model.Mapping) string {
t.mu.Lock()
ips := t.bindsOf(m)
t.mu.Unlock()
for _, ip := range ips {
if ip == "127.0.0.1" {
return ip
}
}
if p := net.ParseIP(ips[0]); p != nil && p.IsUnspecified() {
return "127.0.0.1"
}
return ips[0]
}
+37
View File
@@ -0,0 +1,37 @@
package blacktip
import (
"testing"
"git.veillax.com/Veillax/reef/model"
)
func TestWildcardWebsitesStepAsideForNamedAddresses(t *testing.T) {
all := []model.Mapping{
{ID: "a", Proto: model.ProtoHTTPS, BindIP: "0.0.0.0", Port: 443, Hostname: "a.test"},
{ID: "b", Proto: model.ProtoHTTPS, BindIP: "127.0.0.1", Port: 443, Hostname: "b.test"},
{ID: "c", Proto: model.ProtoHTTPS, BindIP: "0.0.0.0", Port: 8443, Hostname: "c.test"},
{ID: "d", Proto: model.ProtoTCP, BindIP: "0.0.0.0", Port: 443},
}
// the wildcard on 443 gives up the address that "b" names
for _, ip := range bindIPs(all[0], all) {
if ip == "127.0.0.1" {
t.Fatalf("wildcard still listens on 127.0.0.1: %v", bindIPs(all[0], all))
}
}
// the named address keeps its own listener
if got := bindIPs(all[1], all); len(got) != 1 || got[0] != "127.0.0.1" {
t.Fatalf("named mapping: %v", got)
}
// a different port, or a raw TCP port, is not affected
if got := bindIPs(all[2], all); len(got) != 1 || got[0] != "0.0.0.0" {
t.Fatalf("other port: %v", got)
}
if got := bindIPs(all[3], all); len(got) != 1 || got[0] != "0.0.0.0" {
t.Fatalf("raw tcp: %v", got)
}
// with nothing named on that port the wildcard stays a single wildcard listener
if got := bindIPs(all[0], all[:1]); len(got) != 1 || got[0] != "0.0.0.0" {
t.Fatalf("alone: %v", got)
}
}