This file tracks which changes are our own (EmeraldHost-specific) versus upstream
pterodactyl/wings. Use it during upgrades so
our customizations are not accidentally reverted when pulling in upstream changes.
- Baseline for this comparison: upstream tag
v1.13.3(6987d5e) - Last reviewed: 2026-08-14
- Module path: this fork is
github.com/Rene-Roscher/wings(upstream isgithub.com/pterodactyl/wings). Version is injected at build time via ldflags (-X .../system.Version=<tag>);system/const.gostaysdevelopand is not a divergence. - How to regenerate the picture:
Diff direction: in
git fetch upstream --tags git diff --name-status <upstream-tag> HEAD # what differs git diff <upstream-tag> HEAD -- <path> # inspect a single file
git diff <upstream-tag> HEAD, a+line is in our fork, a-line is upstream.
⚠️ The biggest divergence by far is the backup subsystem (~6 000+ lines): an operation registry/queue, retry, WebSocket progress and a heavily customized restore path. Upstream merges inserver/backup*,router/router_server_backup.go,server/server.goandsftp/server.gowill almost always conflict — resolve by keeping ours and grafting upstream's functional/security changes on top (that is exactly how v1.13.1 was merged).v1.13.2 and v1.13.3 were exceptions: both stayed clear of the backup subsystem and merged (nearly) conflict-free — see §4. The v1.13.3 conflicts were only our rewritten
release.yaml(keep ours, see §1.4) and the module-renamed import block ofserver/install.go.
| Path | What | Note |
|---|---|---|
go.mod |
module github.com/pterodactyl/wings → github.com/Rene-Roscher/wings |
Root of the rename; every Go import of the module changes accordingly. |
Dockerfile, Makefile, .github/workflows/{push,release}.yaml, wings.go |
ldflags / SRC_PATH / import path use the Rene-Roscher module |
Required so system.Version is injected and builds resolve. |
~every *.go file |
github.com/pterodactyl/wings/... → github.com/Rene-Roscher/wings/... |
Incidental noise — appears as conflicts on most merges but carries no behavior. Always "keep ours (Rene-Roscher)". |
Upstream's backup path is small (
s.Backup(b)/s.RestoreBackup(b)in a bare goroutine). The fork replaced it with a queued, cancellable, progress-reporting pipeline.
Operation registry, queue, retry (server package)
| Path | What |
|---|---|
server/backup_operations.go |
Fork-new. BackupOperationRegistry + global GetBackupOperationRegistry(): concurrency-limited (maxConcurrentBackups/Restores = 8), queueing, cancellation. Register() returns the 5-tuple (*op, ctx, cancel, err, wasQueued) consumed by the router. Cancel/Complete/CleanupStaleOperations block-receive the semaphore token to release slots. Background StartBackupOperationCleanup. |
server/backup.go |
Backup()/RestoreBackup() are now compat wrappers over BackupWithContext/RestoreBackupWithContext (context + 6h/4h timeouts, atomic state transitions). BackupWithRetry(ctx,b,2) with exponential backoff. Restore does compression auto-detection, progress, restore-stats and per-dir MkdirAll+Chown. On panel-notify failure for a successful backup it does not delete the archive (upstream does). |
server/server.go |
Server.backingUp *AtomicBool (+ init), AtomicStateTransition + ApplyAtomicStateTransition(), CleanupForDestroy() cancels in-flight ops + cleans orphaned backup files, PublishActivity(). |
server/install.go |
IsBackingUp() / SetBackingUp(). (Note: IsInProtectedState() was deliberately not extended with backingUp.) |
server/power.go, server/errors.go |
HandlePowerAction() also blocks while IsBackingUp(), returning new sentinel ErrServerIsBackingUp. |
environment/environment.go (+ docker state whitelist) |
New states backup, restore, backup_queued, restore_queued. |
Progress & activity over WebSocket
| Path | What |
|---|---|
server/backup_progress.go |
Fork-new. BackupProgressUpdate WS payload {backup_id,type,percentage,bytes_written,bytes_total}, throttled tracker, S3 80/20 archive-vs-upload split. |
server/backup/download_progress.go |
Fork-new. DownloadProgressReader / NewDownloadProgressReader — S3 restore download progress. |
server/events.go |
New BackupProgressEvent, DownloadProgressEvent, ActivityEvent. Frontend contract. |
server/activity.go |
New ActivityFile{Downloaded,Compressed,Decompressed,Chmod}; SaveActivity also publishes ActivityEvent over WS. |
Compression & checksums
| Path | What |
|---|---|
server/backup/backup.go |
SHA-256 checksums + ChecksumType: "sha256" (upstream uses sha1). PathForLocalBackup() helper. |
server/backup/compression.go |
Fork-new. CompressionRegistry (gzip/tar/none) + IsValidBackupContentType() — used by the router content-type gate; without it the router won't compile. |
server/backup/backup_local.go |
foundPath + extension-probing LocateLocal (.tar.gz/.tar), auto-detecting Restore, CleanupBackupFilesForServer. |
server/backup/backup_s3.go |
Two-phase backup reuse, success-flag cleanup (failed uploads kept for retry), orphaned-part logging, upload progress (ProgressReader/ProgressTracker), custom HTTP/1.1 transport, part-retry with 100MB memory-buffer threshold / 5GB cap, Restore() expects an already-decompressed tar stream. |
server/filesystem/archive.go |
Archiver no longer skips directory entries → empty directories are preserved in archives. createCompressor() refactor. |
server/filesystem/archive_restore.go |
Fork-new. DetectCompressionFormat (magic bytes) + CreateDecompressor, wired into restore. |
server/filesystem/compress_binary_test.go, server/backup/*_test.go, router/content_type_test.go, server/backup_*_test.go |
Fork-new regression suites for the above. Keep them passing. |
server/filesystem/archive_test.go + archive_stream_test.go |
File layout divergence, not new behavior. Our archive_test.go was replaced wholesale with tests for the fork-only archive_restore.go; upstream's TestArchive_Stream lives in archive_stream_test.go instead. Upstream edits to archive_test.go therefore land in the wrong file on merge — port them into archive_stream_test.go by hand. |
Router API (fork-only endpoints + customized handlers)
| Path | What |
|---|---|
router/router.go |
Fork-only routes GET /backup/operations and DELETE /backup/:backup/cancel. |
router/router_server_backup.go |
cancelServerBackup + getServerBackupOperations (fork-only). postServerBackup / postServerRestoreBackup rewritten: 409 concurrency guards, registry queueing, timeouts, panic recovery, retry, S3 download progress, and content-type via backup.IsValidBackupContentType (gzip+tar) instead of upstream's gzip-only check. |
| Path | What |
|---|---|
sftp/event.go, sftp/handler.go |
EventPublisher interface + publisher on the event handler → SFTP file actions streamed to the panel via Server.PublishActivity (in addition to DB persistence). |
The fork's previous
SmartSecurityProtectorSFTP brute-force/IP-reputation system (and itssftp.security.*config) was removed —sftp/server.gonow matches upstream (vanilla SFTP auth) apart from the module rename. SFTP abuse protection is left to the network layer (firewall / fail2ban) / the Panel.
| Path | What |
|---|---|
.gitignore |
Fork-added .claude-flow/, .hive-mind/, CLAUDE.md. Upstream will never add these — keep on merge. |
Makefile, Dockerfile |
Our build settings (with the renamed module path). |
.github/workflows/{release,binary,docker}.yaml |
Fork-specific release pipeline — always keep ours. Upstream releases by hand: a human pushes a v* tag, release.yaml cuts a draft, a human publishes it. We release automatically from develop instead, and the version is derived from the newest upstream tag that is an ancestor of develop — so our releases always carry the upstream version number. Fork-only releases between upstream versions append a fourth component (v1.13.3.1), cut by dispatching release.yaml with fork_revision; the automatic path never does this. Upstream's release.yaml has diverged beyond recognition; do not merge it (v1.13.3's c57c519 CDN-manifest notification was deliberately dropped — it notifies pterodactyl's own CDN repo). See the header comment in release.yaml for the full flow and recovery steps. |
These sit on different values/fields than upstream; they will re-appear in a generated
config.yml. Review on upgrade.
| Path | Fork value / field | Note |
|---|---|---|
server/backup_operations.go |
maxConcurrentBackups/Restores = 8; cleanup ticker 5 min / op TTL 8 h; backup 6 h / restore 4 h timeouts |
Fork-chosen capacity/timeouts. |
server/backup_progress.go |
250 ms throttle; S3 80/20 split; 1 MB chunking | Determines WS emission rate / S3 percentage curve. |
server/backup/backup_s3.go |
per-part upload Content-Type: application/octet-stream (upstream application/x-gzip) |
Fork choice. Verify the Panel/S3 presigned flow tolerates it. |
Every item below was verified fork-only (git grep against upstream e771816 returns
zero hits) — upstream wings does not do these, so they are our code/behavior, not
inherited upstream defaults that can be ignored. These are not "divergences to preserve" so
much as bugs/concerns in our own additions, worth fixing rather than defending on upgrade.
| Area | Concern (all fork-only) |
|---|---|
| Committed binaries | wings-debug, wings-fixed, wings-test (~41 MB each) and dist/wings_test (~28 MB) are committed build artifacts (~150 MB total), not gitignored. Repo bloat / accidental commits. |
| Backup cleanup scope | cleanupBackupFiles (server.go) and CleanupBackupFilesForServer (backup_local.go) match backup files by filename pattern only and do not filter by the server's ID. Since the backup directory is shared, deleting one server can remove other servers' local backups. |
checksum_type label |
server/backup.go emits "sha256" in most events but still "sha1" in the panel-notify-failure success branch. Reconcile the labels (actual algorithm is sha256). |
validateBackupContent |
Fails a backup on any server-vs-archive file/dir count mismatch (can race a live server writing files), and computes a full SHA-256 over the entire server tree and backup file purely for a debug log line (perf cost on large servers). |
These show up around our customizations but are upstream code. Treating them as fork changes risks duplicating or mis-merging them on the next upgrade.
| Path | Reality |
|---|---|
config/config.go → ResolveToken(remote bool); remote/http.go → Client.SetCredentials() + mutex-guarded credentials; router/router_system.go → token re-resolve/empty-token guard/credential rotation in postUpdateConfiguration |
Upstream v1.13.3 master-key-rotation cluster (07ce5fe, 392e52c, 3e6c2c9): Panel-sent master key resets now propagate to the running daemon, with remote token values barred from file:///$VAR indirection and checked against WINGS_TOKEN_ID/WINGS_TOKEN overrides. Fork edit: module rename only. |
sftp/handler.go → setstatMode() + sftpAttributeExtended rejection |
Upstream v1.13.3 (da1a216) hardening against unbounded allocations from crafted setstat packets. The fork's publisher wiring in NewHandler sits in the same file — both must survive a merge. |
environment/docker/cgroup_burst.go (+ test), applyCpuBurst/clearCpuBurst/SetCpuBurst call sites in container.go/power.go/server/install.go, config_docker.go → CpuPeriod/CpuBurst/CpuShares, environment/settings.go quota math |
Upstream v1.13.3 CPU allocation work (2cc8a10, 6987d5e). Not fork code — only the imports in the new files were renamed to Rene-Roscher (they arrive pterodactyl on every upstream merge; grep for leaks). |
config/config_token_test.go, remote/http_test.go, router/router_system_test.go, environment/docker/cgroup_burst_test.go, config/config_docker_test.go, upstream additions in sftp/handler_test.go |
Upstream v1.13.3 suites, module rename only. Not fork suites. |
router/tokens/websocket.go → isDenylisted(), and Denylisted() on FilePayload / BackupPayload / UploadPayload (+ their new user_uuid claim) |
Upstream v1.13.2 (28af6dd, "update token validation"). Revocation checking was extracted out of WebsocketPayload.Denylisted() into a shared isDenylisted() and applied to the backup-download, file-download and file-upload one-time tokens, which previously only checked IsUniqueRequest()/scope. Also tightened Before(t) → !After(t), so a token issued in the same second as the revocation is now denied. All four files are byte-identical to upstream — keep them that way. |
router/tokens/denylist_test.go |
Upstream v1.13.2, unmodified. Covers the four payload types above. Not a fork suite. |
router/router_download.go, router/router_server_files.go → the token.Denylisted() || guards |
Upstream v1.13.2 call sites. The surrounding files are fork-modified (module rename + activity logging), so these three one-liners are easy to lose in a conflict resolution — check they survive. |
router/router_server_backup.go SSRF cluster — backupRestoreHttpClient, validateBackupDownloadUrl, parseBackupUuid, isBlockedBackupRestoreIP, isExplicitlyBlockedBackupRestoreIP, isAllowedBackupRestoreDestination, isSupportedBackupRestoreContentType, blockedBackupRestorePrefixes, backupDownloadError |
Upstream v1.13.1 backup-restore SSRF hardening. The only fork edit in this cluster: the restore handler calls backup.IsValidBackupContentType instead of isSupportedBackupRestoreContentType (the latter is retained only for upstream parity + its test). |
config/config.go → Backups.RestoreHostAllowlist |
Upstream v1.13.1. Pairs with the SSRF allowlist above. Not a fork field. |
server/backup/backup.go → validateIdentifier() / normalizedIdentifier() (+ Path() path.Base fallback) |
Upstream v1.13.1 UUID hardening. The fork uses them unchanged. |
system/const.go |
Byte-identical to upstream (Version = "develop"). |
.github/FUNDING.yaml (github: [pterodactyl]) |
Upstream default, unchanged (git diff 28af6dd HEAD is empty). Stale for a fork (sponsorship points at upstream) but NOT our change — clean it up if desired, don't track it as a divergence. |
.github/workflows/release.yaml release-bot identity (ci@pterodactyl.io / Pterodactyl CI) |
Upstream default, unchanged. Upstream's release.yaml already sets this identity. Not our divergence. |
⚠️ Panel coupling introduced by v1.13.2.isDenylisted()fails closed: a token with noiat, noserver_uuidor nouser_uuidis rejected outright. Theuser_uuidclaim is new in v1.13.2, so backup downloads, file downloads and file uploads only work against a Panel that putsuser_uuidinto those JWTs. Against an older Panel every such request returns404 "The requested resource was not found on this server."— deploy Panel before Wings, and if downloads/uploads start 404-ing after a Wings upgrade, this is the first thing to check.