// Package outbound is a thin sing-box-shaped wrapper around the Aura UDP client. It does NOT // import github.com/sagernet/sing-box — keeping the dependency footprint small and the build // self-contained for v1. The interface is shaped after sing-box's outbound (Network, // DialContext, ListenPacket) so a follow-up patch can register this as a real outbound by // vendoring the sing-box module + filling in the missing glue. // // See README.md for the concrete integration steps. package outbound import ( "context" "errors" "fmt" "net" "time" "github.com/aura/singbox-aura/aura/handshake" "github.com/aura/singbox-aura/aura/transport" ) // Tag is the identifier this outbound advertises to a sing-box router. Real registration would // set it on the outbound options struct. const Tag = "aura" // Network returns the sing-box network type. Aura is connection-oriented over UDP underneath // but the application-layer abstraction is reliable+ordered for streams (TCP-like) and // best-effort for datagrams (UDP-like), so we expose UDP here — matches how the QUIC outbound // is registered. func Network() []string { return []string{"udp"} } // Outbound is the per-server configuration that a sing-box-style host instantiates once per // upstream. One Outbound can dial many short-lived connections. type Outbound struct { ServerAddr string // e.g. "203.0.113.10:443" HSConfig *handshake.ClientConfig // CA + leaf cert + leaf key + expected server SNI Opts *transport.Options // optional knock + handshake timers } // DialContext opens an Aura UDP connection to the upstream and wraps it as a net.PacketConn // for the sing-box stack to write IP packets to. `network` must be "udp"/"udp4"/"udp6"; // `destination` is the application target the sing-box router computed (unused by v1 — Aura // carries opaque IP packets, not per-flow destinations). func (o *Outbound) DialContext(ctx context.Context, network, destination string) (net.PacketConn, error) { switch network { case "udp", "udp4", "udp6": default: return nil, fmt.Errorf("aura/outbound: unsupported network %q", network) } if o.ServerAddr == "" { return nil, errors.New("aura/outbound: ServerAddr is empty") } if o.HSConfig == nil { return nil, errors.New("aura/outbound: HSConfig is nil") } conn, err := transport.Dial(ctx, o.ServerAddr, o.HSConfig, o.Opts) if err != nil { return nil, err } return &packetConnAdapter{conn: conn, dest: destination}, nil } // ListenPacket is the same call shape sing-box uses for inbound-style transports; for an // outbound this is a convenience that delegates to DialContext. func (o *Outbound) ListenPacket(ctx context.Context, destination string) (net.PacketConn, error) { return o.DialContext(ctx, "udp", destination) } // packetConnAdapter exposes a transport.Connection as net.PacketConn. ReadFrom returns the // next inner IP payload and a placeholder *net.UDPAddr (Aura tunnels opaque IP packets — the // concrete destination addr is decoded by the upper layer). WriteTo simply ships the payload. type packetConnAdapter struct { conn *transport.Connection dest string } func (p *packetConnAdapter) ReadFrom(buf []byte) (int, net.Addr, error) { pkt, err := p.conn.Recv() if err != nil { return 0, nil, err } n := copy(buf, pkt) // We do not have a real source addr at this layer; report the peer's identity as a fake // UDP address so any sing-box code that logs addr.String() gets something sensible. addr, _ := net.ResolveUDPAddr("udp", p.dest) return n, addr, nil } func (p *packetConnAdapter) WriteTo(buf []byte, _ net.Addr) (int, error) { if err := p.conn.Send(buf); err != nil { return 0, err } return len(buf), nil } func (p *packetConnAdapter) Close() error { return p.conn.Close() } func (p *packetConnAdapter) LocalAddr() net.Addr { return &net.UDPAddr{IP: net.IPv4zero} } func (p *packetConnAdapter) SetDeadline(_ time.Time) error { return nil } func (p *packetConnAdapter) SetReadDeadline(_ time.Time) error { return nil } func (p *packetConnAdapter) SetWriteDeadline(_ time.Time) error { return nil }