Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

http/sftp: support listening on passed FDs #7801

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
22 changes: 20 additions & 2 deletions cmd/serve/sftp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"github.com/rclone/rclone/fs/config"
"github.com/rclone/rclone/lib/env"
"github.com/rclone/rclone/lib/file"
sdActivation "github.com/rclone/rclone/lib/sdactivation"
"github.com/rclone/rclone/vfs"
"github.com/rclone/rclone/vfs/vfsflags"
"golang.org/x/crypto/ssh"
Expand Down Expand Up @@ -266,10 +267,27 @@ func (s *server) serve() (err error) {

// Once a ServerConfig has been configured, connections can be
// accepted.
s.listener, err = net.Listen("tcp", s.opt.ListenAddr)
var listener net.Listener

// In case we run in a socket-activated environment, listen on (the first)
// passed FD.
sdListeners, err := sdActivation.Listeners()
if err != nil {
return fmt.Errorf("failed to listen for connection: %w", err)
return fmt.Errorf("unable to acquire listeners: %w", err)
}

if len(sdListeners) > 0 {
if len(sdListeners) > 1 {
fs.LogPrintf(fs.LogLevelWarning, nil, "more than one listener passed, ignoring all but the first.\n")
}
listener = sdListeners[0]
} else {
listener, err = net.Listen("tcp", s.opt.ListenAddr)
if err != nil {
return fmt.Errorf("failed to listen for connection: %w", err)
}
}
s.listener = listener
fs.Logf(nil, "SFTP server listening on %v\n", s.listener.Addr())

go s.acceptConnections()
Expand Down
5 changes: 5 additions & 0 deletions cmd/serve/sftp/sftp.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@ directory.
By default the server binds to localhost:2022 - if you want it to be
reachable externally then supply ` + "`--addr :2022`" + ` for example.

This also supports being run with socket activation, in which case it will
listen on the first passed FD.
It can be configured as described in
https://www.freedesktop.org/software/systemd/man/latest/systemd.socket.html

Note that the default of ` + "`--vfs-cache-mode off`" + ` is fine for the rclone
sftp backend, but it may not be with other SFTP clients.

Expand Down
116 changes: 78 additions & 38 deletions lib/http/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/go-chi/chi/v5"
"github.com/rclone/rclone/fs/config/flags"
"github.com/rclone/rclone/lib/atexit"
sdActivation "github.com/rclone/rclone/lib/sdactivation"
"github.com/spf13/pflag"
)

Expand Down Expand Up @@ -74,6 +75,14 @@ certificate authority certificate.
values are "tls1.0", "tls1.1", "tls1.2" and "tls1.3" (default
"tls1.0").

### Socket activation

