SagerNet / SagerNet/sing-box

[Feature Request] implement healthcheck or extend urltest with HTTP/2 ping

Open
#1,494 7 comments 2 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

enhancement
Dominant language
Go
Stars
38.2k
Forks
4.6k
Avg merge
19d 15h
Merged PRs (30d)
1

Description

It seems that currently sing-box is doing full http(s) requests for urltest by default:

https://github.com/SagerNet/sing-box/blob/ab272cd9533ecb699743502ebb1a169fc6d1655d/common/urltest/urltest.go#L75-L78

https://github.com/SagerNet/sing-box/blob/ab272cd9533ecb699743502ebb1a169fc6d1655d/common/urltest/urltest.go#L94-L122

We both know non-HTTPS captive portal endpoints are unreliable, but the full HTTPS requests impact performance and they're not 1-rtt.

With HTTP/2 ping, we can measure 1-rtt with ~100 bytes' read and write.

With simple tests:

Source code
package main

import (
	"context"
	"crypto/tls"
	"flag"
	"fmt"
	"net"
	"time"

	"golang.org/x/net/http2"
)

type Stats struct {
	conn         net.Conn
	BytesRead    int64
	BytesWritten int64
}

func (t *Stats) Read(b []byte) (n int, err error) {
	n, err = t.conn.Read(b)
	t.BytesRead += int64(n)
	return
}

func (t *Stats) Write(b []byte) (n int, err error) {
	n, err = t.conn.Write(b)
	t.BytesWritten += int64(n)
	return
}

func (t *Stats) Close() error {
	return t.conn.Close()
}

func (t *Stats) LocalAddr() net.Addr {
	return t.conn.LocalAddr()
}

func (t *Stats) RemoteAddr() net.Addr {
	return t.conn.RemoteAddr()
}

func (t *Stats) SetDeadline(time time.Time) error {
	return t.conn.SetDeadline(time)
}

func (t *Stats) SetReadDeadline(time time.Time) error {
	return t.conn.SetReadDeadline(time)
}

func (t *Stats) SetWriteDeadline(time time.Time) error {
	return t.conn.SetWriteDeadline(time)
}

func NewStatsConn(conn net.Conn) *Stats {
	return &Stats{
		conn: conn,
	}
}

func main() {
	resolve := flag.String("resolve", "1.1.1.1", "")
	server := flag.String("server", "cloudflare.com", "ServerName is used to verify the hostname on the returned certificates unless InsecureSkipVerify is given. It is also included in the client's handshake to support virtual hosting unless it is an IP address.")
	port := flag.Uint("port", 443, "")

	flag.Parse()

	var address string
	if *resolve == "" {
		address = fmt.Sprintf("%s:%d", *server, *port)
	} else {
		address = fmt.Sprintf("%s:%d", *resolve, *port)
	}
	tcpConn, err := net.Dial("tcp", address)
	if err != nil {
		panic(err)
	}
	defer tcpConn.Close()

	statsConn := NewStatsConn(tcpConn)

	tlsConn := tls.Client(statsConn, &tls.Config{
		ServerName: *server,
		NextProtos: []string{"h2"},
	})

	tr := http2.Transport{}
	http2Conn, err := tr.NewClientConn(tlsConn)
	if err != nil {
		panic(err)
	}
	defer http2Conn.Close()

	for http2Conn.State().MaxConcurrentStreams == 0 {
		time.Sleep(time.Millisecond * 100)
	}

	fmt.Println("init", statsConn.BytesRead, statsConn.BytesWritten)

	r, w := statsConn.BytesRead, statsConn.BytesWritten

	for i := 0; i < 3; i++ {
		// https://github.com/zckevin/Clash.Meta/tree/b349b464acf0984553597cc95b0092d7866cbbc0/adapter/outboundgroup/http2ping
		start := time.Now()
		err = http2Conn.Ping(context.Background())
		rtt := time.Since(start)

		fmt.Println(*server, "index", i,
			"read", statsConn.BytesRead-r, "written", statsConn.BytesWritten-w,
			"rtt(ms)", uint(rtt.Milliseconds()))
		r, w = statsConn.BytesRead, statsConn.BytesWritten

		if err != nil {
			panic(err)
		}
		time.Sleep(time.Second)
	}
}

$ go run ./http2ping -server dns.pub -resolve 1.12.12.12
init 2712 525
dns.pub index 0 read 46 written 46 rtt(ms) 11
dns.pub index 1 read 46 written 46 rtt(ms) 11
dns.pub index 2 read 46 written 46 rtt(ms) 11

$ go run ./http2ping -server cloudflare.com -resolve 1.1.1.1
init 3033 456
cloudflare.com index 0 read 39 written 39 rtt(ms) 221
cloudflare.com index 1 read 39 written 39 rtt(ms) 219
cloudflare.com index 2 read 39 written 39 rtt(ms) 219

$ go run ./http2ping -server 8.8.8.8 -resolve 8.8.8.8
init 4913 433
8.8.8.8 index 0 read 39 written 39 rtt(ms) 220
8.8.8.8 index 1 read 39 written 39 rtt(ms) 221
8.8.8.8 index 2 read 39 written 39 rtt(ms) 220

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with common/urltest/urltest.go at the linked request and response handling, then review the provided golang.org/x/net/http2 example and its Ping API. Determine whether the work belongs in urltest or as a separate healthcheck, and verify that the selected approach measures HTTP/2 round-trip latency with the intended small read and write.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
networking
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.