Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions configure_data_plane.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,11 @@ func configureAPI(skipBasicAuth bool, maxBodySize int64) (http.Handler, func())
// Override options with env variables
if os.Getenv("HAPROXY_MWORKER") == "1" {
mWorker = true
masterRuntime := os.Getenv("HAPROXY_MASTER_CLI")
if misc.IsUnixSocketAddr(masterRuntime) {
haproxyOptions.MasterRuntime = strings.Replace(masterRuntime, "unix@", "", 1)
masterCLI := os.Getenv("HAPROXY_MASTER_CLI")
if masterRuntime, ok := misc.MasterSocketFromEnv(masterCLI); ok {
haproxyOptions.MasterRuntime = masterRuntime
} else if masterCLI != "" {
log.Warningf("No usable UNIX socket in HAPROXY_MASTER_CLI (%s), keeping the configured master runtime", masterCLI)
}
}

Expand Down
50 changes: 45 additions & 5 deletions misc/misc.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,16 +155,56 @@ func DiscoverChildPaths(path string, spec json.RawMessage) (models.Endpoints, er
return es, nil
}

// IsUnixSocketAddr reports whether addr designates a UNIX socket, either as a
// bare filesystem path or prefixed with the "unix@" address family used by
// HAProxy. Every other address family ("ipv4@", "sockpair@", "fd@", ...) and
// host:port addresses are rejected, as is the empty string.
func IsUnixSocketAddr(addr string) bool {
if strings.HasPrefix(addr, "ipv4@") || strings.HasPrefix(addr, "ipv6@") {
if addr == "" {
return false
}

// check if it has semicolon
if strings.Contains(addr, ":") {
return false
if family, _, found := strings.Cut(addr, "@"); found {
return family == "unix"
}

// A bare address containing a colon is a host:port, not a socket path.
return !strings.Contains(addr, ":")
}

// MasterSocketFromEnv extracts the master CLI socket path from the raw value of
// the HAPROXY_MASTER_CLI environment variable. HAProxy advertises its master
// CLI sockets as a ";"-separated list, for example
// "unix@/var/run/master.sock;sockpair@7", and the Data Plane API can only talk
// to the UNIX ones.
//
// The first socket already bound on the filesystem wins. When none of the
// candidates exists yet the first valid one is returned anyway, so that a
// delayed runtime start can pick it up once HAProxy binds it. The second return
// value is false when the value holds no usable UNIX socket at all, in which
// case the caller must keep whatever master runtime it was configured with.
func MasterSocketFromEnv(value string) (string, bool) {
var candidates []string

for addr := range strings.SplitSeq(value, ";") {
addr = strings.TrimSpace(addr)
if !IsUnixSocketAddr(addr) {
continue
}
socket := strings.TrimPrefix(addr, "unix@")
if socket == "" {
continue
}
if info, err := os.Stat(socket); err == nil && info.Mode()&os.ModeSocket != 0 {
return socket, true
}
candidates = append(candidates, socket)
}

if len(candidates) == 0 {
return "", false
}
return true
return candidates[0], true
}

func ParseTimeout(tOut string) *int64 {
Expand Down
124 changes: 124 additions & 0 deletions misc/misc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ package misc

import (
"math/rand"
"net"
"os"
"path/filepath"
"testing"
)

Expand All @@ -32,3 +35,124 @@ func TestRandomString(t *testing.T) {
}
}
}

func TestIsUnixSocketAddr(t *testing.T) {
tests := []struct {
addr string
want bool
}{
{addr: "", want: false},
{addr: "/var/run/haproxy.sock", want: true},
{addr: "unix@/var/run/haproxy.sock", want: true},
{addr: "sockpair@7", want: false},
{addr: "fd@3", want: false},
{addr: "ipv4@127.0.0.1:1234", want: false},
{addr: "ipv6@::1:1234", want: false},
{addr: "127.0.0.1:1234", want: false},
}

for _, tt := range tests {
t.Run(tt.addr, func(t *testing.T) {
if got := IsUnixSocketAddr(tt.addr); got != tt.want {
t.Errorf("IsUnixSocketAddr(%q) = %v, want %v", tt.addr, got, tt.want)
}
})
}
}

// listenUnix binds a UNIX socket named name inside dir and returns its path.
func listenUnix(t *testing.T, dir, name string) string {
t.Helper()

socket := filepath.Join(dir, name)
l, err := net.Listen("unix", socket)
if err != nil {
t.Fatalf("cannot listen on %s: %v", socket, err)
}
t.Cleanup(func() { l.Close() })

return socket
}

func TestMasterSocketFromEnv(t *testing.T) {
// os.MkdirTemp instead of t.TempDir: the latter embeds the test name in the
// path, which easily overflows the 104 bytes of sun_path on some systems.
dir, err := os.MkdirTemp("", "dpapi")
if err != nil {
t.Fatalf("cannot create temporary directory: %v", err)
}
t.Cleanup(func() { os.RemoveAll(dir) })

bound := listenUnix(t, dir, "master.sock")
second := listenUnix(t, dir, "second.sock")
missing := filepath.Join(dir, "missing.sock")
regular := filepath.Join(dir, "regular")
if err := os.WriteFile(regular, nil, 0o600); err != nil {
t.Fatalf("cannot create regular file: %v", err)
}

tests := []struct {
name string
value string
want string
wantOK bool
}{
{
name: "empty value",
value: "",
want: "",
wantOK: false,
},
{
name: "only a sockpair",
value: "sockpair@7",
want: "",
wantOK: false,
},
{
name: "unix socket followed by a sockpair",
value: "unix@" + bound + ";sockpair@7",
want: bound,
wantOK: true,
},
{
name: "sockpair listed first",
value: "sockpair@7;unix@" + bound,
want: bound,
wantOK: true,
},
{
name: "first bound socket wins",
value: "unix@" + missing + ";unix@" + second,
want: second,
wantOK: true,
},
{
name: "a regular file is not a socket",
value: "unix@" + regular + ";unix@" + bound,
want: bound,
wantOK: true,
},
{
name: "nothing bound yet falls back to the first candidate",
value: "unix@" + missing + ";sockpair@7",
want: missing,
wantOK: true,
},
{
name: "bare path without the unix prefix",
value: bound,
want: bound,
wantOK: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, ok := MasterSocketFromEnv(tt.value)
if got != tt.want || ok != tt.wantOK {
t.Errorf("MasterSocketFromEnv(%q) = (%q, %v), want (%q, %v)", tt.value, got, ok, tt.want, tt.wantOK)
}
})
}
}