Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
36faffb
added capability to set forwarded ports at runtime
jagaimoworks Aug 3, 2024
101656a
added control server route for dynamic port forwarding
jagaimoworks Aug 3, 2024
8a5b481
addressing renaming nits
jagaimoworks Aug 5, 2024
8dfad10
try to silently undo changes when port set/remove fail
jagaimoworks Aug 5, 2024
d489f45
added http error response for failing to set forwarded ports
jagaimoworks Aug 5, 2024
3a808dd
changed http error response to accuratly depict the error
jagaimoworks Aug 5, 2024
44c0ee2
moved statement to improve readability
jagaimoworks Aug 5, 2024
17e5dbf
fix: don´t readd failed port forwardings that failed to be removed an…
jagaimoworks Aug 5, 2024
0817dc4
addressing intrange nit
jagaimoworks Nov 29, 2024
1afbcbd
moved logging responsibility to caller
jagaimoworks Nov 29, 2024
0f56d12
reduced verbosity of the http responses and log the errors as a warni…
jagaimoworks Nov 29, 2024
a812bf2
allow setting NO port forwards => clears forwarded ports
jagaimoworks Nov 29, 2024
c1de4bc
removed logging
jagaimoworks Nov 29, 2024
0d8daa4
nitpicky changes
qdm12 Dec 27, 2024
802b809
Only write status ok on success
qdm12 Dec 27, 2024
19a007f
Use common code for Start and setting ports from control server
qdm12 Dec 27, 2024
3f29f16
Return an error if service is not running
qdm12 Dec 27, 2024
a9cdbe6
Merge branch 'master' into send-ports-forwarded-to-control-server
qdm12 Nov 13, 2025
539fdeb
Fix mutex calls
qdm12 Nov 13, 2025
a6890dd
register new route
qdm12 Nov 13, 2025
de32cae
Merge branch 'master' into send-ports-forwarded-to-control-server
qdm12 Nov 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions internal/portforward/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ type Service interface {
Start(ctx context.Context) (runError <-chan error, err error)
Stop() (err error)
GetPortsForwarded() (ports []uint16)
SetPortsForwarded(ctx context.Context, ports []uint16) (err error)
}

