@@ -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 {
561587func 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