package api // The VPN tab: this machine's WireGuard tunnels, who is on them, and — where the tunnel is // one this machine configures — adding, renaming and removing the devices that dial in. // // Per machine like every other tab: the tunnels shown and edited are the ones on the machine // in the switcher, which is the only honest way to show them, since the inbound server and an // outbound tunnel are different machines here. import ( "context" "encoding/json" "errors" "io" "log" "net/http" "strings" "hamlab/internal/vpn" ) func (s *Server) vpnReport(w http.ResponseWriter, r *http.Request) { jsonOK(w, 200, vpn.Look(r.Context())) } // tunnelOf finds the tunnel a request names, so every editing handler starts from the live // state rather than from whatever the page last saw. func (s *Server) tunnelOf(ctx context.Context, w http.ResponseWriter, name string) (vpn.Tunnel, bool) { for _, t := range vpn.Look(ctx).Tunnels { if t.Name == name { if !t.Managed { jsonErr(w, 400, vpn.ErrUnmanaged.Error()) return t, false } return t, true } } jsonErr(w, 404, "no such tunnel on this machine") return vpn.Tunnel{}, false } // addVPNPeer makes a device and hands back its profile. The profile holds a private key that // is generated for this call and stored nowhere, so it is the only time it can be read — the // same bargain as any password this console generates. func (s *Server) addVPNPeer(w http.ResponseWriter, r *http.Request) { t, ok := s.tunnelOf(r.Context(), w, r.PathValue("iface")) if !ok { return } var body vpn.NewPeer if err := json.NewDecoder(io.LimitReader(r.Body, 8<<10)).Decode(&body); err != nil { jsonErr(w, 400, "bad request") return } made, err := vpn.AddPeer(r.Context(), t, body) if err != nil { code := 400 if errors.Is(err, vpn.ErrUnmanaged) { code = 409 } jsonErr(w, code, err.Error()) return } // The name and address are worth a log line; the profile is emphatically not. log.Printf("vpn: device %q added to %s as %s (by %s)", made.Name, t.Name, made.Address, clientIP(r)) jsonOK(w, 200, made) } func (s *Server) editVPNPeer(w http.ResponseWriter, r *http.Request) { t, ok := s.tunnelOf(r.Context(), w, r.PathValue("iface")) if !ok { return } var body struct{ Name, Address string } if err := json.NewDecoder(io.LimitReader(r.Body, 4<<10)).Decode(&body); err != nil { jsonErr(w, 400, "bad request") return } section := r.PathValue("section") if err := vpn.EditPeer(r.Context(), t, section, body.Name, body.Address); err != nil { jsonErr(w, 400, err.Error()) return } log.Printf("vpn: device %s on %s changed (by %s)", section, t.Name, clientIP(r)) w.WriteHeader(204) } func (s *Server) removeVPNPeer(w http.ResponseWriter, r *http.Request) { t, ok := s.tunnelOf(r.Context(), w, r.PathValue("iface")) if !ok { return } section := r.PathValue("section") var body struct{ Confirm string } _ = json.NewDecoder(io.LimitReader(r.Body, 4<<10)).Decode(&body) // The name has to be typed back. Removing a device is the one action here that cannot be // undone — the key is gone and the profile on the device stops working — and it is one tap // away from a list of devices that all look alike. var want string for _, p := range t.Peers { if p.Section == section { want = p.Name } } if want != "" && !strings.EqualFold(strings.TrimSpace(body.Confirm), want) { jsonErr(w, 400, "type the device's name to remove it") return } if err := vpn.DeletePeer(r.Context(), t, section); err != nil { jsonErr(w, 400, err.Error()) return } log.Printf("vpn: device %s (%s) removed from %s (by %s)", section, want, t.Name, clientIP(r)) w.WriteHeader(204) }