feat(vpn): VPN on-demand Tailscale + agent studioE5 standalone
- Agent studioE5 standalone en Go (console + systray) - VPN on-demand via tailscaled + tailscale up (authkey Headscale) - Resolver/serveur dans le tailnet studioe5 - Caddy on-demand TLS pour les instances - Nouveaux endpoints serveur /api/internal/send-to-node - Suppression des anciens binaires edubox-agent - Suivi dans SUIVI_VPN_ONDEMAND.md
This commit is contained in:
+29
-16
@@ -2,33 +2,46 @@
|
||||
set -e
|
||||
|
||||
VERSION="0.3.0"
|
||||
APP_NAME="studioE5"
|
||||
BIN_NAME="studioE5-agent"
|
||||
LDFLAGS="-X main.version=${VERSION}"
|
||||
|
||||
echo "Building EduBox Agent v${VERSION}..."
|
||||
# On Windows, build a GUI binary so no console window opens on double-click.
|
||||
WIN_LDFLAGS="${LDFLAGS} -H windowsgui"
|
||||
|
||||
echo "Building ${APP_NAME} Agent v${VERSION}..."
|
||||
|
||||
export PATH=$PATH:/usr/local/go/bin
|
||||
|
||||
GOOS=windows GOARCH=amd64 go build -ldflags "${LDFLAGS}" -o edubox-agent.exe .
|
||||
echo " edubox-agent.exe (Windows amd64)"
|
||||
cp edubox-agent.exe "edubox-agent-v${VERSION}.exe"
|
||||
echo " edubox-agent-v${VERSION}.exe (Windows amd64)"
|
||||
GOOS=windows GOARCH=amd64 go build -ldflags "${WIN_LDFLAGS}" -o ${BIN_NAME}.exe .
|
||||
echo " ${BIN_NAME}.exe (Windows amd64)"
|
||||
cp ${BIN_NAME}.exe "${BIN_NAME}-v${VERSION}.exe"
|
||||
echo " ${BIN_NAME}-v${VERSION}.exe (Windows amd64)"
|
||||
|
||||
GOOS=linux GOARCH=amd64 go build -ldflags "${LDFLAGS}" -o edubox-agent .
|
||||
echo " edubox-agent (Linux amd64)"
|
||||
cp edubox-agent "edubox-agent-v${VERSION}"
|
||||
echo " edubox-agent-v${VERSION} (Linux amd64)"
|
||||
GOOS=linux GOARCH=amd64 go build -ldflags "${LDFLAGS}" -o ${BIN_NAME} .
|
||||
echo " ${BIN_NAME} (Linux amd64)"
|
||||
cp ${BIN_NAME} "${BIN_NAME}-v${VERSION}"
|
||||
echo " ${BIN_NAME}-v${VERSION} (Linux amd64)"
|
||||
|
||||
GOOS=darwin GOARCH=amd64 go build -ldflags "${LDFLAGS}" -o edubox-agent-mac .
|
||||
echo " edubox-agent-mac (macOS amd64)"
|
||||
cp edubox-agent-mac "edubox-agent-v${VERSION}-mac"
|
||||
echo " edubox-agent-v${VERSION}-mac (macOS amd64)"
|
||||
# macOS build requires CGO for the systray menu; skip gracefully if unavailable.
|
||||
if GOOS=darwin GOARCH=amd64 CGO_ENABLED=1 go build -ldflags "${LDFLAGS}" -o ${BIN_NAME}-mac . 2>/dev/null; then
|
||||
echo " ${BIN_NAME}-mac (macOS amd64)"
|
||||
cp ${BIN_NAME}-mac "${BIN_NAME}-v${VERSION}-mac"
|
||||
echo " ${BIN_NAME}-v${VERSION}-mac (macOS amd64)"
|
||||
MAC_BUILT=1
|
||||
else
|
||||
echo " ${BIN_NAME}-mac (macOS amd64) - skipped, CGO required for systray"
|
||||
MAC_BUILT=0
|
||||
fi
|
||||
|
||||
# Copy versioned binaries to server/public so the dashboard can serve them
|
||||
SERVER_PUBLIC="../server/public"
|
||||
if [ -d "${SERVER_PUBLIC}" ]; then
|
||||
cp "edubox-agent-v${VERSION}" "${SERVER_PUBLIC}/edubox-agent-v${VERSION}"
|
||||
cp "edubox-agent-v${VERSION}-mac" "${SERVER_PUBLIC}/edubox-agent-v${VERSION}-mac"
|
||||
cp "edubox-agent-v${VERSION}.exe" "${SERVER_PUBLIC}/edubox-agent-v${VERSION}.exe"
|
||||
cp "${BIN_NAME}-v${VERSION}" "${SERVER_PUBLIC}/${BIN_NAME}-v${VERSION}"
|
||||
cp "${BIN_NAME}-v${VERSION}.exe" "${SERVER_PUBLIC}/${BIN_NAME}-v${VERSION}.exe"
|
||||
if [ "$MAC_BUILT" = "1" ]; then
|
||||
cp "${BIN_NAME}-v${VERSION}-mac" "${SERVER_PUBLIC}/${BIN_NAME}-v${VERSION}-mac"
|
||||
fi
|
||||
echo " Copied versioned binaries to ${SERVER_PUBLIC}"
|
||||
fi
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// AgentConfig holds user-editable settings for the agent.
|
||||
type AgentConfig struct {
|
||||
Server string `json:"server"`
|
||||
HeadscaleURL string `json:"headscale_url"`
|
||||
HeadscaleAuthKey string `json:"headscale_auth_key"`
|
||||
NodeID string `json:"node_id"`
|
||||
DataDir string `json:"data_dir"`
|
||||
}
|
||||
|
||||
const configFileName = "studioE5-config.json"
|
||||
|
||||
// defaultConfig returns sensible defaults for a first run.
|
||||
func defaultConfig(dataDir string) *AgentConfig {
|
||||
return &AgentConfig{
|
||||
Server: "ws://localhost:3001",
|
||||
HeadscaleURL: "",
|
||||
HeadscaleAuthKey: "",
|
||||
NodeID: defaultNodeID(),
|
||||
DataDir: dataDir,
|
||||
}
|
||||
}
|
||||
|
||||
// configPath returns the absolute path to the config file.
|
||||
func configPath(dataDir string) string {
|
||||
return filepath.Join(dataDir, configFileName)
|
||||
}
|
||||
|
||||
// loadOrCreateConfig loads the config file. If it does not exist, it creates
|
||||
// one with default values and returns it (the caller can then open the settings UI).
|
||||
func loadOrCreateConfig(dataDir string) (*AgentConfig, bool, error) {
|
||||
cp := configPath(dataDir)
|
||||
|
||||
if _, err := os.Stat(cp); err == nil {
|
||||
data, err := os.ReadFile(cp)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
var cfg AgentConfig
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return &cfg, false, nil
|
||||
}
|
||||
|
||||
cfg := defaultConfig(dataDir)
|
||||
if err := saveConfig(dataDir, cfg); err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
return cfg, true, nil
|
||||
}
|
||||
|
||||
// saveConfig writes the config file to disk.
|
||||
func saveConfig(dataDir string, cfg *AgentConfig) error {
|
||||
cp := configPath(dataDir)
|
||||
data, err := json.MarshalIndent(cfg, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(cp, data, 0644)
|
||||
}
|
||||
+5
-3
@@ -20,18 +20,20 @@ func getContainerEngine() string {
|
||||
return "docker"
|
||||
}
|
||||
|
||||
func writeCompose(dataDir, instanceID, compose string) error {
|
||||
func writeCompose(dataDir, instanceID, compose string, port int) error {
|
||||
dir := instanceDir(dataDir, instanceID)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Ensure the EduBox mu-plugin is available and substitute its path
|
||||
// Ensure the studioE5 mu-plugin is available and substitute its path
|
||||
muDir, err := writeMUPlugin(dataDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
compose = strings.ReplaceAll(compose, "{MU_PLUGINS_DIR}", filepath.Dir(muDir))
|
||||
compose = strings.ReplaceAll(compose, "{INSTANCE_ID}", instanceID)
|
||||
compose = strings.ReplaceAll(compose, "{PORT}", fmt.Sprintf("%d", port))
|
||||
|
||||
f := filepath.Join(dir, "docker-compose.yml")
|
||||
return os.WriteFile(f, []byte(compose), 0644)
|
||||
@@ -115,7 +117,7 @@ fi
|
||||
}
|
||||
|
||||
// stripWordPressHardcodedURLs removes hardcoded WP_HOME/WP_SITEURL defines
|
||||
// from wp-config.php so the EduBox mu-plugin can compute them from the Host
|
||||
// from wp-config.php so the studioE5 mu-plugin can compute them from the Host
|
||||
// header. This is useful when repairing older instances created before the
|
||||
// mu-plugin existed.
|
||||
func stripWordPressHardcodedURLs(dataDir, instanceID string) error {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,51 @@
|
||||
import zlib
|
||||
import struct
|
||||
|
||||
|
||||
def png_chunk(chunk_type: bytes, data: bytes) -> bytes:
|
||||
return (
|
||||
struct.pack(">I", len(data))
|
||||
+ chunk_type
|
||||
+ data
|
||||
+ struct.pack(">I", zlib.crc32(chunk_type + data) & 0xFFFFFFFF)
|
||||
)
|
||||
|
||||
|
||||
def create_png(width: int, height: int, pixels: list) -> bytes:
|
||||
"""Create a PNG from a 2D list of (R, G, B) tuples."""
|
||||
raw = bytearray()
|
||||
for row in pixels:
|
||||
raw.append(0) # no filter
|
||||
for r, g, b in row:
|
||||
raw.extend((r, g, b))
|
||||
compressed = zlib.compress(bytes(raw), 9)
|
||||
|
||||
ihdr_data = struct.pack(">IIBBBBB", width, height, 8, 2, 0, 0, 0)
|
||||
ihdr = png_chunk(b"IHDR", ihdr_data)
|
||||
idat = png_chunk(b"IDAT", compressed)
|
||||
iend = png_chunk(b"IEND", b"")
|
||||
|
||||
return b"\x89PNG\r\n\x1a\n" + ihdr + idat + iend
|
||||
|
||||
|
||||
BLUE = (37, 99, 235)
|
||||
WHITE = (255, 255, 255)
|
||||
SIZE = 64
|
||||
|
||||
pixels = []
|
||||
for y in range(SIZE):
|
||||
row = []
|
||||
for x in range(SIZE):
|
||||
dx = x - SIZE // 2
|
||||
dy = y - SIZE // 2
|
||||
dist = (dx * dx + dy * dy) ** 0.5
|
||||
if dist < SIZE * 0.25:
|
||||
row.append(WHITE)
|
||||
else:
|
||||
row.append(BLUE)
|
||||
pixels.append(row)
|
||||
|
||||
with open("icon.png", "wb") as f:
|
||||
f.write(create_png(SIZE, SIZE, pixels))
|
||||
|
||||
print("Generated icon.png")
|
||||
+1
-41
@@ -3,52 +3,12 @@ module edubox-agent
|
||||
go 1.26.4
|
||||
|
||||
require (
|
||||
fyne.io/systray v1.12.2
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
|
||||
tailscale.com v1.100.0
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.2.0 // indirect
|
||||
github.com/akutz/memconn v0.1.0 // indirect
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect
|
||||
github.com/coder/websocket v1.8.12 // indirect
|
||||
github.com/creachadair/msync v0.7.1 // indirect
|
||||
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/gaissmai/bart v0.26.1 // indirect
|
||||
github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/hdevalence/ed25519consensus v0.2.0 // indirect
|
||||
github.com/huin/goupnp v1.3.0 // indirect
|
||||
github.com/jsimonetti/rtnetlink v1.4.0 // indirect
|
||||
github.com/klauspost/compress v1.18.5 // indirect
|
||||
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect
|
||||
github.com/mdlayher/socket v0.5.0 // indirect
|
||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||
github.com/pires/go-proxyproto v0.8.1 // indirect
|
||||
github.com/safchain/ethtool v0.3.0 // indirect
|
||||
github.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d // indirect
|
||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
|
||||
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd // indirect
|
||||
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect
|
||||
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 // indirect
|
||||
github.com/tailscale/wireguard-go v0.0.0-20260527010701-b48af7099cad // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||
golang.org/x/crypto v0.52.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
|
||||
golang.org/x/net v0.55.0 // indirect
|
||||
golang.org/x/oauth2 v0.36.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.45.0 // indirect
|
||||
golang.org/x/term v0.43.0 // indirect
|
||||
golang.org/x/text v0.37.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
|
||||
gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8 // indirect
|
||||
)
|
||||
|
||||
+2
-224
@@ -1,233 +1,11 @@
|
||||
9fans.net/go v0.0.8-0.20250307142834-96bdba94b63f h1:1C7nZuxUMNz7eiQALRfiqNOm04+m3edWlRff/BYHf0Q=
|
||||
9fans.net/go v0.0.8-0.20250307142834-96bdba94b63f/go.mod h1:hHyrZRryGqVdqrknjq5OWDLGCTJ2NeEvtrpR96mjraM=
|
||||
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||
filippo.io/mkcert v1.4.4 h1:8eVbbwfVlaqUM7OwuftKc2nuYOoTDQWqsoXmzoXZdbc=
|
||||
filippo.io/mkcert v1.4.4/go.mod h1:VyvOchVuAye3BoUsPUOOofKygVwLV2KQMVFJNRq+1dA=
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A=
|
||||
github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw=
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.5 h1:4lS2IB+wwkj5J43Tq/AwvnscBerBJtQQ6YS7puzCI1k=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.5/go.mod h1:SNzldMlDVbN6nWxM7XsUiNXPSa1LWlqiXtvh/1PrJGg=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.58 h1:/d7FUpAPU8Lf2KUdjniQvfNdlMID0Sd9pS23FJ3SS9Y=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.58/go.mod h1:aVYW33Ow10CyMQGFgC0ptMRIqJWvJ4nxZb0sUiuQT/A=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27 h1:7lOW8NUwE9UZekS1DYoiPdVAqZ6A+LheHWb+mHbNOq8=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27/go.mod h1:w1BASFIPOPUae7AgaH4SbjNbfdkxuggLyGfNFTn8ITY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 h1:Pg9URiobXy85kgFev3og2CuOZ8JZUBENF+dcgWBaYNk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7 h1:a8HvP/+ew3tKwSXqL3BCSjiuicr+XTU2eFYeogV9GJE=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7/go.mod h1:Q7XIWsMo0JcMpI/6TGD6XXcXcV1DbTj6e9BKNntIMIM=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.24.14 h1:c5WJ3iHz7rLIgArznb3JCSQT3uUMiz9DLZhIX+1G8ok=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.24.14/go.mod h1:+JJQTxB6N4niArC14YNtxcQtwEqzS3o9Z32n7q33Rfs=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13 h1:f1L/JtUkVODD+k1+IiSJUUv8A++2qVr+Xvb3xWXETMU=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13/go.mod h1:tvqlFoja8/s0o+UruA1Nrezo/df0PzdunMDDurUfg6U=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk=
|
||||
github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
|
||||
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||
github.com/axiomhq/hyperloglog v0.0.0-20240319100328-84253e514e02 h1:bXAPYSbdYbS5VTy92NIUbeDI1qyggi+JYh5op9IFlcQ=
|
||||
github.com/axiomhq/hyperloglog v0.0.0-20240319100328-84253e514e02/go.mod h1:k08r+Yj1PRAmuayFiRK6MYuR5Ve4IuZtTfxErMIh0+c=
|
||||
github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok=
|
||||
github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE=
|
||||
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
|
||||
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0=
|
||||
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
|
||||
github.com/creachadair/mds v0.25.9 h1:080Hr8laN2h+l3NeVCGMBpXtIPnl9mz8e4HLraGPqtA=
|
||||
github.com/creachadair/mds v0.25.9/go.mod h1:4hatI3hRM+qhzuAmqPRFvaBM8mONkS7nsLxkcuTYUIs=
|
||||
github.com/creachadair/msync v0.7.1 h1:SeZmuEBXQPe5GqV/C94ER7QIZPwtvFbeQiykzt/7uho=
|
||||
github.com/creachadair/msync v0.7.1/go.mod h1:8CcFlLsSujfHE5wWm19uUBLHIPDAUr6LXDwneVMO008=
|
||||
github.com/creachadair/taskgroup v0.13.2 h1:3KyqakBuFsm3KkXi/9XIb0QcA8tEzLHLgaoidf0MdVc=
|
||||
github.com/creachadair/taskgroup v0.13.2/go.mod h1:i3V1Zx7H8RjwljUEeUWYT30Lmb9poewSb2XI1yTwD0g=
|
||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbwwpmHn1J5i43Y0uZP97GqasGCzSRJk=
|
||||
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ=
|
||||
github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc h1:8WFBn63wegobsYAX0YjD+8suexZDga5CctH4CCTx2+8=
|
||||
github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
|
||||
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q=
|
||||
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A=
|
||||
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/gaissmai/bart v0.26.1 h1:+w4rnLGNlA2GDVn382Tfe3jOsK5vOr5n4KmigJ9lbTo=
|
||||
github.com/gaissmai/bart v0.26.1/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c=
|
||||
github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I=
|
||||
github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo=
|
||||
github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 h1:vymEbVwYFP/L05h5TKQxvkXoKxNvTpjxYKdF1Nlwuao=
|
||||
github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go4org/hashtriemap v0.0.0-20251130024219-545ba229f689 h1:0psnKZ+N2IP43/SZC8SKx6OpFJwLmQb9m9QyV9BC2f8=
|
||||
github.com/go4org/hashtriemap v0.0.0-20251130024219-545ba229f689/go.mod h1:OGmRfY/9QEK2P5zCRtmqfbCF283xPkU2dvVA4MvbvpI=
|
||||
github.com/go4org/plan9netshell v0.0.0-20250324183649-788daa080737 h1:cf60tHxREO3g1nroKr2osU3JWZsJzkfi7rEg+oAB0Lo=
|
||||
github.com/go4org/plan9netshell v0.0.0-20250324183649-788daa080737/go.mod h1:MIS0jDzbU/vuM9MC4YnBITCv+RYuTRq8dJzmCrFsK9g=
|
||||
fyne.io/systray v1.12.2 h1:Y8DZxgLHsVQt6rY9Zrkkg+j67S7vv/1F2viOWKPpVeA=
|
||||
fyne.io/systray v1.12.2/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
|
||||
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg=
|
||||
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466/go.mod h1:ZiQxhyQ+bbbfxUKVvjfO498oPYvtYhZzycal3G/NHmU=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-tpm v0.9.4 h1:awZRf9FwOeTunQmHoDYSHJps3ie6f1UlhS1fOdPEt1I=
|
||||
github.com/google/go-tpm v0.9.4/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI=
|
||||
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
|
||||
github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU=
|
||||
github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo=
|
||||
github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc=
|
||||
github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
|
||||
github.com/illarion/gonotify/v3 v3.0.2 h1:O7S6vcopHexutmpObkeWsnzMJt/r1hONIEogeVNmJMk=
|
||||
github.com/illarion/gonotify/v3 v3.0.2/go.mod h1:HWGPdPe817GfvY3w7cx6zkbzNZfi3QjcBm/wgVvEL1U=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x56yVhWWlKFE8YpicaSfHwoV8SFbueA=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI=
|
||||
github.com/jellydator/ttlcache/v3 v3.1.0 h1:0gPFG0IHHP6xyUyXq+JaD8fwkDCqgqwohXNJBcYE71g=
|
||||
github.com/jellydator/ttlcache/v3 v3.1.0/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jsimonetti/rtnetlink v1.4.0 h1:Z1BF0fRgcETPEa0Kt0MRk3yV5+kF1FWTni6KUFKrq2I=
|
||||
github.com/jsimonetti/rtnetlink v1.4.0/go.mod h1:5W1jDvWdnthFJ7fxYX1GMK07BUpI4oskfOqvPteYS6E=
|
||||
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
|
||||
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ=
|
||||
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk=
|
||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
|
||||
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
|
||||
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0tpKMbdCXCIfBclan4oHk1Jb+Hrejirg=
|
||||
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42/go.mod h1:BB4YCPDOzfy7FniQ/lxuYQ3dgmM2cZumHbK8RpTjN2o=
|
||||
github.com/mdlayher/sdnotify v1.0.0 h1:Ma9XeLVN/l0qpyx1tNeMSeTjCPH6NtuD6/N9XdTlQ3c=
|
||||
github.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxnXQjCeJlGE=
|
||||
github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI=
|
||||
github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI=
|
||||
github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
|
||||
github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
|
||||
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
|
||||
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||
github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0=
|
||||
github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
||||
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
|
||||
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||
github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
|
||||
github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
|
||||
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0=
|
||||
github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs=
|
||||
github.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d h1:JcGKBZAL7ePLwOhUdN8qGQZlP5GueEiIZwY7R62pejE=
|
||||
github.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4=
|
||||
github.com/tailscale/gliderssh v0.3.4-0.20260330083525-c1389c70ff89 h1:glgVc1ZYMjwN1Q/ITWeuSQyl029uayagaR2sjsifehc=
|
||||
github.com/tailscale/gliderssh v0.3.4-0.20260330083525-c1389c70ff89/go.mod h1:wn16Km1EZOX4UEAyaZa3dBwfFGOJ7neck40NcwosJUw=
|
||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4=
|
||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg=
|
||||
github.com/tailscale/golang-x-crypto v0.0.0-20250404221719-a5573b049869 h1:SRL6irQkKGQKKLzvQP/ke/2ZuB7Py5+XuqtOgSj+iMM=
|
||||
github.com/tailscale/golang-x-crypto v0.0.0-20250404221719-a5573b049869/go.mod h1:ikbF+YT089eInTp9f2vmvy4+ZVnW5hzX1q2WknxSprQ=
|
||||
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd h1:Rf9uhF1+VJ7ZHqxrG8pJ6YacmHvVCmByDmGbAWCc/gA=
|
||||
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd/go.mod h1:EbW0wDK/qEUYI0A5bqq0C2kF8JTQwWONmGDBbzsxxHo=
|
||||
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 h1:uFsXVBE9Qr4ZoF094vE6iYTLDl0qCiKzYXlL6UeWObU=
|
||||
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=
|
||||
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc h1:24heQPtnFR+yfntqhI3oAu9i27nEojcQ4NuBQOo5ZFA=
|
||||
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc/go.mod h1:f93CXfllFsO9ZQVq+Zocb1Gp4G5Fz0b0rXHLOzt/Djc=
|
||||
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 h1:UBPHPtv8+nEAy2PD8RyAhOYvau1ek0HDJqLS/Pysi14=
|
||||
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
|
||||
github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6 h1:l10Gi6w9jxvinoiq15g8OToDdASBni4CyJOdHY1Hr8M=
|
||||
github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6/go.mod h1:ZXRML051h7o4OcI0d3AaILDIad/Xw0IkXaHM17dic1Y=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20260527010701-b48af7099cad h1:Ky26FR5yZ5IKEB0xtm5A8xSTb06ImY7kxBFrvgOmJSg=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20260527010701-b48af7099cad/go.mod h1:6SerzcvHWQchKO2BfNdmquA77CHSECZuFl+D9fp4RnI=
|
||||
github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e h1:zOGKqN5D5hHhiYUp091JqK7DPCqSARyUfduhGUY8Bek=
|
||||
github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg=
|
||||
github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA=
|
||||
github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk=
|
||||
github.com/u-root/u-root v0.14.0 h1:Ka4T10EEML7dQ5XDvO9c3MBN8z4nuSnGjcd1jmU2ivg=
|
||||
github.com/u-root/u-root v0.14.0/go.mod h1:hAyZorapJe4qzbLWlAkmSVCJGbfoU9Pu4jpJ1WMluqE=
|
||||
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=
|
||||
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=
|
||||
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
|
||||
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek=
|
||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
|
||||
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||
golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f h1:phY1HzDcf18Aq9A8KkmRtY9WvOFIxN8wgfvy6Zm1DV8=
|
||||
golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||
golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo=
|
||||
golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
|
||||
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
||||
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
||||
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
|
||||
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
|
||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
|
||||
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||
golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE=
|
||||
golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8 h1:Zy8IV/+FMLxy6j6p87vk/vQGKcdnbprwjTxc8UiUtsA=
|
||||
gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8/go.mod h1:QkHjoMIBaYtpVufgwv3keYAbln78mBoCuShZrPrer1Q=
|
||||
honnef.co/go/tools v0.7.0 h1:w6WUp1VbkqPEgLz4rkBzH/CSU6HkoqNLp6GstyTx3lU=
|
||||
honnef.co/go/tools v0.7.0/go.mod h1:pm29oPxeP3P82ISxZDgIYeOaf9ta6Pi0EWvCFoLG2vc=
|
||||
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
|
||||
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||
software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k=
|
||||
software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
|
||||
tailscale.com v1.100.0 h1:nm/M/dEaW9RaRsGUjW2HsSDpsZ60Jwd9k4gNW9tTFiE=
|
||||
tailscale.com v1.100.0/go.mod h1:DQ9YBy85DpNlSyeU2XRIWzbAu3RsGp/frv+Khg57meE=
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 193 B |
+47
-13
@@ -5,21 +5,22 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
// version is injected at build time via -ldflags "-X main.version=X.Y.Z"
|
||||
var version = "dev"
|
||||
|
||||
const AGENT_VERSION = "0.3.0"
|
||||
const (
|
||||
AGENT_VERSION = "0.3.0"
|
||||
APP_NAME = "studioE5"
|
||||
)
|
||||
|
||||
var (
|
||||
serverAddr = flag.String("server", "ws://localhost:3001", "Adresse WebSocket du serveur")
|
||||
nodeID = flag.String("node-id", defaultNodeID(), "ID du nœud (défaut: hostname)")
|
||||
dataDir = flag.String("data-dir", "./edubox-data", "Répertoire de données")
|
||||
uiEnabled = flag.Bool("ui", true, "Activer l'interface locale HTMX")
|
||||
headscaleURL = flag.String("headscale-url", "", "URL du serveur Headscale (ex: http://151.80.60.98:8080)")
|
||||
headscaleAuthKey = flag.String("headscale-auth-key", "", "Clé d'authentification Headscale")
|
||||
dataDir = flag.String("data-dir", "./studioE5-data", "Répertoire de données")
|
||||
uiEnabled = flag.Bool("ui", true, "Activer l'interface locale HTMX")
|
||||
noTray = flag.Bool("no-tray", false, "Désactiver l'icône dans la barre système (mode console)")
|
||||
)
|
||||
|
||||
func defaultNodeID() string {
|
||||
@@ -49,19 +50,52 @@ func main() {
|
||||
log.Fatalf("Cannot create data-dir: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("[EduBox Agent] version=%s node=%s data-dir=%s", AGENT_VERSION, *nodeID, *dataDir)
|
||||
cfg, created, err := loadOrCreateConfig(*dataDir)
|
||||
if err != nil {
|
||||
log.Fatalf("Cannot load config: %v", err)
|
||||
}
|
||||
|
||||
if cfg.Server == "" {
|
||||
cfg.Server = "ws://localhost:3001"
|
||||
}
|
||||
if cfg.NodeID == "" {
|
||||
cfg.NodeID = defaultNodeID()
|
||||
}
|
||||
if cfg.DataDir == "" {
|
||||
cfg.DataDir = *dataDir
|
||||
}
|
||||
|
||||
if err := saveConfig(*dataDir, cfg); err != nil {
|
||||
log.Fatalf("Cannot save config: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("[%s Agent] version=%s node=%s data-dir=%s server=%s", APP_NAME, AGENT_VERSION, cfg.NodeID, *dataDir, cfg.Server)
|
||||
|
||||
if *uiEnabled {
|
||||
go startUI(*dataDir, *nodeID, *serverAddr)
|
||||
go startUI(*dataDir, cfg.NodeID, cfg.Server)
|
||||
if created {
|
||||
go openBrowser(uiURL + "#settings")
|
||||
}
|
||||
}
|
||||
|
||||
go startWebSocket(*serverAddr, *nodeID, *dataDir)
|
||||
go startWebSocket(cfg.Server, cfg.NodeID, *dataDir, cfg.HeadscaleURL, cfg.HeadscaleAuthKey)
|
||||
|
||||
if *headscaleURL != "" && *headscaleAuthKey != "" {
|
||||
go startTailscaleAndReport(*dataDir, *nodeID, *headscaleURL, *headscaleAuthKey)
|
||||
shutdownCh := make(chan struct{})
|
||||
if *noTray {
|
||||
log.Printf("[%s Agent] running in console mode (no tray)", APP_NAME)
|
||||
<-shutdownCh
|
||||
return
|
||||
}
|
||||
|
||||
select {}
|
||||
// Run tray on its own locked OS thread; keep main blocked so the process
|
||||
// does not exit when systray is not available (e.g. headless Linux).
|
||||
go func() {
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
runTray(APP_NAME, shutdownCh)
|
||||
}()
|
||||
|
||||
<-shutdownCh
|
||||
}
|
||||
|
||||
func startTailscaleAndReport(dataDir, nodeID, headscaleURL, authKey string) {
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"fyne.io/systray"
|
||||
)
|
||||
|
||||
//go:embed icon.png
|
||||
var iconBytes []byte
|
||||
|
||||
const uiURL = "http://localhost:7070"
|
||||
|
||||
func runTray(appName string, shutdownCh chan struct{}) {
|
||||
systray.Run(func() { onTrayReady(appName, shutdownCh) }, func() { onTrayExit(shutdownCh) })
|
||||
}
|
||||
|
||||
func onTrayReady(appName string, shutdownCh chan struct{}) {
|
||||
systray.SetIcon(iconBytes)
|
||||
systray.SetTitle(appName)
|
||||
systray.SetTooltip(fmt.Sprintf("%s Agent - Cliquez pour ouvrir l'interface", appName))
|
||||
|
||||
mOpen := systray.AddMenuItem("Ouvrir l'interface", "Ouvrir l'interface web locale")
|
||||
mInstances := systray.AddMenuItem("Mes instances", "Afficher les instances")
|
||||
mSettings := systray.AddMenuItem("Paramètres", "Ouvrir les paramètres")
|
||||
systray.AddSeparator()
|
||||
mQuit := systray.AddMenuItem("Quitter", "Arrêter l'agent")
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-mOpen.ClickedCh:
|
||||
openBrowser(uiURL)
|
||||
case <-mInstances.ClickedCh:
|
||||
openBrowser(uiURL + "#instances")
|
||||
case <-mSettings.ClickedCh:
|
||||
openBrowser(uiURL + "#settings")
|
||||
case <-mQuit.ClickedCh:
|
||||
close(shutdownCh)
|
||||
systray.Quit()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func onTrayExit(shutdownCh chan struct{}) {
|
||||
log.Printf("Tray exit requested")
|
||||
// If the user did not already trigger shutdown via the menu, signal it now.
|
||||
select {
|
||||
case <-shutdownCh:
|
||||
default:
|
||||
close(shutdownCh)
|
||||
}
|
||||
// Give other goroutines a moment to clean up, then exit.
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func openBrowser(url string) {
|
||||
var cmd string
|
||||
var args []string
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
cmd = "rundll32"
|
||||
args = []string{"url.dll,FileProtocolHandler", url}
|
||||
case "darwin":
|
||||
cmd = "open"
|
||||
args = []string{url}
|
||||
default:
|
||||
cmd = "xdg-open"
|
||||
args = []string{url}
|
||||
}
|
||||
|
||||
if err := exec.Command(cmd, args...).Start(); err != nil {
|
||||
log.Printf("Failed to open browser: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeName(name string) string {
|
||||
return strings.ReplaceAll(name, " ", "")
|
||||
}
|
||||
+139
-65
@@ -2,104 +2,178 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"io"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"tailscale.com/tsnet"
|
||||
)
|
||||
|
||||
var globalTSServer *tsnet.Server
|
||||
var (
|
||||
tsCmd *exec.Cmd
|
||||
tsCmdMu sync.Mutex
|
||||
tsIP string
|
||||
tsDataDir string
|
||||
tsSocket string
|
||||
)
|
||||
|
||||
type tailscaleStatus struct {
|
||||
Self struct {
|
||||
TailscaleIPs []string `json:"TailscaleIPs"`
|
||||
} `json:"Self"`
|
||||
}
|
||||
|
||||
func tailscaleBin(name string) string {
|
||||
// Prefer bundled binaries (tailscale-bin/<os>/tailscaled etc.).
|
||||
ex, err := os.Executable()
|
||||
if err == nil {
|
||||
bundled := filepath.Join(filepath.Dir(ex), "tailscale-bin", runtime.GOOS, name)
|
||||
if runtime.GOOS == "windows" {
|
||||
bundled += ".exe"
|
||||
}
|
||||
if _, err := os.Stat(bundled); err == nil {
|
||||
return bundled
|
||||
}
|
||||
}
|
||||
if p, err := exec.LookPath(name); err == nil {
|
||||
return p
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func startTailscale(dataDir, nodeID, headscaleURL, authKey string) (string, error) {
|
||||
// Configure tsnet to use our Headscale server
|
||||
os.Setenv("TS_AUTHKEY", authKey)
|
||||
os.Setenv("TS_CONTROL_URL", headscaleURL)
|
||||
tsCmdMu.Lock()
|
||||
defer tsCmdMu.Unlock()
|
||||
|
||||
s := &tsnet.Server{
|
||||
Hostname: nodeID,
|
||||
Dir: dataDir,
|
||||
Logf: log.Printf,
|
||||
if tsCmd != nil {
|
||||
return tsIP, nil
|
||||
}
|
||||
|
||||
if err := s.Start(); err != nil {
|
||||
return "", fmt.Errorf("tailscale start: %w", err)
|
||||
if dataDir == "" {
|
||||
return "", fmt.Errorf("tailscale data dir is empty")
|
||||
}
|
||||
tsDataDir = filepath.Join(dataDir, "tailscale")
|
||||
if err := os.MkdirAll(tsDataDir, 0700); err != nil {
|
||||
return "", fmt.Errorf("create tailscale dir: %w", err)
|
||||
}
|
||||
tsSocket = filepath.Join(tsDataDir, "tailscaled.sock")
|
||||
stateFile := filepath.Join(tsDataDir, "tailscaled.state")
|
||||
|
||||
log.Printf("Starting tailscaled for node %s", nodeID)
|
||||
tsCmd = exec.Command(tailscaleBin("tailscaled"),
|
||||
"--state="+stateFile,
|
||||
"--socket="+tsSocket,
|
||||
"--tun=userspace-networking",
|
||||
)
|
||||
tsCmd.Stdout = os.Stdout
|
||||
tsCmd.Stderr = os.Stderr
|
||||
if err := tsCmd.Start(); err != nil {
|
||||
tsCmd = nil
|
||||
return "", fmt.Errorf("start tailscaled: %w", err)
|
||||
}
|
||||
|
||||
globalTSServer = s
|
||||
// Give tailscaled a moment to start listening.
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
// Wait for Tailscale to come up and retrieve IP
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
// Bring the interface up with the auth key.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
lc, err := s.LocalClient()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("tailscale local client: %w", err)
|
||||
upCmd := exec.CommandContext(ctx, tailscaleBin("tailscale"),
|
||||
"--socket="+tsSocket,
|
||||
"up",
|
||||
"--authkey="+authKey,
|
||||
"--login-server="+headscaleURL,
|
||||
"--hostname="+nodeID,
|
||||
"--accept-dns=false",
|
||||
"--operator=root",
|
||||
)
|
||||
upCmd.Stdout = os.Stdout
|
||||
upCmd.Stderr = os.Stderr
|
||||
if err := upCmd.Run(); err != nil {
|
||||
_ = tsCmd.Process.Kill()
|
||||
_ = tsCmd.Wait()
|
||||
tsCmd = nil
|
||||
return "", fmt.Errorf("tailscale up: %w", err)
|
||||
}
|
||||
|
||||
var tailscaleIP string
|
||||
// Wait for an IP address.
|
||||
for {
|
||||
status, err := lc.Status(ctx)
|
||||
out, err := exec.CommandContext(ctx, tailscaleBin("tailscale"),
|
||||
"--socket="+tsSocket,
|
||||
"status", "--json",
|
||||
).Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("tailscale status: %w", err)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
_ = tsCmd.Process.Kill()
|
||||
_ = tsCmd.Wait()
|
||||
tsCmd = nil
|
||||
return "", fmt.Errorf("tailscale status: %w", err)
|
||||
default:
|
||||
time.Sleep(1 * time.Second)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if status.Self != nil && len(status.Self.TailscaleIPs) > 0 {
|
||||
tailscaleIP = status.Self.TailscaleIPs[0].String()
|
||||
var st tailscaleStatus
|
||||
if err := json.Unmarshal(out, &st); err == nil && len(st.Self.TailscaleIPs) > 0 {
|
||||
tsIP = st.Self.TailscaleIPs[0]
|
||||
break
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
_ = tsCmd.Process.Kill()
|
||||
_ = tsCmd.Wait()
|
||||
tsCmd = nil
|
||||
return "", fmt.Errorf("tailscale IP timeout")
|
||||
case <-time.After(1 * time.Second):
|
||||
default:
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Tailscale started with IP: %s", tailscaleIP)
|
||||
return tailscaleIP, nil
|
||||
log.Printf("Tailscale started with IP: %s", tsIP)
|
||||
return tsIP, nil
|
||||
}
|
||||
|
||||
func startTailscaleProxy(port int) (net.Listener, error) {
|
||||
if globalTSServer == nil {
|
||||
return nil, fmt.Errorf("tailscale server not started")
|
||||
}
|
||||
ln, err := globalTSServer.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("tailscale listen on port %d: %w", port, err)
|
||||
}
|
||||
go func() {
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
log.Printf("tailscale proxy accept error on port %d: %v", port, err)
|
||||
return
|
||||
}
|
||||
go handleProxyConn(conn, port)
|
||||
}
|
||||
}()
|
||||
log.Printf("Tailscale proxy started on port %d", port)
|
||||
return ln, nil
|
||||
func stopTailscale() {
|
||||
tsCmdMu.Lock()
|
||||
defer tsCmdMu.Unlock()
|
||||
stopTailscaleLocked()
|
||||
}
|
||||
|
||||
func handleProxyConn(src net.Conn, port int) {
|
||||
defer src.Close()
|
||||
dst, err := net.Dial("tcp", fmt.Sprintf("localhost:%d", port))
|
||||
if err != nil {
|
||||
log.Printf("tailscale proxy dial localhost:%d error: %v", port, err)
|
||||
func stopTailscaleLocked() {
|
||||
if tsCmd == nil || tsCmd.Process == nil {
|
||||
return
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
done := make(chan struct{}, 2)
|
||||
go func() {
|
||||
_, _ = io.Copy(dst, src)
|
||||
done <- struct{}{}
|
||||
}()
|
||||
go func() {
|
||||
_, _ = io.Copy(src, dst)
|
||||
done <- struct{}{}
|
||||
}()
|
||||
<-done
|
||||
if tsSocket != "" {
|
||||
_ = exec.Command(tailscaleBin("tailscale"), "--socket="+tsSocket, "down").Run()
|
||||
}
|
||||
_ = tsCmd.Process.Kill()
|
||||
_ = tsCmd.Wait()
|
||||
tsCmd = nil
|
||||
tsIP = ""
|
||||
log.Printf("Tailscale stopped")
|
||||
}
|
||||
|
||||
func isTailscaleRunning() bool {
|
||||
tsCmdMu.Lock()
|
||||
defer tsCmdMu.Unlock()
|
||||
if tsCmd == nil || tsCmd.Process == nil {
|
||||
return false
|
||||
}
|
||||
// Signal 0 checks process existence without affecting it.
|
||||
return tsCmd.Process.Signal(syscall.Signal(0)) == nil
|
||||
}
|
||||
|
||||
func getTailscaleIP() string {
|
||||
tsCmdMu.Lock()
|
||||
defer tsCmdMu.Unlock()
|
||||
return tsIP
|
||||
}
|
||||
|
||||
|
||||
|
||||
+54
-2
@@ -2,9 +2,12 @@ package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
@@ -20,6 +23,55 @@ func startUI(dataDir, nodeID, serverAddr string) {
|
||||
fmt.Fprint(w, uiHTML)
|
||||
})
|
||||
|
||||
http.HandleFunc("/api/config", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
cfg, _, err := loadOrCreateConfig(dataDir)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// Do not expose the auth key in plain GET unless requested; for local UI it is fine.
|
||||
json.NewEncoder(w).Encode(cfg)
|
||||
case http.MethodPost:
|
||||
var cfg AgentConfig
|
||||
if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if cfg.DataDir == "" {
|
||||
cfg.DataDir = dataDir
|
||||
}
|
||||
if err := saveConfig(dataDir, &cfg); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
default:
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
})
|
||||
|
||||
http.HandleFunc("/api/restart", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
go func() {
|
||||
cmd := exec.Command(os.Args[0], os.Args[1:]...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Printf("Restart failed: %v", err)
|
||||
return
|
||||
}
|
||||
os.Exit(0)
|
||||
}()
|
||||
})
|
||||
|
||||
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
@@ -72,9 +124,9 @@ func startUI(dataDir, nodeID, serverAddr string) {
|
||||
})
|
||||
|
||||
port := "7070"
|
||||
log.Printf("UI starting on http://localhost:%s", port)
|
||||
log.Printf("%s UI starting on http://localhost:%s", APP_NAME, port)
|
||||
if err := http.ListenAndServe("127.0.0.1:"+port, nil); err != nil {
|
||||
log.Fatalf("UI server error: %v", err)
|
||||
log.Fatalf("%s UI server error: %v", APP_NAME, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+101
-3
@@ -2,7 +2,7 @@
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>EduBox Agent</title>
|
||||
<title>studioE5 Agent</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body { font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif; background: #f1f5f9; margin: 0; padding: 2rem; color: #1e293b; }
|
||||
@@ -10,9 +10,13 @@
|
||||
.card { background: white; border-radius: 12px; padding: 1.5rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 1rem; }
|
||||
h1 { font-size: 1.5rem; margin: 0 0 1rem; }
|
||||
h2 { font-size: 1.125rem; margin: 0 0 1rem; }
|
||||
label { display: block; font-size: 0.85rem; font-weight: 600; margin-bottom: 0.25rem; color: #475569; }
|
||||
input { width: 100%; padding: 0.6rem; border: 1px solid #cbd5e1; border-radius: 8px; margin-bottom: 0.75rem; font-size: 1rem; }
|
||||
input:read-only { background: #f1f5f9; }
|
||||
button { width: 100%; padding: 0.7rem; background: #2563eb; color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: 600; font-size: 1rem; }
|
||||
button:hover { background: #1d4ed8; }
|
||||
button.secondary { background: #e2e8f0; color: #1e293b; }
|
||||
button.secondary:hover { background: #cbd5e1; }
|
||||
.status { margin-top: 0.75rem; font-size: 0.9rem; min-height: 1.2rem; }
|
||||
.success { color: #16a34a; }
|
||||
.error { color: #dc2626; }
|
||||
@@ -30,16 +34,44 @@
|
||||
.instance-link { font-size: 0.875rem; color: #2563eb; text-decoration: none; font-weight: 500; }
|
||||
.instance-link:hover { text-decoration: underline; }
|
||||
.empty { text-align: center; color: #64748b; padding: 1rem 0; }
|
||||
.toolbar { display: flex; gap: 0.5rem; margin-top: 1rem; }
|
||||
.toolbar button { flex: 1; }
|
||||
.note { font-size: 0.8rem; color: #64748b; margin-top: 0.5rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<h1>EduBox Agent</h1>
|
||||
<div id="home-card" class="card">
|
||||
<h1>studioE5 Agent</h1>
|
||||
<div id="main">
|
||||
<p class="info">Connexion en cours...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="settings-card" class="card" style="display:none;">
|
||||
<h2>Paramètres</h2>
|
||||
<form id="settings-form" onsubmit="saveSettings(event)">
|
||||
<label for="cfg-server">Serveur WebSocket</label>
|
||||
<input type="text" id="cfg-server" placeholder="ws://localhost:3001">
|
||||
|
||||
<label for="cfg-node">ID du nœud</label>
|
||||
<input type="text" id="cfg-node" placeholder="MON-PC">
|
||||
|
||||
<label for="cfg-headscale-url">URL Headscale</label>
|
||||
<input type="text" id="cfg-headscale-url" placeholder="https://headscale.exemple.com">
|
||||
|
||||
<label for="cfg-headscale-key">Clé Headscale</label>
|
||||
<input type="password" id="cfg-headscale-key" placeholder="hskey-auth-...">
|
||||
|
||||
<label for="cfg-data-dir">Répertoire de données</label>
|
||||
<input type="text" id="cfg-data-dir" readonly>
|
||||
|
||||
<button type="submit">Enregistrer et redémarrer</button>
|
||||
</form>
|
||||
<div id="settings-status" class="status"></div>
|
||||
<p class="note">Le redémarrage est nécessaire pour prendre en compte les nouveaux paramètres.</p>
|
||||
</div>
|
||||
|
||||
<div id="instances-card" class="card" style="display:none;">
|
||||
<h2>Mes instances</h2>
|
||||
<div id="instances" class="instance-list"></div>
|
||||
@@ -49,6 +81,8 @@
|
||||
<script>
|
||||
const ws = new WebSocket('ws://' + location.host + '/ws');
|
||||
const main = document.getElementById('main');
|
||||
const homeCard = document.getElementById('home-card');
|
||||
const settingsCard = document.getElementById('settings-card');
|
||||
const instancesCard = document.getElementById('instances-card');
|
||||
const instancesContainer = document.getElementById('instances');
|
||||
|
||||
@@ -60,6 +94,7 @@
|
||||
const msg = JSON.parse(ev.data);
|
||||
|
||||
if (msg.action === 'not_activated') {
|
||||
showHome();
|
||||
main.innerHTML = `
|
||||
<p>Entre ton code d'activation (6 caractères) :</p>
|
||||
<input type="text" id="code" maxlength="6" placeholder="XXXXXX" onkeydown="if(event.key==='Enter')activate()">
|
||||
@@ -67,9 +102,13 @@
|
||||
<div id="status" class="status"></div>
|
||||
`;
|
||||
} else if (msg.action === 'activated') {
|
||||
showHome();
|
||||
main.innerHTML = `
|
||||
<p class="success">✅ Activé : <strong>${escapeHtml(msg.studentName || '')}</strong></p>
|
||||
<p class="info">Tes instances apparaissent ci-dessous.</p>
|
||||
<div class="toolbar">
|
||||
<button class="secondary" onclick="showSettings()">⚙️ Paramètres</button>
|
||||
</div>
|
||||
`;
|
||||
instancesCard.style.display = 'block';
|
||||
ws.send(JSON.stringify({action: 'instances'}));
|
||||
@@ -130,6 +169,65 @@
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
try {
|
||||
const res = await fetch('/api/config');
|
||||
const cfg = await res.json();
|
||||
document.getElementById('cfg-server').value = cfg.server || '';
|
||||
document.getElementById('cfg-node').value = cfg.node_id || '';
|
||||
document.getElementById('cfg-headscale-url').value = cfg.headscale_url || '';
|
||||
document.getElementById('cfg-headscale-key').value = cfg.headscale_auth_key || '';
|
||||
document.getElementById('cfg-data-dir').value = cfg.data_dir || '';
|
||||
} catch (err) {
|
||||
document.getElementById('settings-status').innerHTML = `<span class="error">Erreur chargement config</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSettings(event) {
|
||||
event.preventDefault();
|
||||
const status = document.getElementById('settings-status');
|
||||
status.innerHTML = 'Enregistrement...';
|
||||
const cfg = {
|
||||
server: document.getElementById('cfg-server').value.trim(),
|
||||
node_id: document.getElementById('cfg-node').value.trim(),
|
||||
headscale_url: document.getElementById('cfg-headscale-url').value.trim(),
|
||||
headscale_auth_key: document.getElementById('cfg-headscale-key').value.trim(),
|
||||
data_dir: document.getElementById('cfg-data-dir').value.trim()
|
||||
};
|
||||
try {
|
||||
const res = await fetch('/api/config', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(cfg)
|
||||
});
|
||||
if (res.ok) {
|
||||
status.innerHTML = '<span class="success">✅ Enregistré. Redémarrage en cours...</span>';
|
||||
await fetch('/api/restart', {method: 'POST'});
|
||||
setTimeout(() => location.reload(), 3000);
|
||||
} else {
|
||||
status.innerHTML = `<span class="error">❌ Erreur ${res.status}</span>`;
|
||||
}
|
||||
} catch (err) {
|
||||
status.innerHTML = `<span class="error">❌ ${escapeHtml(err.message)}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
function showSettings() {
|
||||
homeCard.style.display = 'none';
|
||||
instancesCard.style.display = 'none';
|
||||
settingsCard.style.display = 'block';
|
||||
loadSettings();
|
||||
}
|
||||
|
||||
function showHome() {
|
||||
homeCard.style.display = 'block';
|
||||
settingsCard.style.display = 'none';
|
||||
}
|
||||
|
||||
if (location.hash === '#settings') {
|
||||
showSettings();
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
if (text == null) return '';
|
||||
return String(text)
|
||||
|
||||
+64
-50
@@ -3,7 +3,6 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -29,11 +28,6 @@ var (
|
||||
mainConnMu sync.Mutex
|
||||
)
|
||||
|
||||
var (
|
||||
tsProxies = make(map[int]net.Listener)
|
||||
tsProxiesMu sync.Mutex
|
||||
)
|
||||
|
||||
func sendMessage(msg WSMessage) error {
|
||||
mainConnMu.Lock()
|
||||
defer mainConnMu.Unlock()
|
||||
@@ -86,7 +80,7 @@ func notifyUI(msg map[string]interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
func startWebSocket(serverAddr, nodeID, dataDir string) {
|
||||
func startWebSocket(serverAddr, nodeID, dataDir, headscaleURL, headscaleAuthKey string) {
|
||||
for {
|
||||
conn, _, err := websocket.DefaultDialer.Dial(serverAddr, nil)
|
||||
if err != nil {
|
||||
@@ -144,7 +138,7 @@ func startWebSocket(serverAddr, nodeID, dataDir string) {
|
||||
break
|
||||
}
|
||||
log.Printf("WS received from server: action=%s", msg.Action)
|
||||
handleMessage(conn, msg, dataDir, nodeID)
|
||||
handleMessage(conn, msg, dataDir, nodeID, headscaleURL, headscaleAuthKey)
|
||||
}
|
||||
|
||||
close(done)
|
||||
@@ -157,7 +151,7 @@ func startWebSocket(serverAddr, nodeID, dataDir string) {
|
||||
}
|
||||
}
|
||||
|
||||
func handleMessage(conn *websocket.Conn, msg WSMessage, dataDir, nodeID string) {
|
||||
func handleMessage(conn *websocket.Conn, msg WSMessage, dataDir, nodeID, headscaleURL, headscaleAuthKey string) {
|
||||
switch msg.Action {
|
||||
case "activated":
|
||||
log.Printf("handleMessage: activated received, student=%s", msg.StudentName)
|
||||
@@ -176,6 +170,34 @@ func handleMessage(conn *websocket.Conn, msg WSMessage, dataDir, nodeID string)
|
||||
case "registered":
|
||||
// Server acknowledged our register message; nothing to do.
|
||||
return
|
||||
case "start_vpn":
|
||||
log.Printf("Server requested VPN start")
|
||||
if headscaleURL == "" || headscaleAuthKey == "" {
|
||||
log.Printf("Cannot start VPN: headscale config missing")
|
||||
sendMessage(WSMessage{Action: "vpn_error", NodeID: nodeID, Error: "headscale config missing"})
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
ip, err := startTailscale(dataDir, nodeID, headscaleURL, headscaleAuthKey)
|
||||
if err != nil {
|
||||
log.Printf("start_vpn error: %v", err)
|
||||
sendMessage(WSMessage{Action: "vpn_error", NodeID: nodeID, Error: err.Error()})
|
||||
return
|
||||
}
|
||||
for {
|
||||
if err := sendMessage(WSMessage{Action: "tailscale_ip", NodeID: nodeID, TailscaleIP: ip}); err != nil {
|
||||
log.Printf("Waiting for WebSocket to send tailscale_ip...")
|
||||
time.Sleep(1 * time.Second)
|
||||
continue
|
||||
}
|
||||
log.Printf("Sent tailscale_ip to server: %s", ip)
|
||||
break
|
||||
}
|
||||
}()
|
||||
case "stop_vpn":
|
||||
log.Printf("Server requested VPN stop")
|
||||
stopTailscale()
|
||||
sendMessage(WSMessage{Action: "vpn_stopped", NodeID: nodeID})
|
||||
case "activation_failed":
|
||||
log.Printf("handleMessage: activation_failed received, error=%s", msg.Error)
|
||||
notifyUI(map[string]interface{}{
|
||||
@@ -192,7 +214,7 @@ func handleMessage(conn *websocket.Conn, msg WSMessage, dataDir, nodeID string)
|
||||
}); err != nil {
|
||||
log.Printf("upsertInstance error: %v", err)
|
||||
}
|
||||
if err := writeCompose(dataDir, msg.InstanceID, msg.ComposeConfig); err != nil {
|
||||
if err := writeCompose(dataDir, msg.InstanceID, msg.ComposeConfig, msg.Port); err != nil {
|
||||
log.Printf("writeCompose error: %v", err)
|
||||
_ = upsertInstance(dataDir, &InstanceInfo{ID: msg.InstanceID, TemplateName: msg.Type, Port: msg.Port, Status: "error"})
|
||||
sendMessage(WSMessage{Action: "instance_error", InstanceID: msg.InstanceID, Error: err.Error()})
|
||||
@@ -205,7 +227,7 @@ func handleMessage(conn *websocket.Conn, msg WSMessage, dataDir, nodeID string)
|
||||
return
|
||||
}
|
||||
// Repair older WordPress instances: remove hardcoded WP_HOME/WP_SITEURL
|
||||
// so the EduBox mu-plugin can compute the public URL from the Host header.
|
||||
// so the studioE5 mu-plugin can compute the public URL from the Host header.
|
||||
go func() {
|
||||
// Give the container a moment to be ready before touching wp-config.php
|
||||
time.Sleep(2 * time.Second)
|
||||
@@ -213,16 +235,8 @@ func handleMessage(conn *websocket.Conn, msg WSMessage, dataDir, nodeID string)
|
||||
log.Printf("stripWordPressHardcodedURLs error: %v", err)
|
||||
}
|
||||
}()
|
||||
// Start Tailscale proxy so the server can reach localhost via Tailscale IP
|
||||
tsProxiesMu.Lock()
|
||||
if _, exists := tsProxies[msg.Port]; !exists {
|
||||
if ln, err := startTailscaleProxy(msg.Port); err == nil {
|
||||
tsProxies[msg.Port] = ln
|
||||
} else {
|
||||
log.Printf("startTailscaleProxy error: %v", err)
|
||||
}
|
||||
}
|
||||
tsProxiesMu.Unlock()
|
||||
// Ensure Tailscale is running so the server can reach the node
|
||||
go ensureTailscale(dataDir, nodeID, headscaleURL, headscaleAuthKey, msg.Port)
|
||||
|
||||
status := getInstanceStatus(dataDir, msg.InstanceID)
|
||||
_ = upsertInstance(dataDir, &InstanceInfo{ID: msg.InstanceID, TemplateName: msg.Type, Port: msg.Port, Status: status})
|
||||
@@ -230,15 +244,6 @@ func handleMessage(conn *websocket.Conn, msg WSMessage, dataDir, nodeID string)
|
||||
notifyUI(map[string]interface{}{"action": "instances_updated"})
|
||||
case "stop":
|
||||
log.Printf("Stop instance %s", msg.InstanceID)
|
||||
// Stop Tailscale proxy for this instance port
|
||||
if inst, _ := loadInstances(dataDir); inst[msg.InstanceID] != nil {
|
||||
tsProxiesMu.Lock()
|
||||
if ln, exists := tsProxies[inst[msg.InstanceID].Port]; exists {
|
||||
_ = ln.Close()
|
||||
delete(tsProxies, inst[msg.InstanceID].Port)
|
||||
}
|
||||
tsProxiesMu.Unlock()
|
||||
}
|
||||
if err := dockerComposeDown(dataDir, msg.InstanceID); err != nil {
|
||||
log.Printf("dockerComposeDown error: %v", err)
|
||||
}
|
||||
@@ -249,21 +254,13 @@ func handleMessage(conn *websocket.Conn, msg WSMessage, dataDir, nodeID string)
|
||||
notifyUI(map[string]interface{}{"action": "instances_updated"})
|
||||
case "delete":
|
||||
log.Printf("Delete instance %s", msg.InstanceID)
|
||||
tsProxiesMu.Lock()
|
||||
if inst, _ := loadInstances(dataDir); inst[msg.InstanceID] != nil {
|
||||
if ln, exists := tsProxies[inst[msg.InstanceID].Port]; exists {
|
||||
_ = ln.Close()
|
||||
delete(tsProxies, inst[msg.InstanceID].Port)
|
||||
}
|
||||
}
|
||||
tsProxiesMu.Unlock()
|
||||
dockerComposeRm(dataDir, msg.InstanceID)
|
||||
removeInstance(dataDir, msg.InstanceID)
|
||||
notifyUI(map[string]interface{}{"action": "instances_updated"})
|
||||
case "reset":
|
||||
log.Printf("Reset instance %s", msg.InstanceID)
|
||||
dockerComposeRm(dataDir, msg.InstanceID)
|
||||
if err := writeCompose(dataDir, msg.InstanceID, msg.ComposeConfig); err != nil {
|
||||
if err := writeCompose(dataDir, msg.InstanceID, msg.ComposeConfig, msg.Port); err != nil {
|
||||
log.Printf("writeCompose error: %v", err)
|
||||
_ = upsertInstance(dataDir, &InstanceInfo{ID: msg.InstanceID, TemplateName: msg.Type, Port: msg.Port, Status: "error"})
|
||||
sendMessage(WSMessage{Action: "instance_error", InstanceID: msg.InstanceID, Error: err.Error()})
|
||||
@@ -276,7 +273,7 @@ func handleMessage(conn *websocket.Conn, msg WSMessage, dataDir, nodeID string)
|
||||
return
|
||||
}
|
||||
// Repair older WordPress instances: remove hardcoded WP_HOME/WP_SITEURL
|
||||
// so the EduBox mu-plugin can compute the public URL from the Host header.
|
||||
// so the studioE5 mu-plugin can compute the public URL from the Host header.
|
||||
go func() {
|
||||
// Give the container a moment to be ready before touching wp-config.php
|
||||
time.Sleep(2 * time.Second)
|
||||
@@ -284,16 +281,8 @@ func handleMessage(conn *websocket.Conn, msg WSMessage, dataDir, nodeID string)
|
||||
log.Printf("stripWordPressHardcodedURLs error: %v", err)
|
||||
}
|
||||
}()
|
||||
// Start Tailscale proxy so the server can reach localhost via Tailscale IP
|
||||
tsProxiesMu.Lock()
|
||||
if _, exists := tsProxies[msg.Port]; !exists {
|
||||
if ln, err := startTailscaleProxy(msg.Port); err == nil {
|
||||
tsProxies[msg.Port] = ln
|
||||
} else {
|
||||
log.Printf("startTailscaleProxy error: %v", err)
|
||||
}
|
||||
}
|
||||
tsProxiesMu.Unlock()
|
||||
// Ensure Tailscale is running so the server can reach the node
|
||||
go ensureTailscale(dataDir, nodeID, headscaleURL, headscaleAuthKey, msg.Port)
|
||||
|
||||
status := getInstanceStatus(dataDir, msg.InstanceID)
|
||||
_ = upsertInstance(dataDir, &InstanceInfo{ID: msg.InstanceID, TemplateName: msg.Type, Port: msg.Port, Status: status})
|
||||
@@ -303,3 +292,28 @@ func handleMessage(conn *websocket.Conn, msg WSMessage, dataDir, nodeID string)
|
||||
log.Printf("Unknown action: %s", msg.Action)
|
||||
}
|
||||
}
|
||||
|
||||
func ensureTailscale(dataDir, nodeID, headscaleURL, headscaleAuthKey string, port int) {
|
||||
if headscaleURL == "" || headscaleAuthKey == "" {
|
||||
log.Printf("Cannot ensure Tailscale: headscale config missing")
|
||||
return
|
||||
}
|
||||
if isTailscaleRunning() {
|
||||
return
|
||||
}
|
||||
log.Printf("Tailscale not running, starting it for instance port %d", port)
|
||||
ip, err := startTailscale(dataDir, nodeID, headscaleURL, headscaleAuthKey)
|
||||
if err != nil {
|
||||
log.Printf("ensureTailscale start error: %v", err)
|
||||
return
|
||||
}
|
||||
for {
|
||||
if err := sendMessage(WSMessage{Action: "tailscale_ip", NodeID: nodeID, TailscaleIP: ip}); err != nil {
|
||||
log.Printf("Waiting for WebSocket to send tailscale_ip...")
|
||||
time.Sleep(1 * time.Second)
|
||||
continue
|
||||
}
|
||||
log.Printf("Sent tailscale_ip to server: %s", ip)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user