Initial commit: AetherForge Linux (forge-mesh) v0.1.0-dev
Some checks failed
Test / test (push) Has been cancelled
Some checks failed
Test / test (push) Has been cancelled
This commit is contained in:
308
internal/stratum/proxy.go
Normal file
308
internal/stratum/proxy.go
Normal file
@@ -0,0 +1,308 @@
|
||||
package stratum
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"forge-mesh/internal/config"
|
||||
)
|
||||
|
||||
// Proxy forwards stratum miner connections to upstream pools with per-agent worker suffixes.
|
||||
type Proxy struct {
|
||||
cfg config.StratumConfig
|
||||
listeners []net.Listener
|
||||
active int64
|
||||
wg sync.WaitGroup
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
// New creates a stratum proxy from server config.
|
||||
func New(cfg config.StratumConfig) *Proxy {
|
||||
return &Proxy{cfg: cfg}
|
||||
}
|
||||
|
||||
// ActiveConnections returns the number of live proxied sessions.
|
||||
func (p *Proxy) ActiveConnections() int64 {
|
||||
return atomic.LoadInt64(&p.active)
|
||||
}
|
||||
|
||||
// Start begins listening without requiring the caller to supply a context.
|
||||
func (p *Proxy) Start() error {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
p.cancel = cancel
|
||||
return p.StartContext(ctx)
|
||||
}
|
||||
|
||||
// StartContext listens on XMR and RVN ports and relays to upstream pools.
|
||||
func (p *Proxy) StartContext(ctx context.Context) error {
|
||||
endpoints := []struct {
|
||||
listen string
|
||||
upstream string
|
||||
label string
|
||||
}{
|
||||
{p.cfg.XMRListen, p.cfg.UpstreamXMR, "XMR"},
|
||||
{p.cfg.RVNListen, p.cfg.UpstreamRVN, "RVN"},
|
||||
}
|
||||
|
||||
for _, ep := range endpoints {
|
||||
if ep.listen == "" {
|
||||
continue
|
||||
}
|
||||
ln, err := net.Listen("tcp", ep.listen)
|
||||
if err != nil {
|
||||
p.Stop()
|
||||
return fmt.Errorf("listen %s (%s): %w", ep.listen, ep.label, err)
|
||||
}
|
||||
p.listeners = append(p.listeners, ln)
|
||||
log.Printf("stratum: %s proxy listening on %s -> %s", ep.label, ep.listen, displayUpstream(ep.upstream))
|
||||
|
||||
go p.serveListener(ctx, ln, ep.upstream, ep.label)
|
||||
}
|
||||
|
||||
if len(p.listeners) == 0 {
|
||||
return fmt.Errorf("no stratum listeners configured")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func displayUpstream(upstream string) string {
|
||||
if upstream == "" {
|
||||
return "(local accept)"
|
||||
}
|
||||
return upstream
|
||||
}
|
||||
|
||||
func (p *Proxy) serveListener(ctx context.Context, ln net.Listener, upstream, label string) {
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
if ne, ok := err.(net.Error); ok && ne.Temporary() {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
p.wg.Add(1)
|
||||
go func(c net.Conn) {
|
||||
defer p.wg.Done()
|
||||
p.handleConn(ctx, c, upstream, label)
|
||||
}(conn)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Proxy) handleConn(ctx context.Context, miner net.Conn, upstreamAddr, label string) {
|
||||
defer miner.Close()
|
||||
atomic.AddInt64(&p.active, 1)
|
||||
defer atomic.AddInt64(&p.active, -1)
|
||||
|
||||
agentSuffix := agentSuffixFromAddr(miner.RemoteAddr().String())
|
||||
minerReader := bufio.NewReader(miner)
|
||||
minerWriter := bufio.NewWriter(miner)
|
||||
|
||||
var upstream net.Conn
|
||||
var upstreamReader *bufio.Reader
|
||||
var upstreamWriter *bufio.Writer
|
||||
|
||||
openUpstream := func() error {
|
||||
if upstreamAddr == "" || upstream != nil {
|
||||
return nil
|
||||
}
|
||||
dialer := net.Dialer{Timeout: 10 * time.Second}
|
||||
up, err := dialer.DialContext(ctx, "tcp", upstreamAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
upstream = up
|
||||
upstreamReader = bufio.NewReader(upstream)
|
||||
upstreamWriter = bufio.NewWriter(upstream)
|
||||
log.Printf("stratum: %s relay %s <-> %s worker_suffix=%s", label, miner.RemoteAddr(), upstreamAddr, agentSuffix)
|
||||
return nil
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if upstream != nil {
|
||||
_ = upstream.Close()
|
||||
}
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
line, err := minerReader.ReadString('\n')
|
||||
if err != nil {
|
||||
if upstream != nil {
|
||||
_ = upstream.Close()
|
||||
}
|
||||
return
|
||||
}
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
var req stratumRequest
|
||||
_ = json.Unmarshal([]byte(line), &req)
|
||||
method := strings.ToLower(req.Method)
|
||||
|
||||
if upstreamAddr != "" && upstream == nil && isStratumMethod(method) {
|
||||
if err := openUpstream(); err != nil {
|
||||
log.Printf("stratum: %s upstream dial %s: %v", label, upstreamAddr, err)
|
||||
writeLocalError(minerWriter, req.ID, "upstream unavailable")
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if upstream == nil {
|
||||
handleLocal(minerWriter, req)
|
||||
continue
|
||||
}
|
||||
|
||||
outLine := line
|
||||
if method == "mining.authorize" {
|
||||
outLine = rewriteAuthorize(line, req, agentSuffix)
|
||||
}
|
||||
|
||||
if _, err := upstreamWriter.WriteString(outLine + "\n"); err != nil {
|
||||
return
|
||||
}
|
||||
if err := upstreamWriter.Flush(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
respLine, err := upstreamReader.ReadString('\n')
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if _, err := minerWriter.WriteString(respLine); err != nil {
|
||||
return
|
||||
}
|
||||
if err := minerWriter.Flush(); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type stratumRequest struct {
|
||||
ID json.RawMessage `json:"id"`
|
||||
Method string `json:"method"`
|
||||
Params []json.RawMessage `json:"params"`
|
||||
}
|
||||
|
||||
func isStratumMethod(method string) bool {
|
||||
switch method {
|
||||
case "mining.subscribe", "mining.authorize", "mining.submit", "mining.extranonce.subscribe":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func rewriteAuthorize(line string, req stratumRequest, suffix string) string {
|
||||
if len(req.Params) == 0 || suffix == "" {
|
||||
return line
|
||||
}
|
||||
var worker string
|
||||
if err := json.Unmarshal(req.Params[0], &worker); err != nil {
|
||||
return line
|
||||
}
|
||||
newWorker := WorkerName(worker, suffix)
|
||||
req.Params[0], _ = json.Marshal(newWorker)
|
||||
out, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return line
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
// WorkerName appends an agent-specific suffix to the pool worker name.
|
||||
func WorkerName(baseWorker, agentID string) string {
|
||||
if agentID == "" {
|
||||
return baseWorker
|
||||
}
|
||||
if strings.Contains(baseWorker, ".") {
|
||||
return baseWorker + "." + agentID
|
||||
}
|
||||
return baseWorker + "." + agentID
|
||||
}
|
||||
|
||||
func agentSuffixFromAddr(remote string) string {
|
||||
host, _, err := net.SplitHostPort(remote)
|
||||
if err != nil {
|
||||
return strings.ReplaceAll(remote, ":", "_")
|
||||
}
|
||||
return strings.ReplaceAll(host, ".", "_")
|
||||
}
|
||||
|
||||
func handleLocal(w *bufio.Writer, req stratumRequest) {
|
||||
method := strings.ToLower(req.Method)
|
||||
switch method {
|
||||
case "mining.subscribe":
|
||||
_ = writeJSON(w, map[string]interface{}{
|
||||
"id": req.ID,
|
||||
"result": []interface{}{[]interface{}{"00000000", "00000000", "00000000"}, "00000001", "00000004"},
|
||||
"error": nil,
|
||||
})
|
||||
case "mining.authorize", "mining.submit":
|
||||
_ = writeJSON(w, map[string]interface{}{
|
||||
"id": req.ID,
|
||||
"result": true,
|
||||
"error": nil,
|
||||
})
|
||||
default:
|
||||
_ = writeJSON(w, map[string]interface{}{
|
||||
"id": req.ID,
|
||||
"result": true,
|
||||
"error": nil,
|
||||
})
|
||||
}
|
||||
_ = w.Flush()
|
||||
}
|
||||
|
||||
func writeLocalError(w *bufio.Writer, id json.RawMessage, msg string) {
|
||||
_ = writeJSON(w, map[string]interface{}{
|
||||
"id": id,
|
||||
"result": nil,
|
||||
"error": []interface{}{20, msg, nil},
|
||||
})
|
||||
_ = w.Flush()
|
||||
}
|
||||
|
||||
func writeJSON(w io.Writer, v interface{}) error {
|
||||
data, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data = append(data, '\n')
|
||||
_, err = w.Write(data)
|
||||
return err
|
||||
}
|
||||
|
||||
// Stop closes listeners and waits for active relays to finish.
|
||||
func (p *Proxy) Stop() {
|
||||
if p.cancel != nil {
|
||||
p.cancel()
|
||||
}
|
||||
for _, ln := range p.listeners {
|
||||
_ = ln.Close()
|
||||
}
|
||||
p.wg.Wait()
|
||||
}
|
||||
|
||||
// Close is an alias for Stop for server lifecycle hooks.
|
||||
func (p *Proxy) Close() { p.Stop() }
|
||||
74
internal/stratum/proxy_test.go
Normal file
74
internal/stratum/proxy_test.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package stratum
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"forge-mesh/internal/config"
|
||||
)
|
||||
|
||||
func TestProxyAcceptsConnection(t *testing.T) {
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
accepted := make(chan net.Conn, 1)
|
||||
go func() {
|
||||
conn, err := ln.Accept()
|
||||
if err == nil {
|
||||
accepted <- conn
|
||||
}
|
||||
}()
|
||||
|
||||
conn, err := net.Dial("tcp", ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
select {
|
||||
case c := <-accepted:
|
||||
_, _ = conn.Write([]byte(`{"id":1,"method":"mining.subscribe","params":[]}` + "\n"))
|
||||
buf := make([]byte, 256)
|
||||
n, _ := c.Read(buf)
|
||||
if n == 0 {
|
||||
t.Fatal("expected bytes from miner")
|
||||
}
|
||||
c.Close()
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("accept timeout")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyStartNoUpstream(t *testing.T) {
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
addr := ln.Addr().String()
|
||||
ln.Close()
|
||||
|
||||
p := New(config.StratumConfig{XMRListen: addr})
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
if err := p.StartContext(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer p.Stop()
|
||||
|
||||
conn, err := net.DialTimeout("tcp", addr, time.Second)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
_, err = conn.Write([]byte(`{"id":1,"method":"mining.subscribe","params":[]}` + "\n"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user