Merge pull request #622 from hashicorp/f-sockets

Unix domain sockets
pull/627/head
Armon Dadgar 2015-01-21 16:30:03 -08:00
commit cf04d6ae31
10 changed files with 283 additions and 32 deletions

View File

@ -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"

View File

@ -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
}
}
@ -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)

View File

@ -6,7 +6,6 @@ import (
"io/ioutil"
"log"
"os"
"strings"
"testing"
"github.com/hashicorp/consul/testutil"
@ -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 {
@ -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)
@ -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)
}
if !strings.Contains(err.Error(), socketPath) {
t.Fatalf("expected socket file error, got: %q", err)
// Ensure the file was replaced by the socket
fi, err := os.Stat(socketPath)
if err != nil {
t.Fatalf("err: %s", 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())
}
}

View File

@ -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,
@ -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 {

View File

@ -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)))
@ -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)

View File

@ -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)
}
}
@ -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()

View File

@ -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()
@ -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{
@ -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()
if _, err := NewHTTPServers(agent, conf, agent.logOutput); err != nil {
t.Fatalf("err: %s", err)
}
t.Fatalf("expected socket binding error")
// 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")
}
}

View File

@ -9,7 +9,9 @@ import (
"math/rand"
"os"
"os/exec"
"os/user"
"runtime"
"strconv"
"time"
"github.com/hashicorp/go-msgpack/codec"
@ -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
}

View File

@ -1,6 +1,8 @@
package agent
import (
"io/ioutil"
"os"
"testing"
"time"
)
@ -39,3 +41,59 @@ func TestStringHash(t *testing.T) {
t.Fatalf("bad: %s", out)
}
}
func TestSetFilePermissions(t *testing.T) {
tempFile, err := ioutil.TempFile("", "consul")
if err != nil {
t.Fatalf("err: %s", err)
}
path := tempFile.Name()
defer os.Remove(path)
// Bad UID fails
if err := setFilePermissions(path, UnixSocketPermissions{Usr: "%"}); err == nil {
t.Fatalf("should fail")
}
// Bad GID fails
if err := setFilePermissions(path, UnixSocketPermissions{Grp: "%"}); err == nil {
t.Fatalf("should fail")
}
// Bad mode fails
if err := setFilePermissions(path, UnixSocketPermissions{Perms: "%"}); err == nil {
t.Fatalf("should fail")
}
// Allows omitting user/group/mode
if err := setFilePermissions(path, UnixSocketPermissions{}); err != nil {
t.Fatalf("err: %s", err)
}
// Doesn't change mode if not given
if err := os.Chmod(path, 0700); err != nil {
t.Fatalf("err: %s", err)
}
if err := setFilePermissions(path, UnixSocketPermissions{}); err != nil {
t.Fatalf("err: %s", err)
}
fi, err := os.Stat(path)
if err != nil {
t.Fatalf("err: %s", err)
}
if fi.Mode().String() != "-rwx------" {
t.Fatalf("bad: %s", fi.Mode())
}
// Changes mode if given
if err := setFilePermissions(path, UnixSocketPermissions{Perms: "0777"}); err != nil {
t.Fatalf("err: %s", err)
}
fi, err = os.Stat(path)
if err != nil {
t.Fatalf("err: %s", err)
}
if fi.Mode().String() != "-rwxrwxrwx" {
t.Fatalf("bad: %s", fi.Mode())
}
}

View File

@ -244,8 +244,10 @@ definitions support being updated during a reload.
Both `rpc` and `http` support binding to Unix domain sockets. A socket can be
specified in the form `unix:///path/to/socket`. A new domain socket will be
created at the given path. If the specified file path already exists, Consul
will refuse to start and return an error. For information on how to secure
socket file permissions, refer to the manual page for your operating system.
will attempt to clear the file and create the domain socket in its place.
<br><br>
The permissions of the socket file are tunable via the `unix_sockets` config
construct.
<br><br>
When running Consul agent commands against Unix socket interfaces, use the
`-rpc-addr` or `-http-addr` arguments to specify the path to the socket. You
@ -429,6 +431,23 @@ definitions support being updated during a reload.
* `ui_dir` - Equivalent to the `-ui-dir` command-line flag.
* `unix_sockets` - This allows tuning the ownership and permissions of the
Unix domain socket files created by Consul. Domain sockets are only used if
the HTTP or RPC addresses are configured with the `unix://` prefix. The
following options are valid within this construct, and apply globally to all
sockets created by Consul:
<br>
* `user` - The name or ID of the user who will own the socket file.
* `group` - The group ID ownership of the socket file. Note that this option
currently only supports numeric ID's.
* `mode` - The permission bits to set on the file.
<br>
It is important to note that this option may have different effects on
different operating systems. Linux generally observes socket file permissions,
while many BSD variants ignore permissions on the socket file itself. It is
important to test this feature on your specific distribution. This feature is
currently not functional on Windows hosts.
* `verify_incoming` - If set to True, Consul requires that all incoming
connections make use of TLS, and that the client provides a certificate signed
by the Certificate Authority from the `ca_file`. By default, this is false, and