push
This commit is contained in:
commit
1d167420c3
89 changed files with 10707 additions and 0 deletions
9
cmd/yggdrasil/chuser_other.go
Normal file
9
cmd/yggdrasil/chuser_other.go
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
//go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris
|
||||
|
||||
package main
|
||||
|
||||
import "errors"
|
||||
|
||||
func chuser(user string) error {
|
||||
return errors.New("setting uid/gid is not supported on this platform")
|
||||
}
|
||||
62
cmd/yggdrasil/chuser_unix.go
Normal file
62
cmd/yggdrasil/chuser_unix.go
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/user"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func chuser(input string) error {
|
||||
givenUser, givenGroup, _ := strings.Cut(input, ":")
|
||||
if givenUser == "" {
|
||||
return fmt.Errorf("user is empty")
|
||||
}
|
||||
if strings.Contains(input, ":") && givenGroup == "" {
|
||||
return fmt.Errorf("group is empty")
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
usr *user.User
|
||||
grp *user.Group
|
||||
uid, gid int
|
||||
)
|
||||
|
||||
if usr, err = user.LookupId(givenUser); err != nil {
|
||||
if usr, err = user.Lookup(givenUser); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if uid, err = strconv.Atoi(usr.Uid); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if givenGroup != "" {
|
||||
if grp, err = user.LookupGroupId(givenGroup); err != nil {
|
||||
if grp, err = user.LookupGroup(givenGroup); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
gid, _ = strconv.Atoi(grp.Gid)
|
||||
} else {
|
||||
gid, _ = strconv.Atoi(usr.Gid)
|
||||
}
|
||||
|
||||
if err := unix.Setgroups([]int{gid}); err != nil {
|
||||
return fmt.Errorf("setgroups: %d: %v", gid, err)
|
||||
}
|
||||
if err := unix.Setgid(gid); err != nil {
|
||||
return fmt.Errorf("setgid: %d: %v", gid, err)
|
||||
}
|
||||
if err := unix.Setuid(uid); err != nil {
|
||||
return fmt.Errorf("setuid: %d: %v", uid, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
79
cmd/yggdrasil/chuser_unix_test.go
Normal file
79
cmd/yggdrasil/chuser_unix_test.go
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os/user"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Usernames must not contain a number sign.
|
||||
func TestEmptyString(t *testing.T) {
|
||||
if chuser("") == nil {
|
||||
t.Fatal("the empty string is not a valid user")
|
||||
}
|
||||
}
|
||||
|
||||
// Either omit delimiter and group, or omit both.
|
||||
func TestEmptyGroup(t *testing.T) {
|
||||
if chuser("0:") == nil {
|
||||
t.Fatal("the empty group is not allowed")
|
||||
}
|
||||
}
|
||||
|
||||
// Either user only or user and group.
|
||||
func TestGroupOnly(t *testing.T) {
|
||||
if chuser(":0") == nil {
|
||||
t.Fatal("group only is not allowed")
|
||||
}
|
||||
}
|
||||
|
||||
// Usenames must not contain the number sign.
|
||||
func TestInvalidUsername(t *testing.T) {
|
||||
const username = "#user"
|
||||
if chuser(username) == nil {
|
||||
t.Fatalf("'%s' is not a valid username", username)
|
||||
}
|
||||
}
|
||||
|
||||
// User IDs must be non-negative.
|
||||
func TestInvalidUserid(t *testing.T) {
|
||||
if chuser("-1") == nil {
|
||||
t.Fatal("User ID cannot be negative")
|
||||
}
|
||||
}
|
||||
|
||||
// Change to the current user by ID.
|
||||
func TestCurrentUserid(t *testing.T) {
|
||||
usr, err := user.Current()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if usr.Uid != "0" {
|
||||
t.Skip("setgroups(2): Only the superuser may set new groups.")
|
||||
}
|
||||
|
||||
if err = chuser(usr.Uid); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Change to a common user by name.
|
||||
func TestCommonUsername(t *testing.T) {
|
||||
usr, err := user.Current()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if usr.Uid != "0" {
|
||||
t.Skip("setgroups(2): Only the superuser may set new groups.")
|
||||
}
|
||||
|
||||
if err := chuser("nobody"); err != nil {
|
||||
if _, ok := err.(user.UnknownUserError); ok {
|
||||
t.Skip(err)
|
||||
}
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
329
cmd/yggdrasil/dashboard.go
Normal file
329
cmd/yggdrasil/dashboard.go
Normal file
|
|
@ -0,0 +1,329 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
"embed"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gologme/log"
|
||||
"github.com/yggdrasil-network/yggdrasil-go/src/address"
|
||||
)
|
||||
|
||||
//go:embed dashboard.html
|
||||
var dashboardHTML embed.FS
|
||||
|
||||
type dashboardServer struct {
|
||||
node *node
|
||||
logger *log.Logger
|
||||
username string
|
||||
password string
|
||||
readOnly bool
|
||||
banned map[string]struct{}
|
||||
mu sync.RWMutex
|
||||
http *http.Server
|
||||
}
|
||||
|
||||
type dashboardStatus struct {
|
||||
Self any `json:"self"`
|
||||
Peers []dashboardPeer `json:"peers"`
|
||||
Sessions []dashboardFlow `json:"sessions"`
|
||||
Paths []dashboardPath `json:"paths"`
|
||||
Tree []dashboardTree `json:"tree"`
|
||||
Banned []string `json:"banned"`
|
||||
Now time.Time `json:"now"`
|
||||
}
|
||||
|
||||
type dashboardPeer struct {
|
||||
URI string `json:"uri"`
|
||||
Remote string `json:"remote"`
|
||||
Up bool `json:"up"`
|
||||
Inbound bool `json:"inbound"`
|
||||
IP string `json:"ip"`
|
||||
Uptime string `json:"uptime"`
|
||||
RTT string `json:"rtt"`
|
||||
RX uint64 `json:"rx"`
|
||||
TX uint64 `json:"tx"`
|
||||
RXRate uint64 `json:"rx_rate"`
|
||||
TXRate uint64 `json:"tx_rate"`
|
||||
Priority uint8 `json:"priority"`
|
||||
Cost uint64 `json:"cost"`
|
||||
Banned bool `json:"banned"`
|
||||
}
|
||||
|
||||
type dashboardFlow struct {
|
||||
IP string `json:"ip"`
|
||||
RX uint64 `json:"rx"`
|
||||
TX uint64 `json:"tx"`
|
||||
Uptime string `json:"uptime"`
|
||||
}
|
||||
|
||||
type dashboardPath struct {
|
||||
IP string `json:"ip"`
|
||||
Path []uint64 `json:"path"`
|
||||
}
|
||||
|
||||
type dashboardTree struct {
|
||||
IP string `json:"ip"`
|
||||
Parent string `json:"parent"`
|
||||
}
|
||||
|
||||
func startDashboard(n *node, logger *log.Logger, listenAddr, username, password string, readOnly bool) (*dashboardServer, error) {
|
||||
if strings.TrimSpace(listenAddr) == "" {
|
||||
return nil, nil
|
||||
}
|
||||
d := &dashboardServer{
|
||||
node: n,
|
||||
logger: logger,
|
||||
username: username,
|
||||
password: password,
|
||||
readOnly: readOnly,
|
||||
banned: make(map[string]struct{}),
|
||||
}
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", d.handleIndex)
|
||||
mux.HandleFunc("/api/status", d.handleStatus)
|
||||
mux.HandleFunc("/api/peer/traffic", d.handlePeerTraffic)
|
||||
mux.HandleFunc("/api/peer/ban", d.handlePeerBan)
|
||||
|
||||
d.http = &http.Server{
|
||||
Addr: listenAddr,
|
||||
Handler: d.withAuth(mux),
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
}
|
||||
ln, err := net.Listen("tcp", listenAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
go func() {
|
||||
if err := d.http.Serve(ln); err != nil && err != http.ErrServerClosed {
|
||||
d.logger.Errorf("dashboard failed: %v", err)
|
||||
}
|
||||
}()
|
||||
if readOnly {
|
||||
logger.Infof("Public dashboard listening on http://%s", ln.Addr().String())
|
||||
return d, nil
|
||||
}
|
||||
logger.Infof("Dashboard listening on http://%s", ln.Addr().String())
|
||||
if username == "" || password == "" {
|
||||
logger.Warnln("Dashboard authentication disabled; set both -dashboard-user and -dashboard-password to secure it")
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (d *dashboardServer) stop() {
|
||||
if d == nil || d.http == nil {
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
_ = d.http.Shutdown(ctx)
|
||||
}
|
||||
|
||||
func (d *dashboardServer) withAuth(next http.Handler) http.Handler {
|
||||
if d.username == "" || d.password == "" {
|
||||
return next
|
||||
}
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
u, p, ok := r.BasicAuth()
|
||||
if !ok || subtle.ConstantTimeCompare([]byte(u), []byte(d.username)) != 1 || subtle.ConstantTimeCompare([]byte(p), []byte(d.password)) != 1 {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="kaya"`)
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func (d *dashboardServer) handleIndex(w http.ResponseWriter, _ *http.Request) {
|
||||
bs, err := dashboardHTML.ReadFile("dashboard.html")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
_, _ = w.Write(bs)
|
||||
}
|
||||
|
||||
func (d *dashboardServer) handleStatus(w http.ResponseWriter, _ *http.Request) {
|
||||
status := dashboardStatus{Now: time.Now()}
|
||||
self := d.node.core.GetSelf()
|
||||
subnet := d.node.core.Subnet()
|
||||
status.Self = map[string]any{
|
||||
"public_key": hex.EncodeToString(self.Key),
|
||||
"ip_address": d.node.core.Address().String(),
|
||||
"subnet": (&subnet).String(),
|
||||
"routing_entries": self.RoutingEntries,
|
||||
}
|
||||
|
||||
for _, p := range d.node.core.GetPeers() {
|
||||
uriKey := canonicalPeerKey(p.URI, "")
|
||||
status.Peers = append(status.Peers, dashboardPeer{
|
||||
URI: p.URI,
|
||||
Remote: peerHostFromURI(p.URI),
|
||||
Up: p.Up,
|
||||
Inbound: p.Inbound,
|
||||
IP: keyToIP(p.Key),
|
||||
Uptime: p.Uptime.String(),
|
||||
RTT: p.Latency.String(),
|
||||
RX: p.RXBytes,
|
||||
TX: p.TXBytes,
|
||||
RXRate: p.RXRate,
|
||||
TXRate: p.TXRate,
|
||||
Priority: p.Priority,
|
||||
Cost: p.Cost,
|
||||
Banned: d.isBanned(uriKey),
|
||||
})
|
||||
}
|
||||
sort.Slice(status.Peers, func(i, j int) bool { return status.Peers[i].URI < status.Peers[j].URI })
|
||||
for _, s := range d.node.core.GetSessions() {
|
||||
status.Sessions = append(status.Sessions, dashboardFlow{
|
||||
IP: keyToIP(s.Key),
|
||||
RX: s.RXBytes,
|
||||
TX: s.TXBytes,
|
||||
Uptime: s.Uptime.String(),
|
||||
})
|
||||
}
|
||||
for _, p := range d.node.core.GetPaths() {
|
||||
status.Paths = append(status.Paths, dashboardPath{IP: keyToIP(p.Key), Path: p.Path})
|
||||
}
|
||||
for _, t := range d.node.core.GetTree() {
|
||||
status.Tree = append(status.Tree, dashboardTree{IP: keyToIP(t.Key), Parent: keyToIP(t.Parent)})
|
||||
}
|
||||
status.Banned = d.bannedList()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(status)
|
||||
}
|
||||
|
||||
func (d *dashboardServer) handlePeerTraffic(w http.ResponseWriter, r *http.Request) {
|
||||
if d.readOnly {
|
||||
http.Error(w, "public dashboard is read-only", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
URI string `json:"uri"`
|
||||
Interface string `json:"interface"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
uri, err := url.Parse(req.URI)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
key := canonicalPeerKey(req.URI, req.Interface)
|
||||
if req.Enabled {
|
||||
if d.isBanned(key) {
|
||||
http.Error(w, "peer is banned", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
err = d.node.core.AddPeer(uri, req.Interface)
|
||||
} else {
|
||||
err = d.node.core.RemovePeer(uri, req.Interface)
|
||||
}
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (d *dashboardServer) handlePeerBan(w http.ResponseWriter, r *http.Request) {
|
||||
if d.readOnly {
|
||||
http.Error(w, "public dashboard is read-only", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
URI string `json:"uri"`
|
||||
Interface string `json:"interface"`
|
||||
Banned bool `json:"banned"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
uri, err := url.Parse(req.URI)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
key := canonicalPeerKey(req.URI, req.Interface)
|
||||
if req.Banned {
|
||||
d.mu.Lock()
|
||||
d.banned[key] = struct{}{}
|
||||
d.mu.Unlock()
|
||||
_ = d.node.core.RemovePeer(uri, req.Interface)
|
||||
} else {
|
||||
d.mu.Lock()
|
||||
delete(d.banned, key)
|
||||
d.mu.Unlock()
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (d *dashboardServer) isBanned(k string) bool {
|
||||
d.mu.RLock()
|
||||
_, ok := d.banned[k]
|
||||
d.mu.RUnlock()
|
||||
return ok
|
||||
}
|
||||
|
||||
func (d *dashboardServer) bannedList() []string {
|
||||
d.mu.RLock()
|
||||
out := make([]string, 0, len(d.banned))
|
||||
for k := range d.banned {
|
||||
out = append(out, k)
|
||||
}
|
||||
d.mu.RUnlock()
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func canonicalPeerKey(uri, intf string) string { return uri + "|" + intf }
|
||||
|
||||
func peerHostFromURI(u string) string {
|
||||
pu, err := url.Parse(u)
|
||||
if err != nil {
|
||||
return "-"
|
||||
}
|
||||
host := pu.Host
|
||||
if h, _, err := net.SplitHostPort(host); err == nil {
|
||||
return h
|
||||
}
|
||||
if host == "" {
|
||||
return "-"
|
||||
}
|
||||
return host
|
||||
}
|
||||
|
||||
func keyToIP(k []byte) string {
|
||||
if len(k) == 0 {
|
||||
return "-"
|
||||
}
|
||||
var key [32]byte
|
||||
copy(key[:], k)
|
||||
ip := net.IP(address.AddrForKey(key[:])[:])
|
||||
if ip == nil {
|
||||
return "-"
|
||||
}
|
||||
return ip.String()
|
||||
}
|
||||
251
cmd/yggdrasil/dashboard.html
Normal file
251
cmd/yggdrasil/dashboard.html
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Kaya Dashboard</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg-color: #f4f4f4;
|
||||
--card-bg: #ffffff;
|
||||
--text-main: #1a1a1a;
|
||||
--text-secondary: #666666;
|
||||
--accent-orange: #ff6600;
|
||||
--network-green: #00c853;
|
||||
--border-color: #e0e0e0;
|
||||
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
--font-mono: 'SF Mono', 'Menlo', 'Monaco', 'Courier New', monospace;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
body { font-family: var(--font-sans); background: var(--bg-color); color: var(--text-main); margin: 0; padding: 28px; line-height: 1.4; }
|
||||
header { display: flex; justify-content: space-between; align-items: center; gap: 8px; padding-bottom: 16px; margin-bottom: 22px; border-bottom: 6px solid var(--accent-orange); }
|
||||
header h2 { margin: 0; font-size: 30px; font-weight: 800; letter-spacing: -1px; text-transform: uppercase; }
|
||||
#stamp { font-family: var(--font-mono); font-size: 12px; color: var(--text-secondary); background: #fff; padding: 6px 12px; border: 1px solid var(--border-color); border-radius: 2px; }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(380px, 1fr)); gap: 18px; }
|
||||
.card { background: var(--card-bg); border: 1px solid var(--border-color); border-radius: 4px; padding: 18px; box-shadow: 0 4px 6px rgba(0,0,0,0.02); min-width: 0; }
|
||||
.card h3 { margin: 0 0 12px 0; font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: 1.2px; color: var(--accent-orange); border-bottom: 1px solid var(--border-color); padding-bottom: 8px; }
|
||||
.table-wrap { overflow-x: auto; width: 100%; }
|
||||
table { width: 100%; border-collapse: collapse; font-size: 12px; table-layout: fixed; }
|
||||
th { font-weight: 600; color: var(--text-secondary); padding: 10px 6px; border-bottom: 2px solid var(--border-color); font-size: 11px; text-transform: uppercase; white-space: nowrap; }
|
||||
td { padding: 8px 6px; border-bottom: 1px solid #eee; font-family: var(--font-mono); color: var(--text-main); vertical-align: middle; overflow: hidden; text-overflow: ellipsis; }
|
||||
tr:last-child td { border-bottom: none; }
|
||||
button { background: #fff; color: #d32f2f; border: 1px solid #d32f2f; border-radius: 3px; padding: 6px 10px; cursor: pointer; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.4px; white-space: nowrap; }
|
||||
button:hover { background: #d32f2f; color: #fff; }
|
||||
pre { white-space: pre-wrap; margin: 0; font-family: var(--font-mono); font-size: 11px; background: #fafafa; padding: 12px; border: 1px solid var(--border-color); color: #555; max-height: 340px; overflow: auto; }
|
||||
.net-green { color: var(--network-green); font-weight: bold; }
|
||||
.full-width { grid-column: 1 / -1; }
|
||||
.ip-cell { word-break: break-word; white-space: normal; }
|
||||
.uri-cell { max-width: 260px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.port-icon-wrapper { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; }
|
||||
svg.port-icon { width: 24px; height: 24px; }
|
||||
.port-body { fill: none; stroke: #ddd; stroke-width: 2; }
|
||||
.port-led { fill: #ddd; transition: all 0.2s; }
|
||||
.port-icon.connected .port-body { stroke: var(--accent-orange); }
|
||||
.port-icon.connected .port-led { fill: var(--accent-orange); }
|
||||
.port-icon.active .port-body { stroke: var(--network-green); }
|
||||
.port-icon.active .port-led { fill: var(--network-green); animation: blink-traffic 1s steps(2) infinite; }
|
||||
@keyframes blink-traffic { 0% { opacity: 1; } 50% { opacity: 0; } 100% { opacity: 1; } }
|
||||
.graph-container { width: 100%; height: 220px; background: #fafafa; border: 1px solid var(--border-color); border-radius: 4px; overflow: hidden; }
|
||||
canvas#trafficGraph { width: 100%; height: 100%; display: block; }
|
||||
.graph-stats { display: flex; justify-content: space-between; gap: 8px; margin-bottom: 10px; font-family: var(--font-mono); font-size: 14px; flex-wrap: wrap; }
|
||||
.stat-item { display: flex; align-items: center; gap: 8px; }
|
||||
.stat-val { font-weight: bold; font-size: 16px; }
|
||||
.legend-dot { width: 10px; height: 10px; border-radius: 50%; }
|
||||
@media (max-width: 900px) {
|
||||
body { padding: 12px; }
|
||||
.grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h2>Kaya</h2>
|
||||
<div id="stamp"></div>
|
||||
</header>
|
||||
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<h3>Self Info</h3>
|
||||
<pre id="self"></pre>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Flows (Sessions)</h3>
|
||||
<div class="table-wrap"><table id="flows"></table></div>
|
||||
</div>
|
||||
|
||||
<div class="card full-width">
|
||||
<h3>Peers Switch</h3>
|
||||
<div class="table-wrap"><table id="peers"></table></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Paths</h3>
|
||||
<div class="table-wrap"><table id="paths"></table></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Tree</h3>
|
||||
<div class="table-wrap"><table id="tree"></table></div>
|
||||
</div>
|
||||
|
||||
<div class="card full-width">
|
||||
<h3>Total Bandwidth (Real-time)</h3>
|
||||
<div class="graph-stats">
|
||||
<div class="stat-item">
|
||||
<div class="legend-dot" style="background: var(--network-green);"></div>
|
||||
<div>⬇ Download: <span id="downSpeedEl" class="stat-val net-green">0.00 Mbit/s</span></div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="legend-dot" style="background: var(--accent-orange);"></div>
|
||||
<div>⬆ Upload: <span id="upSpeedEl" class="stat-val" style="color: var(--accent-orange)">0.00 Mbit/s</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="graph-container"><canvas id="trafficGraph"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const peerStore = new Map();
|
||||
const graphData = { maxPoints: 60, download: [], upload: [] };
|
||||
|
||||
async function call(path, payload){
|
||||
const r = await fetch(path,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
|
||||
if(!r.ok) alert(await r.text());
|
||||
await reload();
|
||||
}
|
||||
function tHead(cols){return `<tr>${cols.map(c=>`<th>${c}</th>`).join('')}</tr>`}
|
||||
function esc(v){return String(v??'').replace(/[&<>]/g,m=>({'&':'&','<':'<','>':'>'}[m]))}
|
||||
function peerAction(id, action){
|
||||
const p = peerStore.get(id); if(!p) return;
|
||||
if(action==='disconnect') return call('/api/peer/traffic',{uri:p.uri,enabled:false});
|
||||
}
|
||||
function bytesPerSecToMbit(bytesPerSec) {
|
||||
const mbits = (Number(bytesPerSec) * 8) / 1000000;
|
||||
return Number.isFinite(mbits) ? mbits : 0;
|
||||
}
|
||||
function fmtMbit(bytesPerSec) {
|
||||
return bytesPerSecToMbit(bytesPerSec).toFixed(2) + ' Mbit/s';
|
||||
}
|
||||
|
||||
function drawGraph() {
|
||||
const canvas = document.getElementById('trafficGraph');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const rect = canvas.parentNode.getBoundingClientRect();
|
||||
canvas.width = rect.width;
|
||||
canvas.height = rect.height;
|
||||
const w = canvas.width, h = canvas.height;
|
||||
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
ctx.strokeStyle = '#e0e0e0';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
for(let i=0; i<5; i++) {
|
||||
const y = (h/4) * i;
|
||||
ctx.moveTo(0, y);
|
||||
ctx.lineTo(w, y);
|
||||
}
|
||||
ctx.stroke();
|
||||
|
||||
if (graphData.download.length < 2) return;
|
||||
|
||||
const maxVal = Math.max(...graphData.download, ...graphData.upload) * 1.1 || 1;
|
||||
const drawLine = (data, color) => {
|
||||
ctx.strokeStyle = color.trim();
|
||||
ctx.lineWidth = 2;
|
||||
ctx.lineJoin = 'round';
|
||||
ctx.beginPath();
|
||||
const step = w / (graphData.maxPoints - 1);
|
||||
data.forEach((val, index) => {
|
||||
const x = index * step;
|
||||
const y = h - ((val / maxVal) * h);
|
||||
if (index === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
|
||||
});
|
||||
ctx.stroke();
|
||||
ctx.lineTo(w, h);
|
||||
ctx.lineTo(0, h);
|
||||
ctx.fillStyle = color.trim() + '22';
|
||||
ctx.fill();
|
||||
};
|
||||
|
||||
drawLine(graphData.upload, getComputedStyle(document.documentElement).getPropertyValue('--accent-orange'));
|
||||
drawLine(graphData.download, getComputedStyle(document.documentElement).getPropertyValue('--network-green'));
|
||||
}
|
||||
|
||||
async function reload(){
|
||||
const res = await fetch('/api/status');
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to load dashboard status: HTTP ' + res.status);
|
||||
}
|
||||
const s = await res.json();
|
||||
|
||||
document.getElementById('stamp').textContent = 'Updated ' + new Date(s.now).toLocaleString();
|
||||
document.getElementById('self').textContent = JSON.stringify(s.self,null,2);
|
||||
|
||||
let totalRx = 0, totalTx = 0;
|
||||
const peers = [tHead(['Port','URI','Remote','Up','IP','Uptime','RTT','RX','TX','⬇ RX Rate','⬆ TX Rate','Action'])];
|
||||
peerStore.clear();
|
||||
|
||||
for(const p of s.peers){
|
||||
const id = btoa(p.uri).replace(/=/g,'');
|
||||
peerStore.set(id, p);
|
||||
totalRx += Number(p.rx_rate || 0);
|
||||
totalTx += Number(p.tx_rate || 0);
|
||||
|
||||
const isActive = (p.rx_rate > 0 || p.tx_rate > 0);
|
||||
const isConnected = !!p.up;
|
||||
const portStateClass = isActive ? 'active' : (isConnected ? 'connected' : '');
|
||||
const portIconHtml = `<div class="port-icon-wrapper"><svg class="port-icon ${portStateClass}" viewBox="0 0 24 24"><rect class="port-body" x="4" y="6" width="16" height="12" rx="2" /><path class="port-body" d="M8 10h8v4H8z" /><circle class="port-led" cx="17" cy="9" r="1.5" /></svg></div>`;
|
||||
|
||||
peers.push(`<tr>
|
||||
<td>${portIconHtml}</td>
|
||||
<td class="uri-cell" title="${esc(p.uri)}">${esc(p.uri)}</td>
|
||||
<td>${esc(p.remote)}</td>
|
||||
<td style="font-weight:bold; color:${p.up?'var(--accent-orange)':'#aaa'}">${p.up?'UP':'DOWN'}</td>
|
||||
<td class="ip-cell">${esc(p.ip)}</td>
|
||||
<td>${esc(p.uptime)}</td>
|
||||
<td>${esc(p.rtt)}</td>
|
||||
<td>${esc(p.rx)}</td>
|
||||
<td>${esc(p.tx)}</td>
|
||||
<td class="net-green">⬇ ${fmtMbit(p.rx_rate)}</td>
|
||||
<td style="color:var(--accent-orange)">⬆ ${fmtMbit(p.tx_rate)}</td>
|
||||
<td><button onclick="peerAction('${id}','disconnect')">Disconnect</button></td>
|
||||
</tr>`);
|
||||
}
|
||||
document.getElementById('peers').innerHTML = peers.join('');
|
||||
|
||||
const flows=[tHead(['IP','RX','TX','Uptime'])];
|
||||
for(const f of s.sessions) {
|
||||
flows.push(`<tr><td class="ip-cell">${esc(f.ip)}</td><td>${esc(f.rx)}</td><td>${esc(f.tx)}</td><td>${esc(f.uptime)}</td></tr>`);
|
||||
}
|
||||
document.getElementById('flows').innerHTML=flows.join('');
|
||||
|
||||
const paths=[tHead(['IP','Path'])];
|
||||
for(const p of s.paths) paths.push(`<tr><td class="ip-cell">${esc(p.ip)}</td><td class="ip-cell">${esc(JSON.stringify(p.path))}</td></tr>`);
|
||||
document.getElementById('paths').innerHTML=paths.join('');
|
||||
|
||||
const tree=[tHead(['IP','Parent'])];
|
||||
for(const t of s.tree) tree.push(`<tr><td class="ip-cell">${esc(t.ip)}</td><td class="ip-cell">${esc(t.parent)}</td></tr>`);
|
||||
document.getElementById('tree').innerHTML=tree.join('');
|
||||
|
||||
const downMbit = bytesPerSecToMbit(totalRx);
|
||||
const upMbit = bytesPerSecToMbit(totalTx);
|
||||
graphData.download.push(downMbit);
|
||||
graphData.upload.push(upMbit);
|
||||
if (graphData.download.length > graphData.maxPoints) {
|
||||
graphData.download.shift();
|
||||
graphData.upload.shift();
|
||||
}
|
||||
|
||||
downSpeedEl.textContent = downMbit.toFixed(2) + ' Mbit/s';
|
||||
upSpeedEl.textContent = upMbit.toFixed(2) + ' Mbit/s';
|
||||
drawGraph();
|
||||
}
|
||||
|
||||
setInterval(() => { reload().catch(console.error); }, 2000);
|
||||
reload().catch(console.error);
|
||||
window.addEventListener('resize', drawGraph);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
67
cmd/yggdrasil/logcolor.go
Normal file
67
cmd/yggdrasil/logcolor.go
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
colorReset = "\033[0m"
|
||||
colorDim = "\033[2m"
|
||||
colorCyan = "\033[36m"
|
||||
colorBlue = "\033[94m"
|
||||
colorGreen = "\033[32m"
|
||||
colorYellow = "\033[33m"
|
||||
colorRed = "\033[31m"
|
||||
colorMagenta = "\033[35m"
|
||||
)
|
||||
|
||||
type colorLineWriter struct {
|
||||
w io.Writer
|
||||
buf []byte
|
||||
}
|
||||
|
||||
func newColorLineWriter(w io.Writer) *colorLineWriter {
|
||||
return &colorLineWriter{w: w, buf: make([]byte, 0, 1024)}
|
||||
}
|
||||
|
||||
func (c *colorLineWriter) Write(p []byte) (int, error) {
|
||||
c.buf = append(c.buf, p...)
|
||||
for {
|
||||
i := bytes.IndexByte(c.buf, '\n')
|
||||
if i < 0 {
|
||||
break
|
||||
}
|
||||
line := c.buf[:i]
|
||||
if _, err := c.w.Write([]byte(colorizeLine(string(line)) + "\n")); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
c.buf = c.buf[i+1:]
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func colorizeLine(line string) string {
|
||||
lower := strings.ToLower(line)
|
||||
lineColor := colorCyan
|
||||
switch {
|
||||
case strings.Contains(lower, "error") || strings.Contains(lower, "failed") || strings.Contains(lower, "disconnected"):
|
||||
lineColor = colorRed
|
||||
case strings.Contains(lower, "warn"):
|
||||
lineColor = colorYellow
|
||||
case strings.Contains(lower, "connected") || strings.Contains(lower, "listening") || strings.Contains(lower, "started"):
|
||||
lineColor = colorGreen
|
||||
case strings.Contains(lower, "public key") || strings.Contains(lower, "ipv6") || strings.Contains(lower, "interface"):
|
||||
lineColor = colorBlue
|
||||
case strings.Contains(lower, "sandbox"):
|
||||
lineColor = colorMagenta
|
||||
}
|
||||
|
||||
if len(line) > 20 && line[4] == '/' && line[7] == '/' && line[10] == ' ' && line[13] == ':' && line[16] == ':' {
|
||||
timestamp := line[:19]
|
||||
rest := strings.TrimLeft(line[19:], " ")
|
||||
return colorDim + timestamp + colorReset + " " + lineColor + rest + colorReset
|
||||
}
|
||||
return lineColor + line + colorReset
|
||||
}
|
||||
406
cmd/yggdrasil/main.go
Normal file
406
cmd/yggdrasil/main.go
Normal file
|
|
@ -0,0 +1,406 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"suah.dev/protect"
|
||||
|
||||
"github.com/gologme/log"
|
||||
gsyslog "github.com/hashicorp/go-syslog"
|
||||
"github.com/hjson/hjson-go/v4"
|
||||
"github.com/kardianos/minwinsvc"
|
||||
|
||||
"github.com/yggdrasil-network/yggdrasil-go/src/address"
|
||||
"github.com/yggdrasil-network/yggdrasil-go/src/admin"
|
||||
"github.com/yggdrasil-network/yggdrasil-go/src/config"
|
||||
"github.com/yggdrasil-network/yggdrasil-go/src/ipv6rwc"
|
||||
|
||||
"github.com/yggdrasil-network/yggdrasil-go/src/core"
|
||||
"github.com/yggdrasil-network/yggdrasil-go/src/multicast"
|
||||
"github.com/yggdrasil-network/yggdrasil-go/src/tun"
|
||||
"github.com/yggdrasil-network/yggdrasil-go/src/version"
|
||||
)
|
||||
|
||||
type node struct {
|
||||
core *core.Core
|
||||
tun *tun.TunAdapter
|
||||
multicast *multicast.Multicast
|
||||
admin *admin.AdminSocket
|
||||
}
|
||||
|
||||
// The main function is responsible for configuring and starting Kaya.
|
||||
func main() {
|
||||
defaultThreads := runtime.NumCPU()
|
||||
if defaultThreads < 1 {
|
||||
defaultThreads = 1
|
||||
}
|
||||
genconf := flag.Bool("genconf", false, "print a new config to stdout")
|
||||
useconf := flag.Bool("useconf", false, "read HJSON/JSON config from stdin")
|
||||
useconffile := flag.String("useconffile", "", "read HJSON/JSON config from specified file path")
|
||||
normaliseconf := flag.Bool("normaliseconf", false, "use in combination with either -useconf or -useconffile, outputs your configuration normalised")
|
||||
exportkey := flag.Bool("exportkey", false, "use in combination with either -useconf or -useconffile, outputs your private key in PEM format")
|
||||
confjson := flag.Bool("json", false, "print configuration from -genconf or -normaliseconf as JSON instead of HJSON")
|
||||
autoconf := flag.Bool("autoconf", false, "automatic mode (dynamic IP, peer with IPv6 neighbors)")
|
||||
ver := flag.Bool("version", false, "prints the version of this build")
|
||||
logto := flag.String("logto", "stdout", "file path to log to, \"syslog\" or \"stdout\"")
|
||||
getaddr := flag.Bool("address", false, "use in combination with either -useconf or -useconffile, outputs your IPv6 address")
|
||||
getsnet := flag.Bool("subnet", false, "use in combination with either -useconf or -useconffile, outputs your IPv6 subnet")
|
||||
getpkey := flag.Bool("publickey", false, "use in combination with either -useconf or -useconffile, outputs your public key")
|
||||
loglevel := flag.String("loglevel", "info", "loglevel to enable")
|
||||
threads := flag.Int("threads", defaultThreads, "number of scheduler threads to run (defaults to one per CPU core)")
|
||||
maxThreads := flag.Int("max-threads", 0, "maximum total OS threads for the Go runtime (0 keeps runtime default)")
|
||||
sandbox := flag.Bool("sandbox", true, "enable runtime sandbox hardening where supported")
|
||||
dashboardListen := flag.String("dashboard-listen", "", "HTTP dashboard listen address, e.g. 127.0.0.1:8080")
|
||||
publicInterface := flag.String("public-interface", "", "public read-only dashboard listen address, e.g. 0.0.0.0:8081")
|
||||
dashboardUser := flag.String("dashboard-user", "", "HTTP dashboard username for basic auth")
|
||||
dashboardPassword := flag.String("dashboard-password", "", "HTTP dashboard password for basic auth")
|
||||
chuserto := flag.String("user", "", "user (and, optionally, group) to set UID/GID to")
|
||||
flag.Parse()
|
||||
if *threads < 1 {
|
||||
panic("--threads must be >= 1")
|
||||
}
|
||||
if *maxThreads < 0 {
|
||||
panic("--max-threads must be >= 0")
|
||||
}
|
||||
runtime.GOMAXPROCS(*threads)
|
||||
if *maxThreads > 0 {
|
||||
debug.SetMaxThreads(*maxThreads)
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
defer close(done)
|
||||
|
||||
// Catch interrupts from the operating system to exit gracefully.
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
// Create a new logger that logs output to stdout.
|
||||
var logger *log.Logger
|
||||
switch *logto {
|
||||
case "stdout":
|
||||
var outWriter io.Writer = os.Stdout
|
||||
if info, err := os.Stdout.Stat(); err == nil && (info.Mode()&os.ModeCharDevice) != 0 {
|
||||
outWriter = newColorLineWriter(os.Stdout)
|
||||
}
|
||||
logger = log.New(outWriter, "", log.Flags())
|
||||
|
||||
case "syslog":
|
||||
if syslogger, err := gsyslog.NewLogger(gsyslog.LOG_NOTICE, "DAEMON", version.BuildName()); err == nil {
|
||||
logger = log.New(syslogger, "", log.Flags()&^(log.Ldate|log.Ltime))
|
||||
}
|
||||
|
||||
default:
|
||||
if logfd, err := os.OpenFile(*logto, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); err == nil {
|
||||
logger = log.New(logfd, "", log.Flags())
|
||||
}
|
||||
}
|
||||
if logger == nil {
|
||||
logger = log.New(os.Stdout, "", log.Flags())
|
||||
logger.Warnln("Logging defaulting to stdout")
|
||||
}
|
||||
if *normaliseconf {
|
||||
setLogLevel("error", logger)
|
||||
} else {
|
||||
setLogLevel(*loglevel, logger)
|
||||
}
|
||||
cfg := config.GenerateConfig()
|
||||
var err error
|
||||
switch {
|
||||
case *ver:
|
||||
fmt.Println("Build name:", version.BuildName())
|
||||
fmt.Println("Build version:", version.BuildVersion())
|
||||
return
|
||||
|
||||
case *autoconf:
|
||||
// Use an autoconf-generated config, this will give us random keys and
|
||||
// port numbers, and will use an automatically selected TUN interface.
|
||||
|
||||
case *useconf:
|
||||
if _, err := cfg.ReadFrom(os.Stdin); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
case *useconffile != "":
|
||||
f, err := os.Open(*useconffile)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if _, err := cfg.ReadFrom(f); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
_ = f.Close()
|
||||
|
||||
case *genconf:
|
||||
cfg.AdminListen = ""
|
||||
var bs []byte
|
||||
if *confjson {
|
||||
bs, err = json.MarshalIndent(cfg, "", " ")
|
||||
} else {
|
||||
bs, err = hjson.Marshal(cfg)
|
||||
}
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println(string(bs))
|
||||
return
|
||||
|
||||
default:
|
||||
fmt.Println("Usage:")
|
||||
flag.PrintDefaults()
|
||||
|
||||
if *getaddr || *getsnet {
|
||||
fmt.Println("\nError: You need to specify some config data using -useconf or -useconffile.")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
privateKey := ed25519.PrivateKey(cfg.PrivateKey)
|
||||
publicKey := privateKey.Public().(ed25519.PublicKey)
|
||||
|
||||
switch {
|
||||
case *getaddr:
|
||||
addr := address.AddrForKey(publicKey)
|
||||
ip := net.IP(addr[:])
|
||||
fmt.Println(ip.String())
|
||||
return
|
||||
|
||||
case *getsnet:
|
||||
snet := address.SubnetForKey(publicKey)
|
||||
ipnet := net.IPNet{
|
||||
IP: append(snet[:], 0, 0, 0, 0, 0, 0, 0, 0),
|
||||
Mask: net.CIDRMask(len(snet)*8, 128),
|
||||
}
|
||||
fmt.Println(ipnet.String())
|
||||
return
|
||||
|
||||
case *getpkey:
|
||||
fmt.Println(hex.EncodeToString(publicKey))
|
||||
return
|
||||
|
||||
case *normaliseconf:
|
||||
cfg.AdminListen = ""
|
||||
if cfg.PrivateKeyPath != "" {
|
||||
cfg.PrivateKey = nil
|
||||
}
|
||||
var bs []byte
|
||||
if *confjson {
|
||||
bs, err = json.MarshalIndent(cfg, "", " ")
|
||||
} else {
|
||||
bs, err = hjson.Marshal(cfg)
|
||||
}
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println(string(bs))
|
||||
return
|
||||
|
||||
case *exportkey:
|
||||
pem, err := cfg.MarshalPEMPrivateKey()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println(string(pem))
|
||||
return
|
||||
}
|
||||
|
||||
if *maxThreads > 0 {
|
||||
logger.Infof("Runtime threading: GOMAXPROCS=%d, max OS threads=%d", *threads, *maxThreads)
|
||||
} else {
|
||||
logger.Infof("Runtime threading: GOMAXPROCS=%d", *threads)
|
||||
}
|
||||
|
||||
if err := applySandbox(*sandbox, logger); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
n := &node{}
|
||||
|
||||
// Set up the Kaya node itself.
|
||||
{
|
||||
iprange := net.IPNet{
|
||||
IP: net.ParseIP("200::"),
|
||||
Mask: net.CIDRMask(7, 128),
|
||||
}
|
||||
options := []core.SetupOption{
|
||||
core.NodeInfo(cfg.NodeInfo),
|
||||
core.NodeInfoPrivacy(cfg.NodeInfoPrivacy),
|
||||
core.PeerFilter(func(ip net.IP) bool {
|
||||
return !iprange.Contains(ip)
|
||||
}),
|
||||
}
|
||||
for _, addr := range cfg.Listen {
|
||||
options = append(options, core.ListenAddress(addr))
|
||||
}
|
||||
for _, peer := range cfg.Peers {
|
||||
options = append(options, core.Peer{URI: peer})
|
||||
}
|
||||
for intf, peers := range cfg.InterfacePeers {
|
||||
for _, peer := range peers {
|
||||
options = append(options, core.Peer{URI: peer, SourceInterface: intf})
|
||||
}
|
||||
}
|
||||
for _, allowed := range cfg.AllowedPublicKeys {
|
||||
k, err := hex.DecodeString(allowed)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
options = append(options, core.AllowedPublicKey(k[:]))
|
||||
}
|
||||
if n.core, err = core.New(cfg.Certificate, logger, options...); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
address, subnet := n.core.Address(), n.core.Subnet()
|
||||
logger.Printf("Your public key is %s", hex.EncodeToString(n.core.PublicKey()))
|
||||
logger.Printf("Your IPv6 address is %s", address.String())
|
||||
logger.Printf("Your IPv6 subnet is %s", subnet.String())
|
||||
}
|
||||
|
||||
// Set up the admin socket.
|
||||
{
|
||||
options := []admin.SetupOption{
|
||||
admin.ListenAddress(cfg.AdminListen),
|
||||
}
|
||||
if cfg.LogLookups {
|
||||
options = append(options, admin.LogLookups{})
|
||||
}
|
||||
if n.admin, err = admin.New(n.core, logger, options...); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if n.admin != nil {
|
||||
n.admin.SetupAdminHandlers()
|
||||
}
|
||||
}
|
||||
|
||||
// Set up the multicast module.
|
||||
{
|
||||
options := []multicast.SetupOption{}
|
||||
for _, intf := range cfg.MulticastInterfaces {
|
||||
options = append(options, multicast.MulticastInterface{
|
||||
Regex: regexp.MustCompile(intf.Regex),
|
||||
Beacon: intf.Beacon,
|
||||
Listen: intf.Listen,
|
||||
Port: intf.Port,
|
||||
Priority: uint8(intf.Priority),
|
||||
Password: intf.Password,
|
||||
})
|
||||
}
|
||||
if n.multicast, err = multicast.New(n.core, logger, options...); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if n.admin != nil && n.multicast != nil {
|
||||
n.multicast.SetupAdminHandlers(n.admin)
|
||||
}
|
||||
}
|
||||
|
||||
// Set up the TUN module.
|
||||
{
|
||||
options := []tun.SetupOption{
|
||||
tun.InterfaceName(cfg.IfName),
|
||||
tun.InterfaceMTU(cfg.IfMTU),
|
||||
}
|
||||
if n.tun, err = tun.New(ipv6rwc.NewReadWriteCloser(n.core), logger, options...); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if n.admin != nil && n.tun != nil {
|
||||
n.tun.SetupAdminHandlers(n.admin)
|
||||
}
|
||||
}
|
||||
|
||||
dashboards := make([]*dashboardServer, 0, 2)
|
||||
if dashboard, err := startDashboard(n, logger, *dashboardListen, *dashboardUser, *dashboardPassword, false); err != nil {
|
||||
panic(err)
|
||||
} else if dashboard != nil {
|
||||
dashboards = append(dashboards, dashboard)
|
||||
}
|
||||
if publicDashboard, err := startDashboard(n, logger, *publicInterface, "", "", true); err != nil {
|
||||
panic(err)
|
||||
} else if publicDashboard != nil {
|
||||
dashboards = append(dashboards, publicDashboard)
|
||||
}
|
||||
defer func() {
|
||||
for _, dashboard := range dashboards {
|
||||
if dashboard != nil {
|
||||
dashboard.stop()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
//Windows service shutdown
|
||||
minwinsvc.SetOnExit(func() {
|
||||
logger.Infof("Shutting down service ...")
|
||||
cancel()
|
||||
// Wait for all parts to shutdown properly
|
||||
<-done
|
||||
})
|
||||
|
||||
// Change user if requested
|
||||
if *chuserto != "" {
|
||||
err = chuser(*chuserto)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Promise final modes of operation. At this point, if at all:
|
||||
// - raw socket is created/open
|
||||
// - admin socket is created/open
|
||||
// - privileges are dropped to non-root user
|
||||
//
|
||||
// Peers, InterfacePeers, Listen can be UNIX sockets;
|
||||
// Go's net.Listen.Close() deletes files on shutdown.
|
||||
promises := []string{"stdio", "rpath", "cpath", "inet", "unix", "dns"}
|
||||
if len(cfg.MulticastInterfaces) > 0 {
|
||||
promises = append(promises, "mcast")
|
||||
}
|
||||
if err := protect.Pledge(strings.Join(promises, " ")); err != nil {
|
||||
panic(fmt.Sprintf("pledge: %v: %v", promises, err))
|
||||
}
|
||||
|
||||
// Block until we are told to shut down.
|
||||
<-ctx.Done()
|
||||
|
||||
// Shut down the node.
|
||||
_ = n.admin.Stop()
|
||||
_ = n.multicast.Stop()
|
||||
_ = n.tun.Stop()
|
||||
n.core.Stop()
|
||||
}
|
||||
|
||||
func setLogLevel(loglevel string, logger *log.Logger) {
|
||||
levels := [...]string{"error", "warn", "info", "debug", "trace"}
|
||||
loglevel = strings.ToLower(loglevel)
|
||||
|
||||
contains := func() bool {
|
||||
for _, l := range levels {
|
||||
if l == loglevel {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if !contains() { // set default log level
|
||||
logger.Infoln("Loglevel parse failed. Set default level(info)")
|
||||
loglevel = "info"
|
||||
}
|
||||
|
||||
for _, l := range levels {
|
||||
logger.EnableLevel(l)
|
||||
if l == loglevel {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
27
cmd/yggdrasil/sandbox_linux.go
Normal file
27
cmd/yggdrasil/sandbox_linux.go
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
//go:build linux
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gologme/log"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func applySandbox(enabled bool, logger *log.Logger) error {
|
||||
if !enabled {
|
||||
return nil
|
||||
}
|
||||
if err := unix.Prctl(unix.PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); err != nil {
|
||||
return fmt.Errorf("failed to enable no_new_privs: %w", err)
|
||||
}
|
||||
if err := unix.Prctl(unix.PR_SET_DUMPABLE, 0, 0, 0, 0); err != nil {
|
||||
return fmt.Errorf("failed to disable dumpable state: %w", err)
|
||||
}
|
||||
if err := unix.Setrlimit(unix.RLIMIT_CORE, &unix.Rlimit{Cur: 0, Max: 0}); err != nil {
|
||||
return fmt.Errorf("failed to disable core dumps: %w", err)
|
||||
}
|
||||
logger.Infoln("Linux sandbox hardening enabled: no_new_privs, non-dumpable, core dumps disabled")
|
||||
return nil
|
||||
}
|
||||
9
cmd/yggdrasil/sandbox_other.go
Normal file
9
cmd/yggdrasil/sandbox_other.go
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
//go:build !linux
|
||||
|
||||
package main
|
||||
|
||||
import "github.com/gologme/log"
|
||||
|
||||
func applySandbox(_ bool, _ *log.Logger) error {
|
||||
return nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue