summaryrefslogtreecommitdiffhomepage
path: root/ipn/conffile/conffile.go
blob: c221a3d8c8d94682e523f7d8a6076de8c3d1d146 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause

// Package conffile contains code to load, manipulate, and access config file
// settings.
package conffile

import (
	"bytes"
	"encoding/json"
	"fmt"
	"os"
	"runtime"

	"tailscale.com/feature/buildfeatures"
	"tailscale.com/ipn"
)

// Config describes a config file.
type Config struct {
	Path    string // disk path of HuJSON, or VMUserDataPath
	Raw     []byte // raw bytes from disk, in HuJSON form
	Std     []byte // standardized JSON form
	Version string // "alpha0" for now

	// Parsed is the parsed config, converted from its on-disk version to the
	// latest known format.
	//
	// As of 2023-10-15 there is exactly one format ("alpha0") so this is both
	// the on-disk format and the in-memory upgraded format.
	Parsed ipn.ConfigVAlpha
}

// WantRunning reports whether c is non-nil and it's configured to be running.
func (c *Config) WantRunning() bool {
	return c != nil && !c.Parsed.Enabled.EqualBool(false)
}

// VMUserDataPath is a sentinel value for Load to use to get the data
// from the VM's metadata service's user-data field.
const VMUserDataPath = "vm:user-data"

// hujsonStandardize is set to hujson.Standardize by conffile_hujson.go on
// platforms that support config files.
var hujsonStandardize func([]byte) ([]byte, error)

// Load reads and parses the config file at the provided path on disk.
func Load(path string) (*Config, error) {
	switch runtime.GOOS {
	case "ios", "android":
		// compile-time for deadcode elimination
		return nil, fmt.Errorf("config file loading not supported on %q", runtime.GOOS)
	}
	var c Config
	c.Path = path
	var err error

	switch path {
	case VMUserDataPath:
		c.Raw, err = readVMUserData()
	default:
		c.Raw, err = os.ReadFile(path)
	}
	if err != nil {
		return nil, err
	}
	if buildfeatures.HasHuJSONConf && hujsonStandardize != nil {
		c.Std, err = hujsonStandardize(c.Raw)
		if err != nil {
			return nil, fmt.Errorf("error parsing config file %s HuJSON/JSON: %w", path, err)
		}
	} else {
		c.Std = c.Raw // config file must be valid JSON with ts_omit_hujsonconf
	}
	var ver struct {
		Version string `json:"version"`
	}
	if err := json.Unmarshal(c.Std, &ver); err != nil {
		if !buildfeatures.HasHuJSONConf {
			return nil, fmt.Errorf("error parsing config file %s, which must be valid standard JSON: %w", path, err)
		}
		return nil, fmt.Errorf("error parsing config file %s: %w", path, err)
	}
	switch ver.Version {
	case "":
		return nil, fmt.Errorf("error parsing config file %s: no \"version\" field defined", path)
	case "alpha0":
	default:
		return nil, fmt.Errorf("error parsing config file %s: unsupported \"version\" value %q; want \"alpha0\" for now", path, ver.Version)
	}
	c.Version = ver.Version

	jd := json.NewDecoder(bytes.NewReader(c.Std))
	jd.DisallowUnknownFields()
	err = jd.Decode(&c.Parsed)
	if err != nil {
		return nil, fmt.Errorf("error parsing config file %s: %w", path, err)
	}
	if jd.More() {
		return nil, fmt.Errorf("error parsing config file %s: trailing data after JSON object", path)
	}
	return &c, nil
}