Files
RemLink/internal/admin/network.go
T
qsc20001102 142e5dc7d6
ci / Go checks (ubuntu-latest) (push) Has been cancelled
ci / Go checks (windows-latest) (push) Has been cancelled
初版功能完成
2026-08-29 13:12:17 +08:00

302 lines
10 KiB
Go

// Package admin implements the v1 Server Admin API and network migration.
package admin
import (
"context"
"encoding/json"
"errors"
"fmt"
"net"
"net/netip"
"net/url"
"strconv"
"sync"
"time"
"remlink/internal/bootstrap"
"remlink/internal/database"
"remlink/internal/ipam"
"remlink/internal/overlay/serverwg"
"remlink/internal/protocol"
)
const networkSettingKey = "admin.network"
type Network struct {
OverlayCIDR string `json:"overlay_cidr"`
ServerOverlayIP string `json:"server_overlay_ip"`
WireGuardPort int `json:"wireguard_port"`
SessionUDPPort int `json:"session_udp_port"`
MTU int `json:"mtu"`
ConfigVersion uint64 `json:"config_version"`
}
type NetworkUpdate struct {
OverlayCIDR string `json:"overlay_cidr"`
ServerOverlayIP string `json:"server_overlay_ip"`
WireGuardPort int `json:"wireguard_port"`
SessionUDPPort int `json:"session_udp_port"`
MTU int `json:"mtu"`
RotateJoinToken bool `json:"rotate_join_token,omitempty"`
}
type NetworkView struct {
Network
UptimeSeconds int64 `json:"uptime_seconds"`
}
type IPAM interface {
ChangeNodeAddress(context.Context, string, netip.Addr) error
ReleaseNode(context.Context, string) error
Reconfigure(netip.Prefix, netip.Addr) error
}
type PeerManager interface {
EnsurePeer(context.Context, string, netip.Addr) error
RemovePeer(context.Context, string) error
Reconfigure(context.Context, netip.Prefix, int, []serverwg.Peer) error
LastHandshake(context.Context, string) (*time.Time, error)
}
type BootstrapNetwork interface {
NetworkSnapshot() bootstrap.ServiceConfig
UpdateNetwork(bootstrap.ServiceConfig) error
}
type ControlNetwork interface {
Send(context.Context, string, protocol.ControlMessageType, any) error
SetNetworkConfigVersion(uint64) error
ResetNodeConnection(string, string)
ResetConnections(string)
}
type SessionControl interface {
Disconnect(context.Context, uint64, string) error
DisconnectAll(context.Context, string) error
DisconnectNode(context.Context, string, string) error
BeginNetworkMigration(context.Context, string) error
EndNetworkMigration()
ReconfigureNetwork(netip.Prefix, int, int) error
}
type NetworkManager struct {
mu sync.Mutex
store *database.Store
ipam IPAM
peers PeerManager
bootstrap BootstrapNetwork
control ControlNetwork
sessions SessionControl
current Network
startedAt time.Time
rebindControl func(netip.Addr) error
}
func LoadStoredNetwork(ctx context.Context, store *database.Store, fallback Network) (Network, error) {
value, found, err := database.GetSetting(ctx, store.DB(), networkSettingKey)
if err != nil || !found {
return fallback, err
}
var network Network
if err := json.Unmarshal([]byte(value), &network); err != nil {
return Network{}, fmt.Errorf("decode stored Admin network: %w", err)
}
if _, _, err := validateNetwork(network); err != nil {
return Network{}, fmt.Errorf("validate stored Admin network: %w", err)
}
return network, nil
}
func NewNetworkManager(store *database.Store, ipamManager IPAM, peers PeerManager, bootstrapService BootstrapNetwork,
control ControlNetwork, sessions SessionControl, initial Network, rebindControl func(netip.Addr) error) (*NetworkManager, error) {
if store == nil || ipamManager == nil || peers == nil || bootstrapService == nil || control == nil || sessions == nil {
return nil, errors.New("Admin NetworkManager dependencies are required")
}
if _, _, err := validateNetwork(initial); err != nil {
return nil, err
}
if rebindControl == nil {
rebindControl = func(netip.Addr) error { return nil }
}
return &NetworkManager{
store: store, ipam: ipamManager, peers: peers, bootstrap: bootstrapService,
control: control, sessions: sessions, current: initial, startedAt: time.Now(), rebindControl: rebindControl,
}, nil
}
func (m *NetworkManager) View() NetworkView {
m.mu.Lock()
defer m.mu.Unlock()
return NetworkView{Network: m.current, UptimeSeconds: int64(time.Since(m.startedAt).Seconds())}
}
func (m *NetworkManager) Current() Network {
m.mu.Lock()
defer m.mu.Unlock()
return m.current
}
func (m *NetworkManager) Update(ctx context.Context, input NetworkUpdate) (Network, error) {
m.mu.Lock()
defer m.mu.Unlock()
desired := Network{
OverlayCIDR: input.OverlayCIDR, ServerOverlayIP: input.ServerOverlayIP,
WireGuardPort: input.WireGuardPort, SessionUDPPort: input.SessionUDPPort, MTU: input.MTU,
ConfigVersion: m.current.ConfigVersion + 1,
}
prefix, serverIP, err := validateNetwork(desired)
if err != nil {
return Network{}, err
}
nodes, err := m.store.ListNodes(ctx)
if err != nil {
return Network{}, err
}
assignments, err := ipam.PlanNodeAddresses(nodes, prefix, serverIP)
if err != nil {
return Network{}, err
}
if err := m.sessions.BeginNetworkMigration(ctx, "NETWORK_CONFIG_CHANGED"); err != nil {
return Network{}, err
}
defer m.sessions.EndNetworkMigration()
old := m.current
oldPrefix, oldServerIP, _ := validateNetwork(old)
oldAssignments := make(map[string]netip.Addr, len(nodes))
oldPeers := make([]serverwg.Peer, 0, len(nodes))
newPeers := make([]serverwg.Peer, 0, len(nodes))
for _, node := range nodes {
oldAssignments[node.ID] = node.OverlayIP
oldPeers = append(oldPeers, serverwg.Peer{PublicKey: node.WGPublicKey, Address: node.OverlayIP})
newPeers = append(newPeers, serverwg.Peer{PublicKey: node.WGPublicKey, Address: assignments[node.ID]})
}
serviceConfig := m.bootstrap.NetworkSnapshot()
oldServiceConfig := serviceConfig
serviceConfig.OverlayCIDR = prefix
serviceConfig.ServerOverlayIP = serverIP
serviceConfig.WGEndpoint, err = replaceEndpointPort(serviceConfig.WGEndpoint, desired.WireGuardPort)
if err != nil {
return Network{}, err
}
serviceConfig.ControlURL, err = replaceControlHost(serviceConfig.ControlURL, serverIP)
if err != nil {
return Network{}, err
}
serviceConfig.SessionUDPPort = desired.SessionUDPPort
serviceConfig.MTU = desired.MTU
serviceConfig.ConfigVersion = desired.ConfigVersion
oldEncoded, _ := json.Marshal(old)
desiredEncoded, _ := json.Marshal(desired)
if err := m.sessions.ReconfigureNetwork(prefix, desired.MTU, desired.SessionUDPPort); err != nil {
return Network{}, err
}
rollbackSessions := func() { _ = m.sessions.ReconfigureNetwork(oldPrefix, old.MTU, old.SessionUDPPort) }
if err := m.store.ReplaceNodeOverlayIPsAndSetting(ctx, assignments, networkSettingKey, string(desiredEncoded)); err != nil {
rollbackSessions()
return Network{}, err
}
rollbackDatabase := func() {
_ = m.store.ReplaceNodeOverlayIPsAndSetting(context.Background(), oldAssignments, networkSettingKey, string(oldEncoded))
}
if err := m.ipam.Reconfigure(prefix, serverIP); err != nil {
rollbackDatabase()
rollbackSessions()
return Network{}, err
}
if err := m.bootstrap.UpdateNetwork(serviceConfig); err != nil {
_ = m.ipam.Reconfigure(oldPrefix, oldServerIP)
rollbackDatabase()
rollbackSessions()
return Network{}, err
}
if err := m.control.SetNetworkConfigVersion(desired.ConfigVersion); err != nil {
_ = m.bootstrap.UpdateNetwork(oldServiceConfig)
_ = m.ipam.Reconfigure(oldPrefix, oldServerIP)
rollbackDatabase()
rollbackSessions()
return Network{}, err
}
rollbackPublishedConfig := func() {
_ = m.control.SetNetworkConfigVersion(old.ConfigVersion)
_ = m.bootstrap.UpdateNetwork(oldServiceConfig)
_ = m.ipam.Reconfigure(oldPrefix, oldServerIP)
rollbackDatabase()
rollbackSessions()
}
// Publish the authoritative Bootstrap snapshot before notifying over the
// still-live old Control path. Switching wg0 peers or the listener first
// would make REBOOTSTRAP_REQUIRED physically undeliverable.
for _, node := range nodes {
_ = m.control.Send(ctx, node.ID, protocol.ControlRebootstrapRequired, protocol.RebootstrapRequiredPayload{
ConfigVersion: desired.ConfigVersion, Reason: "NETWORK_CONFIG_CHANGED",
})
}
if err := m.peers.Reconfigure(ctx, netip.PrefixFrom(serverIP, prefix.Bits()), desired.WireGuardPort, newPeers); err != nil {
rollbackPublishedConfig()
return Network{}, err
}
rollbackKernel := func() {
_ = m.peers.Reconfigure(context.Background(), netip.PrefixFrom(oldServerIP, oldPrefix.Bits()), old.WireGuardPort, oldPeers)
}
if serverIP != oldServerIP {
if err := m.rebindControl(serverIP); err != nil {
rollbackKernel()
rollbackPublishedConfig()
return Network{}, fmt.Errorf("rebind Control listener after migration notification: %w", err)
}
}
m.control.ResetConnections("Network configuration changed")
m.current = desired
return desired, nil
}
func validateNetwork(network Network) (netip.Prefix, netip.Addr, error) {
prefix, err := netip.ParsePrefix(network.OverlayCIDR)
if err != nil || !prefix.Addr().Is4() || prefix != prefix.Masked() || prefix.Bits() == 0 || prefix.Bits() > 30 {
return netip.Prefix{}, netip.Addr{}, errors.New("Overlay CIDR must be canonical IPv4 with usable hosts")
}
serverIP, err := netip.ParseAddr(network.ServerOverlayIP)
if err != nil || !serverIP.Is4() || !prefix.Contains(serverIP) || serverIP == prefix.Addr() || serverIP == lastIPv4(prefix) {
return netip.Prefix{}, netip.Addr{}, errors.New("Server Overlay IP must be a usable address in Overlay CIDR")
}
for name, port := range map[string]int{"WireGuard port": network.WireGuardPort, "Session UDP port": network.SessionUDPPort} {
if port < 1 || port > 65535 {
return netip.Prefix{}, netip.Addr{}, fmt.Errorf("%s must be between 1 and 65535", name)
}
}
if network.MTU < 576 || network.MTU > 65535 || network.ConfigVersion == 0 {
return netip.Prefix{}, netip.Addr{}, errors.New("MTU or config version is invalid")
}
return prefix, serverIP, nil
}
func replaceEndpointPort(endpoint string, port int) (string, error) {
host, _, err := net.SplitHostPort(endpoint)
if err != nil {
return "", err
}
return net.JoinHostPort(host, strconv.Itoa(port)), nil
}
func replaceControlHost(raw string, host netip.Addr) (string, error) {
parsed, err := url.Parse(raw)
if err != nil {
return "", err
}
_, port, err := net.SplitHostPort(parsed.Host)
if err != nil {
return "", err
}
parsed.Host = net.JoinHostPort(host.String(), port)
return parsed.String(), nil
}
func lastIPv4(prefix netip.Prefix) netip.Addr {
bytes := prefix.Masked().Addr().As4()
value := uint32(bytes[0])<<24 | uint32(bytes[1])<<16 | uint32(bytes[2])<<8 | uint32(bytes[3])
value |= ^uint32(0) >> prefix.Bits()
return netip.AddrFrom4([4]byte{byte(value >> 24), byte(value >> 16), byte(value >> 8), byte(value)})
}