// aura-client is a standalone Go port of the Aura UDP client. It dials a Rust-side `aura // server`, completes the mutual-auth post-quantum handshake, and exchanges a single round-trip // "hello" / echo on the data path to prove the connection is up. // // Usage: // // aura-client --config client.toml [--message "hello aura"] // // The TOML schema is a small subset of the production Rust client.toml; only the fields the Go // client actually needs are read (server address, PKI paths, expected server name, optional // knock). See README.md for a full example file. package main import ( "context" "crypto/sha256" "encoding/pem" "errors" "flag" "fmt" "log" "os" "os/user" "path/filepath" "strings" "time" "github.com/pelletier/go-toml/v2" "github.com/aura/singbox-aura/aura/handshake" "github.com/aura/singbox-aura/aura/transport" ) // config is the on-disk schema. Mirrors the relevant subset of config/client.toml.example. type config struct { Client struct { ServerAddr string `toml:"server_addr"` SNI string `toml:"sni"` } `toml:"client"` PKI struct { CACert string `toml:"ca_cert"` Cert string `toml:"cert"` Key string `toml:"key"` } `toml:"pki"` Transport struct { Knock struct { Enabled bool `toml:"enabled"` KnockSecretSource string `toml:"knock_secret_source"` } `toml:"knock"` } `toml:"transport"` } func main() { cfgPath := flag.String("config", "client.toml", "path to TOML config") message := flag.String("message", "hello aura", "single message to send on the data path") flag.Parse() cfg, err := loadConfig(*cfgPath) if err != nil { log.Fatalf("load config: %v", err) } hsCfg, err := buildHandshakeConfig(cfg) if err != nil { log.Fatalf("build handshake config: %v", err) } opts := transport.DefaultOptions() if cfg.Transport.Knock.Enabled { // The Rust side derives the knock key as SHA-256(CA-cert-DER). Mirror that here. key, err := caKnockKey(cfg.PKI.CACert) if err != nil { log.Fatalf("knock key: %v", err) } opts.KnockEnabled = true opts.KnockKey = key } ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() log.Printf("aura-client: dialing %s (sni=%s, knock=%t)", cfg.Client.ServerAddr, cfg.Client.SNI, opts.KnockEnabled) conn, err := transport.Dial(ctx, cfg.Client.ServerAddr, hsCfg, opts) if err != nil { log.Fatalf("dial: %v", err) } defer conn.Close() log.Printf("aura-client: connected (peer=%s)", conn.PeerID()) if err := conn.Send([]byte(*message)); err != nil { log.Fatalf("send: %v", err) } log.Printf("aura-client: sent %d bytes", len(*message)) conn.Close() } // loadConfig reads + parses the TOML, expanding ~ in PKI paths. func loadConfig(path string) (*config, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("read %s: %w", path, err) } var c config if err := toml.Unmarshal(data, &c); err != nil { return nil, fmt.Errorf("parse toml: %w", err) } c.PKI.CACert = expandHome(c.PKI.CACert) c.PKI.Cert = expandHome(c.PKI.Cert) c.PKI.Key = expandHome(c.PKI.Key) if c.Client.ServerAddr == "" { return nil, fmt.Errorf("config: [client].server_addr is required") } if c.Client.SNI == "" { return nil, fmt.Errorf("config: [client].sni is required") } for _, p := range []string{c.PKI.CACert, c.PKI.Cert, c.PKI.Key} { if p == "" { return nil, fmt.Errorf("config: [pki].{ca_cert,cert,key} are all required") } } return &c, nil } func buildHandshakeConfig(c *config) (*handshake.ClientConfig, error) { ca, err := os.ReadFile(c.PKI.CACert) if err != nil { return nil, fmt.Errorf("read CA: %w", err) } cert, err := os.ReadFile(c.PKI.Cert) if err != nil { return nil, fmt.Errorf("read cert: %w", err) } key, err := os.ReadFile(c.PKI.Key) if err != nil { return nil, fmt.Errorf("read key: %w", err) } return &handshake.ClientConfig{ CAPEM: ca, CertPEM: cert, KeyPEM: key, ServerName: c.Client.SNI, }, nil } // caKnockKey loads the CA PEM and returns sha256(CA-cert-DER) — same derivation the Rust // side uses for the knock shared secret. func caKnockKey(path string) ([32]byte, error) { var zero [32]byte pemBytes, err := os.ReadFile(path) if err != nil { return zero, fmt.Errorf("read CA: %w", err) } der, err := firstCertDER(pemBytes) if err != nil { return zero, err } return sha256.Sum256(der), nil } // firstCertDER decodes the first CERTIFICATE PEM block. func firstCertDER(pemBytes []byte) ([]byte, error) { rest := pemBytes for { block, r := pem.Decode(rest) if block == nil { return nil, errors.New("no CERTIFICATE block in PEM") } if block.Type == "CERTIFICATE" { return block.Bytes, nil } rest = r } } // expandHome turns a leading ~/ or just ~ into the user's home directory. Matches what the // Rust client.toml loader does. func expandHome(p string) string { if p == "" || !strings.HasPrefix(p, "~") { return p } u, err := user.Current() if err != nil || u.HomeDir == "" { return p } if p == "~" { return u.HomeDir } if strings.HasPrefix(p, "~/") { return filepath.Join(u.HomeDir, p[2:]) } return p }