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 three areas:
- ED2K/eMule TCP and UDP server protocol
- HTTP admin API with an embedded Web UI
- Static shared catalog persistence (JSON / MySQL / PostgreSQL)
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
Statusand messageMessage - Client ID assignment
IdChange(eMule extended fields: ReportedIP, obfuscation port) - Shared file registration
OP_OFFERFILES - Search
SearchRequest(eMule recursive boolean tree and goed2k flat encoding) - User search
OP_SEARCH_USER(empty user list reply) - Search pagination
SearchMore - File source lookup
GetFileSources/OP_GETSOURCES_OBFU - Callback request
CallbackRequest(TCP0x1C) - Callback notification
CallbackRequestIncomingand failureCallbackRequestFailed - TCP protocol obfuscation (DH + RC4)
When server_udp is enabled, the server listens on TCP port + udp_port_offset (default +4) and supports:
| Opcode | Description |
|---|---|
0x96 |
Global server status OP_GLOBSERVSTATREQ / OP_GLOBSERVSTATRES |
0x98 |
Global file search OP_GLOBSEARCHREQ, replies with 0x99 per result |
0x9A |
File source lookup OP_GLOBGETSOURCES, reply 0x9B |
0x9C |
Callback relay OP_GLOBCALLBACKREQ → TCP 0x35 to target client |
0x9E |
Callback failure OP_INVALID_LOWID when target is offline |
0xA2 |
Server info request, reply 0xA3 |
0xA4 |
Server list request, reply 0xA1 (empty list today) |
- Dynamic user table and runtime statistics
- Dynamic shared file index (in-memory; search and source lookup)
- Static catalog persisted to JSON, MySQL, or PostgreSQL
- HTTP admin API and embedded Web UI (Chinese/English at
/) - Admin API token authentication, pagination, filtering, sorting
- Low-ID
ReportedIPfill (reported_public_ipor auto-detected public source IP) - Health checks
- cmd/goed2k-server/main.go: entry point
- ed2ksrv/server.go: TCP server, dynamic user table, stats
- ed2ksrv/server_udp.go: ED2K UDP replies (status, search, sources, callback, …)
- ed2ksrv/server_udp_callback.go: UDP callback relay
OP_GLOBCALLBACKREQ - ed2ksrv/admin.go: HTTP admin API
- ed2ksrv/admin_ui.go: embedded Web admin UI
- 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
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 |
reported_public_ip |
Public IPv4 filled into IdChange.ReportedIP for low-ID clients; auto-detected from public source IP when empty |
High-ID clients may request a low-ID peer to call back via UDP OP_GLOBCALLBACKREQ (0x9C) without sending TCP 0x1C.
Standard payload (10 bytes): <requester IP 4><requester TCP port 2><target client_ID 4>
Legacy 4-byte payload (target client_ID only): requester must already be logged in over TCP from the same source IP as the UDP packet.
On success the server sends TCP CallbackRequestIncoming (0x35) to the target; on failure UDP 0x9E is returned.
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 (eMule recursive tree and goed2k flat encoding)
- ED2K handshake and extended
IdChange - Shared file registration
OP_OFFERFILES - Search, pagination, and user search
OP_SEARCH_USER - Source lookup (including obfuscated replies)
- TCP and UDP callback relay
- UDP global search, source lookup, and server status
- Admin API auth, Web UI, health checks, catalog persistence, statistics
- The reference
goed2kclient does not yet sendOP_OFFERFILES; end-to-end dynamic sharing needs client support - Dynamic share index is in-memory only; admin APIs manage the static catalog only
- Advanced publish flows (incremental updates, fine-grained publish state) are not implemented
- Multiple TCP clients behind the same public IP share one high client ID (assigned by source IP); later logins replace earlier sessions
- Single
admin_tokenonly; no RBAC; audit log is in-memory (last 200 entries) - No OpenAPI/Swagger documentation
- Not a full eMule server: many TCP/UDP opcodes are unimplemented
- Add
OP_OFFERFILESsend logic in thegoed2kclient - Add OpenAPI docs and Swagger UI
- Persist audit logs and add RBAC
- Label dynamic vs static shares in admin APIs
- Optional SQLite storage backend