diff options
Diffstat (limited to 'cmd/mirumw/main.go')
| -rw-r--r-- | cmd/mirumw/main.go | 195 | +179 −16 |
1 files changed, 179 insertions, 16 deletions
diff --git a/cmd/mirumw/main.go b/cmd/mirumw/main.go index 194abfa..1299b86 100644 --- a/cmd/mirumw/main.go +++ b/cmd/mirumw/main.go @@ -5,45 +5,195 @@ package main import ( "context" + "crypto/tls" "flag" "fmt" "log/slog" + "net" "os" + "time" + "mrdimidium/mirum/internal/protocol" + "mrdimidium/mirum/internal/protocol/pb" "mrdimidium/mirum/internal/supervisor" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/protobuf/types/known/timestamppb" "gopkg.in/yaml.v3" ) -type config struct { - Server string `yaml:"server"` -} +// Runtime type of this worker binary. Different worker types +// (mirumw-vm, mirumw-docker, etc.) will have different values. +const workerRuntime = "host" -var cfg = config{ - Server: "localhost:2026", +type config struct { + Name string `yaml:"name"` + Server string `yaml:"server"` + Secret string `yaml:"secret"` + Insecure bool `yaml:"tls_insecure"` } -var configFile = flag.String("config", "", "path to config file") - -func main() { - flag.Parse() +func GetConfig(filename string) (*config, error) { + cfg := &config{ + Server: "localhost:2026", + } - if *configFile != "" { - data, err := os.ReadFile(*configFile) + if filename != "" { + data, err := os.ReadFile(filename) if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) + return nil, fmt.Errorf("couldn't read config: %w", err) } + if err := yaml.Unmarshal(data, &cfg); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) + return nil, fmt.Errorf("couldn't parse config: %w", err) + } + } + + return cfg, nil +} + +type client struct { + cfg *config + conn *grpc.ClientConn + handle pb.MirumClient +} + +func ClientConnect(cfg *config) (*client, error) { + var creds grpc.DialOption + if cfg.Insecure { + host, _, _ := net.SplitHostPort(cfg.Server) + if !isPrivateHost(host) { + return nil, fmt.Errorf("tls_insecure is only allowed for private/loopback addresses, got %s", host) } + + slog.Warn("TLS disabled, connection is not encrypted") + creds = grpc.WithTransportCredentials(insecure.NewCredentials()) + } else { + creds = grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})) + } + + conn, err := grpc.NewClient(cfg.Server, creds) + if err != nil { + return nil, err + } + + return &client{ + cfg: cfg, + conn: conn, + handle: pb.NewMirumClient(conn), + }, nil +} + +func (c *client) Close() error { + return c.conn.Close() +} + +func (c *client) Handshake(ctx context.Context) error { + stream, err := c.handle.Handshake(ctx) + if err != nil { + return fmt.Errorf("open stream: %w", err) } + // Step 1: send worker nonce + workerNonce, err := protocol.GenerateNonce() + if err != nil { + return fmt.Errorf("generate nonce: %w", err) + } + if err := stream.Send(&pb.HandshakeIn{ + Step: &pb.HandshakeIn_WorkerChallenge{ + WorkerChallenge: &pb.WorkerChallenge{Nonce: workerNonce}, + }, + }); err != nil { + return fmt.Errorf("send challenge: %w", err) + } + + // Step 2: receive server challenge, verify server + out, err := stream.Recv() + if err != nil { + return fmt.Errorf("recv server challenge: %w", err) + } + + sc := out.GetServerChallenge() + if sc == nil { + return fmt.Errorf("expected ServerChallenge") + } + if !protocol.VerifyProof([]byte(c.cfg.Secret), workerNonce, sc.GetNonce(), sc.GetProof()) { + return fmt.Errorf("server proof verification failed") + } + + // Step 3: send worker proof + metadata + name := c.cfg.Name + if name == "" { + name, _ = os.Hostname() + } + if err := stream.Send(&pb.HandshakeIn{ + Step: &pb.HandshakeIn_WorkerProof{ + WorkerProof: &pb.WorkerProof{ + Proof: protocol.ComputeProof([]byte(c.cfg.Secret), sc.GetNonce(), workerNonce), + Name: name, + Version: protocol.VersionProto(), + Os: protocol.DetectOs(), + Arch: protocol.DetectArch(), + Runtime: workerRuntime, + WorkerTime: timestamppb.Now(), + }, + }, + }); err != nil { + return fmt.Errorf("send proof: %w", err) + } + + // Step 4: receive result + out, err = stream.Recv() + if err != nil { + return fmt.Errorf("recv result: %w", err) + } + sr := out.GetServerResult() + if sr == nil { + return fmt.Errorf("expected ServerResult") + } + if sr.Error != nil { + return fmt.Errorf("server rejected: %s", *sr.Error) + } + + v := sr.GetServerVersion() + slog.Info("handshake ok", "server_version", fmt.Sprintf("%d.%d.%d", v.GetMajor(), v.GetMinor(), v.GetPatch())) + return nil +} + +func main() { + configFile := flag.String("config", "", "path to config file") + flag.Parse() + sup := supervisor.Detect() ctx := sup.WaitForStop(context.Background()) - slog.Info("connecting", "server", cfg.Server) + cfg, err := GetConfig(*configFile) + if err != nil { + slog.Error("config", "err", err) + os.Exit(1) + } + + client, err := ClientConnect(cfg) + if err != nil { + slog.Error("failed grpc client", "err", err) + os.Exit(1) + } + defer func() { + if err := client.Close(); err != nil { + slog.Warn("close grpc client", "err", err) + } + }() + + handshakeCtx, handshakeCancel := context.WithTimeout(ctx, 30*time.Second) + defer handshakeCancel() + if err := client.Handshake(handshakeCtx); err != nil { + slog.Error("handshake failed", "err", err) + os.Exit(1) + } else { + slog.Info("connected", "server", cfg.Server) + } sup.Ready() go sup.StartWatchdog() @@ -52,3 +202,16 @@ func main() { slog.Info("shutting down") sup.Stopping() } + +func isPrivateHost(host string) bool { + ips, err := net.LookupIP(host) + if err != nil { + return false + } + for _, ip := range ips { + if !ip.IsLoopback() && !ip.IsPrivate() && !ip.IsLinkLocalUnicast() { + return false + } + } + return len(ips) > 0 +} |
