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-serveris 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.
- 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
- 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
- cmd/goed2k-server/main.go: entry point
- ed2ksrv/server.go: TCP server, dynamic user table, stats
- ed2ksrv/server_udp.go: ED2K UDP server status replies
- ed2ksrv/admin.go: HTTP admin API
- ed2ksrv/catalog.go: shared catalog and persistence
- ed2ksrv/offerfiles.go:
OP_OFFERFILEShandling - ed2ksrv/protocol.go: search request parsing
- ed2ksrv/config.go: configuration structs
- config.example.json: sample config
- testdata/catalog.json: sample shared catalog
- LegacyED2KServer gap design index: protocol and operational parity gaps to consider for a fuller ED2K server implementation
If the module is published on GitHub, install by module path:
go install github.com/chenjia404/goed2k-server/cmd/goed2k-server@latestThen run:
goed2k-server -config config.jsonTo use the server package in your own project:
go get github.com/chenjia404/goed2k-server@latestImport:
import "github.com/chenjia404/goed2k-server/ed2ksrv"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/goed2kUpgrade to the latest upstream:
go get github.com/monkeyWie/goed2k@latest
go mod tidyPin to a specific pseudo-version:
go get github.com/monkeyWie/goed2k@v0.0.0-20260319015208-6257e6988ff2
go mod tidyAfter upgrading, run:
go test ./...Notes:
github.com/monkeyWie/goed2kdoes 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
- Go 1.25+
- Network access to the
github.com/monkeyWie/goed2kmodule
Copy the example:
cp config.example.json config.jsonExample 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.
From module source:
go run github.com/chenjia404/goed2k-server/cmd/goed2k-server -config config.jsonOr from a local clone:
go run ./cmd/goed2k-server -config config.jsonDefault listeners:
- ED2K TCP:
:4661 - HTTP admin:
:8080 - ED2K UDP (optional, see below): TCP listen port +
udp_port_offset(default +4, so with TCP4661the UDP port is 4665)
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 defaultudp_port_offsetis 4 (same convention as common eD2k clients, matching aMule's defaultSendUDPPacketoffset). - Disable UDP: set
"server_udp": falseto skip the UDP listener (clients may still show zeros or stale stats). - Firewall / security groups: if
server_udpis enabled, open the matching UDP port in addition to the ED2K TCP port.
The image published for production is chenjia404/goed2k-server on Docker Hub.
Pull:
docker pull chenjia404/goed2k-server:latestThe 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:latest4665: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:latestTo 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.jsonTo build and run from source instead of the Hub image, use the Dockerfile at the repository root.
| 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 |
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_OFFERFILESdynamic shares remain in memory only
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 | 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 |
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.
When admin_token is set, include:
X-Admin-Token: change-meSuccess:
{
"ok": true,
"data": {},
"meta": {}
}Error:
{
"ok": false,
"error": "message"
}Example:
curl http://127.0.0.1:8080/healthzExample:
curl -H 'X-Admin-Token: change-me' \
http://127.0.0.1:8080/api/statsQuery parameters:
search: filter by client name, remote address, listen endpoint, or client hash (substring)page: page number (default1)per_page: page size (default50, max500)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'Example:
curl -H 'X-Admin-Token: change-me' \
http://127.0.0.1:8080/api/clients/2130706433Query parameters:
search: filter by file name or hash (substring)file_type: filter by file typeextension: filter by extensionpage: page number (default1)per_page: page size (default50, max500)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'Example:
curl -H 'X-Admin-Token: change-me' \
http://127.0.0.1:8080/api/files/31D6CFE0D16AE931B73C59D7E0C089C0Example:
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}]
}'Example:
curl -X DELETE \
-H 'X-Admin-Token: change-me' \
http://127.0.0.1:8080/api/files/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAExample:
curl -X POST \
-H 'X-Admin-Token: change-me' \
http://127.0.0.1:8080/api/persistRun 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
- The reference
goed2kclient does not yet shipOP_OFFERFILESsend 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
- Add
OP_OFFERFILESsend logic in thegoed2kclient - Add OpenAPI docs and Swagger UI
- Add RBAC and audit logging
- Migrate static catalog from JSON to SQLite/PostgreSQL where appropriate