Instead of the listening addresses specified above, rclone will listen to all
FDs passed by the service manager, if any (and ignore any arguments passed by ` +
"--{{ .Prefix }}addr`" + `).

This allows rclone to be a socket-activated service. It can be configured as described in
https://www.freedesktop.org/software/systemd/man/latest/systemd.socket.html
`
tmpl, err := template.New("server help").Parse(help)
if err != nil {
Expand Down Expand Up @@ -114,7 +123,7 @@ type Config struct {

// AddFlagsPrefix adds flags for the httplib
func (cfg *Config) AddFlagsPrefix(flagSet *pflag.FlagSet, prefix string) {
flags.StringArrayVarP(flagSet, &cfg.ListenAddr, prefix+"addr", "", cfg.ListenAddr, "IPaddress:Port or :Port to bind server to", prefix)
flags.StringArrayVarP(flagSet, &cfg.ListenAddr, prefix+"addr", "", cfg.ListenAddr, "IPaddress:Port, :Port or [unix://]/path/to/socket to bind server to", prefix)
flags.DurationVarP(flagSet, &cfg.ServerReadTimeout, prefix+"server-read-timeout", "", cfg.ServerReadTimeout, "Timeout for server reading data", prefix)
flags.DurationVarP(flagSet, &cfg.ServerWriteTimeout, prefix+"server-write-timeout", "", cfg.ServerWriteTimeout, "Timeout for server writing data", prefix)
flags.IntVarP(flagSet, &cfg.MaxHeaderBytes, prefix+"max-header-bytes", "", cfg.MaxHeaderBytes, "Maximum size of request header", prefix)
Expand Down Expand Up @@ -194,6 +203,32 @@ func WithTemplate(cfg TemplateConfig) Option {
}
}

// For a given listener, and optional tlsConfig, construct a instance.
// The url string ends up in the `url` field of the `instance`.
// This unconditionally wraps the listener with the provided TLS config if one
// is specified, so all decision logic on whether to use TLS needs to live at
// the callsite.
func newInstance(ctx context.Context, s *Server, listener net.Listener, tlsCfg *tls.Config, url string) *instance {
if tlsCfg != nil {
listener = tls.NewListener(listener, tlsCfg)
}

return &instance{
url: url,
listener: listener,
httpServer: &http.Server{
Handler: s.mux,
ReadTimeout: s.cfg.ServerReadTimeout,
WriteTimeout: s.cfg.ServerWriteTimeout,
MaxHeaderBytes: s.cfg.MaxHeaderBytes,
ReadHeaderTimeout: 10 * time.Second, // time to send the headers
IdleTimeout: 60 * time.Second, // time to keep idle connections open
TLSConfig: tlsCfg,
BaseContext: NewBaseContext(ctx, url),
},
}
}

// NewServer instantiates a new http server using provided listeners and options
// This function is provided if the default http server does not meet a services requirements and should not generally be used
// A http server can listen using multiple listeners. For example, a listener for port 80, and a listener for port 443.
Expand Down Expand Up @@ -242,55 +277,60 @@ func NewServer(ctx context.Context, options ...Option) (*Server, error) {

s.initAuth()

// (Only) listen on FDs provided by the service manager, if any.
sdListeners, err := sdActivation.ListenersWithNames()
if err != nil {
return nil, fmt.Errorf("unable to acquire listeners: %w", err)
}

if len(sdListeners) != 0 {
for listenerName, listeners := range sdListeners {
for i, listener := range listeners {
url := fmt.Sprintf("sd-listen:%s-%d/%s", listenerName, i, s.cfg.BaseURL)
if s.tlsConfig != nil {
url = fmt.Sprintf("sd-listen+tls:%s-%d/%s", listenerName, i, s.cfg.BaseURL)
}

instance := newInstance(ctx, s, listener, s.tlsConfig, url)

s.instances = append(s.instances, *instance)
}
}

return s, nil
}

// Process all listeners specified in the CLI Args.
for _, addr := range s.cfg.ListenAddr {
var url string
var network = "tcp"
var tlsCfg *tls.Config
var instance *instance

if strings.HasPrefix(addr, "unix://") || filepath.IsAbs(addr) {
network = "unix"
addr = strings.TrimPrefix(addr, "unix://")
url = addr

listener, err := net.Listen("unix", addr)
if err != nil {
return nil, err
}
instance = newInstance(ctx, s, listener, s.tlsConfig, addr)
} else if strings.HasPrefix(addr, "tls://") || (len(s.cfg.ListenAddr) == 1 && s.tlsConfig != nil) {
tlsCfg = s.tlsConfig
addr = strings.TrimPrefix(addr, "tls://")
}

var listener net.Listener
if tlsCfg == nil {
listener, err = net.Listen(network, addr)
listener, err := net.Listen("tcp", addr)
if err != nil {
return nil, err
}
instance = newInstance(ctx, s, listener, s.tlsConfig, fmt.Sprintf("https://%s%s/", listener.Addr().String(), s.cfg.BaseURL))
} else {
listener, err = tls.Listen(network, addr, tlsCfg)
}
if err != nil {
return nil, err
}

if network == "tcp" {
var secure string
if tlsCfg != nil {
secure = "s"
// HTTP case
addr = strings.TrimPrefix(addr, "http://")
listener, err := net.Listen("tcp", addr)
if err != nil {
return nil, err
}
url = fmt.Sprintf("http%s://%s%s/", secure, listener.Addr().String(), s.cfg.BaseURL)
}
instance = newInstance(ctx, s, listener, nil, fmt.Sprintf("http://%s%s/", listener.Addr().String(), s.cfg.BaseURL))

ii := instance{
url: url,
listener: listener,
httpServer: &http.Server{
Handler: s.mux,
ReadTimeout: s.cfg.ServerReadTimeout,
WriteTimeout: s.cfg.ServerWriteTimeout,
MaxHeaderBytes: s.cfg.MaxHeaderBytes,
ReadHeaderTimeout: 10 * time.Second, // time to send the headers
IdleTimeout: 60 * time.Second, // time to keep idle connections open
TLSConfig: tlsCfg,
BaseContext: NewBaseContext(ctx, url),
},
}

s.instances = append(s.instances, ii)
s.instances = append(s.instances, *instance)
}

return s, nil
Expand Down
24 changes: 24 additions & 0 deletions lib/sdactivation/sdactivation_stub.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//go:build windows || plan9
// +build windows plan9

// Package sd_activate provides support for systemd socket activation,
// wrapping the coreos/go-systemd package.
// This wraps the underlying go-systemd binary, as it fails to build on plan9
// https://github.com/coreos/go-systemd/pull/440
package sdactivation

import (
"net"
)

// ListenersWithNames maps a listener name to a set of net.Listener instances.
// This wraps the underlying go-systemd binary, as it fails to build on plan9
// https://github.com/coreos/go-systemd/pull/440
func ListenersWithNames() (map[string][]net.Listener, error) {
return make(map[string][]net.Listener), nil
}

// Listeners returns a slice containing a net.Listener for each matching socket type passed to this process.
func Listeners() ([]net.Listener, error) {
return nil, nil
}
24 changes: 24 additions & 0 deletions lib/sdactivation/sdactivation_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//go:build !windows && !plan9
// +build !windows,!plan9

// Package sdactivation provides support for systemd socket activation, wrapping
// the coreos/go-systemd package.
// This wraps the underlying go-systemd library, as it fails to build on plan9
// https://github.com/coreos/go-systemd/pull/440
package sdactivation

import (
"net"

sdActivation "github.com/coreos/go-systemd/v22/activation"
)

// ListenersWithNames maps a listener name to a set of net.Listener instances.
func ListenersWithNames() (map[string][]net.Listener, error) {
return sdActivation.ListenersWithNames()
}

// Listeners returns a slice containing a net.Listener for each matching socket type passed to this process.
func Listeners() ([]net.Listener, error) {
return sdActivation.Listeners()
}