This project is an M17 Relay server that handles client connections, relay interlinks, and communication between them over UDP. It supports various packet types for managing connections, relays, and client states.
If you're familiar with how traditional M17 (and other) reflectors work, this is the same idea but keeps it simple with only one 'module' to link to. In fact, an M17 Relay has no concept of modules at all. With that in mind, M17 Relays will accept connections from M17 Clients that still use CONN packets with modules, but the Relay will silently ignore them. All clients connecting to a Relay will be in the same 'room' as it were.
Multiple Relays can be interlinked together. The interlinking concept should be handled as a hub-and-spoke topology. While a mesh topology could be created, it is untested at this time.
A demo relay is hosted at KC1AWV.net
- Relay Callsign: RLYKC1AWV
- Hostname: relay.kc1awv.net
- UDP Client Port: 17000
An opt-in call-home service is available for listing relays that are made publicly available, and can be found at the M17 Public Relay Status page. There is an API endpoint that can also be used to retrieve a list of available relays in JSON format at M17 Public Relay List API.
An end "user". Some client programs include:
This program. Relays connect M17 Clients or other M17 Relays to each other and passes data between them.
Either a Client or a Relay. Peer is a general term used for anything connecting to a Relay.
Packets sent or received by Peers to or from a Relay for creating, maintaining, and tearing down connections to each other.
Packets sent by Clients to a Relay for distribution to other Peers. Data packets may contain M17 voice stream or (in the future) packet data.
main.go - Program Main functions
config/
config.go - Configuration functions
metrics/
metrics.go - Metrics functions
logging/
logging.go - Log processing functions
relay/
encoding.go - M17 base-40 address encoding
relay.go - Relay functions
callhome/
callhome.go - Call-home functions
config.dist - Example configuration file
go.mod - Go modules
go.sum - Go module checksums
LICENSE - GPL v3 License file
README.md - This README file
The configuration is loaded from a JSON file (config.json
).
The configuration file should include the following fields:
log_level
: (STRING
) The logging level (e.g., "debug", "info", "warn", "error").relay_callsign
: (STRING
) The 'callsign' of the relay. 9 character maximum, only characters allowed by M17 Address Encoding.bind_address
: (STRING
) The address and port to bind the UDP socket to.web_interface_address
: (STRING
) The address and port to bind the web interface to.public_ip
: (STRING
) The public IP address of the Relay, used only for call-home.daemon_mode
: (BOOL
) Daemonize the relay to run in the background.pid_file
: (STRING
) The location where the PID file should be created (daemon mode)log_file
: (STRING
) The location where the log file should be created (daemon mode)uuid
: (STRING
) The unique identifier for the relay. This will be generated and updated by the call-home service if not provided.call_home_enabled
: (BOOL
) Enable or disable the call-home service.target_relays
: (ARRAY
) A list of target relay addresses to connect to.callsign
: (STRING
) 'Callsign' of the relay to connect to.address
: (STRING
) Address and port of the relay to connect to.
Example config.json:
{
"log_level": "debug",
"relay_callsign": "RLY000001",
"bind_address": "127.0.0.1:17000",
"web_interface_address": "127.0.0.1:8080",
"public_ip": "",
"daemon_mode": true,
"pid_file": "/var/run/go-m17-relay/relay.pid",
"log_file": "/var/log/go-m17-relay/relay.log",
"uuid": "",
"call_home_enabled": true,
"target_relays": [
{
"callsign": "RLY000002",
"address": "127.0.0.1:17001"
}
]
}
The call-home service allows the relay to periodically update its status and configuration with a central server. This service can be enabled or disabled via the call_home_enabled
configuration option.
- Information Gathering: The relay will parse this information from the configuration file:
- Relay Callsign
- Relay Public IP Address (if provided)
- Relay Listening Port
- Relay UUID (if provided)
If a public IP address is not provided in the configuration file, the relay will call out to a third-party service (icanhazip.com
for now, this may change later) to determine your public IP address. This is beneficial to sysops that are using dynamic public addresses that may change.
- Initial Call-Home: The relay will send an initial call-home request on startup.
- Periodic Updates: The relay will send periodic updates every 7.5 minutes with a random delay within each interval.
public_ip
: The public IP address of the Relay. If you have a static public IP, set it here. If you have a dynamic public IP, leave it blank, and the public IP will be discovered using icanhazip.com.uuid
: The unique identifier for the Relay. If not provided, it will be generated by the call-home service, and saved to the configuration file.call_home_enabled
: Enable or disable the call-home service.
{
"uuid": "87886fd8-0b28-47af-9580-cf0a1350a5ea",
"callsign": "KC1AWV",
"ip_address": "192.168.1.120",
"udp_port": 17000,
"status": true
}
The call-home request includes the following headers:
Content-Type
:application/json
X-Relay-Program
:go-m17-relay_v0.0.1
Byte | Size | Purpose |
---|---|---|
0-3 | 4 bytes | Magic - ASCII "CONN" |
4-9 | 6 bytes | 'from' callsign as encoded per M17 Address Encoding |
10 | 1 byte | ASCII (A-Z) module to connect to (Optional. Ignored for relays, kept for backwards compatibilty with mrefd/urfd clients) |
Byte | Size | Purpose |
---|---|---|
0-3 | 4 bytes | Magic - ASCII "LINK" |
4-9 | 6 bytes | 'from' callsign as encoded per M17 Address Encoding |
Byte | Size | Purpose |
---|---|---|
0-3 | 4 bytes | Magic - ASCII "LSTN" |
4-9 | 6 bytes | 'from' callsign as encoded per M17 Address Encoding |
Byte | Size | Purpose |
---|---|---|
0-3 | 4 bytes | Magic - ASCII "ACKN" |
Byte | Size | Purpose |
---|---|---|
0-3 | 4 bytes | Magic - ASCII "NACK" |
Byte | Size | Purpose |
---|---|---|
0-3 | 4 bytes | Magic - ASCII "PING" |
4-9 | 6 bytes | 'from' callsign as encoded per M17 Address Encoding |
Byte | Size | Purpose |
---|---|---|
0-3 | 4 bytes | Magic - ASCII "PONG" |
4-9 | 6 bytes | 'from' callsign as encoded per M17 Address Encoding |
Byte | Size | Purpose |
---|---|---|
0-3 | 4 bytes | Magic - ASCII "INFO" |
4 | 1 byte | ASCII character '?' |
Byte | Size | Purpose |
---|---|---|
0-3 | 4 bytes | Magic - ASCII "INFO" |
4-9 | 6 bytes | 'from' callsign as encoded per M17 Address Encoding |
10-13 | 4 bytes | Relay uptime in seconds |
14-15 | 2 bytes | Number of Clients connected to Relay |
16-17 | 2 bytes | Number of Relays interlinked to Relay |
Byte | Size | Purpose |
---|---|---|
0-3 | 4 bytes | Magic - ASCII "DISC" |
4-9 | 6 bytes | 'from' callsign as encoded per M17 Address Encoding |
Field | Size | Description |
---|---|---|
MAGIC | 4 bytes | Magic bytes 0x4d313720 (M17 ) |
StreamID (SID) | 2 bytes | Random bits, changed for each PTT or stream, but consistent from frame to frame within a stream |
LICH | 28 bytes | The meaningful contents of a LICH frame defined in the M17 Protocol Specification |
FN | 2 bytes | Frame number including the last frame indicator at (FN & 0x8000) |
Payload | 16 bytes | Payload (exactly as would be transmitted in an RF stream frame) |
Reserved | 2 bytes | Reserved two byte field for future use. Originally CRC16, but is not needed over IP |
Total: 54 bytes (432 bits)
main()
: The entry point of the application. It loads the configuration, initializes the relay, and starts the services.validateConfig(cfg *config.Config) error
: Validates the configuration.isValidIPAddress(addr string)
: Validates bind and target relay addresses.startServices(ctx context.Context, r *relay.Relay, wg *sync.WaitGroup)
: Starts the relay services.
NewRelay(addr string, callsign string) *Relay
: Initializes a new Relay object.Listen(ctx context.Context, wg *sync.WaitGroup)
: Starts receiving packets and processes them based on the packet type.ConnectToRelays(ctx context.Context, wg *sync.WaitGroup)
: Connects to target relays.PingPeers(ctx context.Context, wg *sync.WaitGroup)
: Pings connected peers.RemoveInactivePeers(ctx context.Context, wg *sync.WaitGroup)
: Removes inactive peers.LogPeerState(ctx context.Context, wg *sync.WaitGroup)
: Logs the state of connected peers.sendLinkPacket(targetAddr string) error
: Sends a LINK packet to the specified target address.sendPing(addr string)
: Sends a PING packet to the specified client.sendPong(addr *net.UDPAddr, callsign string)
: Sends a PONG packet to the specified address.sendPacket(magic string, addr *net.UDPAddr, callsign []byte)
: Sends a control packet to the specified address.handleControlPacket(data []byte, addr *net.UDPAddr)
: Processes incoming control packets.handleConnPacket(callsign string, addr *net.UDPAddr, module byte)
: Processes a connection request (CONN packet).handleLinkPacket(callsign string, addr *net.UDPAddr)
: Processes a link request (LINK packet).handleLstnPacket(callsign string, addr *net.UDPAddr)
: Processes a listen request (LSTN packet).handleAcknPacket(addr *net.UDPAddr)
: Processes an acknowledgment (ACKN) packet.handlePingPacket(callsign string, addr *net.UDPAddr)
: Processes a PING packet and responds with a PONG packet.handlePongPacket(callsign string, addr *net.UDPAddr)
: Processes a PONG packet.handleInfoPacket(addr *net.UDPAddr)
: Processes an INFO? (query) packet and responds with an INFO packet.handleDiscPacket(callsign string, addr *net.UDPAddr)
: Processes a DISCONNECT (DISC) packet and removes the client.relayDataPacket(packet []byte, senderAddr *net.UDPAddr)
: Handles data packets from clients.
NewMetricsCollector(relayCallsign string) *MetricsCollector
: Creates a new MetricsCollector.UpdateMetrics(clients, relays map[string]PeerInfo)
: Updates the metrics.GetMetrics() Metrics
: Returns the current metrics.InitializeHTTPServer(ctx context.Context, wg *sync.WaitGroup, cfg *config.Config)
: Initializes and starts the HTTP server.ServeMetrics(w http.ResponseWriter, req *http.Request)
: Serves the metrics as a JSON response.ServeWebInterface(w http.ResponseWriter, req *http.Request)
: Serves a simple web interface to display the metrics.
InitLogLevel(level string)
: Initializes the logging level.LogDebug(message string, fields map[string]interface{})
: Logs a debug message.LogInfo(message string, fields map[string]interface{})
: Logs an info message.LogWarn(message string, fields map[string]interface{})
: Logs a warning message.LogError(message string, fields map[string]interface{})
: Logs an error message.
CallHome(ctx context.Context, cfg *config.Config)
: Sends a call-home request to the central server.StartPeriodicCallHome(ctx context.Context, wg *sync.WaitGroup, cfg *config.Config)
: Starts the periodic call-home service.getExternalIP() (string, error)
: Retrieves the external IP address of the relay.saveConfig(cfg *config.Config) error
: Saves the updated configuration to the config file.
- Configure the relay server by copying
config.dist
toconfig.json
and editing theconfig.json
file. - Run the relay server using the following command:
Tip
Config file location optional, defaults to working directory
go run main.go [-config /path/to/config.json]
- The relay server will start and log messages based on the configured log level.
This project is licensed under the GNU General Public License v3.0. See the LICENSE file for more details.
Contributions are welcome! Please open an issue or submit a pull request on GitHub.