type Routing interface {
Expand Down
11 changes: 11 additions & 0 deletions internal/portforward/loop.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package portforward

import (
"context"
"errors"
"fmt"
"net/http"
"sync"
Expand Down Expand Up @@ -166,6 +167,16 @@ func (l *Loop) GetPortsForwarded() (ports []uint16) {
return l.service.GetPortsForwarded()
}

var ErrServiceNotStarted = errors.New("port forwarding service not started")

func (l *Loop) SetPortsForwarded(ports []uint16) (err error) {
if l.service == nil {
return fmt.Errorf("%w", ErrServiceNotStarted)
}

return l.service.SetPortsForwarded(l.runCtx, ports)
}

func ptrTo[T any](value T) *T {
return &value
}
28 changes: 28 additions & 0 deletions internal/portforward/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package service

import (
"context"
"fmt"
"net/http"
"slices"
"sync"
)

Expand Down Expand Up @@ -50,3 +52,29 @@ func (s *Service) GetPortsForwarded() (ports []uint16) {
copy(ports, s.ports)
return ports
}

func (s *Service) SetPortsForwarded(ctx context.Context, ports []uint16) (err error) {
s.startStopMutex.Lock()
defer s.startStopMutex.Unlock()
s.portMutex.Lock()
defer s.portMutex.Unlock()

slices.Sort(ports)
if slices.Equal(s.ports, ports) {
return nil
}

err = s.cleanup()
if err != nil {
return fmt.Errorf("cleaning up: %w", err)
}

err = s.onNewPorts(ctx, ports)
if err != nil {
return fmt.Errorf("handling new ports: %w", err)
}

s.logger.Info("updated: " + portsToString(s.ports))

return nil
}
76 changes: 46 additions & 30 deletions internal/portforward/service/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package service
import (
"context"
"fmt"
"slices"

"github.com/qdm12/gluetun/internal/netlink"
"github.com/qdm12/gluetun/internal/provider/utils"
Expand Down Expand Up @@ -47,38 +48,12 @@ func (s *Service) Start(ctx context.Context) (runError <-chan error, err error)
return nil, fmt.Errorf("port forwarding for the first time: %w", err)
}

s.logger.Info(portsToString(ports))

for _, port := range ports {
err = s.portAllower.SetAllowedPort(ctx, port, s.settings.Interface)
if err != nil {
return nil, fmt.Errorf("allowing port in firewall: %w", err)
}

if s.settings.ListeningPort != 0 {
err = s.portAllower.RedirectPort(ctx, s.settings.Interface, port, s.settings.ListeningPort)
if err != nil {
return nil, fmt.Errorf("redirecting port in firewall: %w", err)
}
}
}

err = s.writePortForwardedFile(ports)
if err != nil {
_ = s.cleanup()
return nil, fmt.Errorf("writing port file: %w", err)
}

s.portMutex.Lock()
s.ports = ports
s.portMutex.Unlock()
defer s.portMutex.Unlock()

if s.settings.UpCommand != "" {
err = runCommand(ctx, s.cmder, s.logger, s.settings.UpCommand, ports)
if err != nil {
err = fmt.Errorf("running up command: %w", err)
s.logger.Error(err.Error())
}
err = s.onNewPorts(ctx, ports)
if err != nil {
return nil, err
}

keepPortCtx, keepPortCancel := context.WithCancel(context.Background())
Expand All @@ -101,10 +76,51 @@ func (s *Service) Start(ctx context.Context) (runError <-chan error, err error)
}
s.startStopMutex.Lock()
defer s.startStopMutex.Unlock()
s.portMutex.Lock()
defer s.portMutex.Unlock()
_ = s.cleanup()
runError <- err
}(keepPortCtx, s.settings.PortForwarder, obj, readyCh, runErrorCh, keepPortDoneCh)
<-readyCh

return runErrorCh, nil
}

func (s *Service) onNewPorts(ctx context.Context, ports []uint16) (err error) {
slices.Sort(ports)

s.logger.Info(portsToString(ports))

for _, port := range ports {
err = s.portAllower.SetAllowedPort(ctx, port, s.settings.Interface)
if err != nil {
return fmt.Errorf("allowing port in firewall: %w", err)
}

if s.settings.ListeningPort != 0 {
err = s.portAllower.RedirectPort(ctx, s.settings.Interface, port, s.settings.ListeningPort)
if err != nil {
return fmt.Errorf("redirecting port in firewall: %w", err)
}
}
}

err = s.writePortForwardedFile(ports)
if err != nil {
_ = s.cleanup()
return fmt.Errorf("writing port file: %w", err)
}

s.ports = make([]uint16, len(ports))
copy(s.ports, ports)

if s.settings.UpCommand != "" {
err = runCommand(ctx, s.cmder, s.logger, s.settings.UpCommand, ports)
if err != nil {
err = fmt.Errorf("running up command: %w", err)
s.logger.Error(err.Error())
}
}

return nil
}
3 changes: 0 additions & 3 deletions internal/portforward/service/stop.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,6 @@ func (s *Service) Stop() (err error) {
}

