Files
KKRainbow 3d0c9c3ca5 chore(go): use lowercase module import path (#2560)
* chore(go): use lowercase module import path
* ci: scope checks for Go module changes
2026-09-10 15:13:55 +08:00

195 lines
5.5 KiB
Go

package engine
import (
"encoding/json"
"fmt"
"net/netip"
"net/url"
"github.com/easytier/easytier/easytier-go/platform"
)
const createConfigVersion = 14
const webClientConfigVersion = 1
type createEnvelope struct {
Version uint32 `json:"version"`
Config string `json:"config"`
Environment environmentSnapshot `json:"environment"`
}
type webClientEnvelope struct {
Version uint32 `json:"version"`
Endpoint string `json:"endpoint"`
MachineID string `json:"machine_id"`
Hostname string `json:"hostname"`
SecureMode bool `json:"secure_mode"`
OSType string `json:"os_type"`
Environment environmentSnapshot `json:"environment"`
}
type environmentSnapshot struct {
PublicIPv4 *string `json:"public_ipv4"`
InterfaceIPv4s []string `json:"interface_ipv4s"`
PublicIPv6 *string `json:"public_ipv6"`
InterfaceIPv6s []string `json:"interface_ipv6s"`
MappedListeners []string `json:"mapped_listeners"`
LocalIPs []string `json:"local_ips"`
ProtectedTCPPorts []uint16 `json:"protected_tcp_ports"`
PreferredIPv6Sources []preferredIPv6Source `json:"preferred_ipv6_sources"`
}
type preferredIPv6Source struct {
IP string `json:"ip"`
IfIndex uint32 `json:"ifindex"`
}
func encodeCreateEnvelope(
configTOML string,
snapshot platform.EnvironmentSnapshot,
) ([]byte, error) {
if configTOML == "" {
return nil, fmt.Errorf("EasyTier TOML configuration is empty")
}
environment, err := encodeEnvironmentSnapshot(snapshot)
if err != nil {
return nil, err
}
encoded, err := json.Marshal(createEnvelope{
Version: createConfigVersion,
Config: configTOML,
Environment: environment,
})
if err != nil {
return nil, fmt.Errorf("encode EasyTier create envelope: %w", err)
}
return encoded, nil
}
func encodeWebClientEnvelope(
options WebClientOptions,
snapshot platform.EnvironmentSnapshot,
) ([]byte, error) {
if options.Endpoint == "" {
return nil, fmt.Errorf("EasyTier WebClient endpoint is empty")
}
if options.MachineID == "" {
return nil, fmt.Errorf("EasyTier WebClient machine ID is empty")
}
if options.Hostname == "" {
return nil, fmt.Errorf("EasyTier WebClient hostname is empty")
}
environment, err := encodeEnvironmentSnapshot(snapshot)
if err != nil {
return nil, err
}
encoded, err := json.Marshal(webClientEnvelope{
Version: webClientConfigVersion,
Endpoint: options.Endpoint,
MachineID: options.MachineID,
Hostname: options.Hostname,
SecureMode: options.SecureMode,
OSType: options.OSType,
Environment: environment,
})
if err != nil {
return nil, fmt.Errorf("encode EasyTier WebClient envelope: %w", err)
}
return encoded, nil
}
func encodeEnvironmentSnapshot(
snapshot platform.EnvironmentSnapshot,
) (environmentSnapshot, error) {
encoded := environmentSnapshot{
InterfaceIPv4s: make([]string, len(snapshot.InterfaceIPv4s)),
InterfaceIPv6s: make([]string, len(snapshot.InterfaceIPv6s)),
MappedListeners: append([]string{}, snapshot.MappedListeners...),
LocalIPs: make([]string, len(snapshot.LocalIPs)),
ProtectedTCPPorts: append([]uint16{}, snapshot.ProtectedTCPPorts...),
PreferredIPv6Sources: make([]preferredIPv6Source, len(snapshot.PreferredIPv6Sources)),
}
var err error
encoded.PublicIPv4, err = encodeOptionalIP("public IPv4", snapshot.PublicIPv4, true)
if err != nil {
return environmentSnapshot{}, err
}
encoded.PublicIPv6, err = encodeOptionalIP("public IPv6", snapshot.PublicIPv6, false)
if err != nil {
return environmentSnapshot{}, err
}
for index, address := range snapshot.InterfaceIPv4s {
if !address.Is4() {
return environmentSnapshot{}, fmt.Errorf(
"interface IPv4 %d is not IPv4: %s",
index,
address,
)
}
encoded.InterfaceIPv4s[index] = address.String()
}
for index, address := range snapshot.InterfaceIPv6s {
if !isIPv6(address) {
return environmentSnapshot{}, fmt.Errorf(
"interface IPv6 %d is not IPv6: %s",
index,
address,
)
}
encoded.InterfaceIPv6s[index] = address.String()
}
for index, listener := range encoded.MappedListeners {
parsed, parseErr := url.ParseRequestURI(listener)
if parseErr != nil || parsed.Scheme == "" {
return environmentSnapshot{}, fmt.Errorf(
"mapped listener %d is not an absolute URL: %q",
index,
listener,
)
}
}
for index, address := range snapshot.LocalIPs {
if !address.IsValid() {
return environmentSnapshot{}, fmt.Errorf("local IP %d is invalid", index)
}
encoded.LocalIPs[index] = address.String()
}
for index, source := range snapshot.PreferredIPv6Sources {
if !isIPv6(source.IP) {
return environmentSnapshot{}, fmt.Errorf(
"preferred IPv6 source %d is not IPv6: %s",
index,
source.IP,
)
}
encoded.PreferredIPv6Sources[index] = preferredIPv6Source{
IP: source.IP.String(),
IfIndex: source.IfIndex,
}
}
return encoded, nil
}
func encodeOptionalIP(
name string,
address *netip.Addr,
ipv4 bool,
) (*string, error) {
if address == nil {
return nil, nil
}
valid := address.Is4()
if !ipv4 {
valid = isIPv6(*address)
}
if !valid {
return nil, fmt.Errorf("%s has wrong address family: %s", name, address)
}
encoded := address.String()
return &encoded, nil
}
func isIPv6(address netip.Addr) bool {
return address.Is6() && !address.Is4In6()
}