blob: 01811a5fe5f0c8505d6379cdae3e4ba7faf6e12b [file] [log] [blame]
// Copyright 2023 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can
// found in the LICENSE file.
// 'relay.go' provides a simple interface for running a socket relay
// to a remote server, using the 'socat' tool.
package main
import (
"context"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"sync"
"github.com/golang/glog"
)
// socketRelay contains the parameters for operating a single socket relay.
type socketRelay struct {
// Identifier for this relay, used for logging.
Name string `json:"name"`
// Name of socket file (to be created inside a temporary directory).
SocketFileName string `json:"socket_file_name"`
// Name of environment variable to export, pointing to the socket file.
SocketPathEnvVar string `json:"socket_path_env_var"`
// Remote address of server to connect.
ServerAddr string `json:"server_address"`
// Internal fields follow:
// Full path to socket file.
socketFileFullPath string
}
// initSocketFile establishes a full path to a socket file.
// The directory 'tempdir' should already exist.
func (r *socketRelay) initSocketFile(tempDir string) (string, error) {
r.socketFileFullPath = filepath.Join(tempDir, r.SocketFileName)
glog.V(1).Infof("[%s] using socket file %s", r.Name, r.socketFileFullPath)
// remove existing socket first
err := os.RemoveAll(r.socketFileFullPath)
return r.socketFileFullPath, err
}
// env returns the environment variable string X=Y for the socket file.
func (r *socketRelay) env() (string, error) {
if r.socketFileFullPath == "" {
return "", fmt.Errorf("must call initSocketFile() before env().")
}
env := fmt.Sprintf("%s=%s", r.SocketPathEnvVar, r.socketFileFullPath)
glog.V(1).Infof("[%s] env %s", r.Name, env)
return env, nil
}
// runInterruptOnCancel runs a command that is expected to be terminated
// only by an interrupt signal (Ctrl-C).
func runInterruptOnCancel(ctx context.Context, logPrefix string, command ...string) error {
cmd := exec.CommandContext(ctx, command[0], command[1:]...)
glog.V(0).Infof("%sSpawning process", logPrefix)
interrupted := false
cmd.Cancel = func() error { // .Cancel requires go-1.20
// Send SIGINT (instead of the default SIGKILL) to allow socat to
// clean-up before gracefully exiting.
interrupted = true
if cmd.Process != nil {
glog.V(0).Infof("%sShutting down with SIGINT", logPrefix)
return cmd.Process.Signal(os.Interrupt)
}
return nil
}
err := cmd.Run()
// Cancellation is expected.
if interrupted {
return nil
}
if err == nil {
// Program exited before interrupt.
return nil
}
if errors.Is(err, context.Canceled) {
return nil
}
if exitErr, ok := err.(*exec.ExitError); ok {
if !exitErr.Exited() { // process was terminated
return nil
}
}
return fmt.Errorf("%sprocess error: %v", logPrefix, err)
}
// Run launches a relay. Caller should invoke this in a go-routine to run it in the background.
// 'socatPath' is the path to the 'socat' tool for the relay subprocess.
func (r *socketRelay) run(ctx context.Context, socatPath string) error {
if r.socketFileFullPath == "" {
return fmt.Errorf("Must call initSocketFile() before run().")
}
command := []string{
socatPath,
// For verbose debugging, pass repeated -d options.
fmt.Sprintf("UNIX-LISTEN:%s,unlink-early,fork", r.socketFileFullPath),
fmt.Sprintf("TCP:%s", r.ServerAddr),
}
return runInterruptOnCancel(ctx, fmt.Sprintf("[%s](relay) ", r.Name), command...)
}
// multiRelayWrap sets up multiple socket relay processes, sets up an
// environment, and invokes a function 'f', which usually invokes
// a wrapped subprocess using the new environment, and then shuts down
// the relay processes.
// 'relays' are the set of connections to relay through sockets.
// 'tempDir' is a writeable directory where socket files will be created.
// 'socatPath' is the location of the `socat` tool.
// 'f' is any function or subprocesses that operate while the relays are run.
// Returns the exit code of the wrapped function.
func multiRelayWrap(ctx context.Context, relays []*socketRelay, tempDir string, socatPath string, f func(env []string) error) error {
// Initialize socket files.
for _, r := range relays {
if _, err := r.initSocketFile(tempDir); err != nil {
return err
}
}
// Launch relays, running them in the background.
// Stop relays before returning.
var wg sync.WaitGroup
ctx, cancel := context.WithCancel(ctx)
for _, r := range relays {
r := r
wg.Add(1)
go func() {
defer wg.Done()
if err := r.run(ctx, socatPath); err != nil {
glog.Errorf("[%s] error running relay: %v", r.Name, err)
}
}()
}
defer func() {
cancel()
wg.Wait() // Wait for all relay go-routines to finish exiting.
}()
// Setup a modified environment for the function (usually subprocess).
var env []string
for _, r := range relays {
v, err := r.env()
if err != nil {
return err
}
env = append(env, v)
}
// Call the wrapped function/subprocesses.
return f(env)
}