github.com/chenjia404/goed2k-server is an ED2K/eMule server implemented in Go, compatible with the github.com/monkeyWie/goed2k client protocol.
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