mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-10-09 03:46:14 -08:00
* chore(go): use lowercase module import path * ci: scope checks for Go module changes
481 lines
11 KiB
Go
481 lines
11 KiB
Go
package host
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"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"
|
|
"github.com/easytier/easytier/easytier-go/internal/coreabi"
|
|
"github.com/easytier/easytier/easytier-go/internal/engine"
|
|
"github.com/easytier/easytier/easytier-go/platform"
|
|
"github.com/easytier/easytier/easytier-go/platform/netstd"
|
|
hostproto "github.com/easytier/easytier/easytier-go/proto"
|
|
"github.com/easytier/easytier/easytier-go/proto/api/manage"
|
|
)
|
|
|
|
type State int32
|
|
|
|
const (
|
|
StateCreated State = iota
|
|
StateStarting
|
|
StateRunning
|
|
StateStopping
|
|
StateStopped
|
|
)
|
|
|
|
type Options struct {
|
|
Platform platform.Services
|
|
PacketQueueCapacity int
|
|
}
|
|
|
|
type EmbeddedCoreInfo struct {
|
|
EasyTierCommit string
|
|
SHA256 string
|
|
}
|
|
|
|
// Event is one best-effort notification emitted by an EasyTier instance.
|
|
type Event = engine.Event
|
|
|
|
type Host struct {
|
|
engine *engine.Host
|
|
manager *instanceManager
|
|
}
|
|
|
|
type Instance struct {
|
|
engine *engine.Instance
|
|
id string
|
|
manager *instanceManager
|
|
}
|
|
|
|
func New(ctx context.Context, options Options) (*Host, error) {
|
|
if ctx == nil {
|
|
return nil, fmt.Errorf("create EasyTier host with nil context")
|
|
}
|
|
if artifact.Commit != hostproto.EasyTierCommit {
|
|
return nil, fmt.Errorf(
|
|
"embedded EasyTier commit %s does not match protobuf commit %s",
|
|
artifact.Commit,
|
|
hostproto.EasyTierCommit,
|
|
)
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
default:
|
|
}
|
|
manager := newInstanceManager()
|
|
runtime, err := engine.NewHost(
|
|
ctx,
|
|
engine.Options{
|
|
Services: mergeServices(options.Platform),
|
|
PacketQueueCapacity: options.PacketQueueCapacity,
|
|
Management: manager.handle,
|
|
},
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
host := &Host{engine: runtime, manager: manager}
|
|
manager.host = host
|
|
return host, nil
|
|
}
|
|
|
|
func CoreInfo() EmbeddedCoreInfo {
|
|
return EmbeddedCoreInfo{
|
|
EasyTierCommit: artifact.Commit,
|
|
SHA256: artifact.SHA256,
|
|
}
|
|
}
|
|
|
|
// CreateInstance creates an EasyTier instance from a built configuration.
|
|
func (host *Host) CreateInstance(
|
|
ctx context.Context,
|
|
config InstanceConfig,
|
|
) (*Instance, error) {
|
|
if host == nil {
|
|
return nil, fmt.Errorf("create instance with nil EasyTier host")
|
|
}
|
|
configTOML, err := encodeInstanceConfig(config)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
id, idString, err := newInstanceUUID()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
runtime, err := host.engine.CreateInstance(
|
|
ctx,
|
|
bindInstanceIdentity(configTOML, idString, config.document.networkName),
|
|
)
|
|
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: config.document.networkName,
|
|
}); err != nil {
|
|
_ = runtime.Close(contextutil.WithoutCancel(ctx))
|
|
return nil, err
|
|
}
|
|
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 {
|
|
return nil
|
|
}
|
|
entries := host.manager.snapshot()
|
|
instances := make([]*Instance, len(entries))
|
|
for index, entry := range entries {
|
|
instances[index] = entry.instance
|
|
}
|
|
return instances
|
|
}
|
|
|
|
func (host *Host) Close(ctx context.Context) error {
|
|
if host == nil {
|
|
return nil
|
|
}
|
|
if ctx == nil {
|
|
return fmt.Errorf("close EasyTier host with nil context")
|
|
}
|
|
err := host.engine.Close(ctx)
|
|
if ctx.Err() == nil {
|
|
host.manager.clear()
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (instance *Instance) Start(ctx context.Context) error {
|
|
if instance == nil || instance.engine == nil {
|
|
return fmt.Errorf("start nil EasyTier instance")
|
|
}
|
|
return instance.engine.Start(ctx)
|
|
}
|
|
|
|
func (instance *Instance) Stop(ctx context.Context) error {
|
|
if instance == nil || instance.engine == nil {
|
|
return fmt.Errorf("stop nil EasyTier instance")
|
|
}
|
|
return instance.engine.Stop(ctx)
|
|
}
|
|
|
|
func (instance *Instance) SendPacket(ctx context.Context, packet []byte) error {
|
|
if instance == nil || instance.engine == nil {
|
|
return fmt.Errorf("send packet through nil EasyTier instance")
|
|
}
|
|
return instance.engine.SendPacket(ctx, packet)
|
|
}
|
|
|
|
func (instance *Instance) ReceivePacket(ctx context.Context) ([]byte, error) {
|
|
if instance == nil || instance.engine == nil {
|
|
return nil, fmt.Errorf("receive packet from nil EasyTier instance")
|
|
}
|
|
return instance.engine.ReceivePacket(ctx)
|
|
}
|
|
|
|
// Events returns the instance's bounded event stream.
|
|
//
|
|
// Slow consumers may miss events. The channel closes with the instance.
|
|
func (instance *Instance) Events() <-chan Event {
|
|
if instance == nil || instance.engine == nil {
|
|
return nil
|
|
}
|
|
return instance.engine.Events()
|
|
}
|
|
|
|
func (instance *Instance) Dial(
|
|
ctx context.Context,
|
|
network string,
|
|
address string,
|
|
) (net.Conn, error) {
|
|
if instance == nil || instance.engine == nil {
|
|
return nil, fmt.Errorf("dial through nil EasyTier instance")
|
|
}
|
|
if ctx == nil {
|
|
return nil, fmt.Errorf("dial through EasyTier with nil context")
|
|
}
|
|
switch network {
|
|
case "tcp", "tcp4", "udp", "udp4":
|
|
default:
|
|
return nil, net.UnknownNetworkError(network)
|
|
}
|
|
peer, err := parseDialAddress(address)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var connection net.Conn
|
|
switch network {
|
|
case "tcp", "tcp4":
|
|
connection, err = instance.engine.Dial(ctx, peer)
|
|
case "udp", "udp4":
|
|
connection, err = instance.engine.DialUDP(ctx, peer)
|
|
}
|
|
if err != nil {
|
|
var target net.Addr = net.TCPAddrFromAddrPort(peer)
|
|
if network == "udp" || network == "udp4" {
|
|
target = net.UDPAddrFromAddrPort(peer)
|
|
}
|
|
return nil, &net.OpError{
|
|
Op: "dial",
|
|
Net: network,
|
|
Addr: target,
|
|
Err: err,
|
|
}
|
|
}
|
|
return connection, nil
|
|
}
|
|
|
|
func (instance *Instance) Listen(
|
|
network string,
|
|
address string,
|
|
) (net.Listener, error) {
|
|
if instance == nil || instance.engine == nil {
|
|
return nil, fmt.Errorf("listen through nil EasyTier instance")
|
|
}
|
|
if err := requireNetwork(network, "tcp"); err != nil {
|
|
return nil, err
|
|
}
|
|
port, err := parseBindPort(address)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
listener, err := instance.engine.Listen(port)
|
|
if err != nil {
|
|
return nil, &net.OpError{
|
|
Op: "listen",
|
|
Net: network,
|
|
Addr: &net.TCPAddr{
|
|
IP: net.IPv4zero,
|
|
Port: int(port),
|
|
},
|
|
Err: err,
|
|
}
|
|
}
|
|
return listener, nil
|
|
}
|
|
|
|
func (instance *Instance) ListenPacket(
|
|
network string,
|
|
address string,
|
|
) (net.PacketConn, error) {
|
|
if instance == nil || instance.engine == nil {
|
|
return nil, fmt.Errorf("listen for packets through nil EasyTier instance")
|
|
}
|
|
if err := requireNetwork(network, "udp"); err != nil {
|
|
return nil, err
|
|
}
|
|
port, err := parseBindPort(address)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
connection, err := instance.engine.ListenPacket(port)
|
|
if err != nil {
|
|
return nil, &net.OpError{
|
|
Op: "listen",
|
|
Net: network,
|
|
Addr: &net.UDPAddr{
|
|
IP: net.IPv4zero,
|
|
Port: int(port),
|
|
},
|
|
Err: err,
|
|
}
|
|
}
|
|
return connection, nil
|
|
}
|
|
|
|
func (instance *Instance) Wait(ctx context.Context) error {
|
|
if instance == nil || instance.engine == nil {
|
|
return fmt.Errorf("wait for nil EasyTier instance")
|
|
}
|
|
return instance.engine.Wait(ctx)
|
|
}
|
|
|
|
func (instance *Instance) State() State {
|
|
if instance == nil || instance.engine == nil {
|
|
return StateStopped
|
|
}
|
|
return mapState(instance.engine.State())
|
|
}
|
|
|
|
// ID returns the instance's stable UUID.
|
|
func (instance *Instance) ID() string {
|
|
if instance == nil {
|
|
return ""
|
|
}
|
|
return instance.id
|
|
}
|
|
|
|
func (instance *Instance) Close(ctx context.Context) error {
|
|
if instance == nil || instance.engine == nil {
|
|
return nil
|
|
}
|
|
err := instance.engine.Close(ctx)
|
|
if ctx != nil && ctx.Err() == nil && instance.manager != nil {
|
|
instance.manager.remove(instance)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func mergeServices(configured platform.Services) platform.Services {
|
|
services := netstd.Services()
|
|
if configured.Sockets != nil {
|
|
services.Sockets = configured.Sockets
|
|
}
|
|
if configured.DNS != nil {
|
|
services.DNS = configured.DNS
|
|
}
|
|
if configured.Environment != nil {
|
|
services.Environment = configured.Environment
|
|
}
|
|
services.Snapshot = configured.Snapshot
|
|
return services
|
|
}
|
|
|
|
func mapState(state coreabi.State) State {
|
|
switch state {
|
|
case coreabi.StateCreated:
|
|
return StateCreated
|
|
case coreabi.StateStarting:
|
|
return StateStarting
|
|
case coreabi.StateRunning:
|
|
return StateRunning
|
|
case coreabi.StateStopping:
|
|
return StateStopping
|
|
case coreabi.StateStopped:
|
|
return StateStopped
|
|
default:
|
|
return StateStopped
|
|
}
|
|
}
|
|
|
|
func requireNetwork(network string, protocol string) error {
|
|
if network == protocol || network == protocol+"4" {
|
|
return nil
|
|
}
|
|
return net.UnknownNetworkError(network)
|
|
}
|
|
|
|
func parseDialAddress(address string) (netip.AddrPort, error) {
|
|
host, port, err := splitAddress(address)
|
|
if err != nil {
|
|
return netip.AddrPort{}, err
|
|
}
|
|
ip, err := netip.ParseAddr(host)
|
|
if err != nil || !ip.Is4() {
|
|
return netip.AddrPort{}, &net.AddrError{
|
|
Err: "EasyTier data plane requires an IPv4 literal",
|
|
Addr: address,
|
|
}
|
|
}
|
|
return netip.AddrPortFrom(ip, port), nil
|
|
}
|
|
|
|
func parseBindPort(address string) (uint16, error) {
|
|
host, port, err := splitAddress(address)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if host != "" {
|
|
ip, parseErr := netip.ParseAddr(host)
|
|
if parseErr != nil || !ip.Is4() || !ip.IsUnspecified() {
|
|
return 0, &net.AddrError{
|
|
Err: "EasyTier listeners bind all overlay IPv4 addresses",
|
|
Addr: address,
|
|
}
|
|
}
|
|
}
|
|
return port, nil
|
|
}
|
|
|
|
func splitAddress(address string) (string, uint16, error) {
|
|
host, service, err := net.SplitHostPort(address)
|
|
if err != nil {
|
|
return "", 0, err
|
|
}
|
|
port, err := strconv.ParseUint(service, 10, 16)
|
|
if err != nil {
|
|
return "", 0, &net.AddrError{
|
|
Err: "port must be a decimal number from 0 to 65535",
|
|
Addr: address,
|
|
}
|
|
}
|
|
return host, uint16(port), nil
|
|
}
|