mirror of
https://github.com/p1neappleXpress/OpenFlux.git
synced 2026-10-30 07:43:41 +08:00
* utils: status lines reach the app log; count secrets as Kotlin does Infof now also goes to the log sink, so the Android log screen and the iOS ring buffer show the status lines the CLI prints. Throttled rate-limits warnings that would repeat on every packet. SecretChars counts a secret in UTF-16 units: the core counted bytes, accepting a 10-letter Cyrillic secret that Desktop and Android reject. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * transport: one KDF context rule, and fall back when the peer's differs KDFContexts replaces the copies of the context rule that had drifted apart (CLI, Android bridge, Desktop, iOS). A classic cupsonline client given the rooms as --url now derives the placeholder like the exit that created them; it used the room list, and no packet ever decrypted. The context is a public salt, so a peer may try several: alternates are what other or older builds derive. A packet that fails under the current keys is tried under them (derived lazily); the exit answers under the context the client used, a client that hears nothing cycles through them. A mismatch that dropped everything in silence now heals itself, and a key that really differs is reported in the log. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * transport: classic codec that accepts both framings and falls back batched against legacy dropped every packet in silence. CodecTransport decodes both (they differ in the first byte), sends what the peer sends (batched once it has seen a batch frame, including the iOS fork's empty probe), and the client tries the other framing while the peer is silent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * session: classic compatibility on the same carrier, diagnostics The classic and Session layerings differ in the first byte of a carrier frame, so frameDemux splits one carrier between a Session and a classic pipeline sharing its keys. SetClassic turns it on: a single-carrier client falls back to classic while the exit does not answer the handshake and switches once it does; a classic-configured exit also serves classic clients while no Session client is active. Strict Sessions log classic frames with the fix instead of dropping them silently. Logs: carrier start failures, takeover by another client, mode switches, and a handshake diagnosis (nothing arrived / key differs / exit is classic). The carrier is stopped once instead of twice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * manager: match the peer's carriers when names differ The apps name carriers after their type (mailru, mailru-2) whatever the exit's .conf or panel called them, so a cookie offer or AuthRequired for an unknown name went nowhere. Cookie messages carry the document URL now, and the name is matched by it, then by type when this side has one carrier of it; what cannot be matched is logged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cli: classic setups with a key run as a Session with classic fallback --transport=X with a key now builds a Session: a client speaks classic until the exit answers the handshake, an exit also serves classic clients. A single-carrier --transports/.conf client falls back the same way; only --negotiate is strict. Without a key the classic path stays, with the adaptive codec. The context comes from transport.KDFContexts, alternates included, and a cupsonline exit accepts its room list as one. transportFactory takes the role: a Session client's cupsonline carrier was built as an exit and, with no or dead rooms, created rooms of its own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * mobile: same Session, context and codec fallback as the CLI Classic profiles with a key build a Session with classic fallback, as the CLI does, so a classic profile works against every node; classic direct works too. Single-carrier Session profiles fall back to classic; the node wizard's check stays strict. The context rule is transport.KDFContexts (the link's context first, the derived one as an alternate). SetInitialCookies applies cookies got before the start to Yandex carriers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * share: read links the way every client does; --parse-link Decode accepts what copying and other encoders do to a link: whitespace and line breaks, base64 padding, the standard alphabet, and says what is wrong otherwise. Secrets are counted in characters as Kotlin counts them. --parse-link prints the core's reading of a link as JSON, for apps and debugging; --share also prints the bare link on its own line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * boards: stop sending client pings; docs: reconnect on dead sockets boards sent engine.io pings from the client, which an EIO=4 server treats as an invalid heartbeat and closes the socket: the board dropped every 20 seconds. The server pings and we answer, as before. yandex and mailru left a socket whose keepalive failed open, so a reader on a half-open connection could wait forever; they close it now, and bound each write. Drops and failures to open the document are logged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * transport: tests for mixed builds (codecs, contexts, classic vs Session) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ios: the iOS C library on package mobile (mobile/ios) The iOS app carried its own copy of the core (the saharev1/OpenFlux fork): no Session, its own link importer, its own control channel, so it reached no node the wizard or Desktop set up. The root export_ios*.go here were an even older copy of the same. mobile/ios is the app's C API (every call the app makes, same names and signatures) implemented on package mobile, the Android library: an app that links this core as a submodule and builds it with build_ios.sh gets the Session with classic fallback, the one context rule with its alternates, the codec fallback and the core's link reading. New calls: OpenFluxShareDecode returns a Session profile ready for OpenFluxStartSession / OpenFluxStartSessionPacketTunnel; OpenFluxMode, OpenFluxActiveTransport, OpenFluxSetCodec, OpenFluxSetDebugLevel and the captcha calls the app had no equivalent for. The Network Extension runs the carriers on a phone profile (mobile.SetLowMemory, Volga's SlimVolgaConfig) and keeps the fork's local DNS-over-TLS, ICMP refusal for UDP and GeoSite split tunneling. PROTOCOL_NEGOTIATION.md gave the Session's layering in the classic order (AES inside the batch frame); the Session has always encrypted the batch frame. Fixed, with the classic layering, codec and context fallback and the context rule documented. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * session: say how to fix a classic peer at a Session-only exit Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * crypto: search the context per pipeline; a gone Session client yields in 15s A client's Session hellos and its classic fallback shared one context search, so while a classic exit ignored the hellos the search moved the classic pipeline off the context that exit uses, and the first classic packets went out under the wrong keys. Keys are still derived once per carrier (keyStore); each pipeline searches on its own (keyRing), and a client's pipelines pass on what they learn. An exit configured classic yielded to a classic client only linkTimeout (30 s) after its Session client was last heard; a live one is heard every keepaliveInterval, so it yields after 15 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * build_ios.sh: work from any directory The output directory was made and listed relative to the caller's directory while the library was written under the checkout, so running core/build_ios.sh from an app that links the core as a submodule failed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: p1neappleXpress <a@a.a> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
229 lines
6.1 KiB
Go
229 lines
6.1 KiB
Go
package utils
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
)
|
|
|
|
// Debug levels. Each level prints everything the ones below it do.
|
|
//
|
|
// 0 (default) off: only Infof and log.* status lines
|
|
// 1 -d packet movement: one line per IPv4 packet crossing the
|
|
// tunnel, "-> 52 bytes - UDP 10.10.10.2:53000 -> 8.8.8.8:53 ..."
|
|
// 2 -dd operational logs: sessions, carriers, handshakes, crypto,
|
|
// control messages, errors
|
|
// 3 -ddd hexdumps of packets and frames
|
|
//
|
|
// --sensitive is separate from the level: it adds key material and cookie
|
|
// jars to whatever the level prints.
|
|
const (
|
|
LevelOff = 0
|
|
LevelPackets = 1
|
|
LevelDebug = 2
|
|
LevelHexdump = 3
|
|
)
|
|
|
|
// The logger is created once and never replaced: the mobile bridges change
|
|
// the level and the output on every connect while goroutines of the
|
|
// previous connection may still be logging.
|
|
var (
|
|
level atomic.Int32
|
|
sensitive atomic.Bool
|
|
output = &swapWriter{}
|
|
debugLog = log.New(output, "", log.LstdFlags|log.Lmicroseconds)
|
|
logSinkMu sync.RWMutex
|
|
logSink func(string)
|
|
)
|
|
|
|
func init() {
|
|
output.Set(os.Stderr)
|
|
}
|
|
|
|
// swapWriter lets SetOutput redirect a logger that is already in use.
|
|
type swapWriter struct {
|
|
w atomic.Pointer[io.Writer]
|
|
}
|
|
|
|
func (s *swapWriter) Set(w io.Writer) { s.w.Store(&w) }
|
|
|
|
func (s *swapWriter) Write(p []byte) (int, error) { return (*s.w.Load()).Write(p) }
|
|
|
|
// SetOutput redirects debug and standard log output to w.
|
|
// Used by the iOS bridge to keep logs in its ring buffer.
|
|
func SetOutput(w io.Writer) {
|
|
output.Set(w)
|
|
log.SetOutput(w)
|
|
}
|
|
|
|
// SetLogSink mirrors every debug message (packet lines included) to an
|
|
// embedding application, e.g. the Android log screen. nil stops it.
|
|
func SetLogSink(sink func(string)) {
|
|
logSinkMu.Lock()
|
|
logSink = sink
|
|
logSinkMu.Unlock()
|
|
}
|
|
|
|
// SetLevel sets the debug level, clamped to LevelOff..LevelHexdump.
|
|
func SetLevel(n int) {
|
|
n = min(max(n, LevelOff), LevelHexdump)
|
|
level.Store(int32(n))
|
|
if n > LevelOff {
|
|
log.SetFlags(log.LstdFlags | log.Lmicroseconds | log.Lshortfile)
|
|
}
|
|
}
|
|
|
|
// Level returns the current debug level.
|
|
func Level() int {
|
|
return int(level.Load())
|
|
}
|
|
|
|
// EnableDebug turns on operational logs (LevelDebug), what debug meant
|
|
// before there were levels.
|
|
func EnableDebug() {
|
|
SetLevel(LevelDebug)
|
|
}
|
|
|
|
// SetDebug turns operational logs on (LevelDebug) or all debug output off.
|
|
func SetDebug(on bool) {
|
|
if on {
|
|
SetLevel(LevelDebug)
|
|
return
|
|
}
|
|
SetLevel(LevelOff)
|
|
}
|
|
|
|
// IsVerbose reports whether hexdumps are enabled (LevelHexdump).
|
|
func IsVerbose() bool {
|
|
return Level() >= LevelHexdump
|
|
}
|
|
|
|
// SetSensitive toggles logging of key material and cookie jars.
|
|
func SetSensitive(on bool) {
|
|
sensitive.Store(on)
|
|
}
|
|
|
|
// Sensitive reports whether key material and cookie jars may be logged.
|
|
func Sensitive() bool {
|
|
return sensitive.Load()
|
|
}
|
|
|
|
// Verbosef logs at hexdump level (LevelHexdump / -ddd). Kept for call sites
|
|
// that emit their own detailed dumps instead of network.LogPacket.
|
|
func Verbosef(format string, args ...interface{}) {
|
|
if Level() >= LevelHexdump {
|
|
emit(fmt.Sprintf(format, args...))
|
|
}
|
|
}
|
|
|
|
// Sensitivef logs only when --sensitive is on and operational logs are
|
|
// enabled (LevelDebug and up). For secrets that must never appear otherwise.
|
|
func Sensitivef(format string, args ...interface{}) {
|
|
if Level() >= LevelDebug && sensitive.Load() {
|
|
emit(fmt.Sprintf(format, args...))
|
|
}
|
|
}
|
|
|
|
// Redact renders a secret for logs: "" when debug is off, a short SHA-256 at
|
|
// LevelDebug, and the raw bytes as hex only when --sensitive is on.
|
|
func Redact(label string, b []byte) string {
|
|
switch {
|
|
case Level() < LevelDebug:
|
|
return ""
|
|
case sensitive.Load():
|
|
return fmt.Sprintf("%s[len=%d hex=%s]", label, len(b), hex.EncodeToString(b))
|
|
default:
|
|
return fmt.Sprintf("%s[len=%d sha256=%s]", label, len(b), Sha256Short(b))
|
|
}
|
|
}
|
|
|
|
// Packetf logs a packet-movement line (LevelPackets and up).
|
|
func Packetf(format string, args ...interface{}) {
|
|
if Level() >= LevelPackets {
|
|
emit(fmt.Sprintf(format, args...))
|
|
}
|
|
}
|
|
|
|
// Debugf logs an operational message (LevelDebug and up).
|
|
func Debugf(format string, args ...interface{}) {
|
|
if Level() >= LevelDebug {
|
|
emit(fmt.Sprintf(format, args...))
|
|
}
|
|
}
|
|
|
|
func emit(message string) {
|
|
debugLog.Output(3, message)
|
|
|
|
logSinkMu.RLock()
|
|
sink := logSink
|
|
logSinkMu.RUnlock()
|
|
if sink != nil {
|
|
sink(message)
|
|
}
|
|
}
|
|
|
|
// Infof always logs, regardless of debug level. Used for user-facing status
|
|
// lines (e.g. cups room open/close) that must be visible without --debug.
|
|
//
|
|
// With a log sink set (an embedding app: the Android and iOS bridges) the
|
|
// line goes there, so the app's log screen shows the same status lines as
|
|
// the CLI's output; otherwise to the standard log.
|
|
func Infof(format string, args ...interface{}) {
|
|
message := fmt.Sprintf(format, args...)
|
|
logSinkMu.RLock()
|
|
sink := logSink
|
|
logSinkMu.RUnlock()
|
|
if sink != nil {
|
|
sink(message)
|
|
return
|
|
}
|
|
log.Output(2, message)
|
|
}
|
|
|
|
// throttle remembers when each Throttled key last fired.
|
|
var throttle sync.Map // key -> time.Time
|
|
|
|
// Throttled reports whether a message identified by key may be logged now:
|
|
// true at most once per every. For warnings that would otherwise repeat on
|
|
// every packet (a key mismatch drops each one).
|
|
func Throttled(key string, every time.Duration) bool {
|
|
now := time.Now()
|
|
if last, ok := throttle.Load(key); ok && now.Sub(last.(time.Time)) < every {
|
|
return false
|
|
}
|
|
throttle.Store(key, now)
|
|
return true
|
|
}
|
|
|
|
// SafeGo runs fn in a new goroutine, recovering from any panic so a crash in
|
|
// one worker cannot take down the whole process (critical when this code runs
|
|
// embedded as a library inside a mobile app).
|
|
func SafeGo(name string, fn func()) {
|
|
go func() {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
Debugf("[PANIC] recovered in %s: %v", name, r)
|
|
}
|
|
}()
|
|
fn()
|
|
}()
|
|
}
|
|
|
|
// Sha256Hex returns the full SHA-256 of b as lowercase hex.
|
|
func Sha256Hex(b []byte) string {
|
|
h := sha256.Sum256(b)
|
|
return hex.EncodeToString(h[:])
|
|
}
|
|
|
|
// Sha256Short returns the first 16 hex chars of SHA-256 of b.
|
|
func Sha256Short(b []byte) string {
|
|
h := sha256.Sum256(b)
|
|
return hex.EncodeToString(h[:8])
|
|
}
|