Skip to content

Commit 2a786f2

Browse files
committed
ipn/proxies: impl http reachability checks
1 parent d74990c commit 2a786f2

2 files changed

Lines changed: 112 additions & 7 deletions

File tree

intra/ipn/proxies.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1129,12 +1129,12 @@ func (px *proxifier) Contains(ipprefix *x.Gostr) bool {
11291129
}
11301130

11311131
// Reaches implements x.Router.
1132-
func (px *proxifier) Reaches(hostportOrIPPortCsv *x.Gostr) bool {
1132+
func (px *proxifier) Reaches(urlOrHostPortOrIPPortCsv *x.Gostr) bool {
11331133
px.RLock()
11341134
defer px.RUnlock()
11351135

11361136
for _, p := range px.p {
1137-
if r := p.Router(); r != nil && r.Reaches(hostportOrIPPortCsv) {
1137+
if r := p.Router(); r != nil && r.Reaches(urlOrHostPortOrIPPortCsv) {
11381138
return true
11391139
}
11401140
}

intra/ipn/proxy.go

Lines changed: 110 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"errors"
1212
"fmt"
1313
"net"
14+
"net/http"
1415
"net/netip"
1516
"net/url"
1617
"os"
@@ -247,14 +248,39 @@ func (pxr *proxifier) fromOpts(id string, opts *settings.ProxyOptions) (Proxy, e
247248
return p, err
248249
}
249250

250-
func Reaches(p Proxy, hostportOrIPPortCsv string, protos ...string) bool {
251+
func Reaches(p Proxy, urlOrHostPortOrIPPortCsv string, protos ...string) bool {
251252
if p == nil || p.Status() == END {
252253
return false
253254
}
254-
if len(hostportOrIPPortCsv) <= 0 {
255+
if len(urlOrHostPortOrIPPortCsv) <= 0 {
255256
return true
256257
}
257258

259+
if urls := httpURLs(urlOrHostPortOrIPPortCsv); len(urls) > 0 {
260+
// For URLs, only test HTTPS connectivity
261+
tests := make([]core.WorkCtx[bool], 0)
262+
for _, u := range urls {
263+
tests = append(tests, httpsReachesWorkCtx(p, u))
264+
}
265+
266+
pid := idstr(p)
267+
if len(tests) <= 0 {
268+
log.W("proxy: %s reaches: %v; no HTTPS tests", pid, urlOrHostPortOrIPPortCsv)
269+
return false
270+
}
271+
272+
okays, errs := core.All("reach."+pid, 5*time.Second, tests...)
273+
274+
overall := core.IsAll(errs, func(err error) bool { return err == nil }) &&
275+
core.IsAll(okays, func(ok bool) bool { return ok })
276+
277+
logeif(overall)("proxy: %s reaches: %v verdict (https): reachable? %t [oks? %v; errs? %v]",
278+
pid, urlOrHostPortOrIPPortCsv, overall, okays, errs)
279+
280+
return overall
281+
}
282+
283+
// Original logic for host:port or ip:port
258284
hastcp := has(protos, "tcp") || has(protos, "tcp4") || has(protos, "tcp6")
259285
hasudp := has(protos, "udp") || has(protos, "udp4") || has(protos, "udp6")
260286
hasicmp := has(protos, "icmp") || has(protos, "icmp4") || has(protos, "icmp6")
@@ -270,7 +296,7 @@ func Reaches(p Proxy, hostportOrIPPortCsv string, protos ...string) bool {
270296
// upstream = pdns
271297
// }
272298
ipps := make([]netip.AddrPort, 0)
273-
for x := range strings.SplitSeq(hostportOrIPPortCsv, ",") {
299+
for x := range strings.SplitSeq(urlOrHostPortOrIPPortCsv, ",") {
274300
host, port, err := net.SplitHostPort(x)
275301
if err != nil {
276302
port = "80"
@@ -307,7 +333,7 @@ func Reaches(p Proxy, hostportOrIPPortCsv string, protos ...string) bool {
307333
pid := idstr(p)
308334
if len(tests) <= 0 {
309335
log.W("proxy: %s reaches: %v / %v; no tests for %s",
310-
pid, hostportOrIPPortCsv, ipps, protos)
336+
pid, urlOrHostPortOrIPPortCsv, ipps, protos)
311337
return false
312338
}
313339

@@ -318,7 +344,7 @@ func Reaches(p Proxy, hostportOrIPPortCsv string, protos ...string) bool {
318344
core.IsAll(okays, func(ok bool) bool { return ok })
319345

320346
logeif(overall)("proxy: %s reaches: %v => %v verdict (%s): reachable? %t [oks? %v; errs? %v]",
321-
pid, hostportOrIPPortCsv, ipps, protos, overall, okays, errs)
347+
pid, urlOrHostPortOrIPPortCsv, ipps, protos, overall, okays, errs)
322348

323349
return overall
324350
}
@@ -561,3 +587,82 @@ func removeElem[T comparable](s []T, rmv T) []T {
561587
func addElem[T comparable](s []T, add T) []T {
562588
return core.WithElem(s, add)
563589
}
590+
591+
// httpURLs extracts valid URLs from comma-separated input
592+
func httpURLs(input string) (urls []*url.URL) {
593+
for x := range strings.SplitSeq(input, ",") {
594+
x = strings.TrimSpace(x)
595+
if len(x) == 0 {
596+
continue
597+
}
598+
// Check if it's a URL (contains scheme)
599+
if u, err := url.Parse(x); err == nil && strings.Contains(u.Scheme, "http") {
600+
urls = append(urls, u)
601+
}
602+
}
603+
return urls
604+
}
605+
606+
func httpsReachesWorkCtx(p Proxy, url *url.URL) core.WorkCtx[bool] {
607+
return func(ctx context.Context) (bool, error) {
608+
return httpsReaches(p, url)
609+
}
610+
}
611+
612+
func httpsReaches(p Proxy, url *url.URL) (bool, error) {
613+
start := time.Now()
614+
615+
requestednetwork := url.Fragment
616+
switch requestednetwork {
617+
case "tcp", "tcp4", "tcp6":
618+
case "udp", "udp4", "udp6":
619+
case "v4", "ipv4":
620+
requestednetwork = "tcp4" // default to tcp4 for v4
621+
case "v6", "ipv6":
622+
requestednetwork = "tcp6" // default to tcp6 for v6
623+
default:
624+
requestednetwork = "tcp" // default to tcp for any other case
625+
}
626+
627+
// TODO: share http.Transport across checks
628+
client := &http.Client{
629+
Timeout: 5 * time.Second,
630+
Transport: &http.Transport{
631+
Dial: func(network, addr string) (net.Conn, error) {
632+
if _, err := netip.ParseAddrPort(addr); err != nil {
633+
// addr is likely host:port
634+
network = requestednetwork
635+
}
636+
log.VV("proxy: %s reaches: dial(%s, %s) for %s", idstr(p), network, addr, url)
637+
return p.Dial(network, addr)
638+
},
639+
},
640+
}
641+
642+
req, err := http.NewRequest("HEAD", url.String(), nil)
643+
if err != nil {
644+
return false, fmt.Errorf("proxy: reaches: err creating req: %w", err)
645+
}
646+
req.Header.Set("User-Agent", "intra")
647+
648+
resp, err := client.Do(req)
649+
if resp != nil {
650+
defer core.Close(resp.Body)
651+
}
652+
653+
rtt := time.Since(start)
654+
statuscode := -1
655+
if resp != nil {
656+
statuscode = resp.StatusCode
657+
}
658+
659+
ok := err == nil && statuscode > 0 && statuscode < 500
660+
661+
logeif(!ok)("proxy: %s reaches: %v (%s); ok? %t, status: %d, rtt: %s; err: %v",
662+
idstr(p), url, requestednetwork, ok, statuscode, core.FmtPeriod(rtt), err)
663+
664+
if ok {
665+
err = nil // wipe out err as it makes core.Race discard "ok"
666+
}
667+
return ok, err
668+
}

0 commit comments

Comments
 (0)