diff --git a/README.md b/README.md index 9277ee6..a00c0e7 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,109 @@ You can view the Windows Event Log using Event Viewer or PowerShell: Get-EventLog -LogName Application -Source "OlmWireguardService" -Newest 10 ``` +## HTTP Endpoints + +Olm can be controlled with an embedded http server when using `--enable-http`. This allows you to start it as a daemon and trigger it with the following endpoints: + +### POST /connect +Initiates a new connection request. + +**Request Body:** +```json +{ + "id": "string", + "secret": "string", + "endpoint": "string" +} +``` + +**Required Fields:** +- `id`: Connection identifier +- `secret`: Authentication secret +- `endpoint`: Target endpoint URL + +**Response:** +- **Status Code:** `202 Accepted` +- **Content-Type:** `application/json` + +```json +{ + "status": "connection request accepted" +} +``` + +**Error Responses:** +- `405 Method Not Allowed` - Non-POST requests +- `400 Bad Request` - Invalid JSON or missing required fields + +### GET /status +Returns the current connection status and peer information. + +**Response:** +- **Status Code:** `200 OK` +- **Content-Type:** `application/json` + +```json +{ + "status": "connected", + "connected": true, + "tunnelIP": "100.89.128.3/20", + "version": "version_replaceme", + "peers": { + "10": { + "siteId": 10, + "connected": true, + "rtt": 145338339, + "lastSeen": "2025-08-13T14:39:17.208334428-07:00", + "endpoint": "p.fosrl.io:21820", + "isRelay": true + }, + "8": { + "siteId": 8, + "connected": false, + "rtt": 0, + "lastSeen": "2025-08-13T14:39:19.663823645-07:00", + "endpoint": "p.fosrl.io:21820", + "isRelay": true + } + } +} +``` + +**Fields:** +- `status`: Overall connection status ("connected" or "disconnected") +- `connected`: Boolean connection state +- `tunnelIP`: IP address and subnet of the tunnel (when connected) +- `version`: Olm version string +- `peers`: Map of peer statuses by site ID + - `siteId`: Peer site identifier + - `connected`: Boolean peer connection state + - `rtt`: Peer round-trip time (integer, nanoseconds) + - `lastSeen`: Last time peer was seen (RFC3339 timestamp) + - `endpoint`: Peer endpoint address + - `isRelay`: Whether the peer is relayed (true) or direct (false) + +**Error Responses:** +- `405 Method Not Allowed` - Non-GET requests + +## Usage Examples + +### Connect to a peer +```bash +curl -X POST http://localhost:8080/connect \ + -H "Content-Type: application/json" \ + -d '{ + "id": "31frd0uzbjvp721", + "secret": "h51mmlknrvrwv8s4r1i210azhumt6isgbpyavxodibx1k2d6", + "endpoint": "https://example.com" + }' +``` + +### Check connection status +```bash +curl http://localhost:8080/status +``` + ## Build ### Container diff --git a/httpserver/httpserver.go b/httpserver/httpserver.go index a3c3d3b..4f57cca 100644 --- a/httpserver/httpserver.go +++ b/httpserver/httpserver.go @@ -23,6 +23,8 @@ type PeerStatus struct { Connected bool `json:"connected"` RTT time.Duration `json:"rtt"` LastSeen time.Time `json:"lastSeen"` + Endpoint string `json:"endpoint,omitempty"` + IsRelay bool `json:"isRelay"` } // StatusResponse is returned by the status endpoint @@ -30,6 +32,7 @@ type StatusResponse struct { Status string `json:"status"` Connected bool `json:"connected"` TunnelIP string `json:"tunnelIP,omitempty"` + Version string `json:"version,omitempty"` PeerStatuses map[int]*PeerStatus `json:"peers,omitempty"` } @@ -42,6 +45,8 @@ type HTTPServer struct { peerStatuses map[int]*PeerStatus connectedAt time.Time isConnected bool + tunnelIP string + version string } // NewHTTPServer creates a new HTTP server @@ -87,8 +92,8 @@ func (s *HTTPServer) GetConnectionChannel() <-chan ConnectionRequest { return s.connectionChan } -// UpdatePeerStatus updates the status of a peer -func (s *HTTPServer) UpdatePeerStatus(siteID int, connected bool, rtt time.Duration) { +// UpdatePeerStatus updates the status of a peer including endpoint and relay info +func (s *HTTPServer) UpdatePeerStatus(siteID int, connected bool, rtt time.Duration, endpoint string, isRelay bool) { s.statusMu.Lock() defer s.statusMu.Unlock() @@ -103,6 +108,8 @@ func (s *HTTPServer) UpdatePeerStatus(siteID int, connected bool, rtt time.Durat status.Connected = connected status.RTT = rtt status.LastSeen = time.Now() + status.Endpoint = endpoint + status.IsRelay = isRelay } // SetConnectionStatus sets the overall connection status @@ -120,6 +127,37 @@ func (s *HTTPServer) SetConnectionStatus(isConnected bool) { } } +// SetTunnelIP sets the tunnel IP address +func (s *HTTPServer) SetTunnelIP(tunnelIP string) { + s.statusMu.Lock() + defer s.statusMu.Unlock() + s.tunnelIP = tunnelIP +} + +// SetVersion sets the olm version +func (s *HTTPServer) SetVersion(version string) { + s.statusMu.Lock() + defer s.statusMu.Unlock() + s.version = version +} + +// UpdatePeerRelayStatus updates only the relay status of a peer +func (s *HTTPServer) UpdatePeerRelayStatus(siteID int, endpoint string, isRelay bool) { + s.statusMu.Lock() + defer s.statusMu.Unlock() + + status, exists := s.peerStatuses[siteID] + if !exists { + status = &PeerStatus{ + SiteID: siteID, + } + s.peerStatuses[siteID] = status + } + + status.Endpoint = endpoint + status.IsRelay = isRelay +} + // handleConnect handles the /connect endpoint func (s *HTTPServer) handleConnect(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { @@ -163,6 +201,8 @@ func (s *HTTPServer) handleStatus(w http.ResponseWriter, r *http.Request) { resp := StatusResponse{ Connected: s.isConnected, + TunnelIP: s.tunnelIP, + Version: s.version, PeerStatuses: s.peerStatuses, } diff --git a/main.go b/main.go index 67ea1dd..a9cf81e 100644 --- a/main.go +++ b/main.go @@ -331,6 +331,7 @@ func runOlmMainWithArgs(ctx context.Context, args []string) { var httpServer *httpserver.HTTPServer if enableHTTP { httpServer = httpserver.NewHTTPServer(httpAddr) + httpServer.SetVersion(olmVersion) if err := httpServer.Start(); err != nil { logger.Fatal("Failed to start HTTP server: %v", err) } @@ -372,31 +373,31 @@ func runOlmMainWithArgs(ctx context.Context, args []string) { // } // } - // // wait until we have a client id and secret and endpoint - // waitCount := 0 - // for id == "" || secret == "" || endpoint == "" { - // select { - // case <-ctx.Done(): - // logger.Info("Context cancelled while waiting for credentials") - // return - // default: - // missing := []string{} - // if id == "" { - // missing = append(missing, "id") - // } - // if secret == "" { - // missing = append(missing, "secret") - // } - // if endpoint == "" { - // missing = append(missing, "endpoint") - // } - // waitCount++ - // if waitCount%10 == 1 { // Log every 10 seconds instead of every second - // logger.Debug("Waiting for missing parameters: %v (waiting %d seconds)", missing, waitCount) - // } - // time.Sleep(1 * time.Second) - // } - // } + // wait until we have a client id and secret and endpoint + waitCount := 0 + for id == "" || secret == "" || endpoint == "" { + select { + case <-ctx.Done(): + logger.Info("Context cancelled while waiting for credentials") + return + default: + missing := []string{} + if id == "" { + missing = append(missing, "id") + } + if secret == "" { + missing = append(missing, "secret") + } + if endpoint == "" { + missing = append(missing, "endpoint") + } + waitCount++ + if waitCount%10 == 1 { // Log every 10 seconds instead of every second + logger.Debug("Waiting for missing parameters: %v (waiting %d seconds)", missing, waitCount) + } + time.Sleep(1 * time.Second) + } + } // parse the mtu string into an int mtuInt, err = strconv.Atoi(mtu) @@ -644,10 +645,27 @@ func runOlmMainWithArgs(ctx context.Context, args []string) { logger.Error("Failed to configure interface: %v", err) } + // Set tunnel IP in HTTP server + if httpServer != nil { + httpServer.SetTunnelIP(wgData.TunnelIP) + } + peerMonitor = peermonitor.NewPeerMonitor( func(siteID int, connected bool, rtt time.Duration) { if httpServer != nil { - httpServer.UpdatePeerStatus(siteID, connected, rtt) + // Find the site config to get endpoint information + var endpoint string + var isRelay bool + for _, site := range wgData.Sites { + if site.SiteId == siteID { + endpoint = site.Endpoint + // TODO: We'll need to track relay status separately + // For now, assume not using relay unless we get relay data + isRelay = !doHolepunch + break + } + } + httpServer.UpdatePeerStatus(siteID, connected, rtt, endpoint, isRelay) } if connected { logger.Info("Peer %d is now connected (RTT: %v)", siteID, rtt) @@ -664,7 +682,7 @@ func runOlmMainWithArgs(ctx context.Context, args []string) { // loop over the sites and call ConfigurePeer for each one for _, site := range wgData.Sites { if httpServer != nil { - httpServer.UpdatePeerStatus(site.SiteId, false, 0) + httpServer.UpdatePeerStatus(site.SiteId, false, 0, site.Endpoint, false) } err = ConfigurePeer(dev, site, privateKey, endpoint) if err != nil { @@ -893,18 +911,23 @@ func runOlmMainWithArgs(ctx context.Context, args []string) { return } - var removeData RelayPeerData - if err := json.Unmarshal(jsonData, &removeData); err != nil { - logger.Error("Error unmarshaling remove data: %v", err) + var relayData RelayPeerData + if err := json.Unmarshal(jsonData, &relayData); err != nil { + logger.Error("Error unmarshaling relay data: %v", err) return } - primaryRelay, err := resolveDomain(removeData.Endpoint) + primaryRelay, err := resolveDomain(relayData.Endpoint) if err != nil { logger.Warn("Failed to resolve primary relay endpoint: %v", err) } - peerMonitor.HandleFailover(removeData.SiteId, primaryRelay) + // Update HTTP server to mark this peer as using relay + if httpServer != nil { + httpServer.UpdatePeerRelayStatus(relayData.SiteId, relayData.Endpoint, true) + } + + peerMonitor.HandleFailover(relayData.SiteId, primaryRelay) }) olm.RegisterHandler("olm/register/no-sites", func(msg websocket.WSMessage) {