mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-10-09 03:46:14 -08:00
* fix(core): normalize secure keys for TOML instances
* feat(wasi): run core behind Cloudflare WebSockets
Introduce the Cloudflare Worker WASI host that runs the EasyTier core
behind host-upgraded WebSockets.
- Worker package scaffold (wrangler Durable Object, build-wasm script,
vitest config) and core-runtime/websocket-host/data-plane runtime.
- WASI host WebSocket tunnel ABI (imports, adapter, runtime exports)
with bounded receive memory and bounded admission queue.
- Route host sockets through the portable listener plan
(HostListenerRegistration, listener queue, admission handler split).
- Build the WASM guest with the aes-gcm feature so secure peer
sessions have their cipher available.
* feat(wasi): add outbound browser client runtime
Add the outbound-only WASI runtime and browser connector host so
browser pages can dial EasyTier peers through WebSocket relays.
- CoreConnectivityMode::{OutboundOnly, InboundOnly} gating for
listeners, discovery, and direct connectivity modules.
- ExternalTunnelConnector plumbing through composite/connector_host/
manual for browser WebSocket dials.
- Browser/Node smoke entries with shared helpers
(smoke-shared.ts).
* feat(wasi): extend browser data plane with TCP half-close
Add the data-plane pieces the browser runtime needs for full-duplex
TCP streams behind host WebSockets:
- Guest TCP shutdown_write operation with submit/take ABI pair
(DATA_PLANE_ABI_VERSION 3 -> 4) and smoltcp half-close support.
- Worker data-plane TCP listener/stream plumbing and core-runtime
listener registration.
- Unit coverage for the new session ops and listener wiring.
* refactor(wasi): make host tunnel ABI transport-neutral
Replace WebSocket-specific core and WASI boundaries with a
message-oriented Host Tunnel interface. Keep WebSocket framing and text
rejection in the Cloudflare host while preserving payload boundaries,
ownership, cancellation, backpressure, and EOF behavior.
Rename feature flags and guest imports and exports to the Host Tunnel
ABI. Update both Worker profiles, tests, and architecture documentation.
* feat(web): split WASI hosts into publishable npm packages
Extract the shared JSPI, WASI, Host Tunnel, and data-plane runtime
into @easytier/runtime. Keep ABI handles, guest memory, TOML, and
operation broker details behind its adapter entry point.
Add typed, auto-starting @easytier/browser and factory-based
@easytier/cloudflare packages. Ship a matching Wasm profile with
each platform package and validate its capabilities before packing.
Persist Cloudflare instance identity in Durable Object storage,
centralize WebSocket admission ownership, and add package-level
coverage for the public interfaces.
* fix(web): make public packages portable
Embed the browser Wasm artifact in the published JavaScript entry
point. This lets esbuild consumers bundle the package without an asset
loader or a copied file.
Return Cloudflare's nominal Durable Object base type and document the
named subclass export required by generated Wrangler bindings.
* docs(web): add public package walkthrough
Expand both package READMEs with installation, configuration, local
validation, health checks, and deployment instructions.
Add a standalone Vite and Wrangler example that imports only the
public Browser and Cloudflare entries. Generate Worker bindings from
configuration and keep local secrets outside version control.
* chore(go): import EasyTier Go host
Add the standalone Go host runtime as a monorepo subtree without
carrying its development branch ancestry.
Preserve its API, tests, examples, generated protobuf bindings, and
embedded WASI artifacts.
* refactor(hosts): colocate Go and JavaScript runtimes
Move the browser, Cloudflare, shared runtime, and web example into
the easytier-js subtree. Update workspace metadata, build paths, and
documentation for the new layout.
Adopt github.com/EasyTier/EasyTier/easytier-go as the Go module path.
Resolve artifact and protobuf generation from the enclosing monorepo.
* build(web): isolate JavaScript host workspace
Keep public browser and Cloudflare packages outside the legacy frontend
workspace so root installs and cross-platform builds do not pull workerd.
Make each package build generate its required WASI artifact from a clean
checkout. Add a dedicated workflow that runs the same install and check
commands documented for contributors.
Move JavaScript dependencies into a scoped lockfile and restore the root
workspace lockfile to its pre-host state.
608 lines
17 KiB
Go
608 lines
17 KiB
Go
package host
|
|
|
|
import (
|
|
"crypto/ecdh"
|
|
"crypto/rand"
|
|
"fmt"
|
|
"net/netip"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
// P2PPolicy controls when an instance attempts peer-to-peer connections.
|
|
//
|
|
// Disable normally suppresses P2P, but a peer advertising Need may still
|
|
// request it. Need advertises that this instance requires P2P and may override
|
|
// a peer's Disable setting. Lazy defers background P2P until traffic needs it,
|
|
// except for peers advertising Need.
|
|
type P2PPolicy struct {
|
|
Disable bool
|
|
Need bool
|
|
Lazy bool
|
|
}
|
|
|
|
// HolePunchingPolicy selects the hole-punching methods available to an
|
|
// instance. SymmetricUDP requires UDP.
|
|
type HolePunchingPolicy struct {
|
|
TCP bool
|
|
UDP bool
|
|
SymmetricUDP bool
|
|
}
|
|
|
|
// PortForwardProtocol identifies the transport used by a port-forward rule.
|
|
type PortForwardProtocol string
|
|
|
|
const (
|
|
PortForwardTCP PortForwardProtocol = "tcp"
|
|
PortForwardUDP PortForwardProtocol = "udp"
|
|
)
|
|
|
|
// PortForwardConfig exposes a host socket through the EasyTier data plane.
|
|
type PortForwardConfig struct {
|
|
Protocol PortForwardProtocol
|
|
Bind netip.AddrPort
|
|
Destination netip.AddrPort
|
|
}
|
|
|
|
// InstanceConfig is an immutable EasyTier instance configuration.
|
|
//
|
|
// Its zero value is invalid. Construct one with InstanceConfigBuilder.
|
|
type InstanceConfig struct {
|
|
document *instanceConfigDocument
|
|
}
|
|
|
|
// InstanceConfigBuilder builds a validated InstanceConfig.
|
|
//
|
|
// A builder is not safe for concurrent use.
|
|
type InstanceConfigBuilder struct {
|
|
document instanceConfigDocument
|
|
networkSecretSet bool
|
|
}
|
|
|
|
type instanceConfigDocument struct {
|
|
networkName string
|
|
networkSecret string
|
|
hostname *string
|
|
ipv4 *netip.Prefix
|
|
peers []string
|
|
listeners []string
|
|
portForwards []PortForwardConfig
|
|
stunServers []string
|
|
stunServersSet bool
|
|
stunServersV6 []string
|
|
stunServersV6Set bool
|
|
p2p *P2PPolicy
|
|
holePunching *HolePunchingPolicy
|
|
encryption *bool
|
|
secureMode secureMode
|
|
securePrivateKey []byte
|
|
}
|
|
|
|
type secureMode uint8
|
|
|
|
const (
|
|
secureModeDisabled secureMode = iota
|
|
secureModeAutomatic
|
|
secureModeManual
|
|
)
|
|
|
|
// NewInstanceConfigBuilder starts a configuration for networkName.
|
|
func NewInstanceConfigBuilder(networkName string) *InstanceConfigBuilder {
|
|
return &InstanceConfigBuilder{
|
|
document: instanceConfigDocument{networkName: networkName},
|
|
}
|
|
}
|
|
|
|
// NetworkSecret selects shared-secret network authentication.
|
|
//
|
|
// An empty secret is valid unless secure mode is enabled.
|
|
func (builder *InstanceConfigBuilder) NetworkSecret(
|
|
secret string,
|
|
) *InstanceConfigBuilder {
|
|
builder.document.networkSecret = secret
|
|
builder.networkSecretSet = true
|
|
return builder
|
|
}
|
|
|
|
// Hostname sets the hostname advertised by this instance.
|
|
func (builder *InstanceConfigBuilder) Hostname(
|
|
hostname string,
|
|
) *InstanceConfigBuilder {
|
|
builder.document.hostname = &hostname
|
|
return builder
|
|
}
|
|
|
|
// IPv4 sets the instance's virtual IPv4 address and network prefix.
|
|
func (builder *InstanceConfigBuilder) IPv4(
|
|
prefix netip.Prefix,
|
|
) *InstanceConfigBuilder {
|
|
builder.document.ipv4 = &prefix
|
|
return builder
|
|
}
|
|
|
|
// AddPeers appends TCP or UDP peer endpoints.
|
|
func (builder *InstanceConfigBuilder) AddPeers(
|
|
uris ...string,
|
|
) *InstanceConfigBuilder {
|
|
builder.document.peers = append(builder.document.peers, uris...)
|
|
return builder
|
|
}
|
|
|
|
// AddListeners appends TCP or UDP listener endpoints.
|
|
func (builder *InstanceConfigBuilder) AddListeners(
|
|
uris ...string,
|
|
) *InstanceConfigBuilder {
|
|
builder.document.listeners = append(builder.document.listeners, uris...)
|
|
return builder
|
|
}
|
|
|
|
// AddPortForwards appends TCP or UDP host-to-overlay forwarding rules.
|
|
func (builder *InstanceConfigBuilder) AddPortForwards(
|
|
forwards ...PortForwardConfig,
|
|
) *InstanceConfigBuilder {
|
|
builder.document.portForwards = append(builder.document.portForwards, forwards...)
|
|
return builder
|
|
}
|
|
|
|
// STUNServers replaces the IPv4 UDP STUN server list.
|
|
//
|
|
// Calling STUNServers with no arguments explicitly disables the list. Omitting
|
|
// the call uses the embedded core's defaults.
|
|
func (builder *InstanceConfigBuilder) STUNServers(
|
|
servers ...string,
|
|
) *InstanceConfigBuilder {
|
|
builder.document.stunServers = append([]string(nil), servers...)
|
|
builder.document.stunServersSet = true
|
|
return builder
|
|
}
|
|
|
|
// STUNServersV6 replaces the IPv6 UDP STUN server list.
|
|
//
|
|
// Calling STUNServersV6 with no arguments explicitly disables the list.
|
|
// Omitting the call uses the embedded core's defaults.
|
|
func (builder *InstanceConfigBuilder) STUNServersV6(
|
|
servers ...string,
|
|
) *InstanceConfigBuilder {
|
|
builder.document.stunServersV6 = append([]string(nil), servers...)
|
|
builder.document.stunServersV6Set = true
|
|
return builder
|
|
}
|
|
|
|
// P2P sets the instance's P2P connection policy.
|
|
func (builder *InstanceConfigBuilder) P2P(
|
|
policy P2PPolicy,
|
|
) *InstanceConfigBuilder {
|
|
builder.document.p2p = &policy
|
|
return builder
|
|
}
|
|
|
|
// HolePunching sets the instance's TCP and UDP hole-punching policy.
|
|
func (builder *InstanceConfigBuilder) HolePunching(
|
|
policy HolePunchingPolicy,
|
|
) *InstanceConfigBuilder {
|
|
builder.document.holePunching = &policy
|
|
return builder
|
|
}
|
|
|
|
// Encryption explicitly enables or disables EasyTier data encryption.
|
|
func (builder *InstanceConfigBuilder) Encryption(
|
|
enabled bool,
|
|
) *InstanceConfigBuilder {
|
|
builder.document.encryption = &enabled
|
|
return builder
|
|
}
|
|
|
|
// SecureMode enables secure mode with an automatically generated X25519 key.
|
|
func (builder *InstanceConfigBuilder) SecureMode() *InstanceConfigBuilder {
|
|
builder.document.secureMode = secureModeAutomatic
|
|
builder.document.securePrivateKey = nil
|
|
return builder
|
|
}
|
|
|
|
// SecureModeWithPrivateKey enables secure mode with a caller-provided raw
|
|
// 32-byte X25519 private key. The key is copied immediately.
|
|
func (builder *InstanceConfigBuilder) SecureModeWithPrivateKey(
|
|
privateKey []byte,
|
|
) *InstanceConfigBuilder {
|
|
builder.document.secureMode = secureModeManual
|
|
builder.document.securePrivateKey = append([]byte(nil), privateKey...)
|
|
return builder
|
|
}
|
|
|
|
// Build validates the builder and returns an immutable configuration.
|
|
func (builder *InstanceConfigBuilder) Build() (InstanceConfig, error) {
|
|
if builder == nil {
|
|
return InstanceConfig{}, invalidInstanceConfig("", "builder is nil")
|
|
}
|
|
if err := builder.validateIdentity(); err != nil {
|
|
return InstanceConfig{}, err
|
|
}
|
|
|
|
document := builder.document.clone()
|
|
if err := validateHostname(document.hostname); err != nil {
|
|
return InstanceConfig{}, err
|
|
}
|
|
if err := validateIPv4(document.ipv4); err != nil {
|
|
return InstanceConfig{}, err
|
|
}
|
|
if err := normalizeEndpoints(document.peers, false, "peers"); err != nil {
|
|
return InstanceConfig{}, err
|
|
}
|
|
if err := normalizeEndpoints(document.listeners, true, "listeners"); err != nil {
|
|
return InstanceConfig{}, err
|
|
}
|
|
if err := validatePortForwards(document.portForwards); err != nil {
|
|
return InstanceConfig{}, err
|
|
}
|
|
if document.stunServersSet {
|
|
if err := validateSTUNServers(document.stunServers, false, "stun_servers"); err != nil {
|
|
return InstanceConfig{}, err
|
|
}
|
|
}
|
|
if document.stunServersV6Set {
|
|
if err := validateSTUNServers(
|
|
document.stunServersV6,
|
|
true,
|
|
"stun_servers_v6",
|
|
); err != nil {
|
|
return InstanceConfig{}, err
|
|
}
|
|
}
|
|
if document.holePunching != nil &&
|
|
document.holePunching.SymmetricUDP &&
|
|
!document.holePunching.UDP {
|
|
return InstanceConfig{}, invalidInstanceConfig(
|
|
"hole_punching.symmetric_udp",
|
|
"requires UDP hole punching",
|
|
)
|
|
}
|
|
if document.secureMode != secureModeDisabled {
|
|
if document.networkSecret == "" {
|
|
return InstanceConfig{}, invalidInstanceConfig(
|
|
"secure_mode",
|
|
"requires a non-empty network secret",
|
|
)
|
|
}
|
|
privateKey, err := builder.secureModePrivateKey()
|
|
if err != nil {
|
|
return InstanceConfig{}, err
|
|
}
|
|
document.securePrivateKey = privateKey
|
|
}
|
|
|
|
return InstanceConfig{document: &document}, nil
|
|
}
|
|
|
|
func (builder *InstanceConfigBuilder) validateIdentity() error {
|
|
if !utf8.ValidString(builder.document.networkName) {
|
|
return invalidInstanceConfig("network.name", "must be valid UTF-8")
|
|
}
|
|
if builder.document.networkName == "" {
|
|
return invalidInstanceConfig("network.name", "must not be empty")
|
|
}
|
|
if !builder.networkSecretSet {
|
|
return invalidInstanceConfig("network.secret", "must be explicitly set")
|
|
}
|
|
if !utf8.ValidString(builder.document.networkSecret) {
|
|
return invalidInstanceConfig("network.secret", "must be valid UTF-8")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (builder *InstanceConfigBuilder) secureModePrivateKey() ([]byte, error) {
|
|
switch builder.document.secureMode {
|
|
case secureModeAutomatic:
|
|
if len(builder.document.securePrivateKey) == 0 {
|
|
privateKey, err := ecdh.X25519().GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("generate secure mode X25519 key: %w", err)
|
|
}
|
|
builder.document.securePrivateKey = privateKey.Bytes()
|
|
}
|
|
case secureModeManual:
|
|
if len(builder.document.securePrivateKey) != 32 {
|
|
return nil, invalidInstanceConfig(
|
|
"secure_mode.private_key",
|
|
"must contain exactly 32 bytes",
|
|
)
|
|
}
|
|
if _, err := ecdh.X25519().NewPrivateKey(
|
|
builder.document.securePrivateKey,
|
|
); err != nil {
|
|
return nil, invalidInstanceConfig(
|
|
"secure_mode.private_key",
|
|
"is not a valid X25519 private key",
|
|
)
|
|
}
|
|
default:
|
|
return nil, invalidInstanceConfig("secure_mode", "has an invalid mode")
|
|
}
|
|
return append([]byte(nil), builder.document.securePrivateKey...), nil
|
|
}
|
|
|
|
func validateHostname(hostname *string) error {
|
|
if hostname == nil {
|
|
return nil
|
|
}
|
|
if !utf8.ValidString(*hostname) {
|
|
return invalidInstanceConfig("hostname", "must be valid UTF-8")
|
|
}
|
|
if *hostname == "" {
|
|
return invalidInstanceConfig("hostname", "must not be empty")
|
|
}
|
|
if utf8.RuneCountInString(*hostname) > 32 {
|
|
return invalidInstanceConfig("hostname", "must not exceed 32 characters")
|
|
}
|
|
for _, character := range *hostname {
|
|
if unicode.IsControl(character) {
|
|
return invalidInstanceConfig("hostname", "must not contain control characters")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateIPv4(prefix *netip.Prefix) error {
|
|
if prefix == nil {
|
|
return nil
|
|
}
|
|
if !prefix.IsValid() || !prefix.Addr().Is4() {
|
|
return invalidInstanceConfig("ipv4", "must be a valid IPv4 prefix")
|
|
}
|
|
if prefix.Bits() == 32 {
|
|
return invalidInstanceConfig(
|
|
"ipv4",
|
|
"must include a network prefix shorter than /32",
|
|
)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validatePortForwards(forwards []PortForwardConfig) error {
|
|
seen := make(map[PortForwardConfig]struct{}, len(forwards))
|
|
for index, forward := range forwards {
|
|
field := fmt.Sprintf("port_forwards[%d]", index)
|
|
if forward.Protocol != PortForwardTCP &&
|
|
forward.Protocol != PortForwardUDP {
|
|
return invalidInstanceConfig(
|
|
field+".protocol",
|
|
"must be TCP or UDP",
|
|
)
|
|
}
|
|
if !forward.Bind.IsValid() || !forward.Bind.Addr().Is4() {
|
|
return invalidInstanceConfig(
|
|
field+".bind",
|
|
"must be a valid IPv4 socket address",
|
|
)
|
|
}
|
|
if !forward.Destination.IsValid() ||
|
|
!forward.Destination.Addr().Is4() {
|
|
return invalidInstanceConfig(
|
|
field+".destination",
|
|
"must be a valid IPv4 socket address",
|
|
)
|
|
}
|
|
if forward.Destination.Port() == 0 {
|
|
return invalidInstanceConfig(
|
|
field+".destination",
|
|
"port must not be zero",
|
|
)
|
|
}
|
|
if _, exists := seen[forward]; exists {
|
|
return invalidInstanceConfig(field, "duplicates another port forward")
|
|
}
|
|
seen[forward] = struct{}{}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func normalizeEndpoints(values []string, listener bool, field string) error {
|
|
seen := make(map[string]struct{}, len(values))
|
|
for index, value := range values {
|
|
normalized, err := normalizeEndpoint(value, listener)
|
|
if err != nil {
|
|
return invalidInstanceConfig(
|
|
fmt.Sprintf("%s[%d]", field, index),
|
|
err.Error(),
|
|
)
|
|
}
|
|
if _, exists := seen[normalized]; exists {
|
|
return invalidInstanceConfig(
|
|
fmt.Sprintf("%s[%d]", field, index),
|
|
"duplicates another endpoint",
|
|
)
|
|
}
|
|
seen[normalized] = struct{}{}
|
|
values[index] = normalized
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func normalizeEndpoint(value string, listener bool) (string, error) {
|
|
if !utf8.ValidString(value) {
|
|
return "", fmt.Errorf("must be valid UTF-8")
|
|
}
|
|
if value == "" || strings.TrimSpace(value) != value {
|
|
return "", fmt.Errorf("must be a non-empty URI without surrounding whitespace")
|
|
}
|
|
parsed, err := url.ParseRequestURI(value)
|
|
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
|
return "", fmt.Errorf("must be an absolute TCP or UDP URI")
|
|
}
|
|
parsed.Scheme = strings.ToLower(parsed.Scheme)
|
|
if parsed.Scheme != "tcp" && parsed.Scheme != "udp" {
|
|
return "", fmt.Errorf("must use the TCP or UDP scheme")
|
|
}
|
|
if parsed.User != nil ||
|
|
parsed.Opaque != "" ||
|
|
parsed.Path != "" ||
|
|
parsed.RawPath != "" ||
|
|
parsed.RawQuery != "" ||
|
|
parsed.ForceQuery ||
|
|
parsed.Fragment != "" {
|
|
return "", fmt.Errorf("must not contain credentials, a path, query, or fragment")
|
|
}
|
|
host := parsed.Hostname()
|
|
if host == "" {
|
|
return "", fmt.Errorf("must include a host")
|
|
}
|
|
if port := parsed.Port(); port != "" {
|
|
value, parseErr := strconv.ParseUint(port, 10, 16)
|
|
if parseErr != nil || (!listener && value == 0) {
|
|
return "", fmt.Errorf("has an invalid port")
|
|
}
|
|
}
|
|
if listener {
|
|
if _, err := netip.ParseAddr(host); err != nil {
|
|
return "", fmt.Errorf("listener host must be an IP address")
|
|
}
|
|
}
|
|
return parsed.String(), nil
|
|
}
|
|
|
|
func validateSTUNServers(values []string, ipv6 bool, field string) error {
|
|
seen := make(map[string]struct{}, len(values))
|
|
for index, value := range values {
|
|
if err := validateSTUNServer(value, ipv6); err != nil {
|
|
return invalidInstanceConfig(
|
|
fmt.Sprintf("%s[%d]", field, index),
|
|
err.Error(),
|
|
)
|
|
}
|
|
if _, exists := seen[value]; exists {
|
|
return invalidInstanceConfig(
|
|
fmt.Sprintf("%s[%d]", field, index),
|
|
"duplicates another STUN server",
|
|
)
|
|
}
|
|
seen[value] = struct{}{}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateSTUNServer(value string, ipv6 bool) error {
|
|
if !utf8.ValidString(value) {
|
|
return fmt.Errorf("must be valid UTF-8")
|
|
}
|
|
if value == "" || strings.TrimSpace(value) != value {
|
|
return fmt.Errorf("must be non-empty without surrounding whitespace")
|
|
}
|
|
if domain, found := strings.CutPrefix(value, "txt:"); found {
|
|
if !validSTUNHostname(domain) {
|
|
return fmt.Errorf("has an invalid TXT lookup name")
|
|
}
|
|
return nil
|
|
}
|
|
if address, err := netip.ParseAddr(value); err == nil {
|
|
return validateSTUNAddressFamily(address, ipv6)
|
|
}
|
|
if address, err := netip.ParseAddrPort(value); err == nil {
|
|
if address.Port() == 0 {
|
|
return fmt.Errorf("port must not be zero")
|
|
}
|
|
return validateSTUNAddressFamily(address.Addr(), ipv6)
|
|
}
|
|
|
|
parsed, err := url.Parse("stun://" + value)
|
|
if err != nil ||
|
|
parsed.Host == "" ||
|
|
parsed.User != nil ||
|
|
parsed.Path != "" ||
|
|
parsed.RawQuery != "" ||
|
|
parsed.Fragment != "" {
|
|
return fmt.Errorf("must be a host, IP address, socket address, or txt: name")
|
|
}
|
|
host := parsed.Hostname()
|
|
if !validSTUNHostname(host) {
|
|
return fmt.Errorf("has an invalid host")
|
|
}
|
|
if strings.HasSuffix(parsed.Host, ":") {
|
|
return fmt.Errorf("has an invalid port")
|
|
}
|
|
if port := parsed.Port(); port != "" {
|
|
value, parseErr := strconv.ParseUint(port, 10, 16)
|
|
if parseErr != nil || value == 0 {
|
|
return fmt.Errorf("has an invalid port")
|
|
}
|
|
}
|
|
if address, err := netip.ParseAddr(host); err == nil {
|
|
return validateSTUNAddressFamily(address, ipv6)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validSTUNHostname(value string) bool {
|
|
if value == "" || strings.ContainsAny(value, " \t\r\n/?#@[]:") {
|
|
return false
|
|
}
|
|
for _, character := range value {
|
|
if unicode.IsControl(character) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func validateSTUNAddressFamily(address netip.Addr, ipv6 bool) error {
|
|
if ipv6 {
|
|
if address.Is6() && !address.Is4In6() {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("literal address must be IPv6")
|
|
}
|
|
if address.Is4() {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("literal address must be IPv4")
|
|
}
|
|
|
|
func invalidInstanceConfig(field string, message string) error {
|
|
if field == "" {
|
|
return fmt.Errorf("invalid EasyTier instance config: %s", message)
|
|
}
|
|
return fmt.Errorf("invalid EasyTier instance config %s: %s", field, message)
|
|
}
|
|
|
|
func (document instanceConfigDocument) clone() instanceConfigDocument {
|
|
cloned := document
|
|
cloned.peers = append([]string(nil), document.peers...)
|
|
cloned.listeners = append([]string(nil), document.listeners...)
|
|
cloned.portForwards = append([]PortForwardConfig(nil), document.portForwards...)
|
|
cloned.stunServers = append([]string(nil), document.stunServers...)
|
|
cloned.stunServersV6 = append([]string(nil), document.stunServersV6...)
|
|
cloned.securePrivateKey = append([]byte(nil), document.securePrivateKey...)
|
|
if document.hostname != nil {
|
|
value := *document.hostname
|
|
cloned.hostname = &value
|
|
}
|
|
if document.ipv4 != nil {
|
|
value := *document.ipv4
|
|
cloned.ipv4 = &value
|
|
}
|
|
if document.p2p != nil {
|
|
value := *document.p2p
|
|
cloned.p2p = &value
|
|
}
|
|
if document.holePunching != nil {
|
|
value := *document.holePunching
|
|
cloned.holePunching = &value
|
|
}
|
|
if document.encryption != nil {
|
|
value := *document.encryption
|
|
cloned.encryption = &value
|
|
}
|
|
return cloned
|
|
}
|
|
|
|
// Format prevents configuration secrets and private keys from being printed.
|
|
func (config InstanceConfig) Format(state fmt.State, _ rune) {
|
|
_, _ = state.Write([]byte("InstanceConfig{<redacted>}"))
|
|
}
|
|
|
|
// Format prevents configuration secrets and private keys from being printed.
|
|
func (builder InstanceConfigBuilder) Format(state fmt.State, _ rune) {
|
|
_, _ = state.Write([]byte("InstanceConfigBuilder{<redacted>}"))
|
|
}
|