mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-24 16:41:30 +02:00
Introduce a namespaced preference store owned by the profile manager, persisted next to the profile config as <id>.prefs.json and deleted with the profile. Sections are opaque JSON, so the profile manager stays free of any feature schema, and writes reuse the state file's atomic path. Migrate the Android SSH known-hosts store and session list onto it. Both previously lived outside the profile lifecycle: known hosts in a per-profile file under filesDir, the session list in Java SharedPreferences, each needing its own sweep against the live profile list to avoid outliving the profile they belonged to. A profile ID that got reused would have inherited the trusted keys of a deleted profile. Both now share the "ssh" and "ssh-sessions" namespaces of the profile's preferences, so deleting a profile takes them along and the Java-side pruning is gone. The known-hosts entries keep the OpenSSH line format, only the container changed, and host key verification keeps rejecting a changed key outright. SetKnownHostsPath becomes SetKnownHostsStore, taking the config dir and profile ID instead of a file path. Existing known-hosts files are not migrated: hosts trusted before this change prompt for confirmation once more, which errs towards safety.
169 lines
4.0 KiB
Go
169 lines
4.0 KiB
Go
//go:build android
|
|
|
|
package android
|
|
|
|
import (
|
|
"bytes"
|
|
"net"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
|
|
gossh "golang.org/x/crypto/ssh"
|
|
"golang.org/x/crypto/ssh/knownhosts"
|
|
)
|
|
|
|
const knownHostsNamespace = "ssh"
|
|
|
|
const (
|
|
hostKeyUnknown hostKeyVerdict = iota
|
|
hostKeyMatched
|
|
hostKeyChanged
|
|
)
|
|
|
|
var knownHostsMu sync.Mutex
|
|
|
|
type hostKeyVerdict uint8
|
|
|
|
type knownHostsSection struct {
|
|
KnownHosts []string `json:"knownHosts"`
|
|
}
|
|
|
|
type knownHostsStore struct {
|
|
prefs prefsStore
|
|
}
|
|
|
|
// RemoveKnownHost deletes every known-hosts entry for host:port from the
|
|
// profile's store, so a host trusted for a session that is being deleted does
|
|
// not linger. Java calls this only once no session targets that host, so a
|
|
// shared host stays trusted. A missing entry is not an error: the goal state
|
|
// is "absent".
|
|
func RemoveKnownHost(configDir, profileID, host string, port int) error {
|
|
store, err := openKnownHostsStore(configDir, profileID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return store.removeHost(host, port)
|
|
}
|
|
|
|
func openKnownHostsStore(configDir, profileID string) (*knownHostsStore, error) {
|
|
prefs, err := newProfilePrefs(configDir, profileID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &knownHostsStore{prefs: prefs}, nil
|
|
}
|
|
|
|
func (st *knownHostsStore) verify(hostname string, remote net.Addr, key gossh.PublicKey) (hostKeyVerdict, error) {
|
|
lines, err := st.lines()
|
|
if err != nil {
|
|
return hostKeyUnknown, err
|
|
}
|
|
targets := knownHostsTargets(hostname, remote)
|
|
|
|
verdict := hostKeyUnknown
|
|
for _, line := range lines {
|
|
pubKey, ok := knownHostsLineKey(line, targets)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if pubKey.Type() == key.Type() && bytes.Equal(pubKey.Marshal(), key.Marshal()) {
|
|
return hostKeyMatched, nil
|
|
}
|
|
verdict = hostKeyChanged
|
|
}
|
|
return verdict, nil
|
|
}
|
|
|
|
func (st *knownHostsStore) append(hostname string, remote net.Addr, key gossh.PublicKey) error {
|
|
line := knownhosts.Line(knownHostsTargets(hostname, remote), key)
|
|
|
|
knownHostsMu.Lock()
|
|
defer knownHostsMu.Unlock()
|
|
|
|
lines, err := st.lines()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return st.prefs.Put(knownHostsNamespace, knownHostsSection{KnownHosts: append(lines, line)})
|
|
}
|
|
|
|
func (st *knownHostsStore) removeHost(host string, port int) error {
|
|
target := knownhosts.Normalize(net.JoinHostPort(host, strconv.Itoa(port)))
|
|
|
|
knownHostsMu.Lock()
|
|
defer knownHostsMu.Unlock()
|
|
|
|
lines, err := st.lines()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
kept := make([]string, 0, len(lines))
|
|
for _, line := range lines {
|
|
if knownHostsLineMatches(line, target) {
|
|
continue
|
|
}
|
|
kept = append(kept, line)
|
|
}
|
|
if len(kept) == len(lines) {
|
|
return nil
|
|
}
|
|
return st.prefs.Put(knownHostsNamespace, knownHostsSection{KnownHosts: kept})
|
|
}
|
|
|
|
func (st *knownHostsStore) lines() ([]string, error) {
|
|
var section knownHostsSection
|
|
if _, err := st.prefs.Get(knownHostsNamespace, §ion); err != nil {
|
|
return nil, err
|
|
}
|
|
return section.KnownHosts, nil
|
|
}
|
|
|
|
func knownHostsTargets(hostname string, remote net.Addr) []string {
|
|
targets := []string{knownhosts.Normalize(hostname)}
|
|
if remote != nil {
|
|
if normalized := knownhosts.Normalize(remote.String()); normalized != targets[0] {
|
|
targets = append(targets, normalized)
|
|
}
|
|
}
|
|
return targets
|
|
}
|
|
|
|
func knownHostsLineKey(line string, targets []string) (gossh.PublicKey, bool) {
|
|
trimmed := strings.TrimSpace(line)
|
|
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
|
return nil, false
|
|
}
|
|
_, hosts, pubKey, _, _, err := gossh.ParseKnownHosts([]byte(trimmed))
|
|
if err != nil {
|
|
return nil, false
|
|
}
|
|
for _, host := range hosts {
|
|
for _, target := range targets {
|
|
if host == target {
|
|
return pubKey, true
|
|
}
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
// knownHostsLineMatches reports whether a known-hosts line's address list
|
|
// contains the normalized target. Comment and blank lines never match.
|
|
func knownHostsLineMatches(line, target string) bool {
|
|
trimmed := strings.TrimSpace(line)
|
|
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
|
return false
|
|
}
|
|
fields := strings.Fields(trimmed)
|
|
if len(fields) == 0 {
|
|
return false
|
|
}
|
|
for _, addr := range strings.Split(fields[0], ",") {
|
|
if addr == target {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|