feat(easytier-go): expose CreateInstanceTOML and ShowNodeInfo (#2557)

This commit is contained in:
MiaM1ku
2026-09-08 20:27:52 +08:00
committed by GitHub
parent e972307510
commit b1f87f025b
8 changed files with 244 additions and 0 deletions
+11
View File
@@ -55,6 +55,17 @@ connection, err := instance.Dial(ctx, "tcp4", "10.144.0.2:8080")
packets, err := instance.ListenPacket("udp4", ":5353")
```
`CreateInstanceTOML` loads a native EasyTier TOML document. Pass an empty
`instanceID` to allocate a UUID. Existing `instance_id` and `instance_name`
keys in the document are replaced by the host.
```go
instance, err := host.CreateInstanceTOML(ctx, "office", "", configTOML)
```
`Instance.ShowNodeInfo` returns this instance's virtual IPv4 address and
advertised hostname.
### Web Client management
A host can also connect to an EasyTier Web configuration server. The embedded
+63
View File
@@ -6,6 +6,9 @@ import (
"net"
"net/netip"
"strconv"
"strings"
"github.com/EasyTier/EasyTier/easytier-go/proto/common"
"github.com/EasyTier/EasyTier/easytier-go/internal/artifact"
"github.com/EasyTier/EasyTier/easytier-go/internal/contextutil"
@@ -132,6 +135,66 @@ func (host *Host) CreateInstance(
return instance, nil
}
// CreateInstanceTOML creates an EasyTier instance from a TOML configuration.
// instanceName is used as the instance_name field. If instanceID is empty, a
// new UUID is generated. Existing instance_id and instance_name keys in the
// TOML are replaced by bindInstanceIdentity.
func (host *Host) CreateInstanceTOML(
ctx context.Context,
instanceName string,
instanceID string,
configTOML string,
) (*Instance, error) {
if host == nil {
return nil, fmt.Errorf("create instance with nil EasyTier host")
}
if ctx == nil {
return nil, fmt.Errorf("create EasyTier instance with nil context")
}
if strings.TrimSpace(configTOML) == "" {
return nil, fmt.Errorf("EasyTier TOML configuration is empty")
}
if instanceName == "" {
instanceName = "easytier"
}
var (
id *common.UUID
idString string
err error
)
if instanceID != "" {
id, idString, err = parseInstanceUUID(instanceID)
} else {
id, idString, err = newInstanceUUID()
}
if err != nil {
return nil, err
}
runtime, err := host.engine.CreateInstance(
ctx,
bindInstanceIdentity(stripInstanceIdentity(configTOML), idString, instanceName),
)
if err != nil {
return nil, err
}
instance := &Instance{
engine: runtime,
id: idString,
manager: host.manager,
}
if err := host.manager.register(&managedInstance{
id: id,
instance: instance,
owner: instanceOwnerApplication,
source: manage.ConfigSource_ConfigSourceUser,
name: instanceName,
}); err != nil {
_ = runtime.Close(contextutil.WithoutCancel(ctx))
return nil, err
}
return instance, nil
}
// Instances returns a stable snapshot of all application- and Web-owned instances.
func (host *Host) Instances() []*Instance {
if host == nil || host.manager == nil {
@@ -140,3 +140,21 @@ func quoteTOMLString(value string) string {
}
return string(encoded)
}
func stripInstanceIdentity(configTOML string) string {
var kept []string
for _, line := range strings.Split(configTOML, "\n") {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "instance_id") || strings.HasPrefix(trimmed, "instance_name") {
key := trimmed
if idx := strings.IndexByte(trimmed, '='); idx >= 0 {
key = strings.TrimSpace(trimmed[:idx])
}
if key == "instance_id" || key == "instance_name" {
continue
}
}
kept = append(kept, line)
}
return strings.TrimSpace(strings.Join(kept, "\n"))
}
@@ -0,0 +1,59 @@
package host
import (
"strings"
"testing"
)
func TestStripInstanceIdentityRemovesIdentityKeys(t *testing.T) {
input := `instance_id = "87ede5a2-9c3d-492d-9bbe-989b9d07e742"
instance_name = "old-name"
hostname = "node-a"
instance_identity = "keep"
[network_identity]
network_name = "example"
`
got := stripInstanceIdentity(input)
if strings.Contains(got, "instance_id =") {
t.Fatalf("instance_id was not stripped:\n%s", got)
}
if strings.Contains(got, "instance_name =") {
t.Fatalf("instance_name was not stripped:\n%s", got)
}
if !strings.Contains(got, `hostname = "node-a"`) {
t.Fatalf("hostname was stripped:\n%s", got)
}
if !strings.Contains(got, `instance_identity = "keep"`) {
t.Fatalf("unrelated key was stripped:\n%s", got)
}
if !strings.Contains(got, "[network_identity]") {
t.Fatalf("network identity section was stripped:\n%s", got)
}
}
func TestStripInstanceIdentityIgnoresCommentedAndPrefixedKeys(t *testing.T) {
input := `# instance_id = "commented"
instance_id_backup = "keep"
`
got := stripInstanceIdentity(input)
if !strings.Contains(got, `# instance_id = "commented"`) {
t.Fatalf("comment was stripped:\n%s", got)
}
if !strings.Contains(got, `instance_id_backup = "keep"`) {
t.Fatalf("prefixed key was stripped:\n%s", got)
}
}
func TestBindInstanceIdentityPrependsCanonicalKeys(t *testing.T) {
got := bindInstanceIdentity("hostname = \"node\"\n", "11111111-2222-4333-8444-555555555555", "mihomo")
wantPrefix := `instance_id = "11111111-2222-4333-8444-555555555555"
instance_name = "mihomo"
`
if !strings.HasPrefix(got, wantPrefix) {
t.Fatalf("bindInstanceIdentity() = %q, want prefix %q", got, wantPrefix)
}
if !strings.Contains(got, `hostname = "node"`) {
t.Fatalf("original config was lost: %q", got)
}
}
+21
View File
@@ -4,6 +4,7 @@ import (
"context"
"crypto/rand"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"sort"
@@ -710,6 +711,26 @@ func peerRoutePairs(
return pairs
}
func parseInstanceUUID(text string) (*common.UUID, string, error) {
normalized := strings.ReplaceAll(strings.TrimSpace(text), "-", "")
if len(normalized) != 32 {
return nil, "", fmt.Errorf("invalid EasyTier instance ID %q", text)
}
var value [16]byte
decoded, err := hex.DecodeString(normalized)
if err != nil || len(decoded) != 16 {
return nil, "", fmt.Errorf("invalid EasyTier instance ID %q", text)
}
copy(value[:], decoded)
id := &common.UUID{
Part1: binary.BigEndian.Uint32(value[0:4]),
Part2: binary.BigEndian.Uint32(value[4:8]),
Part3: binary.BigEndian.Uint32(value[8:12]),
Part4: binary.BigEndian.Uint32(value[12:16]),
}
return id, uuidString(id), nil
}
func newInstanceUUID() (*common.UUID, string, error) {
var value [16]byte
if _, err := rand.Read(value[:]); err != nil {
+16
View File
@@ -23,6 +23,9 @@ type PeerInfo = apiinstance.PeerInfo
// Route describes one route visible to an EasyTier instance.
type Route = apiinstance.Route
// NodeInfo describes this EasyTier instance.
type NodeInfo = apiinstance.NodeInfo
// ListPeer returns the peers visible to this EasyTier instance.
func (instance *Instance) ListPeer(
ctx context.Context,
@@ -65,6 +68,19 @@ func (instance *Instance) listRouteResponse(
return response, nil
}
// ShowNodeInfo returns this instance's node information, including its
// virtual IPv4 address and advertised hostname.
func (instance *Instance) ShowNodeInfo(ctx context.Context) (*NodeInfo, error) {
response, err := instance.showNodeInfo(ctx)
if err != nil {
return nil, err
}
if response.NodeInfo == nil {
return nil, fmt.Errorf("EasyTier ShowNodeInfo returned no node info")
}
return response.NodeInfo, nil
}
func (instance *Instance) showNodeInfo(
ctx context.Context,
) (*apiinstance.ShowNodeInfoResponse, error) {
+53
View File
@@ -0,0 +1,53 @@
package host
import (
"testing"
)
func TestParseInstanceUUIDAcceptsCanonicalAndHex(t *testing.T) {
canonical := "87ede5a2-9c3d-492d-9bbe-989b9d07e742"
id, text, err := parseInstanceUUID(canonical)
if err != nil {
t.Fatalf("parse canonical UUID: %v", err)
}
if text != canonical {
t.Fatalf("canonical UUID string = %q, want %q", text, canonical)
}
if id == nil {
t.Fatal("parsed UUID was nil")
}
_, compact, err := parseInstanceUUID("87EDE5A29C3D492D9BBE989B9D07E742")
if err != nil {
t.Fatalf("parse compact UUID: %v", err)
}
if compact != canonical {
t.Fatalf("compact UUID string = %q, want %q", compact, canonical)
}
}
func TestParseInstanceUUIDRejectsInvalidValues(t *testing.T) {
for _, input := range []string{"", "not-a-uuid", "87ede5a2-9c3d-492d-9bbe", "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"} {
if _, _, err := parseInstanceUUID(input); err == nil {
t.Fatalf("parseInstanceUUID(%q) succeeded", input)
}
}
}
func TestNewInstanceUUIDRoundTrip(t *testing.T) {
id, text, err := newInstanceUUID()
if err != nil {
t.Fatalf("newInstanceUUID: %v", err)
}
parsed, parsedText, err := parseInstanceUUID(text)
if err != nil {
t.Fatalf("parse generated UUID: %v", err)
}
if parsedText != text {
t.Fatalf("round-trip UUID string = %q, want %q", parsedText, text)
}
if parsed.GetPart1() != id.GetPart1() || parsed.GetPart2() != id.GetPart2() ||
parsed.GetPart3() != id.GetPart3() || parsed.GetPart4() != id.GetPart4() {
t.Fatalf("round-trip UUID parts = %+v, want %+v", parsed, id)
}
}
+3
View File
@@ -7,3 +7,6 @@ type PeerInfo = internalhost.PeerInfo
// Route describes one route visible to an EasyTier instance.
type Route = internalhost.Route
// NodeInfo describes this EasyTier instance.
type NodeInfo = internalhost.NodeInfo