Skip to content
 
 

Repository files navigation

goed2k-server

github.com/chenjia404/goed2k-server is an ED2K/eMule server implemented in Go, compatible with the github.com/monkeyWie/goed2k client protocol.

eMuleBB fork role: emulebb/goed2k-server is the fixed deterministic server used by local eMuleBB harness scenarios. It is test infrastructure, not an evolving service or product. Change it only when required to keep the maintained harness working.

The current release focuses on two areas:

  • ED2K/eMule TCP server protocol
  • HTTP administration API

The goal is not to replicate the full official eMule server, but to provide a runnable, testable, extensible server foundation so you can extend protocol behavior and business logic.

Features

ED2K protocol (implemented)

  • Client login handshake LoginRequest
  • Server status Status
  • Server message Message
  • Client ID assignment IdChange
  • Shared file registration OP_OFFERFILES
  • Search SearchRequest
  • Search pagination SearchMore
  • File source lookup GetFileSources
  • Callback request CallbackRequest
  • Callback notification CallbackRequestIncoming
  • Callback failure CallbackRequestFailed

Runtime (implemented)

  • eMule-compatible UDP server status (OP_GLOBSERVSTATREQ / OP_GLOBSERVSTATRES, used to refresh soft/hard file limits and related fields in the client)
  • Dynamic user table
  • Runtime statistics
  • Dynamic shared file registration, updates, and revocation on disconnect
  • Static shared catalog persisted to JSON, MySQL, or PostgreSQL
  • HTTP admin API
  • Admin API token authentication
  • List pagination, filtering, and sorting
  • Health checks

Project layout

Design documents

Install and import

Run as a CLI

If the module is published on GitHub, install by module path:

go install github.com/chenjia404/goed2k-server/cmd/goed2k-server@latest

Then run:

goed2k-server -config config.json

Import as a Go module

To use the server package in your own project:

go get github.com/chenjia404/goed2k-server@latest

Import:

import "github.com/chenjia404/goed2k-server/ed2ksrv"

goed2k dependency version

This project depends on the remote module:

github.com/monkeyWie/goed2k v0.0.0-20260319015208-6257e6988ff2

Check the resolved version:

go list -m github.com/monkeyWie/goed2k

Upgrade to the latest upstream:

go get github.com/monkeyWie/goed2k@latest
go mod tidy

Pin to a specific pseudo-version:

go get github.com/monkeyWie/goed2k@v0.0.0-20260319015208-6257e6988ff2
go mod tidy

After upgrading, run:

go test ./...

Notes:

  • github.com/monkeyWie/goed2k does not have stable tags yet, so Go uses pseudo-versions
  • A pseudo-version maps to a specific commit and is suitable for reproducible builds
  • When upstream publishes semantic tags, you can switch to those

Requirements

  • Go 1.25+
  • Network access to the github.com/monkeyWie/goed2k module

Quick start

1. Create a config file

Copy the example:

cp config.example.json config.json

Example contents:

{
  "listen_address": ":4661",
  "admin_listen_address": ":8080",
  "admin_token": "change-me",
  "server_name": "goed2k-server",
  "server_description": "Minimal eD2k/eMule compatible server",
  "message": "Welcome to goed2k-server",
  "storage_backend": "json",
  "catalog_path": "testdata/catalog.json",
  "database_dsn": "",
  "database_table": "shared_files",
  "search_batch_size": 2,
  "tcp_flags": 0,
  "aux_port": 0,
  "protocol_obfuscation": true,
  "server_udp": true,
  "udp_port_offset": 4,
  "soft_files_limit": 5000,
  "hard_files_limit": 200000,
  "max_users_advertised": 500000
}

See the Configuration table below and config.example.json for all fields.

2. Start the server

From module source:

go run github.com/chenjia404/goed2k-server/cmd/goed2k-server -config config.json

Or from a local clone:

go run ./cmd/goed2k-server -config config.json

Default listeners:

  • ED2K TCP: :4661
  • HTTP admin: :8080
  • ED2K UDP (optional, see below): TCP listen port + udp_port_offset (default +4, so with TCP 4661 the UDP port is 4665)

UDP port (eMule / aMule clients)

