diff --git a/internal/configuration/settings/portforward.go b/internal/configuration/settings/portforward.go index d8b2457bd..11cb88a33 100644 --- a/internal/configuration/settings/portforward.go +++ b/internal/configuration/settings/portforward.go @@ -29,6 +29,11 @@ type PortForwarding struct { // to write to a file. It cannot be nil for the // internal state Filepath *string `json:"status_file_path"` + // Command is the port forwarding status command + // to use. It can be the empty string to indicate not + // to run a command. It cannot be nil for the + // internal state + Command *string `json:"status_command"` // ListeningPort is the port traffic would be redirected to from the // forwarded port. The redirection is disabled if it is set to 0, which // is its default as well. @@ -83,6 +88,7 @@ func (p *PortForwarding) Copy() (copied PortForwarding) { Enabled: gosettings.CopyPointer(p.Enabled), Provider: gosettings.CopyPointer(p.Provider), Filepath: gosettings.CopyPointer(p.Filepath), + Command: gosettings.CopyPointer(p.Command), ListeningPort: gosettings.CopyPointer(p.ListeningPort), Username: p.Username, Password: p.Password, @@ -93,6 +99,7 @@ func (p *PortForwarding) OverrideWith(other PortForwarding) { p.Enabled = gosettings.OverrideWithPointer(p.Enabled, other.Enabled) p.Provider = gosettings.OverrideWithPointer(p.Provider, other.Provider) p.Filepath = gosettings.OverrideWithPointer(p.Filepath, other.Filepath) + p.Command = gosettings.OverrideWithPointer(p.Command, other.Command) p.ListeningPort = gosettings.OverrideWithPointer(p.ListeningPort, other.ListeningPort) p.Username = gosettings.OverrideWithComparable(p.Username, other.Username) p.Password = gosettings.OverrideWithComparable(p.Password, other.Password) @@ -102,6 +109,7 @@ func (p *PortForwarding) setDefaults() { p.Enabled = gosettings.DefaultPointer(p.Enabled, false) p.Provider = gosettings.DefaultPointer(p.Provider, "") p.Filepath = gosettings.DefaultPointer(p.Filepath, "/tmp/gluetun/forwarded_port") + p.Command = gosettings.DefaultPointer(p.Command, "") p.ListeningPort = gosettings.DefaultPointer(p.ListeningPort, 0) } @@ -134,6 +142,11 @@ func (p PortForwarding) toLinesNode() (node *gotree.Node) { } node.Appendf("Forwarded port file path: %s", filepath) + command := *p.Command + if command != "" { + node.Appendf("Forwarded port command: %s", command) + } + if p.Username != "" { credentialsNode := node.Appendf("Credentials:") credentialsNode.Appendf("Username: %s", p.Username) @@ -162,6 +175,9 @@ func (p *PortForwarding) read(r *reader.Reader) (err error) { "PRIVATE_INTERNET_ACCESS_VPN_PORT_FORWARDING_STATUS_FILE", )) + p.Command = r.Get("VPN_PORT_FORWARDING_UP_COMMAND", + reader.ForceLowercase(false)) + p.ListeningPort, err = r.Uint16Ptr("VPN_PORT_FORWARDING_LISTENING_PORT") if err != nil { return err diff --git a/internal/portforward/loop.go b/internal/portforward/loop.go index c8d02a3b6..5ca28b2a7 100644 --- a/internal/portforward/loop.go +++ b/internal/portforward/loop.go @@ -41,6 +41,7 @@ func NewLoop(settings settings.PortForwarding, routing Routing, Service: service.Settings{ Enabled: settings.Enabled, Filepath: *settings.Filepath, + Command: *settings.Command, ListeningPort: *settings.ListeningPort, }, }, diff --git a/internal/portforward/service/command.go b/internal/portforward/service/command.go new file mode 100644 index 000000000..f721a4d2b --- /dev/null +++ b/internal/portforward/service/command.go @@ -0,0 +1,32 @@ +package service + +import ( + "context" + "fmt" + "os" + "os/exec" + "strings" +) + +func (s *Service) runUpCommand(ctx context.Context, ports []uint16) (err error) { + // run command replacing {{PORTS}} with the ports (space separated) + portStrings := make([]string, len(ports)) + for i, port := range ports { + portStrings[i] = fmt.Sprint(int(port)) + } + portsString := strings.Join(portStrings, ",") + + rawCommand := strings.ReplaceAll(s.settings.Command, "{{PORTS}}", portsString) + s.logger.Info("running port forward command " + rawCommand) + command := strings.Split(rawCommand, " ") + // it is a user input and we trust it so we can ignore the gosec warning + cmd := exec.CommandContext(ctx, command[0], command[1:]...) // #nosec G204 + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err = cmd.Run() + if err != nil { + return fmt.Errorf("running command: %w", err) + } + + return nil +} diff --git a/internal/portforward/service/settings.go b/internal/portforward/service/settings.go index 1782226b0..00d6901c9 100644 --- a/internal/portforward/service/settings.go +++ b/internal/portforward/service/settings.go @@ -12,6 +12,7 @@ type Settings struct { Enabled *bool PortForwarder PortForwarder Filepath string + Command string Interface string // needed for PIA and ProtonVPN, tun0 for example ServerName string // needed for PIA CanPortForward bool // needed for PIA @@ -24,6 +25,7 @@ func (s Settings) Copy() (copied Settings) { copied.Enabled = gosettings.CopyPointer(s.Enabled) copied.PortForwarder = s.PortForwarder copied.Filepath = s.Filepath + copied.Command = s.Command copied.Interface = s.Interface copied.ServerName = s.ServerName copied.CanPortForward = s.CanPortForward @@ -37,6 +39,7 @@ func (s *Settings) OverrideWith(update Settings) { s.Enabled = gosettings.OverrideWithPointer(s.Enabled, update.Enabled) s.PortForwarder = gosettings.OverrideWithComparable(s.PortForwarder, update.PortForwarder) s.Filepath = gosettings.OverrideWithComparable(s.Filepath, update.Filepath) + s.Command = gosettings.OverrideWithComparable(s.Command, update.Command) s.Interface = gosettings.OverrideWithComparable(s.Interface, update.Interface) s.ServerName = gosettings.OverrideWithComparable(s.ServerName, update.ServerName) s.CanPortForward = gosettings.OverrideWithComparable(s.CanPortForward, update.CanPortForward) diff --git a/internal/portforward/service/start.go b/internal/portforward/service/start.go index b6bc77267..1711e030f 100644 --- a/internal/portforward/service/start.go +++ b/internal/portforward/service/start.go @@ -69,6 +69,14 @@ func (s *Service) Start(ctx context.Context) (runError <-chan error, err error) return nil, fmt.Errorf("writing port file: %w", err) } + if s.settings.Command != "" { + err = s.runUpCommand(ctx, ports) + if err != nil { + _ = s.cleanup() + return nil, fmt.Errorf("running port forward command: %w", err) + } + } + s.portMutex.Lock() s.ports = ports s.portMutex.Unlock()