Compare commits

...

16 Commits

Author SHA1 Message Date
Dane Everitt
93417dddb1 Update CHANGELOG.md 2021-01-08 21:23:25 -08:00
Dane Everitt
044c46fc9a Merge branch 'develop' of https://github.com/pterodactyl/wings into develop 2021-01-08 21:21:37 -08:00
Dane Everitt
c9d972d544 Revert usage of ContainerWait, return to io.Copy blocking
Until https://github.com/moby/moby/issues/41827 is resolved this code causes chaos to unfold on machines and causes servers to be non-terminatable.

This logic was intially changed to logical purposes, but this io.Copy logic works perfectly fine (even if not immediately intuitive).
2021-01-08 21:21:09 -08:00
Matthew Penner
0aab4b1ac2 environment(docker): re-attach to container logs after EOF 2021-01-08 08:19:33 -07:00
Matthew Penner
4f4b4fd2e6 environment(docker): cleanup code 2021-01-08 08:15:40 -07:00
Matthew Penner
66c9be357c Potential fix for servers being marked as stopping after being marked as offline 2021-01-07 19:32:15 -07:00
Matthew Penner
1d36811dfe Fix v being shown twice on wings boot 2021-01-07 16:44:09 -07:00
Dane Everitt
6e74123c65 Update CHANGELOG.md 2021-01-06 21:42:09 -08:00
Dane Everitt
b82f5f9a32 [security] deny downloading files from internal locations 2021-01-06 21:34:18 -08:00
Dane Everitt
1937d0366d cleanup; fix environment stats not reporting network TX correctly 2021-01-06 20:47:44 -08:00
Dane Everitt
963a906c30 Less obtuse logic for polling resource usage when attaching a container 2021-01-06 20:36:29 -08:00
Jakob
3f6eb7e41a no need for additional decode (#81)
file paths used to be url-encoded twice, which is no longer the case.
2021-01-03 17:20:16 -08:00
Omar Kamel
a822c7c340 typo in docker-compose file (#82)
minor typo i noticed while messing around
2021-01-03 16:24:28 -08:00
Matthew Penner
b8fb86f5a4 Update Dockerfile to use busybox 1.33.0 2021-01-03 12:46:06 -07:00
Matthew Penner
ee0c7f09b3 Fix user problems when running inside of Docker 2021-01-02 12:58:58 -07:00
Matthew Penner
d3ddf8cf39 Mark server as not transferring after archive failure 2021-01-02 10:11:25 -07:00
18 changed files with 300 additions and 158 deletions

View File

@@ -48,3 +48,9 @@ debug
.DS_Store .DS_Store
*.pprof *.pprof
*.pdf *.pdf
Dockerfile
CHANGELOG.md
Makefile
README.md
wings-api.paw

View File

@@ -1,5 +1,20 @@
# Changelog # Changelog
## v1.2.2
### Fixed
* Reverts changes to logic handling blocking until a server process is done running when polling stats. This change exposed a bug in the underlying Docker system causing servers to enter a state in which Wings was unable to terminate the process and Docker commands would hang if executed against the container.
## v1.2.1
### Fixed
* Fixes servers not be properly marked as no longer transfering if an error occurs during the archive process.
* Fixes problems with user detection when running Wings inside a Docker container.
* Fixes filename decoding issues with multiple endpoints related to the file manager (namely move/copy/delete).
* **[Security]** Fixes vulnerability allowing a malicious user to abuse the remote file download utilitity to scan or access resources on the local network.
* Fixes network `tx` stats not correctly being reported (was previously reporting `rx` for both `rx` and `tx`).
### Changed
* Cleans up the logic related to polling resources for the server to make a little more sense and not do pointless `io.Copy()` operations.
## v1.2.0 ## v1.2.0
### Fixed ### Fixed
* Fixes log compression being set on the Docker containers being created to avoid errors on some versions of Docker. * Fixes log compression being set on the Docker containers being created to avoid errors on some versions of Docker.

View File

@@ -24,16 +24,9 @@ RUN upx wings
# --------------------------------------- # # --------------------------------------- #
# Stage 2 (Final) # Stage 2 (Final)
FROM busybox:1.32.0 FROM busybox:1.33.0
LABEL org.opencontainers.image.title="Wings" RUN echo "ID=\"busybox\"" > /etc/os-release
LABEL org.opencontainers.image.version="$VERSION"
LABEL org.opencontainers.image.description="The server control plane for Pterodactyl Panel. Written from the ground-up with security, speed, and stability in mind."
LABEL org.opencontainers.image.url="https://pterodactyl.io"
LABEL org.opencontainers.image.documentation="https://pterodactyl.io/project/introduction.html"
LABEL org.opencontainers.image.vendor="Pterodactyl Software"
LABEL org.opencontainers.image.source="https://github.com/pterodactyl/wings"
LABEL org.opencontainers.image.licenses="MIT"
COPY --from=builder /app/wings /usr/bin/ COPY --from=builder /app/wings /usr/bin/

View File

@@ -406,7 +406,7 @@ __ [blue][bold]Pterodactyl[reset] _____/___/_______ _______ ______
\_____\ \/\/ / / / __ / ___/ \_____\ \/\/ / / / __ / ___/
\___\ / / / / /_/ /___ / \___\ / / / / /_/ /___ /
\___/\___/___/___/___/___ /______/ \___/\___/___/___/___/___ /______/
/_______/ [bold]v%s[reset] /_______/ [bold]%s[reset]
Copyright © 2018 - 2021 Dane Everitt & Contributors Copyright © 2018 - 2021 Dane Everitt & Contributors

View File

@@ -223,6 +223,36 @@ func (c *Configuration) GetPath() string {
// If files are not owned by this user there will be issues with permissions on Docker // If files are not owned by this user there will be issues with permissions on Docker
// mount points. // mount points.
func (c *Configuration) EnsurePterodactylUser() (*user.User, error) { func (c *Configuration) EnsurePterodactylUser() (*user.User, error) {
sysName, err := getSystemName()
if err != nil {
return nil, err
}
// Our way of detecting if wings is running inside of Docker.
if sysName == "busybox" {
uid := os.Getenv("WINGS_UID")
if uid == "" {
uid = "988"
}
gid := os.Getenv("WINGS_GID")
if gid == "" {
gid = "988"
}
username := os.Getenv("WINGS_USERNAME")
if username == "" {
username = "pterodactyl"
}
u := &user.User{
Uid: uid,
Gid: gid,
Username: username,
}
return u, c.setSystemUser(u)
}
u, err := user.Lookup(c.System.Username) u, err := user.Lookup(c.System.Username)
// If an error is returned but it isn't the unknown user error just abort // If an error is returned but it isn't the unknown user error just abort
@@ -233,17 +263,12 @@ func (c *Configuration) EnsurePterodactylUser() (*user.User, error) {
return nil, err return nil, err
} }
sysName, err := getSystemName() command := fmt.Sprintf("useradd --system --no-create-home --shell /usr/sbin/nologin %s", c.System.Username)
if err != nil {
return nil, err
}
command := fmt.Sprintf("useradd --system --no-create-home --shell /bin/false %s", c.System.Username)
// Alpine Linux is the only OS we currently support that doesn't work with the useradd command, so // Alpine Linux is the only OS we currently support that doesn't work with the useradd command, so
// in those cases we just modify the command a bit to work as expected. // in those cases we just modify the command a bit to work as expected.
if strings.HasPrefix(sysName, "alpine") { if strings.HasPrefix(sysName, "alpine") {
command = fmt.Sprintf("adduser -S -D -H -G %[1]s -s /bin/false %[1]s", c.System.Username) command = fmt.Sprintf("adduser -S -D -H -G %[1]s -s /sbin/nologin %[1]s", c.System.Username)
// We have to create the group first on Alpine, so do that here before continuing on // We have to create the group first on Alpine, so do that here before continuing on
// to the user creation process. // to the user creation process.
@@ -267,8 +292,15 @@ func (c *Configuration) EnsurePterodactylUser() (*user.User, error) {
// Set the system user into the configuration and then write it to the disk so that // Set the system user into the configuration and then write it to the disk so that
// it is persisted on boot. // it is persisted on boot.
func (c *Configuration) setSystemUser(u *user.User) error { func (c *Configuration) setSystemUser(u *user.User) error {
uid, _ := strconv.Atoi(u.Uid) uid, err := strconv.Atoi(u.Uid)
gid, _ := strconv.Atoi(u.Gid) if err != nil {
return err
}
gid, err := strconv.Atoi(u.Gid)
if err != nil {
return err
}
c.Lock() c.Lock()
c.System.Username = u.Username c.System.Username = u.Username

View File

@@ -1,4 +1,5 @@
version: '3.8' version: '3.8'
services: services:
wings: wings:
image: ghcr.io/pterodactyl/wings:latest image: ghcr.io/pterodactyl/wings:latest
@@ -11,7 +12,9 @@ services:
tty: true tty: true
environment: environment:
TZ: "UTC" TZ: "UTC"
DEBUG: "false" WINGS_UID: 988
WINGS_GID: 988
WINGS_USERNAME: pterodactyl
volumes: volumes:
- "/var/run/docker.sock:/var/run/docker.sock" - "/var/run/docker.sock:/var/run/docker.sock"
- "/var/lib/docker/containers/:/var/lib/docker/containers/" - "/var/lib/docker/containers/:/var/lib/docker/containers/"
@@ -21,8 +24,9 @@ services:
- "/tmp/pterodactyl/:/tmp/pterodactyl/" - "/tmp/pterodactyl/:/tmp/pterodactyl/"
# you may need /srv/daemon-data if you are upgrading from an old daemon # you may need /srv/daemon-data if you are upgrading from an old daemon
#- "/srv/daemon-data/:/srv/daemon-data/" #- "/srv/daemon-data/:/srv/daemon-data/"
# Required for ssl if you user let's encrypt. uncomment to use. # Required for ssl if you use let's encrypt. uncomment to use.
#- "/etc/letsencrypt/:/etc/letsencrypt/" #- "/etc/letsencrypt/:/etc/letsencrypt/"
networks: networks:
wings0: wings0:
name: wings0 name: wings0
@@ -31,4 +35,4 @@ networks:
config: config:
- subnet: "172.21.0.0/16" - subnet: "172.21.0.0/16"
driver_opts: driver_opts:
com.docker.network.bridge.name: wings0 com.docker.network.bridge.name: wings0

View File

@@ -1,20 +0,0 @@
package docker
import "io"
type Console struct {
HandlerFunc *func(string)
}
var _ io.Writer = Console{}
func (c Console) Write(b []byte) (int, error) {
if c.HandlerFunc != nil {
l := make([]byte, len(b))
copy(l, b)
(*c.HandlerFunc)(string(l))
}
return len(b), nil
}

View File

@@ -26,10 +26,26 @@ type imagePullStatus struct {
Progress string `json:"progress"` Progress string `json:"progress"`
} }
// A custom console writer that allows us to keep a function blocked until the
// given stream is properly closed. This does nothing special, only exists to
// make a noop io.Writer.
type noopWriter struct{}
var _ io.Writer = noopWriter{}
// Implement the required Write function to satisfy the io.Writer interface.
func (nw noopWriter) Write(b []byte) (int, error) {
return len(b), nil
}
// Attaches to the docker container itself and ensures that we can pipe data in and out // Attaches to the docker container itself and ensures that we can pipe data in and out
// of the process stream. This should not be used for reading console data as you *will* // of the process stream. This should not be used for reading console data as you *will*
// miss important output at the beginning because of the time delay with attaching to the // miss important output at the beginning because of the time delay with attaching to the
// output. // output.
//
// Calling this function will poll resources for the container in the background until the
// provided context is canceled by the caller. Failure to cancel said context will cause
// background memory leaks as the goroutine will not exit.
func (e *Environment) Attach() error { func (e *Environment) Attach() error {
if e.IsAttached() { if e.IsAttached() {
return nil return nil
@@ -53,10 +69,8 @@ func (e *Environment) Attach() error {
e.SetStream(&st) e.SetStream(&st)
} }
c := new(Console) go func() {
go func(console *Console) {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
defer e.stream.Close() defer e.stream.Close()
defer func() { defer func() {
@@ -64,27 +78,32 @@ func (e *Environment) Attach() error {
e.SetStream(nil) e.SetStream(nil)
}() }()
// Poll resources in a separate thread since this will block the copy call below go func() {
// from being reached until it is completed if not run in a separate process. However,
// we still want it to be stopped when the copy operation below is finished running which
// indicates that the container is no longer running.
go func(ctx context.Context) {
if err := e.pollResources(ctx); err != nil { if err := e.pollResources(ctx); err != nil {
l := log.WithField("environment_id", e.Id)
if !errors.Is(err, context.Canceled) { if !errors.Is(err, context.Canceled) {
l.WithField("error", err).Error("error during environment resource polling") e.log().WithField("error", err).Error("error during environment resource polling")
} else { } else {
l.Warn("stopping server resource polling: context canceled") e.log().Warn("stopping server resource polling: context canceled")
} }
} }
}(ctx) }()
// Stream the reader output to the console which will then fire off events and handle console // Block the completion of this routine until the container is no longer running. This allows
// throttling and sending the output to the user. // the pollResources function to run until it needs to be stopped. Because the container
if _, err := io.Copy(console, e.stream.Reader); err != nil { // can be polled for resource usage, even when stopped, we need to have this logic present
log.WithField("environment_id", e.Id).WithField("error", err).Error("error while copying environment output to console") // in order to cancel the context and therefore stop the routine that is spawned.
//
// For now, DO NOT use client#ContainerWait from the Docker package. There is a nasty
// bug causing containers to hang on deletion and cause servers to lock up on the system.
//
// This weird code isn't intuitive, but it keeps the function from ending until the container
// is stopped and therefore the stream reader ends up closed.
// @see https://github.com/moby/moby/issues/41827
c := new(noopWriter)
if _, err := io.Copy(c, e.stream.Reader); err != nil {
e.log().WithField("error", err).Error("could not copy from environment stream to noop writer")
} }
}(c) }()
return nil return nil
} }
@@ -259,6 +278,8 @@ func (e *Environment) Destroy() error {
Force: true, Force: true,
}) })
e.SetState(environment.ProcessOfflineState)
// Don't trigger a destroy failure if we try to delete a container that does not // Don't trigger a destroy failure if we try to delete a container that does not
// exist on the system. We're just a step ahead of ourselves in that case. // exist on the system. We're just a step ahead of ourselves in that case.
// //
@@ -267,8 +288,6 @@ func (e *Environment) Destroy() error {
return nil return nil
} }
e.SetState(environment.ProcessOfflineState)
return err return err
} }
@@ -280,7 +299,6 @@ func (e *Environment) followOutput() error {
if err != nil { if err != nil {
return err return err
} }
return errors.New(fmt.Sprintf("no such container: %s", e.Id)) return errors.New(fmt.Sprintf("no such container: %s", e.Id))
} }
@@ -295,19 +313,39 @@ func (e *Environment) followOutput() error {
if err != nil { if err != nil {
return err return err
} }
go func(reader io.ReadCloser) {
defer reader.Close() go e.scanOutput(reader)
evts := e.Events()
err := system.ScanReader(reader, func(line string) {
evts.Publish(environment.ConsoleOutputEvent, line)
})
if err != nil && err != io.EOF {
log.WithField("error", err).WithField("container_id", e.Id).Warn("error processing scanner line in console output")
}
}(reader)
return nil return nil
} }
func (e *Environment) scanOutput(reader io.ReadCloser) {
defer reader.Close()
events := e.Events()
err := system.ScanReader(reader, func(line string) {
events.Publish(environment.ConsoleOutputEvent, line)
})
if err != nil && err != io.EOF {
log.WithField("error", err).WithField("container_id", e.Id).Warn("error processing scanner line in console output")
return
}
// Return here if the server is offline or currently stopping.
if e.State() == environment.ProcessStoppingState || e.State() == environment.ProcessOfflineState {
return
}
// Close the current reader before starting a new one, the defer will still run
// but it will do nothing if we already closed the stream.
_ = reader.Close()
// Start following the output of the server again.
go e.followOutput()
}
// Pulls the image from Docker. If there is an error while pulling the image from the source // Pulls the image from Docker. If there is an error while pulling the image from the source
// but the image already exists locally, we will report that error to the logger but continue // but the image already exists locally, we will report that error to the logger but continue
// with the process. // with the process.
@@ -391,9 +429,11 @@ func (e *Environment) ensureImageExists(image string) error {
// I'm not sure what the best approach here is, but this will block execution until the image // I'm not sure what the best approach here is, but this will block execution until the image
// is done being pulled, which is what we need. // is done being pulled, which is what we need.
scanner := bufio.NewScanner(out) scanner := bufio.NewScanner(out)
for scanner.Scan() { for scanner.Scan() {
s := imagePullStatus{} s := imagePullStatus{}
fmt.Println(scanner.Text()) fmt.Println(scanner.Text())
if err := json.Unmarshal(scanner.Bytes(), &s); err == nil { if err := json.Unmarshal(scanner.Bytes(), &s); err == nil {
e.Events().Publish(environment.DockerImagePullStatus, s.Status+" "+s.Progress) e.Events().Publish(environment.DockerImagePullStatus, s.Status+" "+s.Progress)
} }

View File

@@ -2,6 +2,7 @@ package docker
import ( import (
"context" "context"
"github.com/apex/log"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/pterodactyl/wings/api" "github.com/pterodactyl/wings/api"
@@ -70,6 +71,10 @@ func New(id string, m *Metadata, c *environment.Configuration) (*Environment, er
return e, nil return e, nil
} }
func (e *Environment) log() *log.Entry {
return log.WithField("environment", e.Type()).WithField("container_id", e.Id)
}
func (e *Environment) Type() string { func (e *Environment) Type() string {
return "docker" return "docker"
} }
@@ -77,8 +82,9 @@ func (e *Environment) Type() string {
// Set if this process is currently attached to the process. // Set if this process is currently attached to the process.
func (e *Environment) SetStream(s *types.HijackedResponse) { func (e *Environment) SetStream(s *types.HijackedResponse) {
e.mu.Lock() e.mu.Lock()
defer e.mu.Unlock()
e.stream = s e.stream = s
e.mu.Unlock()
} }
// Determine if the this process is currently attached to the container. // Determine if the this process is currently attached to the container.
@@ -93,6 +99,7 @@ func (e *Environment) Events() *events.EventBus {
e.eventMu.Do(func() { e.eventMu.Do(func() {
e.emitter = events.New() e.emitter = events.New()
}) })
return e.emitter return e.emitter
} }
@@ -169,12 +176,14 @@ func (e *Environment) Config() *environment.Configuration {
// Sets the stop configuration for the environment. // Sets the stop configuration for the environment.
func (e *Environment) SetStopConfiguration(c api.ProcessStopConfiguration) { func (e *Environment) SetStopConfiguration(c api.ProcessStopConfiguration) {
e.mu.Lock() e.mu.Lock()
defer e.mu.Unlock()
e.meta.Stop = c e.meta.Stop = c
e.mu.Unlock()
} }
func (e *Environment) SetImage(i string) { func (e *Environment) SetImage(i string) {
e.mu.Lock() e.mu.Lock()
defer e.mu.Unlock()
e.meta.Image = i e.meta.Image = i
e.mu.Unlock()
} }

View File

@@ -20,10 +20,9 @@ import (
// //
// This process will also confirm that the server environment exists and is in a bootable // This process will also confirm that the server environment exists and is in a bootable
// state. This ensures that unexpected container deletion while Wings is running does // state. This ensures that unexpected container deletion while Wings is running does
// not result in the server becoming unbootable. // not result in the server becoming un-bootable.
func (e *Environment) OnBeforeStart() error { func (e *Environment) OnBeforeStart() error {
// Always destroy and re-create the server container to ensure that synced data from // Always destroy and re-create the server container to ensure that synced data from the Panel is used.
// the Panel is usee.
if err := e.client.ContainerRemove(context.Background(), e.Id, types.ContainerRemoveOptions{RemoveVolumes: true}); err != nil { if err := e.client.ContainerRemove(context.Background(), e.Id, types.ContainerRemoveOptions{RemoveVolumes: true}); err != nil {
if !client.IsErrNotFound(err) { if !client.IsErrNotFound(err) {
return errors.WithMessage(err, "failed to remove server docker container during pre-boot") return errors.WithMessage(err, "failed to remove server docker container during pre-boot")
@@ -49,6 +48,7 @@ func (e *Environment) OnBeforeStart() error {
// call to OnBeforeStart(). // call to OnBeforeStart().
func (e *Environment) Start() error { func (e *Environment) Start() error {
sawError := false sawError := false
// If sawError is set to true there was an error somewhere in the pipeline that // If sawError is set to true there was an error somewhere in the pipeline that
// got passed up, but we also want to ensure we set the server to be offline at // got passed up, but we also want to ensure we set the server to be offline at
// that point. // that point.
@@ -235,7 +235,7 @@ func (e *Environment) Terminate(signal os.Signal) error {
sig := strings.TrimSuffix(strings.TrimPrefix(signal.String(), "signal "), "ed") sig := strings.TrimSuffix(strings.TrimPrefix(signal.String(), "signal "), "ed")
if err := e.client.ContainerKill(context.Background(), e.Id, sig); err != nil { if err := e.client.ContainerKill(context.Background(), e.Id, sig); err != nil && !client.IsErrNotFound(err) {
return err return err
} }

View File

@@ -4,12 +4,10 @@ import (
"context" "context"
"emperror.dev/errors" "emperror.dev/errors"
"encoding/json" "encoding/json"
"github.com/apex/log"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/pterodactyl/wings/environment" "github.com/pterodactyl/wings/environment"
"io" "io"
"math" "math"
"sync/atomic"
) )
// Attach to the instance and then automatically emit an event whenever the resource usage for the // Attach to the instance and then automatically emit an event whenever the resource usage for the
@@ -19,63 +17,51 @@ func (e *Environment) pollResources(ctx context.Context) error {
return errors.New("cannot enable resource polling on a stopped server") return errors.New("cannot enable resource polling on a stopped server")
} }
l := log.WithField("container_id", e.Id) e.log().Info("starting resource polling for container")
l.Debug("starting resource polling for container") defer e.log().Debug("stopped resource polling for container")
defer l.Debug("stopped resource polling for container")
stats, err := e.client.ContainerStats(context.Background(), e.Id, true) stats, err := e.client.ContainerStats(ctx, e.Id, true)
if err != nil { if err != nil {
return err return err
} }
defer stats.Body.Close() defer stats.Body.Close()
dec := json.NewDecoder(stats.Body) dec := json.NewDecoder(stats.Body)
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return ctx.Err() return ctx.Err()
default: default:
var v *types.StatsJSON var v types.StatsJSON
if err := dec.Decode(&v); err != nil { if err := dec.Decode(&v); err != nil {
if err != io.EOF { if err != io.EOF && !errors.Is(err, context.Canceled) {
l.WithField("error", err).Warn("error while processing Docker stats output for container") e.log().WithField("error", err).Warn("error while processing Docker stats output for container")
} else { } else {
l.Debug("io.EOF encountered during stats decode, stopping polling...") e.log().Debug("io.EOF encountered during stats decode, stopping polling...")
} }
return nil return nil
} }
// Disable collection if the server is in an offline state and this process is still running. // Disable collection if the server is in an offline state and this process is still running.
if e.st.Load() == environment.ProcessOfflineState { if e.st.Load() == environment.ProcessOfflineState {
l.Debug("process in offline state while resource polling is still active; stopping poll") e.log().Debug("process in offline state while resource polling is still active; stopping poll")
return nil return nil
} }
var rx uint64
var tx uint64
for _, nw := range v.Networks {
atomic.AddUint64(&rx, nw.RxBytes)
atomic.AddUint64(&tx, nw.RxBytes)
}
st := environment.Stats{ st := environment.Stats{
Memory: calculateDockerMemory(v.MemoryStats), Memory: calculateDockerMemory(v.MemoryStats),
MemoryLimit: v.MemoryStats.Limit, MemoryLimit: v.MemoryStats.Limit,
CpuAbsolute: calculateDockerAbsoluteCpu(&v.PreCPUStats, &v.CPUStats), CpuAbsolute: calculateDockerAbsoluteCpu(v.PreCPUStats, v.CPUStats),
Network: struct { Network: environment.NetworkStats{},
RxBytes uint64 `json:"rx_bytes"` }
TxBytes uint64 `json:"tx_bytes"`
}{ for _, nw := range v.Networks {
RxBytes: rx, st.Network.RxBytes += nw.RxBytes
TxBytes: tx, st.Network.TxBytes += nw.TxBytes
},
} }
if b, err := json.Marshal(st); err != nil { if b, err := json.Marshal(st); err != nil {
l.WithField("error", err).Warn("error while marshaling stats object for environment") e.log().WithField("error", err).Warn("error while marshaling stats object for environment")
} else { } else {
e.Events().Publish(environment.ResourceEvent, string(b)) e.Events().Publish(environment.ResourceEvent, string(b))
} }
@@ -108,7 +94,7 @@ func calculateDockerMemory(stats types.MemoryStats) uint64 {
// by the defined CPU limits on the container. // by the defined CPU limits on the container.
// //
// @see https://github.com/docker/cli/blob/aa097cf1aa19099da70930460250797c8920b709/cli/command/container/stats_helpers.go#L166 // @see https://github.com/docker/cli/blob/aa097cf1aa19099da70930460250797c8920b709/cli/command/container/stats_helpers.go#L166
func calculateDockerAbsoluteCpu(pStats *types.CPUStats, stats *types.CPUStats) float64 { func calculateDockerAbsoluteCpu(pStats types.CPUStats, stats types.CPUStats) float64 {
// Calculate the change in CPU usage between the current and previous reading. // Calculate the change in CPU usage between the current and previous reading.
cpuDelta := float64(stats.CPUUsage.TotalUsage) - float64(pStats.CPUUsage.TotalUsage) cpuDelta := float64(stats.CPUUsage.TotalUsage) - float64(pStats.CPUUsage.TotalUsage)

View File

@@ -19,8 +19,9 @@ var ErrNotAttached = errors.New("not attached to instance")
func (e *Environment) setStream(s *types.HijackedResponse) { func (e *Environment) setStream(s *types.HijackedResponse) {
e.mu.Lock() e.mu.Lock()
defer e.mu.Unlock()
e.stream = s e.stream = s
e.mu.Unlock()
} }
// Sends the specified command to the stdin of the running container instance. There is no // Sends the specified command to the stdin of the running container instance. There is no
@@ -71,7 +72,7 @@ func (e *Environment) Readlog(lines int) ([]string, error) {
// Docker stores the logs for server output in a JSON format. This function will iterate over the JSON // Docker stores the logs for server output in a JSON format. This function will iterate over the JSON
// that was read from the log file and parse it into a more human readable format. // that was read from the log file and parse it into a more human readable format.
func (e *Environment) parseLogToStrings(b []byte) ([]string, error) { func (e *Environment) parseLogToStrings(b []byte) ([]string, error) {
var hasError = false hasError := false
var out []string var out []string
scanner := bufio.NewScanner(bytes.NewReader(b)) scanner := bufio.NewScanner(bytes.NewReader(b))

View File

@@ -24,8 +24,10 @@ type Stats struct {
// Disk int64 `json:"disk_bytes"` // Disk int64 `json:"disk_bytes"`
// Current network transmit in & out for a container. // Current network transmit in & out for a container.
Network struct { Network NetworkStats `json:"network"`
RxBytes uint64 `json:"rx_bytes"` }
TxBytes uint64 `json:"tx_bytes"`
} `json:"network"` type NetworkStats struct {
RxBytes uint64 `json:"rx_bytes"`
TxBytes uint64 `json:"tx_bytes"`
} }

View File

@@ -4,17 +4,50 @@ import (
"context" "context"
"emperror.dev/errors" "emperror.dev/errors"
"encoding/json" "encoding/json"
"fmt"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/pterodactyl/wings/server" "github.com/pterodactyl/wings/server"
"io" "io"
"net"
"net/http" "net/http"
"net/url" "net/url"
"path/filepath" "path/filepath"
"regexp"
"strings" "strings"
"sync" "sync"
"time" "time"
) )
var client = &http.Client{Timeout: time.Hour * 12}
var instance = &Downloader{
// Tracks all of the active downloads.
downloadCache: make(map[string]*Download),
// Tracks all of the downloads active for a given server instance. This is
// primarily used to make things quicker and keep the code a little more
// legible throughout here.
serverCache: make(map[string][]string),
}
// Regex to match the end of an IPv4/IPv6 address. This allows the port to be removed
// so that we are just working with the raw IP address in question.
var ipMatchRegex = regexp.MustCompile(`(:\d+)$`)
// Internal IP ranges that should be blocked if the resource requested resolves within.
var internalRanges = []*net.IPNet{
mustParseCIDR("127.0.0.1/8"),
mustParseCIDR("10.0.0.0/8"),
mustParseCIDR("172.16.0.0/12"),
mustParseCIDR("192.168.0.0/16"),
mustParseCIDR("169.254.0.0/16"),
mustParseCIDR("::1/128"),
mustParseCIDR("fe80::/10"),
mustParseCIDR("fc00::/7"),
}
const ErrInternalResolution = errors.Sentinel("downloader: destination resolves to internal network location")
const ErrInvalidIPAddress = errors.Sentinel("downloader: invalid IP address")
const ErrDownloadFailed = errors.Sentinel("downloader: download request failed")
type Counter struct { type Counter struct {
total int total int
onWrite func(total int) onWrite func(total int)
@@ -27,12 +60,6 @@ func (c *Counter) Write(p []byte) (int, error) {
return n, nil return n, nil
} }
type Downloader struct {
mu sync.RWMutex
downloadCache map[string]*Download
serverCache map[string][]string
}
type DownloadRequest struct { type DownloadRequest struct {
URL *url.URL URL *url.URL
Directory string Directory string
@@ -47,16 +74,6 @@ type Download struct {
cancelFunc *context.CancelFunc cancelFunc *context.CancelFunc
} }
var client = &http.Client{Timeout: time.Hour * 12}
var instance = &Downloader{
// Tracks all of the active downloads.
downloadCache: make(map[string]*Download),
// Tracks all of the downloads active for a given server instance. This is
// primarily used to make things quicker and keep the code a little more
// legible throughout here.
serverCache: make(map[string][]string),
}
// Starts a new tracked download which allows for cancelation later on by calling // Starts a new tracked download which allows for cancelation later on by calling
// the Downloader.Cancel function. // the Downloader.Cancel function.
func New(s *server.Server, r DownloadRequest) *Download { func New(s *server.Server, r DownloadRequest) *Download {
@@ -108,15 +125,24 @@ func (dl *Download) Execute() error {
dl.cancelFunc = &cancel dl.cancelFunc = &cancel
defer dl.Cancel() defer dl.Cancel()
// Always ensure that we're checking the destination for the download to avoid a malicious
// user from accessing internal network resources.
if err := dl.isExternalNetwork(ctx); err != nil {
return err
}
// At this point we have verified the destination is not within the local network, so we can
// now make a request to that URL and pull down the file, saving it to the server's data
// directory.
req, err := http.NewRequestWithContext(ctx, http.MethodGet, dl.req.URL.String(), nil) req, err := http.NewRequestWithContext(ctx, http.MethodGet, dl.req.URL.String(), nil)
if err != nil { if err != nil {
return errors.WrapIf(err, "downloader: failed to create request") return errors.WrapIf(err, "downloader: failed to create request")
} }
req.Header.Set("User-Agent", "Pterodactyl Panel (https://pterodactyl.io)") req.Header.Set("User-Agent", "Pterodactyl Panel (https://pterodactyl.io)")
res, err := client.Do(req) // lgtm [go/request-forgery] res, err := client.Do(req)
if err != nil { if err != nil {
return errors.New("downloader: failed opening request to download file") return ErrDownloadFailed
} }
defer res.Body.Close() defer res.Body.Close()
if res.StatusCode != http.StatusOK { if res.StatusCode != http.StatusOK {
@@ -178,6 +204,52 @@ func (dl *Download) counter(contentLength int64) *Counter {
} }
} }
// Verifies that a given download resolves to a location not within the current local
// network for the machine. If the final destination of a resource is within the local
// network an ErrInternalResolution error is returned.
func (dl *Download) isExternalNetwork(ctx context.Context) error {
dialer := &net.Dialer{
LocalAddr: nil,
}
host := dl.req.URL.Host
if !ipMatchRegex.MatchString(host) {
if dl.req.URL.Scheme == "https" {
host = host + ":443"
} else {
host = host + ":80"
}
}
c, err := dialer.DialContext(ctx, "tcp", host)
if err != nil {
return errors.WithStack(err)
}
c.Close()
ip := net.ParseIP(ipMatchRegex.ReplaceAllString(c.RemoteAddr().String(), ""))
if ip == nil {
return errors.WithStack(ErrInvalidIPAddress)
}
if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || ip.IsInterfaceLocalMulticast() {
return errors.WithStack(ErrInternalResolution)
}
for _, block := range internalRanges {
if block.Contains(ip) {
return errors.WithStack(ErrInternalResolution)
}
}
return nil
}
// Defines a global downloader struct that keeps track of all currently processing downloads
// for the machine.
type Downloader struct {
mu sync.RWMutex
downloadCache map[string]*Download
serverCache map[string][]string
}
// Tracks a download in the internal cache for this instance. // Tracks a download in the internal cache for this instance.
func (d *Downloader) track(dl *Download) { func (d *Downloader) track(dl *Download) {
d.mu.Lock() d.mu.Lock()
@@ -222,3 +294,11 @@ func (d *Downloader) remove(dlid string) {
d.serverCache[sid] = out d.serverCache[sid] = out
} }
} }
func mustParseCIDR(ip string) *net.IPNet {
_, block, err := net.ParseCIDR(ip)
if err != nil {
panic(fmt.Errorf("downloader: failed to parse CIDR: %s", err))
}
return block
}

View File

@@ -2,14 +2,6 @@ package router
import ( import (
"context" "context"
"emperror.dev/errors"
"github.com/apex/log"
"github.com/gin-gonic/gin"
"github.com/pterodactyl/wings/router/downloader"
"github.com/pterodactyl/wings/router/tokens"
"github.com/pterodactyl/wings/server"
"github.com/pterodactyl/wings/server/filesystem"
"golang.org/x/sync/errgroup"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"net/url" "net/url"
@@ -18,16 +10,21 @@ import (
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"emperror.dev/errors"
"github.com/apex/log"
"github.com/gin-gonic/gin"
"github.com/pterodactyl/wings/router/downloader"
"github.com/pterodactyl/wings/router/tokens"
"github.com/pterodactyl/wings/server"
"github.com/pterodactyl/wings/server/filesystem"
"golang.org/x/sync/errgroup"
) )
// Returns the contents of a file on the server. // Returns the contents of a file on the server.
func getServerFileContents(c *gin.Context) { func getServerFileContents(c *gin.Context) {
s := ExtractServer(c) s := ExtractServer(c)
f, err := url.QueryUnescape(c.Query("file")) f := c.Query("file")
if err != nil {
WithError(c, err)
return
}
p := "/" + strings.TrimLeft(f, "/") p := "/" + strings.TrimLeft(f, "/")
st, err := s.Filesystem().Stat(p) st, err := s.Filesystem().Stat(p)
if err != nil { if err != nil {
@@ -64,11 +61,7 @@ func getServerFileContents(c *gin.Context) {
// Returns the contents of a directory for a server. // Returns the contents of a directory for a server.
func getServerListDirectory(c *gin.Context) { func getServerListDirectory(c *gin.Context) {
s := ExtractServer(c) s := ExtractServer(c)
dir, err := url.QueryUnescape(c.Query("directory")) dir := c.Query("directory")
if err != nil {
WithError(c, err)
return
}
if stats, err := s.Filesystem().ListDirectory(dir); err != nil { if stats, err := s.Filesystem().ListDirectory(dir); err != nil {
WithError(c, err) WithError(c, err)
} else { } else {
@@ -212,11 +205,7 @@ func postServerDeleteFiles(c *gin.Context) {
func postServerWriteFile(c *gin.Context) { func postServerWriteFile(c *gin.Context) {
s := GetServer(c.Param("server")) s := GetServer(c.Param("server"))
f, err := url.QueryUnescape(c.Query("file")) f := c.Query("file")
if err != nil {
NewServerError(err, s).Abort(c)
return
}
f = "/" + strings.TrimLeft(f, "/") f = "/" + strings.TrimLeft(f, "/")
if err := s.Filesystem().Writefile(f, c.Request.Body); err != nil { if err := s.Filesystem().Writefile(f, c.Request.Body); err != nil {

View File

@@ -129,6 +129,9 @@ func postServerArchive(c *gin.Context) {
return return
} }
// Mark the server as not being transferred so it can actually be used.
s.SetTransferring(false)
s.Events().Publish(server.TransferStatusEvent, "failure") s.Events().Publish(server.TransferStatusEvent, "failure")
sendTransferLog("Attempting to notify panel of archive failure..") sendTransferLog("Attempting to notify panel of archive failure..")

View File

@@ -64,9 +64,11 @@ func (s *Server) StartEventListeners() {
// to terminate again. // to terminate again.
if s.Environment.State() != environment.ProcessStoppingState { if s.Environment.State() != environment.ProcessStoppingState {
s.Environment.SetState(environment.ProcessStoppingState) s.Environment.SetState(environment.ProcessStoppingState)
go func() { go func() {
s.Log().Warn("stopping server instance, violating throttle limits") s.Log().Warn("stopping server instance, violating throttle limits")
s.PublishConsoleOutputFromDaemon("Your server is being stopped for outputting too much data in a short period of time.") s.PublishConsoleOutputFromDaemon("Your server is being stopped for outputting too much data in a short period of time.")
// Completely skip over server power actions and terminate the running instance. This gives the // Completely skip over server power actions and terminate the running instance. This gives the
// server 15 seconds to finish stopping gracefully before it is forcefully terminated. // server 15 seconds to finish stopping gracefully before it is forcefully terminated.
if err := s.Environment.WaitForStop(config.Get().Throttles.StopGracePeriod, true); err != nil { if err := s.Environment.WaitForStop(config.Get().Throttles.StopGracePeriod, true); err != nil {

View File

@@ -2,5 +2,5 @@ package system
var ( var (
// The current version of this software. // The current version of this software.
Version = "0.0.1" Version = "v0.0.1"
) )