eMule sends a global server status request over UDP (OP_GLOBSERVSTATREQ). After the server replies with OP_GLOBSERVSTATRES, the client can refresh soft file limit, hard file limit, max users, and related server-list fields. TCP-only connections often leave those at 0.

  • Port formula: UDP port = TCP listen port + udp_port_offset. The default udp_port_offset is 4 (same convention as common eD2k clients, matching aMule's default SendUDPPacket offset).
  • Disable UDP: set "server_udp": false to skip the UDP listener (clients may still show zeros or stale stats).
  • Firewall / security groups: if server_udp is enabled, open the matching UDP port in addition to the ED2K TCP port.

Docker

The image published for production is chenjia404/goed2k-server on Docker Hub.

Pull:

docker pull chenjia404/goed2k-server:latest

The container entrypoint runs /app/goed2k-server with default arguments -config /app/config.json (see the Dockerfile in this repo). Map host ports and mount your config.json at /app/config.json:

docker run -d --name goed2k-server \
  -p 4661:4661 -p 4665:4665/udp -p 8080:8080 \
  -v /path/to/config.json:/app/config.json:ro \
  chenjia404/goed2k-server:latest

4665:4665/udp matches default TCP 4661 with udp_port_offset 4. If you change the TCP port in listen_address, map TCP port + udp_port_offset for UDP.

When storage_backend is json, ensure catalog_path refers to a file that exists inside the container, usually by mounting your catalog and pointing catalog_path at that path. Example: host files under /srv/goed2k/, with catalog_path set to /data/catalog.json:

docker run -d --name goed2k-server \
  -p 4661:4661 -p 4665:4665/udp -p 8080:8080 \
  -v /srv/goed2k/config.json:/app/config.json:ro \
  -v /srv/goed2k/catalog.json:/data/catalog.json:ro \
  chenjia404/goed2k-server:latest

To use another config path, pass arguments after the image name (overriding the default -config /app/config.json):

docker run --rm -p 4661:4661 -p 4665:4665/udp -p 8080:8080 \
  -v /path/to/other.json:/other/config.json:ro \
  chenjia404/goed2k-server:latest -config /other/config.json

To build and run from source instead of the Hub image, use the Dockerfile at the repository root.

Configuration

Field Description
listen_address ED2K TCP listen address
admin_listen_address HTTP admin listen address
admin_token Admin token; if non-empty, requests must send X-Admin-Token
server_name Server name
server_description Server description
message Message clients receive after connecting
storage_backend Persistence backend: json, mysql, or pgsql
catalog_path Path to the static shared catalog file for the json backend
database_dsn Connection string for mysql or pgsql
database_table Table name (default shared_files)
search_batch_size Results per search page
tcp_flags TCP flags returned in IdChange
aux_port Auxiliary port returned in IdChange
protocol_obfuscation Enable eMule-style TCP obfuscation (DH + RC4) when the first byte is not ED2K
server_udp Enable UDP server status replies (default true)
udp_port_offset UDP listen port offset from TCP (default 4, i.e. TCP 4661 -> UDP 4665)
soft_files_limit Soft file limit advertised in the UDP reply (eMule display and publish policy)
hard_files_limit Hard file limit advertised in the UDP reply
max_users_advertised Max users advertised in the UDP reply

Database storage examples

MySQL:

{
  "storage_backend": "mysql",
  "database_dsn": "user:password@tcp(127.0.0.1:3306)/goed2k?charset=utf8mb4&parseTime=true",
  "database_table": "shared_files"
}

PostgreSQL:

{
  "storage_backend": "pgsql",
  "database_dsn": "postgres://user:password@127.0.0.1:5432/goed2k?sslmode=disable",
  "database_table": "shared_files"
}

When using a database backend:

  • Tables are created automatically on startup
  • The static catalog is loaded from the database into the in-memory index
  • Admin API create/delete/persist operations write back to the database
  • Runtime OP_OFFERFILES dynamic shares remain in memory only

Shared catalog format

The shared catalog is a JSON file pointed to by catalog_path.

Example:

{
  "files": [
    {
      "hash": "31D6CFE0D16AE931B73C59D7E0C089C0",
      "name": "ubuntu-24.04-desktop-amd64.iso",
      "size": 6144000000,
      "file_type": "Iso",
      "extension": "iso",
      "sources": 12,
      "complete_sources": 10,
      "endpoints": [
        {
          "host": "127.0.0.1",
          "port": 4662
        }
      ]
    }
  ]
}

Field reference

Field Description
hash ED2K file hash
name File name
size File size
file_type File type, e.g. Iso, Audio
extension Extension
media_codec Media codec (optional)
media_length Media duration (optional)
media_bitrate Media bitrate (optional)
sources Source count; defaults to endpoints length if omitted
complete_sources Complete sources; defaults to sources if omitted
endpoints List of source addresses returned to clients

Dynamic shared file registration

After login, clients can register shared files with OP_OFFERFILES (0x15).

Current behavior:

  • Reported files enter a runtime dynamic index
  • The dynamic index participates in search and source queries
  • On disconnect, dynamic shares are revoked automatically
  • Dynamic shares are not written to the static catalog.json

This is session/runtime data and is not mixed with the static catalog maintained via the HTTP admin API in the same persistence layer.

HTTP admin API

Authentication

When admin_token is set, include:

X-Admin-Token: change-me

Response shape

Success:

{
  "ok": true,
  "data": {},
  "meta": {}
}

Error:

{
  "ok": false,
  "error": "message"
}

Health

GET /healthz

GET /api/healthz

Example:

curl http://127.0.0.1:8080/healthz

Statistics

GET /api/stats

Example:

curl -H 'X-Admin-Token: change-me' \
  http://127.0.0.1:8080/api/stats

Client list

GET /api/clients

Query parameters:

  • search: filter by client name, remote address, listen endpoint, or client hash (substring)
  • page: page number (default 1)
  • per_page: page size (default 50, max 500)
  • sort: id, name, connected_at, last_seen_at

Example:

curl -H 'X-Admin-Token: change-me' \
  'http://127.0.0.1:8080/api/clients?search=test&page=1&per_page=20&sort=name'

Client detail

GET /api/clients/{id}

Example:

curl -H 'X-Admin-Token: change-me' \
  http://127.0.0.1:8080/api/clients/2130706433

File list

GET /api/files

Query parameters:

  • search: filter by file name or hash (substring)
  • file_type: filter by file type
  • extension: filter by extension
  • page: page number (default 1)
  • per_page: page size (default 50, max 500)
  • sort: name, size, sources

Example:

curl -H 'X-Admin-Token: change-me' \
  'http://127.0.0.1:8080/api/files?search=ubuntu&file_type=Iso&sort=size&page=1&per_page=10'

File detail

GET /api/files/{hash}

Example:

curl -H 'X-Admin-Token: change-me' \
  http://127.0.0.1:8080/api/files/31D6CFE0D16AE931B73C59D7E0C089C0

Create or update file

POST /api/files

Example:

curl -X POST \
  -H 'X-Admin-Token: change-me' \
  -H 'Content-Type: application/json' \
  http://127.0.0.1:8080/api/files \
  -d '{
    "hash":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
    "name":"runtime-added-demo.mp3",
    "size":4096,
    "file_type":"Audio",
    "extension":"mp3",
    "endpoints":[{"host":"127.0.0.9","port":4662}]
  }'

