diff --git a/CONTEXT.md b/CONTEXT.md index 88976145..486d78ea 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -91,6 +91,14 @@ model for management readback, while the shared host-aware normalization path omits capabilities that the compact runtime cannot execute. Omitted settings are silent no-ops and must not be advertised as live network capabilities. +## Embedded logging + +Logging belongs to the WASI runtime, which installs its subscriber at module +startup. The independent logger ABI and process-level `LoggerRpc` control +the same module-wide level, including before any Instance exists. Instance +configuration and lifecycle never initialize or reset logging. The Host owns +the output sink through WASI stdout; output errors do not interrupt Core. + ## Web compatibility Host The Web compatibility Host runs the portable EasyTier guest in JavaScript diff --git a/easytier-core/src/wasi/abi.rs b/easytier-core/src/wasi/abi.rs index 16d3e109..b640cd10 100644 --- a/easytier-core/src/wasi/abi.rs +++ b/easytier-core/src/wasi/abi.rs @@ -67,6 +67,13 @@ pub const GUEST_EXPORTS: &[&str] = &[ // WASI command initialization. This only binds the runtime; core lifecycle // still starts through the instance exports below. "_start", + // Module-wide logging; no instance handle is required. Levels 0..=5 are + // off, error, warn, info, debug, trace (matching api.logger.LogLevel). + // set_level(u32) -> i32 returns 0, or -3 without changing the level for + // invalid input. level() -> u32 defaults to info. _start is idempotent + // and does not reset the level. Output uses the WASI stdout sink. + "easytier_logger_set_level", + "easytier_logger_level", // Guest-memory buffers. "easytier_buffer_alloc", "easytier_buffer_free", diff --git a/easytier-core/src/wasi/logger.rs b/easytier-core/src/wasi/logger.rs new file mode 100644 index 00000000..25ea23be --- /dev/null +++ b/easytier-core/src/wasi/logger.rs @@ -0,0 +1,256 @@ +//! Module-wide logging, initialized before any instance or WebClient is created. + +use std::{ + fmt::Write as _, + io::{self, Write}, + sync::{ + Arc, Mutex, + atomic::{AtomicU8, Ordering}, + }, +}; + +#[cfg(target_os = "wasi")] +use std::sync::OnceLock; + +use tracing::{ + Event, Metadata, Subscriber, + field::{Field, Visit}, + level_filters::LevelFilter, + span::{Attributes, Id, Record}, + subscriber::Interest, +}; + +// These values also match api.logger.LogLevel. Keep the basic WASI logger +// independent of the optional management protobuf API. +const LEVELS: [LevelFilter; 6] = [ + LevelFilter::OFF, + LevelFilter::ERROR, + LevelFilter::WARN, + LevelFilter::INFO, + LevelFilter::DEBUG, + LevelFilter::TRACE, +]; + +struct Logger { + level: AtomicU8, + writer: Mutex, +} + +impl Logger { + fn new(writer: W) -> Self { + Self { + level: AtomicU8::new(3), + writer: Mutex::new(writer), + } + } + + fn set_level(&self, level: u32) -> bool { + if level >= LEVELS.len() as u32 { + return false; + } + self.level.store(level as u8, Ordering::Relaxed); + true + } + + fn level(&self) -> u32 { + u32::from(self.level.load(Ordering::Relaxed)) + } +} + +impl Subscriber for Logger { + fn enabled(&self, metadata: &Metadata<'_>) -> bool { + metadata.is_event() && *metadata.level() <= LEVELS[self.level() as usize] + } + + fn register_callsite(&self, metadata: &'static Metadata<'static>) -> Interest { + if metadata.is_event() { + Interest::sometimes() + } else { + Interest::never() + } + } + + fn max_level_hint(&self) -> Option { + // The host can enable trace after a callsite has already been visited. + Some(LevelFilter::TRACE) + } + + fn new_span(&self, _: &Attributes<'_>) -> Id { + Id::from_u64(1) + } + + fn record(&self, _: &Id, _: &Record<'_>) {} + + fn record_follows_from(&self, _: &Id, _: &Id) {} + + fn enter(&self, _: &Id) {} + + fn exit(&self, _: &Id) {} + + fn event(&self, event: &Event<'_>) { + struct Fields(String); + + impl Visit for Fields { + fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) { + let _ = write!(self.0, " {}={value:?}", field.name()); + } + } + + let mut fields = Fields(String::new()); + event.record(&mut fields); + // A host logging sink may fail; that must not trap the guest runtime. + let _ = writeln!( + self.writer.lock().unwrap(), + "[{}] {}{}", + event.metadata().level(), + event.metadata().target(), + fields.0, + ); + } +} + +#[cfg(target_os = "wasi")] +fn logger() -> &'static Arc> { + static LOGGER: OnceLock>> = OnceLock::new(); + LOGGER.get_or_init(|| { + let logger = Arc::new(Logger::new(io::stdout())); + tracing::subscriber::set_global_default(logger.clone()) + .expect("WASI runtime owns the tracing subscriber"); + logger + }) +} + +#[cfg(target_os = "wasi")] +pub(super) fn init() { + logger(); +} + +/// Sets the module-wide level without requiring an instance handle. +/// Values 0..=5 mean off, error, warn, info, debug, and trace. +/// Invalid values return -3 and leave the current level unchanged. +#[cfg(target_os = "wasi")] +#[unsafe(no_mangle)] +pub extern "C" fn easytier_logger_set_level(level: u32) -> i32 { + if logger().set_level(level) { 0 } else { -3 } +} + +/// Returns the module-wide level using the same values as the setter. +#[cfg(target_os = "wasi")] +#[unsafe(no_mangle)] +pub extern "C" fn easytier_logger_level() -> u32 { + logger().level() +} + +#[cfg(feature = "management")] +impl crate::management::LoggerControl for Logger { + fn set_level(&self, name: &str) -> anyhow::Result<()> { + let filter = name.parse::()?; + let level = LEVELS + .iter() + .position(|level| *level == filter) + .expect("every tracing level has an ABI value"); + self.set_level(level as u32); + Ok(()) + } + + fn level(&self) -> String { + LEVELS[self.level() as usize] + .to_string() + .to_ascii_lowercase() + } +} + +#[cfg(all(target_os = "wasi", feature = "management"))] +pub(super) fn control() -> Arc { + logger().clone() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Clone, Default)] + struct Output(Arc>>); + + impl Write for Output { + fn write(&mut self, bytes: &[u8]) -> io::Result { + self.0.lock().unwrap().extend_from_slice(bytes); + Ok(bytes.len()) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } + } + + fn emit() { + tracing::error!(target: "logger_test", "error"); + tracing::info!(target: "logger_test", "info"); + tracing::trace!(target: "logger_test", "trace"); + } + + #[test] + fn changing_level_rechecks_existing_callsites() { + let output = Output::default(); + let logger = Arc::new(Logger::new(output.clone())); + tracing::subscriber::with_default(logger.clone(), || { + for (level, expected) in [ + (3, vec!["error", "info"]), + (5, vec!["error", "info", "trace"]), + (0, vec![]), + (3, vec!["error", "info"]), + ] { + assert!(logger.set_level(level)); + output.0.lock().unwrap().clear(); + emit(); + let bytes = output.0.lock().unwrap(); + let text = std::str::from_utf8(&bytes).unwrap(); + let messages = text + .lines() + .map(|line| line.split_once("message=").unwrap().1) + .collect::>(); + assert_eq!(messages, expected); + } + }); + } + + #[test] + fn invalid_level_preserves_current_filter() { + let logger = Logger::new(io::sink()); + assert_eq!(logger.level(), 3); + for level in [6, 256, u32::MAX] { + assert!(!logger.set_level(level)); + assert_eq!(logger.level(), 3); + } + } + + #[test] + fn output_errors_do_not_panic() { + struct BrokenWriter; + + impl Write for BrokenWriter { + fn write(&mut self, _: &[u8]) -> io::Result { + Err(io::Error::from(io::ErrorKind::BrokenPipe)) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } + } + + tracing::subscriber::with_default(Logger::new(BrokenWriter), emit); + } + + #[cfg(feature = "management")] + #[test] + fn management_control_uses_the_same_filter() { + let logger = Logger::new(io::sink()); + let control: &dyn crate::management::LoggerControl = &logger; + control.set_level("trace").unwrap(); + assert_eq!(logger.level(), 5); + assert!(logger.set_level(0)); + assert_eq!(control.level(), "off"); + assert!(control.set_level("invalid").is_err()); + assert_eq!(logger.level(), 0); + } +} diff --git a/easytier-core/src/wasi/mod.rs b/easytier-core/src/wasi/mod.rs index 0b80dce4..d93bc5e9 100644 --- a/easytier-core/src/wasi/mod.rs +++ b/easytier-core/src/wasi/mod.rs @@ -12,6 +12,8 @@ pub mod abi; pub mod adapter; #[cfg(target_os = "wasi")] pub(crate) mod imports; +#[cfg(any(test, target_os = "wasi"))] +mod logger; #[cfg(target_os = "wasi")] pub(crate) mod runtime; #[cfg(any(test, target_os = "wasi"))] diff --git a/easytier-core/src/wasi/runtime.rs b/easytier-core/src/wasi/runtime.rs index 0750f84c..a3b616e4 100644 --- a/easytier-core/src/wasi/runtime.rs +++ b/easytier-core/src/wasi/runtime.rs @@ -681,7 +681,9 @@ mod abi { #[unsafe(no_mangle)] /// WASI command entrypoint used only to initialize the host runtime. - pub extern "C" fn _start() {} + pub extern "C" fn _start() { + crate::wasi::logger::init(); + } #[unsafe(no_mangle)] /// Allocates a guest-owned ABI buffer and returns its linear-memory offset. diff --git a/easytier-core/src/wasi/web_client.rs b/easytier-core/src/wasi/web_client.rs index de03be6f..6919872c 100644 --- a/easytier-core/src/wasi/web_client.rs +++ b/easytier-core/src/wasi/web_client.rs @@ -260,6 +260,12 @@ impl WebClientBackend for WasiWebClientBackend { fn register(&self, registry: &ServiceRegistry) { registry.register(self.handler.clone(), ""); register_forwarded_instance_management_rpc(self.handler.clone(), registry); + registry.register( + crate::proto::api::logger::LoggerRpcServer::new( + crate::management::LoggerManagementRpc::new(crate::wasi::logger::control()), + ), + "", + ); } async fn instance_ids(&self) -> anyhow::Result> { diff --git a/easytier-go/README.md b/easytier-go/README.md index 295b9caa..6bf49319 100644 --- a/easytier-go/README.md +++ b/easytier-go/README.md @@ -66,6 +66,33 @@ instance, err := host.CreateInstanceTOML(ctx, "office", "", configTOML) `Instance.ShowNodeInfo` returns this instance's virtual IPv4 address and advertised hostname. +### Logging + +Logging belongs to the Host and is independent of instance TOML and lifecycle. +Pass an `io.Writer` when creating the Host to receive guest logs; nil discards +them. The default level is `LogLevelInfo`. Change it before creating instances +or at runtime through the Host: + +```go +host, err := corehost.New(ctx, corehost.Options{LogWriter: os.Stdout}) +if err != nil { + return err +} +defer host.Close(ctx) +if err := host.SetLogLevel(ctx, corehost.LogLevelDebug); err != nil { + return err +} +``` + +`host.LogLevel(ctx)` reads the current level. `LogLevelOff` disables all logs. +The level applies to every instance and the WebClient in this Host; other +Hosts are independent. The WebClient's existing `LoggerRpc` controls the same +level. Creating or closing an instance never resets it. + +Writes are synchronous, so the writer must not call back into this Host. +Writer errors do not interrupt guest execution, and the Host does not close +the writer. + ### Web Client management A host can also connect to an EasyTier Web configuration server. The embedded diff --git a/easytier-go/internal/artifact/easytier_core.wasm b/easytier-go/internal/artifact/easytier_core.wasm index 681b7c7e..db02e556 100644 Binary files a/easytier-go/internal/artifact/easytier_core.wasm and b/easytier-go/internal/artifact/easytier_core.wasm differ diff --git a/easytier-go/internal/artifact/provenance.go b/easytier-go/internal/artifact/provenance.go index 16a7c5a2..afed2653 100644 --- a/easytier-go/internal/artifact/provenance.go +++ b/easytier-go/internal/artifact/provenance.go @@ -3,6 +3,6 @@ package artifact const ( - Commit = "9a2290f312dbdec6f40e8b13d8a6de5f66e4c570" - SHA256 = "044f9ed79079f449741744d9dd0559d6844e1fff2f1a1994710811904f119986" + Commit = "c51437c673a71f69b4b028c8d566a7b23b33e65c" + SHA256 = "17d6dd4561c1f1e818e7d70ed031c3cf8ea5dcfc1fd0461a6753f1f23f5fa6ab" ) diff --git a/easytier-go/internal/coreabi/logger.go b/easytier-go/internal/coreabi/logger.go new file mode 100644 index 00000000..73a96312 --- /dev/null +++ b/easytier-go/internal/coreabi/logger.go @@ -0,0 +1,57 @@ +package coreabi + +import ( + "context" + "fmt" + + "github.com/metacubex/wazero/api" +) + +// LogLevel uses the module-wide logger ABI values, matching api.logger.LogLevel. +type LogLevel uint32 + +const ( + LogLevelOff LogLevel = iota + LogLevelError + LogLevelWarn + LogLevelInfo + LogLevelDebug + LogLevelTrace +) + +func SetLogLevel(ctx context.Context, module api.Module, level LogLevel) error { + status, err := callLogger(ctx, module, "easytier_logger_set_level", uint64(level)) + if err != nil { + return err + } + if int32(status) != 0 { + return fmt.Errorf("set EasyTier log level %d: status=%d", level, int32(status)) + } + return nil +} + +func GetLogLevel(ctx context.Context, module api.Module) (LogLevel, error) { + value, err := callLogger(ctx, module, "easytier_logger_level") + if err != nil { + return 0, err + } + if value > uint64(LogLevelTrace) { + return 0, fmt.Errorf("EasyTier guest returned invalid log level %d", value) + } + return LogLevel(value), nil +} + +func callLogger(ctx context.Context, module api.Module, name string, args ...uint64) (uint64, error) { + function := module.ExportedFunction(name) + if function == nil { + return 0, fmt.Errorf("EasyTier guest does not export %s", name) + } + results, err := function.Call(ctx, args...) + if err != nil { + return 0, fmt.Errorf("%s: %w", name, err) + } + if len(results) != 1 { + return 0, fmt.Errorf("%s returned %d values, want 1", name, len(results)) + } + return results[0], nil +} diff --git a/easytier-go/internal/engine/host.go b/easytier-go/internal/engine/host.go index 267162f3..db2c769d 100644 --- a/easytier-go/internal/engine/host.go +++ b/easytier-go/internal/engine/host.go @@ -5,6 +5,7 @@ import ( "crypto/rand" "errors" "fmt" + "io" "runtime/debug" "sync" "time" @@ -27,6 +28,7 @@ type Options struct { Services platform.Services PacketQueueCapacity int Management reactor.ManagementHandler + LogWriter io.Writer } type Host struct { @@ -102,7 +104,11 @@ func NewHost(ctx context.Context, options Options) (_ *Host, err error) { return nil, fmt.Errorf("compile embedded EasyTier core: %w", err) } defer compiled.Close(contextutil.WithoutCancel(ctx)) - module, err = runtime.InstantiateModule(ctx, compiled, newModuleConfig()) + moduleConfig := newModuleConfig() + if options.LogWriter != nil { + moduleConfig = moduleConfig.WithStdout(options.LogWriter) + } + module, err = runtime.InstantiateModule(ctx, compiled, moduleConfig) if err != nil { return nil, fmt.Errorf("instantiate embedded EasyTier core: %w", err) } diff --git a/easytier-go/internal/engine/logger.go b/easytier-go/internal/engine/logger.go new file mode 100644 index 00000000..4276e7b0 --- /dev/null +++ b/easytier-go/internal/engine/logger.go @@ -0,0 +1,48 @@ +package engine + +import ( + "context" + "fmt" + + "github.com/easytier/easytier/easytier-go/internal/coreabi" +) + +func (host *Host) SetLogLevel(ctx context.Context, level coreabi.LogLevel) error { + if host == nil { + return fmt.Errorf("set log level with nil EasyTier engine host") + } + if ctx == nil { + return fmt.Errorf("set EasyTier log level with nil context") + } + host.mu.Lock() + defer host.mu.Unlock() + if host.closed { + return fmt.Errorf("set log level with closed EasyTier engine host") + } + host.guestMu.Lock() + defer host.guestMu.Unlock() + if err := ctx.Err(); err != nil { + return err + } + return coreabi.SetLogLevel(ctx, host.module, level) +} + +func (host *Host) LogLevel(ctx context.Context) (coreabi.LogLevel, error) { + if host == nil { + return 0, fmt.Errorf("get log level with nil EasyTier engine host") + } + if ctx == nil { + return 0, fmt.Errorf("get EasyTier log level with nil context") + } + host.mu.Lock() + defer host.mu.Unlock() + if host.closed { + return 0, fmt.Errorf("get log level with closed EasyTier engine host") + } + host.guestMu.Lock() + defer host.guestMu.Unlock() + if err := ctx.Err(); err != nil { + return 0, err + } + return coreabi.GetLogLevel(ctx, host.module) +} diff --git a/easytier-go/internal/engine/logger_test.go b/easytier-go/internal/engine/logger_test.go new file mode 100644 index 00000000..ec085c63 --- /dev/null +++ b/easytier-go/internal/engine/logger_test.go @@ -0,0 +1,32 @@ +package engine + +import ( + "context" + "testing" + "time" + + "github.com/easytier/easytier/easytier-go/internal/coreabi" + "github.com/easytier/easytier/easytier-go/platform/netstd" +) + +func TestLoggerInitializationDoesNotResetLevel(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + host, err := NewHost(ctx, Options{Services: netstd.Services()}) + if err != nil { + t.Fatal(err) + } + defer host.Close(ctx) + if err := host.SetLogLevel(ctx, coreabi.LogLevelTrace); err != nil { + t.Fatal(err) + } + host.guestMu.Lock() + _, err = host.module.ExportedFunction("_start").Call(ctx) + host.guestMu.Unlock() + if err != nil { + t.Fatal(err) + } + if level, err := host.LogLevel(ctx); err != nil || level != coreabi.LogLevelTrace { + t.Fatalf("repeated initialization changed level: %d, %v", level, err) + } +} diff --git a/easytier-go/internal/host/host.go b/easytier-go/internal/host/host.go index c4224b80..e51ca3b5 100644 --- a/easytier-go/internal/host/host.go +++ b/easytier-go/internal/host/host.go @@ -3,6 +3,7 @@ package host import ( "context" "fmt" + "io" "net" "net/netip" "strconv" @@ -33,6 +34,10 @@ const ( type Options struct { Platform platform.Services PacketQueueCapacity int + // LogWriter receives module-wide guest logs. Nil discards output. + // Writes run synchronously while the guest is executing; the writer must + // not call back into this Host. The Host does not close the writer. + LogWriter io.Writer } type EmbeddedCoreInfo struct { @@ -77,6 +82,7 @@ func New(ctx context.Context, options Options) (*Host, error) { Services: mergeServices(options.Platform), PacketQueueCapacity: options.PacketQueueCapacity, Management: manager.handle, + LogWriter: options.LogWriter, }, ) if err != nil { diff --git a/easytier-go/internal/host/logger.go b/easytier-go/internal/host/logger.go new file mode 100644 index 00000000..d9d9f5a3 --- /dev/null +++ b/easytier-go/internal/host/logger.go @@ -0,0 +1,36 @@ +package host + +import ( + "context" + "fmt" + + "github.com/easytier/easytier/easytier-go/internal/coreabi" +) + +type LogLevel = coreabi.LogLevel + +const ( + LogLevelOff = coreabi.LogLevelOff + LogLevelError = coreabi.LogLevelError + LogLevelWarn = coreabi.LogLevelWarn + LogLevelInfo = coreabi.LogLevelInfo + LogLevelDebug = coreabi.LogLevelDebug + LogLevelTrace = coreabi.LogLevelTrace +) + +// SetLogLevel changes logging for this Host, including all its instances and +// its WebClient. It can be called before any instance is created. +func (host *Host) SetLogLevel(ctx context.Context, level LogLevel) error { + if host == nil { + return fmt.Errorf("set log level with nil EasyTier host") + } + return host.engine.SetLogLevel(ctx, level) +} + +// LogLevel returns this Host's current log level, which defaults to Info. +func (host *Host) LogLevel(ctx context.Context) (LogLevel, error) { + if host == nil { + return 0, fmt.Errorf("get log level with nil EasyTier host") + } + return host.engine.LogLevel(ctx) +} diff --git a/easytier-go/logger.go b/easytier-go/logger.go new file mode 100644 index 00000000..2e92a279 --- /dev/null +++ b/easytier-go/logger.go @@ -0,0 +1,15 @@ +package host + +import internalhost "github.com/easytier/easytier/easytier-go/internal/host" + +// LogLevel controls logging for an entire Host, independently of its instances. +type LogLevel = internalhost.LogLevel + +const ( + LogLevelOff = internalhost.LogLevelOff + LogLevelError = internalhost.LogLevelError + LogLevelWarn = internalhost.LogLevelWarn + LogLevelInfo = internalhost.LogLevelInfo + LogLevelDebug = internalhost.LogLevelDebug + LogLevelTrace = internalhost.LogLevelTrace +) diff --git a/easytier-go/proto/acl/acl.pb.go b/easytier-go/proto/acl/acl.pb.go index d3951429..af9837b4 100644 --- a/easytier-go/proto/acl/acl.pb.go +++ b/easytier-go/proto/acl/acl.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 -// protoc v7.36.1 +// protoc v7.35.1 // source: acl.proto package acl diff --git a/easytier-go/proto/api/config/api_config.pb.go b/easytier-go/proto/api/config/api_config.pb.go index a3510dab..909bfa07 100644 --- a/easytier-go/proto/api/config/api_config.pb.go +++ b/easytier-go/proto/api/config/api_config.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 -// protoc v7.36.1 +// protoc v7.35.1 // source: api_config.proto package config diff --git a/easytier-go/proto/api/instance/api_instance.pb.go b/easytier-go/proto/api/instance/api_instance.pb.go index fef899ca..e22584ba 100644 --- a/easytier-go/proto/api/instance/api_instance.pb.go +++ b/easytier-go/proto/api/instance/api_instance.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 -// protoc v7.36.1 +// protoc v7.35.1 // source: api_instance.proto package instance @@ -4399,17 +4399,17 @@ var file_api_instance_proto_goTypes = []any{ nil, // 66: api.instance.ListForeignNetworkResponse.ForeignNetworksEntry (*ListGlobalForeignNetworkResponse_OneForeignNetwork)(nil), // 67: api.instance.ListGlobalForeignNetworkResponse.OneForeignNetwork (*ListGlobalForeignNetworkResponse_ForeignNetworks)(nil), // 68: api.instance.ListGlobalForeignNetworkResponse.ForeignNetworks - nil, // 69: api.instance.ListGlobalForeignNetworkResponse.ForeignNetworksEntry - nil, // 70: api.instance.MetricSnapshot.LabelsEntry - (*common.UUID)(nil), // 71: common.UUID - (*common.TunnelInfo)(nil), // 72: common.TunnelInfo - (peer_rpc.SecureAuthLevel)(0), // 73: peer_rpc.SecureAuthLevel - (peer_rpc.PeerIdentityType)(0), // 74: peer_rpc.PeerIdentityType - (*common.Ipv4Inet)(nil), // 75: common.Ipv4Inet - (*common.StunInfo)(nil), // 76: common.StunInfo - (*common.PeerFeatureFlag)(nil), // 77: common.PeerFeatureFlag - (*common.Ipv6Inet)(nil), // 78: common.Ipv6Inet - (*peer_rpc.GetIpListResponse)(nil), // 79: peer_rpc.GetIpListResponse + nil, // 69: api.instance.ListGlobalForeignNetworkResponse.ForeignNetworksEntry + nil, // 70: api.instance.MetricSnapshot.LabelsEntry + (*common.UUID)(nil), // 71: common.UUID + (*common.TunnelInfo)(nil), // 72: common.TunnelInfo + (peer_rpc.SecureAuthLevel)(0), // 73: peer_rpc.SecureAuthLevel + (peer_rpc.PeerIdentityType)(0), // 74: peer_rpc.PeerIdentityType + (*common.Ipv4Inet)(nil), // 75: common.Ipv4Inet + (*common.StunInfo)(nil), // 76: common.StunInfo + (*common.PeerFeatureFlag)(nil), // 77: common.PeerFeatureFlag + (*common.Ipv6Inet)(nil), // 78: common.Ipv6Inet + (*peer_rpc.GetIpListResponse)(nil), // 79: peer_rpc.GetIpListResponse (*peer_rpc.RouteForeignNetworkSummary)(nil), // 80: peer_rpc.RouteForeignNetworkSummary (*common.Url)(nil), // 81: common.Url (*common.SocketAddr)(nil), // 82: common.SocketAddr diff --git a/easytier-go/proto/api/manage/api_manage.pb.go b/easytier-go/proto/api/manage/api_manage.pb.go index 4d144664..d3d8492d 100644 --- a/easytier-go/proto/api/manage/api_manage.pb.go +++ b/easytier-go/proto/api/manage/api_manage.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 -// protoc v7.36.1 +// protoc v7.35.1 // source: api_manage.proto package manage @@ -200,8 +200,12 @@ type NetworkConfig struct { VpnPortalConfig *VpnPortalConfig `protobuf:"bytes,69,opt,name=vpn_portal_config,json=vpnPortalConfig,proto3,oneof" json:"vpn_portal_config,omitempty"` ManagedCredentials []*ManagedCredentialConfig `protobuf:"bytes,71,rep,name=managed_credentials,json=managedCredentials,proto3" json:"managed_credentials,omitempty"` PreferPeerRelay *bool `protobuf:"varint,72,opt,name=prefer_peer_relay,json=preferPeerRelay,proto3,oneof" json:"prefer_peer_relay,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Bootstrap subnet used when DHCP has not learned a subnet from peers. + DhcpIpv4Cidr *string `protobuf:"bytes,74,opt,name=dhcp_ipv4_cidr,json=dhcpIpv4Cidr,proto3,oneof" json:"dhcp_ipv4_cidr,omitempty"` + Dhcpv6 *bool `protobuf:"varint,75,opt,name=dhcpv6,proto3,oneof" json:"dhcpv6,omitempty"` + DhcpIpv6Cidr *string `protobuf:"bytes,76,opt,name=dhcp_ipv6_cidr,json=dhcpIpv6Cidr,proto3,oneof" json:"dhcp_ipv6_cidr,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *NetworkConfig) Reset() { @@ -722,6 +726,27 @@ func (x *NetworkConfig) GetPreferPeerRelay() bool { return false } +func (x *NetworkConfig) GetDhcpIpv4Cidr() string { + if x != nil && x.DhcpIpv4Cidr != nil { + return *x.DhcpIpv4Cidr + } + return "" +} + +func (x *NetworkConfig) GetDhcpv6() bool { + if x != nil && x.Dhcpv6 != nil { + return *x.Dhcpv6 + } + return false +} + +func (x *NetworkConfig) GetDhcpIpv6Cidr() string { + if x != nil && x.DhcpIpv6Cidr != nil { + return *x.DhcpIpv6Cidr + } + return "" +} + type ManagedCredentialConfig struct { state protoimpl.MessageState `protogen:"open.v1"` CredentialId string `protobuf:"bytes,1,opt,name=credential_id,json=credentialId,proto3" json:"credential_id,omitempty"` @@ -923,8 +948,10 @@ type VpnPortalConfig struct { WireguardListen string `protobuf:"bytes,1,opt,name=wireguard_listen,json=wireguardListen,proto3" json:"wireguard_listen,omitempty"` WireguardPrivateKey *string `protobuf:"bytes,2,opt,name=wireguard_private_key,json=wireguardPrivateKey,proto3,oneof" json:"wireguard_private_key,omitempty"` Clients []*VpnPortalClientConfig `protobuf:"bytes,3,rep,name=clients,proto3" json:"clients,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Omitted in existing configurations, where the portal is enabled. + Enabled *bool `protobuf:"varint,4,opt,name=enabled,proto3,oneof" json:"enabled,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *VpnPortalConfig) Reset() { @@ -978,6 +1005,13 @@ func (x *VpnPortalConfig) GetClients() []*VpnPortalClientConfig { return nil } +func (x *VpnPortalConfig) GetEnabled() bool { + if x != nil && x.Enabled != nil { + return *x.Enabled + } + return false +} + type NetworkPeerConfig struct { state protoimpl.MessageState `protogen:"open.v1"` Uri string `protobuf:"bytes,1,opt,name=uri,proto3" json:"uri,omitempty"` @@ -1115,8 +1149,9 @@ type MyNodeInfo struct { StunInfo *common.StunInfo `protobuf:"bytes,5,opt,name=stun_info,json=stunInfo,proto3" json:"stun_info,omitempty"` Listeners []*common.Url `protobuf:"bytes,6,rep,name=listeners,proto3" json:"listeners,omitempty"` // Deprecated: Marked as deprecated in api_manage.proto. - VpnPortalCfg *string `protobuf:"bytes,7,opt,name=vpn_portal_cfg,json=vpnPortalCfg,proto3,oneof" json:"vpn_portal_cfg,omitempty"` - PeerId uint32 `protobuf:"varint,8,opt,name=peer_id,json=peerId,proto3" json:"peer_id,omitempty"` + VpnPortalCfg *string `protobuf:"bytes,7,opt,name=vpn_portal_cfg,json=vpnPortalCfg,proto3,oneof" json:"vpn_portal_cfg,omitempty"` + PeerId uint32 `protobuf:"varint,8,opt,name=peer_id,json=peerId,proto3" json:"peer_id,omitempty"` + VirtualIpv6 *common.Ipv6Inet `protobuf:"bytes,9,opt,name=virtual_ipv6,json=virtualIpv6,proto3" json:"virtual_ipv6,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1208,6 +1243,13 @@ func (x *MyNodeInfo) GetPeerId() uint32 { return 0 } +func (x *MyNodeInfo) GetVirtualIpv6() *common.Ipv6Inet { + if x != nil { + return x.VirtualIpv6 + } + return nil +} + type NetworkInstanceRunningInfo struct { state protoimpl.MessageState `protogen:"open.v1"` DevName string `protobuf:"bytes,1,opt,name=dev_name,json=devName,proto3" json:"dev_name,omitempty"` @@ -2169,7 +2211,7 @@ var File_api_manage_proto protoreflect.FileDescriptor const file_api_manage_proto_rawDesc = "" + "\n" + "\x10api_manage.proto\x12\n" + - "api.manage\x1a\fcommon.proto\x1a\x0epeer_rpc.proto\x1a\x12api_instance.proto\x1a\tacl.proto\"\x80$\n" + + "api.manage\x1a\fcommon.proto\x1a\x0epeer_rpc.proto\x1a\x12api_instance.proto\x1a\tacl.proto\"\xa4%\n" + "\rNetworkConfig\x12$\n" + "\vinstance_id\x18\x01 \x01(\tH\x00R\n" + "instanceId\x88\x01\x01\x12\x17\n" + @@ -2250,7 +2292,10 @@ const file_api_manage_proto_rawDesc = "" + "\x05peers\x18D \x03(\v2\x1d.api.manage.NetworkPeerConfigR\x05peers\x12L\n" + "\x11vpn_portal_config\x18E \x01(\v2\x1b.api.manage.VpnPortalConfigH8R\x0fvpnPortalConfig\x88\x01\x01\x12T\n" + "\x13managed_credentials\x18G \x03(\v2#.api.manage.ManagedCredentialConfigR\x12managedCredentials\x12/\n" + - "\x11prefer_peer_relay\x18H \x01(\bH9R\x0fpreferPeerRelay\x88\x01\x01B\x0e\n" + + "\x11prefer_peer_relay\x18H \x01(\bH9R\x0fpreferPeerRelay\x88\x01\x01\x12)\n" + + "\x0edhcp_ipv4_cidr\x18J \x01(\tH:R\fdhcpIpv4Cidr\x88\x01\x01\x12\x1b\n" + + "\x06dhcpv6\x18K \x01(\bH;R\x06dhcpv6\x88\x01\x01\x12)\n" + + "\x0edhcp_ipv6_cidr\x18L \x01(\tH api.manage.NetworkingMethod @@ -2506,53 +2559,54 @@ var file_api_manage_proto_depIdxs = []int32{ 34, // 11: api.manage.MyNodeInfo.ips:type_name -> peer_rpc.GetIpListResponse 35, // 12: api.manage.MyNodeInfo.stun_info:type_name -> common.StunInfo 36, // 13: api.manage.MyNodeInfo.listeners:type_name -> common.Url - 9, // 14: api.manage.NetworkInstanceRunningInfo.my_node_info:type_name -> api.manage.MyNodeInfo - 37, // 15: api.manage.NetworkInstanceRunningInfo.routes:type_name -> api.instance.Route - 38, // 16: api.manage.NetworkInstanceRunningInfo.peers:type_name -> api.instance.PeerInfo - 39, // 17: api.manage.NetworkInstanceRunningInfo.peer_route_pairs:type_name -> api.instance.PeerRoutePair - 40, // 18: api.manage.NetworkInstanceRunningInfo.foreign_network_summary:type_name -> peer_rpc.RouteForeignNetworkSummary - 29, // 19: api.manage.NetworkInstanceRunningInfoMap.map:type_name -> api.manage.NetworkInstanceRunningInfoMap.MapEntry - 41, // 20: api.manage.NetworkMeta.inst_id:type_name -> common.UUID - 1, // 21: api.manage.NetworkMeta.source:type_name -> api.manage.ConfigSource - 2, // 22: api.manage.ValidateConfigRequest.config:type_name -> api.manage.NetworkConfig - 41, // 23: api.manage.RunNetworkInstanceRequest.inst_id:type_name -> common.UUID - 2, // 24: api.manage.RunNetworkInstanceRequest.config:type_name -> api.manage.NetworkConfig - 1, // 25: api.manage.RunNetworkInstanceRequest.source:type_name -> api.manage.ConfigSource - 41, // 26: api.manage.RunNetworkInstanceResponse.inst_id:type_name -> common.UUID - 41, // 27: api.manage.RetainNetworkInstanceRequest.inst_ids:type_name -> common.UUID - 41, // 28: api.manage.RetainNetworkInstanceResponse.remain_inst_ids:type_name -> common.UUID - 41, // 29: api.manage.CollectNetworkInfoRequest.inst_ids:type_name -> common.UUID - 11, // 30: api.manage.CollectNetworkInfoResponse.info:type_name -> api.manage.NetworkInstanceRunningInfoMap - 41, // 31: api.manage.ListNetworkInstanceResponse.inst_ids:type_name -> common.UUID - 41, // 32: api.manage.DeleteNetworkInstanceRequest.inst_ids:type_name -> common.UUID - 41, // 33: api.manage.DeleteNetworkInstanceResponse.remain_inst_ids:type_name -> common.UUID - 41, // 34: api.manage.GetNetworkInstanceConfigRequest.inst_id:type_name -> common.UUID - 2, // 35: api.manage.GetNetworkInstanceConfigResponse.config:type_name -> api.manage.NetworkConfig - 1, // 36: api.manage.GetNetworkInstanceConfigResponse.source:type_name -> api.manage.ConfigSource - 41, // 37: api.manage.ListNetworkInstanceMetaRequest.inst_ids:type_name -> common.UUID - 12, // 38: api.manage.ListNetworkInstanceMetaResponse.metas:type_name -> api.manage.NetworkMeta - 10, // 39: api.manage.NetworkInstanceRunningInfoMap.MapEntry.value:type_name -> api.manage.NetworkInstanceRunningInfo - 13, // 40: api.manage.WebClientService.ValidateConfig:input_type -> api.manage.ValidateConfigRequest - 15, // 41: api.manage.WebClientService.RunNetworkInstance:input_type -> api.manage.RunNetworkInstanceRequest - 17, // 42: api.manage.WebClientService.RetainNetworkInstance:input_type -> api.manage.RetainNetworkInstanceRequest - 19, // 43: api.manage.WebClientService.CollectNetworkInfo:input_type -> api.manage.CollectNetworkInfoRequest - 21, // 44: api.manage.WebClientService.ListNetworkInstance:input_type -> api.manage.ListNetworkInstanceRequest - 23, // 45: api.manage.WebClientService.DeleteNetworkInstance:input_type -> api.manage.DeleteNetworkInstanceRequest - 25, // 46: api.manage.WebClientService.GetNetworkInstanceConfig:input_type -> api.manage.GetNetworkInstanceConfigRequest - 27, // 47: api.manage.WebClientService.ListNetworkInstanceMeta:input_type -> api.manage.ListNetworkInstanceMetaRequest - 14, // 48: api.manage.WebClientService.ValidateConfig:output_type -> api.manage.ValidateConfigResponse - 16, // 49: api.manage.WebClientService.RunNetworkInstance:output_type -> api.manage.RunNetworkInstanceResponse - 18, // 50: api.manage.WebClientService.RetainNetworkInstance:output_type -> api.manage.RetainNetworkInstanceResponse - 20, // 51: api.manage.WebClientService.CollectNetworkInfo:output_type -> api.manage.CollectNetworkInfoResponse - 22, // 52: api.manage.WebClientService.ListNetworkInstance:output_type -> api.manage.ListNetworkInstanceResponse - 24, // 53: api.manage.WebClientService.DeleteNetworkInstance:output_type -> api.manage.DeleteNetworkInstanceResponse - 26, // 54: api.manage.WebClientService.GetNetworkInstanceConfig:output_type -> api.manage.GetNetworkInstanceConfigResponse - 28, // 55: api.manage.WebClientService.ListNetworkInstanceMeta:output_type -> api.manage.ListNetworkInstanceMetaResponse - 48, // [48:56] is the sub-list for method output_type - 40, // [40:48] is the sub-list for method input_type - 40, // [40:40] is the sub-list for extension type_name - 40, // [40:40] is the sub-list for extension extendee - 0, // [0:40] is the sub-list for field type_name + 37, // 14: api.manage.MyNodeInfo.virtual_ipv6:type_name -> common.Ipv6Inet + 9, // 15: api.manage.NetworkInstanceRunningInfo.my_node_info:type_name -> api.manage.MyNodeInfo + 38, // 16: api.manage.NetworkInstanceRunningInfo.routes:type_name -> api.instance.Route + 39, // 17: api.manage.NetworkInstanceRunningInfo.peers:type_name -> api.instance.PeerInfo + 40, // 18: api.manage.NetworkInstanceRunningInfo.peer_route_pairs:type_name -> api.instance.PeerRoutePair + 41, // 19: api.manage.NetworkInstanceRunningInfo.foreign_network_summary:type_name -> peer_rpc.RouteForeignNetworkSummary + 29, // 20: api.manage.NetworkInstanceRunningInfoMap.map:type_name -> api.manage.NetworkInstanceRunningInfoMap.MapEntry + 42, // 21: api.manage.NetworkMeta.inst_id:type_name -> common.UUID + 1, // 22: api.manage.NetworkMeta.source:type_name -> api.manage.ConfigSource + 2, // 23: api.manage.ValidateConfigRequest.config:type_name -> api.manage.NetworkConfig + 42, // 24: api.manage.RunNetworkInstanceRequest.inst_id:type_name -> common.UUID + 2, // 25: api.manage.RunNetworkInstanceRequest.config:type_name -> api.manage.NetworkConfig + 1, // 26: api.manage.RunNetworkInstanceRequest.source:type_name -> api.manage.ConfigSource + 42, // 27: api.manage.RunNetworkInstanceResponse.inst_id:type_name -> common.UUID + 42, // 28: api.manage.RetainNetworkInstanceRequest.inst_ids:type_name -> common.UUID + 42, // 29: api.manage.RetainNetworkInstanceResponse.remain_inst_ids:type_name -> common.UUID + 42, // 30: api.manage.CollectNetworkInfoRequest.inst_ids:type_name -> common.UUID + 11, // 31: api.manage.CollectNetworkInfoResponse.info:type_name -> api.manage.NetworkInstanceRunningInfoMap + 42, // 32: api.manage.ListNetworkInstanceResponse.inst_ids:type_name -> common.UUID + 42, // 33: api.manage.DeleteNetworkInstanceRequest.inst_ids:type_name -> common.UUID + 42, // 34: api.manage.DeleteNetworkInstanceResponse.remain_inst_ids:type_name -> common.UUID + 42, // 35: api.manage.GetNetworkInstanceConfigRequest.inst_id:type_name -> common.UUID + 2, // 36: api.manage.GetNetworkInstanceConfigResponse.config:type_name -> api.manage.NetworkConfig + 1, // 37: api.manage.GetNetworkInstanceConfigResponse.source:type_name -> api.manage.ConfigSource + 42, // 38: api.manage.ListNetworkInstanceMetaRequest.inst_ids:type_name -> common.UUID + 12, // 39: api.manage.ListNetworkInstanceMetaResponse.metas:type_name -> api.manage.NetworkMeta + 10, // 40: api.manage.NetworkInstanceRunningInfoMap.MapEntry.value:type_name -> api.manage.NetworkInstanceRunningInfo + 13, // 41: api.manage.WebClientService.ValidateConfig:input_type -> api.manage.ValidateConfigRequest + 15, // 42: api.manage.WebClientService.RunNetworkInstance:input_type -> api.manage.RunNetworkInstanceRequest + 17, // 43: api.manage.WebClientService.RetainNetworkInstance:input_type -> api.manage.RetainNetworkInstanceRequest + 19, // 44: api.manage.WebClientService.CollectNetworkInfo:input_type -> api.manage.CollectNetworkInfoRequest + 21, // 45: api.manage.WebClientService.ListNetworkInstance:input_type -> api.manage.ListNetworkInstanceRequest + 23, // 46: api.manage.WebClientService.DeleteNetworkInstance:input_type -> api.manage.DeleteNetworkInstanceRequest + 25, // 47: api.manage.WebClientService.GetNetworkInstanceConfig:input_type -> api.manage.GetNetworkInstanceConfigRequest + 27, // 48: api.manage.WebClientService.ListNetworkInstanceMeta:input_type -> api.manage.ListNetworkInstanceMetaRequest + 14, // 49: api.manage.WebClientService.ValidateConfig:output_type -> api.manage.ValidateConfigResponse + 16, // 50: api.manage.WebClientService.RunNetworkInstance:output_type -> api.manage.RunNetworkInstanceResponse + 18, // 51: api.manage.WebClientService.RetainNetworkInstance:output_type -> api.manage.RetainNetworkInstanceResponse + 20, // 52: api.manage.WebClientService.CollectNetworkInfo:output_type -> api.manage.CollectNetworkInfoResponse + 22, // 53: api.manage.WebClientService.ListNetworkInstance:output_type -> api.manage.ListNetworkInstanceResponse + 24, // 54: api.manage.WebClientService.DeleteNetworkInstance:output_type -> api.manage.DeleteNetworkInstanceResponse + 26, // 55: api.manage.WebClientService.GetNetworkInstanceConfig:output_type -> api.manage.GetNetworkInstanceConfigResponse + 28, // 56: api.manage.WebClientService.ListNetworkInstanceMeta:output_type -> api.manage.ListNetworkInstanceMetaResponse + 49, // [49:57] is the sub-list for method output_type + 41, // [41:49] is the sub-list for method input_type + 41, // [41:41] is the sub-list for extension type_name + 41, // [41:41] is the sub-list for extension extendee + 0, // [0:41] is the sub-list for field type_name } func init() { file_api_manage_proto_init() } diff --git a/easytier-go/proto/common/common.pb.go b/easytier-go/proto/common/common.pb.go index 81806daa..30120e5a 100644 --- a/easytier-go/proto/common/common.pb.go +++ b/easytier-go/proto/common/common.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 -// protoc v7.36.1 +// protoc v7.35.1 // source: common.proto package common diff --git a/easytier-go/proto/easytier/annotations.pb.go b/easytier-go/proto/easytier/annotations.pb.go index 4eb36121..fc0ff363 100644 --- a/easytier-go/proto/easytier/annotations.pb.go +++ b/easytier-go/proto/easytier/annotations.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 -// protoc v7.36.1 +// protoc v7.35.1 // source: annotations.proto package easytier diff --git a/easytier-go/proto/error/error.pb.go b/easytier-go/proto/error/error.pb.go index 373a60c6..fbaff4b7 100644 --- a/easytier-go/proto/error/error.pb.go +++ b/easytier-go/proto/error/error.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 -// protoc v7.36.1 +// protoc v7.35.1 // source: error.proto package error diff --git a/easytier-go/proto/peer_rpc/peer_rpc.pb.go b/easytier-go/proto/peer_rpc/peer_rpc.pb.go index 4084e562..d2f5456c 100644 --- a/easytier-go/proto/peer_rpc/peer_rpc.pb.go +++ b/easytier-go/proto/peer_rpc/peer_rpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 -// protoc v7.36.1 +// protoc v7.35.1 // source: peer_rpc.proto package peer_rpc @@ -1146,12 +1146,15 @@ func (*SyncRouteInfoRequest_ConnBitmap) isSyncRouteInfoRequest_ConnInfo() {} func (*SyncRouteInfoRequest_ConnPeerList) isSyncRouteInfoRequest_ConnInfo() {} type SyncRouteInfoResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - IsInitiator bool `protobuf:"varint,1,opt,name=is_initiator,json=isInitiator,proto3" json:"is_initiator,omitempty"` - SessionId uint64 `protobuf:"varint,2,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` - Error *SyncRouteInfoError `protobuf:"varint,3,opt,name=error,proto3,enum=peer_rpc.SyncRouteInfoError,oneof" json:"error,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + IsInitiator bool `protobuf:"varint,1,opt,name=is_initiator,json=isInitiator,proto3" json:"is_initiator,omitempty"` + SessionId uint64 `protobuf:"varint,2,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` + Error *SyncRouteInfoError `protobuf:"varint,3,opt,name=error,proto3,enum=peer_rpc.SyncRouteInfoError,oneof" json:"error,omitempty"` + // Missing data for peers referenced by accepted topology. Empty in steady state. + MissingPeerIds []uint32 `protobuf:"varint,4,rep,packed,name=missing_peer_ids,json=missingPeerIds,proto3" json:"missing_peer_ids,omitempty"` + MissingConnPeerIds []uint32 `protobuf:"varint,5,rep,packed,name=missing_conn_peer_ids,json=missingConnPeerIds,proto3" json:"missing_conn_peer_ids,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *SyncRouteInfoResponse) Reset() { @@ -1205,6 +1208,20 @@ func (x *SyncRouteInfoResponse) GetError() SyncRouteInfoError { return SyncRouteInfoError_DuplicatePeerId } +func (x *SyncRouteInfoResponse) GetMissingPeerIds() []uint32 { + if x != nil { + return x.MissingPeerIds + } + return nil +} + +func (x *SyncRouteInfoResponse) GetMissingConnPeerIds() []uint32 { + if x != nil { + return x.MissingConnPeerIds + } + return nil +} + type AcquireIpv6PublicAddrLeaseRequest struct { state protoimpl.MessageState `protogen:"open.v1"` PeerId uint32 `protobuf:"varint,1,opt,name=peer_id,json=peerId,proto3" json:"peer_id,omitempty"` @@ -1697,8 +1714,10 @@ type SelectPunchListenerRequest struct { state protoimpl.MessageState `protogen:"open.v1"` ForceNew bool `protobuf:"varint,1,opt,name=force_new,json=forceNew,proto3" json:"force_new,omitempty"` PreferPortMapping bool `protobuf:"varint,2,opt,name=prefer_port_mapping,json=preferPortMapping,proto3" json:"prefer_port_mapping,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Register source observation before returning the listener address. + TransactionId *uint32 `protobuf:"varint,3,opt,name=transaction_id,json=transactionId,proto3,oneof" json:"transaction_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *SelectPunchListenerRequest) Reset() { @@ -1745,11 +1764,20 @@ func (x *SelectPunchListenerRequest) GetPreferPortMapping() bool { return false } +func (x *SelectPunchListenerRequest) GetTransactionId() uint32 { + if x != nil && x.TransactionId != nil { + return *x.TransactionId + } + return 0 +} + type SelectPunchListenerResponse struct { state protoimpl.MessageState `protogen:"open.v1"` ListenerMappedAddr *common.SocketAddr `protobuf:"bytes,1,opt,name=listener_mapped_addr,json=listenerMappedAddr,proto3" json:"listener_mapped_addr,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Echoed only after source observation is registered; absent on older peers. + TransactionId *uint32 `protobuf:"varint,2,opt,name=transaction_id,json=transactionId,proto3,oneof" json:"transaction_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *SelectPunchListenerResponse) Reset() { @@ -1789,6 +1817,13 @@ func (x *SelectPunchListenerResponse) GetListenerMappedAddr() *common.SocketAddr return nil } +func (x *SelectPunchListenerResponse) GetTransactionId() uint32 { + if x != nil && x.TransactionId != nil { + return *x.TransactionId + } + return 0 +} + type SendPunchPacketConeRequest struct { state protoimpl.MessageState `protogen:"open.v1"` ListenerMappedAddr *common.SocketAddr `protobuf:"bytes,1,opt,name=listener_mapped_addr,json=listenerMappedAddr,proto3" json:"listener_mapped_addr,omitempty"` @@ -3459,12 +3494,14 @@ const file_peer_rpc_proto_rawDesc = "" + "connBitmap\x12C\n" + "\x0econn_peer_list\x18\a \x01(\v2\x1b.peer_rpc.RouteConnPeerListH\x00R\fconnPeerList\x12V\n" + "\x15foreign_network_infos\x18\x06 \x01(\v2\".peer_rpc.RouteForeignNetworkInfosR\x13foreignNetworkInfosB\v\n" + - "\tconn_info\"\x9c\x01\n" + + "\tconn_info\"\xf9\x01\n" + "\x15SyncRouteInfoResponse\x12!\n" + "\fis_initiator\x18\x01 \x01(\bR\visInitiator\x12\x1d\n" + "\n" + "session_id\x18\x02 \x01(\x04R\tsessionId\x127\n" + - "\x05error\x18\x03 \x01(\x0e2\x1c.peer_rpc.SyncRouteInfoErrorH\x00R\x05error\x88\x01\x01B\b\n" + + "\x05error\x18\x03 \x01(\x0e2\x1c.peer_rpc.SyncRouteInfoErrorH\x00R\x05error\x88\x01\x01\x12(\n" + + "\x10missing_peer_ids\x18\x04 \x03(\rR\x0emissingPeerIds\x121\n" + + "\x15missing_conn_peer_ids\x18\x05 \x03(\rR\x12missingConnPeerIdsB\b\n" + "\x06_error\"c\n" + "!AcquireIpv6PublicAddrLeaseRequest\x12\x17\n" + "\apeer_id\x18\x01 \x01(\rR\x06peerId\x12%\n" + @@ -3505,12 +3542,16 @@ const file_peer_rpc_proto_rawDesc = "" + "\x0econnector_addr\x18\x01 \x01(\v2\x12.common.SocketAddrR\rconnectorAddr\x12#\n" + "\rlistener_port\x18\x02 \x01(\rR\flistenerPort\x12>\n" + "\x12preferred_src_ipv6\x18\x03 \x01(\v2\x10.common.Ipv6AddrR\x10preferredSrcIpv6\x12;\n" + - "\x0fconnector_addrs\x18\x04 \x03(\v2\x12.common.SocketAddrR\x0econnectorAddrs\"i\n" + + "\x0fconnector_addrs\x18\x04 \x03(\v2\x12.common.SocketAddrR\x0econnectorAddrs\"\xa8\x01\n" + "\x1aSelectPunchListenerRequest\x12\x1b\n" + "\tforce_new\x18\x01 \x01(\bR\bforceNew\x12.\n" + - "\x13prefer_port_mapping\x18\x02 \x01(\bR\x11preferPortMapping\"c\n" + + "\x13prefer_port_mapping\x18\x02 \x01(\bR\x11preferPortMapping\x12*\n" + + "\x0etransaction_id\x18\x03 \x01(\rH\x00R\rtransactionId\x88\x01\x01B\x11\n" + + "\x0f_transaction_id\"\xa2\x01\n" + "\x1bSelectPunchListenerResponse\x12D\n" + - "\x14listener_mapped_addr\x18\x01 \x01(\v2\x12.common.SocketAddrR\x12listenerMappedAddr\"\xcb\x02\n" + + "\x14listener_mapped_addr\x18\x01 \x01(\v2\x12.common.SocketAddrR\x12listenerMappedAddr\x12*\n" + + "\x0etransaction_id\x18\x02 \x01(\rH\x00R\rtransactionId\x88\x01\x01B\x11\n" + + "\x0f_transaction_id\"\xcb\x02\n" + "\x1aSendPunchPacketConeRequest\x12D\n" + "\x14listener_mapped_addr\x18\x01 \x01(\v2\x12.common.SocketAddrR\x12listenerMappedAddr\x12/\n" + "\tdest_addr\x18\x02 \x01(\v2\x12.common.SocketAddrR\bdestAddr\x12%\n" + @@ -3886,6 +3927,8 @@ func file_peer_rpc_proto_init() { } file_peer_rpc_proto_msgTypes[13].OneofWrappers = []any{} file_peer_rpc_proto_msgTypes[18].OneofWrappers = []any{} + file_peer_rpc_proto_msgTypes[22].OneofWrappers = []any{} + file_peer_rpc_proto_msgTypes[23].OneofWrappers = []any{} file_peer_rpc_proto_msgTypes[38].OneofWrappers = []any{} file_peer_rpc_proto_msgTypes[41].OneofWrappers = []any{} file_peer_rpc_proto_msgTypes[42].OneofWrappers = []any{} diff --git a/easytier-go/proto/provenance.go b/easytier-go/proto/provenance.go index c26dbcda..4c6318e4 100644 --- a/easytier-go/proto/provenance.go +++ b/easytier-go/proto/provenance.go @@ -3,6 +3,6 @@ package proto const ( - EasyTierCommit = "9a2290f312dbdec6f40e8b13d8a6de5f66e4c570" - SchemaSHA256 = "c145983fee99e08f6207c425cea0952c1aaf50f9bfea12c834d1ec985e0b39df" + EasyTierCommit = "c51437c673a71f69b4b028c8d566a7b23b33e65c" + SchemaSHA256 = "e49a4af09e5b66089eebb73fda78ffca7e8468a5a938cf7a593d689881686f0a" ) diff --git a/easytier-go/proto/web/web.pb.go b/easytier-go/proto/web/web.pb.go index 4b12bbfa..ece7eb55 100644 --- a/easytier-go/proto/web/web.pb.go +++ b/easytier-go/proto/web/web.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 -// protoc v7.36.1 +// protoc v7.35.1 // source: web.proto package web diff --git a/easytier-go/tests/host_logger_test.go b/easytier-go/tests/host_logger_test.go new file mode 100644 index 00000000..0265dc31 --- /dev/null +++ b/easytier-go/tests/host_logger_test.go @@ -0,0 +1,157 @@ +package host_test + +import ( + "bytes" + "context" + "io" + "strings" + "sync" + "testing" + "time" + + corehost "github.com/easytier/easytier/easytier-go" +) + +type logOutput struct { + mu sync.Mutex + buffer bytes.Buffer + fail bool + writes int +} + +func (output *logOutput) Write(data []byte) (int, error) { + output.mu.Lock() + defer output.mu.Unlock() + output.writes++ + if output.fail { + return 0, io.ErrClosedPipe + } + return output.buffer.Write(data) +} + +func (output *logOutput) take() string { + output.mu.Lock() + defer output.mu.Unlock() + text := output.buffer.String() + output.buffer.Reset() + return text +} + +func TestHostLoggingIsIndependentOfInstances(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + output := &logOutput{} + host, err := corehost.New(ctx, corehost.Options{LogWriter: output}) + if err != nil { + t.Fatal(err) + } + defer host.Close(ctx) + assertLevel := func(want corehost.LogLevel) { + t.Helper() + got, err := host.LogLevel(ctx) + if err != nil || got != want { + t.Fatalf("log level = %d, %v; want %d", got, err, want) + } + } + assertLevel(corehost.LogLevelInfo) + if err := host.SetLogLevel(ctx, corehost.LogLevelOff); err != nil { + t.Fatal(err) + } + create := func(id int) *corehost.Instance { + t.Helper() + instance, err := host.CreateInstance(ctx, instanceConfig(t, id, "10.144.0.1", 0, false, false)) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = instance.Close(context.Background()) }) + if err := instance.Start(ctx); err != nil { + t.Fatal(err) + } + return instance + } + first := create(1) + assertLevel(corehost.LogLevelOff) + if text := output.take(); text != "" { + t.Fatalf("logging is off but received %q", text) + } + if err := host.SetLogLevel(ctx, corehost.LogLevelTrace); err != nil { + t.Fatal(err) + } + second := create(2) + assertLevel(corehost.LogLevelTrace) + if text := output.take(); !strings.Contains(text, "[INFO]") { + t.Fatalf("guest did not write info logs: %q", text) + } + if err := first.Close(ctx); err != nil { + t.Fatal(err) + } + assertLevel(corehost.LogLevelTrace) + if err := host.SetLogLevel(ctx, corehost.LogLevelOff); err != nil { + t.Fatal(err) + } + output.take() + if err := second.Close(ctx); err != nil { + t.Fatal(err) + } + third := create(3) + if err := third.Close(ctx); err != nil { + t.Fatal(err) + } + assertLevel(corehost.LogLevelOff) + if text := output.take(); text != "" { + t.Fatalf("logging resumed during instance lifecycle: %q", text) + } + if err := host.SetLogLevel(ctx, corehost.LogLevel(6)); err == nil { + t.Fatal("invalid log level was accepted") + } + assertLevel(corehost.LogLevelOff) + + other, err := corehost.New(ctx, corehost.Options{}) + if err != nil { + t.Fatal(err) + } + defer other.Close(ctx) + if level, err := other.LogLevel(ctx); err != nil || level != corehost.LogLevelInfo { + t.Fatalf("another Host inherited log level: %d, %v", level, err) + } + if err := other.SetLogLevel(ctx, corehost.LogLevelDebug); err != nil { + t.Fatal(err) + } + assertLevel(corehost.LogLevelOff) + if err := host.Close(ctx); err != nil { + t.Fatal(err) + } + if err := host.SetLogLevel(ctx, corehost.LogLevelInfo); err == nil { + t.Fatal("changed log level after Host shutdown") + } +} + +func TestHostLoggingWriterFailureDoesNotStopInstance(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + output := &logOutput{fail: true} + host, err := corehost.New(ctx, corehost.Options{LogWriter: output}) + if err != nil { + t.Fatal(err) + } + defer host.Close(ctx) + instance, err := host.CreateInstance(ctx, instanceConfig(t, 1, "10.144.0.1", 0, false, false)) + if err != nil { + t.Fatal(err) + } + defer instance.Close(ctx) + if err := instance.Start(ctx); err != nil { + t.Fatal(err) + } + if state := instance.State(); state != corehost.StateRunning { + t.Fatalf("instance state = %d, want running", state) + } + if err := instance.Close(ctx); err != nil { + t.Fatal(err) + } + output.mu.Lock() + defer output.mu.Unlock() + if output.writes == 0 { + t.Fatal("test did not exercise the failing writer") + } +}