diff --git a/Dockerfile b/Dockerfile index ead7d213..f8b03bdd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -85,7 +85,7 @@ ENV VPNSP="private internet access" \ SHADOWSOCKS_PASSWORD= \ SHADOWSOCKS_METHOD=chacha20-ietf-poly1305 ENTRYPOINT /entrypoint -EXPOSE 8888/tcp 8388/tcp 8388/udp +EXPOSE 8000/tcp 8888/tcp 8388/tcp 8388/udp HEALTHCHECK --interval=3m --timeout=3s --start-period=20s --retries=1 CMD /entrypoint healthcheck RUN apk add -q --progress --no-cache --update openvpn ca-certificates iptables unbound tinyproxy tzdata && \ echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories && \ diff --git a/README.md b/README.md index 9427432d..d23c2db1 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,7 @@ - Change the many [environment variables](#environment-variables) available - Use `-p 8888:8888/tcp` to access the HTTP web proxy (and put your LAN in `EXTRA_SUBNETS` environment variable, in example `192.168.1.0/24`) - Use `-p 8388:8388/tcp -p 8388:8388/udp` to access the SOCKS5 proxy (and put your LAN in `EXTRA_SUBNETS` environment variable, in example `192.168.1.0/24`) + - Use `-p 8000:8000/tcp` to access the [HTTP control server](#HTTP-control-server) built-in - Pass additional arguments to *openvpn* using Docker's command function (commands after the image name) 1. You can update the image with `docker pull qmcgaw/private-internet-access:latest`. There are also docker tags for older versions available: @@ -260,6 +261,12 @@ When `PORT_FORWARDING=on`, a port will be forwarded on the PIA server side and w It can be useful to mount this file as a volume to read it from other containers, for example to configure a torrenting client. +## HTTP control server + +A built-in HTTP server listens on port `8000` to modify the state of the container. You have the following routes available: + +- `http://:8000/openvpn/actions/restart` restarts the openvpn process + ## FAQ Please refer to [the FAQ table of content](https://github.com/qdm12/private-internet-access-docker/blob/master/doc/faq.md#Table-of-content) diff --git a/cmd/main.go b/cmd/main.go index c45714fb..a4ec4eca 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -27,6 +27,7 @@ import ( "github.com/qdm12/private-internet-access-docker/internal/params" "github.com/qdm12/private-internet-access-docker/internal/pia" "github.com/qdm12/private-internet-access-docker/internal/routing" + "github.com/qdm12/private-internet-access-docker/internal/server" "github.com/qdm12/private-internet-access-docker/internal/settings" "github.com/qdm12/private-internet-access-docker/internal/shadowsocks" "github.com/qdm12/private-internet-access-docker/internal/splash" @@ -267,13 +268,20 @@ func main() { go streamMerger.Merge(ctx, stdout, command.MergeName("shadowsocks"), command.MergeColor(constants.ColorShadowsocks())) go streamMerger.Merge(ctx, stderr, command.MergeName("shadowsocks error"), command.MergeColor(constants.ColorShadowsocksError())) } + + httpServer := server.New("0.0.0.0:8000", logger) + // Runs openvpn and restarts it if it does not exit cleanly + openvpnCancelSet, signalOpenvpnCancelSet := context.WithCancel(context.Background()) go func() { waitErrors := make(chan error) for { - stream, waitFn, err := ovpnConf.Start(ctx) + openvpnCtx, openvpnCancel := context.WithCancel(ctx) + stream, waitFn, err := ovpnConf.Start(openvpnCtx) e.FatalOnError(err) - go streamMerger.Merge(ctx, stream, command.MergeName("openvpn"), command.MergeColor(constants.ColorOpenvpn())) + httpServer.SetOpenVPNRestart(openvpnCancel) + signalOpenvpnCancelSet() + go streamMerger.Merge(openvpnCtx, stream, command.MergeName("openvpn"), command.MergeColor(constants.ColorOpenvpn())) waiter.Add(func() error { err := <-waitErrors logger.Error("openvpn: %s", err) @@ -284,8 +292,18 @@ func main() { } else { break } + openvpnCancel() } }() + + <-openvpnCancelSet.Done() + + waiter.Add(func() error { + err := httpServer.Run(ctx) + logger.Error("http server: %s", err) + return err + }) + signalsCh := make(chan os.Signal, 1) signal.Notify(signalsCh, syscall.SIGINT, diff --git a/doc/firewall.md b/doc/firewall.md index d1f387d2..b7c21bd4 100644 --- a/doc/firewall.md +++ b/doc/firewall.md @@ -37,3 +37,4 @@ You need the following to allow communicating with the VPN servers - If `SHADOWSOCKS=on`, allow inbound TCP 8388 and UDP 8388 from your LAN - If `TINYPROXY=on`, allow inbound TCP 8888 from your LAN +- If you want access to the built-in HTTP control server, allow inbound TCP 8000 from your LAN diff --git a/docker-compose.yml b/docker-compose.yml index 16807811..89cf73a0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,9 +8,10 @@ services: network_mode: bridge init: true ports: - - 8888:8888/tcp - - 8388:8388/tcp - - 8388:8388/udp + - 8888:8888/tcp # Tinyproxy + - 8388:8388/tcp # Shadowsocks + - 8388:8388/udp # Shadowsocks + - 8000:8000/tcp # Built-in HTTP control server # command: environment: # More variables are available, see the readme table diff --git a/internal/server/server.go b/internal/server/server.go new file mode 100644 index 00000000..a1a8996f --- /dev/null +++ b/internal/server/server.go @@ -0,0 +1,93 @@ +package server + +import ( + "context" + "fmt" + "net/http" + "sync" + "time" + + "github.com/qdm12/golibs/logging" +) + +type Server interface { + SetOpenVPNRestart(f func()) + Run(ctx context.Context) error +} + +type server struct { + address string + logger logging.Logger + restartOpenvpn func() + sync.RWMutex +} + +func New(address string, logger logging.Logger) Server { + return &server{ + address: address, + logger: logger.WithPrefix("http server: "), + } +} + +func (s *server) Run(ctx context.Context) error { + if s.restartOpenvpn == nil { + s.logger.Warn("restartOpenvpn function is not set") + } + server := http.Server{Addr: s.address, Handler: s.makeHandler()} + go func() { + <-ctx.Done() + shutdownCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + if err := server.Shutdown(shutdownCtx); err != nil { + s.logger.Error("failed shutting down: %s", err) + } + }() + s.logger.Info("listening on %s", s.address) + return server.ListenAndServe() +} + +func (s *server) SetOpenVPNRestart(f func()) { + s.Lock() + defer s.Unlock() + s.restartOpenvpn = f +} + +func (s *server) makeHandler() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + s.logger.Info("HTTP %s %s", r.Method, r.RequestURI) + switch r.Method { + case http.MethodGet: + switch r.RequestURI { + case "/openvpn/actions/restart": + s.RLock() + defer s.RUnlock() + if s.restartOpenvpn == nil { + functionNotSet("restartOpenvpn", s.logger, w) + return + } + s.restartOpenvpn() + default: + routeDoesNotExist(s.logger, w, r) + } + default: + routeDoesNotExist(s.logger, w, r) + } + } +} + +func routeDoesNotExist(logger logging.Logger, w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusBadRequest) + _, err := w.Write([]byte(fmt.Sprintf("Nothing here for %s %s", r.Method, r.RequestURI))) + if err != nil { + logger.Error(err) + } +} + +func functionNotSet(functionName string, logger logging.Logger, w http.ResponseWriter) { + logger.Error("function %s is not set", functionName) + w.WriteHeader(http.StatusInternalServerError) + _, err := w.Write([]byte(fmt.Sprintf("%s function is not set", functionName))) + if err != nil { + logger.Error(err) + } +}