Delete file

DELETE /api/files/{hash}

Example:

curl -X DELETE \
  -H 'X-Admin-Token: change-me' \
  http://127.0.0.1:8080/api/files/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

Persist catalog manually

POST /api/persist

Example:

curl -X POST \
  -H 'X-Admin-Token: change-me' \
  http://127.0.0.1:8080/api/persist

Tests

Run all tests:

go test ./...

Coverage includes:

  • Search request decoding
  • ED2K handshake
  • Shared file registration OP_OFFERFILES
  • Search and pagination
  • Source queries
  • Admin API authentication
  • Health checks
  • Client detail/list
  • File detail/list/create/delete
  • Catalog persistence
  • Statistics API

Current limitations

  • The reference goed2k client does not yet ship OP_OFFERFILES send logic; the server supports it, but the client still needs sending implemented
  • The dynamic share index is in-memory only and is not restored across restarts
  • Advanced publish flows (incremental updates, finer-grained publish state) are not implemented
  • No user authentication, RBAC, or audit log persistence
  • No Web UI
  • No database storage; the static catalog is currently persisted as JSON files only

Suggested next steps

  1. Add OP_OFFERFILES send logic in the goed2k client
  2. Add OpenAPI docs and Swagger UI
  3. Add RBAC and audit logging
  4. Migrate static catalog from JSON to SQLite/PostgreSQL where appropriate

About

Fixed-purpose Go eD2K server used by the eMuleBB test harness.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages