147 lines
6.0 KiB
Go
147 lines
6.0 KiB
Go
package bootstrap
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
"net/url"
|
|
"os"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
|
|
|
"remlink/internal/identity"
|
|
"remlink/internal/model"
|
|
)
|
|
|
|
// EnrollConfig contains first-run inputs which are deliberately not persisted
|
|
// as plaintext secrets. JoinToken is used only if identity has no NodeToken.
|
|
type EnrollConfig struct {
|
|
NodeType model.NodeType
|
|
NodeName string
|
|
ServerURL string
|
|
JoinToken string
|
|
Version string
|
|
OSVersion string
|
|
}
|
|
|
|
// Enroll loads or creates an identity, calls register/config, validates the
|
|
// authoritative NetworkConfig, and persists the latest token/version.
|
|
func Enroll(ctx context.Context, store *identity.Store, client *Client, config EnrollConfig) (identity.Identity, NetworkConfig, error) {
|
|
current, err := store.Load()
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
current, err = identity.New(config.NodeType, config.NodeName, config.ServerURL)
|
|
if err != nil {
|
|
return identity.Identity{}, NetworkConfig{}, err
|
|
}
|
|
if err := store.Save(current); err != nil {
|
|
return identity.Identity{}, NetworkConfig{}, err
|
|
}
|
|
} else if err != nil {
|
|
return identity.Identity{}, NetworkConfig{}, err
|
|
}
|
|
if current.NodeType != config.NodeType {
|
|
return identity.Identity{}, NetworkConfig{}, errors.New("persisted Node type does not match this executable")
|
|
}
|
|
configuredServerURL := strings.TrimRight(strings.TrimSpace(config.ServerURL), "/")
|
|
if configuredServerURL != current.ServerURL {
|
|
// A first launch may create identity.json before the operator has filled
|
|
// in the real Server URL and Join Token. Until registration succeeds the
|
|
// identity has no server-side credentials or owned routes, so rebinding
|
|
// that local draft identity is safe and makes portable packages editable.
|
|
if current.NodeToken == "" && current.ConfigVersion == 0 && len(current.OwnedRoutes) == 0 {
|
|
current.ServerURL = configuredServerURL
|
|
if err := store.Save(current); err != nil {
|
|
return identity.Identity{}, NetworkConfig{}, fmt.Errorf("update unregistered Node identity Server URL: %w", err)
|
|
}
|
|
} else {
|
|
return identity.Identity{}, NetworkConfig{}, errors.New("配置的 Server URL 与已注册的 Node 身份不一致;如需切换 Server,请退出程序后删除 identity.json 并重新注册")
|
|
}
|
|
}
|
|
current.NodeName = strings.TrimSpace(config.NodeName)
|
|
var network NetworkConfig
|
|
if current.NodeToken == "" {
|
|
if config.JoinToken == "" {
|
|
return identity.Identity{}, NetworkConfig{}, errors.New("Join Token is required for first registration")
|
|
}
|
|
response, err := client.Register(ctx, RegisterRequest{
|
|
JoinToken: config.JoinToken, NodeID: current.NodeID, NodeType: current.NodeType,
|
|
NodeName: current.NodeName, WGPublicKey: current.PublicKey(),
|
|
Version: config.Version, OSVersion: config.OSVersion,
|
|
})
|
|
if err != nil {
|
|
return identity.Identity{}, NetworkConfig{}, err
|
|
}
|
|
current.NodeToken = response.NodeToken
|
|
network = response.Network
|
|
} else {
|
|
response, err := client.Config(ctx, ConfigRequest{NodeID: current.NodeID, NodeToken: current.NodeToken})
|
|
if err != nil {
|
|
return identity.Identity{}, NetworkConfig{}, err
|
|
}
|
|
network = response.Network
|
|
}
|
|
if err := ValidateNetworkConfig(network); err != nil {
|
|
return identity.Identity{}, NetworkConfig{}, err
|
|
}
|
|
current.ConfigVersion = network.ConfigVersion
|
|
if err := store.Save(current); err != nil {
|
|
return identity.Identity{}, NetworkConfig{}, err
|
|
}
|
|
return current, network, nil
|
|
}
|
|
|
|
// ValidateNetworkConfig rejects malformed or internally inconsistent Server data.
|
|
func ValidateNetworkConfig(config NetworkConfig) error {
|
|
prefix, err := netip.ParsePrefix(config.OverlayCIDR)
|
|
if err != nil || !prefix.Addr().Is4() || prefix != prefix.Masked() || prefix.Bits() == 0 || prefix.Bits() > 30 {
|
|
return errors.New("NetworkConfig overlay_cidr must be a canonical IPv4 prefix")
|
|
}
|
|
nodeIP, err := netip.ParseAddr(config.OverlayIP)
|
|
if err != nil || !usableOverlayAddress(prefix, nodeIP) {
|
|
return errors.New("NetworkConfig overlay_ip must belong to overlay_cidr")
|
|
}
|
|
serverIP, err := netip.ParseAddr(config.ServerOverlayIP)
|
|
if err != nil || !usableOverlayAddress(prefix, serverIP) || serverIP == nodeIP {
|
|
return errors.New("NetworkConfig server_overlay_ip must be a distinct address in overlay_cidr")
|
|
}
|
|
if _, err := wgtypes.ParseKey(config.ServerWGPublicKey); err != nil {
|
|
return fmt.Errorf("NetworkConfig Server WireGuard key: %w", err)
|
|
}
|
|
if config.ServerWGEndpoint == "" || config.ControlURL == "" {
|
|
return errors.New("NetworkConfig endpoints must not be empty")
|
|
}
|
|
wgHost, wgPortText, err := net.SplitHostPort(config.ServerWGEndpoint)
|
|
if err != nil || strings.TrimSpace(wgHost) == "" {
|
|
return errors.New("NetworkConfig server_wg_endpoint must be host:port")
|
|
}
|
|
wgPort, err := strconv.Atoi(wgPortText)
|
|
if err != nil || wgPort < 1 || wgPort > 65535 {
|
|
return errors.New("NetworkConfig server_wg_endpoint port is invalid")
|
|
}
|
|
controlURL, err := url.Parse(config.ControlURL)
|
|
if err != nil || (controlURL.Scheme != "ws" && controlURL.Scheme != "wss") || controlURL.Path != "/control" ||
|
|
controlURL.Opaque != "" || controlURL.User != nil || controlURL.RawQuery != "" || controlURL.ForceQuery || controlURL.Fragment != "" {
|
|
return errors.New("NetworkConfig control_url must be an absolute ws(s) /control URL")
|
|
}
|
|
controlHost, controlPortText, err := net.SplitHostPort(controlURL.Host)
|
|
if err != nil || controlHost != serverIP.String() {
|
|
return errors.New("NetworkConfig control_url must target server_overlay_ip with an explicit port")
|
|
}
|
|
controlPort, err := strconv.Atoi(controlPortText)
|
|
if err != nil || controlPort < 1 || controlPort > 65535 {
|
|
return errors.New("NetworkConfig control_url port is invalid")
|
|
}
|
|
if config.ConfigVersion == 0 || config.SessionUDPPort < 1 || config.SessionUDPPort > 65535 || config.MTU < 576 || config.MTU > 65535 {
|
|
return errors.New("NetworkConfig version, UDP port, or MTU is invalid")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CurrentOSVersion is a non-secret Bootstrap capability label.
|
|
func CurrentOSVersion() string { return runtime.GOOS + "/" + runtime.GOARCH }
|