func (s *Service) cleanup() (err error) {
s.portMutex.Lock()
defer s.portMutex.Unlock()

if s.settings.DownCommand != "" {
const downTimeout = 60 * time.Second
ctx, cancel := context.WithTimeout(context.Background(), downTimeout)
Expand Down
4 changes: 2 additions & 2 deletions internal/server/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func newHandler(ctx context.Context, logger Logger, logging bool,
authSettings auth.Settings,
buildInfo models.BuildInformation,
vpnLooper VPNLooper,
pfGetter PortForwardedGetter,
pf PortForwarding,
dnsLooper DNSLoop,
updaterLooper UpdaterLooper,
publicIPLooper PublicIPLoop,
Expand All @@ -29,7 +29,7 @@ func newHandler(ctx context.Context, logger Logger, logging bool,
dns := newDNSHandler(ctx, dnsLooper, logger)
updater := newUpdaterHandler(ctx, updaterLooper, logger)
publicip := newPublicIPHandler(publicIPLooper, logger)
portForward := newPortForwardHandler(ctx, pfGetter, logger)
portForward := newPortForwardHandler(ctx, pf, logger)

handler.v0 = newHandlerV0(ctx, logger, vpnLooper, dnsLooper, updaterLooper)
handler.v1 = newHandlerV1(logger, buildInfo, vpn, openvpn, dns, updater, publicip, portForward)
Expand Down
3 changes: 2 additions & 1 deletion internal/server/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ type DNSLoop interface {
GetStatus() (status models.LoopStatus)
}

type PortForwardedGetter interface {
type PortForwarding interface {
GetPortsForwarded() (ports []uint16)
SetPortsForwarded(ports []uint16) (err error)
}

type PublicIPLoop interface {
Expand Down
1 change: 1 addition & 0 deletions internal/server/middlewares/auth/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,4 +133,5 @@ var validRoutes = map[string]struct{}{ //nolint:gochecknoglobals
http.MethodPut + " /v1/updater/status": {},
http.MethodGet + " /v1/publicip/ip": {},
http.MethodGet + " /v1/portforward": {},
http.MethodPut + " /v1/portforward": {},
}
28 changes: 26 additions & 2 deletions internal/server/portforward.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ package server
import (
"context"
"encoding/json"
"fmt"
"net/http"
)

func newPortForwardHandler(ctx context.Context,
portForward PortForwardedGetter, warner warner,
portForward PortForwarding, warner warner,
) http.Handler {
return &portForwardHandler{
ctx: ctx,
Expand All @@ -18,14 +19,16 @@ func newPortForwardHandler(ctx context.Context,

type portForwardHandler struct {
ctx context.Context //nolint:containedctx
portForward PortForwardedGetter
portForward PortForwarding
warner warner
}

func (h *portForwardHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
h.getPortForwarded(w)
case http.MethodPut:
h.setPortForwarded(w, r)
default:
errMethodNotSupported(w, r.Method)
}
Expand All @@ -50,3 +53,24 @@ func (h *portForwardHandler) getPortForwarded(w http.ResponseWriter) {
w.WriteHeader(http.StatusInternalServerError)
}
}

func (h *portForwardHandler) setPortForwarded(w http.ResponseWriter, r *http.Request) {
var data portsWrapper

decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&data)
if err != nil {
h.warner.Warn(fmt.Sprintf("failed setting forwarded ports: %s", err))
http.Error(w, "failed setting forwarded ports", http.StatusBadRequest)
return
}

err = h.portForward.SetPortsForwarded(data.Ports)
if err != nil {
h.warner.Warn(fmt.Sprintf("failed setting forwarded ports: %s", err))
http.Error(w, "failed setting forwarded ports", http.StatusInternalServerError)
return
}

w.WriteHeader(http.StatusOK)
}
4 changes: 2 additions & 2 deletions internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (

func New(ctx context.Context, address string, logEnabled bool, logger Logger,
authConfigPath string, buildInfo models.BuildInformation, openvpnLooper VPNLooper,
pfGetter PortForwardedGetter, dnsLooper DNSLoop,
pf PortForwarding, dnsLooper DNSLoop,
updaterLooper UpdaterLooper, publicIPLooper PublicIPLoop, storage Storage,
ipv6Supported bool) (
server *httpserver.Server, err error,
Expand All @@ -33,7 +33,7 @@ func New(ctx context.Context, address string, logEnabled bool, logger Logger,
}

handler, err := newHandler(ctx, logger, logEnabled, authSettings, buildInfo,
openvpnLooper, pfGetter, dnsLooper, updaterLooper, publicIPLooper,
openvpnLooper, pf, dnsLooper, updaterLooper, publicIPLooper,
storage, ipv6Supported)
if err != nil {
return nil, fmt.Errorf("creating handler: %w", err)
Expand Down