Compare commits
7 Commits
v1.4.2
...
feature/me
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dafbbab2ed | ||
|
|
08a7ccd175 | ||
|
|
8336f6ff29 | ||
|
|
e0078eee0a | ||
|
|
c0063d2c61 | ||
|
|
f74a74cd5e | ||
|
|
35b2c420ec |
@@ -1,5 +1,14 @@
|
||||
# Changelog
|
||||
|
||||
## v1.4.4
|
||||
### Added
|
||||
* **[security]** Adds support for limiting the total number of pids any one container can have active at once to prevent malicious users from impacting other instances on the same node.
|
||||
* Server install containers now use the limits assigned to the server, or a globally defined minimum amount of memory and CPU rather than having unlimited resources.
|
||||
|
||||
## v1.4.3
|
||||
This build was created to address `CVE-2021-33196` in `Go` which requires a new binary
|
||||
be built on the latest `go1.15` version.
|
||||
|
||||
## v1.4.2
|
||||
### Fixed
|
||||
* Fixes the `~` character not being properly trimmed from container image names when creating a new server.
|
||||
|
||||
11
cmd/root.go
11
cmd/root.go
@@ -4,6 +4,7 @@ import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/pterodactyl/wings/metrics"
|
||||
log2 "log"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -137,6 +138,9 @@ func rootCmdRun(cmd *cobra.Command, _ []string) {
|
||||
"gid": config.Get().System.User.Gid,
|
||||
}).Info("configured system user successfully")
|
||||
|
||||
done := make(chan bool)
|
||||
go metrics.Initialize(done)
|
||||
|
||||
pclient := remote.New(
|
||||
config.Get().PanelLocation,
|
||||
remote.WithCredentials(config.Get().AuthenticationTokenId, config.Get().AuthenticationToken),
|
||||
@@ -199,6 +203,12 @@ func rootCmdRun(cmd *cobra.Command, _ []string) {
|
||||
continue
|
||||
}
|
||||
|
||||
if states[s.Id()] == environment.ProcessRunningState {
|
||||
metrics.ServerStatus.WithLabelValues(s.Id()).Set(1)
|
||||
} else {
|
||||
metrics.ServerStatus.WithLabelValues(s.Id()).Set(0)
|
||||
}
|
||||
|
||||
pool.Submit(func() {
|
||||
s.Log().Info("configuring server environment and restoring to previous state")
|
||||
var st string
|
||||
@@ -346,6 +356,7 @@ func rootCmdRun(cmd *cobra.Command, _ []string) {
|
||||
if err := s.ListenAndServe(); err != nil {
|
||||
log.WithField("error", err).Fatal("failed to configure HTTP server")
|
||||
}
|
||||
<-done
|
||||
}
|
||||
|
||||
// Reads the configuration from the disk and then sets up the global singleton
|
||||
|
||||
@@ -91,6 +91,12 @@ type ApiConfiguration struct {
|
||||
UploadLimit int `default:"100" json:"upload_limit" yaml:"upload_limit"`
|
||||
}
|
||||
|
||||
// MetricsConfiguration .
|
||||
type MetricsConfiguration struct {
|
||||
// Bind .
|
||||
Bind string `default:":9000" yaml:"bind"`
|
||||
}
|
||||
|
||||
// RemoteQueryConfiguration defines the configuration settings for remote requests
|
||||
// from Wings to the Panel.
|
||||
type RemoteQueryConfiguration struct {
|
||||
@@ -263,6 +269,7 @@ type Configuration struct {
|
||||
Api ApiConfiguration `json:"api" yaml:"api"`
|
||||
System SystemConfiguration `json:"system" yaml:"system"`
|
||||
Docker DockerConfiguration `json:"docker" yaml:"docker"`
|
||||
Metrics MetricsConfiguration `json:"metrics" yaml:"metrics"`
|
||||
|
||||
// Defines internal throttling configurations for server processes to prevent
|
||||
// someone from running an endless loop that spams data to logs.
|
||||
|
||||
@@ -55,6 +55,21 @@ type DockerConfiguration struct {
|
||||
// utilizes host memory for this value, and that we do not keep track of the space used here
|
||||
// so avoid allocating too much to a server.
|
||||
TmpfsSize uint `default:"100" json:"tmpfs_size" yaml:"tmpfs_size"`
|
||||
|
||||
// ContainerPidLimit sets the total number of processes that can be active in a container
|
||||
// at any given moment. This is a security concern in shared-hosting environments where a
|
||||
// malicious process could create enough processes to cause the host node to run out of
|
||||
// available pids and crash.
|
||||
ContainerPidLimit int64 `default:"256" json:"container_pid_limit" yaml:"container_pid_limit"`
|
||||
|
||||
// InstallLimits defines the limits on the installer containers that prevents a server's
|
||||
// installation process from unintentionally consuming more resources than expected. This
|
||||
// is used in conjunction with the server's defined limits. Whichever value is higher will
|
||||
// take precedence in the install containers.
|
||||
InstallerLimits struct {
|
||||
Memory int64 `default:"1024" json:"memory" yaml:"memory"`
|
||||
Cpu int64 `default:"100" json:"cpu" yaml:"cpu"`
|
||||
} `json:"installer_limits" yaml:"installer_limits"`
|
||||
}
|
||||
|
||||
// RegistryConfiguration defines the authentication credentials for a given
|
||||
|
||||
@@ -132,7 +132,7 @@ func (e *Environment) InSituUpdate() error {
|
||||
//
|
||||
// @see https://github.com/moby/moby/issues/41946
|
||||
if _, err := e.client.ContainerUpdate(ctx, e.Id, container.UpdateConfig{
|
||||
Resources: e.resources(),
|
||||
Resources: e.Configuration.Limits().AsContainerResources(),
|
||||
}); err != nil {
|
||||
return errors.Wrap(err, "environment/docker: could not update container")
|
||||
}
|
||||
@@ -203,7 +203,7 @@ func (e *Environment) Create() error {
|
||||
|
||||
// Define resource limits for the container based on the data passed through
|
||||
// from the Panel.
|
||||
Resources: e.resources(),
|
||||
Resources: e.Configuration.Limits().AsContainerResources(),
|
||||
|
||||
DNS: config.Get().Docker.Network.Dns,
|
||||
|
||||
@@ -486,6 +486,7 @@ func (e *Environment) convertMounts() []mount.Mount {
|
||||
|
||||
func (e *Environment) resources() container.Resources {
|
||||
l := e.Configuration.Limits()
|
||||
pids := l.ProcessLimit()
|
||||
|
||||
return container.Resources{
|
||||
Memory: l.BoundedMemoryLimit(),
|
||||
@@ -497,5 +498,6 @@ func (e *Environment) resources() container.Resources {
|
||||
BlkioWeight: l.IoWeight,
|
||||
OomKillDisable: &l.OOMDisabled,
|
||||
CpusetCpus: l.Threads,
|
||||
PidsLimit: &pids,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package docker
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/pterodactyl/wings/metrics"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
@@ -212,5 +213,15 @@ func (e *Environment) SetState(state string) {
|
||||
// If the state changed make sure we update the internal tracking to note that.
|
||||
e.st.Store(state)
|
||||
e.Events().Publish(environment.StateChangeEvent, state)
|
||||
|
||||
if state == environment.ProcessRunningState || state == environment.ProcessOfflineState {
|
||||
val := 0
|
||||
if state == environment.ProcessRunningState {
|
||||
val = 1
|
||||
} else {
|
||||
metrics.ResetServer(e.Id)
|
||||
}
|
||||
metrics.ServerStatus.WithLabelValues(e.Id).Set(float64(val))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/json"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/pterodactyl/wings/environment"
|
||||
"github.com/pterodactyl/wings/metrics"
|
||||
"io"
|
||||
"math"
|
||||
)
|
||||
@@ -60,6 +61,11 @@ func (e *Environment) pollResources(ctx context.Context) error {
|
||||
st.Network.TxBytes += nw.TxBytes
|
||||
}
|
||||
|
||||
metrics.ServerCPU.WithLabelValues(e.Id).Set(st.CpuAbsolute)
|
||||
metrics.ServerMemory.WithLabelValues(e.Id).Set(float64(st.Memory))
|
||||
metrics.ServerNetworkRx.WithLabelValues(e.Id).Set(float64(st.Network.RxBytes))
|
||||
metrics.ServerNetworkTx.WithLabelValues(e.Id).Set(float64(st.Network.TxBytes))
|
||||
|
||||
if b, err := json.Marshal(st); err != nil {
|
||||
e.log().WithField("error", err).Warn("error while marshaling stats object for environment")
|
||||
} else {
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/pterodactyl/wings/config"
|
||||
)
|
||||
|
||||
type Mount struct {
|
||||
@@ -28,8 +30,8 @@ type Mount struct {
|
||||
ReadOnly bool `json:"read_only"`
|
||||
}
|
||||
|
||||
// The build settings for a given server that impact docker container creation and
|
||||
// resource limits for a server instance.
|
||||
// Limits is the build settings for a given server that impact docker container
|
||||
// creation and resource limits for a server instance.
|
||||
type Limits struct {
|
||||
// The total amount of memory in megabytes that this server is allowed to
|
||||
// use on the host system.
|
||||
@@ -56,51 +58,76 @@ type Limits struct {
|
||||
OOMDisabled bool `json:"oom_disabled"`
|
||||
}
|
||||
|
||||
// Converts the CPU limit for a server build into a number that can be better understood
|
||||
// by the Docker environment. If there is no limit set, return -1 which will indicate to
|
||||
// Docker that it has unlimited CPU quota.
|
||||
func (r *Limits) ConvertedCpuLimit() int64 {
|
||||
if r.CpuLimit == 0 {
|
||||
// ConvertedCpuLimit converts the CPU limit for a server build into a number
|
||||
// that can be better understood by the Docker environment. If there is no limit
|
||||
// set, return -1 which will indicate to Docker that it has unlimited CPU quota.
|
||||
func (l Limits) ConvertedCpuLimit() int64 {
|
||||
if l.CpuLimit == 0 {
|
||||
return -1
|
||||
}
|
||||
|
||||
return r.CpuLimit * 1000
|
||||
return l.CpuLimit * 1000
|
||||
}
|
||||
|
||||
// Set the hard limit for memory usage to be 5% more than the amount of memory assigned to
|
||||
// the server. If the memory limit for the server is < 4G, use 10%, if less than 2G use
|
||||
// 15%. This avoids unexpected crashes from processes like Java which run over the limit.
|
||||
func (r *Limits) MemoryOverheadMultiplier() float64 {
|
||||
if r.MemoryLimit <= 2048 {
|
||||
// MemoryOverheadMultiplier sets the hard limit for memory usage to be 5% more
|
||||
// than the amount of memory assigned to the server. If the memory limit for the
|
||||
// server is < 4G, use 10%, if less than 2G use 15%. This avoids unexpected
|
||||
// crashes from processes like Java which run over the limit.
|
||||
func (l Limits) MemoryOverheadMultiplier() float64 {
|
||||
if l.MemoryLimit <= 2048 {
|
||||
return 1.15
|
||||
} else if r.MemoryLimit <= 4096 {
|
||||
} else if l.MemoryLimit <= 4096 {
|
||||
return 1.10
|
||||
}
|
||||
|
||||
return 1.05
|
||||
}
|
||||
|
||||
func (r *Limits) BoundedMemoryLimit() int64 {
|
||||
return int64(math.Round(float64(r.MemoryLimit) * r.MemoryOverheadMultiplier() * 1_000_000))
|
||||
func (l Limits) BoundedMemoryLimit() int64 {
|
||||
return int64(math.Round(float64(l.MemoryLimit) * l.MemoryOverheadMultiplier() * 1_000_000))
|
||||
}
|
||||
|
||||
// Returns the amount of swap available as a total in bytes. This is returned as the amount
|
||||
// of memory available to the server initially, PLUS the amount of additional swap to include
|
||||
// which is the format used by Docker.
|
||||
func (r *Limits) ConvertedSwap() int64 {
|
||||
if r.Swap < 0 {
|
||||
// ConvertedSwap returns the amount of swap available as a total in bytes. This
|
||||
// is returned as the amount of memory available to the server initially, PLUS
|
||||
// the amount of additional swap to include which is the format used by Docker.
|
||||
func (l Limits) ConvertedSwap() int64 {
|
||||
if l.Swap < 0 {
|
||||
return -1
|
||||
}
|
||||
|
||||
return (r.Swap * 1_000_000) + r.BoundedMemoryLimit()
|
||||
return (l.Swap * 1_000_000) + l.BoundedMemoryLimit()
|
||||
}
|
||||
|
||||
// ProcessLimit returns the process limit for a container. This is currently
|
||||
// defined at a system level and not on a per-server basis.
|
||||
func (l Limits) ProcessLimit() int64 {
|
||||
return config.Get().Docker.ContainerPidLimit
|
||||
}
|
||||
|
||||
func (l Limits) AsContainerResources() container.Resources {
|
||||
pids := l.ProcessLimit()
|
||||
|
||||
return container.Resources{
|
||||
Memory: l.BoundedMemoryLimit(),
|
||||
MemoryReservation: l.MemoryLimit * 1_000_000,
|
||||
MemorySwap: l.ConvertedSwap(),
|
||||
CPUQuota: l.ConvertedCpuLimit(),
|
||||
CPUPeriod: 100_000,
|
||||
CPUShares: 1024,
|
||||
BlkioWeight: l.IoWeight,
|
||||
OomKillDisable: &l.OOMDisabled,
|
||||
CpusetCpus: l.Threads,
|
||||
PidsLimit: &pids,
|
||||
}
|
||||
}
|
||||
|
||||
type Variables map[string]interface{}
|
||||
|
||||
// Ugly hacky function to handle environment variables that get passed through as not-a-string
|
||||
// from the Panel. Ideally we'd just say only pass strings, but that is a fragile idea and if a
|
||||
// string wasn't passed through you'd cause a crash or the server to become unavailable. For now
|
||||
// try to handle the most likely values from the JSON and hope for the best.
|
||||
// Get is an ugly hacky function to handle environment variables that get passed
|
||||
// through as not-a-string from the Panel. Ideally we'd just say only pass
|
||||
// strings, but that is a fragile idea and if a string wasn't passed through
|
||||
// you'd cause a crash or the server to become unavailable. For now try to
|
||||
// handle the most likely values from the JSON and hope for the best.
|
||||
func (v Variables) Get(key string) string {
|
||||
val, ok := v[key]
|
||||
if !ok {
|
||||
|
||||
107
metrics/metrics.go
Normal file
107
metrics/metrics.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"github.com/apex/log"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/pterodactyl/wings/config"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Metrics struct {
|
||||
handler http.Handler
|
||||
}
|
||||
|
||||
const (
|
||||
namespace = "pterodactyl"
|
||||
subsystem = "wings"
|
||||
)
|
||||
|
||||
var (
|
||||
bootTimeSeconds = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "boot_time_seconds",
|
||||
Help: "Boot time of this instance since epoch (1970)",
|
||||
})
|
||||
timeSeconds = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "time_seconds",
|
||||
Help: "System time in seconds since epoch (1970)",
|
||||
})
|
||||
|
||||
ServerStatus = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "server_status",
|
||||
}, []string{"server_id"})
|
||||
ServerCPU = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "server_cpu",
|
||||
}, []string{"server_id"})
|
||||
ServerMemory = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "server_memory",
|
||||
}, []string{"server_id"})
|
||||
ServerNetworkRx = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "server_network_rx",
|
||||
}, []string{"server_id"})
|
||||
ServerNetworkTx = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "server_network_tx",
|
||||
}, []string{"server_id"})
|
||||
|
||||
HTTPRequestsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "http_requests_total",
|
||||
}, []string{"method", "route_path", "raw_path", "raw_query", "code"})
|
||||
)
|
||||
|
||||
func Initialize(done chan bool) {
|
||||
bootTimeSeconds.Set(float64(time.Now().UnixNano()) / 1e9)
|
||||
ticker := time.NewTicker(time.Second)
|
||||
go func() {
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
// Received a "signal" on the done channel.
|
||||
log.Debug("metrics: done")
|
||||
return
|
||||
case t := <-ticker.C:
|
||||
// Update the current time.
|
||||
timeSeconds.Set(float64(t.UnixNano()) / 1e9)
|
||||
}
|
||||
}
|
||||
}()
|
||||
if err := http.ListenAndServe(config.Get().Metrics.Bind, promhttp.Handler()); err != nil && err != http.ErrServerClosed {
|
||||
log.WithField("error", err).Error("failed to start metrics server")
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteServer will remove any existing labels from being scraped by Prometheus.
|
||||
// Any previously scraped data will still be persisted by Prometheus.
|
||||
func DeleteServer(sID string) {
|
||||
ServerStatus.DeleteLabelValues(sID)
|
||||
ServerCPU.DeleteLabelValues(sID)
|
||||
ServerMemory.DeleteLabelValues(sID)
|
||||
ServerNetworkRx.DeleteLabelValues(sID)
|
||||
ServerNetworkTx.DeleteLabelValues(sID)
|
||||
}
|
||||
|
||||
// ResetServer will reset a server's metrics to their default values except the status.
|
||||
func ResetServer(sID string) {
|
||||
ServerCPU.WithLabelValues(sID).Set(0)
|
||||
ServerMemory.WithLabelValues(sID).Set(0)
|
||||
ServerNetworkRx.WithLabelValues(sID).Set(0)
|
||||
ServerNetworkTx.WithLabelValues(sID).Set(0)
|
||||
}
|
||||
@@ -3,9 +3,11 @@ package middleware
|
||||
import (
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
"github.com/pterodactyl/wings/metrics"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"emperror.dev/errors"
|
||||
@@ -352,3 +354,19 @@ func ExtractManager(c *gin.Context) *server.Manager {
|
||||
}
|
||||
panic("middleware/middleware: cannot extract server manager: not present in context")
|
||||
}
|
||||
|
||||
func Metrics() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
path := c.Request.URL.Path
|
||||
rawQuery := c.Request.URL.RawQuery
|
||||
|
||||
c.Next()
|
||||
|
||||
// Skip over the server websocket endpoint.
|
||||
if strings.HasSuffix(c.FullPath(), "/ws") {
|
||||
return
|
||||
}
|
||||
|
||||
metrics.HTTPRequestsTotal.WithLabelValues(c.Request.Method, c.FullPath(), path, rawQuery, strconv.Itoa(c.Writer.Status())).Inc()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ func Configure(m *server.Manager, client remote.Client) *gin.Engine {
|
||||
|
||||
router := gin.New()
|
||||
router.Use(gin.Recovery())
|
||||
router.Use(middleware.Metrics())
|
||||
router.Use(middleware.AttachRequestID(), middleware.CaptureErrors(), middleware.SetAccessControlHeaders())
|
||||
router.Use(middleware.AttachServerManager(m), middleware.AttachApiClient(client))
|
||||
// @todo log this into a different file so you can setup IP blocking for abusive requests and such.
|
||||
|
||||
55
server/filesystem/compress_test.go
Normal file
55
server/filesystem/compress_test.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package filesystem
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
. "github.com/franela/goblin"
|
||||
)
|
||||
|
||||
// Given an archive named test.{ext}, with the following file structure:
|
||||
// test/
|
||||
// |──inside/
|
||||
// |────finside.txt
|
||||
// |──outside.txt
|
||||
// this test will ensure that it's being decompressed as expected
|
||||
func TestFilesystem_DecompressFile(t *testing.T) {
|
||||
g := Goblin(t)
|
||||
fs, rfs := NewFs()
|
||||
|
||||
g.Describe("Decompress", func() {
|
||||
|
||||
for _, ext := range []string{"zip", "rar", "tar", "tar.gz"} {
|
||||
g.It("can decompress a "+ext, func() {
|
||||
// copy the file to the new FS
|
||||
c, err := ioutil.ReadFile("./testdata/test." + ext)
|
||||
g.Assert(err).IsNil()
|
||||
err = rfs.CreateServerFile("./test."+ext, c)
|
||||
g.Assert(err).IsNil()
|
||||
|
||||
// decompress
|
||||
err = fs.DecompressFile("/", "test."+ext)
|
||||
g.Assert(err).IsNil()
|
||||
|
||||
// make sure everything is where it is supposed to be
|
||||
_, err = rfs.StatServerFile("test/outside.txt")
|
||||
g.Assert(err).IsNil()
|
||||
|
||||
st, err := rfs.StatServerFile("test/inside")
|
||||
g.Assert(err).IsNil()
|
||||
g.Assert(st.IsDir()).IsTrue()
|
||||
|
||||
_, err = rfs.StatServerFile("test/inside/finside.txt")
|
||||
g.Assert(err).IsNil()
|
||||
g.Assert(st.IsDir()).IsTrue()
|
||||
})
|
||||
}
|
||||
|
||||
g.AfterEach(func() {
|
||||
rfs.reset()
|
||||
atomic.StoreInt64(&fs.diskUsed, 0)
|
||||
atomic.StoreInt64(&fs.diskLimit, 0)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -44,17 +44,21 @@ type rootFs struct {
|
||||
root string
|
||||
}
|
||||
|
||||
func (rfs *rootFs) CreateServerFile(p string, c string) error {
|
||||
func (rfs *rootFs) CreateServerFile(p string, c []byte) error {
|
||||
f, err := os.Create(filepath.Join(rfs.root, "/server", p))
|
||||
|
||||
if err == nil {
|
||||
f.Write([]byte(c))
|
||||
f.Write(c)
|
||||
f.Close()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (rfs *rootFs) CreateServerFileFromString(p string, c string) error {
|
||||
return rfs.CreateServerFile(p, []byte(c))
|
||||
}
|
||||
|
||||
func (rfs *rootFs) StatServerFile(p string) (os.FileInfo, error) {
|
||||
return os.Stat(filepath.Join(rfs.root, "/server", p))
|
||||
}
|
||||
@@ -79,7 +83,7 @@ func TestFilesystem_Readfile(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
g.It("opens a file if it exists on the system", func() {
|
||||
err := rfs.CreateServerFile("test.txt", "testing")
|
||||
err := rfs.CreateServerFileFromString("test.txt", "testing")
|
||||
g.Assert(err).IsNil()
|
||||
|
||||
err = fs.Readfile("test.txt", buf)
|
||||
@@ -103,7 +107,7 @@ func TestFilesystem_Readfile(t *testing.T) {
|
||||
})
|
||||
|
||||
g.It("cannot open a file outside the root directory", func() {
|
||||
err := rfs.CreateServerFile("/../test.txt", "testing")
|
||||
err := rfs.CreateServerFileFromString("/../test.txt", "testing")
|
||||
g.Assert(err).IsNil()
|
||||
|
||||
err = fs.Readfile("/../test.txt", buf)
|
||||
@@ -281,13 +285,13 @@ func TestFilesystem_Rename(t *testing.T) {
|
||||
|
||||
g.Describe("Rename", func() {
|
||||
g.BeforeEach(func() {
|
||||
if err := rfs.CreateServerFile("source.txt", "text content"); err != nil {
|
||||
if err := rfs.CreateServerFileFromString("source.txt", "text content"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
|
||||
g.It("returns an error if the target already exists", func() {
|
||||
err := rfs.CreateServerFile("target.txt", "taget content")
|
||||
err := rfs.CreateServerFileFromString("target.txt", "taget content")
|
||||
g.Assert(err).IsNil()
|
||||
|
||||
err = fs.Rename("source.txt", "target.txt")
|
||||
@@ -314,7 +318,7 @@ func TestFilesystem_Rename(t *testing.T) {
|
||||
})
|
||||
|
||||
g.It("does not allow renaming from a location outside the root", func() {
|
||||
err := rfs.CreateServerFile("/../ext-source.txt", "taget content")
|
||||
err := rfs.CreateServerFileFromString("/../ext-source.txt", "taget content")
|
||||
|
||||
err = fs.Rename("/../ext-source.txt", "target.txt")
|
||||
g.Assert(err).IsNotNil()
|
||||
@@ -378,7 +382,7 @@ func TestFilesystem_Copy(t *testing.T) {
|
||||
|
||||
g.Describe("Copy", func() {
|
||||
g.BeforeEach(func() {
|
||||
if err := rfs.CreateServerFile("source.txt", "text content"); err != nil {
|
||||
if err := rfs.CreateServerFileFromString("source.txt", "text content"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -392,7 +396,7 @@ func TestFilesystem_Copy(t *testing.T) {
|
||||
})
|
||||
|
||||
g.It("should return an error if the source is outside the root", func() {
|
||||
err := rfs.CreateServerFile("/../ext-source.txt", "text content")
|
||||
err := rfs.CreateServerFileFromString("/../ext-source.txt", "text content")
|
||||
|
||||
err = fs.Copy("../ext-source.txt")
|
||||
g.Assert(err).IsNotNil()
|
||||
@@ -403,7 +407,7 @@ func TestFilesystem_Copy(t *testing.T) {
|
||||
err := os.MkdirAll(filepath.Join(rfs.root, "/nested/in/dir"), 0755)
|
||||
g.Assert(err).IsNil()
|
||||
|
||||
err = rfs.CreateServerFile("/../nested/in/dir/ext-source.txt", "external content")
|
||||
err = rfs.CreateServerFileFromString("/../nested/in/dir/ext-source.txt", "external content")
|
||||
g.Assert(err).IsNil()
|
||||
|
||||
err = fs.Copy("../nested/in/dir/ext-source.txt")
|
||||
@@ -464,7 +468,7 @@ func TestFilesystem_Copy(t *testing.T) {
|
||||
err := os.MkdirAll(filepath.Join(rfs.root, "/server/nested/in/dir"), 0755)
|
||||
g.Assert(err).IsNil()
|
||||
|
||||
err = rfs.CreateServerFile("nested/in/dir/source.txt", "test content")
|
||||
err = rfs.CreateServerFileFromString("nested/in/dir/source.txt", "test content")
|
||||
g.Assert(err).IsNil()
|
||||
|
||||
err = fs.Copy("nested/in/dir/source.txt")
|
||||
@@ -492,7 +496,7 @@ func TestFilesystem_Delete(t *testing.T) {
|
||||
|
||||
g.Describe("Delete", func() {
|
||||
g.BeforeEach(func() {
|
||||
if err := rfs.CreateServerFile("source.txt", "test content"); err != nil {
|
||||
if err := rfs.CreateServerFileFromString("source.txt", "test content"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -500,7 +504,7 @@ func TestFilesystem_Delete(t *testing.T) {
|
||||
})
|
||||
|
||||
g.It("does not delete files outside the root directory", func() {
|
||||
err := rfs.CreateServerFile("/../ext-source.txt", "external content")
|
||||
err := rfs.CreateServerFileFromString("/../ext-source.txt", "external content")
|
||||
|
||||
err = fs.Delete("../ext-source.txt")
|
||||
g.Assert(err).IsNotNil()
|
||||
@@ -544,7 +548,7 @@ func TestFilesystem_Delete(t *testing.T) {
|
||||
g.Assert(err).IsNil()
|
||||
|
||||
for _, s := range sources {
|
||||
err = rfs.CreateServerFile(s, "test content")
|
||||
err = rfs.CreateServerFileFromString(s, "test content")
|
||||
g.Assert(err).IsNil()
|
||||
}
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ func TestFilesystem_Blocks_Symlinks(t *testing.T) {
|
||||
g := Goblin(t)
|
||||
fs, rfs := NewFs()
|
||||
|
||||
if err := rfs.CreateServerFile("/../malicious.txt", "external content"); err != nil {
|
||||
if err := rfs.CreateServerFileFromString("/../malicious.txt", "external content"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -181,7 +181,7 @@ func TestFilesystem_Blocks_Symlinks(t *testing.T) {
|
||||
})
|
||||
|
||||
g.It("cannot rename a file to a location outside the directory root", func() {
|
||||
rfs.CreateServerFile("my_file.txt", "internal content")
|
||||
rfs.CreateServerFileFromString("my_file.txt", "internal content")
|
||||
|
||||
err := fs.Rename("my_file.txt", "external_dir/my_file.txt")
|
||||
g.Assert(err).IsNotNil()
|
||||
|
||||
BIN
server/filesystem/testdata/test.rar
vendored
Normal file
BIN
server/filesystem/testdata/test.rar
vendored
Normal file
Binary file not shown.
BIN
server/filesystem/testdata/test.tar
vendored
Normal file
BIN
server/filesystem/testdata/test.tar
vendored
Normal file
Binary file not shown.
BIN
server/filesystem/testdata/test.tar.gz
vendored
Normal file
BIN
server/filesystem/testdata/test.tar.gz
vendored
Normal file
Binary file not shown.
BIN
server/filesystem/testdata/test.zip
vendored
Normal file
BIN
server/filesystem/testdata/test.zip
vendored
Normal file
Binary file not shown.
@@ -434,6 +434,7 @@ func (ip *InstallationProcess) Execute() (string, error) {
|
||||
ReadOnly: false,
|
||||
},
|
||||
},
|
||||
Resources: ip.resourceLimits(),
|
||||
Tmpfs: map[string]string{
|
||||
"/tmp": "rw,exec,nosuid,size=" + tmpfsSize + "M",
|
||||
},
|
||||
@@ -530,6 +531,43 @@ func (ip *InstallationProcess) StreamOutput(ctx context.Context, id string) erro
|
||||
return nil
|
||||
}
|
||||
|
||||
// resourceLimits returns the install container specific resource limits. This
|
||||
// looks at the globally defined install container limits and attempts to use
|
||||
// the higher of the two (defined limits & server limits). This allows for servers
|
||||
// with super low limits (e.g. Discord bots with 128Mb of memory) to perform more
|
||||
// intensive installation processes if needed.
|
||||
//
|
||||
// This also avoids a server with limits such as 4GB of memory from accidentally
|
||||
// consuming 2-5x the defined limits during the install process and causing
|
||||
// system instability.
|
||||
func (ip *InstallationProcess) resourceLimits() container.Resources {
|
||||
limits := config.Get().Docker.InstallerLimits
|
||||
|
||||
// Create a copy of the configuration so we're not accidentally making changes
|
||||
// to the underlying server build data.
|
||||
c := *ip.Server.Config()
|
||||
cfg := c.Build
|
||||
if cfg.MemoryLimit < limits.Memory {
|
||||
cfg.MemoryLimit = limits.Memory
|
||||
}
|
||||
// Only apply the CPU limit if neither one is currently set to unlimited. If the
|
||||
// installer CPU limit is unlimited don't even waste time with the logic, just
|
||||
// set the config to unlimited for this.
|
||||
if limits.Cpu == 0 {
|
||||
cfg.CpuLimit = 0
|
||||
} else if cfg.CpuLimit != 0 && cfg.CpuLimit < limits.Cpu {
|
||||
cfg.CpuLimit = limits.Cpu
|
||||
}
|
||||
|
||||
resources := cfg.AsContainerResources()
|
||||
// Explicitly remove the PID limits for the installation container. These scripts are
|
||||
// defined at an administrative level and users can't manually execute things like a
|
||||
// fork bomb during this process.
|
||||
resources.PidsLimit = nil
|
||||
|
||||
return resources
|
||||
}
|
||||
|
||||
// SyncInstallState makes a HTTP request to the Panel instance notifying it that
|
||||
// the server has completed the installation process, and what the state of the
|
||||
// server is. A boolean value of "true" means everything was successful, "false"
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/pterodactyl/wings/metrics"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
@@ -72,6 +73,9 @@ func (m *Manager) Add(s *Server) {
|
||||
m.mu.Lock()
|
||||
m.servers = append(m.servers, s)
|
||||
m.mu.Unlock()
|
||||
|
||||
// Add the server to the metrics with a offline status.
|
||||
metrics.ServerStatus.WithLabelValues(s.Id()).Set(0)
|
||||
}
|
||||
|
||||
// Get returns a single server instance and a boolean value indicating if it was
|
||||
@@ -117,6 +121,9 @@ func (m *Manager) Remove(filter func(match *Server) bool) {
|
||||
for _, v := range m.servers {
|
||||
if !filter(v) {
|
||||
r = append(r, v)
|
||||
} else {
|
||||
// Delete the server from the metric.
|
||||
metrics.DeleteServer(v.Id())
|
||||
}
|
||||
}
|
||||
m.servers = r
|
||||
|
||||
Reference in New Issue
Block a user