Skip to content

Commit

Permalink
Merge pull request #622 from hashicorp/f-sockets
Browse files Browse the repository at this point in the history
Unix domain sockets
  • Loading branch information
armon committed Jan 22, 2015
2 parents 149f25a + c958824 commit cf04d6a
Show file tree
Hide file tree
Showing 10 changed files with 283 additions and 32 deletions.
7 changes: 0 additions & 7 deletions command/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,6 @@ const (
// Path to save local agent checks
checksDir = "checks"

// errSocketFileExists is the human-friendly error message displayed when
// trying to bind a socket to an existing file.
errSocketFileExists = "A file exists at the requested socket path %q. " +
"If Consul was not shut down properly, the socket file may " +
"be left behind. If the path looks correct, remove the file " +
"and try again."

// The ID of the faux health checks for maintenance mode
serviceMaintCheckPrefix = "_service_maintenance"
nodeMaintCheckID = "_node_maintenance"
Expand Down
23 changes: 18 additions & 5 deletions command/agent/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,11 +295,15 @@ func (c *Command) setupAgent(config *Config, logOutput io.Writer, logWriter *log
return err
}

// Error if we are trying to bind a domain socket to an existing path
if path, ok := unixSocketAddr(config.Addresses.RPC); ok {
if _, err := os.Stat(path); err == nil || !os.IsNotExist(err) {
c.Ui.Output(fmt.Sprintf(errSocketFileExists, path))
return fmt.Errorf(errSocketFileExists, path)
// Clear the domain socket file if it exists
socketPath, isSocket := unixSocketAddr(config.Addresses.RPC)
if isSocket {
if _, err := os.Stat(socketPath); !os.IsNotExist(err) {
agent.logger.Printf("[WARN] agent: Replacing socket %q", socketPath)
}
if err := os.Remove(socketPath); err != nil && !os.IsNotExist(err) {
c.Ui.Output(fmt.Sprintf("Error removing socket file: %s", err))
return err
}
}

Expand All @@ -310,6 +314,15 @@ func (c *Command) setupAgent(config *Config, logOutput io.Writer, logWriter *log
return err
}

// Set up ownership/permission bits on the socket file
if isSocket {
if err := setFilePermissions(socketPath, config.UnixSockets); err != nil {
agent.Shutdown()
c.Ui.Error(fmt.Sprintf("Error setting up socket: %s", err))
return err
}
}

// Start the IPC layer
c.Ui.Output("Starting Consul agent RPC...")
c.rpcServer = NewAgentRPC(agent, rpcListener, logOutput, logWriter)
Expand Down
31 changes: 21 additions & 10 deletions command/agent/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"io/ioutil"
"log"
"os"
"strings"
"testing"

"github.com/hashicorp/consul/testutil"
Expand Down Expand Up @@ -166,7 +165,7 @@ func TestRetryJoinWanFail(t *testing.T) {
}
}

func TestSetupAgent_UnixSocket_Fails(t *testing.T) {
func TestSetupAgent_RPCUnixSocket_FileExists(t *testing.T) {
conf := nextConfig()
tmpDir, err := ioutil.TempDir("", "consul")
if err != nil {
Expand All @@ -185,10 +184,12 @@ func TestSetupAgent_UnixSocket_Fails(t *testing.T) {
conf.Server = true
conf.Bootstrap = true

// Set socket address to an existing file. Consul should fail to
// start and return an error.
// Set socket address to an existing file.
conf.Addresses.RPC = "unix://" + socketPath

// Custom mode for socket file
conf.UnixSockets.Perms = "0777"

shutdownCh := make(chan struct{})
defer close(shutdownCh)

Expand All @@ -200,12 +201,22 @@ func TestSetupAgent_UnixSocket_Fails(t *testing.T) {
logWriter := NewLogWriter(512)
logOutput := new(bytes.Buffer)

// Ensure we got an error mentioning the socket file
err = cmd.setupAgent(conf, logOutput, logWriter)
if err == nil {
t.Fatalf("should have failed")
// Ensure the server is created
if err := cmd.setupAgent(conf, logOutput, logWriter); err != nil {
t.Fatalf("err: %s", err)
}

// Ensure the file was replaced by the socket
fi, err := os.Stat(socketPath)
if err != nil {
t.Fatalf("err: %s", err)
}
if !strings.Contains(err.Error(), socketPath) {
t.Fatalf("expected socket file error, got: %q", err)
if fi.Mode()&os.ModeSocket == 0 {
t.Fatalf("expected socket to replace file")
}

// Ensure permissions were applied to the socket file
if fi.Mode().String() != "Srwxrwxrwx" {
t.Fatalf("bad permissions: %s", fi.Mode())
}
}
38 changes: 38 additions & 0 deletions command/agent/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,35 @@ type Config struct {

// WatchPlans contains the compiled watches
WatchPlans []*watch.WatchPlan `mapstructure:"-" json:"-"`

// UnixSockets is a map of socket configuration data
UnixSockets UnixSocketConfig `mapstructure:"unix_sockets"`
}

// UnixSocketPermissions contains information about a unix socket, and
// implements the FilePermissions interface.
type UnixSocketPermissions struct {
Usr string `mapstructure:"user"`
Grp string `mapstructure:"group"`
Perms string `mapstructure:"mode"`
}

func (u UnixSocketPermissions) User() string {
return u.Usr
}

func (u UnixSocketPermissions) Group() string {
return u.Grp
}

func (u UnixSocketPermissions) Mode() string {
return u.Perms
}

// UnixSocketConfig stores information about various unix sockets which
// Consul creates and uses for communication.
type UnixSocketConfig struct {
UnixSocketPermissions `mapstructure:",squash"`
}

// unixSocketAddr tests if a given address describes a domain socket,
Expand Down Expand Up @@ -889,6 +918,15 @@ func MergeConfig(a, b *Config) *Config {
if b.DisableAnonymousSignature {
result.DisableAnonymousSignature = true
}
if b.UnixSockets.Usr != "" {
result.UnixSockets.Usr = b.UnixSockets.Usr
}
if b.UnixSockets.Grp != "" {
result.UnixSockets.Grp = b.UnixSockets.Grp
}
if b.UnixSockets.Perms != "" {
result.UnixSockets.Perms = b.UnixSockets.Perms
}

if len(b.HTTPAPIResponseHeaders) != 0 {
if result.HTTPAPIResponseHeaders == nil {
Expand Down
24 changes: 24 additions & 0 deletions command/agent/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,23 @@ func TestDecodeConfig(t *testing.T) {
t.Fatalf("bad: %#v", config)
}

// Domain socket permissions
input = `{"unix_sockets": {"user": "500", "group": "500", "mode": "0700"}}`
config, err = DecodeConfig(bytes.NewReader([]byte(input)))
if err != nil {
t.Fatalf("err: %s", err)
}

if config.UnixSockets.Usr != "500" {
t.Fatalf("bad: %#v", config)
}
if config.UnixSockets.Grp != "500" {
t.Fatalf("bad: %#v", config)
}
if config.UnixSockets.Perms != "0700" {
t.Fatalf("bad: %#v", config)
}

// Disable updates
input = `{"disable_update_check": true, "disable_anonymous_signature": true}`
config, err = DecodeConfig(bytes.NewReader([]byte(input)))
Expand Down Expand Up @@ -1034,6 +1051,13 @@ func TestMergeConfig(t *testing.T) {
HTTPAPIResponseHeaders: map[string]string{
"Access-Control-Allow-Origin": "*",
},
UnixSockets: UnixSocketConfig{
UnixSocketPermissions{
Usr: "500",
Grp: "500",
Perms: "0700",
},
},
}

c := MergeConfig(a, b)
Expand Down
17 changes: 14 additions & 3 deletions command/agent/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,13 @@ func NewHTTPServers(agent *Agent, config *Config, logOutput io.Writer) ([]*HTTPS
}

// Error if we are trying to bind a domain socket to an existing path
if path, ok := unixSocketAddr(config.Addresses.HTTP); ok {
if _, err := os.Stat(path); err == nil || !os.IsNotExist(err) {
return nil, fmt.Errorf(errSocketFileExists, path)
socketPath, isSocket := unixSocketAddr(config.Addresses.HTTP)
if isSocket {
if _, err := os.Stat(socketPath); !os.IsNotExist(err) {
agent.logger.Printf("[WARN] agent: Replacing socket %q", socketPath)
}
if err := os.Remove(socketPath); err != nil && !os.IsNotExist(err) {
return nil, fmt.Errorf("error removing socket file: %s", err)
}
}

Expand All @@ -113,6 +117,13 @@ func NewHTTPServers(agent *Agent, config *Config, logOutput io.Writer) ([]*HTTPS
list = tcpKeepAliveListener{ln.(*net.TCPListener)}
}

// Set up ownership/permission bits on the socket file
if isSocket {
if err := setFilePermissions(socketPath, config.UnixSockets); err != nil {
return nil, fmt.Errorf("Failed setting up HTTP socket: %s", err)
}
}

// Create the mux
mux := http.NewServeMux()

Expand Down
30 changes: 25 additions & 5 deletions command/agent/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ func TestHTTPServer_UnixSocket(t *testing.T) {

dir, srv := makeHTTPServerWithConfig(t, func(c *Config) {
c.Addresses.HTTP = "unix://" + socket

// Only testing mode, since uid/gid might not be settable
// from test environment.
c.UnixSockets = UnixSocketConfig{}
c.UnixSockets.Perms = "0777"
})
defer os.RemoveAll(dir)
defer srv.Shutdown()
Expand All @@ -77,6 +82,15 @@ func TestHTTPServer_UnixSocket(t *testing.T) {
t.Fatalf("err: %s", err)
}

// Ensure the mode was set properly
fi, err := os.Stat(socket)
if err != nil {
t.Fatalf("err: %s", err)
}
if fi.Mode().String() != "Srwxrwxrwx" {
t.Fatalf("bad permissions: %s", fi.Mode())
}

// Ensure we can get a response from the socket.
path, _ := unixSocketAddr(srv.agent.config.Addresses.HTTP)
client := &http.Client{
Expand Down Expand Up @@ -132,11 +146,17 @@ func TestHTTPServer_UnixSocket_FileExists(t *testing.T) {
defer os.RemoveAll(dir)

// Try to start the server with the same path anyways.
if servers, err := NewHTTPServers(agent, conf, agent.logOutput); err == nil {
for _, server := range servers {
server.Shutdown()
}
t.Fatalf("expected socket binding error")
if _, err := NewHTTPServers(agent, conf, agent.logOutput); err != nil {
t.Fatalf("err: %s", err)
}

// Ensure the file was replaced by the socket
fi, err = os.Stat(socket)
if err != nil {
t.Fatalf("err: %s", err)
}
if fi.Mode()&os.ModeSocket == 0 {
t.Fatalf("expected socket to replace file")
}
}

Expand Down
64 changes: 64 additions & 0 deletions command/agent/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import (
"math/rand"
"os"
"os/exec"
"os/user"
"runtime"
"strconv"
"time"

"github.com/hashicorp/go-msgpack/codec"
Expand Down Expand Up @@ -97,3 +99,65 @@ func encodeMsgPack(msg interface{}) ([]byte, error) {
func stringHash(s string) string {
return fmt.Sprintf("%x", md5.Sum([]byte(s)))
}

// FilePermissions is an interface which allows a struct to set
// ownership and permissions easily on a file it describes.
type FilePermissions interface {
// User returns a user ID or user name
User() string

// Group returns a group ID. Group names are not supported.
Group() string

// Mode returns a string of file mode bits e.g. "0644"
Mode() string
}

// setFilePermissions handles configuring ownership and permissions settings
// on a given file. It takes a path and any struct implementing the
// FilePermissions interface. All permission/ownership settings are optional.
// If no user or group is specified, the current user/group will be used. Mode
// is optional, and has no default (the operation is not performed if absent).
// User may be specified by name or ID, but group may only be specified by ID.
func setFilePermissions(path string, p FilePermissions) error {
var err error
uid, gid := os.Getuid(), os.Getgid()

if p.User() != "" {
if uid, err = strconv.Atoi(p.User()); err == nil {
goto GROUP
}

// Try looking up the user by name
if u, err := user.Lookup(p.User()); err == nil {
uid, _ = strconv.Atoi(u.Uid)
goto GROUP
}

return fmt.Errorf("invalid user specified: %v", p.User())
}

GROUP:
if p.Group() != "" {
if gid, err = strconv.Atoi(p.Group()); err != nil {
return fmt.Errorf("invalid group specified: %v", p.Group())
}
}
if err := os.Chown(path, uid, gid); err != nil {
return fmt.Errorf("failed setting ownership to %d:%d on %q: %s",
uid, gid, path, err)
}

if p.Mode() != "" {
mode, err := strconv.ParseUint(p.Mode(), 8, 32)
if err != nil {
return fmt.Errorf("invalid mode specified: %v", p.Mode())
}
if err := os.Chmod(path, os.FileMode(mode)); err != nil {
return fmt.Errorf("failed setting permissions to %d on %q: %s",
mode, path, err)
}
}

return nil
}
Loading

0 comments on commit cf04d6a

Please sign in to comment.