Repository: cloudflare/cloudflared Branch: master Commit: d2a87e9b9345 Files: 3152 Total size: 31.3 MB Directory structure: gitextract_x69hvzqa/ ├── .ci/ │ ├── apt-internal.gitlab-ci.yml │ ├── ci-image.gitlab-ci.yml │ ├── commons.gitlab-ci.yml │ ├── github.gitlab-ci.yml │ ├── image/ │ │ ├── .docker-images │ │ └── Dockerfile │ ├── linux.gitlab-ci.yml │ ├── mac.gitlab-ci.yml │ ├── release.gitlab-ci.yml │ ├── scripts/ │ │ ├── component-tests.sh │ │ ├── fmt-check.sh │ │ ├── github-push.sh │ │ ├── linux/ │ │ │ ├── build-packages-fips.sh │ │ │ └── build-packages.sh │ │ ├── mac/ │ │ │ ├── build.sh │ │ │ └── install-go.sh │ │ ├── package-windows.sh │ │ ├── release-target.sh │ │ ├── vuln-check.sh │ │ └── windows/ │ │ ├── builds.ps1 │ │ ├── component-test.ps1 │ │ ├── go-wrapper.ps1 │ │ └── sign-msi.ps1 │ └── windows.gitlab-ci.yml ├── .docker-images ├── .dockerignore ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── ---bug-report.md │ │ ├── ---documentation.md │ │ └── ---feature-request.md │ └── workflows/ │ ├── check.yaml │ └── semgrep.yml ├── .gitignore ├── .gitlab-ci.yml ├── .golangci.yaml ├── .mac_resources/ │ ├── scripts/ │ │ └── postinstall │ └── uninstall.sh ├── .vulnignore ├── AGENTS.md ├── CHANGES.md ├── Dockerfile ├── Dockerfile.amd64 ├── Dockerfile.arm64 ├── LICENSE ├── Makefile ├── README.md ├── RELEASE_NOTES ├── carrier/ │ ├── carrier.go │ ├── carrier_test.go │ ├── websocket.go │ └── websocket_test.go ├── catalog-info.yaml ├── cfapi/ │ ├── base_client.go │ ├── client.go │ ├── hostname.go │ ├── hostname_test.go │ ├── ip_route.go │ ├── ip_route_filter.go │ ├── ip_route_test.go │ ├── tunnel.go │ ├── tunnel_filter.go │ ├── tunnel_test.go │ ├── virtual_network.go │ ├── virtual_network_filter.go │ └── virtual_network_test.go ├── cfio/ │ └── copy.go ├── cfsetup.yaml ├── check-fips.sh ├── client/ │ ├── config.go │ └── config_test.go ├── cloudflared.wxs ├── cloudflared_man_template ├── cmd/ │ └── cloudflared/ │ ├── access/ │ │ ├── carrier.go │ │ ├── cmd.go │ │ ├── validation.go │ │ └── validation_test.go │ ├── app_forward_service.go │ ├── app_service.go │ ├── cliutil/ │ │ ├── build_info.go │ │ ├── deprecated.go │ │ ├── errors.go │ │ ├── handler.go │ │ ├── logger.go │ │ └── management.go │ ├── common_service.go │ ├── flags/ │ │ └── flags.go │ ├── generic_service.go │ ├── linux_service.go │ ├── macos_service.go │ ├── main.go │ ├── management/ │ │ ├── cmd.go │ │ └── cmd_test.go │ ├── proxydns/ │ │ └── cmd.go │ ├── service_template.go │ ├── tail/ │ │ └── cmd.go │ ├── tunnel/ │ │ ├── cmd.go │ │ ├── cmd_test.go │ │ ├── configuration.go │ │ ├── configuration_test.go │ │ ├── credential_finder.go │ │ ├── filesystem.go │ │ ├── info.go │ │ ├── ingress_subcommands.go │ │ ├── login.go │ │ ├── quick_tunnel.go │ │ ├── signal.go │ │ ├── signal_test.go │ │ ├── subcommand_context.go │ │ ├── subcommand_context_teamnet.go │ │ ├── subcommand_context_test.go │ │ ├── subcommand_context_vnets.go │ │ ├── subcommands.go │ │ ├── subcommands_test.go │ │ ├── tag.go │ │ ├── tag_test.go │ │ ├── teamnet_subcommands.go │ │ └── vnets_subcommands.go │ ├── updater/ │ │ ├── check.go │ │ ├── service.go │ │ ├── update.go │ │ ├── update_test.go │ │ ├── workers_service.go │ │ ├── workers_service_test.go │ │ └── workers_update.go │ └── windows_service.go ├── component-tests/ │ ├── .gitignore │ ├── README.md │ ├── cli.py │ ├── config.py │ ├── config.yaml │ ├── conftest.py │ ├── constants.py │ ├── requirements.txt │ ├── setup.py │ ├── test_config.py │ ├── test_edge_discovery.py │ ├── test_logging.py │ ├── test_management.py │ ├── test_pq.py │ ├── test_quicktunnels.py │ ├── test_reconnect.py │ ├── test_service.py │ ├── test_tail.py │ ├── test_termination.py │ ├── test_token.py │ ├── test_tunnel.py │ └── util.py ├── config/ │ ├── configuration.go │ ├── configuration_test.go │ ├── manager.go │ ├── manager_test.go │ └── model.go ├── connection/ │ ├── connection.go │ ├── connection_test.go │ ├── control.go │ ├── errors.go │ ├── event.go │ ├── header.go │ ├── header_test.go │ ├── http2.go │ ├── http2_test.go │ ├── json.go │ ├── metrics.go │ ├── observer.go │ ├── observer_test.go │ ├── protocol.go │ ├── protocol_test.go │ ├── quic.go │ ├── quic_connection.go │ ├── quic_connection_test.go │ ├── quic_datagram_v2.go │ ├── quic_datagram_v2_test.go │ ├── quic_datagram_v3.go │ └── tunnelsforha.go ├── credentials/ │ ├── credentials.go │ ├── credentials_test.go │ ├── origin_cert.go │ ├── origin_cert_test.go │ ├── test-cert-no-token.pem │ ├── test-cert-unknown-block.pem │ └── test-cloudflare-tunnel-cert-json.pem ├── datagramsession/ │ ├── event.go │ ├── manager.go │ ├── manager_test.go │ ├── metrics.go │ ├── session.go │ └── session_test.go ├── diagnostic/ │ ├── client.go │ ├── consts.go │ ├── diagnostic.go │ ├── diagnostic_utils.go │ ├── diagnostic_utils_test.go │ ├── error.go │ ├── handlers.go │ ├── handlers_test.go │ ├── log_collector.go │ ├── log_collector_docker.go │ ├── log_collector_host.go │ ├── log_collector_kubernetes.go │ ├── log_collector_utils.go │ ├── network/ │ │ ├── collector.go │ │ ├── collector_unix.go │ │ ├── collector_unix_test.go │ │ ├── collector_utils.go │ │ ├── collector_windows.go │ │ └── collector_windows_test.go │ ├── system_collector.go │ ├── system_collector_linux.go │ ├── system_collector_macos.go │ ├── system_collector_test.go │ ├── system_collector_utils.go │ └── system_collector_windows.go ├── edgediscovery/ │ ├── allregions/ │ │ ├── address.go │ │ ├── address_test.go │ │ ├── discovery.go │ │ ├── discovery_test.go │ │ ├── mocks_for_test.go │ │ ├── region.go │ │ ├── region_test.go │ │ ├── regions.go │ │ ├── regions_test.go │ │ └── usedby.go │ ├── dial.go │ ├── edgediscovery.go │ ├── edgediscovery_test.go │ ├── mocks_for_test.go │ ├── protocol.go │ └── protocol_test.go ├── features/ │ ├── features.go │ ├── selector.go │ └── selector_test.go ├── fips/ │ ├── fips.go │ └── nofips.go ├── flow/ │ ├── limiter.go │ ├── limiter_test.go │ └── metrics.go ├── github_message.py ├── github_release.py ├── go.mod ├── go.sum ├── hello/ │ ├── hello.go │ └── hello_test.go ├── ingress/ │ ├── config.go │ ├── config_test.go │ ├── constants_test.go │ ├── icmp_darwin.go │ ├── icmp_darwin_test.go │ ├── icmp_generic.go │ ├── icmp_linux.go │ ├── icmp_linux_test.go │ ├── icmp_metrics.go │ ├── icmp_posix.go │ ├── icmp_posix_test.go │ ├── icmp_windows.go │ ├── icmp_windows_test.go │ ├── ingress.go │ ├── ingress_test.go │ ├── middleware/ │ │ ├── jwtvalidator.go │ │ ├── jwtvalidator_test.go │ │ └── middleware.go │ ├── origin_connection.go │ ├── origin_connection_test.go │ ├── origin_dialer.go │ ├── origin_icmp_proxy.go │ ├── origin_icmp_proxy_test.go │ ├── origin_proxy.go │ ├── origin_proxy_test.go │ ├── origin_service.go │ ├── origin_service_test.go │ ├── origins/ │ │ ├── dns.go │ │ ├── dns_test.go │ │ ├── metrics.go │ │ └── metrics_test.go │ ├── packet_router.go │ ├── packet_router_test.go │ ├── rule.go │ └── rule_test.go ├── internal/ │ └── test/ │ └── wstest.go ├── ipaccess/ │ ├── access.go │ └── access_test.go ├── logger/ │ ├── configuration.go │ ├── console.go │ ├── console_test.go │ ├── create.go │ └── create_test.go ├── management/ │ ├── events.go │ ├── events_test.go │ ├── logger.go │ ├── logger_test.go │ ├── middleware.go │ ├── middleware_test.go │ ├── service.go │ ├── service_test.go │ ├── session.go │ ├── session_test.go │ ├── token.go │ └── token_test.go ├── metrics/ │ ├── config.go │ ├── metrics.go │ ├── metrics_test.go │ ├── readiness.go │ └── readiness_test.go ├── mocks/ │ ├── mock_limiter.go │ └── mockgen.go ├── orchestration/ │ ├── config.go │ ├── config_test.go │ ├── metrics.go │ ├── orchestrator.go │ └── orchestrator_test.go ├── overwatch/ │ ├── app_manager.go │ ├── manager.go │ └── manager_test.go ├── packet/ │ ├── decoder.go │ ├── decoder_test.go │ ├── encoder.go │ ├── funnel.go │ ├── funnel_test.go │ ├── packet.go │ ├── packet_test.go │ └── session.go ├── postinst.sh ├── postrm.sh ├── proxy/ │ ├── logger.go │ ├── metrics.go │ ├── proxy.go │ ├── proxy_posix_test.go │ └── proxy_test.go ├── quic/ │ ├── constants.go │ ├── conversion.go │ ├── datagram.go │ ├── datagram_test.go │ ├── datagramv2.go │ ├── metrics.go │ ├── param_unix.go │ ├── param_windows.go │ ├── safe_stream.go │ ├── safe_stream_test.go │ ├── tracing.go │ └── v3/ │ ├── datagram.go │ ├── datagram_errors.go │ ├── datagram_test.go │ ├── icmp.go │ ├── icmp_test.go │ ├── manager.go │ ├── manager_test.go │ ├── metrics.go │ ├── metrics_test.go │ ├── muxer.go │ ├── muxer_test.go │ ├── request.go │ ├── request_test.go │ ├── session.go │ ├── session_fuzz_test.go │ └── session_test.go ├── release/ │ └── index.html ├── release_pkgs.py ├── retry/ │ ├── backoffhandler.go │ └── backoffhandler_test.go ├── signal/ │ ├── safe_signal.go │ └── safe_signal_test.go ├── socks/ │ ├── auth_handler.go │ ├── authenticator.go │ ├── connection_handler.go │ ├── connection_handler_test.go │ ├── dialer.go │ ├── request.go │ ├── request_handler.go │ ├── request_handler_test.go │ └── request_test.go ├── sshgen/ │ ├── sshgen.go │ └── sshgen_test.go ├── stream/ │ ├── debug.go │ ├── stream.go │ └── stream_test.go ├── supervisor/ │ ├── conn_aware_logger.go │ ├── external_control.go │ ├── fuse.go │ ├── metrics.go │ ├── pqtunnels.go │ ├── pqtunnels_test.go │ ├── supervisor.go │ ├── tunnel.go │ ├── tunnel_test.go │ └── tunnelsforha.go ├── tlsconfig/ │ ├── certreloader.go │ ├── cloudflare_ca.go │ ├── hello_ca.go │ ├── testcert.pem │ ├── testcert2.pem │ ├── testkey.pem │ ├── tlsconfig.go │ └── tlsconfig_test.go ├── token/ │ ├── encrypt.go │ ├── launch_browser_darwin.go │ ├── launch_browser_other.go │ ├── launch_browser_unix.go │ ├── launch_browser_windows.go │ ├── path.go │ ├── shell.go │ ├── signal_test.go │ ├── token.go │ ├── token_test.go │ └── transfer.go ├── tracing/ │ ├── client.go │ ├── client_test.go │ ├── identity.go │ ├── identity_test.go │ ├── tracing.go │ └── tracing_test.go ├── tunnelrpc/ │ ├── metrics/ │ │ └── metrics.go │ ├── pogs/ │ │ ├── cloudflared_server.go │ │ ├── configuration_manager.go │ │ ├── errors.go │ │ ├── quic_metadata_protocol.go │ │ ├── registration_server.go │ │ ├── registration_server_test.go │ │ ├── session_manager.go │ │ └── tag.go │ ├── proto/ │ │ ├── go.capnp │ │ ├── quic_metadata_protocol.capnp │ │ ├── quic_metadata_protocol.capnp.go │ │ ├── tunnelrpc.capnp │ │ └── tunnelrpc.capnp.go │ ├── quic/ │ │ ├── cloudflared_client.go │ │ ├── cloudflared_server.go │ │ ├── protocol.go │ │ ├── request_client_stream.go │ │ ├── request_server_stream.go │ │ ├── request_server_stream_test.go │ │ ├── session_client.go │ │ └── session_server.go │ ├── registration_client.go │ ├── registration_server.go │ └── utils.go ├── tunnelstate/ │ └── conntracker.go ├── validation/ │ ├── validation.go │ └── validation_test.go ├── vendor/ │ ├── github.com/ │ │ ├── BurntSushi/ │ │ │ └── toml/ │ │ │ ├── .gitignore │ │ │ ├── COPYING │ │ │ ├── README.md │ │ │ ├── decode.go │ │ │ ├── decode_go116.go │ │ │ ├── deprecated.go │ │ │ ├── doc.go │ │ │ ├── encode.go │ │ │ ├── error.go │ │ │ ├── internal/ │ │ │ │ └── tz.go │ │ │ ├── lex.go │ │ │ ├── meta.go │ │ │ ├── parse.go │ │ │ ├── type_fields.go │ │ │ └── type_toml.go │ │ ├── beorn7/ │ │ │ └── perks/ │ │ │ ├── LICENSE │ │ │ └── quantile/ │ │ │ ├── exampledata.txt │ │ │ └── stream.go │ │ ├── cespare/ │ │ │ └── xxhash/ │ │ │ └── v2/ │ │ │ ├── LICENSE.txt │ │ │ ├── README.md │ │ │ ├── testall.sh │ │ │ ├── xxhash.go │ │ │ ├── xxhash_amd64.s │ │ │ ├── xxhash_arm64.s │ │ │ ├── xxhash_asm.go │ │ │ ├── xxhash_other.go │ │ │ ├── xxhash_safe.go │ │ │ └── xxhash_unsafe.go │ │ ├── coreos/ │ │ │ ├── go-oidc/ │ │ │ │ └── v3/ │ │ │ │ ├── LICENSE │ │ │ │ ├── NOTICE │ │ │ │ └── oidc/ │ │ │ │ ├── jose.go │ │ │ │ ├── jwks.go │ │ │ │ ├── oidc.go │ │ │ │ └── verify.go │ │ │ └── go-systemd/ │ │ │ └── v22/ │ │ │ ├── LICENSE │ │ │ ├── NOTICE │ │ │ └── daemon/ │ │ │ ├── sdnotify.go │ │ │ └── watchdog.go │ │ ├── cpuguy83/ │ │ │ └── go-md2man/ │ │ │ └── v2/ │ │ │ ├── LICENSE.md │ │ │ └── md2man/ │ │ │ ├── md2man.go │ │ │ └── roff.go │ │ ├── davecgh/ │ │ │ └── go-spew/ │ │ │ ├── LICENSE │ │ │ └── spew/ │ │ │ ├── bypass.go │ │ │ ├── bypasssafe.go │ │ │ ├── common.go │ │ │ ├── config.go │ │ │ ├── doc.go │ │ │ ├── dump.go │ │ │ ├── format.go │ │ │ └── spew.go │ │ ├── facebookgo/ │ │ │ └── grace/ │ │ │ └── gracenet/ │ │ │ └── net.go │ │ ├── fortytw2/ │ │ │ └── leaktest/ │ │ │ ├── .travis.yml │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ └── leaktest.go │ │ ├── fsnotify/ │ │ │ └── fsnotify/ │ │ │ ├── .editorconfig │ │ │ ├── .gitattributes │ │ │ ├── .gitignore │ │ │ ├── .travis.yml │ │ │ ├── AUTHORS │ │ │ ├── CHANGELOG.md │ │ │ ├── CONTRIBUTING.md │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── fen.go │ │ │ ├── fsnotify.go │ │ │ ├── inotify.go │ │ │ ├── inotify_poller.go │ │ │ ├── kqueue.go │ │ │ ├── open_mode_bsd.go │ │ │ ├── open_mode_darwin.go │ │ │ └── windows.go │ │ ├── getsentry/ │ │ │ └── sentry-go/ │ │ │ ├── .codecov.yml │ │ │ ├── .craft.yml │ │ │ ├── .gitattributes │ │ │ ├── .gitignore │ │ │ ├── .golangci.yml │ │ │ ├── CHANGELOG.md │ │ │ ├── CONTRIBUTING.md │ │ │ ├── LICENSE │ │ │ ├── MIGRATION.md │ │ │ ├── Makefile │ │ │ ├── README.md │ │ │ ├── attribute/ │ │ │ │ ├── builder.go │ │ │ │ ├── rawhelpers.go │ │ │ │ └── value.go │ │ │ ├── batch_processor.go │ │ │ ├── check_in.go │ │ │ ├── client.go │ │ │ ├── doc.go │ │ │ ├── dsn.go │ │ │ ├── dynamic_sampling_context.go │ │ │ ├── exception.go │ │ │ ├── hub.go │ │ │ ├── integrations.go │ │ │ ├── interfaces.go │ │ │ ├── internal/ │ │ │ │ ├── debug/ │ │ │ │ │ └── transport.go │ │ │ │ ├── debuglog/ │ │ │ │ │ └── log.go │ │ │ │ ├── http/ │ │ │ │ │ └── transport.go │ │ │ │ ├── otel/ │ │ │ │ │ └── baggage/ │ │ │ │ │ ├── README.md │ │ │ │ │ ├── baggage.go │ │ │ │ │ └── internal/ │ │ │ │ │ └── baggage/ │ │ │ │ │ └── baggage.go │ │ │ │ ├── protocol/ │ │ │ │ │ ├── dsn.go │ │ │ │ │ ├── envelope.go │ │ │ │ │ ├── interfaces.go │ │ │ │ │ ├── log_batch.go │ │ │ │ │ ├── metric_batch.go │ │ │ │ │ ├── types.go │ │ │ │ │ └── uuid.go │ │ │ │ ├── ratelimit/ │ │ │ │ │ ├── category.go │ │ │ │ │ ├── deadline.go │ │ │ │ │ ├── doc.go │ │ │ │ │ ├── map.go │ │ │ │ │ ├── rate_limits.go │ │ │ │ │ └── retry_after.go │ │ │ │ ├── telemetry/ │ │ │ │ │ ├── bucketed_buffer.go │ │ │ │ │ ├── buffer.go │ │ │ │ │ ├── processor.go │ │ │ │ │ ├── ring_buffer.go │ │ │ │ │ ├── scheduler.go │ │ │ │ │ └── trace_aware.go │ │ │ │ └── util/ │ │ │ │ ├── map.go │ │ │ │ └── util.go │ │ │ ├── log.go │ │ │ ├── log_batch_processor.go │ │ │ ├── log_fallback.go │ │ │ ├── metric_batch_processor.go │ │ │ ├── metrics.go │ │ │ ├── mocks.go │ │ │ ├── propagation_context.go │ │ │ ├── scope.go │ │ │ ├── sentry.go │ │ │ ├── sourcereader.go │ │ │ ├── span_recorder.go │ │ │ ├── stacktrace.go │ │ │ ├── traces_sampler.go │ │ │ ├── tracing.go │ │ │ ├── transport.go │ │ │ └── util.go │ │ ├── go-chi/ │ │ │ ├── chi/ │ │ │ │ └── v5/ │ │ │ │ ├── .gitignore │ │ │ │ ├── CHANGELOG.md │ │ │ │ ├── CONTRIBUTING.md │ │ │ │ ├── LICENSE │ │ │ │ ├── Makefile │ │ │ │ ├── README.md │ │ │ │ ├── SECURITY.md │ │ │ │ ├── chain.go │ │ │ │ ├── chi.go │ │ │ │ ├── context.go │ │ │ │ ├── mux.go │ │ │ │ ├── path_value.go │ │ │ │ ├── path_value_fallback.go │ │ │ │ └── tree.go │ │ │ └── cors/ │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── cors.go │ │ │ └── utils.go │ │ ├── go-jose/ │ │ │ └── go-jose/ │ │ │ └── v4/ │ │ │ ├── .gitignore │ │ │ ├── .golangci.yml │ │ │ ├── .travis.yml │ │ │ ├── CONTRIBUTING.md │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── SECURITY.md │ │ │ ├── asymmetric.go │ │ │ ├── cipher/ │ │ │ │ ├── cbc_hmac.go │ │ │ │ ├── concat_kdf.go │ │ │ │ ├── ecdh_es.go │ │ │ │ └── key_wrap.go │ │ │ ├── crypter.go │ │ │ ├── doc.go │ │ │ ├── encoding.go │ │ │ ├── json/ │ │ │ │ ├── LICENSE │ │ │ │ ├── README.md │ │ │ │ ├── decode.go │ │ │ │ ├── encode.go │ │ │ │ ├── indent.go │ │ │ │ ├── scanner.go │ │ │ │ ├── stream.go │ │ │ │ └── tags.go │ │ │ ├── jwe.go │ │ │ ├── jwk.go │ │ │ ├── jws.go │ │ │ ├── jwt/ │ │ │ │ ├── builder.go │ │ │ │ ├── claims.go │ │ │ │ ├── doc.go │ │ │ │ ├── errors.go │ │ │ │ ├── jwt.go │ │ │ │ └── validation.go │ │ │ ├── opaque.go │ │ │ ├── shared.go │ │ │ ├── signing.go │ │ │ └── symmetric.go │ │ ├── go-logr/ │ │ │ ├── logr/ │ │ │ │ ├── .golangci.yaml │ │ │ │ ├── CHANGELOG.md │ │ │ │ ├── CONTRIBUTING.md │ │ │ │ ├── LICENSE │ │ │ │ ├── README.md │ │ │ │ ├── SECURITY.md │ │ │ │ ├── context.go │ │ │ │ ├── context_noslog.go │ │ │ │ ├── context_slog.go │ │ │ │ ├── discard.go │ │ │ │ ├── funcr/ │ │ │ │ │ ├── funcr.go │ │ │ │ │ └── slogsink.go │ │ │ │ ├── logr.go │ │ │ │ ├── sloghandler.go │ │ │ │ ├── slogr.go │ │ │ │ └── slogsink.go │ │ │ └── stdr/ │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ └── stdr.go │ │ ├── gobwas/ │ │ │ ├── httphead/ │ │ │ │ ├── LICENSE │ │ │ │ ├── README.md │ │ │ │ ├── cookie.go │ │ │ │ ├── head.go │ │ │ │ ├── httphead.go │ │ │ │ ├── lexer.go │ │ │ │ ├── octet.go │ │ │ │ ├── option.go │ │ │ │ └── writer.go │ │ │ ├── pool/ │ │ │ │ ├── LICENSE │ │ │ │ ├── README.md │ │ │ │ ├── generic.go │ │ │ │ ├── internal/ │ │ │ │ │ └── pmath/ │ │ │ │ │ └── pmath.go │ │ │ │ ├── option.go │ │ │ │ ├── pbufio/ │ │ │ │ │ ├── pbufio.go │ │ │ │ │ ├── pbufio_go110.go │ │ │ │ │ └── pbufio_go19.go │ │ │ │ ├── pbytes/ │ │ │ │ │ ├── pbytes.go │ │ │ │ │ ├── pool.go │ │ │ │ │ └── pool_sanitize.go │ │ │ │ └── pool.go │ │ │ └── ws/ │ │ │ ├── .gitignore │ │ │ ├── LICENSE │ │ │ ├── Makefile │ │ │ ├── README.md │ │ │ ├── check.go │ │ │ ├── cipher.go │ │ │ ├── dialer.go │ │ │ ├── dialer_tls_go17.go │ │ │ ├── dialer_tls_go18.go │ │ │ ├── doc.go │ │ │ ├── errors.go │ │ │ ├── frame.go │ │ │ ├── http.go │ │ │ ├── nonce.go │ │ │ ├── read.go │ │ │ ├── server.go │ │ │ ├── util.go │ │ │ ├── util_purego.go │ │ │ ├── util_unsafe.go │ │ │ ├── write.go │ │ │ └── wsutil/ │ │ │ ├── cipher.go │ │ │ ├── dialer.go │ │ │ ├── extenstion.go │ │ │ ├── handler.go │ │ │ ├── helper.go │ │ │ ├── reader.go │ │ │ ├── upgrader.go │ │ │ ├── utf8.go │ │ │ ├── writer.go │ │ │ └── wsutil.go │ │ ├── google/ │ │ │ ├── gopacket/ │ │ │ │ ├── .gitignore │ │ │ │ ├── .travis.gofmt.sh │ │ │ │ ├── .travis.golint.sh │ │ │ │ ├── .travis.govet.sh │ │ │ │ ├── .travis.install.sh │ │ │ │ ├── .travis.script.sh │ │ │ │ ├── .travis.yml │ │ │ │ ├── AUTHORS │ │ │ │ ├── CONTRIBUTING.md │ │ │ │ ├── LICENSE │ │ │ │ ├── README.md │ │ │ │ ├── base.go │ │ │ │ ├── decode.go │ │ │ │ ├── doc.go │ │ │ │ ├── flows.go │ │ │ │ ├── gc │ │ │ │ ├── layerclass.go │ │ │ │ ├── layers/ │ │ │ │ │ ├── .lint_blacklist │ │ │ │ │ ├── arp.go │ │ │ │ │ ├── asf.go │ │ │ │ │ ├── asf_presencepong.go │ │ │ │ │ ├── base.go │ │ │ │ │ ├── bfd.go │ │ │ │ │ ├── cdp.go │ │ │ │ │ ├── ctp.go │ │ │ │ │ ├── dhcpv4.go │ │ │ │ │ ├── dhcpv6.go │ │ │ │ │ ├── dhcpv6_options.go │ │ │ │ │ ├── dns.go │ │ │ │ │ ├── doc.go │ │ │ │ │ ├── dot11.go │ │ │ │ │ ├── dot1q.go │ │ │ │ │ ├── eap.go │ │ │ │ │ ├── eapol.go │ │ │ │ │ ├── endpoints.go │ │ │ │ │ ├── enums.go │ │ │ │ │ ├── enums_generated.go │ │ │ │ │ ├── erspan2.go │ │ │ │ │ ├── etherip.go │ │ │ │ │ ├── ethernet.go │ │ │ │ │ ├── fddi.go │ │ │ │ │ ├── fuzz_layer.go │ │ │ │ │ ├── gen_linted.sh │ │ │ │ │ ├── geneve.go │ │ │ │ │ ├── gre.go │ │ │ │ │ ├── gtp.go │ │ │ │ │ ├── iana_ports.go │ │ │ │ │ ├── icmp4.go │ │ │ │ │ ├── icmp6.go │ │ │ │ │ ├── icmp6msg.go │ │ │ │ │ ├── igmp.go │ │ │ │ │ ├── ip4.go │ │ │ │ │ ├── ip6.go │ │ │ │ │ ├── ipsec.go │ │ │ │ │ ├── layertypes.go │ │ │ │ │ ├── lcm.go │ │ │ │ │ ├── linux_sll.go │ │ │ │ │ ├── llc.go │ │ │ │ │ ├── lldp.go │ │ │ │ │ ├── loopback.go │ │ │ │ │ ├── mldv1.go │ │ │ │ │ ├── mldv2.go │ │ │ │ │ ├── modbustcp.go │ │ │ │ │ ├── mpls.go │ │ │ │ │ ├── ndp.go │ │ │ │ │ ├── ntp.go │ │ │ │ │ ├── ospf.go │ │ │ │ │ ├── pflog.go │ │ │ │ │ ├── ports.go │ │ │ │ │ ├── ppp.go │ │ │ │ │ ├── pppoe.go │ │ │ │ │ ├── prism.go │ │ │ │ │ ├── radiotap.go │ │ │ │ │ ├── radius.go │ │ │ │ │ ├── rmcp.go │ │ │ │ │ ├── rudp.go │ │ │ │ │ ├── sctp.go │ │ │ │ │ ├── sflow.go │ │ │ │ │ ├── sip.go │ │ │ │ │ ├── stp.go │ │ │ │ │ ├── tcp.go │ │ │ │ │ ├── tcpip.go │ │ │ │ │ ├── test_creator.py │ │ │ │ │ ├── tls.go │ │ │ │ │ ├── tls_alert.go │ │ │ │ │ ├── tls_appdata.go │ │ │ │ │ ├── tls_cipherspec.go │ │ │ │ │ ├── tls_handshake.go │ │ │ │ │ ├── udp.go │ │ │ │ │ ├── udplite.go │ │ │ │ │ ├── usb.go │ │ │ │ │ ├── vrrp.go │ │ │ │ │ └── vxlan.go │ │ │ │ ├── layers_decoder.go │ │ │ │ ├── layertype.go │ │ │ │ ├── packet.go │ │ │ │ ├── parser.go │ │ │ │ ├── time.go │ │ │ │ └── writer.go │ │ │ ├── pprof/ │ │ │ │ ├── AUTHORS │ │ │ │ ├── CONTRIBUTORS │ │ │ │ ├── LICENSE │ │ │ │ └── profile/ │ │ │ │ ├── encode.go │ │ │ │ ├── filter.go │ │ │ │ ├── index.go │ │ │ │ ├── legacy_java_profile.go │ │ │ │ ├── legacy_profile.go │ │ │ │ ├── merge.go │ │ │ │ ├── profile.go │ │ │ │ ├── proto.go │ │ │ │ └── prune.go │ │ │ └── uuid/ │ │ │ ├── CHANGELOG.md │ │ │ ├── CONTRIBUTING.md │ │ │ ├── CONTRIBUTORS │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── dce.go │ │ │ ├── doc.go │ │ │ ├── hash.go │ │ │ ├── marshal.go │ │ │ ├── node.go │ │ │ ├── node_js.go │ │ │ ├── node_net.go │ │ │ ├── null.go │ │ │ ├── sql.go │ │ │ ├── time.go │ │ │ ├── util.go │ │ │ ├── uuid.go │ │ │ ├── version1.go │ │ │ ├── version4.go │ │ │ ├── version6.go │ │ │ └── version7.go │ │ ├── gorilla/ │ │ │ └── websocket/ │ │ │ ├── .gitignore │ │ │ ├── AUTHORS │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── client.go │ │ │ ├── compression.go │ │ │ ├── conn.go │ │ │ ├── doc.go │ │ │ ├── join.go │ │ │ ├── json.go │ │ │ ├── mask.go │ │ │ ├── mask_safe.go │ │ │ ├── prepared.go │ │ │ ├── proxy.go │ │ │ ├── server.go │ │ │ ├── tls_handshake.go │ │ │ ├── tls_handshake_116.go │ │ │ ├── util.go │ │ │ └── x_net_proxy.go │ │ ├── grpc-ecosystem/ │ │ │ └── grpc-gateway/ │ │ │ └── v2/ │ │ │ ├── LICENSE │ │ │ ├── internal/ │ │ │ │ └── httprule/ │ │ │ │ ├── BUILD.bazel │ │ │ │ ├── compile.go │ │ │ │ ├── fuzz.go │ │ │ │ ├── parse.go │ │ │ │ └── types.go │ │ │ ├── runtime/ │ │ │ │ ├── BUILD.bazel │ │ │ │ ├── context.go │ │ │ │ ├── convert.go │ │ │ │ ├── doc.go │ │ │ │ ├── errors.go │ │ │ │ ├── fieldmask.go │ │ │ │ ├── handler.go │ │ │ │ ├── marshal_httpbodyproto.go │ │ │ │ ├── marshal_json.go │ │ │ │ ├── marshal_jsonpb.go │ │ │ │ ├── marshal_proto.go │ │ │ │ ├── marshaler.go │ │ │ │ ├── marshaler_registry.go │ │ │ │ ├── mux.go │ │ │ │ ├── pattern.go │ │ │ │ ├── proto2_convert.go │ │ │ │ └── query.go │ │ │ └── utilities/ │ │ │ ├── BUILD.bazel │ │ │ ├── doc.go │ │ │ ├── pattern.go │ │ │ ├── readerfactory.go │ │ │ ├── string_array_flag.go │ │ │ └── trie.go │ │ ├── klauspost/ │ │ │ └── compress/ │ │ │ ├── LICENSE │ │ │ ├── flate/ │ │ │ │ ├── deflate.go │ │ │ │ ├── dict_decoder.go │ │ │ │ ├── fast_encoder.go │ │ │ │ ├── huffman_bit_writer.go │ │ │ │ ├── huffman_code.go │ │ │ │ ├── huffman_sortByFreq.go │ │ │ │ ├── huffman_sortByLiteral.go │ │ │ │ ├── inflate.go │ │ │ │ ├── inflate_gen.go │ │ │ │ ├── level1.go │ │ │ │ ├── level2.go │ │ │ │ ├── level3.go │ │ │ │ ├── level4.go │ │ │ │ ├── level5.go │ │ │ │ ├── level6.go │ │ │ │ ├── matchlen_generic.go │ │ │ │ ├── regmask_amd64.go │ │ │ │ ├── regmask_other.go │ │ │ │ ├── stateless.go │ │ │ │ └── token.go │ │ │ └── internal/ │ │ │ └── le/ │ │ │ ├── le.go │ │ │ ├── unsafe_disabled.go │ │ │ └── unsafe_enabled.go │ │ ├── mattn/ │ │ │ ├── go-colorable/ │ │ │ │ ├── LICENSE │ │ │ │ ├── README.md │ │ │ │ ├── colorable_appengine.go │ │ │ │ ├── colorable_others.go │ │ │ │ ├── colorable_windows.go │ │ │ │ ├── go.test.sh │ │ │ │ └── noncolorable.go │ │ │ └── go-isatty/ │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── doc.go │ │ │ ├── go.test.sh │ │ │ ├── isatty_bsd.go │ │ │ ├── isatty_others.go │ │ │ ├── isatty_plan9.go │ │ │ ├── isatty_solaris.go │ │ │ ├── isatty_tcgets.go │ │ │ └── isatty_windows.go │ │ ├── mitchellh/ │ │ │ └── go-homedir/ │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ └── homedir.go │ │ ├── modern-go/ │ │ │ └── concurrent/ │ │ │ ├── .gitignore │ │ │ ├── .travis.yml │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── executor.go │ │ │ ├── go_above_19.go │ │ │ ├── go_below_19.go │ │ │ ├── log.go │ │ │ ├── test.sh │ │ │ └── unbounded_executor.go │ │ ├── munnerz/ │ │ │ └── goautoneg/ │ │ │ ├── LICENSE │ │ │ ├── Makefile │ │ │ ├── README.txt │ │ │ └── autoneg.go │ │ ├── onsi/ │ │ │ └── ginkgo/ │ │ │ └── v2/ │ │ │ ├── LICENSE │ │ │ ├── config/ │ │ │ │ └── deprecated.go │ │ │ ├── formatter/ │ │ │ │ ├── colorable_others.go │ │ │ │ ├── colorable_windows.go │ │ │ │ └── formatter.go │ │ │ ├── ginkgo/ │ │ │ │ ├── build/ │ │ │ │ │ └── build_command.go │ │ │ │ ├── command/ │ │ │ │ │ ├── abort.go │ │ │ │ │ ├── command.go │ │ │ │ │ └── program.go │ │ │ │ ├── generators/ │ │ │ │ │ ├── boostrap_templates.go │ │ │ │ │ ├── bootstrap_command.go │ │ │ │ │ ├── generate_command.go │ │ │ │ │ ├── generate_templates.go │ │ │ │ │ └── generators_common.go │ │ │ │ ├── internal/ │ │ │ │ │ ├── compile.go │ │ │ │ │ ├── gocovmerge.go │ │ │ │ │ ├── profiles_and_reports.go │ │ │ │ │ ├── run.go │ │ │ │ │ ├── test_suite.go │ │ │ │ │ ├── utils.go │ │ │ │ │ └── verify_version.go │ │ │ │ ├── labels/ │ │ │ │ │ └── labels_command.go │ │ │ │ ├── main.go │ │ │ │ ├── outline/ │ │ │ │ │ ├── ginkgo.go │ │ │ │ │ ├── import.go │ │ │ │ │ ├── outline.go │ │ │ │ │ └── outline_command.go │ │ │ │ ├── run/ │ │ │ │ │ └── run_command.go │ │ │ │ ├── unfocus/ │ │ │ │ │ └── unfocus_command.go │ │ │ │ └── watch/ │ │ │ │ ├── delta.go │ │ │ │ ├── delta_tracker.go │ │ │ │ ├── dependencies.go │ │ │ │ ├── package_hash.go │ │ │ │ ├── package_hashes.go │ │ │ │ ├── suite.go │ │ │ │ └── watch_command.go │ │ │ ├── internal/ │ │ │ │ ├── interrupt_handler/ │ │ │ │ │ ├── interrupt_handler.go │ │ │ │ │ ├── sigquit_swallower_unix.go │ │ │ │ │ └── sigquit_swallower_windows.go │ │ │ │ └── parallel_support/ │ │ │ │ ├── client_server.go │ │ │ │ ├── http_client.go │ │ │ │ ├── http_server.go │ │ │ │ ├── rpc_client.go │ │ │ │ ├── rpc_server.go │ │ │ │ └── server_handler.go │ │ │ ├── reporters/ │ │ │ │ ├── default_reporter.go │ │ │ │ ├── deprecated_reporter.go │ │ │ │ ├── json_report.go │ │ │ │ ├── junit_report.go │ │ │ │ ├── reporter.go │ │ │ │ └── teamcity_report.go │ │ │ └── types/ │ │ │ ├── code_location.go │ │ │ ├── config.go │ │ │ ├── deprecated_types.go │ │ │ ├── deprecation_support.go │ │ │ ├── enum_support.go │ │ │ ├── errors.go │ │ │ ├── file_filter.go │ │ │ ├── flags.go │ │ │ ├── label_filter.go │ │ │ ├── report_entry.go │ │ │ ├── types.go │ │ │ └── version.go │ │ ├── pkg/ │ │ │ └── errors/ │ │ │ ├── .gitignore │ │ │ ├── .travis.yml │ │ │ ├── LICENSE │ │ │ ├── Makefile │ │ │ ├── README.md │ │ │ ├── appveyor.yml │ │ │ ├── errors.go │ │ │ ├── go113.go │ │ │ └── stack.go │ │ ├── pmezard/ │ │ │ └── go-difflib/ │ │ │ ├── LICENSE │ │ │ └── difflib/ │ │ │ └── difflib.go │ │ ├── prometheus/ │ │ │ ├── client_golang/ │ │ │ │ ├── LICENSE │ │ │ │ ├── NOTICE │ │ │ │ ├── internal/ │ │ │ │ │ └── github.com/ │ │ │ │ │ └── golang/ │ │ │ │ │ └── gddo/ │ │ │ │ │ ├── LICENSE │ │ │ │ │ └── httputil/ │ │ │ │ │ ├── header/ │ │ │ │ │ │ └── header.go │ │ │ │ │ └── negotiate.go │ │ │ │ └── prometheus/ │ │ │ │ ├── .gitignore │ │ │ │ ├── README.md │ │ │ │ ├── build_info_collector.go │ │ │ │ ├── collector.go │ │ │ │ ├── collectorfunc.go │ │ │ │ ├── counter.go │ │ │ │ ├── desc.go │ │ │ │ ├── doc.go │ │ │ │ ├── expvar_collector.go │ │ │ │ ├── fnv.go │ │ │ │ ├── gauge.go │ │ │ │ ├── get_pid.go │ │ │ │ ├── get_pid_gopherjs.go │ │ │ │ ├── go_collector.go │ │ │ │ ├── go_collector_go116.go │ │ │ │ ├── go_collector_latest.go │ │ │ │ ├── histogram.go │ │ │ │ ├── internal/ │ │ │ │ │ ├── almost_equal.go │ │ │ │ │ ├── difflib.go │ │ │ │ │ ├── go_collector_options.go │ │ │ │ │ ├── go_runtime_metrics.go │ │ │ │ │ └── metric.go │ │ │ │ ├── labels.go │ │ │ │ ├── metric.go │ │ │ │ ├── num_threads.go │ │ │ │ ├── num_threads_gopherjs.go │ │ │ │ ├── observer.go │ │ │ │ ├── process_collector.go │ │ │ │ ├── process_collector_darwin.go │ │ │ │ ├── process_collector_mem_cgo_darwin.c │ │ │ │ ├── process_collector_mem_cgo_darwin.go │ │ │ │ ├── process_collector_mem_nocgo_darwin.go │ │ │ │ ├── process_collector_not_supported.go │ │ │ │ ├── process_collector_procfsenabled.go │ │ │ │ ├── process_collector_windows.go │ │ │ │ ├── promauto/ │ │ │ │ │ └── auto.go │ │ │ │ ├── promhttp/ │ │ │ │ │ ├── delegator.go │ │ │ │ │ ├── http.go │ │ │ │ │ ├── instrument_client.go │ │ │ │ │ ├── instrument_server.go │ │ │ │ │ ├── internal/ │ │ │ │ │ │ └── compression.go │ │ │ │ │ └── option.go │ │ │ │ ├── registry.go │ │ │ │ ├── summary.go │ │ │ │ ├── timer.go │ │ │ │ ├── untyped.go │ │ │ │ ├── value.go │ │ │ │ ├── vec.go │ │ │ │ ├── vnext.go │ │ │ │ └── wrap.go │ │ │ ├── client_model/ │ │ │ │ ├── LICENSE │ │ │ │ ├── NOTICE │ │ │ │ └── go/ │ │ │ │ └── metrics.pb.go │ │ │ ├── common/ │ │ │ │ ├── LICENSE │ │ │ │ ├── NOTICE │ │ │ │ ├── expfmt/ │ │ │ │ │ ├── decode.go │ │ │ │ │ ├── encode.go │ │ │ │ │ ├── expfmt.go │ │ │ │ │ ├── fuzz.go │ │ │ │ │ ├── openmetrics_create.go │ │ │ │ │ ├── text_create.go │ │ │ │ │ └── text_parse.go │ │ │ │ └── model/ │ │ │ │ ├── alert.go │ │ │ │ ├── fingerprinting.go │ │ │ │ ├── fnv.go │ │ │ │ ├── labels.go │ │ │ │ ├── labelset.go │ │ │ │ ├── labelset_string.go │ │ │ │ ├── metadata.go │ │ │ │ ├── metric.go │ │ │ │ ├── model.go │ │ │ │ ├── signature.go │ │ │ │ ├── silence.go │ │ │ │ ├── time.go │ │ │ │ ├── value.go │ │ │ │ ├── value_float.go │ │ │ │ ├── value_histogram.go │ │ │ │ └── value_type.go │ │ │ └── procfs/ │ │ │ ├── .gitignore │ │ │ ├── .golangci.yml │ │ │ ├── CODE_OF_CONDUCT.md │ │ │ ├── CONTRIBUTING.md │ │ │ ├── LICENSE │ │ │ ├── MAINTAINERS.md │ │ │ ├── Makefile │ │ │ ├── Makefile.common │ │ │ ├── NOTICE │ │ │ ├── README.md │ │ │ ├── SECURITY.md │ │ │ ├── arp.go │ │ │ ├── buddyinfo.go │ │ │ ├── cmdline.go │ │ │ ├── cpuinfo.go │ │ │ ├── cpuinfo_armx.go │ │ │ ├── cpuinfo_loong64.go │ │ │ ├── cpuinfo_mipsx.go │ │ │ ├── cpuinfo_others.go │ │ │ ├── cpuinfo_ppcx.go │ │ │ ├── cpuinfo_riscvx.go │ │ │ ├── cpuinfo_s390x.go │ │ │ ├── cpuinfo_x86.go │ │ │ ├── crypto.go │ │ │ ├── doc.go │ │ │ ├── fs.go │ │ │ ├── fs_statfs_notype.go │ │ │ ├── fs_statfs_type.go │ │ │ ├── fscache.go │ │ │ ├── internal/ │ │ │ │ ├── fs/ │ │ │ │ │ └── fs.go │ │ │ │ └── util/ │ │ │ │ ├── parse.go │ │ │ │ ├── readfile.go │ │ │ │ ├── sysreadfile.go │ │ │ │ ├── sysreadfile_compat.go │ │ │ │ └── valueparser.go │ │ │ ├── ipvs.go │ │ │ ├── kernel_random.go │ │ │ ├── loadavg.go │ │ │ ├── mdstat.go │ │ │ ├── meminfo.go │ │ │ ├── mountinfo.go │ │ │ ├── mountstats.go │ │ │ ├── net_conntrackstat.go │ │ │ ├── net_dev.go │ │ │ ├── net_ip_socket.go │ │ │ ├── net_protocols.go │ │ │ ├── net_route.go │ │ │ ├── net_sockstat.go │ │ │ ├── net_softnet.go │ │ │ ├── net_tcp.go │ │ │ ├── net_tls_stat.go │ │ │ ├── net_udp.go │ │ │ ├── net_unix.go │ │ │ ├── net_wireless.go │ │ │ ├── net_xfrm.go │ │ │ ├── netstat.go │ │ │ ├── proc.go │ │ │ ├── proc_cgroup.go │ │ │ ├── proc_cgroups.go │ │ │ ├── proc_environ.go │ │ │ ├── proc_fdinfo.go │ │ │ ├── proc_interrupts.go │ │ │ ├── proc_io.go │ │ │ ├── proc_limits.go │ │ │ ├── proc_maps.go │ │ │ ├── proc_netstat.go │ │ │ ├── proc_ns.go │ │ │ ├── proc_psi.go │ │ │ ├── proc_smaps.go │ │ │ ├── proc_snmp.go │ │ │ ├── proc_snmp6.go │ │ │ ├── proc_stat.go │ │ │ ├── proc_status.go │ │ │ ├── proc_sys.go │ │ │ ├── schedstat.go │ │ │ ├── slab.go │ │ │ ├── softirqs.go │ │ │ ├── stat.go │ │ │ ├── swaps.go │ │ │ ├── thread.go │ │ │ ├── ttar │ │ │ ├── vm.go │ │ │ └── zoneinfo.go │ │ ├── quic-go/ │ │ │ └── quic-go/ │ │ │ ├── .gitignore │ │ │ ├── .golangci.yml │ │ │ ├── Changelog.md │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── SECURITY.md │ │ │ ├── buffer_pool.go │ │ │ ├── client.go │ │ │ ├── closed_conn.go │ │ │ ├── codecov.yml │ │ │ ├── config.go │ │ │ ├── conn_id_generator.go │ │ │ ├── conn_id_manager.go │ │ │ ├── connection.go │ │ │ ├── connection_logging.go │ │ │ ├── connection_timer.go │ │ │ ├── crypto_stream.go │ │ │ ├── crypto_stream_manager.go │ │ │ ├── datagram_queue.go │ │ │ ├── errors.go │ │ │ ├── frame_sorter.go │ │ │ ├── framer.go │ │ │ ├── interface.go │ │ │ ├── internal/ │ │ │ │ ├── ackhandler/ │ │ │ │ │ ├── ack_eliciting.go │ │ │ │ │ ├── ackhandler.go │ │ │ │ │ ├── ecn.go │ │ │ │ │ ├── frame.go │ │ │ │ │ ├── interfaces.go │ │ │ │ │ ├── mockgen.go │ │ │ │ │ ├── packet.go │ │ │ │ │ ├── packet_number_generator.go │ │ │ │ │ ├── received_packet_handler.go │ │ │ │ │ ├── received_packet_history.go │ │ │ │ │ ├── received_packet_tracker.go │ │ │ │ │ ├── send_mode.go │ │ │ │ │ ├── sent_packet_handler.go │ │ │ │ │ └── sent_packet_history.go │ │ │ │ ├── congestion/ │ │ │ │ │ ├── bandwidth.go │ │ │ │ │ ├── clock.go │ │ │ │ │ ├── cubic.go │ │ │ │ │ ├── cubic_sender.go │ │ │ │ │ ├── hybrid_slow_start.go │ │ │ │ │ ├── interface.go │ │ │ │ │ └── pacer.go │ │ │ │ ├── flowcontrol/ │ │ │ │ │ ├── base_flow_controller.go │ │ │ │ │ ├── connection_flow_controller.go │ │ │ │ │ ├── interface.go │ │ │ │ │ └── stream_flow_controller.go │ │ │ │ ├── handshake/ │ │ │ │ │ ├── aead.go │ │ │ │ │ ├── cipher_suite.go │ │ │ │ │ ├── crypto_setup.go │ │ │ │ │ ├── fake_conn.go │ │ │ │ │ ├── header_protector.go │ │ │ │ │ ├── hkdf.go │ │ │ │ │ ├── initial_aead.go │ │ │ │ │ ├── interface.go │ │ │ │ │ ├── retry.go │ │ │ │ │ ├── session_ticket.go │ │ │ │ │ ├── tls_config.go │ │ │ │ │ ├── token_generator.go │ │ │ │ │ ├── token_protector.go │ │ │ │ │ ├── updatable_aead.go │ │ │ │ │ ├── xor_nonce_aead_boring.go │ │ │ │ │ └── xor_nonce_aead_noboring.go │ │ │ │ ├── protocol/ │ │ │ │ │ ├── connection_id.go │ │ │ │ │ ├── encryption_level.go │ │ │ │ │ ├── key_phase.go │ │ │ │ │ ├── packet_number.go │ │ │ │ │ ├── params.go │ │ │ │ │ ├── perspective.go │ │ │ │ │ ├── protocol.go │ │ │ │ │ ├── stream.go │ │ │ │ │ └── version.go │ │ │ │ ├── qerr/ │ │ │ │ │ ├── error_codes.go │ │ │ │ │ └── errors.go │ │ │ │ ├── utils/ │ │ │ │ │ ├── buffered_write_closer.go │ │ │ │ │ ├── linkedlist/ │ │ │ │ │ │ ├── README.md │ │ │ │ │ │ └── linkedlist.go │ │ │ │ │ ├── log.go │ │ │ │ │ ├── rand.go │ │ │ │ │ ├── ringbuffer/ │ │ │ │ │ │ └── ringbuffer.go │ │ │ │ │ ├── rtt_stats.go │ │ │ │ │ └── timer.go │ │ │ │ └── wire/ │ │ │ │ ├── ack_frame.go │ │ │ │ ├── ack_range.go │ │ │ │ ├── connection_close_frame.go │ │ │ │ ├── crypto_frame.go │ │ │ │ ├── data_blocked_frame.go │ │ │ │ ├── datagram_frame.go │ │ │ │ ├── extended_header.go │ │ │ │ ├── frame.go │ │ │ │ ├── frame_parser.go │ │ │ │ ├── handshake_done_frame.go │ │ │ │ ├── header.go │ │ │ │ ├── log.go │ │ │ │ ├── max_data_frame.go │ │ │ │ ├── max_stream_data_frame.go │ │ │ │ ├── max_streams_frame.go │ │ │ │ ├── new_connection_id_frame.go │ │ │ │ ├── new_token_frame.go │ │ │ │ ├── path_challenge_frame.go │ │ │ │ ├── path_response_frame.go │ │ │ │ ├── ping_frame.go │ │ │ │ ├── pool.go │ │ │ │ ├── reset_stream_frame.go │ │ │ │ ├── retire_connection_id_frame.go │ │ │ │ ├── short_header.go │ │ │ │ ├── stop_sending_frame.go │ │ │ │ ├── stream_data_blocked_frame.go │ │ │ │ ├── stream_frame.go │ │ │ │ ├── streams_blocked_frame.go │ │ │ │ ├── transport_parameters.go │ │ │ │ └── version_negotiation.go │ │ │ ├── logging/ │ │ │ │ ├── connection_tracer.go │ │ │ │ ├── connection_tracer_multiplexer.go │ │ │ │ ├── frame.go │ │ │ │ ├── generate_multiplexer.go │ │ │ │ ├── interface.go │ │ │ │ ├── multiplexer.tmpl │ │ │ │ ├── packet_header.go │ │ │ │ ├── tracer.go │ │ │ │ ├── tracer_multiplexer.go │ │ │ │ └── types.go │ │ │ ├── mockgen.go │ │ │ ├── mtu_discoverer.go │ │ │ ├── oss-fuzz.sh │ │ │ ├── packet_handler_map.go │ │ │ ├── packet_packer.go │ │ │ ├── packet_unpacker.go │ │ │ ├── path_manager.go │ │ │ ├── path_manager_outgoing.go │ │ │ ├── quicvarint/ │ │ │ │ ├── io.go │ │ │ │ └── varint.go │ │ │ ├── receive_stream.go │ │ │ ├── retransmission_queue.go │ │ │ ├── send_conn.go │ │ │ ├── send_queue.go │ │ │ ├── send_stream.go │ │ │ ├── server.go │ │ │ ├── stateless_reset.go │ │ │ ├── stream.go │ │ │ ├── streams_map.go │ │ │ ├── streams_map_incoming.go │ │ │ ├── streams_map_outgoing.go │ │ │ ├── sys_conn.go │ │ │ ├── sys_conn_buffers.go │ │ │ ├── sys_conn_buffers_write.go │ │ │ ├── sys_conn_df.go │ │ │ ├── sys_conn_df_darwin.go │ │ │ ├── sys_conn_df_linux.go │ │ │ ├── sys_conn_df_windows.go │ │ │ ├── sys_conn_helper_darwin.go │ │ │ ├── sys_conn_helper_freebsd.go │ │ │ ├── sys_conn_helper_linux.go │ │ │ ├── sys_conn_helper_nonlinux.go │ │ │ ├── sys_conn_no_oob.go │ │ │ ├── sys_conn_oob.go │ │ │ ├── sys_conn_windows.go │ │ │ ├── token_store.go │ │ │ ├── tools.go │ │ │ └── transport.go │ │ ├── rs/ │ │ │ └── zerolog/ │ │ │ ├── .gitignore │ │ │ ├── .travis.yml │ │ │ ├── CNAME │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── _config.yml │ │ │ ├── array.go │ │ │ ├── console.go │ │ │ ├── context.go │ │ │ ├── ctx.go │ │ │ ├── encoder.go │ │ │ ├── encoder_cbor.go │ │ │ ├── encoder_json.go │ │ │ ├── event.go │ │ │ ├── fields.go │ │ │ ├── globals.go │ │ │ ├── go112.go │ │ │ ├── hook.go │ │ │ ├── internal/ │ │ │ │ ├── cbor/ │ │ │ │ │ ├── README.md │ │ │ │ │ ├── base.go │ │ │ │ │ ├── cbor.go │ │ │ │ │ ├── decode_stream.go │ │ │ │ │ ├── string.go │ │ │ │ │ ├── time.go │ │ │ │ │ └── types.go │ │ │ │ └── json/ │ │ │ │ ├── base.go │ │ │ │ ├── bytes.go │ │ │ │ ├── string.go │ │ │ │ ├── time.go │ │ │ │ └── types.go │ │ │ ├── log/ │ │ │ │ └── log.go │ │ │ ├── log.go │ │ │ ├── not_go112.go │ │ │ ├── sampler.go │ │ │ ├── syslog.go │ │ │ └── writer.go │ │ ├── russross/ │ │ │ └── blackfriday/ │ │ │ └── v2/ │ │ │ ├── .gitignore │ │ │ ├── .travis.yml │ │ │ ├── LICENSE.txt │ │ │ ├── README.md │ │ │ ├── block.go │ │ │ ├── doc.go │ │ │ ├── entities.go │ │ │ ├── esc.go │ │ │ ├── html.go │ │ │ ├── inline.go │ │ │ ├── markdown.go │ │ │ ├── node.go │ │ │ └── smartypants.go │ │ └── stretchr/ │ │ └── testify/ │ │ ├── LICENSE │ │ ├── assert/ │ │ │ ├── assertion_compare.go │ │ │ ├── assertion_format.go │ │ │ ├── assertion_format.go.tmpl │ │ │ ├── assertion_forward.go │ │ │ ├── assertion_forward.go.tmpl │ │ │ ├── assertion_order.go │ │ │ ├── assertions.go │ │ │ ├── doc.go │ │ │ ├── errors.go │ │ │ ├── forward_assertions.go │ │ │ ├── http_assertions.go │ │ │ └── yaml/ │ │ │ ├── yaml_custom.go │ │ │ ├── yaml_default.go │ │ │ └── yaml_fail.go │ │ └── require/ │ │ ├── doc.go │ │ ├── forward_requirements.go │ │ ├── require.go │ │ ├── require.go.tmpl │ │ ├── require_forward.go │ │ ├── require_forward.go.tmpl │ │ └── requirements.go │ ├── go.opentelemetry.io/ │ │ ├── auto/ │ │ │ └── sdk/ │ │ │ ├── CONTRIBUTING.md │ │ │ ├── LICENSE │ │ │ ├── VERSIONING.md │ │ │ ├── doc.go │ │ │ ├── internal/ │ │ │ │ └── telemetry/ │ │ │ │ ├── attr.go │ │ │ │ ├── doc.go │ │ │ │ ├── id.go │ │ │ │ ├── number.go │ │ │ │ ├── resource.go │ │ │ │ ├── scope.go │ │ │ │ ├── span.go │ │ │ │ ├── status.go │ │ │ │ ├── traces.go │ │ │ │ └── value.go │ │ │ ├── limit.go │ │ │ ├── span.go │ │ │ ├── tracer.go │ │ │ └── tracer_provider.go │ │ ├── contrib/ │ │ │ └── propagators/ │ │ │ ├── LICENSE │ │ │ └── jaeger/ │ │ │ ├── context.go │ │ │ ├── doc.go │ │ │ └── jaeger_propagator.go │ │ ├── otel/ │ │ │ ├── .clomonitor.yml │ │ │ ├── .codespellignore │ │ │ ├── .codespellrc │ │ │ ├── .gitattributes │ │ │ ├── .gitignore │ │ │ ├── .golangci.yml │ │ │ ├── .lycheeignore │ │ │ ├── .markdownlint.yaml │ │ │ ├── CHANGELOG.md │ │ │ ├── CODEOWNERS │ │ │ ├── CONTRIBUTING.md │ │ │ ├── LICENSE │ │ │ ├── Makefile │ │ │ ├── README.md │ │ │ ├── RELEASING.md │ │ │ ├── SECURITY-INSIGHTS.yml │ │ │ ├── VERSIONING.md │ │ │ ├── attribute/ │ │ │ │ ├── README.md │ │ │ │ ├── doc.go │ │ │ │ ├── encoder.go │ │ │ │ ├── filter.go │ │ │ │ ├── hash.go │ │ │ │ ├── internal/ │ │ │ │ │ ├── attribute.go │ │ │ │ │ └── xxhash/ │ │ │ │ │ └── xxhash.go │ │ │ │ ├── iterator.go │ │ │ │ ├── key.go │ │ │ │ ├── kv.go │ │ │ │ ├── rawhelpers.go │ │ │ │ ├── set.go │ │ │ │ ├── type_string.go │ │ │ │ └── value.go │ │ │ ├── baggage/ │ │ │ │ ├── README.md │ │ │ │ ├── baggage.go │ │ │ │ ├── context.go │ │ │ │ └── doc.go │ │ │ ├── codes/ │ │ │ │ ├── README.md │ │ │ │ ├── codes.go │ │ │ │ └── doc.go │ │ │ ├── dependencies.Dockerfile │ │ │ ├── doc.go │ │ │ ├── error_handler.go │ │ │ ├── exporters/ │ │ │ │ └── otlp/ │ │ │ │ └── otlptrace/ │ │ │ │ ├── LICENSE │ │ │ │ ├── README.md │ │ │ │ ├── clients.go │ │ │ │ ├── doc.go │ │ │ │ ├── exporter.go │ │ │ │ ├── internal/ │ │ │ │ │ └── tracetransform/ │ │ │ │ │ ├── attribute.go │ │ │ │ │ ├── instrumentation.go │ │ │ │ │ ├── resource.go │ │ │ │ │ └── span.go │ │ │ │ └── version.go │ │ │ ├── handler.go │ │ │ ├── internal/ │ │ │ │ ├── baggage/ │ │ │ │ │ ├── baggage.go │ │ │ │ │ └── context.go │ │ │ │ └── global/ │ │ │ │ ├── handler.go │ │ │ │ ├── instruments.go │ │ │ │ ├── internal_logging.go │ │ │ │ ├── meter.go │ │ │ │ ├── propagator.go │ │ │ │ ├── state.go │ │ │ │ └── trace.go │ │ │ ├── internal_logging.go │ │ │ ├── metric/ │ │ │ │ ├── LICENSE │ │ │ │ ├── README.md │ │ │ │ ├── asyncfloat64.go │ │ │ │ ├── asyncint64.go │ │ │ │ ├── config.go │ │ │ │ ├── doc.go │ │ │ │ ├── embedded/ │ │ │ │ │ ├── README.md │ │ │ │ │ └── embedded.go │ │ │ │ ├── instrument.go │ │ │ │ ├── meter.go │ │ │ │ ├── noop/ │ │ │ │ │ ├── README.md │ │ │ │ │ └── noop.go │ │ │ │ ├── syncfloat64.go │ │ │ │ └── syncint64.go │ │ │ ├── metric.go │ │ │ ├── propagation/ │ │ │ │ ├── README.md │ │ │ │ ├── baggage.go │ │ │ │ ├── doc.go │ │ │ │ ├── propagation.go │ │ │ │ └── trace_context.go │ │ │ ├── propagation.go │ │ │ ├── renovate.json │ │ │ ├── requirements.txt │ │ │ ├── sdk/ │ │ │ │ ├── LICENSE │ │ │ │ ├── README.md │ │ │ │ ├── instrumentation/ │ │ │ │ │ ├── README.md │ │ │ │ │ ├── doc.go │ │ │ │ │ ├── library.go │ │ │ │ │ └── scope.go │ │ │ │ ├── internal/ │ │ │ │ │ └── x/ │ │ │ │ │ ├── README.md │ │ │ │ │ ├── features.go │ │ │ │ │ └── x.go │ │ │ │ ├── resource/ │ │ │ │ │ ├── README.md │ │ │ │ │ ├── auto.go │ │ │ │ │ ├── builtin.go │ │ │ │ │ ├── config.go │ │ │ │ │ ├── container.go │ │ │ │ │ ├── doc.go │ │ │ │ │ ├── env.go │ │ │ │ │ ├── host_id.go │ │ │ │ │ ├── host_id_bsd.go │ │ │ │ │ ├── host_id_darwin.go │ │ │ │ │ ├── host_id_exec.go │ │ │ │ │ ├── host_id_linux.go │ │ │ │ │ ├── host_id_readfile.go │ │ │ │ │ ├── host_id_unsupported.go │ │ │ │ │ ├── host_id_windows.go │ │ │ │ │ ├── os.go │ │ │ │ │ ├── os_release_darwin.go │ │ │ │ │ ├── os_release_unix.go │ │ │ │ │ ├── os_unix.go │ │ │ │ │ ├── os_unsupported.go │ │ │ │ │ ├── os_windows.go │ │ │ │ │ ├── process.go │ │ │ │ │ └── resource.go │ │ │ │ ├── trace/ │ │ │ │ │ ├── README.md │ │ │ │ │ ├── batch_span_processor.go │ │ │ │ │ ├── doc.go │ │ │ │ │ ├── event.go │ │ │ │ │ ├── evictedqueue.go │ │ │ │ │ ├── id_generator.go │ │ │ │ │ ├── internal/ │ │ │ │ │ │ ├── env/ │ │ │ │ │ │ │ └── env.go │ │ │ │ │ │ └── observ/ │ │ │ │ │ │ ├── batch_span_processor.go │ │ │ │ │ │ ├── doc.go │ │ │ │ │ │ ├── simple_span_processor.go │ │ │ │ │ │ └── tracer.go │ │ │ │ │ ├── link.go │ │ │ │ │ ├── provider.go │ │ │ │ │ ├── sampler_env.go │ │ │ │ │ ├── sampling.go │ │ │ │ │ ├── simple_span_processor.go │ │ │ │ │ ├── snapshot.go │ │ │ │ │ ├── span.go │ │ │ │ │ ├── span_exporter.go │ │ │ │ │ ├── span_limits.go │ │ │ │ │ ├── span_processor.go │ │ │ │ │ └── tracer.go │ │ │ │ └── version.go │ │ │ ├── semconv/ │ │ │ │ ├── internal/ │ │ │ │ │ └── http.go │ │ │ │ ├── v1.37.0/ │ │ │ │ │ ├── MIGRATION.md │ │ │ │ │ ├── README.md │ │ │ │ │ ├── attribute_group.go │ │ │ │ │ ├── doc.go │ │ │ │ │ ├── error_type.go │ │ │ │ │ ├── exception.go │ │ │ │ │ └── schema.go │ │ │ │ ├── v1.39.0/ │ │ │ │ │ ├── MIGRATION.md │ │ │ │ │ ├── README.md │ │ │ │ │ ├── attribute_group.go │ │ │ │ │ ├── doc.go │ │ │ │ │ ├── error_type.go │ │ │ │ │ ├── exception.go │ │ │ │ │ ├── otelconv/ │ │ │ │ │ │ └── metric.go │ │ │ │ │ └── schema.go │ │ │ │ └── v1.7.0/ │ │ │ │ ├── README.md │ │ │ │ ├── doc.go │ │ │ │ ├── exception.go │ │ │ │ ├── http.go │ │ │ │ ├── resource.go │ │ │ │ ├── schema.go │ │ │ │ └── trace.go │ │ │ ├── trace/ │ │ │ │ ├── LICENSE │ │ │ │ ├── README.md │ │ │ │ ├── auto.go │ │ │ │ ├── config.go │ │ │ │ ├── context.go │ │ │ │ ├── doc.go │ │ │ │ ├── embedded/ │ │ │ │ │ ├── README.md │ │ │ │ │ └── embedded.go │ │ │ │ ├── hex.go │ │ │ │ ├── internal/ │ │ │ │ │ └── telemetry/ │ │ │ │ │ ├── attr.go │ │ │ │ │ ├── doc.go │ │ │ │ │ ├── id.go │ │ │ │ │ ├── number.go │ │ │ │ │ ├── resource.go │ │ │ │ │ ├── scope.go │ │ │ │ │ ├── span.go │ │ │ │ │ ├── status.go │ │ │ │ │ ├── traces.go │ │ │ │ │ └── value.go │ │ │ │ ├── nonrecording.go │ │ │ │ ├── noop/ │ │ │ │ │ ├── README.md │ │ │ │ │ └── noop.go │ │ │ │ ├── noop.go │ │ │ │ ├── provider.go │ │ │ │ ├── span.go │ │ │ │ ├── trace.go │ │ │ │ ├── tracer.go │ │ │ │ └── tracestate.go │ │ │ ├── trace.go │ │ │ ├── verify_released_changelog.sh │ │ │ ├── version.go │ │ │ └── versions.yaml │ │ └── proto/ │ │ └── otlp/ │ │ ├── LICENSE │ │ ├── collector/ │ │ │ └── trace/ │ │ │ └── v1/ │ │ │ ├── trace_service.pb.go │ │ │ ├── trace_service.pb.gw.go │ │ │ └── trace_service_grpc.pb.go │ │ ├── common/ │ │ │ └── v1/ │ │ │ └── common.pb.go │ │ ├── resource/ │ │ │ └── v1/ │ │ │ └── resource.pb.go │ │ └── trace/ │ │ └── v1/ │ │ └── trace.pb.go │ ├── go.uber.org/ │ │ └── mock/ │ │ ├── AUTHORS │ │ ├── LICENSE │ │ ├── gomock/ │ │ │ ├── call.go │ │ │ ├── callset.go │ │ │ ├── controller.go │ │ │ ├── doc.go │ │ │ ├── matchers.go │ │ │ └── string.go │ │ └── mockgen/ │ │ ├── deprecated.go │ │ ├── generic.go │ │ ├── gob.go │ │ ├── mockgen.go │ │ ├── model/ │ │ │ └── model.go │ │ ├── package_mode.go │ │ ├── parse.go │ │ └── version.go │ ├── golang.org/ │ │ └── x/ │ │ ├── crypto/ │ │ │ ├── LICENSE │ │ │ ├── PATENTS │ │ │ ├── blake2b/ │ │ │ │ ├── blake2b.go │ │ │ │ ├── blake2bAVX2_amd64.go │ │ │ │ ├── blake2bAVX2_amd64.s │ │ │ │ ├── blake2b_amd64.s │ │ │ │ ├── blake2b_generic.go │ │ │ │ ├── blake2b_ref.go │ │ │ │ ├── blake2x.go │ │ │ │ └── register.go │ │ │ ├── blowfish/ │ │ │ │ ├── block.go │ │ │ │ ├── cipher.go │ │ │ │ └── const.go │ │ │ ├── chacha20/ │ │ │ │ ├── chacha_arm64.go │ │ │ │ ├── chacha_arm64.s │ │ │ │ ├── chacha_generic.go │ │ │ │ ├── chacha_noasm.go │ │ │ │ ├── chacha_ppc64x.go │ │ │ │ ├── chacha_ppc64x.s │ │ │ │ ├── chacha_s390x.go │ │ │ │ ├── chacha_s390x.s │ │ │ │ └── xor.go │ │ │ ├── chacha20poly1305/ │ │ │ │ ├── chacha20poly1305.go │ │ │ │ ├── chacha20poly1305_amd64.go │ │ │ │ ├── chacha20poly1305_amd64.s │ │ │ │ ├── chacha20poly1305_generic.go │ │ │ │ ├── chacha20poly1305_noasm.go │ │ │ │ └── xchacha20poly1305.go │ │ │ ├── curve25519/ │ │ │ │ └── curve25519.go │ │ │ ├── hkdf/ │ │ │ │ └── hkdf.go │ │ │ ├── internal/ │ │ │ │ ├── alias/ │ │ │ │ │ ├── alias.go │ │ │ │ │ └── alias_purego.go │ │ │ │ └── poly1305/ │ │ │ │ ├── mac_noasm.go │ │ │ │ ├── poly1305.go │ │ │ │ ├── sum_amd64.s │ │ │ │ ├── sum_asm.go │ │ │ │ ├── sum_generic.go │ │ │ │ ├── sum_loong64.s │ │ │ │ ├── sum_ppc64x.s │ │ │ │ ├── sum_s390x.go │ │ │ │ └── sum_s390x.s │ │ │ ├── nacl/ │ │ │ │ ├── box/ │ │ │ │ │ └── box.go │ │ │ │ └── secretbox/ │ │ │ │ └── secretbox.go │ │ │ ├── salsa20/ │ │ │ │ └── salsa/ │ │ │ │ ├── hsalsa20.go │ │ │ │ ├── salsa208.go │ │ │ │ ├── salsa20_amd64.go │ │ │ │ ├── salsa20_amd64.s │ │ │ │ ├── salsa20_noasm.go │ │ │ │ └── salsa20_ref.go │ │ │ └── ssh/ │ │ │ ├── buffer.go │ │ │ ├── certs.go │ │ │ ├── channel.go │ │ │ ├── cipher.go │ │ │ ├── client.go │ │ │ ├── client_auth.go │ │ │ ├── common.go │ │ │ ├── connection.go │ │ │ ├── doc.go │ │ │ ├── handshake.go │ │ │ ├── internal/ │ │ │ │ └── bcrypt_pbkdf/ │ │ │ │ └── bcrypt_pbkdf.go │ │ │ ├── kex.go │ │ │ ├── keys.go │ │ │ ├── mac.go │ │ │ ├── messages.go │ │ │ ├── mlkem.go │ │ │ ├── mux.go │ │ │ ├── server.go │ │ │ ├── session.go │ │ │ ├── ssh_gss.go │ │ │ ├── streamlocal.go │ │ │ ├── tcpip.go │ │ │ └── transport.go │ │ ├── mod/ │ │ │ ├── LICENSE │ │ │ ├── PATENTS │ │ │ ├── internal/ │ │ │ │ └── lazyregexp/ │ │ │ │ └── lazyre.go │ │ │ ├── modfile/ │ │ │ │ ├── print.go │ │ │ │ ├── read.go │ │ │ │ ├── rule.go │ │ │ │ └── work.go │ │ │ ├── module/ │ │ │ │ ├── module.go │ │ │ │ └── pseudo.go │ │ │ └── semver/ │ │ │ └── semver.go │ │ ├── net/ │ │ │ ├── LICENSE │ │ │ ├── PATENTS │ │ │ ├── bpf/ │ │ │ │ ├── asm.go │ │ │ │ ├── constants.go │ │ │ │ ├── doc.go │ │ │ │ ├── instructions.go │ │ │ │ ├── setter.go │ │ │ │ ├── vm.go │ │ │ │ └── vm_instructions.go │ │ │ ├── context/ │ │ │ │ └── context.go │ │ │ ├── http/ │ │ │ │ └── httpguts/ │ │ │ │ ├── guts.go │ │ │ │ └── httplex.go │ │ │ ├── http2/ │ │ │ │ ├── .gitignore │ │ │ │ ├── ascii.go │ │ │ │ ├── ciphers.go │ │ │ │ ├── client_conn_pool.go │ │ │ │ ├── config.go │ │ │ │ ├── config_go124.go │ │ │ │ ├── config_pre_go124.go │ │ │ │ ├── databuffer.go │ │ │ │ ├── errors.go │ │ │ │ ├── flow.go │ │ │ │ ├── frame.go │ │ │ │ ├── gotrack.go │ │ │ │ ├── hpack/ │ │ │ │ │ ├── encode.go │ │ │ │ │ ├── hpack.go │ │ │ │ │ ├── huffman.go │ │ │ │ │ ├── static_table.go │ │ │ │ │ └── tables.go │ │ │ │ ├── http2.go │ │ │ │ ├── pipe.go │ │ │ │ ├── server.go │ │ │ │ ├── timer.go │ │ │ │ ├── transport.go │ │ │ │ ├── unencrypted.go │ │ │ │ ├── write.go │ │ │ │ ├── writesched.go │ │ │ │ ├── writesched_priority.go │ │ │ │ ├── writesched_random.go │ │ │ │ └── writesched_roundrobin.go │ │ │ ├── icmp/ │ │ │ │ ├── dstunreach.go │ │ │ │ ├── echo.go │ │ │ │ ├── endpoint.go │ │ │ │ ├── extension.go │ │ │ │ ├── helper_posix.go │ │ │ │ ├── interface.go │ │ │ │ ├── ipv4.go │ │ │ │ ├── ipv6.go │ │ │ │ ├── listen_posix.go │ │ │ │ ├── listen_stub.go │ │ │ │ ├── message.go │ │ │ │ ├── messagebody.go │ │ │ │ ├── mpls.go │ │ │ │ ├── multipart.go │ │ │ │ ├── packettoobig.go │ │ │ │ ├── paramprob.go │ │ │ │ ├── sys_freebsd.go │ │ │ │ └── timeexceeded.go │ │ │ ├── idna/ │ │ │ │ ├── go118.go │ │ │ │ ├── idna10.0.0.go │ │ │ │ ├── idna9.0.0.go │ │ │ │ ├── pre_go118.go │ │ │ │ ├── punycode.go │ │ │ │ ├── tables10.0.0.go │ │ │ │ ├── tables11.0.0.go │ │ │ │ ├── tables12.0.0.go │ │ │ │ ├── tables13.0.0.go │ │ │ │ ├── tables15.0.0.go │ │ │ │ ├── tables9.0.0.go │ │ │ │ ├── trie.go │ │ │ │ ├── trie12.0.0.go │ │ │ │ ├── trie13.0.0.go │ │ │ │ └── trieval.go │ │ │ ├── internal/ │ │ │ │ ├── httpcommon/ │ │ │ │ │ ├── ascii.go │ │ │ │ │ ├── headermap.go │ │ │ │ │ └── request.go │ │ │ │ ├── iana/ │ │ │ │ │ └── const.go │ │ │ │ ├── socket/ │ │ │ │ │ ├── cmsghdr.go │ │ │ │ │ ├── cmsghdr_bsd.go │ │ │ │ │ ├── cmsghdr_linux_32bit.go │ │ │ │ │ ├── cmsghdr_linux_64bit.go │ │ │ │ │ ├── cmsghdr_solaris_64bit.go │ │ │ │ │ ├── cmsghdr_stub.go │ │ │ │ │ ├── cmsghdr_unix.go │ │ │ │ │ ├── cmsghdr_zos_s390x.go │ │ │ │ │ ├── complete_dontwait.go │ │ │ │ │ ├── complete_nodontwait.go │ │ │ │ │ ├── empty.s │ │ │ │ │ ├── error_unix.go │ │ │ │ │ ├── error_windows.go │ │ │ │ │ ├── iovec_32bit.go │ │ │ │ │ ├── iovec_64bit.go │ │ │ │ │ ├── iovec_solaris_64bit.go │ │ │ │ │ ├── iovec_stub.go │ │ │ │ │ ├── mmsghdr_stub.go │ │ │ │ │ ├── mmsghdr_unix.go │ │ │ │ │ ├── msghdr_bsd.go │ │ │ │ │ ├── msghdr_bsdvar.go │ │ │ │ │ ├── msghdr_linux.go │ │ │ │ │ ├── msghdr_linux_32bit.go │ │ │ │ │ ├── msghdr_linux_64bit.go │ │ │ │ │ ├── msghdr_openbsd.go │ │ │ │ │ ├── msghdr_solaris_64bit.go │ │ │ │ │ ├── msghdr_stub.go │ │ │ │ │ ├── msghdr_zos_s390x.go │ │ │ │ │ ├── norace.go │ │ │ │ │ ├── race.go │ │ │ │ │ ├── rawconn.go │ │ │ │ │ ├── rawconn_mmsg.go │ │ │ │ │ ├── rawconn_msg.go │ │ │ │ │ ├── rawconn_nommsg.go │ │ │ │ │ ├── rawconn_nomsg.go │ │ │ │ │ ├── socket.go │ │ │ │ │ ├── sys.go │ │ │ │ │ ├── sys_bsd.go │ │ │ │ │ ├── sys_const_unix.go │ │ │ │ │ ├── sys_linux.go │ │ │ │ │ ├── sys_linux_386.go │ │ │ │ │ ├── sys_linux_386.s │ │ │ │ │ ├── sys_linux_amd64.go │ │ │ │ │ ├── sys_linux_arm.go │ │ │ │ │ ├── sys_linux_arm64.go │ │ │ │ │ ├── sys_linux_loong64.go │ │ │ │ │ ├── sys_linux_mips.go │ │ │ │ │ ├── sys_linux_mips64.go │ │ │ │ │ ├── sys_linux_mips64le.go │ │ │ │ │ ├── sys_linux_mipsle.go │ │ │ │ │ ├── sys_linux_ppc.go │ │ │ │ │ ├── sys_linux_ppc64.go │ │ │ │ │ ├── sys_linux_ppc64le.go │ │ │ │ │ ├── sys_linux_riscv64.go │ │ │ │ │ ├── sys_linux_s390x.go │ │ │ │ │ ├── sys_linux_s390x.s │ │ │ │ │ ├── sys_netbsd.go │ │ │ │ │ ├── sys_posix.go │ │ │ │ │ ├── sys_stub.go │ │ │ │ │ ├── sys_unix.go │ │ │ │ │ ├── sys_windows.go │ │ │ │ │ ├── sys_zos_s390x.go │ │ │ │ │ ├── sys_zos_s390x.s │ │ │ │ │ ├── zsys_aix_ppc64.go │ │ │ │ │ ├── zsys_darwin_amd64.go │ │ │ │ │ ├── zsys_darwin_arm64.go │ │ │ │ │ ├── zsys_dragonfly_amd64.go │ │ │ │ │ ├── zsys_freebsd_386.go │ │ │ │ │ ├── zsys_freebsd_amd64.go │ │ │ │ │ ├── zsys_freebsd_arm.go │ │ │ │ │ ├── zsys_freebsd_arm64.go │ │ │ │ │ ├── zsys_freebsd_riscv64.go │ │ │ │ │ ├── zsys_linux_386.go │ │ │ │ │ ├── zsys_linux_amd64.go │ │ │ │ │ ├── zsys_linux_arm.go │ │ │ │ │ ├── zsys_linux_arm64.go │ │ │ │ │ ├── zsys_linux_loong64.go │ │ │ │ │ ├── zsys_linux_mips.go │ │ │ │ │ ├── zsys_linux_mips64.go │ │ │ │ │ ├── zsys_linux_mips64le.go │ │ │ │ │ ├── zsys_linux_mipsle.go │ │ │ │ │ ├── zsys_linux_ppc.go │ │ │ │ │ ├── zsys_linux_ppc64.go │ │ │ │ │ ├── zsys_linux_ppc64le.go │ │ │ │ │ ├── zsys_linux_riscv64.go │ │ │ │ │ ├── zsys_linux_s390x.go │ │ │ │ │ ├── zsys_netbsd_386.go │ │ │ │ │ ├── zsys_netbsd_amd64.go │ │ │ │ │ ├── zsys_netbsd_arm.go │ │ │ │ │ ├── zsys_netbsd_arm64.go │ │ │ │ │ ├── zsys_openbsd_386.go │ │ │ │ │ ├── zsys_openbsd_amd64.go │ │ │ │ │ ├── zsys_openbsd_arm.go │ │ │ │ │ ├── zsys_openbsd_arm64.go │ │ │ │ │ ├── zsys_openbsd_mips64.go │ │ │ │ │ ├── zsys_openbsd_ppc64.go │ │ │ │ │ ├── zsys_openbsd_riscv64.go │ │ │ │ │ ├── zsys_solaris_amd64.go │ │ │ │ │ └── zsys_zos_s390x.go │ │ │ │ ├── socks/ │ │ │ │ │ ├── client.go │ │ │ │ │ └── socks.go │ │ │ │ └── timeseries/ │ │ │ │ └── timeseries.go │ │ │ ├── ipv4/ │ │ │ │ ├── batch.go │ │ │ │ ├── control.go │ │ │ │ ├── control_bsd.go │ │ │ │ ├── control_pktinfo.go │ │ │ │ ├── control_stub.go │ │ │ │ ├── control_unix.go │ │ │ │ ├── control_windows.go │ │ │ │ ├── control_zos.go │ │ │ │ ├── dgramopt.go │ │ │ │ ├── doc.go │ │ │ │ ├── endpoint.go │ │ │ │ ├── genericopt.go │ │ │ │ ├── header.go │ │ │ │ ├── helper.go │ │ │ │ ├── iana.go │ │ │ │ ├── icmp.go │ │ │ │ ├── icmp_linux.go │ │ │ │ ├── icmp_stub.go │ │ │ │ ├── packet.go │ │ │ │ ├── payload.go │ │ │ │ ├── payload_cmsg.go │ │ │ │ ├── payload_nocmsg.go │ │ │ │ ├── sockopt.go │ │ │ │ ├── sockopt_posix.go │ │ │ │ ├── sockopt_stub.go │ │ │ │ ├── sys_aix.go │ │ │ │ ├── sys_asmreq.go │ │ │ │ ├── sys_asmreq_stub.go │ │ │ │ ├── sys_asmreqn.go │ │ │ │ ├── sys_asmreqn_stub.go │ │ │ │ ├── sys_bpf.go │ │ │ │ ├── sys_bpf_stub.go │ │ │ │ ├── sys_bsd.go │ │ │ │ ├── sys_darwin.go │ │ │ │ ├── sys_dragonfly.go │ │ │ │ ├── sys_freebsd.go │ │ │ │ ├── sys_linux.go │ │ │ │ ├── sys_solaris.go │ │ │ │ ├── sys_ssmreq.go │ │ │ │ ├── sys_ssmreq_stub.go │ │ │ │ ├── sys_stub.go │ │ │ │ ├── sys_windows.go │ │ │ │ ├── sys_zos.go │ │ │ │ ├── zsys_aix_ppc64.go │ │ │ │ ├── zsys_darwin.go │ │ │ │ ├── zsys_dragonfly.go │ │ │ │ ├── zsys_freebsd_386.go │ │ │ │ ├── zsys_freebsd_amd64.go │ │ │ │ ├── zsys_freebsd_arm.go │ │ │ │ ├── zsys_freebsd_arm64.go │ │ │ │ ├── zsys_freebsd_riscv64.go │ │ │ │ ├── zsys_linux_386.go │ │ │ │ ├── zsys_linux_amd64.go │ │ │ │ ├── zsys_linux_arm.go │ │ │ │ ├── zsys_linux_arm64.go │ │ │ │ ├── zsys_linux_loong64.go │ │ │ │ ├── zsys_linux_mips.go │ │ │ │ ├── zsys_linux_mips64.go │ │ │ │ ├── zsys_linux_mips64le.go │ │ │ │ ├── zsys_linux_mipsle.go │ │ │ │ ├── zsys_linux_ppc.go │ │ │ │ ├── zsys_linux_ppc64.go │ │ │ │ ├── zsys_linux_ppc64le.go │ │ │ │ ├── zsys_linux_riscv64.go │ │ │ │ ├── zsys_linux_s390x.go │ │ │ │ ├── zsys_netbsd.go │ │ │ │ ├── zsys_openbsd.go │ │ │ │ ├── zsys_solaris.go │ │ │ │ └── zsys_zos_s390x.go │ │ │ ├── ipv6/ │ │ │ │ ├── batch.go │ │ │ │ ├── control.go │ │ │ │ ├── control_rfc2292_unix.go │ │ │ │ ├── control_rfc3542_unix.go │ │ │ │ ├── control_stub.go │ │ │ │ ├── control_unix.go │ │ │ │ ├── control_windows.go │ │ │ │ ├── dgramopt.go │ │ │ │ ├── doc.go │ │ │ │ ├── endpoint.go │ │ │ │ ├── genericopt.go │ │ │ │ ├── header.go │ │ │ │ ├── helper.go │ │ │ │ ├── iana.go │ │ │ │ ├── icmp.go │ │ │ │ ├── icmp_bsd.go │ │ │ │ ├── icmp_linux.go │ │ │ │ ├── icmp_solaris.go │ │ │ │ ├── icmp_stub.go │ │ │ │ ├── icmp_windows.go │ │ │ │ ├── icmp_zos.go │ │ │ │ ├── payload.go │ │ │ │ ├── payload_cmsg.go │ │ │ │ ├── payload_nocmsg.go │ │ │ │ ├── sockopt.go │ │ │ │ ├── sockopt_posix.go │ │ │ │ ├── sockopt_stub.go │ │ │ │ ├── sys_aix.go │ │ │ │ ├── sys_asmreq.go │ │ │ │ ├── sys_asmreq_stub.go │ │ │ │ ├── sys_bpf.go │ │ │ │ ├── sys_bpf_stub.go │ │ │ │ ├── sys_bsd.go │ │ │ │ ├── sys_darwin.go │ │ │ │ ├── sys_freebsd.go │ │ │ │ ├── sys_linux.go │ │ │ │ ├── sys_solaris.go │ │ │ │ ├── sys_ssmreq.go │ │ │ │ ├── sys_ssmreq_stub.go │ │ │ │ ├── sys_stub.go │ │ │ │ ├── sys_windows.go │ │ │ │ ├── sys_zos.go │ │ │ │ ├── zsys_aix_ppc64.go │ │ │ │ ├── zsys_darwin.go │ │ │ │ ├── zsys_dragonfly.go │ │ │ │ ├── zsys_freebsd_386.go │ │ │ │ ├── zsys_freebsd_amd64.go │ │ │ │ ├── zsys_freebsd_arm.go │ │ │ │ ├── zsys_freebsd_arm64.go │ │ │ │ ├── zsys_freebsd_riscv64.go │ │ │ │ ├── zsys_linux_386.go │ │ │ │ ├── zsys_linux_amd64.go │ │ │ │ ├── zsys_linux_arm.go │ │ │ │ ├── zsys_linux_arm64.go │ │ │ │ ├── zsys_linux_loong64.go │ │ │ │ ├── zsys_linux_mips.go │ │ │ │ ├── zsys_linux_mips64.go │ │ │ │ ├── zsys_linux_mips64le.go │ │ │ │ ├── zsys_linux_mipsle.go │ │ │ │ ├── zsys_linux_ppc.go │ │ │ │ ├── zsys_linux_ppc64.go │ │ │ │ ├── zsys_linux_ppc64le.go │ │ │ │ ├── zsys_linux_riscv64.go │ │ │ │ ├── zsys_linux_s390x.go │ │ │ │ ├── zsys_netbsd.go │ │ │ │ ├── zsys_openbsd.go │ │ │ │ ├── zsys_solaris.go │ │ │ │ └── zsys_zos_s390x.go │ │ │ ├── nettest/ │ │ │ │ ├── conntest.go │ │ │ │ ├── nettest.go │ │ │ │ ├── nettest_stub.go │ │ │ │ ├── nettest_unix.go │ │ │ │ └── nettest_windows.go │ │ │ ├── proxy/ │ │ │ │ ├── dial.go │ │ │ │ ├── direct.go │ │ │ │ ├── per_host.go │ │ │ │ ├── proxy.go │ │ │ │ └── socks5.go │ │ │ ├── trace/ │ │ │ │ ├── events.go │ │ │ │ ├── histogram.go │ │ │ │ └── trace.go │ │ │ └── websocket/ │ │ │ ├── client.go │ │ │ ├── dial.go │ │ │ ├── hybi.go │ │ │ ├── server.go │ │ │ └── websocket.go │ │ ├── oauth2/ │ │ │ ├── .travis.yml │ │ │ ├── CONTRIBUTING.md │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── deviceauth.go │ │ │ ├── internal/ │ │ │ │ ├── doc.go │ │ │ │ ├── oauth2.go │ │ │ │ ├── token.go │ │ │ │ └── transport.go │ │ │ ├── oauth2.go │ │ │ ├── pkce.go │ │ │ ├── token.go │ │ │ └── transport.go │ │ ├── sync/ │ │ │ ├── LICENSE │ │ │ ├── PATENTS │ │ │ └── errgroup/ │ │ │ └── errgroup.go │ │ ├── sys/ │ │ │ ├── LICENSE │ │ │ ├── PATENTS │ │ │ ├── cpu/ │ │ │ │ ├── asm_aix_ppc64.s │ │ │ │ ├── asm_darwin_x86_gc.s │ │ │ │ ├── byteorder.go │ │ │ │ ├── cpu.go │ │ │ │ ├── cpu_aix.go │ │ │ │ ├── cpu_arm.go │ │ │ │ ├── cpu_arm64.go │ │ │ │ ├── cpu_arm64.s │ │ │ │ ├── cpu_darwin_x86.go │ │ │ │ ├── cpu_gc_arm64.go │ │ │ │ ├── cpu_gc_s390x.go │ │ │ │ ├── cpu_gc_x86.go │ │ │ │ ├── cpu_gc_x86.s │ │ │ │ ├── cpu_gccgo_arm64.go │ │ │ │ ├── cpu_gccgo_s390x.go │ │ │ │ ├── cpu_gccgo_x86.c │ │ │ │ ├── cpu_gccgo_x86.go │ │ │ │ ├── cpu_linux.go │ │ │ │ ├── cpu_linux_arm.go │ │ │ │ ├── cpu_linux_arm64.go │ │ │ │ ├── cpu_linux_loong64.go │ │ │ │ ├── cpu_linux_mips64x.go │ │ │ │ ├── cpu_linux_noinit.go │ │ │ │ ├── cpu_linux_ppc64x.go │ │ │ │ ├── cpu_linux_riscv64.go │ │ │ │ ├── cpu_linux_s390x.go │ │ │ │ ├── cpu_loong64.go │ │ │ │ ├── cpu_loong64.s │ │ │ │ ├── cpu_mips64x.go │ │ │ │ ├── cpu_mipsx.go │ │ │ │ ├── cpu_netbsd_arm64.go │ │ │ │ ├── cpu_openbsd_arm64.go │ │ │ │ ├── cpu_openbsd_arm64.s │ │ │ │ ├── cpu_other_arm.go │ │ │ │ ├── cpu_other_arm64.go │ │ │ │ ├── cpu_other_mips64x.go │ │ │ │ ├── cpu_other_ppc64x.go │ │ │ │ ├── cpu_other_riscv64.go │ │ │ │ ├── cpu_other_x86.go │ │ │ │ ├── cpu_ppc64x.go │ │ │ │ ├── cpu_riscv64.go │ │ │ │ ├── cpu_s390x.go │ │ │ │ ├── cpu_s390x.s │ │ │ │ ├── cpu_wasm.go │ │ │ │ ├── cpu_x86.go │ │ │ │ ├── cpu_zos.go │ │ │ │ ├── cpu_zos_s390x.go │ │ │ │ ├── endian_big.go │ │ │ │ ├── endian_little.go │ │ │ │ ├── hwcap_linux.go │ │ │ │ ├── parse.go │ │ │ │ ├── proc_cpuinfo_linux.go │ │ │ │ ├── runtime_auxv.go │ │ │ │ ├── runtime_auxv_go121.go │ │ │ │ ├── syscall_aix_gccgo.go │ │ │ │ ├── syscall_aix_ppc64_gc.go │ │ │ │ └── syscall_darwin_x86_gc.go │ │ │ ├── execabs/ │ │ │ │ ├── execabs.go │ │ │ │ ├── execabs_go118.go │ │ │ │ └── execabs_go119.go │ │ │ ├── plan9/ │ │ │ │ ├── asm.s │ │ │ │ ├── asm_plan9_386.s │ │ │ │ ├── asm_plan9_amd64.s │ │ │ │ ├── asm_plan9_arm.s │ │ │ │ ├── const_plan9.go │ │ │ │ ├── dir_plan9.go │ │ │ │ ├── env_plan9.go │ │ │ │ ├── errors_plan9.go │ │ │ │ ├── mkall.sh │ │ │ │ ├── mkerrors.sh │ │ │ │ ├── mksysnum_plan9.sh │ │ │ │ ├── pwd_plan9.go │ │ │ │ ├── race.go │ │ │ │ ├── race0.go │ │ │ │ ├── str.go │ │ │ │ ├── syscall.go │ │ │ │ ├── syscall_plan9.go │ │ │ │ ├── zsyscall_plan9_386.go │ │ │ │ ├── zsyscall_plan9_amd64.go │ │ │ │ ├── zsyscall_plan9_arm.go │ │ │ │ └── zsysnum_plan9.go │ │ │ ├── unix/ │ │ │ │ ├── .gitignore │ │ │ │ ├── README.md │ │ │ │ ├── affinity_linux.go │ │ │ │ ├── aliases.go │ │ │ │ ├── asm_aix_ppc64.s │ │ │ │ ├── asm_bsd_386.s │ │ │ │ ├── asm_bsd_amd64.s │ │ │ │ ├── asm_bsd_arm.s │ │ │ │ ├── asm_bsd_arm64.s │ │ │ │ ├── asm_bsd_ppc64.s │ │ │ │ ├── asm_bsd_riscv64.s │ │ │ │ ├── asm_linux_386.s │ │ │ │ ├── asm_linux_amd64.s │ │ │ │ ├── asm_linux_arm.s │ │ │ │ ├── asm_linux_arm64.s │ │ │ │ ├── asm_linux_loong64.s │ │ │ │ ├── asm_linux_mips64x.s │ │ │ │ ├── asm_linux_mipsx.s │ │ │ │ ├── asm_linux_ppc64x.s │ │ │ │ ├── asm_linux_riscv64.s │ │ │ │ ├── asm_linux_s390x.s │ │ │ │ ├── asm_openbsd_mips64.s │ │ │ │ ├── asm_solaris_amd64.s │ │ │ │ ├── asm_zos_s390x.s │ │ │ │ ├── auxv.go │ │ │ │ ├── auxv_unsupported.go │ │ │ │ ├── bluetooth_linux.go │ │ │ │ ├── bpxsvc_zos.go │ │ │ │ ├── bpxsvc_zos.s │ │ │ │ ├── cap_freebsd.go │ │ │ │ ├── constants.go │ │ │ │ ├── dev_aix_ppc.go │ │ │ │ ├── dev_aix_ppc64.go │ │ │ │ ├── dev_darwin.go │ │ │ │ ├── dev_dragonfly.go │ │ │ │ ├── dev_freebsd.go │ │ │ │ ├── dev_linux.go │ │ │ │ ├── dev_netbsd.go │ │ │ │ ├── dev_openbsd.go │ │ │ │ ├── dev_zos.go │ │ │ │ ├── dirent.go │ │ │ │ ├── endian_big.go │ │ │ │ ├── endian_little.go │ │ │ │ ├── env_unix.go │ │ │ │ ├── fcntl.go │ │ │ │ ├── fcntl_darwin.go │ │ │ │ ├── fcntl_linux_32bit.go │ │ │ │ ├── fdset.go │ │ │ │ ├── gccgo.go │ │ │ │ ├── gccgo_c.c │ │ │ │ ├── gccgo_linux_amd64.go │ │ │ │ ├── ifreq_linux.go │ │ │ │ ├── ioctl_linux.go │ │ │ │ ├── ioctl_signed.go │ │ │ │ ├── ioctl_unsigned.go │ │ │ │ ├── ioctl_zos.go │ │ │ │ ├── mkall.sh │ │ │ │ ├── mkerrors.sh │ │ │ │ ├── mmap_nomremap.go │ │ │ │ ├── mremap.go │ │ │ │ ├── pagesize_unix.go │ │ │ │ ├── pledge_openbsd.go │ │ │ │ ├── ptrace_darwin.go │ │ │ │ ├── ptrace_ios.go │ │ │ │ ├── race.go │ │ │ │ ├── race0.go │ │ │ │ ├── readdirent_getdents.go │ │ │ │ ├── readdirent_getdirentries.go │ │ │ │ ├── sockcmsg_dragonfly.go │ │ │ │ ├── sockcmsg_linux.go │ │ │ │ ├── sockcmsg_unix.go │ │ │ │ ├── sockcmsg_unix_other.go │ │ │ │ ├── sockcmsg_zos.go │ │ │ │ ├── symaddr_zos_s390x.s │ │ │ │ ├── syscall.go │ │ │ │ ├── syscall_aix.go │ │ │ │ ├── syscall_aix_ppc.go │ │ │ │ ├── syscall_aix_ppc64.go │ │ │ │ ├── syscall_bsd.go │ │ │ │ ├── syscall_darwin.go │ │ │ │ ├── syscall_darwin_amd64.go │ │ │ │ ├── syscall_darwin_arm64.go │ │ │ │ ├── syscall_darwin_libSystem.go │ │ │ │ ├── syscall_dragonfly.go │ │ │ │ ├── syscall_dragonfly_amd64.go │ │ │ │ ├── syscall_freebsd.go │ │ │ │ ├── syscall_freebsd_386.go │ │ │ │ ├── syscall_freebsd_amd64.go │ │ │ │ ├── syscall_freebsd_arm.go │ │ │ │ ├── syscall_freebsd_arm64.go │ │ │ │ ├── syscall_freebsd_riscv64.go │ │ │ │ ├── syscall_hurd.go │ │ │ │ ├── syscall_hurd_386.go │ │ │ │ ├── syscall_illumos.go │ │ │ │ ├── syscall_linux.go │ │ │ │ ├── syscall_linux_386.go │ │ │ │ ├── syscall_linux_alarm.go │ │ │ │ ├── syscall_linux_amd64.go │ │ │ │ ├── syscall_linux_amd64_gc.go │ │ │ │ ├── syscall_linux_arm.go │ │ │ │ ├── syscall_linux_arm64.go │ │ │ │ ├── syscall_linux_gc.go │ │ │ │ ├── syscall_linux_gc_386.go │ │ │ │ ├── syscall_linux_gc_arm.go │ │ │ │ ├── syscall_linux_gccgo_386.go │ │ │ │ ├── syscall_linux_gccgo_arm.go │ │ │ │ ├── syscall_linux_loong64.go │ │ │ │ ├── syscall_linux_mips64x.go │ │ │ │ ├── syscall_linux_mipsx.go │ │ │ │ ├── syscall_linux_ppc.go │ │ │ │ ├── syscall_linux_ppc64x.go │ │ │ │ ├── syscall_linux_riscv64.go │ │ │ │ ├── syscall_linux_s390x.go │ │ │ │ ├── syscall_linux_sparc64.go │ │ │ │ ├── syscall_netbsd.go │ │ │ │ ├── syscall_netbsd_386.go │ │ │ │ ├── syscall_netbsd_amd64.go │ │ │ │ ├── syscall_netbsd_arm.go │ │ │ │ ├── syscall_netbsd_arm64.go │ │ │ │ ├── syscall_openbsd.go │ │ │ │ ├── syscall_openbsd_386.go │ │ │ │ ├── syscall_openbsd_amd64.go │ │ │ │ ├── syscall_openbsd_arm.go │ │ │ │ ├── syscall_openbsd_arm64.go │ │ │ │ ├── syscall_openbsd_libc.go │ │ │ │ ├── syscall_openbsd_mips64.go │ │ │ │ ├── syscall_openbsd_ppc64.go │ │ │ │ ├── syscall_openbsd_riscv64.go │ │ │ │ ├── syscall_solaris.go │ │ │ │ ├── syscall_solaris_amd64.go │ │ │ │ ├── syscall_unix.go │ │ │ │ ├── syscall_unix_gc.go │ │ │ │ ├── syscall_unix_gc_ppc64x.go │ │ │ │ ├── syscall_zos_s390x.go │ │ │ │ ├── sysvshm_linux.go │ │ │ │ ├── sysvshm_unix.go │ │ │ │ ├── sysvshm_unix_other.go │ │ │ │ ├── timestruct.go │ │ │ │ ├── unveil_openbsd.go │ │ │ │ ├── vgetrandom_linux.go │ │ │ │ ├── vgetrandom_unsupported.go │ │ │ │ ├── xattr_bsd.go │ │ │ │ ├── zerrors_aix_ppc.go │ │ │ │ ├── zerrors_aix_ppc64.go │ │ │ │ ├── zerrors_darwin_amd64.go │ │ │ │ ├── zerrors_darwin_arm64.go │ │ │ │ ├── zerrors_dragonfly_amd64.go │ │ │ │ ├── zerrors_freebsd_386.go │ │ │ │ ├── zerrors_freebsd_amd64.go │ │ │ │ ├── zerrors_freebsd_arm.go │ │ │ │ ├── zerrors_freebsd_arm64.go │ │ │ │ ├── zerrors_freebsd_riscv64.go │ │ │ │ ├── zerrors_linux.go │ │ │ │ ├── zerrors_linux_386.go │ │ │ │ ├── zerrors_linux_amd64.go │ │ │ │ ├── zerrors_linux_arm.go │ │ │ │ ├── zerrors_linux_arm64.go │ │ │ │ ├── zerrors_linux_loong64.go │ │ │ │ ├── zerrors_linux_mips.go │ │ │ │ ├── zerrors_linux_mips64.go │ │ │ │ ├── zerrors_linux_mips64le.go │ │ │ │ ├── zerrors_linux_mipsle.go │ │ │ │ ├── zerrors_linux_ppc.go │ │ │ │ ├── zerrors_linux_ppc64.go │ │ │ │ ├── zerrors_linux_ppc64le.go │ │ │ │ ├── zerrors_linux_riscv64.go │ │ │ │ ├── zerrors_linux_s390x.go │ │ │ │ ├── zerrors_linux_sparc64.go │ │ │ │ ├── zerrors_netbsd_386.go │ │ │ │ ├── zerrors_netbsd_amd64.go │ │ │ │ ├── zerrors_netbsd_arm.go │ │ │ │ ├── zerrors_netbsd_arm64.go │ │ │ │ ├── zerrors_openbsd_386.go │ │ │ │ ├── zerrors_openbsd_amd64.go │ │ │ │ ├── zerrors_openbsd_arm.go │ │ │ │ ├── zerrors_openbsd_arm64.go │ │ │ │ ├── zerrors_openbsd_mips64.go │ │ │ │ ├── zerrors_openbsd_ppc64.go │ │ │ │ ├── zerrors_openbsd_riscv64.go │ │ │ │ ├── zerrors_solaris_amd64.go │ │ │ │ ├── zerrors_zos_s390x.go │ │ │ │ ├── zptrace_armnn_linux.go │ │ │ │ ├── zptrace_linux_arm64.go │ │ │ │ ├── zptrace_mipsnn_linux.go │ │ │ │ ├── zptrace_mipsnnle_linux.go │ │ │ │ ├── zptrace_x86_linux.go │ │ │ │ ├── zsymaddr_zos_s390x.s │ │ │ │ ├── zsyscall_aix_ppc.go │ │ │ │ ├── zsyscall_aix_ppc64.go │ │ │ │ ├── zsyscall_aix_ppc64_gc.go │ │ │ │ ├── zsyscall_aix_ppc64_gccgo.go │ │ │ │ ├── zsyscall_darwin_amd64.go │ │ │ │ ├── zsyscall_darwin_amd64.s │ │ │ │ ├── zsyscall_darwin_arm64.go │ │ │ │ ├── zsyscall_darwin_arm64.s │ │ │ │ ├── zsyscall_dragonfly_amd64.go │ │ │ │ ├── zsyscall_freebsd_386.go │ │ │ │ ├── zsyscall_freebsd_amd64.go │ │ │ │ ├── zsyscall_freebsd_arm.go │ │ │ │ ├── zsyscall_freebsd_arm64.go │ │ │ │ ├── zsyscall_freebsd_riscv64.go │ │ │ │ ├── zsyscall_illumos_amd64.go │ │ │ │ ├── zsyscall_linux.go │ │ │ │ ├── zsyscall_linux_386.go │ │ │ │ ├── zsyscall_linux_amd64.go │ │ │ │ ├── zsyscall_linux_arm.go │ │ │ │ ├── zsyscall_linux_arm64.go │ │ │ │ ├── zsyscall_linux_loong64.go │ │ │ │ ├── zsyscall_linux_mips.go │ │ │ │ ├── zsyscall_linux_mips64.go │ │ │ │ ├── zsyscall_linux_mips64le.go │ │ │ │ ├── zsyscall_linux_mipsle.go │ │ │ │ ├── zsyscall_linux_ppc.go │ │ │ │ ├── zsyscall_linux_ppc64.go │ │ │ │ ├── zsyscall_linux_ppc64le.go │ │ │ │ ├── zsyscall_linux_riscv64.go │ │ │ │ ├── zsyscall_linux_s390x.go │ │ │ │ ├── zsyscall_linux_sparc64.go │ │ │ │ ├── zsyscall_netbsd_386.go │ │ │ │ ├── zsyscall_netbsd_amd64.go │ │ │ │ ├── zsyscall_netbsd_arm.go │ │ │ │ ├── zsyscall_netbsd_arm64.go │ │ │ │ ├── zsyscall_openbsd_386.go │ │ │ │ ├── zsyscall_openbsd_386.s │ │ │ │ ├── zsyscall_openbsd_amd64.go │ │ │ │ ├── zsyscall_openbsd_amd64.s │ │ │ │ ├── zsyscall_openbsd_arm.go │ │ │ │ ├── zsyscall_openbsd_arm.s │ │ │ │ ├── zsyscall_openbsd_arm64.go │ │ │ │ ├── zsyscall_openbsd_arm64.s │ │ │ │ ├── zsyscall_openbsd_mips64.go │ │ │ │ ├── zsyscall_openbsd_mips64.s │ │ │ │ ├── zsyscall_openbsd_ppc64.go │ │ │ │ ├── zsyscall_openbsd_ppc64.s │ │ │ │ ├── zsyscall_openbsd_riscv64.go │ │ │ │ ├── zsyscall_openbsd_riscv64.s │ │ │ │ ├── zsyscall_solaris_amd64.go │ │ │ │ ├── zsyscall_zos_s390x.go │ │ │ │ ├── zsysctl_openbsd_386.go │ │ │ │ ├── zsysctl_openbsd_amd64.go │ │ │ │ ├── zsysctl_openbsd_arm.go │ │ │ │ ├── zsysctl_openbsd_arm64.go │ │ │ │ ├── zsysctl_openbsd_mips64.go │ │ │ │ ├── zsysctl_openbsd_ppc64.go │ │ │ │ ├── zsysctl_openbsd_riscv64.go │ │ │ │ ├── zsysnum_darwin_amd64.go │ │ │ │ ├── zsysnum_darwin_arm64.go │ │ │ │ ├── zsysnum_dragonfly_amd64.go │ │ │ │ ├── zsysnum_freebsd_386.go │ │ │ │ ├── zsysnum_freebsd_amd64.go │ │ │ │ ├── zsysnum_freebsd_arm.go │ │ │ │ ├── zsysnum_freebsd_arm64.go │ │ │ │ ├── zsysnum_freebsd_riscv64.go │ │ │ │ ├── zsysnum_linux_386.go │ │ │ │ ├── zsysnum_linux_amd64.go │ │ │ │ ├── zsysnum_linux_arm.go │ │ │ │ ├── zsysnum_linux_arm64.go │ │ │ │ ├── zsysnum_linux_loong64.go │ │ │ │ ├── zsysnum_linux_mips.go │ │ │ │ ├── zsysnum_linux_mips64.go │ │ │ │ ├── zsysnum_linux_mips64le.go │ │ │ │ ├── zsysnum_linux_mipsle.go │ │ │ │ ├── zsysnum_linux_ppc.go │ │ │ │ ├── zsysnum_linux_ppc64.go │ │ │ │ ├── zsysnum_linux_ppc64le.go │ │ │ │ ├── zsysnum_linux_riscv64.go │ │ │ │ ├── zsysnum_linux_s390x.go │ │ │ │ ├── zsysnum_linux_sparc64.go │ │ │ │ ├── zsysnum_netbsd_386.go │ │ │ │ ├── zsysnum_netbsd_amd64.go │ │ │ │ ├── zsysnum_netbsd_arm.go │ │ │ │ ├── zsysnum_netbsd_arm64.go │ │ │ │ ├── zsysnum_openbsd_386.go │ │ │ │ ├── zsysnum_openbsd_amd64.go │ │ │ │ ├── zsysnum_openbsd_arm.go │ │ │ │ ├── zsysnum_openbsd_arm64.go │ │ │ │ ├── zsysnum_openbsd_mips64.go │ │ │ │ ├── zsysnum_openbsd_ppc64.go │ │ │ │ ├── zsysnum_openbsd_riscv64.go │ │ │ │ ├── zsysnum_zos_s390x.go │ │ │ │ ├── ztypes_aix_ppc.go │ │ │ │ ├── ztypes_aix_ppc64.go │ │ │ │ ├── ztypes_darwin_amd64.go │ │ │ │ ├── ztypes_darwin_arm64.go │ │ │ │ ├── ztypes_dragonfly_amd64.go │ │ │ │ ├── ztypes_freebsd_386.go │ │ │ │ ├── ztypes_freebsd_amd64.go │ │ │ │ ├── ztypes_freebsd_arm.go │ │ │ │ ├── ztypes_freebsd_arm64.go │ │ │ │ ├── ztypes_freebsd_riscv64.go │ │ │ │ ├── ztypes_linux.go │ │ │ │ ├── ztypes_linux_386.go │ │ │ │ ├── ztypes_linux_amd64.go │ │ │ │ ├── ztypes_linux_arm.go │ │ │ │ ├── ztypes_linux_arm64.go │ │ │ │ ├── ztypes_linux_loong64.go │ │ │ │ ├── ztypes_linux_mips.go │ │ │ │ ├── ztypes_linux_mips64.go │ │ │ │ ├── ztypes_linux_mips64le.go │ │ │ │ ├── ztypes_linux_mipsle.go │ │ │ │ ├── ztypes_linux_ppc.go │ │ │ │ ├── ztypes_linux_ppc64.go │ │ │ │ ├── ztypes_linux_ppc64le.go │ │ │ │ ├── ztypes_linux_riscv64.go │ │ │ │ ├── ztypes_linux_s390x.go │ │ │ │ ├── ztypes_linux_sparc64.go │ │ │ │ ├── ztypes_netbsd_386.go │ │ │ │ ├── ztypes_netbsd_amd64.go │ │ │ │ ├── ztypes_netbsd_arm.go │ │ │ │ ├── ztypes_netbsd_arm64.go │ │ │ │ ├── ztypes_openbsd_386.go │ │ │ │ ├── ztypes_openbsd_amd64.go │ │ │ │ ├── ztypes_openbsd_arm.go │ │ │ │ ├── ztypes_openbsd_arm64.go │ │ │ │ ├── ztypes_openbsd_mips64.go │ │ │ │ ├── ztypes_openbsd_ppc64.go │ │ │ │ ├── ztypes_openbsd_riscv64.go │ │ │ │ ├── ztypes_solaris_amd64.go │ │ │ │ └── ztypes_zos_s390x.go │ │ │ └── windows/ │ │ │ ├── aliases.go │ │ │ ├── dll_windows.go │ │ │ ├── env_windows.go │ │ │ ├── eventlog.go │ │ │ ├── exec_windows.go │ │ │ ├── memory_windows.go │ │ │ ├── mkerrors.bash │ │ │ ├── mkknownfolderids.bash │ │ │ ├── mksyscall.go │ │ │ ├── race.go │ │ │ ├── race0.go │ │ │ ├── registry/ │ │ │ │ ├── key.go │ │ │ │ ├── mksyscall.go │ │ │ │ ├── syscall.go │ │ │ │ ├── value.go │ │ │ │ └── zsyscall_windows.go │ │ │ ├── security_windows.go │ │ │ ├── service.go │ │ │ ├── setupapi_windows.go │ │ │ ├── str.go │ │ │ ├── svc/ │ │ │ │ ├── eventlog/ │ │ │ │ │ ├── install.go │ │ │ │ │ └── log.go │ │ │ │ ├── mgr/ │ │ │ │ │ ├── config.go │ │ │ │ │ ├── mgr.go │ │ │ │ │ ├── recovery.go │ │ │ │ │ └── service.go │ │ │ │ ├── security.go │ │ │ │ └── service.go │ │ │ ├── syscall.go │ │ │ ├── syscall_windows.go │ │ │ ├── types_windows.go │ │ │ ├── types_windows_386.go │ │ │ ├── types_windows_amd64.go │ │ │ ├── types_windows_arm.go │ │ │ ├── types_windows_arm64.go │ │ │ ├── zerrors_windows.go │ │ │ ├── zknownfolderids_windows.go │ │ │ └── zsyscall_windows.go │ │ ├── term/ │ │ │ ├── CONTRIBUTING.md │ │ │ ├── LICENSE │ │ │ ├── PATENTS │ │ │ ├── README.md │ │ │ ├── codereview.cfg │ │ │ ├── term.go │ │ │ ├── term_plan9.go │ │ │ ├── term_unix.go │ │ │ ├── term_unix_bsd.go │ │ │ ├── term_unix_other.go │ │ │ ├── term_unsupported.go │ │ │ ├── term_windows.go │ │ │ └── terminal.go │ │ ├── text/ │ │ │ ├── LICENSE │ │ │ ├── PATENTS │ │ │ ├── cases/ │ │ │ │ ├── cases.go │ │ │ │ ├── context.go │ │ │ │ ├── fold.go │ │ │ │ ├── icu.go │ │ │ │ ├── info.go │ │ │ │ ├── map.go │ │ │ │ ├── tables10.0.0.go │ │ │ │ ├── tables11.0.0.go │ │ │ │ ├── tables12.0.0.go │ │ │ │ ├── tables13.0.0.go │ │ │ │ ├── tables15.0.0.go │ │ │ │ ├── tables9.0.0.go │ │ │ │ └── trieval.go │ │ │ ├── internal/ │ │ │ │ ├── internal.go │ │ │ │ ├── language/ │ │ │ │ │ ├── common.go │ │ │ │ │ ├── compact/ │ │ │ │ │ │ ├── compact.go │ │ │ │ │ │ ├── language.go │ │ │ │ │ │ ├── parents.go │ │ │ │ │ │ ├── tables.go │ │ │ │ │ │ └── tags.go │ │ │ │ │ ├── compact.go │ │ │ │ │ ├── compose.go │ │ │ │ │ ├── coverage.go │ │ │ │ │ ├── language.go │ │ │ │ │ ├── lookup.go │ │ │ │ │ ├── match.go │ │ │ │ │ ├── parse.go │ │ │ │ │ ├── tables.go │ │ │ │ │ └── tags.go │ │ │ │ ├── match.go │ │ │ │ └── tag/ │ │ │ │ └── tag.go │ │ │ ├── language/ │ │ │ │ ├── coverage.go │ │ │ │ ├── doc.go │ │ │ │ ├── language.go │ │ │ │ ├── match.go │ │ │ │ ├── parse.go │ │ │ │ ├── tables.go │ │ │ │ └── tags.go │ │ │ ├── secure/ │ │ │ │ └── bidirule/ │ │ │ │ ├── bidirule.go │ │ │ │ ├── bidirule10.0.0.go │ │ │ │ └── bidirule9.0.0.go │ │ │ ├── transform/ │ │ │ │ └── transform.go │ │ │ └── unicode/ │ │ │ ├── bidi/ │ │ │ │ ├── bidi.go │ │ │ │ ├── bracket.go │ │ │ │ ├── core.go │ │ │ │ ├── prop.go │ │ │ │ ├── tables10.0.0.go │ │ │ │ ├── tables11.0.0.go │ │ │ │ ├── tables12.0.0.go │ │ │ │ ├── tables13.0.0.go │ │ │ │ ├── tables15.0.0.go │ │ │ │ ├── tables9.0.0.go │ │ │ │ └── trieval.go │ │ │ └── norm/ │ │ │ ├── composition.go │ │ │ ├── forminfo.go │ │ │ ├── input.go │ │ │ ├── iter.go │ │ │ ├── normalize.go │ │ │ ├── readwriter.go │ │ │ ├── tables10.0.0.go │ │ │ ├── tables11.0.0.go │ │ │ ├── tables12.0.0.go │ │ │ ├── tables13.0.0.go │ │ │ ├── tables15.0.0.go │ │ │ ├── tables9.0.0.go │ │ │ ├── transform.go │ │ │ └── trie.go │ │ └── tools/ │ │ ├── LICENSE │ │ ├── PATENTS │ │ ├── cover/ │ │ │ └── profile.go │ │ ├── go/ │ │ │ ├── ast/ │ │ │ │ ├── astutil/ │ │ │ │ │ ├── enclosing.go │ │ │ │ │ ├── imports.go │ │ │ │ │ ├── rewrite.go │ │ │ │ │ └── util.go │ │ │ │ └── inspector/ │ │ │ │ ├── inspector.go │ │ │ │ ├── iter.go │ │ │ │ ├── typeof.go │ │ │ │ └── walk.go │ │ │ ├── gcexportdata/ │ │ │ │ ├── gcexportdata.go │ │ │ │ └── importer.go │ │ │ ├── packages/ │ │ │ │ ├── doc.go │ │ │ │ ├── external.go │ │ │ │ ├── golist.go │ │ │ │ ├── golist_overlay.go │ │ │ │ ├── loadmode_string.go │ │ │ │ ├── packages.go │ │ │ │ └── visit.go │ │ │ └── types/ │ │ │ ├── objectpath/ │ │ │ │ └── objectpath.go │ │ │ └── typeutil/ │ │ │ ├── callee.go │ │ │ ├── imports.go │ │ │ ├── map.go │ │ │ ├── methodsetcache.go │ │ │ └── ui.go │ │ ├── imports/ │ │ │ └── forward.go │ │ └── internal/ │ │ ├── aliases/ │ │ │ ├── aliases.go │ │ │ └── aliases_go122.go │ │ ├── astutil/ │ │ │ └── edge/ │ │ │ └── edge.go │ │ ├── event/ │ │ │ ├── core/ │ │ │ │ ├── event.go │ │ │ │ ├── export.go │ │ │ │ └── fast.go │ │ │ ├── doc.go │ │ │ ├── event.go │ │ │ ├── keys/ │ │ │ │ ├── keys.go │ │ │ │ ├── standard.go │ │ │ │ └── util.go │ │ │ └── label/ │ │ │ └── label.go │ │ ├── gcimporter/ │ │ │ ├── bimport.go │ │ │ ├── exportdata.go │ │ │ ├── gcimporter.go │ │ │ ├── iexport.go │ │ │ ├── iimport.go │ │ │ ├── iimport_go122.go │ │ │ ├── predeclared.go │ │ │ ├── support.go │ │ │ └── ureader_yes.go │ │ ├── gocommand/ │ │ │ ├── invoke.go │ │ │ ├── invoke_notunix.go │ │ │ ├── invoke_unix.go │ │ │ ├── vendor.go │ │ │ └── version.go │ │ ├── gopathwalk/ │ │ │ └── walk.go │ │ ├── imports/ │ │ │ ├── fix.go │ │ │ ├── imports.go │ │ │ ├── mod.go │ │ │ ├── mod_cache.go │ │ │ ├── sortimports.go │ │ │ ├── source.go │ │ │ ├── source_env.go │ │ │ └── source_modindex.go │ │ ├── modindex/ │ │ │ ├── directories.go │ │ │ ├── index.go │ │ │ ├── lookup.go │ │ │ ├── modindex.go │ │ │ ├── symbols.go │ │ │ └── types.go │ │ ├── packagesinternal/ │ │ │ └── packages.go │ │ ├── pkgbits/ │ │ │ ├── codes.go │ │ │ ├── decoder.go │ │ │ ├── doc.go │ │ │ ├── encoder.go │ │ │ ├── flags.go │ │ │ ├── reloc.go │ │ │ ├── support.go │ │ │ ├── sync.go │ │ │ ├── syncmarker_string.go │ │ │ └── version.go │ │ ├── stdlib/ │ │ │ ├── deps.go │ │ │ ├── import.go │ │ │ ├── manifest.go │ │ │ └── stdlib.go │ │ ├── typeparams/ │ │ │ ├── common.go │ │ │ ├── coretype.go │ │ │ ├── free.go │ │ │ ├── normalize.go │ │ │ ├── termlist.go │ │ │ └── typeterm.go │ │ ├── typesinternal/ │ │ │ ├── classify_call.go │ │ │ ├── element.go │ │ │ ├── errorcode.go │ │ │ ├── errorcode_string.go │ │ │ ├── qualifier.go │ │ │ ├── recv.go │ │ │ ├── toonew.go │ │ │ ├── types.go │ │ │ ├── varkind.go │ │ │ └── zerovalue.go │ │ └── versions/ │ │ ├── features.go │ │ ├── gover.go │ │ ├── types.go │ │ └── versions.go │ ├── google.golang.org/ │ │ ├── genproto/ │ │ │ └── googleapis/ │ │ │ ├── api/ │ │ │ │ ├── LICENSE │ │ │ │ └── httpbody/ │ │ │ │ └── httpbody.pb.go │ │ │ └── rpc/ │ │ │ ├── LICENSE │ │ │ └── status/ │ │ │ └── status.pb.go │ │ ├── grpc/ │ │ │ ├── AUTHORS │ │ │ ├── CODE-OF-CONDUCT.md │ │ │ ├── CONTRIBUTING.md │ │ │ ├── GOVERNANCE.md │ │ │ ├── LICENSE │ │ │ ├── MAINTAINERS.md │ │ │ ├── Makefile │ │ │ ├── NOTICE.txt │ │ │ ├── README.md │ │ │ ├── SECURITY.md │ │ │ ├── attributes/ │ │ │ │ └── attributes.go │ │ │ ├── backoff/ │ │ │ │ └── backoff.go │ │ │ ├── backoff.go │ │ │ ├── balancer/ │ │ │ │ ├── balancer.go │ │ │ │ ├── base/ │ │ │ │ │ ├── balancer.go │ │ │ │ │ └── base.go │ │ │ │ ├── conn_state_evaluator.go │ │ │ │ ├── endpointsharding/ │ │ │ │ │ └── endpointsharding.go │ │ │ │ ├── grpclb/ │ │ │ │ │ └── state/ │ │ │ │ │ └── state.go │ │ │ │ ├── pickfirst/ │ │ │ │ │ ├── internal/ │ │ │ │ │ │ └── internal.go │ │ │ │ │ ├── pickfirst.go │ │ │ │ │ └── pickfirstleaf/ │ │ │ │ │ └── pickfirstleaf.go │ │ │ │ ├── roundrobin/ │ │ │ │ │ └── roundrobin.go │ │ │ │ └── subconn.go │ │ │ ├── balancer_wrapper.go │ │ │ ├── binarylog/ │ │ │ │ └── grpc_binarylog_v1/ │ │ │ │ └── binarylog.pb.go │ │ │ ├── call.go │ │ │ ├── channelz/ │ │ │ │ └── channelz.go │ │ │ ├── clientconn.go │ │ │ ├── codec.go │ │ │ ├── codes/ │ │ │ │ ├── code_string.go │ │ │ │ └── codes.go │ │ │ ├── connectivity/ │ │ │ │ └── connectivity.go │ │ │ ├── credentials/ │ │ │ │ ├── credentials.go │ │ │ │ ├── insecure/ │ │ │ │ │ └── insecure.go │ │ │ │ └── tls.go │ │ │ ├── dialoptions.go │ │ │ ├── doc.go │ │ │ ├── encoding/ │ │ │ │ ├── encoding.go │ │ │ │ ├── encoding_v2.go │ │ │ │ └── proto/ │ │ │ │ └── proto.go │ │ │ ├── experimental/ │ │ │ │ └── stats/ │ │ │ │ ├── metricregistry.go │ │ │ │ └── metrics.go │ │ │ ├── grpclog/ │ │ │ │ ├── component.go │ │ │ │ ├── grpclog.go │ │ │ │ ├── internal/ │ │ │ │ │ ├── grpclog.go │ │ │ │ │ ├── logger.go │ │ │ │ │ └── loggerv2.go │ │ │ │ ├── logger.go │ │ │ │ └── loggerv2.go │ │ │ ├── health/ │ │ │ │ └── grpc_health_v1/ │ │ │ │ ├── health.pb.go │ │ │ │ └── health_grpc.pb.go │ │ │ ├── interceptor.go │ │ │ ├── internal/ │ │ │ │ ├── backoff/ │ │ │ │ │ └── backoff.go │ │ │ │ ├── balancer/ │ │ │ │ │ └── gracefulswitch/ │ │ │ │ │ ├── config.go │ │ │ │ │ └── gracefulswitch.go │ │ │ │ ├── balancerload/ │ │ │ │ │ └── load.go │ │ │ │ ├── binarylog/ │ │ │ │ │ ├── binarylog.go │ │ │ │ │ ├── binarylog_testutil.go │ │ │ │ │ ├── env_config.go │ │ │ │ │ ├── method_logger.go │ │ │ │ │ └── sink.go │ │ │ │ ├── buffer/ │ │ │ │ │ └── unbounded.go │ │ │ │ ├── channelz/ │ │ │ │ │ ├── channel.go │ │ │ │ │ ├── channelmap.go │ │ │ │ │ ├── funcs.go │ │ │ │ │ ├── logging.go │ │ │ │ │ ├── server.go │ │ │ │ │ ├── socket.go │ │ │ │ │ ├── subchannel.go │ │ │ │ │ ├── syscall_linux.go │ │ │ │ │ ├── syscall_nonlinux.go │ │ │ │ │ └── trace.go │ │ │ │ ├── credentials/ │ │ │ │ │ ├── credentials.go │ │ │ │ │ ├── spiffe.go │ │ │ │ │ ├── syscallconn.go │ │ │ │ │ └── util.go │ │ │ │ ├── envconfig/ │ │ │ │ │ ├── envconfig.go │ │ │ │ │ ├── observability.go │ │ │ │ │ └── xds.go │ │ │ │ ├── experimental.go │ │ │ │ ├── grpclog/ │ │ │ │ │ └── prefix_logger.go │ │ │ │ ├── grpcsync/ │ │ │ │ │ ├── callback_serializer.go │ │ │ │ │ ├── event.go │ │ │ │ │ └── pubsub.go │ │ │ │ ├── grpcutil/ │ │ │ │ │ ├── compressor.go │ │ │ │ │ ├── encode_duration.go │ │ │ │ │ ├── grpcutil.go │ │ │ │ │ ├── metadata.go │ │ │ │ │ ├── method.go │ │ │ │ │ └── regex.go │ │ │ │ ├── idle/ │ │ │ │ │ └── idle.go │ │ │ │ ├── internal.go │ │ │ │ ├── metadata/ │ │ │ │ │ └── metadata.go │ │ │ │ ├── pretty/ │ │ │ │ │ └── pretty.go │ │ │ │ ├── proxyattributes/ │ │ │ │ │ └── proxyattributes.go │ │ │ │ ├── resolver/ │ │ │ │ │ ├── config_selector.go │ │ │ │ │ ├── delegatingresolver/ │ │ │ │ │ │ └── delegatingresolver.go │ │ │ │ │ ├── dns/ │ │ │ │ │ │ ├── dns_resolver.go │ │ │ │ │ │ └── internal/ │ │ │ │ │ │ └── internal.go │ │ │ │ │ ├── passthrough/ │ │ │ │ │ │ └── passthrough.go │ │ │ │ │ └── unix/ │ │ │ │ │ └── unix.go │ │ │ │ ├── serviceconfig/ │ │ │ │ │ ├── duration.go │ │ │ │ │ └── serviceconfig.go │ │ │ │ ├── stats/ │ │ │ │ │ ├── labels.go │ │ │ │ │ └── metrics_recorder_list.go │ │ │ │ ├── status/ │ │ │ │ │ └── status.go │ │ │ │ ├── syscall/ │ │ │ │ │ ├── syscall_linux.go │ │ │ │ │ └── syscall_nonlinux.go │ │ │ │ ├── tcp_keepalive_others.go │ │ │ │ ├── tcp_keepalive_unix.go │ │ │ │ ├── tcp_keepalive_windows.go │ │ │ │ └── transport/ │ │ │ │ ├── bdp_estimator.go │ │ │ │ ├── client_stream.go │ │ │ │ ├── controlbuf.go │ │ │ │ ├── defaults.go │ │ │ │ ├── flowcontrol.go │ │ │ │ ├── handler_server.go │ │ │ │ ├── http2_client.go │ │ │ │ ├── http2_server.go │ │ │ │ ├── http_util.go │ │ │ │ ├── logging.go │ │ │ │ ├── networktype/ │ │ │ │ │ └── networktype.go │ │ │ │ ├── proxy.go │ │ │ │ ├── server_stream.go │ │ │ │ └── transport.go │ │ │ ├── keepalive/ │ │ │ │ └── keepalive.go │ │ │ ├── mem/ │ │ │ │ ├── buffer_pool.go │ │ │ │ ├── buffer_slice.go │ │ │ │ └── buffers.go │ │ │ ├── metadata/ │ │ │ │ └── metadata.go │ │ │ ├── peer/ │ │ │ │ └── peer.go │ │ │ ├── picker_wrapper.go │ │ │ ├── preloader.go │ │ │ ├── resolver/ │ │ │ │ ├── dns/ │ │ │ │ │ └── dns_resolver.go │ │ │ │ ├── map.go │ │ │ │ └── resolver.go │ │ │ ├── resolver_wrapper.go │ │ │ ├── rpc_util.go │ │ │ ├── server.go │ │ │ ├── service_config.go │ │ │ ├── serviceconfig/ │ │ │ │ └── serviceconfig.go │ │ │ ├── stats/ │ │ │ │ ├── handlers.go │ │ │ │ ├── metrics.go │ │ │ │ └── stats.go │ │ │ ├── status/ │ │ │ │ └── status.go │ │ │ ├── stream.go │ │ │ ├── stream_interfaces.go │ │ │ ├── tap/ │ │ │ │ └── tap.go │ │ │ ├── trace.go │ │ │ ├── trace_notrace.go │ │ │ ├── trace_withtrace.go │ │ │ └── version.go │ │ └── protobuf/ │ │ ├── LICENSE │ │ ├── PATENTS │ │ ├── encoding/ │ │ │ ├── protodelim/ │ │ │ │ └── protodelim.go │ │ │ ├── protojson/ │ │ │ │ ├── decode.go │ │ │ │ ├── doc.go │ │ │ │ ├── encode.go │ │ │ │ └── well_known_types.go │ │ │ ├── prototext/ │ │ │ │ ├── decode.go │ │ │ │ ├── doc.go │ │ │ │ └── encode.go │ │ │ └── protowire/ │ │ │ └── wire.go │ │ ├── internal/ │ │ │ ├── descfmt/ │ │ │ │ └── stringer.go │ │ │ ├── descopts/ │ │ │ │ └── options.go │ │ │ ├── detrand/ │ │ │ │ └── rand.go │ │ │ ├── editiondefaults/ │ │ │ │ ├── defaults.go │ │ │ │ └── editions_defaults.binpb │ │ │ ├── encoding/ │ │ │ │ ├── defval/ │ │ │ │ │ └── default.go │ │ │ │ ├── json/ │ │ │ │ │ ├── decode.go │ │ │ │ │ ├── decode_number.go │ │ │ │ │ ├── decode_string.go │ │ │ │ │ ├── decode_token.go │ │ │ │ │ └── encode.go │ │ │ │ ├── messageset/ │ │ │ │ │ └── messageset.go │ │ │ │ ├── tag/ │ │ │ │ │ └── tag.go │ │ │ │ └── text/ │ │ │ │ ├── decode.go │ │ │ │ ├── decode_number.go │ │ │ │ ├── decode_string.go │ │ │ │ ├── decode_token.go │ │ │ │ ├── doc.go │ │ │ │ └── encode.go │ │ │ ├── errors/ │ │ │ │ └── errors.go │ │ │ ├── filedesc/ │ │ │ │ ├── build.go │ │ │ │ ├── desc.go │ │ │ │ ├── desc_init.go │ │ │ │ ├── desc_lazy.go │ │ │ │ ├── desc_list.go │ │ │ │ ├── desc_list_gen.go │ │ │ │ ├── editions.go │ │ │ │ └── placeholder.go │ │ │ ├── filetype/ │ │ │ │ └── build.go │ │ │ ├── flags/ │ │ │ │ ├── flags.go │ │ │ │ ├── proto_legacy_disable.go │ │ │ │ └── proto_legacy_enable.go │ │ │ ├── genid/ │ │ │ │ ├── any_gen.go │ │ │ │ ├── api_gen.go │ │ │ │ ├── descriptor_gen.go │ │ │ │ ├── doc.go │ │ │ │ ├── duration_gen.go │ │ │ │ ├── empty_gen.go │ │ │ │ ├── field_mask_gen.go │ │ │ │ ├── go_features_gen.go │ │ │ │ ├── goname.go │ │ │ │ ├── map_entry.go │ │ │ │ ├── name.go │ │ │ │ ├── source_context_gen.go │ │ │ │ ├── struct_gen.go │ │ │ │ ├── timestamp_gen.go │ │ │ │ ├── type_gen.go │ │ │ │ ├── wrappers.go │ │ │ │ └── wrappers_gen.go │ │ │ ├── impl/ │ │ │ │ ├── api_export.go │ │ │ │ ├── api_export_opaque.go │ │ │ │ ├── bitmap.go │ │ │ │ ├── bitmap_race.go │ │ │ │ ├── checkinit.go │ │ │ │ ├── codec_extension.go │ │ │ │ ├── codec_field.go │ │ │ │ ├── codec_field_opaque.go │ │ │ │ ├── codec_gen.go │ │ │ │ ├── codec_map.go │ │ │ │ ├── codec_message.go │ │ │ │ ├── codec_message_opaque.go │ │ │ │ ├── codec_messageset.go │ │ │ │ ├── codec_tables.go │ │ │ │ ├── codec_unsafe.go │ │ │ │ ├── convert.go │ │ │ │ ├── convert_list.go │ │ │ │ ├── convert_map.go │ │ │ │ ├── decode.go │ │ │ │ ├── encode.go │ │ │ │ ├── enum.go │ │ │ │ ├── equal.go │ │ │ │ ├── extension.go │ │ │ │ ├── lazy.go │ │ │ │ ├── legacy_enum.go │ │ │ │ ├── legacy_export.go │ │ │ │ ├── legacy_extension.go │ │ │ │ ├── legacy_file.go │ │ │ │ ├── legacy_message.go │ │ │ │ ├── merge.go │ │ │ │ ├── merge_gen.go │ │ │ │ ├── message.go │ │ │ │ ├── message_opaque.go │ │ │ │ ├── message_opaque_gen.go │ │ │ │ ├── message_reflect.go │ │ │ │ ├── message_reflect_field.go │ │ │ │ ├── message_reflect_field_gen.go │ │ │ │ ├── message_reflect_gen.go │ │ │ │ ├── pointer_unsafe.go │ │ │ │ ├── pointer_unsafe_opaque.go │ │ │ │ ├── presence.go │ │ │ │ └── validate.go │ │ │ ├── order/ │ │ │ │ ├── order.go │ │ │ │ └── range.go │ │ │ ├── pragma/ │ │ │ │ └── pragma.go │ │ │ ├── protolazy/ │ │ │ │ ├── bufferreader.go │ │ │ │ ├── lazy.go │ │ │ │ └── pointer_unsafe.go │ │ │ ├── set/ │ │ │ │ └── ints.go │ │ │ ├── strs/ │ │ │ │ ├── strings.go │ │ │ │ └── strings_unsafe.go │ │ │ └── version/ │ │ │ └── version.go │ │ ├── proto/ │ │ │ ├── checkinit.go │ │ │ ├── decode.go │ │ │ ├── decode_gen.go │ │ │ ├── doc.go │ │ │ ├── encode.go │ │ │ ├── encode_gen.go │ │ │ ├── equal.go │ │ │ ├── extension.go │ │ │ ├── merge.go │ │ │ ├── messageset.go │ │ │ ├── proto.go │ │ │ ├── proto_methods.go │ │ │ ├── proto_reflect.go │ │ │ ├── reset.go │ │ │ ├── size.go │ │ │ ├── size_gen.go │ │ │ ├── wrapperopaque.go │ │ │ └── wrappers.go │ │ ├── protoadapt/ │ │ │ └── convert.go │ │ ├── reflect/ │ │ │ ├── protoreflect/ │ │ │ │ ├── methods.go │ │ │ │ ├── proto.go │ │ │ │ ├── source.go │ │ │ │ ├── source_gen.go │ │ │ │ ├── type.go │ │ │ │ ├── value.go │ │ │ │ ├── value_equal.go │ │ │ │ ├── value_union.go │ │ │ │ └── value_unsafe.go │ │ │ └── protoregistry/ │ │ │ └── registry.go │ │ ├── runtime/ │ │ │ ├── protoiface/ │ │ │ │ ├── legacy.go │ │ │ │ └── methods.go │ │ │ └── protoimpl/ │ │ │ ├── impl.go │ │ │ └── version.go │ │ └── types/ │ │ └── known/ │ │ ├── anypb/ │ │ │ └── any.pb.go │ │ ├── durationpb/ │ │ │ └── duration.pb.go │ │ ├── fieldmaskpb/ │ │ │ └── field_mask.pb.go │ │ ├── structpb/ │ │ │ └── struct.pb.go │ │ ├── timestamppb/ │ │ │ └── timestamp.pb.go │ │ └── wrapperspb/ │ │ └── wrappers.pb.go │ ├── gopkg.in/ │ │ ├── natefinch/ │ │ │ └── lumberjack.v2/ │ │ │ ├── .gitignore │ │ │ ├── .travis.yml │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── chown.go │ │ │ ├── chown_linux.go │ │ │ └── lumberjack.go │ │ ├── yaml.v2/ │ │ │ ├── .travis.yml │ │ │ ├── LICENSE │ │ │ ├── LICENSE.libyaml │ │ │ ├── NOTICE │ │ │ ├── README.md │ │ │ ├── apic.go │ │ │ ├── decode.go │ │ │ ├── emitterc.go │ │ │ ├── encode.go │ │ │ ├── parserc.go │ │ │ ├── readerc.go │ │ │ ├── resolve.go │ │ │ ├── scannerc.go │ │ │ ├── sorter.go │ │ │ ├── writerc.go │ │ │ ├── yaml.go │ │ │ ├── yamlh.go │ │ │ └── yamlprivateh.go │ │ └── yaml.v3/ │ │ ├── LICENSE │ │ ├── NOTICE │ │ ├── README.md │ │ ├── apic.go │ │ ├── decode.go │ │ ├── emitterc.go │ │ ├── encode.go │ │ ├── parserc.go │ │ ├── readerc.go │ │ ├── resolve.go │ │ ├── scannerc.go │ │ ├── sorter.go │ │ ├── writerc.go │ │ ├── yaml.go │ │ ├── yamlh.go │ │ └── yamlprivateh.go │ ├── modules.txt │ ├── nhooyr.io/ │ │ └── websocket/ │ │ ├── .gitignore │ │ ├── LICENSE.txt │ │ ├── README.md │ │ ├── accept.go │ │ ├── accept_js.go │ │ ├── close.go │ │ ├── close_notjs.go │ │ ├── compress.go │ │ ├── compress_notjs.go │ │ ├── conn.go │ │ ├── conn_notjs.go │ │ ├── dial.go │ │ ├── doc.go │ │ ├── frame.go │ │ ├── internal/ │ │ │ ├── bpool/ │ │ │ │ └── bpool.go │ │ │ ├── errd/ │ │ │ │ └── wrap.go │ │ │ ├── wsjs/ │ │ │ │ └── wsjs_js.go │ │ │ └── xsync/ │ │ │ ├── go.go │ │ │ └── int64.go │ │ ├── netconn.go │ │ ├── read.go │ │ ├── stringer.go │ │ ├── write.go │ │ └── ws_js.go │ └── zombiezen.com/ │ └── go/ │ └── capnproto2/ │ ├── .gitignore │ ├── .travis.yml │ ├── AUTHORS │ ├── BUILD.bazel │ ├── CHANGELOG.md │ ├── CONTRIBUTING.md │ ├── CONTRIBUTORS │ ├── LICENSE │ ├── README.md │ ├── WORKSPACE │ ├── address.go │ ├── canonical.go │ ├── capability.go │ ├── capn.go │ ├── doc.go │ ├── encoding/ │ │ └── text/ │ │ ├── BUILD.bazel │ │ └── marshal.go │ ├── go.capnp.go │ ├── internal/ │ │ ├── fulfiller/ │ │ │ ├── BUILD.bazel │ │ │ └── fulfiller.go │ │ ├── nodemap/ │ │ │ ├── BUILD.bazel │ │ │ └── nodemap.go │ │ ├── packed/ │ │ │ ├── BUILD.bazel │ │ │ ├── discard.go │ │ │ ├── discard_go14.go │ │ │ ├── fuzz.go │ │ │ └── packed.go │ │ ├── queue/ │ │ │ ├── BUILD.bazel │ │ │ └── queue.go │ │ ├── schema/ │ │ │ ├── BUILD.bazel │ │ │ └── schema.capnp.go │ │ └── strquote/ │ │ ├── BUILD.bazel │ │ └── strquote.go │ ├── list.go │ ├── mem.go │ ├── mem_18.go │ ├── mem_other.go │ ├── pogs/ │ │ ├── BUILD.bazel │ │ ├── doc.go │ │ ├── extract.go │ │ ├── fields.go │ │ └── insert.go │ ├── pointer.go │ ├── rawpointer.go │ ├── readlimit.go │ ├── regen.sh │ ├── rpc/ │ │ ├── BUILD.bazel │ │ ├── answer.go │ │ ├── errors.go │ │ ├── internal/ │ │ │ └── refcount/ │ │ │ ├── BUILD.bazel │ │ │ └── refcount.go │ │ ├── introspect.go │ │ ├── log.go │ │ ├── question.go │ │ ├── rpc.go │ │ ├── tables.go │ │ └── transport.go │ ├── schemas/ │ │ ├── BUILD.bazel │ │ └── schemas.go │ ├── server/ │ │ ├── BUILD.bazel │ │ └── server.go │ ├── std/ │ │ └── capnp/ │ │ └── rpc/ │ │ ├── BUILD.bazel │ │ └── rpc.capnp.go │ ├── strings.go │ └── struct.go ├── watcher/ │ ├── file.go │ ├── file_test.go │ └── notify.go ├── websocket/ │ ├── connection.go │ ├── websocket.go │ └── websocket_test.go └── wix.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .ci/apt-internal.gitlab-ci.yml ================================================ .register_inputs: ®ister_inputs stage: release-internal runOnBranches: "^master$" COMPONENT: "common" .register_inputs_stable_bookworm: ®ister_inputs_stable_bookworm <<: *register_inputs runOnChangesTo: ['RELEASE_NOTES'] FLAVOR: "bookworm" SERIES: "stable" .register_inputs_stable_trixie: ®ister_inputs_stable_trixie <<: *register_inputs runOnChangesTo: ['RELEASE_NOTES'] FLAVOR: "trixie" SERIES: "stable" .register_inputs_next_bookworm: ®ister_inputs_next_bookworm <<: *register_inputs FLAVOR: "bookworm" SERIES: next .register_inputs_next_trixie: ®ister_inputs_next_trixie <<: *register_inputs FLAVOR: "trixie" SERIES: next ################################################ ### Generate Debian Package for Internal APT ### ################################################ .cloudflared-apt-build: &cloudflared_apt_build stage: package needs: - ci-image-get-image-ref - linux-packaging # For consistency, we only run this job after we knew we could build the packages for external delivery image: $BUILD_IMAGE cache: {} script: - make cloudflared-deb artifacts: paths: - cloudflared*.deb ############## ### Stable ### ############## cloudflared-amd64-stable: <<: *cloudflared_apt_build rules: - !reference [.default-rules, run-on-release] variables: &amd64-stable-vars GOOS: linux GOARCH: amd64 FIPS: true ORIGINAL_NAME: true CGO_ENABLED: 1 cloudflared-arm64-stable: <<: *cloudflared_apt_build rules: - !reference [.default-rules, run-on-release] variables: &arm64-stable-vars GOOS: linux GOARCH: arm64 FIPS: false # TUN-7595 ORIGINAL_NAME: true CGO_ENABLED: 1 ############ ### Next ### ############ cloudflared-amd64-next: <<: *cloudflared_apt_build rules: - !reference [.default-rules, run-on-master] variables: <<: *amd64-stable-vars NIGHTLY: true cloudflared-arm64-next: <<: *cloudflared_apt_build rules: - !reference [.default-rules, run-on-master] variables: <<: *arm64-stable-vars NIGHTLY: true include: - local: .ci/commons.gitlab-ci.yml ########################################## ### Publish Packages to Internal Repos ### ########################################## # Bookworm AMD64 - component: $CI_SERVER_FQDN/cloudflare/ci/apt-register/register@~latest inputs: <<: *register_inputs_stable_bookworm jobPrefix: cloudflared-bookworm-amd64 needs: &amd64-stable ["cloudflared-amd64-stable"] # Bookworm ARM64 - component: $CI_SERVER_FQDN/cloudflare/ci/apt-register/register@~latest inputs: <<: *register_inputs_stable_bookworm jobPrefix: cloudflared-bookworm-arm64 needs: &arm64-stable ["cloudflared-arm64-stable"] # Trixie AMD64 - component: $CI_SERVER_FQDN/cloudflare/ci/apt-register/register@~latest inputs: <<: *register_inputs_stable_trixie jobPrefix: cloudflared-trixie-amd64 needs: *amd64-stable # Trixie ARM64 - component: $CI_SERVER_FQDN/cloudflare/ci/apt-register/register@~latest inputs: <<: *register_inputs_stable_trixie jobPrefix: cloudflared-trixie-arm64 needs: *arm64-stable ################################################## ### Publish Nightly Packages to Internal Repos ### ################################################## # Bookworm AMD64 - component: $CI_SERVER_FQDN/cloudflare/ci/apt-register/register@~latest inputs: <<: *register_inputs_next_bookworm jobPrefix: cloudflared-nightly-bookworm-amd64 needs: &amd64-next ['cloudflared-amd64-next'] # Bookworm ARM64 - component: $CI_SERVER_FQDN/cloudflare/ci/apt-register/register@~latest inputs: <<: *register_inputs_next_bookworm jobPrefix: cloudflared-nightly-bookworm-arm64 needs: &arm64-next ['cloudflared-arm64-next'] # Trixie AMD64 - component: $CI_SERVER_FQDN/cloudflare/ci/apt-register/register@~latest inputs: <<: *register_inputs_next_trixie jobPrefix: cloudflared-nightly-trixie-amd64 needs: *amd64-next # Trixie ARM64 - component: $CI_SERVER_FQDN/cloudflare/ci/apt-register/register@~latest inputs: <<: *register_inputs_next_trixie jobPrefix: cloudflared-nightly-trixie-arm64 needs: *arm64-next ================================================ FILE: .ci/ci-image.gitlab-ci.yml ================================================ # Builds a custom CI Image when necessary include: ##################################################### ############## Build and Push CI Image ############## ##################################################### - component: $CI_SERVER_FQDN/cloudflare/ci/docker-image/build-push-image@~latest inputs: stage: pre-build jobPrefix: ci-image runOnChangesTo: [".ci/image/**"] runOnMR: true runOnBranches: '^master$' commentImageRefs: false runner: vm-linux-x86-4cpu-8gb EXTRA_DIB_ARGS: "--manifest=.ci/image/.docker-images" ##################################################### ## Resolve the image reference for downstream jobs ## ##################################################### - component: $CI_SERVER_FQDN/cloudflare/ci/docker-image/get-image-ref@~latest inputs: stage: pre-build jobPrefix: ci-image runOnMR: true runOnBranches: '^master$' IMAGE_PATH: "$REGISTRY_HOST/stash/tun/cloudflared/ci-image/master" VARIABLE_NAME: BUILD_IMAGE needs: - job: ci-image-build-push-image optional: true ================================================ FILE: .ci/commons.gitlab-ci.yml ================================================ ## A set of predefined rules to use on the different jobs .default-rules: # Rules to run the job only on the master branch run-on-master: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH when: on_success - when: never # Rules to run the job only on merge requests run-on-mr: - if: $CI_COMMIT_TAG when: never - if: $CI_PIPELINE_SOURCE == "merge_request_event" when: on_success - when: never # Rules to run the job on merge_requests and master branch run-always: - if: $CI_COMMIT_TAG when: never - if: $CI_PIPELINE_SOURCE == "merge_request_event" - if: $CI_COMMIT_BRANCH != null && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH when: on_success - when: never # Rules to run the job only when a release happens run-on-release: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH changes: - 'RELEASE_NOTES' when: on_success - when: never .component-tests: image: $BUILD_IMAGE rules: - !reference [.default-rules, run-always] variables: COMPONENT_TESTS_CONFIG: component-test-config.yaml COMPONENT_TESTS_CONFIG_CONTENT: Y2xvdWRmbGFyZWRfYmluYXJ5OiBjbG91ZGZsYXJlZC5leGUKY3JlZGVudGlhbHNfZmlsZTogY3JlZC5qc29uCm9yaWdpbmNlcnQ6IGNlcnQucGVtCnpvbmVfZG9tYWluOiBhcmdvdHVubmVsdGVzdC5jb20Kem9uZV90YWc6IDQ4Nzk2ZjFlNzBiYjc2NjljMjliYjUxYmEyODJiZjY1 secrets: DNS_API_TOKEN: vault: gitlab/cloudflare/tun/cloudflared/_dev/_terraform_atlantis/component_tests_token/data@kv file: false COMPONENT_TESTS_ORIGINCERT: vault: gitlab/cloudflare/tun/cloudflared/_dev/component_tests_cert_pem/data@kv file: false cache: {} ================================================ FILE: .ci/github.gitlab-ci.yml ================================================ include: - local: .ci/commons.gitlab-ci.yml ###################################### ### Sync master branch with Github ### ###################################### push-github: stage: sync rules: - !reference [.default-rules, run-on-master] script: - ./.ci/scripts/github-push.sh secrets: CLOUDFLARED_DEPLOY_SSH_KEY: vault: gitlab/cloudflare/tun/cloudflared/_branch/master/cloudflared_github_ssh/data@kv file: false cache: {} ================================================ FILE: .ci/image/.docker-images ================================================ images: - name: ci-image ================================================ FILE: .ci/image/Dockerfile ================================================ ARG CLOUDFLARE_DOCKER_REGISTRY_HOST FROM ${CLOUDFLARE_DOCKER_REGISTRY_HOST:-registry.cfdata.org}/stash/cf/debian-images/trixie/main:2026.1.0@sha256:e32092fd01520f5ae7de1fa6bb5a721720900ebeaa48e98f36f6f86168833cd7 RUN apt-get update && \ apt-get upgrade -y && \ apt-get install --no-install-recommends --allow-downgrades -y \ build-essential \ git \ go-boring=1.24.13-1 \ libffi-dev \ procps \ python3-dev \ python3-pip \ python3-setuptools \ python3-venv \ # tool to create msi packages wixl \ # install ruby and rpm which are required to install fpm package builder rpm \ ruby \ ruby-dev \ rubygems \ # create deb and rpm repository files reprepro \ createrepo-c \ # gcc for cross architecture compilation in arm gcc-aarch64-linux-gnu \ libc6-dev-arm64-cross && \ rm -rf /var/lib/apt/lists/* && \ # Install fpm gem gem install fpm --no-document && \ # Initialize rpm repository, SQL Lite DB mkdir -p /var/lib/rpm && \ rpm --initdb && \ chmod -R 777 /var/lib/rpm && \ # Create work directory mkdir -p opt WORKDIR /opt ================================================ FILE: .ci/linux.gitlab-ci.yml ================================================ .golang-inputs: &golang_inputs runOnMR: true runOnBranches: "^master$" outputDir: artifacts runner: linux-x86-8cpu-16gb stage: build golangVersion: "boring-1.24" imageVersion: "3462-0b23466e0715@sha256:42e8533370666a2463041572293a79e1449001ef803a993e6a860be00858c806" CGO_ENABLED: 1 .default-packaging-job: &packaging-job-defaults stage: package needs: - ci-image-get-image-ref rules: - !reference [.default-rules, run-on-master] image: $BUILD_IMAGE cache: {} artifacts: paths: - artifacts/* include: ################### ### Linux Build ### ################### - component: $CI_SERVER_FQDN/cloudflare/ci/golang/boring-make@~latest inputs: <<: *golang_inputs jobPrefix: linux-build GOLANG_MAKE_TARGET: ci-build ######################## ### Linux FIPS Build ### ######################## - component: $CI_SERVER_FQDN/cloudflare/ci/golang/boring-make@~latest inputs: <<: *golang_inputs jobPrefix: linux-fips-build GOLANG_MAKE_TARGET: ci-fips-build ################# ### Unit Tests ## ################# - component: $CI_SERVER_FQDN/cloudflare/ci/golang/boring-make@~latest inputs: <<: *golang_inputs stage: test jobPrefix: test GOLANG_MAKE_TARGET: ci-test ###################### ### Unit Tests FIPS ## ###################### - component: $CI_SERVER_FQDN/cloudflare/ci/golang/boring-make@~latest inputs: <<: *golang_inputs stage: test jobPrefix: test-fips GOLANG_MAKE_TARGET: ci-fips-test ################# ### Vuln Check ## ################# - component: $CI_SERVER_FQDN/cloudflare/ci/golang/boring-make@~latest inputs: <<: *golang_inputs runOnBranches: "^$" stage: validate jobPrefix: vulncheck GOLANG_MAKE_TARGET: vulncheck ################################# ### Run Linux Component Tests ### ################################# linux-component-tests: &linux-component-tests stage: test extends: .component-tests needs: - ci-image-get-image-ref - linux-build-boring-make script: - ./.ci/scripts/component-tests.sh variables: &component-tests-variables CI: 1 COMPONENT_TESTS_CONFIG_CONTENT: Y2xvdWRmbGFyZWRfYmluYXJ5OiAuL2Nsb3VkZmxhcmVkCmNyZWRlbnRpYWxzX2ZpbGU6IGNyZWQuanNvbgpvcmlnaW5jZXJ0OiBjZXJ0LnBlbQp6b25lX2RvbWFpbjogYXJnb3R1bm5lbHRlc3QuY29tCnpvbmVfdGFnOiA0ODc5NmYxZTcwYmI3NjY5YzI5YmI1MWJhMjgyYmY2NQ== tags: - linux-x86-8cpu-16gb artifacts: reports: junit: report.xml ###################################### ### Run Linux FIPS Component Tests ### ###################################### linux-component-tests-fips: <<: *linux-component-tests needs: - ci-image-get-image-ref - linux-fips-build-boring-make variables: <<: *component-tests-variables COMPONENT_TESTS_FIPS: 1 ################################ ####### Linux Packaging ######## ################################ linux-packaging: <<: *packaging-job-defaults parallel: matrix: - ARCH: ["386", "amd64", "arm", "armhf", "arm64"] script: - ./.ci/scripts/linux/build-packages.sh ${ARCH} ################################ ##### Linux FIPS Packaging ##### ################################ linux-packaging-fips: <<: *packaging-job-defaults script: - ./.ci/scripts/linux/build-packages-fips.sh ================================================ FILE: .ci/mac.gitlab-ci.yml ================================================ include: - local: .ci/commons.gitlab-ci.yml ############################### ### Defaults for Mac Builds ### ############################### .mac-build-defaults: &mac-build-defaults rules: - !reference [.default-rules, run-on-mr] tags: - "macstadium-${RUNNER_ARCH}" parallel: matrix: - RUNNER_ARCH: [arm, intel] cache: {} ###################################### ### Build Cloudflared Mac Binaries ### ###################################### macos-build-cloudflared: &mac-build <<: *mac-build-defaults stage: build artifacts: paths: - artifacts/* script: - '[ "${RUNNER_ARCH}" = "arm" ] && export TARGET_ARCH=arm64' - '[ "${RUNNER_ARCH}" = "intel" ] && export TARGET_ARCH=amd64' - ARCH=$(uname -m) - echo ARCH=$ARCH - TARGET_ARCH=$TARGET_ARCH - ./.ci/scripts/mac/install-go.sh "$MAC_GO_VERSION" - BUILD_SCRIPT=.ci/scripts/mac/build.sh - if [[ ! -x ${BUILD_SCRIPT} ]] ; then exit ; fi - set -euo pipefail - echo "Executing ${BUILD_SCRIPT}" - exec ${BUILD_SCRIPT} ############################################### ### Build and Sign Cloudflared Mac Binaries ### ############################################### macos-build-and-sign-cloudflared: <<: *mac-build rules: - !reference [.default-rules, run-on-master] secrets: APPLE_DEV_CA_CERT: vault: gitlab/cloudflare/tun/cloudflared/_branch/master/apple_dev_ca_cert_v2/data@kv file: false CFD_CODE_SIGN_CERT: vault: gitlab/cloudflare/tun/cloudflared/_branch/master/cfd_code_sign_cert_v2/data@kv file: false CFD_CODE_SIGN_KEY: vault: gitlab/cloudflare/tun/cloudflared/_branch/master/cfd_code_sign_key_v2/data@kv file: false CFD_CODE_SIGN_PASS: vault: gitlab/cloudflare/tun/cloudflared/_branch/master/cfd_code_sign_pass_v2/data@kv file: false CFD_INSTALLER_CERT: vault: gitlab/cloudflare/tun/cloudflared/_branch/master/cfd_installer_cert_v2/data@kv file: false CFD_INSTALLER_KEY: vault: gitlab/cloudflare/tun/cloudflared/_branch/master/cfd_installer_key_v2/data@kv file: false CFD_INSTALLER_PASS: vault: gitlab/cloudflare/tun/cloudflared/_branch/master/cfd_installer_pass_v2/data@kv file: false ================================================ FILE: .ci/release.gitlab-ci.yml ================================================ include: - local: .ci/commons.gitlab-ci.yml ###################################### ### Build and Push DockerHub Image ### ###################################### - component: $CI_SERVER_FQDN/cloudflare/ci/docker-image/build-push-image@~latest inputs: stage: release jobPrefix: docker-hub runOnMR: false runOnBranches: '^master$' runOnChangesTo: ['RELEASE_NOTES'] needs: - generate-version-file - release-cloudflared-to-r2 commentImageRefs: false runner: vm-linux-x86-4cpu-8gb # Based on if the CI reference is protected or not the CI component will # either use _BRANCH or _PROD, therefore, to prevent the pipelines from failing # we simply set both to the same value. DOCKER_USER_BRANCH: &docker-hub-user svcgithubdockerhubcloudflar045 DOCKER_PASSWORD_BRANCH: &docker-hub-password gitlab/cloudflare/tun/cloudflared/_dev/dockerhub/svc_password/data DOCKER_USER_PROD: *docker-hub-user DOCKER_PASSWORD_PROD: *docker-hub-password EXTRA_DIB_ARGS: --overwrite .default-release-job: &release-job-defaults stage: release image: $BUILD_IMAGE cache: paths: - .cache/pip variables: &release-job-variables PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" # KV Vars KV_NAMESPACE: 380e19aa04314648949b6ad841417ebe KV_ACCOUNT: &cf-account 5ab4e9dfbd435d24068829fda0077963 # R2 Vars R2_BUCKET: cloudflared-pkgs R2_ACCOUNT_ID: *cf-account # APT and RPM Repository Vars GPG_PUBLIC_KEY_URL: "https://pkg.cloudflare.com/cloudflare-ascii-pubkey.gpg" PKG_URL: "https://pkg.cloudflare.com/cloudflared" BINARY_NAME: cloudflared secrets: KV_API_TOKEN: vault: gitlab/cloudflare/tun/cloudflared/_dev/cfd_kv_api_token/data@kv file: false API_KEY: vault: gitlab/cloudflare/tun/cloudflared/_dev/cfd_github_api_key/data@kv file: false R2_CLIENT_ID: vault: gitlab/cloudflare/tun/cloudflared/_dev/_terraform_atlantis/r2_api_token/client_id@kv file: false R2_CLIENT_SECRET: vault: gitlab/cloudflare/tun/cloudflared/_dev/_terraform_atlantis/r2_api_token/client_secret@kv file: false LINUX_SIGNING_PUBLIC_KEY: vault: gitlab/cloudflare/tun/cloudflared/_dev/gpg_v1/public_key@kv file: false LINUX_SIGNING_PRIVATE_KEY: vault: gitlab/cloudflare/tun/cloudflared/_dev/gpg_v1/private_key@kv file: false LINUX_SIGNING_PUBLIC_KEY_2: vault: gitlab/cloudflare/tun/cloudflared/_dev/gpg_v2/public_key@kv file: false LINUX_SIGNING_PRIVATE_KEY_2: vault: gitlab/cloudflare/tun/cloudflared/_dev/gpg_v2/private_key@kv file: false ########################################### ### Push Cloudflared Binaries to Github ### ########################################### release-cloudflared-to-github: <<: *release-job-defaults rules: - !reference [.default-rules, run-on-release] needs: - ci-image-get-image-ref - linux-packaging - linux-packaging-fips - macos-build-and-sign-cloudflared - windows-package-sign script: - ./.ci/scripts/release-target.sh github-release ######################################### ### Upload Cloudflared Binaries to R2 ### ######################################### release-cloudflared-to-r2: <<: *release-job-defaults rules: - !reference [.default-rules, run-on-release] needs: - ci-image-get-image-ref - linux-packaging # We only release non-FIPS binaries to R2 - release-cloudflared-to-github script: - ./.ci/scripts/release-target.sh r2-linux-release ################################################# ### Upload Cloudflared Nightly Binaries to R2 ### ################################################# release-cloudflared-nightly-to-r2: <<: *release-job-defaults rules: - !reference [.default-rules, run-on-master] variables: <<: *release-job-variables R2_BUCKET: cloudflared-pkgs-next GPG_PUBLIC_KEY_URL: "https://next.pkg.cloudflare.com/cloudflare-ascii-pubkey.gpg" PKG_URL: "https://next.pkg.cloudflare.com/cloudflared" needs: - ci-image-get-image-ref - linux-packaging # We only release non-FIPS binaries to R2 script: - ./.ci/scripts/release-target.sh r2-linux-release ############################# ### Generate Version File ### ############################# generate-version-file: <<: *release-job-defaults rules: - !reference [.default-rules, run-on-release] needs: - ci-image-get-image-ref script: - make generate-docker-version artifacts: paths: - versions ================================================ FILE: .ci/scripts/component-tests.sh ================================================ #!/bin/bash set -e -u -o pipefail # Fetch cloudflared from the artifacts folder mv ./artifacts/cloudflared ./cloudflared python3 -m venv env . env/bin/activate pip install --upgrade -r component-tests/requirements.txt # Creates and routes a Named Tunnel for this build. Also constructs # config file from env vars. python3 component-tests/setup.py --type create # Define the cleanup function cleanup() { # The Named Tunnel is deleted and its route unprovisioned here. python3 component-tests/setup.py --type cleanup } # The trap will call the cleanup function on script exit trap cleanup EXIT pytest component-tests -o log_cli=true --log-cli-level=INFO --junit-xml=report.xml ================================================ FILE: .ci/scripts/fmt-check.sh ================================================ #!/bin/bash set -e -u -o pipefail OUTPUT=$(go run -mod=readonly golang.org/x/tools/cmd/goimports@v0.30.0 -l -d -local github.com/cloudflare/cloudflared $(go list -mod=vendor -f '{{.Dir}}' -a ./... | fgrep -v tunnelrpc)) if [ -n "$OUTPUT" ] ; then PAGER=$(which colordiff || echo cat) echo echo "Code formatting issues found, use 'make fmt' to correct them" echo echo "$OUTPUT" | $PAGER exit 1 fi ================================================ FILE: .ci/scripts/github-push.sh ================================================ #!/bin/bash set -e -u -o pipefail BRANCH="master" TMP_PATH="$PWD/tmp" PRIVATE_KEY_PATH="$TMP_PATH/github-deploy-key" PUBLIC_KEY_GITHUB_PATH="$TMP_PATH/github.pub" mkdir -p $TMP_PATH # Setup Private Key echo "$CLOUDFLARED_DEPLOY_SSH_KEY" > $PRIVATE_KEY_PATH chmod 400 $PRIVATE_KEY_PATH # Download GitHub Public Key for KnownHostsFile ssh-keyscan -t ed25519 github.com > $PUBLIC_KEY_GITHUB_PATH # Setup git ssh command with the right configurations export GIT_SSH_COMMAND="ssh -o UserKnownHostsFile=$PUBLIC_KEY_GITHUB_PATH -o IdentitiesOnly=yes -i $PRIVATE_KEY_PATH" # Add GitHub as a new remote git remote add github git@github.com:cloudflare/cloudflared.git || true # GitLab doesn't pull branch references, instead it creates a new one on each pipeline. # Therefore, we need to manually fetch the reference to then push it to GitHub. git fetch origin $BRANCH:$BRANCH git push -u github $BRANCH if TAG="$(git describe --tags --exact-match 2>/dev/null)"; then git push -u github "$TAG" fi ================================================ FILE: .ci/scripts/linux/build-packages-fips.sh ================================================ #!/bin/bash set -e -u -o pipefail VERSION=$(git describe --tags --always --match "[0-9][0-9][0-9][0-9].*.*") echo $VERSION # This controls the directory the built artifacts go into export ARTIFACT_DIR=artifacts/ mkdir -p $ARTIFACT_DIR arch=("amd64") export TARGET_ARCH=$arch export TARGET_OS=linux export FIPS=true # For BoringCrypto to link, we need CGO enabled. Otherwise compilation fails. export CGO_ENABLED=1 make cloudflared-deb mv cloudflared-fips\_$VERSION\_$arch.deb $ARTIFACT_DIR/cloudflared-fips-linux-$arch.deb # rpm packages invert the - and _ and use x86_64 instead of amd64. RPMVERSION=$(echo $VERSION | sed -r 's/-/_/g') RPMARCH="x86_64" make cloudflared-rpm mv cloudflared-fips-$RPMVERSION-1.$RPMARCH.rpm $ARTIFACT_DIR/cloudflared-fips-linux-$RPMARCH.rpm # finally move the linux binary as well. mv ./cloudflared $ARTIFACT_DIR/cloudflared-fips-linux-$arch ================================================ FILE: .ci/scripts/linux/build-packages.sh ================================================ #!/bin/bash set -e -u -o pipefail # Check if architecture argument is provided if [ $# -eq 0 ]; then echo "Error: Architecture argument is required" echo "Usage: $0 " exit 1 fi # Parameters arch=$1 # Get Version VERSION=$(git describe --tags --always --match "[0-9][0-9][0-9][0-9].*.*") echo $VERSION # Disable FIPS module in go-boring export GOEXPERIMENT=noboringcrypto export CGO_ENABLED=0 # This controls the directory the built artifacts go into export ARTIFACT_DIR=artifacts/ mkdir -p $ARTIFACT_DIR export TARGET_OS=linux unset TARGET_ARM export TARGET_ARCH=$arch ## Support for arm platforms without hardware FPU enabled if [[ $arch == arm ]] ; then export TARGET_ARCH=arm export TARGET_ARM=5 fi ## Support for armhf builds if [[ $arch == armhf ]] ; then export TARGET_ARCH=arm export TARGET_ARM=7 fi make cloudflared-deb mv cloudflared\_$VERSION\_$arch.deb $ARTIFACT_DIR/cloudflared-linux-$arch.deb # rpm packages invert the - and _ and use x86_64 instead of amd64. RPMVERSION=$(echo $VERSION|sed -r 's/-/_/g') RPMARCH=$arch if [ $arch == "amd64" ];then RPMARCH="x86_64" fi if [ $arch == "arm64" ]; then RPMARCH="aarch64" fi make cloudflared-rpm mv cloudflared-$RPMVERSION-1.$RPMARCH.rpm $ARTIFACT_DIR/cloudflared-linux-$RPMARCH.rpm # finally move the linux binary as well. mv ./cloudflared $ARTIFACT_DIR/cloudflared-linux-$arch ================================================ FILE: .ci/scripts/mac/build.sh ================================================ #!/bin/bash set -exo pipefail if [[ "$(uname)" != "Darwin" ]] ; then echo "This should be run on macOS" exit 1 fi if [[ "amd64" != "${TARGET_ARCH}" && "arm64" != "${TARGET_ARCH}" ]] then echo "TARGET_ARCH must be amd64 or arm64" exit 1 fi go version export GO111MODULE=on # build 'cloudflared-darwin-amd64.tgz' mkdir -p artifacts TARGET_DIRECTORY=".build" BINARY_NAME="cloudflared" VERSION=$(git describe --tags --always --dirty="-dev") PRODUCT="cloudflared" APPLE_CA_CERT="apple_dev_ca.cert" CODE_SIGN_PRIV="code_sign.p12" CODE_SIGN_CERT="code_sign.cer" INSTALLER_PRIV="installer.p12" INSTALLER_CERT="installer.cer" BUNDLE_ID="com.cloudflare.cloudflared" SEC_DUP_MSG="security: SecKeychainItemImport: The specified item already exists in the keychain." export PATH="$PATH:/usr/local/bin" FILENAME="$(pwd)/artifacts/cloudflared-darwin-$TARGET_ARCH.tgz" PKGNAME="$(pwd)/artifacts/cloudflared-$TARGET_ARCH.pkg" mkdir -p ../src/github.com/cloudflare/ cp -r . ../src/github.com/cloudflare/cloudflared cd ../src/github.com/cloudflare/cloudflared # Imports certificates to the Apple KeyChain import_certificate() { local CERTIFICATE_NAME=$1 local CERTIFICATE_ENV_VAR=$2 local CERTIFICATE_FILE_NAME=$3 echo "Importing $CERTIFICATE_NAME" if [[ ! -z "$CERTIFICATE_ENV_VAR" ]]; then # write certificate to disk and then import it keychain echo -n -e ${CERTIFICATE_ENV_VAR} | base64 -D > ${CERTIFICATE_FILE_NAME} # we set || true here and for every `security import invoke` because the "duplicate SecKeychainItemImport" error # will cause set -e to exit 1. It is okay we do this because we deliberately handle this error in the lines below. local out=$(security import ${CERTIFICATE_FILE_NAME} -T /usr/bin/pkgbuild -A 2>&1) || true local exitcode=$? # delete the certificate from disk rm -rf ${CERTIFICATE_FILE_NAME} if [ -n "$out" ]; then if [ $exitcode -eq 0 ]; then echo "$out" else if [ "$out" != "${SEC_DUP_MSG}" ]; then echo "$out" >&2 exit $exitcode else echo "already imported code signing certificate" fi fi fi fi } create_cloudflared_build_keychain() { # Reusing the private key password as the keychain key local PRIVATE_KEY_PASS=$1 # Create keychain only if it doesn't already exist if [ ! -f "$HOME/Library/Keychains/cloudflared_build_keychain.keychain-db" ]; then security create-keychain -p "$PRIVATE_KEY_PASS" cloudflared_build_keychain else echo "Keychain already exists: cloudflared_build_keychain" fi # Append temp keychain to the user domain security list-keychains -d user -s cloudflared_build_keychain $(security list-keychains -d user | sed s/\"//g) # Remove relock timeout security set-keychain-settings cloudflared_build_keychain # Unlock keychain so it doesn't require password security unlock-keychain -p "$PRIVATE_KEY_PASS" cloudflared_build_keychain } # Imports private keys to the Apple KeyChain import_private_keys() { local PRIVATE_KEY_NAME=$1 local PRIVATE_KEY_ENV_VAR=$2 local PRIVATE_KEY_FILE_NAME=$3 local PRIVATE_KEY_PASS=$4 echo "Importing $PRIVATE_KEY_NAME" if [[ ! -z "$PRIVATE_KEY_ENV_VAR" ]]; then if [[ ! -z "$PRIVATE_KEY_PASS" ]]; then # write private key to disk and then import it keychain echo -n -e ${PRIVATE_KEY_ENV_VAR} | base64 -D > ${PRIVATE_KEY_FILE_NAME} # we set || true here and for every `security import invoke` because the "duplicate SecKeychainItemImport" error # will cause set -e to exit 1. It is okay we do this because we deliberately handle this error in the lines below. local out=$(security import ${PRIVATE_KEY_FILE_NAME} -k cloudflared_build_keychain -P "$PRIVATE_KEY_PASS" -T /usr/bin/pkgbuild -A -P "${PRIVATE_KEY_PASS}" 2>&1) || true local exitcode=$? rm -rf ${PRIVATE_KEY_FILE_NAME} if [ -n "$out" ]; then if [ $exitcode -eq 0 ]; then echo "$out" else if [ "$out" != "${SEC_DUP_MSG}" ]; then echo "$out" >&2 exit $exitcode fi fi fi fi fi } # Create temp keychain only for this build create_cloudflared_build_keychain "${CFD_CODE_SIGN_PASS}" # Add Apple Root Developer certificate to the key chain import_certificate "Apple Developer CA" "${APPLE_DEV_CA_CERT}" "${APPLE_CA_CERT}" # Add code signing private key to the key chain import_private_keys "Developer ID Application" "${CFD_CODE_SIGN_KEY}" "${CODE_SIGN_PRIV}" "${CFD_CODE_SIGN_PASS}" # Add code signing certificate to the key chain import_certificate "Developer ID Application" "${CFD_CODE_SIGN_CERT}" "${CODE_SIGN_CERT}" # Add package signing private key to the key chain import_private_keys "Developer ID Installer" "${CFD_INSTALLER_KEY}" "${INSTALLER_PRIV}" "${CFD_INSTALLER_PASS}" # Add package signing certificate to the key chain import_certificate "Developer ID Installer" "${CFD_INSTALLER_CERT}" "${INSTALLER_CERT}" # get the code signing certificate name if [[ ! -z "$CFD_CODE_SIGN_NAME" ]]; then CODE_SIGN_NAME="${CFD_CODE_SIGN_NAME}" else if [[ -n "$(security find-certificate -c "Developer ID Application" cloudflared_build_keychain | cut -d'"' -f 4 -s | grep "Developer ID Application:" | head -1)" ]]; then CODE_SIGN_NAME=$(security find-certificate -c "Developer ID Application" cloudflared_build_keychain | cut -d'"' -f 4 -s | grep "Developer ID Application:" | head -1) else CODE_SIGN_NAME="" fi fi # get the package signing certificate name if [[ ! -z "$CFD_INSTALLER_NAME" ]]; then PKG_SIGN_NAME="${CFD_INSTALLER_NAME}" else if [[ -n "$(security find-certificate -c "Developer ID Installer" cloudflared_build_keychain | cut -d'"' -f 4 -s | grep "Developer ID Installer:" | head -1)" ]]; then PKG_SIGN_NAME=$(security find-certificate -c "Developer ID Installer" cloudflared_build_keychain | cut -d'"' -f 4 -s | grep "Developer ID Installer:" | head -1) else PKG_SIGN_NAME="" fi fi # cleanup the build directory because the previous execution might have failed without cleaning up. rm -rf "${TARGET_DIRECTORY}" export TARGET_OS="darwin" GOCACHE="$PWD/../../../../" GOPATH="$PWD/../../../../" CGO_ENABLED=1 make cloudflared # This allows apple tools to use the certificates in the keychain without requiring password input. # This command always needs to run after the certificates have been loaded into the keychain if [[ ! -z "$CFD_CODE_SIGN_PASS" ]]; then security set-key-partition-list -S apple-tool:,apple: -s -k "${CFD_CODE_SIGN_PASS}" cloudflared_build_keychain fi # sign the cloudflared binary if [[ ! -z "$CODE_SIGN_NAME" ]]; then codesign --keychain $HOME/Library/Keychains/cloudflared_build_keychain.keychain-db -s "${CODE_SIGN_NAME}" -fv --options runtime --timestamp ${BINARY_NAME} # notarize the binary # TODO: TUN-5789 fi ARCH_TARGET_DIRECTORY="${TARGET_DIRECTORY}/${TARGET_ARCH}-build" # creating build directory rm -rf $ARCH_TARGET_DIRECTORY mkdir -p "${ARCH_TARGET_DIRECTORY}" mkdir -p "${ARCH_TARGET_DIRECTORY}/contents" cp -r ".mac_resources/scripts" "${ARCH_TARGET_DIRECTORY}/scripts" # copy cloudflared into the build directory cp ${BINARY_NAME} "${ARCH_TARGET_DIRECTORY}/contents/${PRODUCT}" # compress cloudflared into a tar and gzipped file tar czf "$FILENAME" "${BINARY_NAME}" # build the installer package if [[ ! -z "$PKG_SIGN_NAME" ]]; then pkgbuild --identifier com.cloudflare.${PRODUCT} \ --version ${VERSION} \ --scripts ${ARCH_TARGET_DIRECTORY}/scripts \ --root ${ARCH_TARGET_DIRECTORY}/contents \ --install-location /usr/local/bin \ --keychain cloudflared_build_keychain \ --sign "${PKG_SIGN_NAME}" \ ${PKGNAME} # notarize the package # TODO: TUN-5789 else pkgbuild --identifier com.cloudflare.${PRODUCT} \ --version ${VERSION} \ --scripts ${ARCH_TARGET_DIRECTORY}/scripts \ --root ${ARCH_TARGET_DIRECTORY}/contents \ --install-location /usr/local/bin \ ${PKGNAME} fi # cleanup build directory because this script is not ran within containers, # which might lead to future issues in subsequent runs. rm -rf "${TARGET_DIRECTORY}" # cleanup the keychain security default-keychain -d user -s login.keychain-db security list-keychains -d user -s login.keychain-db security delete-keychain cloudflared_build_keychain ================================================ FILE: .ci/scripts/mac/install-go.sh ================================================ rm -rf /tmp/go export GOCACHE=/tmp/gocache rm -rf $GOCACHE if [ -z "$1" ] then echo "No go version supplied" fi brew install "$1" go version which go go env ================================================ FILE: .ci/scripts/package-windows.sh ================================================ #!/bin/bash set -e -u -o pipefail python3 -m venv env . env/bin/activate pip install pynacl==1.4.0 pygithub==1.55 VERSION=$(git describe --tags --always --match "[0-9][0-9][0-9][0-9].*.*") echo $VERSION export TARGET_OS=windows # This controls the directory the built artifacts go into export BUILT_ARTIFACT_DIR=artifacts/ export FINAL_ARTIFACT_DIR=artifacts/ mkdir -p $BUILT_ARTIFACT_DIR mkdir -p $FINAL_ARTIFACT_DIR windowsArchs=("amd64" "386") for arch in ${windowsArchs[@]}; do export TARGET_ARCH=$arch # Copy .exe from artifacts directory cp $BUILT_ARTIFACT_DIR/cloudflared-windows-$arch.exe ./cloudflared.exe make cloudflared-msi # Copy msi into final directory mv cloudflared-$VERSION-$arch.msi $FINAL_ARTIFACT_DIR/cloudflared-windows-$arch.msi done ================================================ FILE: .ci/scripts/release-target.sh ================================================ #!/bin/bash set -e -u -o pipefail # Check if a make target is provided as an argument if [ $# -eq 0 ]; then echo "Error: Make target argument is required" echo "Usage: $0 " exit 1 fi MAKE_TARGET=$1 python3 -m venv venv source venv/bin/activate # Our release scripts are written in python, so we should install their dependecies here. pip install pynacl==1.4.0 pygithub==1.55 boto3==1.42.30 python-gnupg==0.4.9 make $MAKE_TARGET ================================================ FILE: .ci/scripts/vuln-check.sh ================================================ #!/bin/bash set -e -u # Define the file to store the list of vulnerabilities to ignore. IGNORE_FILE=".vulnignore" go version # Check if the ignored vulnerabilities file exists. If not, create an empty one. if [ ! -f "$IGNORE_FILE" ]; then touch "$IGNORE_FILE" echo "Created an empty file to store ignored vulnerabilities: $IGNORE_FILE" echo "# Add vulnerability IDs (e.g., GO-2022-0450) to ignore, one per line." >>"$IGNORE_FILE" echo "# You can also add comments on the same line after the ID." >>"$IGNORE_FILE" echo "" >>"$IGNORE_FILE" fi # Run govulncheck and capture its output. VULN_OUTPUT=$(go run -mod=readonly golang.org/x/vuln/cmd/govulncheck@latest ./... || true) # Print the govuln output echo "=====================================" echo "Full Output of govulncheck:" echo "=====================================" echo "$VULN_OUTPUT" echo "=====================================" echo "End of govulncheck Output" echo "=====================================" # Process the ignore file to remove comments and empty lines. # The 'cut' command gets the vulnerability ID and removes anything after the '#'. # The 'grep' command filters out empty lines and lines starting with '#'. CLEAN_IGNORES=$(grep -v '^\s*#' "$IGNORE_FILE" | cut -d'#' -f1 | sed 's/ //g' | sort -u || true) # Filter out the ignored vulnerabilities. UNIGNORED_VULNS=$(echo "$VULN_OUTPUT" | grep 'Vulnerability' || true) # If the list of ignored vulnerabilities is not empty, filter them out. if [ -n "$CLEAN_IGNORES" ]; then UNIGNORED_VULNS=$(echo "$UNIGNORED_VULNS" | grep -vFf <(echo "$CLEAN_IGNORES") || true) fi # If there are any vulnerabilities that were not in our ignore list, print them and exit with an error. if [ -n "$UNIGNORED_VULNS" ]; then echo "🚨 Found new, unignored vulnerabilities:" echo "-------------------------------------" echo "$UNIGNORED_VULNS" echo "-------------------------------------" echo "Exiting with an error. ❌" exit 1 else echo "🎉 No new vulnerabilities found. All clear! ✨" exit 0 fi ================================================ FILE: .ci/scripts/windows/builds.ps1 ================================================ Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" $ProgressPreference = "SilentlyContinue" $env:TARGET_OS = "windows" $env:LOCAL_OS = "windows" $TIMESTAMP_RFC3161 = "http://timestamp.digicert.com" New-Item -Path ".\artifacts" -ItemType Directory Write-Output "Building for amd64" $env:TARGET_ARCH = "amd64" $env:LOCAL_ARCH = "amd64" $env:CGO_ENABLED = 1 & make cloudflared if ($LASTEXITCODE -ne 0) { throw "Failed to build cloudflared for amd64" } # Sign build azuresigntool.exe sign -kvu $env:KEY_VAULT_URL -kvi "$env:KEY_VAULT_CLIENT_ID" -kvs "$env:KEY_VAULT_SECRET" -kvc "$env:KEY_VAULT_CERTIFICATE" -kvt "$env:KEY_VAULT_TENANT_ID" -tr "$TIMESTAMP_RFC3161" -d "Cloudflare Tunnel Daemon" .\cloudflared.exe copy .\cloudflared.exe .\artifacts\cloudflared-windows-amd64.exe Write-Output "Building for 386" $env:TARGET_ARCH = "386" $env:LOCAL_ARCH = "386" $env:CGO_ENABLED = 0 & make cloudflared if ($LASTEXITCODE -ne 0) { throw "Failed to build cloudflared for 386" } ## Sign build azuresigntool.exe sign -kvu $env:KEY_VAULT_URL -kvi "$env:KEY_VAULT_CLIENT_ID" -kvs "$env:KEY_VAULT_SECRET" -kvc "$env:KEY_VAULT_CERTIFICATE" -kvt "$env:KEY_VAULT_TENANT_ID" -tr "$TIMESTAMP_RFC3161" -d "Cloudflare Tunnel Daemon" .\cloudflared.exe copy .\cloudflared.exe .\artifacts\cloudflared-windows-386.exe ================================================ FILE: .ci/scripts/windows/component-test.ps1 ================================================ Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" $ProgressPreference = "SilentlyContinue" $env:TARGET_OS = "windows" $env:LOCAL_OS = "windows" $env:TARGET_ARCH = "amd64" $env:LOCAL_ARCH = "amd64" $env:CGO_ENABLED = 1 python --version python -m pip --version Write-Host "Building cloudflared" & make cloudflared if ($LASTEXITCODE -ne 0) { throw "Failed to build cloudflared" } Write-Host "Running unit tests" # Not testing with race detector because of https://github.com/golang/go/issues/61058 # We already test it on other platforms go test -failfast -v -mod=vendor ./... if ($LASTEXITCODE -ne 0) { throw "Failed unit tests" } # On Gitlab runners we need to add all of this addresses to the NO_PROXY list in order for the tests to run. $env:NO_PROXY = "pypi.org,files.pythonhosted.org,api.cloudflare.com,argotunneltest.com,argotunnel.com,trycloudflare.com,${env:NO_PROXY}" Write-Host "No Proxy: ${env:NO_PROXY}" Write-Host "Running component tests" try { python -m pip --disable-pip-version-check install --upgrade -r component-tests/requirements.txt --use-pep517 python component-tests/setup.py --type create python -m pytest component-tests -o log_cli=true --log-cli-level=INFO --junit-xml=report.xml if ($LASTEXITCODE -ne 0) { throw "Failed component tests" } } finally { python component-tests/setup.py --type cleanup } ================================================ FILE: .ci/scripts/windows/go-wrapper.ps1 ================================================ Param( [string]$GoVersion, [string]$ScriptToExecute ) # The script is a wrapper that downloads a specific version # of go, adds it to the PATH and executes a script with that go # version in the path. Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" $ProgressPreference = "SilentlyContinue" # Get the path to the system's temporary directory. $tempPath = [System.IO.Path]::GetTempPath() # Create a unique name for the new temporary folder. $folderName = "go_" + (Get-Random) # Join the temp path and the new folder name to create the full path. $fullPath = Join-Path -Path $tempPath -ChildPath $folderName # Store the current value of PATH environment variable. $oldPath = $env:Path # Use a try...finally block to ensure the temporrary folder and PATH are cleaned up. try { # Create the temporary folder. Write-Host "Creating temporary folder at: $fullPath" $newTempFolder = New-Item -ItemType Directory -Path $fullPath -Force # Download go $url = "https://go.dev/dl/$GoVersion.windows-amd64.zip" $destinationFile = Join-Path -Path $newTempFolder.FullName -ChildPath "go$GoVersion.windows-amd64.zip" Write-Host "Downloading go from: $url" Invoke-WebRequest -Uri $url -OutFile $destinationFile Write-Host "File downloaded to: $destinationFile" # Unzip the downloaded file. Write-Host "Unzipping the file..." Expand-Archive -Path $destinationFile -DestinationPath $newTempFolder.FullName -Force Write-Host "File unzipped successfully." # Define the go/bin path wich is inside the temporary folder $goBinPath = Join-Path -Path $fullPath -ChildPath "go\bin" # Add the go/bin path to the PATH environment variable. $env:Path = "$goBinPath;$($env:Path)" Write-Host "Added $goBinPath to the environment PATH." go env go version & $ScriptToExecute } finally { # Cleanup: Remove the path from the environment variable and then the temporary folder. Write-Host "Starting cleanup..." $env:Path = $oldPath Write-Host "Reverted changes in the environment PATH." # Remove the temporary folder and its contents. if (Test-Path -Path $fullPath) { Remove-Item -Path $fullPath -Recurse -Force Write-Host "Temporary folder and its contents have been removed." } else { Write-Host "Temporary folder does not exist, no cleanup needed." } } ================================================ FILE: .ci/scripts/windows/sign-msi.ps1 ================================================ # Sign Windows artifacts using azuretool # This script processes MSI files from the artifacts directory $ErrorActionPreference = "Stop" # Define paths $ARTIFACT_DIR = "artifacts" $TIMESTAMP_RFC3161 = "http://timestamp.digicert.com" Write-Host "Looking for Windows artifacts to sign in $ARTIFACT_DIR..." # Find all Windows MSI files $msiFiles = Get-ChildItem -Path $ARTIFACT_DIR -Filter "cloudflared-windows-*.msi" -ErrorAction SilentlyContinue if ($msiFiles.Count -eq 0) { Write-Host "No Windows MSI files found in $ARTIFACT_DIR" exit 1 } Write-Host "Found $($msiFiles.Count) file(s) to sign:" foreach ($file in $msiFiles) { Write-Host "Running azuretool sign for $($file.Name)" azuresigntool.exe sign -kvu $env:KEY_VAULT_URL -kvi "$env:KEY_VAULT_CLIENT_ID" -kvs "$env:KEY_VAULT_SECRET" -kvc "$env:KEY_VAULT_CERTIFICATE" -kvt "$env:KEY_VAULT_TENANT_ID" -tr "$TIMESTAMP_RFC3161" -d "Cloudflare Tunnel Daemon" .\\$ARTIFACT_DIR\\$($file.Name) } Write-Host "Signing process completed" ================================================ FILE: .ci/windows.gitlab-ci.yml ================================================ include: - local: .ci/commons.gitlab-ci.yml ################################### ### Defaults for Windows Builds ### ################################### .windows-build-defaults: &windows-build-defaults rules: - !reference [.default-rules, run-always] tags: - windows-x86 cache: {} ########################################## ### Build Cloudflared Windows Binaries ### ########################################## windows-build-cloudflared: <<: *windows-build-defaults stage: build script: - powershell -ExecutionPolicy Bypass -File ".\.ci\scripts\windows\go-wrapper.ps1" "${WIN_GO_VERSION}" ".\.ci\scripts\windows\builds.ps1" artifacts: paths: - artifacts/* ###################################################### ### Load Environment Variables for Component Tests ### ###################################################### windows-load-env-variables: stage: pre-build extends: .component-tests script: - echo "COMPONENT_TESTS_CONFIG=$COMPONENT_TESTS_CONFIG" >> windows.env - echo "COMPONENT_TESTS_CONFIG_CONTENT=$COMPONENT_TESTS_CONFIG_CONTENT" >> windows.env - echo "DNS_API_TOKEN=$DNS_API_TOKEN" >> windows.env # We have to encode the `COMPONENT_TESTS_ORIGINCERT` secret, because it content is a file, otherwise we can't export it using gitlab - echo "COMPONENT_TESTS_ORIGINCERT=$(echo "$COMPONENT_TESTS_ORIGINCERT" | base64 -w0)" >> windows.env - echo "KEY_VAULT_URL=$KEY_VAULT_URL" >> windows.env - echo "KEY_VAULT_CLIENT_ID=$KEY_VAULT_CLIENT_ID" >> windows.env - echo "KEY_VAULT_TENANT_ID=$KEY_VAULT_TENANT_ID" >> windows.env - echo "KEY_VAULT_SECRET=$KEY_VAULT_SECRET" >> windows.env - echo "KEY_VAULT_CERTIFICATE=$KEY_VAULT_CERTIFICATE" >> windows.env variables: COMPONENT_TESTS_CONFIG_CONTENT: Y2xvdWRmbGFyZWRfYmluYXJ5OiAuL2Nsb3VkZmxhcmVkLmV4ZQpjcmVkZW50aWFsc19maWxlOiBjcmVkLmpzb24Kb3JpZ2luY2VydDogY2VydC5wZW0Kem9uZV9kb21haW46IGFyZ290dW5uZWx0ZXN0LmNvbQp6b25lX3RhZzogNDg3OTZmMWU3MGJiNzY2OWMyOWJiNTFiYTI4MmJmNjU= secrets: KEY_VAULT_URL: vault: gitlab/cloudflare/tun/cloudflared/_dev/azure_vault/app_info/key_vault_url@kv file: false KEY_VAULT_CLIENT_ID: vault: gitlab/cloudflare/tun/cloudflared/_dev/azure_vault/app_info/key_vault_client_id@kv file: false KEY_VAULT_TENANT_ID: vault: gitlab/cloudflare/tun/cloudflared/_dev/azure_vault/app_info/key_vault_tenant_id@kv file: false KEY_VAULT_SECRET: vault: gitlab/cloudflare/tun/cloudflared/_dev/azure_vault/secret/key_vault_secret@kv file: false KEY_VAULT_CERTIFICATE: vault: gitlab/cloudflare/tun/cloudflared/_dev/azure_vault/certificate/key_vault_certificate@kv file: false artifacts: access: 'none' reports: dotenv: windows.env ################################### ### Run Windows Component Tests ### ################################### windows-component-tests-cloudflared: <<: *windows-build-defaults stage: test needs: ["windows-load-env-variables"] script: # We have to decode the secret we encoded on the `windows-load-env-variables` job - $env:COMPONENT_TESTS_ORIGINCERT = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($env:COMPONENT_TESTS_ORIGINCERT)) - powershell -ExecutionPolicy Bypass -File ".\.ci\scripts\windows\go-wrapper.ps1" "${WIN_GO_VERSION}" ".\.ci\scripts\windows\component-test.ps1" artifacts: reports: junit: report.xml ################################ ### Package Windows Binaries ### ################################ windows-package: rules: - !reference [.default-rules, run-on-master] stage: package needs: - ci-image-get-image-ref - windows-build-cloudflared image: $BUILD_IMAGE script: - .ci/scripts/package-windows.sh cache: {} artifacts: paths: - artifacts/* ############################# ### Sign Windows Binaries ### ############################# windows-package-sign: <<: *windows-build-defaults rules: - !reference [.default-rules, run-on-master] stage: package needs: - windows-package - windows-load-env-variables script: - powershell -ExecutionPolicy Bypass -File ".\.ci\scripts\windows\sign-msi.ps1" artifacts: paths: - artifacts/* ================================================ FILE: .docker-images ================================================ images: - name: cloudflared dockerfile: Dockerfile.$ARCH context: . version_file: versions registries: - name: docker.io/cloudflare user: env:DOCKER_USER password: env:DOCKER_PASSWORD architectures: - amd64 - arm64 ================================================ FILE: .dockerignore ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/---bug-report.md ================================================ --- name: "\U0001F41B Bug report" about: Create a report to help us improve cloudflared title: "\U0001F41B" labels: 'Priority: Normal, Type: Bug' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Configure '...' 2. Run '....' 3. See error If it's an issue with Cloudflare Tunnel: 4. Tunnel ID : 5. cloudflared config: **Expected behavior** A clear and concise description of what you expected to happen. **Environment and versions** - OS: [e.g. MacOS] - Architecture: [e.g. AMD, ARM] - Version: [e.g. 2022.02.0] **Logs and errors** If applicable, add logs or errors to help explain your problem. **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/---documentation.md ================================================ --- name: "\U0001F4DD Documentation" about: Request new or updated documentation for cloudflared title: "\U0001F4DD" labels: 'Priority: Normal, Type: Documentation' --- **Available Documentation** A link to the documentation that is available today and the areas which could be improved. **Suggested Documentation** A clear and concise description of the documentation, tutorial, or guide that should be added. **Additional context** Add any other context or screenshots about the documentation request here. ================================================ FILE: .github/ISSUE_TEMPLATE/---feature-request.md ================================================ --- name: "\U0001F4A1 Feature request" about: Suggest a feature or enhancement for cloudflared title: "\U0001F4A1" labels: 'Priority: Normal, Type: Feature Request' --- **Describe the feature you'd like** A clear and concise description of the feature. What problem does it solve for you? **Describe alternatives you've considered** Are there any alternatives to solving this problem? If so, what was your experience with them? **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/workflows/check.yaml ================================================ on: [push, pull_request] name: Check jobs: check: strategy: matrix: go-version: [1.22.x] os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - name: Install Go uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} - name: Checkout code uses: actions/checkout@v4 - name: Test run: make test ================================================ FILE: .github/workflows/semgrep.yml ================================================ on: pull_request: {} workflow_dispatch: {} push: branches: - main - master schedule: - cron: '0 0 * * *' name: Semgrep config jobs: semgrep: name: semgrep/ci runs-on: ubuntu-latest env: SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} SEMGREP_URL: https://cloudflare.semgrep.dev SEMGREP_APP_URL: https://cloudflare.semgrep.dev SEMGREP_VERSION_CHECK_URL: https://cloudflare.semgrep.dev/api/check-version container: image: semgrep/semgrep steps: - uses: actions/checkout@v4 - run: semgrep ci ================================================ FILE: .gitignore ================================================ /tmp /bin .idea .build .vscode \#*\# cscope.* /cloudflared /cloudflared.pkg /cloudflared.exe /cloudflared.msi /cloudflared-x86-64* /cloudflared.1 /packaging .DS_Store *-session.log ssh_server_tests/.env /.cover built_artifacts/ component-tests/.venv /artifacts ================================================ FILE: .gitlab-ci.yml ================================================ variables: GO_VERSION: "1.24.13" MAC_GO_VERSION: "go@$GO_VERSION" WIN_GO_VERSION: "go$GO_VERSION" GIT_DEPTH: "0" default: id_tokens: VAULT_ID_TOKEN: aud: https://vault.cfdata.org stages: [ sync, pre-build, build, validate, test, package, release, release-internal, review, ] include: ##################################################### ########## Import Commons Configurations ############ ##################################################### - local: .ci/commons.gitlab-ci.yml ##################################################### ########### Sync Repository with Github ############# ##################################################### - local: .ci/github.gitlab-ci.yml ##################################################### ############# Build or Fetch CI Image ############### ##################################################### - local: .ci/ci-image.gitlab-ci.yml ##################################################### ################## Linux Builds ################### ##################################################### - local: .ci/linux.gitlab-ci.yml ##################################################### ################## Windows Builds ################### ##################################################### - local: .ci/windows.gitlab-ci.yml ##################################################### ################### macOS Builds #################### ##################################################### - local: .ci/mac.gitlab-ci.yml ##################################################### ################# Release Packages ################## ##################################################### - local: .ci/release.gitlab-ci.yml ##################################################### ########## Release Packages Internally ############## ##################################################### - local: .ci/apt-internal.gitlab-ci.yml ##################################################### ############## Manual Claude Review ################# ##################################################### - component: $CI_SERVER_FQDN/cloudflare/ci/ai/review@~latest ================================================ FILE: .golangci.yaml ================================================ linters: enable: # Some of the linters below are commented out. We should uncomment and start running them, but they return # too many problems to fix in one commit. Something for later. - asasalint # Check for pass []any as any in variadic func(...any). - asciicheck # Checks that all code identifiers does not have non-ASCII symbols in the name. - bidichk # Checks for dangerous unicode character sequences. - bodyclose # Checks whether HTTP response body is closed successfully. - decorder # Check declaration order and count of types, constants, variables and functions. - dogsled # Checks assignments with too many blank identifiers (e.g. x, , , _, := f()). - dupl # Tool for code clone detection. - dupword # Checks for duplicate words in the source code. - durationcheck # Check for two durations multiplied together. - errcheck # Errcheck is a program for checking for unchecked errors in Go code. These unchecked errors can be critical bugs in some cases. - errname # Checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error. - exhaustive # Check exhaustiveness of enum switch statements. - gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification. - goimports # Check import statements are formatted according to the 'goimport' command. Reformat imports in autofix mode. - gosec # Inspects source code for security problems. - gosimple # Linter for Go source code that specializes in simplifying code. - govet # Vet examines Go source code and reports suspicious constructs. It is roughly the same as 'go vet' and uses its passes. - ineffassign # Detects when assignments to existing variables are not used. - importas # Enforces consistent import aliases. - misspell # Finds commonly misspelled English words. - prealloc # Finds slice declarations that could potentially be pre-allocated. - promlinter # Check Prometheus metrics naming via promlint. - sloglint # Ensure consistent code style when using log/slog. - sqlclosecheck # Checks that sql.Rows, sql.Stmt, sqlx.NamedStmt, pgx.Query are closed. - staticcheck # It's a set of rules from staticcheck. It's not the same thing as the staticcheck binary. - usetesting # Reports uses of functions with replacement inside the testing package. - testableexamples # Linter checks if examples are testable (have an expected output). - testifylint # Checks usage of github.com/stretchr/testify. - tparallel # Tparallel detects inappropriate usage of t.Parallel() method in your Go test codes. - unconvert # Remove unnecessary type conversions. - unused # Checks Go code for unused constants, variables, functions and types. - wastedassign # Finds wasted assignment statements. - whitespace # Whitespace is a linter that checks for unnecessary newlines at the start and end of functions, if, for, etc. - zerologlint # Detects the wrong usage of zerolog that a user forgets to dispatch with Send or Msg. # Other linters are disabled, list of all is here: https://golangci-lint.run/usage/linters/ run: timeout: 5m modules-download-mode: vendor # output configuration options output: formats: - format: 'colored-line-number' print-issued-lines: true print-linter-name: true issues: # Maximum issues count per one linter. # Set to 0 to disable. # Default: 50 max-issues-per-linter: 50 # Maximum count of issues with the same text. # Set to 0 to disable. # Default: 3 max-same-issues: 15 # Show only new issues: if there are unstaged changes or untracked files, # only those changes are analyzed, else only changes in HEAD~ are analyzed. # It's a super-useful option for integration of golangci-lint into existing large codebase. # It's not practical to fix all existing issues at the moment of integration: # much better don't allow issues in new code. # # Default: false new: true # Show only new issues created after git revision `REV`. # Default: "" new-from-rev: ac34f94d423273c8fa8fdbb5f2ac60e55f2c77d5 # Show issues in any part of update files (requires new-from-rev or new-from-patch). # Default: false whole-files: true # Which dirs to exclude: issues from them won't be reported. # Can use regexp here: `generated.*`, regexp is applied on full path, # including the path prefix if one is set. # Default dirs are skipped independently of this option's value (see exclude-dirs-use-default). # "/" will be replaced by current OS file path separator to properly work on Windows. # Default: [] exclude-dirs: - vendor linters-settings: # Check exhaustiveness of enum switch statements. exhaustive: # Presence of "default" case in switch statements satisfies exhaustiveness, # even if all enum members are not listed. # Default: false default-signifies-exhaustive: true ================================================ FILE: .mac_resources/scripts/postinstall ================================================ #!/bin/bash # uninstall first in case this is an upgrade /usr/local/bin/cloudflared service uninstall # install the new service using launchctl /usr/local/bin/cloudflared service install ================================================ FILE: .mac_resources/uninstall.sh ================================================ #!/bin/bash /usr/local/bin/cloudflared service uninstall rm /usr/local/bin/cloudflared pkgutil --forget com.cloudflare.cloudflared ================================================ FILE: .vulnignore ================================================ # Add vulnerability IDs (e.g., GO-2022-0450) to ignore, one per line. # You can also add comments on the same line after the ID. ================================================ FILE: AGENTS.md ================================================ # Cloudflared Cloudflare's command-line tool and networking daemon written in Go. Production-grade tunneling and network connectivity services used by millions of developers and organizations worldwide. ## Essential Commands ### Build & Test (Always run before commits) ```bash # Full development check (run before any commit) make test lint # Build for current platform make cloudflared # Run all unit tests with coverage make test make cover # Run specific test go test -run TestFunctionName ./path/to/package # Run tests with race detection go test -race ./... ``` ### Platform-Specific Builds ```bash # Linux TARGET_OS=linux TARGET_ARCH=amd64 make cloudflared # Windows TARGET_OS=windows TARGET_ARCH=amd64 make cloudflared # macOS ARM64 TARGET_OS=darwin TARGET_ARCH=arm64 make cloudflared # FIPS compliant build FIPS=true make cloudflared ``` ### Code Quality & Formatting ```bash # Run linter (38+ enabled linters) make lint # Auto-fix formatting make fmt gofmt -w . goimports -w . # Security scanning make vet # Component tests (Python integration tests) cd component-tests && python -m pytest test_file.py::test_function_name ``` ## Project Knowledge ### Package Structure - Use meaningful package names that reflect functionality - Package names should be lowercase, single words when possible - Avoid generic names like `util`, `common`, `helper` ### Function and Method Guidelines ```go // Good: Clear purpose, proper error handling func (c *Connection) HandleRequest(ctx context.Context, req *http.Request) error { if req == nil { return errors.New("request cannot be nil") } // Implementation... return nil } ``` ### Error Handling - Always handle errors explicitly, never ignore them - Use `fmt.Errorf` for error wrapping - Create meaningful error messages with context - Use error variables for common errors ```go // Good error handling patterns if err != nil { return fmt.Errorf("failed to process connection: %w", err) } ``` ### Logging Standards - Use `github.com/rs/zerolog` for structured logging - Include relevant context fields - Use appropriate log levels (Debug, Info, Warn, Error) ```go logger.Info(). Str("tunnelID", tunnel.ID). Int("connIndex", connIndex). Msg("Connection established") ``` ### Testing Patterns - Use `github.com/stretchr/testify` for assertions - Test files end with `_test.go` - Use table-driven tests for multiple scenarios - Always use `t.Parallel()` for parallel-safe tests - Use meaningful test names that describe behavior ```go func TestMetricsListenerCreation(t *testing.T) { t.Parallel() // Test implementation assert.Equal(t, expected, actual) require.NoError(t, err) } ``` ### Constants and Variables ```go const ( MaxGracePeriod = time.Minute * 3 MaxConcurrentStreams = math.MaxUint32 LogFieldConnIndex = "connIndex" ) var ( // Group related variables switchingProtocolText = fmt.Sprintf("%d %s", http.StatusSwitchingProtocols, http.StatusText(http.StatusSwitchingProtocols)) flushableContentTypes = []string{sseContentType, grpcContentType, sseJsonContentType} ) ``` ### Type Definitions - Define interfaces close to their usage - Keep interfaces small and focused - Use descriptive names for complex types ```go type TunnelConnection interface { Serve(ctx context.Context) error } type TunnelProperties struct { Credentials Credentials QuickTunnelUrl string } ``` ## Key Architectural Patterns ### Context Usage - Always accept `context.Context` as first parameter for long-running operations - Respect context cancellation in loops and blocking operations - Pass context through call chains ### Concurrency - Use channels for goroutine communication - Protect shared state with mutexes - Prefer `sync.RWMutex` for read-heavy workloads ### Configuration - Use structured configuration with validation - Support both file-based and CLI flag configuration - Provide sensible defaults ### Metrics and Observability - Instrument code with Prometheus metrics - Use OpenTelemetry for distributed tracing - Include structured logging with relevant context ## Boundaries ### ✅ Always Do - Run `make test lint` before any commit - Handle all errors explicitly with proper context - Use `github.com/rs/zerolog` for all logging - Add `t.Parallel()` to all parallel-safe tests - Follow the import grouping conventions - Use meaningful variable and function names - Include context.Context for long-running operations - Close resources in defer statements ### ⚠️ Ask First Before - Adding new dependencies to go.mod - Modifying CI/CD configuration files - Changing build system or Makefile - Modifying component test infrastructure - Adding new linter rules or changing golangci-lint config - Making breaking changes to public APIs - Changing logging levels or structured logging fields ### 🚫 Never Do - Ignore errors without explicit handling (`_ = err`) - Use generic package names (`util`, `helper`, `common`) - Commit code that fails `make test lint` - Use `fmt.Print*` instead of structured logging - Modify vendor dependencies directly - Commit secrets, credentials, or sensitive data - Use deprecated or unsafe Go patterns - Skip testing for new functionality - Remove existing tests unless they're genuinely invalid ## Dependencies Management - Use Go modules (`go.mod`) exclusively - Vendor dependencies for reproducible builds - Keep dependencies up-to-date and secure - Prefer standard library when possible - Cloudflared uses a fork of quic-go always check release notes before bumping this dependency. ## Security Considerations - FIPS compliance support available - Vulnerability scanning integrated in CI - Credential handling follows security best practices - Network security with TLS/QUIC protocols - Regular security audits and updates - Post quantum encryption ## Common Patterns to Follow 1. **Graceful shutdown**: Always implement proper cleanup 2. **Resource management**: Close resources in defer statements 3. **Error propagation**: Wrap errors with meaningful context 4. **Configuration validation**: Validate inputs early 5. **Logging consistency**: Use structured logging throughout 6. **Testing coverage**: Aim for comprehensive test coverage 7. **Documentation**: Comment exported functions and types Remember: This is a mission-critical networking tool used in production by many organizations. Code quality, security, and reliability are paramount. ================================================ FILE: CHANGES.md ================================================ ## 2026.2.0 ### Breaking Change - Removes the `proxy-dns` feature from cloudflared. This feature allowed running a local DNS over HTTPS (DoH) proxy. Users who relied on this functionality should migrate to alternative solutions. Removed commands and flags: - `cloudflared proxy-dns` - `cloudflared tunnel proxy-dns` - `--proxy-dns`, `--proxy-dns-port`, `--proxy-dns-address`, `--proxy-dns-upstream`, `--proxy-dns-max-upstream-conns`, `--proxy-dns-bootstrap` - `resolver` section in configuration file ## 2025.7.1 ### Notices - `cloudflared` will no longer officially support Debian and Ubuntu distros that reached end-of-life: `buster`, `bullseye`, `impish`, `trusty`. ## 2025.1.1 ### New Features - This release introduces the use of new Post Quantum curves and the ability to use Post Quantum curves when running tunnels with the QUIC protocol this applies to non-FIPS and FIPS builds. ## 2024.12.2 ### New Features - This release introduces the ability to collect troubleshooting information from one instance of cloudflared running on the local machine. The command can be executed as `cloudflared tunnel diag`. ## 2024.12.1 ### Notices - The use of the `--metrics` is still honoured meaning that if this flag is set the metrics server will try to bind it, however, this version includes a change that makes the metrics server bind to a port with a semi-deterministic approach. If the metrics flag is not present the server will bind to the first available port of the range 20241 to 20245. In case of all ports being unavailable then the fallback is to bind to a random port. ## 2024.10.0 ### Bug Fixes - We fixed a bug related to `--grace-period`. Tunnels that use QUIC as transport weren't abiding by this waiting period before forcefully closing the connections to the edge. From now on, both QUIC and HTTP2 tunnels will wait for either the grace period to end (defaults to 30 seconds) or until the last in-flight request is handled. Users that wish to maintain the previous behavior should set `--grace-period` to 0 if `--protocol` is set to `quic`. This will force `cloudflared` to shutdown as soon as either SIGTERM or SIGINT is received. ## 2024.2.1 ### Notices - Starting from this version, tunnel diagnostics will be enabled by default. This will allow the engineering team to remotely get diagnostics from cloudflared during debug activities. Users still have the capability to opt-out of this feature by defining `--management-diagnostics=false` (or env `TUNNEL_MANAGEMENT_DIAGNOSTICS`). ## 2023.9.0 ### Notices - The `warp-routing` `enabled: boolean` flag is no longer supported in the configuration file. Warp Routing traffic (eg TCP, UDP, ICMP) traffic is proxied to cloudflared if routes to the target tunnel are configured. This change does not affect remotely managed tunnels, but for locally managed tunnels, users that might be relying on this feature flag to block traffic should instead guarantee that tunnel has no Private Routes configured for the tunnel. ## 2023.7.0 ### New Features - You can now enable additional diagnostics over the management.argotunnel.com service for your active cloudflared connectors via a new runtime flag `--management-diagnostics` (or env `TUNNEL_MANAGEMENT_DIAGNOSTICS`). This feature is provided as opt-in and requires the flag to enable. Endpoints such as /metrics provides your prometheus metrics endpoint another mechanism to be reached. Additionally /debug/pprof/(goroutine|heap) are also introduced to allow for remotely retrieving active pprof information from a running cloudflared connector. ## 2023.4.1 ### New Features - You can now stream your logs from your remote cloudflared to your local terminal with `cloudflared tail `. This new feature requires the remote cloudflared to be version 2023.4.1 or higher. ## 2023.3.2 ### Notices - Due to the nature of QuickTunnels (https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/do-more-with-tunnels/trycloudflare/) and its intended usage for testing and experiment of Cloudflare Tunnels, starting from 2023.3.2, QuickTunnels only make a single connection to the edge. If users want to use Tunnels in a production environment, they should move to Named Tunnels instead. (https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/tunnel-guide/remote/#set-up-a-tunnel-remotely-dashboard-setup) ## 2023.3.1 ### Breaking Change - Running a tunnel without ingress rules defined in configuration file nor from the CLI flags will no longer provide a default ingress rule to localhost:8080 and instead will return HTTP response code 503 for all incoming HTTP requests. ### Security Fixes - Windows 32 bit machines MSI now defaults to Program Files to install cloudflared. (See CVE-2023-1314). The cloudflared client itself is unaffected. This just changes how the installer works on 32 bit windows machines. ### Bug Fixes - Fixed a bug that would cause running tunnel on Bastion mode and without ingress rules to crash. ## 2023.2.2 ### Notices - Legacy tunnels were officially deprecated on December 1, 2022. Starting with this version, cloudflared no longer supports connecting legacy tunnels. - h2mux tunnel connection protocol is no longer supported. Any tunnels still configured to use this protocol will alert and use http2 tunnel protocol instead. We recommend using quic protocol for all tunnels going forward. ## 2023.2.1 ### Bug fixes - Fixed a bug in TCP connection proxy that could result in the connection being closed before all data was written. - cloudflared now correctly aborts body write if connection to origin service fails after response headers were sent already. - Fixed a bug introduced in the previous release where debug endpoints were removed. ## 2022.12.0 ### Improvements - cloudflared now attempts to try other edge addresses before falling back to a lower protocol. - cloudflared tunnel no longer spins up a quick tunnel. The call has to be explicit and provide a --url flag. - cloudflared will now randomly pick the first or second region to connect to instead of always connecting to region2 first. ## 2022.9.0 ### New Features - cloudflared now rejects ingress rules with invalid http status codes for http_status. ## 2022.8.1 ### New Features - cloudflared now remembers if it connected to a certain protocol successfully. If it did, it does not fall back to a lower protocol on connection failures. ## 2022.7.1 ### New Features - It is now possible to connect cloudflared tunnel to Cloudflare Global Network with IPv6. See `cloudflared tunnel --help` and look for `edge-ip-version` for more information. For now, the default behavior is to still connect with IPv4 only. ### Bug Fixes - Several bug fixes related with QUIC transport (used between cloudflared tunnel and Cloudflare Global Network). Updating to this version is highly recommended. ## 2022.4.0 ### Bug Fixes - `cloudflared tunnel run` no longer logs the Tunnel token or JSON credentials in clear text as those are the secret that allows to run the Tunnel. ## 2022.3.4 ### New Features - It is now possible to retrieve the credentials that allow to run a Tunnel in case you forgot/lost them. This is achievable with: `cloudflared tunnel token --cred-file /path/to/file.json TUNNEL`. This new feature only works for Tunnels created with cloudflared version 2022.3.0 or more recent. ### Bug Fixes - `cloudflared service install` now starts the underlying agent service on Linux operating system (similarly to the behaviour in Windows and MacOS). ## 2022.3.3 ### Bug Fixes - `cloudflared service install` now starts the underlying agent service on Windows operating system (similarly to the behaviour in MacOS). ## 2022.3.1 ### Bug Fixes - Various fixes to the reliability of `quic` protocol, including an edge case that could lead to cloudflared crashing. ## 2022.3.0 ### New Features - It is now possible to configure Ingress Rules to point to an origin served by unix socket with either HTTP or HTTPS. If the origin starts with `unix:/` then we assume HTTP (existing behavior). Otherwise, the origin can start with `unix+tls:/` for HTTPS. ## 2022.2.1 ### New Features - This project now has a new LICENSE that is more compliant with open source purposes. ### Bug Fixes - Various fixes to the reliability of `quic` protocol. ## 2022.1.3 ### New Features - New `cloudflared tunnel vnet` commands to allow for private routing to be virtualized. This means that the same CIDR can now be used to point to two different Tunnels with `cloudflared tunnel route ip` command. More information will be made available on blog.cloudflare.com and developers.cloudflare.com/cloudflare-one once the feature is globally available. ### Bug Fixes - Correctly handle proxying UDP datagrams with no payload. - Bug fix for origins that use Server-Sent Events (SSE). ## 2022.1.0 ### Improvements - If a specific `protocol` property is defined (e.g. for `quic`), cloudflared no longer falls back to an older protocol (such as `http2`) in face of connectivity errors. This is important because some features are only supported in a specific protocol (e.g. UDP proxying only works for `quic`). Hence, if a user chooses a protocol, cloudflared now adheres to it no matter what. ### Bug Fixes - Stopping cloudflared running with `quic` protocol now respects graceful shutdown. ## 2021.12.2 ### Bug Fixes - Fix logging when `quic` transport is used and UDP traffic is proxied. - FIPS compliant cloudflared binaries will now be released as separate artifacts. Recall that these are only for linux and amd64. ## 2021.12.1 ### Bug Fixes - Fixes Github issue #530 where cloudflared 2021.12.0 could not reach origins that were HTTPS and using certain encryption methods forbidden by FIPS compliance (such as Let's Encrypt certificates). To address this fix we have temporarily reverted FIPS compliance from amd64 linux binaries that was recently introduced (or fixed actually as it was never working before). ## 2021.12.0 ### New Features - Cloudflared binary released for amd64 linux is now FIPS compliant. ### Improvements - Logging about connectivity to Cloudflare edge now only yields `ERR` level logging if there are no connections to Cloudflare edge that are active. Otherwise it logs `WARN` level. ### Bug Fixes - Fixes Github issue #501. ## 2021.11.0 ### Improvements - Fallback from `protocol:quic` to `protocol:http2` immediately if UDP connectivity isn't available. This could be because of a firewall or egress rule. ## 2021.10.4 ### Improvements - Collect quic transport metrics on RTT, packets and bytes transferred. ### Bug Fixes - Fix race condition that was writing to the connection after the http2 handler returns. ## 2021.9.2 ### New features - `cloudflared` can now run with `quic` as the underlying tunnel transport protocol. To try it, change or add "protocol: quic" to your config.yml file or run cloudflared with the `--protocol quic` flag. e.g: `cloudflared tunnel --protocol quic run ` ### Bug Fixes - Fixed some generic transport bugs in `quic` mode. It's advised to upgrade to at least this version (2021.9.2) when running `cloudflared` with `quic` protocol. - `cloudflared` docker images will now show version. ## 2021.8.4 ### Improvements - Temporary tunnels (those hosted on trycloudflare.com that do not require a Cloudflare login) now run as Named Tunnels underneath. We recall that these tunnels should not be relied upon for production usage as they come with no guarantee of uptime. Previous cloudflared versions will soon be unable to run legacy temporary tunnels and will require an update (to this version or more recent). ## 2021.8.2 ### Improvements - Because Equinox os shutting down, all cloudflared releases are now present [here](https://github.com/cloudflare/cloudflared/releases). [Equinox](https://dl.equinox.io/cloudflare/cloudflared/stable) will no longer receive updates. ## 2021.8.0 ### Bug fixes - Prevents tunnel from accidentally running when only proxy-dns should run. ### Improvements - If auto protocol transport lookup fails, we now default to a transport instead of not connecting. ## 2021.6.0 ### Bug Fixes - Fixes a http2 transport (the new default for Named Tunnels) to work with unix socket origins. ## 2021.5.10 ### Bug Fixes - Fixes a memory leak in h2mux transport that connects cloudflared to Cloudflare edge. ## 2021.5.9 ### New Features - Uses new Worker based login helper service to facilitate token exchange in cloudflared flows. ### Bug Fixes - Fixes Centos-7 builds. ## 2021.5.8 ### New Features - When creating a DNS record to point a hostname at a tunnel, you can now use --overwrite-dns to overwrite any existing DNS records with that hostname. This works both when using the CLI to provision DNS, as well as when starting an adhoc named tunnel, e.g.: - `cloudflared tunnel route dns --overwrite-dns foo-tunnel foo.example.com` - `cloudflared tunnel --overwrite-dns --name foo-tunnel --hostname foo.example.com` ## 2021.5.7 ### New Features - Named Tunnels will automatically select the protocol to connect to Cloudflare's edge network. ## 2021.5.0 ### New Features - It is now possible to run the same tunnel using more than one `cloudflared` instance. This is a server-side change and is compatible with any client version that uses Named Tunnels. To get started, visit our [developer documentation](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/run-tunnel/deploy-cloudflared-replicas). - `cloudflared tunnel ingress validate` will now warn about unused keys in your config file. This is helpful for detecting typos in your config. - If `cloudflared` detects it is running inside a Linux container, it will limit itself to use only the number of CPUs the pod has been granted, instead of trying to use every CPU available. ## 2021.4.0 ### Bug Fixes - Fixed proxying of websocket requests to avoid possibility of losing initial frames that were sent in the same TCP packet as response headers [#345](https://github.com/cloudflare/cloudflared/issues/345). - `proxy-dns` option now works in conjunction with running a named tunnel [#346](https://github.com/cloudflare/cloudflared/issues/346). ## 2021.3.6 ### Bug Fixes - Reverted 2021.3.5 improvement to use HTTP/2 in a best-effort manner between cloudflared and origin services because it was found to break in some cases. ## 2021.3.5 ### Improvements - HTTP/2 transport is now always chosen if origin server supports it and the service url scheme is HTTPS. This was previously done in a best attempt manner. ### Bug Fixes - The MacOS binaries were not successfully released in 2021.3.3 and 2021.3.4. This release is aimed at addressing that. ## 2021.3.3 ### Improvements - Tunnel create command, as well as, running ad-hoc tunnels using `cloudflared tunnel -name NAME`, will not overwrite existing files when writing tunnel credentials. ### Bug Fixes - Tunnel create and delete commands no longer use path to credentials from the configuration file. If you need to place tunnel credentials file at a specific location, you must use `--credentials-file` flag. - Access ssh-gen creates properly named keys for SSH short lived certs. ## 2021.3.2 ### New Features - It is now possible to obtain more detailed information about the cloudflared connectors to Cloudflare Edge via `cloudflared tunnel info `. It is possible to sort the output as well as output in different formats, such as: `cloudflared tunnel info --sort-by version --invert-sort --output json `. You can obtain more information via `cloudflared tunnel info --help`. ### Bug Fixes - Don't look for configuration file in default paths when `--config FILE` flag is present after `tunnel` subcommand. - cloudflared access token command now functions correctly with the new token-per-app change from 2021.3.0. ## 2021.3.0 ### New Features - [Cloudflare One Routing](https://developers.cloudflare.com/cloudflare-one/tutorials/warp-to-tunnel) specific commands now show up in the `cloudflared tunnel route --help` output. - There is a new ingress type that allows cloudflared to proxy SOCKS5 as a bastion. You can use it with an ingress rule by adding `service: socks-proxy`. Traffic is routed to any destination specified by the SOCKS5 packet but only if allowed by a rule. In the following example we allow proxying to a certain CIDR but explicitly forbid one address within it: ``` ingress: - hostname: socks.example.com service: socks-proxy originRequest: ipRules: - prefix: 192.168.1.8/32 allow: false - prefix: 192.168.1.0/24 ports: [80, 443] allow: true ``` ### Improvements - Nested commands, such as `cloudflared tunnel run`, now consider CLI arguments even if they appear earlier on the command. For instance, `cloudflared --config config.yaml tunnel run` will now behave the same as `cloudflared tunnel --config config.yaml run` - Warnings are now shown in the output logs whenever cloudflared is running without the most recent version and `no-autoupdate` is `true`. - Access tokens are now stored per Access App instead of per request path. This decreases the number of times that the user is required to authenticate with an Access policy redundantly. ### Bug Fixes - GitHub [PR #317](https://github.com/cloudflare/cloudflared/issues/317) was broken in 2021.2.5 and is now fixed again. ## 2021.2.5 ### New Features - We introduce [Cloudflare One Routing](https://developers.cloudflare.com/cloudflare-one/tutorials/warp-to-tunnel) in beta mode. Cloudflare customer can now connect users and private networks with RFC 1918 IP addresses via the Cloudflare edge network. Users running Cloudflare WARP client in the same organization can connect to the services made available by Argo Tunnel IP routes. Please share your feedback in the GitHub issue tracker. ## 2021.2.4 ### Bug Fixes - Reverts the Improvement released in 2021.2.3 for CLI arguments as it introduced a regression where cloudflared failed to read URLs in configuration files. - cloudflared now logs the reason for failed connections if the error is recoverable. ## 2021.2.3 ### Backward Incompatible Changes - Removes db-connect. The Cloudflare Workers product will continue to support db-connect implementations with versions of cloudflared that predate this release and include support for db-connect. ### New Features - Introduces support for proxy configurations with websockets in arbitrary TCP connections (#318). ### Improvements - (reverted) Nested command line argument handling. ### Bug Fixes - The maximum number of upstream connections is now limited by default which should fix reported issues of cloudflared exhausting CPU usage when faced with connectivity issues. ================================================ FILE: Dockerfile ================================================ # use a builder image for building cloudflare ARG TARGET_GOOS ARG TARGET_GOARCH FROM golang:1.24.13 AS builder ENV GO111MODULE=on \ CGO_ENABLED=0 \ TARGET_GOOS=${TARGET_GOOS} \ TARGET_GOARCH=${TARGET_GOARCH} \ # the CONTAINER_BUILD envvar is used set github.com/cloudflare/cloudflared/metrics.Runtime=virtual # which changes how cloudflared binds the metrics server CONTAINER_BUILD=1 WORKDIR /go/src/github.com/cloudflare/cloudflared/ # copy our sources into the builder image COPY . . # compile cloudflared RUN make cloudflared # use a distroless base image with glibc FROM gcr.io/distroless/base-debian13:nonroot LABEL org.opencontainers.image.source="https://github.com/cloudflare/cloudflared" # copy our compiled binary COPY --from=builder --chown=nonroot /go/src/github.com/cloudflare/cloudflared/cloudflared /usr/local/bin/ # run as nonroot user # We need to use numeric user id's because Kubernetes doesn't support strings: # https://github.com/kubernetes/kubernetes/blob/v1.33.2/pkg/kubelet/kuberuntime/security_context_others.go#L49 # The `nonroot` user maps to `65532`, from: https://github.com/GoogleContainerTools/distroless/blob/main/common/variables.bzl#L18 USER 65532:65532 # command / entrypoint of container ENTRYPOINT ["cloudflared", "--no-autoupdate"] CMD ["version"] ================================================ FILE: Dockerfile.amd64 ================================================ # use a builder image for building cloudflare FROM golang:1.24.13 AS builder ENV GO111MODULE=on \ CGO_ENABLED=0 \ # the CONTAINER_BUILD envvar is used set github.com/cloudflare/cloudflared/metrics.Runtime=virtual # which changes how cloudflared binds the metrics server CONTAINER_BUILD=1 WORKDIR /go/src/github.com/cloudflare/cloudflared/ # copy our sources into the builder image COPY . . # compile cloudflared RUN GOOS=linux GOARCH=amd64 make cloudflared # use a distroless base image with glibc FROM gcr.io/distroless/base-debian13:nonroot LABEL org.opencontainers.image.source="https://github.com/cloudflare/cloudflared" # copy our compiled binary COPY --from=builder --chown=nonroot /go/src/github.com/cloudflare/cloudflared/cloudflared /usr/local/bin/ # run as nonroot user # We need to use numeric user id's because Kubernetes doesn't support strings: # https://github.com/kubernetes/kubernetes/blob/v1.33.2/pkg/kubelet/kuberuntime/security_context_others.go#L49 # The `nonroot` user maps to `65532`, from: https://github.com/GoogleContainerTools/distroless/blob/main/common/variables.bzl#L18 USER 65532:65532 # command / entrypoint of container ENTRYPOINT ["cloudflared", "--no-autoupdate"] CMD ["version"] ================================================ FILE: Dockerfile.arm64 ================================================ # use a builder image for building cloudflare FROM golang:1.24.13 AS builder ENV GO111MODULE=on \ CGO_ENABLED=0 \ # the CONTAINER_BUILD envvar is used set github.com/cloudflare/cloudflared/metrics.Runtime=virtual # which changes how cloudflared binds the metrics server CONTAINER_BUILD=1 WORKDIR /go/src/github.com/cloudflare/cloudflared/ # copy our sources into the builder image COPY . . # compile cloudflared RUN GOOS=linux GOARCH=arm64 make cloudflared # use a distroless base image with glibc FROM gcr.io/distroless/base-debian13:nonroot-arm64 LABEL org.opencontainers.image.source="https://github.com/cloudflare/cloudflared" # copy our compiled binary COPY --from=builder --chown=nonroot /go/src/github.com/cloudflare/cloudflared/cloudflared /usr/local/bin/ # run as nonroot user # We need to use numeric user id's because Kubernetes doesn't support strings: # https://github.com/kubernetes/kubernetes/blob/v1.33.2/pkg/kubelet/kuberuntime/security_context_others.go#L49 # The `nonroot` user maps to `65532`, from: https://github.com/GoogleContainerTools/distroless/blob/main/common/variables.bzl#L18 USER 65532:65532 # command / entrypoint of container ENTRYPOINT ["cloudflared", "--no-autoupdate"] CMD ["version"] ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: Makefile ================================================ # The targets cannot be run in parallel .NOTPARALLEL: VERSION := $(shell git describe --tags --always --match "[0-9][0-9][0-9][0-9].*.*") MSI_VERSION := $(shell git tag -l --sort=v:refname | grep "w" | tail -1 | cut -c2-) #MSI_VERSION expects the format of the tag to be: (wX.X.X). Starts with the w character to not break cfsetup. #e.g. w3.0.1 or w4.2.10. It trims off the w character when creating the MSI. ifeq ($(ORIGINAL_NAME), true) # Used for builds that want FIPS compilation but want the artifacts generated to still have the original name. BINARY_NAME := cloudflared else ifeq ($(FIPS), true) # Used for FIPS compliant builds that do not match the case above. BINARY_NAME := cloudflared-fips else # Used for all other (non-FIPS) builds. BINARY_NAME := cloudflared endif ifeq ($(NIGHTLY), true) DEB_PACKAGE_NAME := $(BINARY_NAME)-nightly NIGHTLY_FLAGS := --conflicts cloudflared --replaces cloudflared else DEB_PACKAGE_NAME := $(BINARY_NAME) endif # Use git in windows since we don't have access to the `date` tool ifeq ($(TARGET_OS), windows) DATE := $(shell git log -1 --format="%ad" --date=format-local:'%Y-%m-%dT%H:%M UTC' -- RELEASE_NOTES) else DATE := $(shell date -u -r RELEASE_NOTES '+%Y-%m-%d-%H:%M UTC') endif VERSION_FLAGS := -X "main.Version=$(VERSION)" -X "main.BuildTime=$(DATE)" ifdef PACKAGE_MANAGER VERSION_FLAGS := $(VERSION_FLAGS) -X "github.com/cloudflare/cloudflared/cmd/cloudflared/updater.BuiltForPackageManager=$(PACKAGE_MANAGER)" endif ifdef CONTAINER_BUILD VERSION_FLAGS := $(VERSION_FLAGS) -X "github.com/cloudflare/cloudflared/metrics.Runtime=virtual" endif LINK_FLAGS := ifeq ($(FIPS), true) LINK_FLAGS := -linkmode=external -extldflags=-static $(LINK_FLAGS) # Prevent linking with libc regardless of CGO enabled or not. GO_BUILD_TAGS := $(GO_BUILD_TAGS) osusergo netgo fips VERSION_FLAGS := $(VERSION_FLAGS) -X "main.BuildType=FIPS" endif LDFLAGS := -ldflags='$(VERSION_FLAGS) $(LINK_FLAGS)' ifneq ($(GO_BUILD_TAGS),) GO_BUILD_TAGS := -tags "$(GO_BUILD_TAGS)" endif ifeq ($(debug), 1) GO_BUILD_TAGS += -gcflags="all=-N -l" endif IMPORT_PATH := github.com/cloudflare/cloudflared PACKAGE_DIR := $(CURDIR)/packaging PREFIX := /usr INSTALL_BINDIR := $(PREFIX)/bin/ INSTALL_MANDIR := $(PREFIX)/share/man/man1/ LOCAL_ARCH ?= $(shell uname -m) ifneq ($(GOARCH),) TARGET_ARCH ?= $(GOARCH) else ifeq ($(LOCAL_ARCH),x86_64) TARGET_ARCH ?= amd64 else ifeq ($(LOCAL_ARCH),amd64) TARGET_ARCH ?= amd64 else ifeq ($(LOCAL_ARCH),386) TARGET_ARCH ?= 386 else ifeq ($(LOCAL_ARCH),i686) TARGET_ARCH ?= amd64 else ifeq ($(shell echo $(LOCAL_ARCH) | head -c 5),armv8) TARGET_ARCH ?= arm64 else ifeq ($(LOCAL_ARCH),aarch64) TARGET_ARCH ?= arm64 else ifeq ($(LOCAL_ARCH),arm64) TARGET_ARCH ?= arm64 else ifeq ($(shell echo $(LOCAL_ARCH) | head -c 4),armv) TARGET_ARCH ?= arm else ifeq ($(LOCAL_ARCH),s390x) TARGET_ARCH ?= s390x else $(error This system's architecture $(LOCAL_ARCH) isn't supported) endif LOCAL_OS ?= $(shell go env GOOS) ifeq ($(LOCAL_OS),linux) TARGET_OS ?= linux else ifeq ($(LOCAL_OS),darwin) TARGET_OS ?= darwin else ifeq ($(LOCAL_OS),windows) TARGET_OS ?= windows else ifeq ($(LOCAL_OS),freebsd) TARGET_OS ?= freebsd else ifeq ($(LOCAL_OS),openbsd) TARGET_OS ?= openbsd else $(error This system's OS $(LOCAL_OS) isn't supported) endif ifeq ($(TARGET_OS), windows) EXECUTABLE_PATH=./$(BINARY_NAME).exe else EXECUTABLE_PATH=./$(BINARY_NAME) endif ifeq ($(FLAVOR), centos-7) TARGET_PUBLIC_REPO ?= el7 else TARGET_PUBLIC_REPO ?= $(FLAVOR) endif ifneq ($(TARGET_ARM), ) ARM_COMMAND := GOARM=$(TARGET_ARM) endif ifeq ($(TARGET_ARM), 7) PACKAGE_ARCH := armhf else PACKAGE_ARCH := $(TARGET_ARCH) endif #for FIPS compliance, FPM defaults to MD5. RPM_DIGEST := --rpm-digest sha256 GO_TEST_LOG_OUTPUT = /tmp/gotest.log .PHONY: all all: cloudflared test .PHONY: clean clean: go clean .PHONY: vulncheck vulncheck: @./.ci/scripts/vuln-check.sh .PHONY: cloudflared cloudflared: ifeq ($(FIPS), true) $(info Building cloudflared with go-fips) endif GOOS=$(TARGET_OS) GOARCH=$(TARGET_ARCH) $(ARM_COMMAND) go build -mod=vendor $(GO_BUILD_TAGS) $(LDFLAGS) $(IMPORT_PATH)/cmd/cloudflared ifeq ($(FIPS), true) ./check-fips.sh cloudflared endif .PHONY: container container: docker build --build-arg=TARGET_ARCH=$(TARGET_ARCH) --build-arg=TARGET_OS=$(TARGET_OS) -t cloudflare/cloudflared-$(TARGET_OS)-$(TARGET_ARCH):"$(VERSION)" . .PHONY: generate-docker-version generate-docker-version: echo latest $(VERSION) > versions .PHONY: test test: vet $Q go test -json -v -mod=vendor -race $(LDFLAGS) ./... 2>&1 | tee $(GO_TEST_LOG_OUTPUT) ifneq ($(FIPS), true) @go run -mod=readonly github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt@latest -input $(GO_TEST_LOG_OUTPUT) endif .PHONY: cover cover: @echo "" @echo "=====> Total test coverage: <=====" @echo "" # Print the overall coverage here for quick access. $Q go tool cover -func ".cover/c.out" | grep "total:" | awk '{print $$3}' # Generate the HTML report that can be viewed from the browser in CI. $Q go tool cover -html ".cover/c.out" -o .cover/all.html .PHONY: fuzz fuzz: @go test -fuzz=FuzzIPDecoder -fuzztime=600s ./packet @go test -fuzz=FuzzICMPDecoder -fuzztime=600s ./packet @go test -fuzz=FuzzSessionWrite -fuzztime=600s ./quic/v3 @go test -fuzz=FuzzSessionRead -fuzztime=600s ./quic/v3 @go test -fuzz=FuzzRegistrationDatagram -fuzztime=600s ./quic/v3 @go test -fuzz=FuzzPayloadDatagram -fuzztime=600s ./quic/v3 @go test -fuzz=FuzzRegistrationResponseDatagram -fuzztime=600s ./quic/v3 @go test -fuzz=FuzzNewIdentity -fuzztime=600s ./tracing @go test -fuzz=FuzzNewAccessValidator -fuzztime=600s ./validation cloudflared.1: cloudflared_man_template sed -e 's/\$${VERSION}/$(VERSION)/; s/\$${DATE}/$(DATE)/' cloudflared_man_template > cloudflared.1 install: cloudflared cloudflared.1 mkdir -p $(DESTDIR)$(INSTALL_BINDIR) $(DESTDIR)$(INSTALL_MANDIR) install -m755 cloudflared $(DESTDIR)$(INSTALL_BINDIR)/cloudflared install -m644 cloudflared.1 $(DESTDIR)$(INSTALL_MANDIR)/cloudflared.1 # When we build packages, the package name will be FIPS-aware. # But we keep the binary installed by it to be named "cloudflared" regardless. define build_package mkdir -p $(PACKAGE_DIR) cp cloudflared $(PACKAGE_DIR)/cloudflared cp cloudflared.1 $(PACKAGE_DIR)/cloudflared.1 fpm -C $(PACKAGE_DIR) -s dir -t $(1) \ --description 'Cloudflare Tunnel daemon' \ --vendor 'Cloudflare' \ --license 'Apache License Version 2.0' \ --url 'https://github.com/cloudflare/cloudflared' \ -m 'Cloudflare ' \ -a $(PACKAGE_ARCH) -v $(VERSION) -n $(DEB_PACKAGE_NAME) $(RPM_DIGEST) $(NIGHTLY_FLAGS) --after-install postinst.sh --after-remove postrm.sh \ cloudflared=$(INSTALL_BINDIR) cloudflared.1=$(INSTALL_MANDIR) endef .PHONY: cloudflared-deb cloudflared-deb: cloudflared cloudflared.1 $(call build_package,deb) .PHONY: cloudflared-rpm cloudflared-rpm: cloudflared cloudflared.1 $(call build_package,rpm) .PHONY: cloudflared-msi cloudflared-msi: wixl --define Version=$(VERSION) --define Path=$(EXECUTABLE_PATH) --output cloudflared-$(VERSION)-$(TARGET_ARCH).msi cloudflared.wxs .PHONY: github-release-dryrun github-release-dryrun: python3 github_release.py --path $(PWD)/built_artifacts --release-version $(VERSION) --dry-run .PHONY: github-release github-release: python3 github_release.py --path $(PWD)/artifacts/ --release-version $(VERSION) python3 github_message.py --release-version $(VERSION) .PHONY: r2-linux-release r2-linux-release: python3 ./release_pkgs.py .PHONY: r2-next-linux-release # Publishes to a separate R2 repository during GPG key rollover, using dual-key signing. r2-next-linux-release: python3 ./release_pkgs.py --upload-repo-file .PHONY: capnp capnp: which capnp # https://capnproto.org/install.html which capnpc-go # go install zombiezen.com/go/capnproto2/capnpc-go@latest capnp compile -ogo tunnelrpc/proto/tunnelrpc.capnp tunnelrpc/proto/quic_metadata_protocol.capnp .PHONY: vet vet: $Q go vet -mod=vendor github.com/cloudflare/cloudflared/... .PHONY: fmt fmt: @goimports -l -w -local github.com/cloudflare/cloudflared $$(go list -mod=vendor -f '{{.Dir}}' -a ./... | fgrep -v tunnelrpc/proto) @go fmt $$(go list -mod=vendor -f '{{.Dir}}' -a ./... | fgrep -v tunnelrpc/proto) .PHONY: fmt-check fmt-check: @./.ci/scripts/fmt-check.sh .PHONY: lint lint: @golangci-lint run .PHONY: mocks mocks: go generate mocks/mockgen.go .PHONY: ci-build ci-build: @GOOS=linux GOARCH=amd64 $(MAKE) cloudflared @mkdir -p artifacts @mv cloudflared artifacts/cloudflared .PHONY: ci-fips-build ci-fips-build: @FIPS=true GOOS=linux GOARCH=amd64 $(MAKE) cloudflared @mkdir -p artifacts @mv cloudflared artifacts/cloudflared .PHONY: ci-test ci-test: fmt-check lint test @go run -mod=readonly github.com/jstemmer/go-junit-report/v2@latest -in $(GO_TEST_LOG_OUTPUT) -parser gojson -out report.xml -set-exit-code .PHONY: ci-fips-test ci-fips-test: @FIPS=true $(MAKE) ci-test ================================================ FILE: README.md ================================================ # Cloudflare Tunnel client Contains the command-line client for Cloudflare Tunnel, a tunneling daemon that proxies traffic from the Cloudflare network to your origins. This daemon sits between Cloudflare network and your origin (e.g. a webserver). Cloudflare attracts client requests and sends them to you via this daemon, without requiring you to poke holes on your firewall --- your origin can remain as closed as possible. Extensive documentation can be found in the [Cloudflare Tunnel section](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel) of the Cloudflare Docs. All usages related with proxying to your origins are available under `cloudflared tunnel help`. You can also use `cloudflared` to access Tunnel origins (that are protected with `cloudflared tunnel`) for TCP traffic at Layer 4 (i.e., not HTTP/websocket), which is relevant for use cases such as SSH, RDP, etc. Such usages are available under `cloudflared access help`. You can instead use [WARP client](https://developers.cloudflare.com/warp-client/) to access private origins behind Tunnels for Layer 4 traffic without requiring `cloudflared access` commands on the client side. ## Before you get started Before you use Cloudflare Tunnel, you'll need to complete a few steps in the Cloudflare dashboard: you need to add a website to your Cloudflare account. Note that today it is possible to use Tunnel without a website (e.g. for private routing), but for legacy reasons this requirement is still necessary: 1. [Add a website to Cloudflare](https://developers.cloudflare.com/fundamentals/manage-domains/add-site/) 2. [Change your domain nameservers to Cloudflare](https://developers.cloudflare.com/dns/zone-setups/full-setup/setup/) ## Installing `cloudflared` Downloads are available as standalone binaries, a Docker image, and Debian, RPM, and Homebrew packages. You can also find releases [here](https://github.com/cloudflare/cloudflared/releases) on the `cloudflared` GitHub repository. * You can [install on macOS](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/#macos) via Homebrew or by downloading the [latest Darwin amd64 release](https://github.com/cloudflare/cloudflared/releases) * Binaries, Debian, and RPM packages for Linux [can be found here](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/#linux) * A Docker image of `cloudflared` is [available on DockerHub](https://hub.docker.com/r/cloudflare/cloudflared) * You can install on Windows machines with the [steps here](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/#windows) * To build from source, install the required version of go, mentioned in the [Development](#development) section below. Then you can run `make cloudflared`. User documentation for Cloudflare Tunnel can be found at https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/ ## Creating Tunnels and routing traffic Once installed, you can authenticate `cloudflared` into your Cloudflare account and begin creating Tunnels to serve traffic to your origins. * Create a Tunnel with [these instructions](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/get-started/create-remote-tunnel/) * Route traffic to that Tunnel: * Via public [DNS records in Cloudflare](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/routing-to-tunnel/dns/) * Or via a public hostname guided by a [Cloudflare Load Balancer](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/routing-to-tunnel/public-load-balancers/) * Or from [WARP client private traffic](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/private-net/) ## TryCloudflare Want to test Cloudflare Tunnel before adding a website to Cloudflare? You can do so with TryCloudflare using the documentation [available here](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/do-more-with-tunnels/trycloudflare/). ## Deprecated versions Cloudflare currently supports versions of cloudflared that are **within one year** of the most recent release. Breaking changes unrelated to feature availability may be introduced that will impact versions released more than one year ago. You can read more about upgrading cloudflared in our [developer documentation](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/update-cloudflared/). For example, as of January 2023 Cloudflare will support cloudflared version 2023.1.1 to cloudflared 2022.1.1. ## Development ### Requirements - [GNU Make](https://www.gnu.org/software/make/) - [capnp](https://capnproto.org/install.html) - [go >= 1.24](https://go.dev/doc/install) - Optional tools: - [capnpc-go](https://pkg.go.dev/zombiezen.com/go/capnproto2/capnpc-go) - [goimports](https://pkg.go.dev/golang.org/x/tools/cmd/goimports) - [golangci-lint](https://github.com/golangci/golangci-lint) - [gomocks](https://pkg.go.dev/go.uber.org/mock) ### Build To build cloudflared locally run `make cloudflared` ### Test To locally run the tests run `make test` ### Linting To format the code and keep a good code quality use `make fmt` and `make lint` ### Mocks After changes on interfaces you might need to regenerate the mocks, so run `make mock` ================================================ FILE: RELEASE_NOTES ================================================ 2026.3.0 - 2026-03-05 TUN-10292: Add cloudflared management token command - 2026-03-03 chore: Addressing small fixes and typos - 2026-03-03 fix: Update go-sentry and go-oidc to address CVE's - 2026-02-24 TUN-10258: add agents.md - 2026-02-23 TUN-10267: Update mods to fix CVE GO-2026-4394 - 2026-02-20 TUN-10247: Update tail command to use /management/logs endpoint - 2026-02-11 TUN-9858: Add more information to proxy-dns removal message 2026.2.0 - 2026-02-06 TUN-10216: TUN fix cloudflare vulnerabilities GO-2026-4340 and GO-2026-4341 - 2026-02-02 TUN-9858: Remove proxy-dns feature from cloudflared 2026.1.2 - 2026-01-23 Revert "TUN-9863: Update pipelines to use cloudflared EV Certificate" - 2026-01-21 Revert "TUN-9886 notarize cloudflared" - 2025-12-12 TUN-9886 notarize cloudflared 2026.1.1 - 2026-01-19 fix: Update boto3 to run on trixie - 2026-01-19 fix: Fix wixl bundling tool for windows msi packages - 2026-01-19 fix: rpm bundling and rpm key import 2026.1.0 - 2026-01-13 TUN-10162: Update go to 1.24.11 and Debian distroless to debian13 - 2025-11-21 Replace jira.cfops.it with jira.cfdata.org in connection/http2_test.go - 2025-11-19 TUN-9863: Update pipelines to use cloudflared EV Certificate - 2025-11-07 TUN-9800: Migrate apt internal builds to Gitlab - 2025-11-04 TUN-9998: Don't need to read origin cert to determine if the endpoint is fedramp - 2025-10-13 TUN-9910: Make the metadata key to carry HTTP status over QUIC transport a constant 2025.11.1 - 2025-11-07 TUN-9800: Fix docker hub push step 2025.11.0 - 2025-11-06 TUN-9863: Introduce Code Signing for Windows Builds - 2025-11-06 TUN-9800: Prefix gitlab steps with operating system - 2025-11-04 chore: Update cloudflared signing key name in index.html - 2025-10-31 chore: add claude review - 2025-10-31 Chore: Update documentation links in README - 2025-10-31 TUN-9800: Add pipelines for linux packaging 2025.10.1 - 2025-10-30 chore: Update ci image to use goboring 1.24.9 - 2025-10-28 TUN-9849: Add cf-proxy-* to control response headers - 2025-10-24 TUN-9961: Add pkg.cloudflared.com index.html to git repo - 2025-10-23 TUN-9954: Update from go1.24.6 to go1.24.9 - 2025-10-23 Fix systemd service installation hanging - 2025-10-21 TUN-9941: Use new GPG key for RPM builds - 2025-10-21 TUN-9941: Fix typo causing r2-release-next deployment to fail - 2025-10-21 TUN-9941: Lookup correct key for RPM signature - 2025-10-15 TUN-9919: Make RPM postinstall scriplet idempotent - 2025-10-14 TUN-9916: Fix the cloudflared binary path used in the component test 2025.10.0 - 2025-10-14 chore: Fix upload of RPM repo file during double signing - 2025-10-13 TUN-9882: Bump datagram v3 write channel capacity - 2025-10-10 chore: Fix import of GPG keys when two keys are provided - 2025-10-10 chore: Fix parameter order when uploading RPM .repo file to R2 - 2025-10-10 TUN-9883: Add new datagram v3 feature flag - 2025-10-09 chore: Force usage of go-boring 1.24 - 2025-10-08 TUN-9882: Improve metrics for datagram v3 - 2025-10-07 GRC-16749: Add fedramp tags to catalog - 2025-10-07 TUN-9882: Add buffers for UDP and ICMP datagrams in datagram v3 - 2025-10-07 TUN-9882: Add write deadline for UDP origin writes - 2025-09-29 TUN-9776: Support signing Debian packages with two keys for rollover - 2025-09-22 TUN-9800: Add pipeline to sync between gitlab and github repos 2025.9.1 - 2025-09-22 TUN-9855: Create script to ignore vulnerabilities from govuln check - 2025-09-19 TUN-9852: Remove fmt.Println from cloudflared access command 2025.9.0 - 2025-09-15 TUN-9820: Add support for FedRAMP in originRequest Access config - 2025-09-11 TUN-9800: Migrate cloudflared-ci pipelines to Gitlab CI - 2025-09-04 TUN-9803: Add windows builds to gitlab-ci - 2025-08-27 TUN-9755: Set endpoint in tunnel credentials when generating locally managed tunnel with a Fed token 2025.8.1 - 2025-08-19 AUTH-7480 update fed callback url for login helper - 2025-08-19 CUSTESC-53681: Correct QUIC connection management for datagram handlers - 2025-08-12 AUTH-7260: Add support for login interstitial auto closure 2025.8.0 - 2025-08-07 vuln: Fix GO-2025-3770 vulnerability - 2025-07-23 TUN-9583: set proper url and hostname for cloudflared tail command - 2025-07-07 TUN-9542: Remove unsupported Debian-based releases 2025.7.0 - 2025-07-03 TUN-9540: Use numeric user id for Dockerfiles - 2025-07-01 TUN-9161: Remove P256Kyber768Draft00PQKex curve from nonFips curve preferences - 2025-07-01 TUN-9531: Bump go-boring from 1.24.2 to 1.24.4 - 2025-07-01 TUN-9511: Add metrics for virtual DNS origin - 2025-06-30 TUN-9470: Add OriginDialerService to include TCP - 2025-06-30 TUN-9473: Add --dns-resolver-addrs flag - 2025-06-27 TUN-9472: Add virtual DNS service - 2025-06-23 TUN-9469: Centralize UDP origin proxy dialing as ingress service 2025.6.1 - 2025-06-16 TUN-9467: add vulncheck to cloudflared - 2025-06-16 TUN-9495: Remove references to cloudflare-go - 2025-06-16 TUN-9371: Add logging format as JSON - 2025-06-12 TUN-9467: bump coredns to solve CVE 2025.6.0 - 2025-06-06 TUN-9016: update go to 1.24 - 2025-06-05 TUN-9171: Use `is_default_network` instead of `is_default` to create vnet's 2025.5.0 - 2025-05-14 TUN-9319: Add dynamic loading of features to connections via ConnectionOptionsSnapshot - 2025-05-13 TUN-9322: Add metric for unsupported RPC commands for datagram v3 - 2025-05-07 TUN-9291: Remove dynamic reloading of features for datagram v3 2025.4.2 - 2025-04-30 chore: Do not use gitlab merge request pipelines - 2025-04-30 DEVTOOLS-16383: Create GitlabCI pipeline to release Mac builds - 2025-04-24 TUN-9255: Improve flush on write conditions in http2 tunnel type to match what is done on the edge - 2025-04-10 SDLC-3727 - Adding FIPS status to backstage 2025.4.0 - 2025-04-02 Fix broken links in `cmd/cloudflared/*.go` related to running tunnel as a service - 2025-04-02 chore: remove repetitive words - 2025-04-01 Fix messages to point to one.dash.cloudflare.com - 2025-04-01 feat: emit explicit errors for the `service` command on unsupported OSes - 2025-04-01 Use RELEASE_NOTES date instead of build date - 2025-04-01 chore: Update tunnel configuration link in the readme - 2025-04-01 fix: expand home directory for credentials file - 2025-04-01 fix: Use path and filepath operation appropriately - 2025-04-01 feat: Adds a new command line for tunnel run for token file - 2025-04-01 chore: fix linter rules - 2025-03-17 TUN-9101: Don't ignore errors on `cloudflared access ssh` - 2025-03-06 TUN-9089: Pin go import to v0.30.0, v0.31.0 requires go 1.23 2025.2.1 - 2025-02-26 TUN-9016: update base-debian to v12 - 2025-02-25 TUN-8960: Connect to FED API GW based on the OriginCert's endpoint - 2025-02-25 TUN-9007: modify logic to resolve region when the tunnel token has an endpoint field - 2025-02-13 SDLC-3762: Remove backstage.io/source-location from catalog-info.yaml - 2025-02-06 TUN-8914: Create a flags module to group all cloudflared cli flags 2025.2.0 - 2025-02-03 TUN-8914: Add a new configuration to locally override the max-active-flows - 2025-02-03 Bump x/crypto to 0.31.0 2025.1.1 - 2025-01-30 TUN-8858: update go to 1.22.10 and include quic-go FIPS changes - 2025-01-30 TUN-8855: fix lint issues - 2025-01-30 TUN-8855: Update PQ curve preferences - 2025-01-30 TUN-8857: remove restriction for using FIPS and PQ - 2025-01-30 TUN-8894: report FIPS+PQ error to Sentry when dialling to the edge - 2025-01-22 TUN-8904: Rename Connect Response Flow Rate Limited metadata - 2025-01-21 AUTH-6633 Fix cloudflared access login + warp as auth - 2025-01-20 TUN-8861: Add session limiter to UDP session manager - 2025-01-20 TUN-8861: Rename Session Limiter to Flow Limiter - 2025-01-17 TUN-8900: Add import of Apple Developer Certificate Authority to macOS Pipeline - 2025-01-17 TUN-8871: Accept login flag to authenticate with Fedramp environment - 2025-01-16 TUN-8866: Add linter to cloudflared repository - 2025-01-14 TUN-8861: Add session limiter to TCP session manager - 2025-01-13 TUN-8861: Add configuration for active sessions limiter - 2025-01-09 TUN-8848: Don't treat connection shutdown as an error condition when RPC server is done 2025.1.0 - 2025-01-06 TUN-8842: Add Ubuntu Noble and 'any' debian distributions to release script - 2025-01-06 TUN-8807: Add support_datagram_v3 to remote feature rollout - 2024-12-20 TUN-8829: add CONTAINER_BUILD to dockerfiles 2024.12.2 - 2024-12-19 TUN-8822: Prevent concurrent usage of ICMPDecoder - 2024-12-18 TUN-8818: update changes document to reflect newly added diag subcommand - 2024-12-17 TUN-8817: Increase close session channel by one since there are two writers - 2024-12-13 TUN-8797: update CHANGES.md with note about semi-deterministic approach used to bind metrics server - 2024-12-13 TUN-8724: Add CLI command for diagnostic procedure - 2024-12-11 TUN-8786: calculate cli flags once for the diagnostic procedure - 2024-12-11 TUN-8792: Make diag/system endpoint always return a JSON - 2024-12-10 TUN-8783: fix log collectors for the diagnostic procedure - 2024-12-10 TUN-8785: include the icmp sources in the diag's tunnel state - 2024-12-10 TUN-8784: Set JSON encoder options to print formatted JSON when writing diag files 2024.12.1 - 2024-12-10 TUN-8795: update createrepo to createrepo_c to fix the release_pkgs.py script 2024.12.0 - 2024-12-09 TUN-8640: Add ICMP support for datagram V3 - 2024-12-09 TUN-8789: make python package installation consistent - 2024-12-06 TUN-8781: Add Trixie, drop Buster. Default to Bookworm - 2024-12-05 TUN-8775: Make sure the session Close can only be called once - 2024-12-04 TUN-8725: implement diagnostic procedure - 2024-12-04 TUN-8767: include raw output from network collector in diagnostic zipfile - 2024-12-04 TUN-8770: add cli configuration and tunnel configuration to diagnostic zipfile - 2024-12-04 TUN-8768: add job report to diagnostic zipfile - 2024-12-03 TUN-8726: implement compression routine to be used in diagnostic procedure - 2024-12-03 TUN-8732: implement port selection algorithm - 2024-12-03 TUN-8762: fix argument order when invoking tracert and modify network info output parsing. - 2024-12-03 TUN-8769: fix k8s log collector arguments - 2024-12-03 TUN-8727: extend client to include function to get cli configuration and tunnel configuration - 2024-11-29 TUN-8729: implement network collection for diagnostic procedure - 2024-11-29 TUN-8727: implement metrics, runtime, system, and tunnelstate in diagnostic http client - 2024-11-27 TUN-8733: add log collection for docker - 2024-11-27 TUN-8734: add log collection for kubernetes - 2024-11-27 TUN-8640: Refactor ICMPRouter to support new ICMPResponders - 2024-11-26 TUN-8735: add managed/local log collection - 2024-11-25 TUN-8728: implement diag/tunnel endpoint - 2024-11-25 TUN-8730: implement diag/configuration - 2024-11-22 TUN-8737: update metrics server port selection - 2024-11-22 TUN-8731: Implement diag/system endpoint - 2024-11-21 TUN-8748: Migrated datagram V3 flows to use migrated context 2024.11.1 - 2024-11-18 Add cloudflared tunnel ready command - 2024-11-14 Make metrics a requirement for tunnel ready command - 2024-11-12 TUN-8701: Simplify flow registration logs for datagram v3 - 2024-11-11 add: new go-fuzz targets - 2024-11-07 TUN-8701: Add metrics and adjust logs for datagram v3 - 2024-11-06 TUN-8709: Add session migration for datagram v3 - 2024-11-04 Fixed 404 in README.md to TryCloudflare - 2024-09-24 Update semgrep.yml 2024.11.0 - 2024-11-05 VULN-66059: remove ssh server tests - 2024-11-04 TUN-8700: Add datagram v3 muxer - 2024-11-04 TUN-8646: Allow experimental feature support for datagram v3 - 2024-11-04 TUN-8641: Expose methods to simplify V3 Datagram parsing on the edge - 2024-10-31 TUN-8708: Bump python min version to 3.10 - 2024-10-31 TUN-8667: Add datagram v3 session manager - 2024-10-25 TUN-8692: remove dashes from session id - 2024-10-24 TUN-8694: Rework release script - 2024-10-24 TUN-8661: Refactor connection methods to support future different datagram muxing methods - 2024-07-22 TUN-8553: Bump go to 1.22.5 and go-boring 1.22.5-1 2024.10.1 - 2024-10-23 TUN-8694: Fix github release script - 2024-10-21 Revert "TUN-8592: Use metadata from the edge to determine if request body is empty for QUIC transport" - 2024-10-18 TUN-8688: Correct UDP bind for IPv6 edge connectivity on macOS - 2024-10-17 TUN-8685: Bump coredns dependency - 2024-10-16 TUN-8638: Add datagram v3 serializers and deserializers - 2024-10-15 chore: Remove h2mux code - 2024-10-11 TUN-8631: Abort release on version mismatch 2024.10.0 - 2024-10-01 TUN-8646: Add datagram v3 support feature flag - 2024-09-30 TUN-8621: Fix cloudflared version in change notes to account for release date - 2024-09-19 Adding semgrep yaml file - 2024-09-12 TUN-8632: Delay checking auto-update by the provided frequency - 2024-09-11 TUN-8630: Check checksum of downloaded binary to compare to current for auto-updating - 2024-09-09 TUN-8629: Cloudflared update on Windows requires running it twice to update - 2024-09-06 PPIP-2310: Update quick tunnel disclaimer - 2024-08-30 TUN-8621: Prevent QUIC connection from closing before grace period after unregistering - 2024-08-09 TUN-8592: Use metadata from the edge to determine if request body is empty for QUIC transport - 2024-06-26 TUN-8484: Print response when QuickTunnel can't be unmarshalled 2024.9.1 - 2024-09-10 Revert Release 2024.9.0 2024.9.0 - 2024-09-10 TUN-8621: Fix cloudflared version in change notes. - 2024-09-06 PPIP-2310: Update quick tunnel disclaimer - 2024-08-30 TUN-8621: Prevent QUIC connection from closing before grace period after unregistering - 2024-08-09 TUN-8592: Use metadata from the edge to determine if request body is empty for QUIC transport - 2024-06-26 TUN-8484: Print response when QuickTunnel can't be unmarshalled 2024.8.3 - 2024-08-15 TUN-8591 login command without extra text - 2024-03-25 remove code that will not be executed - 2024-03-25 remove code that will not be executed 2024.8.2 - 2024-08-05 TUN-8583: change final directory of artifacts - 2024-08-05 TUN-8585: Avoid creating GH client when dry-run is true 2024.7.3 - 2024-07-31 TUN-8546: Fix final artifacts paths 2024.7.2 - 2024-07-17 TUN-8546: rework MacOS build script 2024.7.1 - 2024-07-16 TUN-8543: use -p flag to create intermediate directories 2024.7.0 - 2024-07-05 TUN-8520: add macos arm64 build - 2024-07-05 TUN-8523: refactor makefile and cfsetup - 2024-07-02 TUN-8504: Use pre-installed python version instead of downloading it on Windows builds - 2024-06-26 TUN-8489: Add default noop logger for capnprpc - 2024-06-25 TUN-8487: Add user-agent for quick-tunnel requests - 2023-12-12 TUN-8057: cloudflared uses new PQ curve ID 2024.6.1 - 2024-06-12 TUN-8461: Don't log Failed to send session payload if the error is EOF - 2024-06-07 TUN-8456: Update quic-go to 0.45 and collect mtu and congestion control metrics - 2024-06-06 TUN-8452: Add flag to control QUIC stream-level flow control limit - 2024-06-06 TUN-8451: Log QUIC flow control frames and transport parameters received - 2024-06-05 TUN-8449: Add flag to control QUIC connection-level flow control limit and increase default to 30MB 2024.6.0 - 2024-05-30 TUN-8441: Correct UDP total sessions metric to a counter and add new ICMP metrics - 2024-05-28 TUN-8422: Add metrics for capnp method calls - 2024-05-24 TUN-8424: Refactor capnp registration server - 2024-05-23 TUN-8427: Fix BackoffHandler's internally shared clock structure - 2024-05-21 TUN-8425: Remove ICMP binding for quick tunnels - 2024-05-20 TUN-8423: Deprecate older legacy tunnel capnp interfaces - 2024-05-15 TUN-8419: Add capnp safe transport - 2024-05-13 TUN-8415: Refactor capnp rpc into a single module 2024.5.0 - 2024-05-07 TUN-8407: Upgrade go to version 1.22.2 2024.4.1 - 2024-04-22 TUN-8380: Add sleep before requesting quick tunnel as temporary fix for component tests - 2024-04-19 TUN-8374: Close UDP socket if registration fails - 2024-04-18 TUN-8371: Bump quic-go to v0.42.0 - 2024-04-03 TUN-8333: Bump go-jose dependency to v4 - 2024-04-02 TUN-8331: Add unit testing for AccessJWTValidator middleware 2024.4.0 - 2024-04-02 feat: provide short version (#1206) - 2024-04-02 Format code - 2024-01-18 feat: auto tls sni - 2023-12-24 fix checkInPingGroup bugs - 2023-12-15 Add environment variables for TCP tunnel hostname / destination / URL. 2024.3.0 - 2024-03-14 TUN-8281: Run cloudflared query list tunnels/routes endpoint in a paginated way - 2024-03-13 TUN-8297: Improve write timeout logging on safe_stream.go - 2024-03-07 TUN-8290: Remove `|| true` from postrm.sh - 2024-03-05 TUN-8275: Skip write timeout log on "no network activity" - 2024-01-23 Update postrm.sh to fix incomplete uninstall - 2024-01-05 fix typo in errcheck for response parsing logic in CreateTunnel routine - 2023-12-23 Update linux_service.go - 2023-12-07 ci: bump actions/checkout to v4 - 2023-12-07 ci/check: bump actions/setup-go to v5 - 2023-04-28 check.yaml: bump actions/setup-go to v4 2024.2.1 - 2024-02-20 TUN-8242: Update Changes.md file with new remote diagnostics behaviour - 2024-02-19 TUN-8238: Fix type mismatch introduced by fast-forward - 2024-02-16 TUN-8243: Collect metrics on the number of QUIC frames sent/received - 2024-02-15 TUN-8238: Refactor proxy logging - 2024-02-14 TUN-8242: Enable remote diagnostics by default - 2024-02-12 TUN-8236: Add write timeout to quic and tcp connections - 2024-02-09 TUN-8224: Fix safety of TCP stream logging, separate connect and ack log messages 2024.2.0 - 2024-02-07 TUN-8224: Count and collect metrics on stream connect successes/errors 2024.1.5 - 2024-01-22 TUN-8176: Support ARM platforms that don't have an FPU or have it enabled in kernel - 2024-01-15 TUN-8158: Bring back commit e6537418859afcac29e56a39daa08bcabc09e048 and fixes infinite loop on linux when the socket is closed 2024.1.4 - 2024-01-19 Revert "TUN-8158: Add logging to confirm when ICMP reply is returned to the edge" 2024.1.3 - 2024-01-15 TUN-8161: Fix broken ARM build for armv6 - 2024-01-15 TUN-8158: Add logging to confirm when ICMP reply is returned to the edge 2024.1.2 - 2024-01-11 TUN-8147: Disable ECN usage due to bugs in detecting if supported - 2024-01-11 TUN-8146: Fix export path for install-go command - 2024-01-11 TUN-8146: Fix Makefile targets should not be run in parallel and install-go script was missing shebang - 2024-01-10 TUN-8140: Remove homebrew scripts 2024.1.1 - 2024-01-10 TUN-8134: Revert installed prefix to /usr - 2024-01-09 TUN-8130: Fix path to install go for mac build - 2024-01-09 TUN-8129: Use the same build command between branch and release builds - 2024-01-09 TUN-8130: Install go tool chain in /tmp on build agents - 2024-01-09 TUN-8134: Install cloudflare go as part of make install - 2024-01-08 TUN-8118: Disable FIPS module to build with go-boring without CGO_ENABLED 2024.1.0 - 2024-01-01 TUN-7934: Update quic-go to a version that queues datagrams for better throughput and drops large datagram - 2023-12-20 TUN-8072: Need to set GOCACHE in mac go installation script - 2023-12-17 TUN-8072: Add script to download cloudflare go for Mac build agents - 2023-12-15 Fix nil pointer dereference segfault when passing "null" config json to cloudflared tunnel ingress validate (#1070) - 2023-12-15 configuration.go: fix developerPortal link (#960) - 2023-12-14 tunnelrpc/pogs: fix dropped test errors (#1106) - 2023-12-14 cmd/cloudflared/updater: fix dropped error (#1055) - 2023-12-14 use os.Executable to discover the path to cloudflared (#1040) - 2023-12-14 Remove extraneous `period` from Path Environment Variable (#1009) - 2023-12-14 Use CLI context when running tunnel (#597) - 2023-12-14 TUN-8066: Define scripts to build on Windows agents - 2023-12-11 TUN-8052: Update go to 1.21.5 - 2023-12-07 TUN-7970: Default to enable post quantum encryption for quic transport - 2023-12-04 TUN-8006: Update quic-go to latest upstream - 2023-11-15 VULN-44842 Add a flag that allows users to not send the Access JWT to stdout - 2023-11-13 TUN-7965: Remove legacy incident status page check - 2023-11-13 AUTH-5682 Org token flow in Access logins should pass CF_AppSession cookie 2023.10.0 - 2023-10-06 TUN-7864: Document cloudflared versions support - 2023-10-03 CUSTESC-33731: Make rule match test report rule in 0-index base - 2023-09-22 TUN-7824: Fix usage of systemctl status to detect which services are installed - 2023-09-20 TUN-7813: Improve tunnel delete command to use cascade delete - 2023-09-20 TUN-7787: cloudflared only list ip routes targeted for cfd_tunnel - 2023-09-15 TUN-7787: Refactor cloudflared to use new route endpoints based on route IDs - 2023-09-08 TUN-7776: Remove warp-routing flag from cloudflared - 2023-09-05 TUN-7756: Clarify that QUIC is mandatory to support ICMP proxying 2023.8.2 - 2023-08-25 TUN-7700: Implement feature selector to determine if connections will prefer post quantum cryptography - 2023-08-22 TUN-7707: Use X25519Kyber768Draft00 curve when post-quantum feature is enabled 2023.8.1 - 2023-08-23 TUN-7718: Update R2 Token to no longer encode secret 2023.8.0 - 2023-07-26 TUN-7584: Bump go 1.20.6 2023.7.3 - 2023-07-25 TUN-7628: Correct Host parsing for Access - 2023-07-24 TUN-7624: Fix flaky TestBackoffGracePeriod test in cloudflared 2023.7.2 - 2023-07-19 TUN-7599: Onboard cloudflared to Software Dashboard - 2023-07-19 TUN-7587: Remove junos builds - 2023-07-18 TUN-7597: Add flag to disable auto-update services to be installed - 2023-07-17 TUN-7594: Add nightly arm64 cloudflared internal deb publishes - 2023-07-14 TUN-7586: Upgrade go-jose/go-jose/v3 and core-os/go-oidc/v3 - 2023-07-14 TUN-7589: Remove legacy golang.org/x/crypto/ssh/terminal package usage - 2023-07-14 TUN-7590: Remove usages of ioutil - 2023-07-14 TUN-7585: Remove h2mux compression - 2023-07-14 TUN-7588: Update package coreos/go-systemd 2023.7.1 - 2023-07-13 TUN-7582: Correct changelog wording for --management-diagnostics - 2023-07-12 TUN-7575: Add option to disable PTMU discovery over QUIC 2023.7.0 - 2023-07-06 TUN-7558: Flush on Writes for StreamBasedOriginProxy - 2023-07-05 TUN-7553: Add flag to enable management diagnostic services - 2023-07-05 TUN-7564: Support cf-trace-id for cloudflared access - 2023-07-05 TUN-7477: Decrement UDP sessions on shutdown - 2023-07-03 TUN-7545: Add support for full bidirectionally streaming with close signal propagation - 2023-06-30 TUN-7549: Add metrics route to management service - 2023-06-30 TUN-7551: Complete removal of raven-go to sentry-go - 2023-06-30 TUN-7550: Add pprof endpoint to management service - 2023-06-29 TUN-7543: Add --debug-stream flag to cloudflared access ssh - 2023-06-26 TUN-6011: Remove docker networks from ICMP Proxy test - 2023-06-20 AUTH-5328 Pass cloudflared_token_check param when running cloudflared access login 2023.6.1 - 2023-06-19 TUN-7480: Added a timeout for unregisterUDP. - 2023-06-16 TUN-7477: Add UDP/TCP session metrics - 2023-06-14 TUN-7468: Increase the limit of incoming streams 2023.6.0 - 2023-06-15 TUN-7471: Fixes cloudflared not closing the quic stream on unregister UDP session - 2023-06-09 TUN-7463: Add default ingress rule if no ingress rules are provided when updating the configuration - 2023-05-31 TUN-7447: Add a cover build to report code coverage 2023.5.1 - 2023-05-16 TUN-7424: Add CORS headers to host_details responses - 2023-05-11 TUN-7421: Add *.cloudflare.com to permitted Origins for management WebSocket requests - 2023-05-05 TUN-7404: Default configuration version set to -1 - 2023-05-05 TUN-7227: Migrate to devincarr/quic-go 2023.5.0 - 2023-04-27 TUN-7398: Add support for quic safe stream to set deadline - 2023-04-26 TUN-7394: Retry StartFirstTunnel on quic.ApplicationErrors - 2023-04-26 TUN-7392: Ignore release checksum upload if asset already uploaded - 2023-04-25 TUN-7392: Ignore duplicate artifact uploads for github release - 2023-04-25 TUN-7393: Add json output for cloudflared tail - 2023-04-24 TUN-7390: Remove Debian stretch builds 2023.4.2 - 2023-04-24 TUN-7133: Add sampling support for streaming logs - 2023-04-21 TUN-7141: Add component tests for streaming logs - 2023-04-21 TUN-7373: Streaming logs override for same actor - 2023-04-20 TUN-7383: Bump requirements.txt - 2023-04-19 TUN-7361: Add a label to override hostname - 2023-04-19 TUN-7378: Remove RPC debug logs - 2023-04-18 TUN-7360: Add Get Host Details handler in management service - 2023-04-17 AUTH-3122 Verify that Access tokens are still valid in curl command - 2023-04-17 TUN-7129: Categorize TCP logs for streaming logs - 2023-04-17 TUN-7130: Categorize UDP logs for streaming logs - 2023-04-10 AUTH-4887 Add aud parameter to token transfer url 2023.4.1 - 2023-04-13 TUN-7368: Report destination address for TCP requests in logs - 2023-04-12 TUN-7134: Acquire token for cloudflared tail - 2023-04-12 TUN-7131: Add cloudflared log event to connection messages and enable streaming logs - 2023-04-11 TUN-7132 TUN-7136: Add filter support for streaming logs - 2023-04-06 TUN-7354: Don't warn for empty ingress rules when using --token - 2023-04-06 TUN-7128: Categorize logs from public hostname locations - 2023-04-06 TUN-7351: Add streaming logs session ping and timeout - 2023-04-06 TUN-7335: Fix cloudflared update not working in windows 2023.4.0 - 2023-04-07 TUN-7356: Bump golang.org/x/net package to 0.7.0 - 2023-04-07 TUN-7357: Bump to go 1.19.6 - 2023-04-06 TUN-7127: Disconnect logger level requirement for management - 2023-04-05 TUN-7332: Remove legacy tunnel force flag - 2023-04-05 TUN-7135: Add cloudflared tail - 2023-04-04 Add suport for OpenBSD (#916) - 2023-04-04 Fix typo (#918) - 2023-04-04 TUN-7125: Add management streaming logs WebSocket protocol - 2023-03-30 TUN-9999: Remove classic tunnel component tests - 2023-03-30 TUN-7126: Add Management logger io.Writer - 2023-03-29 TUN-7324: Add http.Hijacker to connection.ResponseWriter - 2023-03-29 TUN-7333: Default features checkable at runtime across all packages - 2023-03-21 TUN-7124: Add intercept ingress rule for management requests 2023.3.1 - 2023-03-13 TUN-7271: Return 503 status code when no ingress rules configured - 2023-03-10 TUN-7272: Fix cloudflared returning non supported status service which breaks configuration migration - 2023-03-09 TUN-7259: Add warning for missing ingress rules - 2023-03-09 TUN-7268: Default to Program Files as location for win32 - 2023-03-07 TUN-7252: Remove h2mux connection - 2023-03-07 TUN-7253: Adopt http.ResponseWriter for connection.ResponseWriter - 2023-03-06 TUN-7245: Add bastion flag to origin service check - 2023-03-06 EDGESTORE-108: Remove deprecated s3v2 signature - 2023-03-02 TUN-7226: Fixed a missed rename 2023.3.0 - 2023-03-01 GH-352: Add Tunnel CLI option "edge-bind-address" (#870) - 2023-03-01 Fixed WIX template to allow MSI upgrades (#838) - 2023-02-28 TUN-7213: Decode Base64 encoded key before writing it - 2023-02-28 check.yaml: update actions to v3 (#876) - 2023-02-27 TUN-7213: Debug homebrew-cloudflare build - 2023-02-15 RTG-2476 Add qtls override for Go 1.20 2023.2.2 - 2023-02-22 TUN-7197: Add connIndex tag to debug messages of incoming requests - 2023-02-08 TUN-7167: Respect protocol overrides with --token - 2023-02-06 TUN-7065: Remove classic tunnel creation - 2023-02-06 TUN-6938: Force h2mux protocol to http2 for named tunnels - 2023-02-06 TUN-6938: Provide QUIC as first in protocol list - 2023-02-03 TUN-7158: Correct TCP tracing propagation - 2023-02-01 TUN-7151: Update changes file with latest release notices 2023.2.1 - 2023-02-01 TUN-7065: Revert Ingress Rule check for named tunnel configurations - 2023-02-01 Revert "TUN-7065: Revert Ingress Rule check for named tunnel configurations" - 2023-02-01 Revert "TUN-7065: Remove classic tunnel creation" 2023.1.0 - 2023-01-10 TUN-7064: RPM digests are now sha256 instead of md5sum - 2023-01-04 RTG-2418 Update qtls - 2022-12-24 TUN-7057: Remove dependency github.com/gorilla/mux - 2022-12-24 TUN-6724: Migrate to sentry-go from raven-go 2022.12.1 - 2022-12-20 TUN-7021: Fix proxy-dns not starting when cloudflared tunnel is run - 2022-12-15 TUN-7010: Changelog for release 2022.12.0 2022.12.0 - 2022-12-14 TUN-6999: cloudflared should attempt other edge addresses before falling back on protocol - 2022-12-13 TUN-7004: Dont show local config dirs for remotely configured tuns - 2022-12-12 TUN-7003: Tempoarily disable erroneous notarize-app - 2022-12-12 TUN-7003: Add back a missing fi - 2022-12-07 TUN-7000: Reduce metric cardinality of closedConnections metric by removing error as tag - 2022-12-07 TUN-6994: Improve logging config file not found - 2022-12-07 TUN-7002: Randomise first region selection - 2022-12-07 TUN-6995: Disable quick-tunnels spin up by default - 2022-12-05 TUN-6984: Add bash set x to improve visibility during builds - 2022-12-05 TUN-6984: [CI] Ignore security import errors for code_sigining - 2022-12-05 TUN-6984: [CI] Don't fail on unset. - 2022-11-30 TUN-6984: Set euo pipefile for homebrew builds 2022.11.1 - 2022-11-29 TUN-6981: We should close UDP socket if failed to connecto to edge - 2022-11-25 CUSTESC-23757: Fix a bug where a wildcard ingress rule would match an host without starting with a dot - 2022-11-24 TUN-6970: Print newline when printing tunnel token - 2022-11-22 TUN-6963: Refactor Metrics service setup 2022.11.0 - 2022-11-16 Revert "TUN-6935: Cloudflared should use APIToken instead of serviceKey" - 2022-11-16 TUN-6929: Use same protocol for other connections as first one - 2022-11-14 TUN-6941: Reduce log level to debug when failing to proxy ICMP reply - 2022-11-14 TUN-6935: Cloudflared should use APIToken instead of serviceKey - 2022-11-14 TUN-6935: Cloudflared should use APIToken instead of serviceKey - 2022-11-11 TUN-6937: Bump golang.org/x/* packages to new release tags - 2022-11-10 ZTC-234: macOS tests - 2022-11-09 TUN-6927: Refactor validate access configuration to allow empty audTags only - 2022-11-08 ZTC-234: Replace ICMP funnels when ingress connection changes - 2022-11-04 TUN-6917: Bump go to 1.19.3 - 2022-11-02 Issue #574: Better ssh config for short-lived cert (#763) - 2022-10-28 TUN-6898: Fix bug handling IPv6 based ingresses with missing port - 2022-10-28 TUN-6898: Refactor addPortIfMissing 2022.10.3 - 2022-10-24 TUN-6871: Add default feature to cloudflared to support EOF on QUIC connections - 2022-10-19 TUN-6876: Fix flaky TestTraceICMPRouterEcho by taking account request span can return before reply - 2022-10-18 TUN-6867: Clear spans right after they are serialized to avoid returning duplicate spans 2022.10.2 - 2022-10-18 TUN-6869: Fix Makefile complaining about missing GO packages - 2022-10-18 TUN-6864: Don't reuse port in quic unit tests - 2022-10-18 TUN-6868: Return left padded tracing ID when tracing identity is converted to string 2022.10.1 - 2022-10-16 TUN-6861: Trace ICMP on Windows - 2022-10-15 TUN-6860: Send access configuration keys to the edge - 2022-10-14 TUN-6858: Trace ICMP reply - 2022-10-13 TUN-6855: Add DatagramV2Type for IP packet with trace and tracing spans - 2022-10-13 TUN-6856: Refactor to lay foundation for tracing ICMP - 2022-10-13 TUN-6604: Trace icmp echo request on Linux and Darwin - 2022-10-12 Fix log message (#591) - 2022-10-12 TUN-6853: Reuse source port when connecting to the edge for quic connections - 2022-10-11 TUN-6829: Allow user of datagramsession to control logging level of errors - 2022-10-10 RTG-2276 Update qtls and go mod tidy - 2022-10-05 Add post-quantum flag to quick tunnel - 2022-10-05 TUN-6823: Update github release message to pull from KV - 2022-10-04 TUN-6825: Fix cloudflared:version images require arch hyphens - 2022-10-03 TUN-6806: Add ingress rule number to log when filtering due to middlware handler - 2022-08-17 Label correct container - 2022-08-16 Fix typo in help text for `cloudflared tunnel route lb` - 2022-07-18 drop usage of cat when sed is invoked to generate the manpage - 2021-03-15 update-build-readme - 2021-03-15 fix link 2022.10.0 - 2022-09-30 TUN-6755: Remove unused publish functions - 2022-09-30 TUN-6813: Only proxy ICMP packets when warp-routing is enabled - 2022-09-29 TUN-6811: Ping group range should be parsed as int32 - 2022-09-29 TUN-6812: Drop IP packets if ICMP proxy is not initialized - 2022-09-28 TUN-6716: Document limitation of Windows ICMP proxy - 2022-09-28 TUN-6810: Add component test for post-quantum - 2022-09-27 TUN-6715: Provide suggestion to add cloudflared to ping_group_range if it failed to open ICMP socket - 2022-09-22 TUN-6792: Fix brew core release by not auditing the formula - 2022-09-22 TUN-6774: Validate OriginRequest.Access to add Ingress.Middleware - 2022-09-22 TUN-6775: Add middleware.Handler verification to ProxyHTTP - 2022-09-22 TUN-6791: Calculate ICMPv6 checksum - 2022-09-22 TUN-6801: Add punycode alternatives for ingress rules - 2022-09-21 TUN-6772: Add a JWT Validator as an ingress verifier - 2022-09-21 TUN-6772: Add a JWT Validator as an ingress verifier - 2022-09-21 TUN-6774: Validate OriginRequest.Access to add Ingress.Middleware - 2022-09-21 TUN-6772: Add a JWT Validator as an ingress verifier - 2022-09-20 TUN-6741: ICMP proxy tries to listen on specific IPv4 & IPv6 when possible 2022.9.1 - 2022-09-20 TUN-6777: Fix race condition in TestFunnelIdleTimeout - 2022-09-20 TUN-6595: Enable datagramv2 and icmp proxy by default - 2022-09-20 TUN-6773: Add access based configuration to ingress.OriginRequestConfig - 2022-09-19 TUN-6778: Cleanup logs about ICMP - 2022-09-19 TUN-6779: cloudflared should also use the root CAs from system pool to validate edge certificate - 2022-09-19 TUN-6780: Add support for certReload to also include support for client certificates - 2022-09-16 TUN-6767: Build ICMP proxy for Windows only when CGO is enabled - 2022-09-15 TUN-6590: Use Windows Teamcity agent to build binary - 2022-09-13 TUN-6592: Decrement TTL and return ICMP time exceed if it's 0 - 2022-09-09 TUN-6749: Fix icmp_generic build - 2022-09-09 TUN-6744: On posix platforms, assign unique echo ID per (src, dst, echo ID) - 2022-09-08 TUN-6743: Support ICMPv6 echo on Windows - 2022-09-08 TUN-6689: Utilize new RegisterUDPSession to begin tracing - 2022-09-07 TUN-6688: Update RegisterUdpSession capnproto to include trace context - 2022-09-06 TUN-6740: Detect no UDP packets allowed and fallback from QUIC in that case - 2022-09-06 TUN-6654: Support ICMPv6 on Linux and Darwin - 2022-09-02 TUN-6696: Refactor flow into funnel and close idle funnels - 2022-09-02 TUN-6718: Bump go and go-boring 1.18.6 - 2022-08-29 TUN-6531: Implement ICMP proxy for Windows using IcmpSendEcho - 2022-08-24 RTG-1339 Support post-quantum hybrid key exchange 2022.9.0 - 2022-09-05 TUN-6737: Fix datagramV2Type should be declared in its own block so it starts at 0 - 2022-09-01 TUN-6725: Fix testProxySSEAllData - 2022-09-01 TUN-6726: Fix maxDatagramPayloadSize for Windows QUIC datagrams - 2022-09-01 TUN-6729: Fix flaky TestClosePreviousProxies - 2022-09-01 TUN-6728: Verify http status code ingress rule - 2022-08-25 TUN-6695: Implement ICMP proxy for linux 2022.8.4 - 2022-08-31 TUN-6717: Update Github action to run with Go 1.19 - 2022-08-31 TUN-6720: Remove forcibly closing connection during reconnect signal - 2022-08-29 Release 2022.8.3 2022.8.3 - 2022-08-26 TUN-6708: Fix replace flow logic - 2022-08-25 TUN-6705: Tunnel should retry connections forever - 2022-08-25 TUN-6704: Honor protocol flag when edge discovery is unreachable - 2022-08-25 TUN-6699: Add metric for packet too big dropped - 2022-08-24 TUN-6691: Properly error check for net.ErrClosed - 2022-08-22 TUN-6679: Allow client side of quic request to close body - 2022-08-22 TUN-6586: Change ICMP proxy to only build for Darwin and use echo ID to track flows - 2022-08-18 TUN-6530: Implement ICMPv4 proxy - 2022-08-17 TUN-6666: Define packet package - 2022-08-17 TUN-6667: DatagramMuxerV2 provides a method to receive RawPacket - 2022-08-16 TUN-6657: Ask for Tunnel ID and Configuration on Bug Report - 2022-08-16 TUN-6676: Add suport for trailers in http2 connections - 2022-08-11 TUN-6575: Consume cf-trace-id from incoming http2 TCP requests 2022.8.2 - 2022-08-16 TUN-6656: Docker for arm64 should not be deployed in an amd64 container 2022.8.1 - 2022-08-15 TUN-6617: Updated CHANGES.md for protocol stickiness - 2022-08-12 EDGEPLAT-3918: bump go and go-boring to 1.18.5 - 2022-08-12 TUN-6652: Publish dockerfile for both amd64 and arm64 - 2022-08-11 TUN-6617: Dont fallback to http2 if QUIC conn was successful. - 2022-08-11 TUN-6617: Dont fallback to http2 if QUIC conn was successful. - 2022-08-11 Revert "TUN-6617: Dont fallback to http2 if QUIC conn was successful." - 2022-08-11 TUN-6617: Dont fallback to http2 if QUIC conn was successful. - 2022-08-01 TUN-6584: Define QUIC datagram v2 format to support proxying IP packets 2022.8.0 - 2022-08-10 TUN-6637: Upgrade quic-go - 2022-08-10 TUN-6646: Add support to SafeStreamCloser to close only write side of stream - 2022-08-09 TUN-6642: Fix unexpected close of quic stream triggered by upstream origin close - 2022-08-09 TUN-6639: Validate cyclic ingress configuration - 2022-08-08 TUN-6637: Upgrade go version and quic-go - 2022-08-08 TUN-6639: Validate cyclic ingress configuration - 2022-08-04 EDGEPLAT-3918: build cloudflared for Bookworm - 2022-08-02 Revert "TUN-6576: Consume cf-trace-id from incoming TCP requests to create root span" - 2022-07-27 TUN-6601: Update gopkg.in/yaml.v3 references in modules - 2022-07-26 TUN-6576: Consume cf-trace-id from incoming TCP requests to create root span - 2022-07-26 TUN-6576: Consume cf-trace-id from incoming TCP requests to create root span - 2022-07-25 TUN-6598: Remove auto assignees on github issues - 2022-07-20 TUN-6583: Remove legacy --ui flag - 2022-07-20 cURL supports stdin and uses os pipes directly without copying - 2022-07-07 TUN-6517: Use QUIC stream context while proxying HTTP requests and TCP connections 2022.7.1 - 2022-07-06 TUN-6503: Fix transport fallback from QUIC in face of dial error "no network activity" 2022.7.0 - 2022-07-05 TUN-6499: Remove log that is per datagram - 2022-06-24 TUN-6460: Rename metric label location to edge_location - 2022-06-24 TUN-6459: Add cloudflared user-agent to access calls - 2022-06-17 TUN-6427: Differentiate between upstream request closed/canceled and failed origin requests - 2022-06-17 TUN-6388: Fix first tunnel connection not retrying - 2022-06-13 TUN-6384: Correct duplicate connection error to fetch new IP first - 2022-06-13 TUN-6373: Add edge-ip-version to remotely pushed configuration - 2022-06-07 TUN-6010: Add component tests for --edge-ip-version - 2022-05-20 TUN-6007: Implement new edge discovery algorithm - 2022-02-18 Ensure service install directories are created before writing file 2022.6.3 - 2022-06-20 TUN-6362: Add armhf support to cloudflare packaging 2022.6.2 - 2022-06-13 TUN-6381: Write error data on QUIC stream when we fail to talk to the origin; separate logging for protocol errors vs. origin errors. - 2022-06-17 TUN-6414: Remove go-sumtype from cloudflared build process - 2022-06-01 Add Http2Origin option to force HTTP/2 origin connections - 2022-06-02 fix ingress rules unit test - 2022-06-09 Update remaining OriginRequestConfig functions for Http2Origins - 2022-05-31 Add image source label to docker container. - 2022-05-10 Warp Private Network link updated 2022.6.1 - 2022-06-14 TUN-6395: Fix writing RPM repo data 2022.6.0 - 2022-06-14 Revert "TUN-6010: Add component tests for --edge-ip-version" - 2022-06-14 Revert "TUN-6373: Add edge-ip-version to remotely pushed configuration" - 2022-06-14 Revert "TUN-6384: Correct duplicate connection error to fetch new IP first" - 2022-06-14 Revert "TUN-6007: Implement new edge discovery algorithm" - 2022-06-13 TUN-6385: Don't share err between acceptStream loop and per-stream goroutines - 2022-06-13 TUN-6384: Correct duplicate connection error to fetch new IP first - 2022-06-13 TUN-6373: Add edge-ip-version to remotely pushed configuration - 2022-06-13 TUN-6380: Enforce connect and keep-alive timeouts for TCP connections in both WARP routing and websocket based TCP proxy. - 2022-06-11 Update issue templates - 2022-06-11 Amendment to previous PR - 2022-06-09 TUN-6347: Add TCP stream logs with FlowID - 2022-06-08 TUN-6361: Add cloudflared arm builds to pkging as well - 2022-06-07 TUN-6357: Add connector id to ready check endpoint - 2022-06-07 TUN-6010: Add component tests for --edge-ip-version - 2022-06-06 TUN-6191: Update quic-go to v0.27.1 and with custom patch to allow keep alive period to be configurable - 2022-06-03 TUN-6343: Fix QUIC->HTTP2 fallback - 2022-06-02 TUN-6339: Add config for IPv6 support - 2022-06-02 TUN-6341: Fix default config value for edge-ip-version - 2022-06-01 TUN-6323: Add Xenial and Trusty for Ubuntu pkging - 2022-05-31 TUN-6210: Add cloudflared.repo to make it easy for yum installs - 2022-05-30 TUN-6293: Update yaml v3 to latest hotfix - 2022-05-20 TUN-6007: Implement new edge discovery algorithm 2022.5.3 - 2022-05-30 TUN-6308: Add debug logs to see if packets are sent/received from edge - 2022-05-30 TUN-6301: Allow to update logger used by UDP session manager 2022.5.2 - 2022-05-23 TUN-6270: Import gpg keys from environment variables - 2022-05-24 TUN-6209: Improve feedback process if release_pkgs to deb and rpm fail - 2022-05-24 TUN-6280: Don't wrap qlog connection tracer for gatethering QUIC metrics since we're not writing qlog files. - 2022-05-25 TUN-6209: Sign RPM packages - 2022-05-25 TUN-6285: Upload pkg assets to repos when cloudflared is released. - 2022-05-24 TUN-6282: Upgrade golang to 1.17.10, go-boring to 1.17.9 - 2022-05-26 TUN-6292: Debug builds for cloudflared - 2022-05-28 TUN-6304: Fixed some file permission issues - 2022-05-11 TUN-6197: Publish to brew core should not try to open the browser - 2022-05-12 TUN-5943: Add RPM support - 2022-05-18 TUN-6248: Fix panic in cloudflared during tracing when origin doesn't provide header map - 2022-05-18 TUN-6250: Add upstream response status code to tracing span attributes 2022.5.1 - 2022-05-06 TUN-6146: Release_pkgs is now a generic command line script - 2022-05-06 TUN-6185: Fix tcpOverWSOriginService not using original scheme for String representation - 2022-05-05 TUN-6175: Simply debian packaging by structural upload - 2022-05-05 TUN-5945: Added support for Ubuntu releases - 2022-05-04 TUN-6054: Create and upload deb packages to R2 - 2022-05-03 TUN-6161: Set git user/email for brew core release - 2022-05-03 TUN-6166: Fix mocked QUIC transport for UDP proxy manager to return expected error - 2022-04-27 TUN-6016: Push local managed tunnels configuration to the edge 2022.5.0 - 2022-05-02 TUN-6158: Update golang.org/x/crypto - 2022-04-20 VULN-8383 Bump yaml.v2 to yaml.v3 - 2022-04-21 TUN-6123: For a given connection with edge, close all datagram sessions through this connection when it's closed - 2022-04-20 TUN-6015: Add RPC method for pushing local config - 2022-04-21 TUN-6130: Fix vendoring due to case sensitive typo in package - 2022-04-27 TUN-6142: Add tunnel details support to RPC - 2022-04-28 TUN-6014: Add remote config flag as default feature - 2022-04-12 TUN-6000: Another fix for publishing to brew core - 2022-04-11 TUN-5990: Add otlp span export to response header - 2022-04-19 TUN-6070: First connection retries other edge IPs if the error is quic timeout(likely due to firewall blocking UDP) - 2022-04-11 TUN-6030: Add ttfb span for origin http request 2022.4.1 - 2022-04-11 TUN-6035: Reduce buffer size when proxying data - 2022-04-11 TUN-6038: Reduce buffer size used for proxying data - 2022-04-11 TUN-6043: Allow UI-managed Tunnels to fallback from QUIC but warn about that - 2022-04-07 TUN-6000 add version argument to bump-formula-pr - 2022-04-06 TUN-5989: Add in-memory otlp exporter 2022.4.0 - 2022-04-01 TUN-5973: Add backoff for non-recoverable errors as well - 2022-04-05 TUN-5992: Use QUIC protocol for remotely managed tunnels when protocol is unspecified - 2022-04-06 Update Makefile - 2022-04-06 TUN-5995: Update prometheus to 1.12.1 to avoid vulnerabilities - 2022-04-07 TUN-5995: Force prometheus v1.12.1 usage - 2022-04-07 TUN-4130: cloudflared docker images now have a latest tag - 2022-03-30 TUN-5842: Fix flaky TestConcurrentUpdateAndRead by making sure resources are released - 2022-03-30 carrier: fix dropped errors - 2022-03-25 TUN-5959: tidy go.mod - 2022-03-25 TUN-5958: Fix release to homebrew core - 2022-03-28 TUN-5960: Do not log the tunnel token or json credentials - 2022-03-28 TUN-5956: Add timeout to session manager APIs 2022.3.4 - 2022-03-22 TUN-5918: Clean up text in cloudflared tunnel --help - 2022-03-22 TUN-5895 run brew bump-formula-pr on release - 2022-03-22 TUN-5915: New cloudflared command to allow to retrieve the token credentials for a Tunnel - 2022-03-24 TUN-5933: Better messaging to help user when installing service if it is already installed - 2022-03-25 TUN-5954: Start cloudflared service in Linux too similarly to other OSs - 2022-03-14 TUN-5869: Add configuration endpoint in metrics server 2022.3.3 - 2022-03-17 TUN-5893: Start windows service on install, stop on uninstall. Previously user had to manually start the service after running 'cloudflared tunnel install' and stop the service before running uninstall command. - 2022-03-17 Revert "CC-796: Remove dependency on unsupported version of go-oidc" - 2022-03-18 TUN-5881: Clarify success (or lack thereof) of (un)installing cloudflared service - 2022-03-18 CC-796: Remove dependency on unsupported version of go-oidc - 2022-03-18 TUN-5907: Change notes for 2022.3.3 2022.3.2 - 2022-03-10 TUN-5833: Create constant for allow-remote-config - 2022-03-15 TUN-5867: Return error if service was already installed - 2022-03-16 TUN-5833: Send feature `allow_remote_config` if Tunnel is run with --token - 2022-03-08 TUN-5849: Remove configuration debug log - 2022-03-08 TUN-5850: Update CHANGES.md with latest releases - 2022-03-08 TUN-5851: Update all references to point to Apache License 2.0 - 2022-03-07 TUN-5853 Add "install" make target and build package manager info into executable - 2022-03-08 TUN-5801: Add custom wrapper for OriginConfig for JSON serde - 2022-03-09 TUN-5703: Add prometheus metric for current configuration version - 2022-02-05 CC-796: Remove dependency on unsupported version of go-oidc 2022.3.1 - 2022-03-04 TUN-5837: Log panic recovery in http2 logic with debug level log - 2022-03-04 TUN-5696: HTTP/2 Configuration Update - 2022-03-04 TUN-5836: Avoid websocket#Stream function from crashing cloudflared with unexpected memory access - 2022-03-05 TUN-5836: QUIC transport no longer sets body to nil in any condition 2022.3.0 - 2022-03-02 TUN-5680: Adapt component tests for new service install based on token - 2022-02-21 TUN-5682: Remove name field from credentials - 2022-02-21 TUN-5681: Add support for running tunnel using Token - 2022-02-28 TUN-5824: Update updater no-update-in-shell link - 2022-02-28 TUN-5823: Warn about legacy flags that are ignored when ingress rules are used - 2022-02-28 TUN-5737: Support https protocol over unix socket origin - 2022-02-23 TUN-5679: Add support for service install using Tunnel Token 2022.2.2 - 2022-02-22 TUN-5754: Allow ingress validate to take plaintext option - 2022-02-17 TUN-5678: Cloudflared uses typed tunnel API 2022.2.1 - 2022-02-10 TUN-5184: Handle errors in bidrectional streaming (websocket#Stream) gracefully when 1 side has ended - 2022-02-14 Update issue templates - 2022-02-14 Update issue templates - 2022-02-11 TUN-5768: Update cloudflared license file - 2022-02-11 TUN-5698: Make ingress rules and warp routing dynamically configurable - 2022-02-14 TUN-5678: Adapt cloudflared to use new typed APIs - 2022-02-17 Revert "TUN-5678: Adapt cloudflared to use new typed APIs" - 2022-02-11 TUN-5697: Listen for UpdateConfiguration RPC in quic transport - 2022-02-04 TUN-5744: Add a test to make sure cloudflared uses scheme defined in ingress rule, not X-Forwarded-Proto header - 2022-02-07 TUN-5749: Refactor cloudflared to pave way for reconfigurable ingress - Split origin into supervisor and proxy packages - Create configManager to handle dynamic config - 2021-10-19 TUN-5184: Make sure outstanding websocket write is finished, and no more writes after shutdown 2022.2.0 - 2022-02-02 TUN-4947: Use http when talking to Unix sockets origins - 2022-02-02 TUN-5695: Define RPC method to update configuration - 2022-01-27 TUN-5621: Correctly manage QUIC stream closing - 2022-01-28 TUN-5702: Allow to deserialize config from JSON 2022.1.3 - 2022-01-21 TUN-5477: Unhide vnet commands - 2022-01-24 TUN-5669: Change network command to vnet - 2022-01-25 TUN-5675: Remove github.com/dgrijalva/jwt-go dependency by upgrading coredns version - 2022-01-27 TUN-5719: Re-attempt connection to edge with QUIC despite network error when there is no fallback - 2022-01-28 TUN-5724: Fix SSE streaming by guaranteeing we write everything we read - 2022-01-17 TUN-5547: Bump golang x/net package to fix http2 transport bugs - 2022-01-19 TUN-5659: Proxy UDP with zero-byte payload - 2021-10-22 Add X-Forwarded-Host for http proxy 2022.1.2 - 2022-01-13 TUN-5650: Fix pynacl version to 1.4.0 and pygithub version to 1.55 so release doesn't break unexpectedly 2022.1.1 - 2022-01-10 TUN-5631: Build everything with go 1.17.5 - 2022-01-06 TUN-5623: Configure quic max datagram frame size to 1350 bytes for none Windows platforms 2022.1.0 - 2022-01-03 TUN-5612: Add support for specifying TLS min/max version - 2022-01-03 TUN-5612: Make tls min/max version public visible - 2022-01-03 TUN-5551: Internally published debian artifacts are now named just cloudflared even though they are FIPS compliant - 2022-01-04 TUN-5600: Close QUIC transports as soon as possible while respecting graceful shutdown - 2022-01-05 TUN-5616: Never fallback transport if user chooses it on purpose - 2022-01-05 TUN-5204: Unregister QUIC transports on disconnect - 2022-01-04 TUN-5600: Add coverage to component tests for various transports 2021.12.4 - 2021-12-27 TUN-5482: Refactor tunnelstore client related packages for more coherent package - 2021-12-27 TUN-5551: Change internally published debian package to be FIPS compliant - 2021-12-27 TUN-5551: Show whether the binary was built for FIPS compliance 2021.12.3 - 2021-12-22 TUN-5584: Changes for release 2021.12.2 - 2021-12-22 TUN-5590: QUIC datagram max user payload is 1217 bytes - 2021-12-22 TUN-5593: Read full packet from UDP connection, even if it exceeds MTU of the transport. When packet length is greater than the MTU of the transport, we will silently drop packets (for now). - 2021-12-23 TUN-5597: Log session ID when session is terminated by edge 2021.12.2 - 2021-12-20 TUN-5571: Remove redundant session manager log, it's already logged in origin/tunnel.ServeQUIC - 2021-12-20 TUN-5570: Only log RPC server events at error level to reduce noise - 2021-12-14 TUN-5494: Send a RPC with terminate reason to edge if the session is closed locally - 2021-11-09 TUN-5551: Reintroduce FIPS compliance for linux amd64 now as separate binaries 2021.12.1 - 2021-12-16 TUN-5549: Revert "TUN-5277: Ensure cloudflared binary is FIPS compliant on linux amd64" 2021.12.0 - 2021-12-13 TUN-5530: Get current time from ticker - 2021-12-15 TUN-5544: Update CHANGES.md for next release - 2021-12-07 TUN-5519: Adjust URL for virtual_networks endpoint to match what we will publish - 2021-12-02 TUN-5488: Close session after it's idle for a period defined by registerUdpSession RPC - 2021-12-09 TUN-5504: Fix upload of packages to public repo - 2021-11-30 TUN-5481: Create abstraction for Origin UDP Connection - 2021-11-30 TUN-5422: Define RPC to unregister session - 2021-11-26 TUN-5361: Commands for managing virtual networks - 2021-11-29 TUN-5362: Adjust route ip commands to be aware of virtual networks - 2021-11-23 TUN-5301: Separate datagram multiplex and session management logic from quic connection logic - 2021-11-10 TUN-5405: Update net package to v0.0.0-20211109214657-ef0fda0de508 - 2021-11-10 TUN-5408: Update quic package to v0.24.0 - 2021-11-12 Fix typos - 2021-11-13 Fix for Issue #501: Unexpected User-agent insertion when tunneling http request - 2021-11-16 TUN-5129: Remove `-dev` suffix when computing version and Git has uncommitted changes - 2021-11-18 TUN-5441: Fix message about available protocols - 2021-11-12 TUN-5300: Define RPC to register UDP sessions - 2021-11-14 TUN-5299: Send/receive QUIC datagram from edge and proxy to origin as UDP - 2021-11-04 TUN-5387: Updated CHANGES.md for 2021.11.0 - 2021-11-08 TUN-5368: Log connection issues with LogLevel that depends on tunnel state - 2021-11-09 TUN-5397: Log cloudflared output when it fails to connect tunnel - 2021-11-09 TUN-5277: Ensure cloudflared binary is FIPS compliant on linux amd64 - 2021-11-08 TUN-5393: Content-length is no longer a control header for non-h2mux transports 2021.11.0 - 2021-11-03 TUN-5285: Fallback to HTTP2 immediately if connection times out with no network activity - 2021-09-29 Add flag to 'tunnel create' subcommand to specify a base64-encoded secret 2021.10.5 - 2021-10-25 Update change log for release 2021.10.4 - 2021-10-25 Revert "TUN-5184: Make sure outstanding websocket write is finished, and no more writes after shutdown" 2021.10.4 - 2021-10-21 TUN-5287: Fix misuse of wait group in TestQUICServer that caused the test to exit immediately - 2021-10-21 TUN-5286: Upgrade crypto/ssh package to fix CVE-2020-29652 - 2021-10-18 TUN-5262: Allow to configure max fetch size for listing queries - 2021-10-19 TUN-5262: Improvements to `max-fetch-size` that allow to deal with large number of tunnels in account - 2021-10-15 TUN-5261: Collect QUIC metrics about RTT, packets and bytes transfered and log events at tracing level - 2021-10-19 TUN-5184: Make sure outstanding websocket write is finished, and no more writes after shutdown 2021.10.3 - 2021-10-14 TUN-5255: Fix potential panic if Cloudflare API fails to respond to GetTunnel(id) during delete command - 2021-10-14 TUN-5257: Fix more cfsetup targets that were broken by recent package changes 2021.10.2 - 2021-10-11 TUN-5138: Switch to QUIC on auto protocol based on threshold - 2021-10-14 TUN-5250: Add missing packages for cfsetup to succeed in github release pkgs target 2021.10.1 - 2021-10-12 TUN-5246: Use protocol: quic for Quick tunnels if one is not already set - 2021-10-13 TUN-5249: Revert "TUN-5138: Switch to QUIC on auto protocol based on threshold" 2021.10.0 - 2021-10-11 TUN-5138: Switch to QUIC on auto protocol based on threshold - 2021-10-07 TUN-5195: Do not set empty body if not applicable - 2021-10-08 UN-5213: Increase MaxStreams value for QUIC transport - 2021-09-28 TUN-5169: Release 2021.9.2 CHANGES.md - 2021-09-28 TUN-5164: Update README and clean up references to Argo Tunnel (using Cloudflare Tunnel instead) 2021.9.2 - 2021-09-21 TUN-5129: Use go 1.17 and copy .git folder to docker build to compute version - 2021-09-21 TUN-5128: Enforce maximum grace period - 2021-09-22 TUN-5141: Make sure websocket pinger returns before streaming returns - 2021-09-24 TUN-5142: Add asynchronous servecontrolstream for QUIC - 2021-09-24 TUN-5142: defer close rpcconn inside unregister instead of ServeControlStream - 2021-09-27 TUN-5160: Set request.ContentLength when this value is in request header 2021.9.1 - 2021-09-21 TUN-5118: Quic connection now detects duplicate connections similar to http2 - 2021-09-15 Fix TryCloudflare link 2021.9.0 - 2021-09-02 Fix broken TryCloudflare link - 2021-09-03 Add support for taking named tunnel credentials from an environment variable - 2021-08-30 TUN-5012: Use patched go-sumtype - 2021-08-31 TUN-5011: Use the region parameter in fallback SRV lookup - 2021-08-31 TUN-5029: Do not strip cf- prefixed headers - 2021-08-29 TUN-5009: Updated github action to use go 1.17.x for checks - 2021-08-28 TUN-5010: --region should be a string flag - 2021-08-10 Allow building on arm64 platforms - 2021-06-09 Update README.md - 2021-05-31 🖌️ Allow providing TokenID and TokenSecret as env vars when calling cloudflared access - 2021-05-31 🎨 Prefix env var parameters with TUNNEL 2021.8.7 - 2021-08-28 Revert "TUN-4926: Implement --region configuration option" 2021.8.6 - 2021-08-27 TUN-5000: De-flake logging to dir component test in Windows by increasing to buffer to cope with more logging - 2021-08-27 TUN-5003: Fix cfsetup for non-FIPS golang version 2021.8.5 - 2021-08-27 TUN-4961: Update quic-go to latest - 2021-08-27 Release 2021.8.4 2021.8.4 - 2021-08-26 TUN-4974: Fix regression where we were debug logging by accident - 2021-08-26 TUN-4970: Only default to http2 for warp-routing if protocol is h2mux - 2021-08-26 TUN-4981: Improve readability of prepareTunnelConfig method - 2021-08-26 TUN-4926: Implement --region configuration option - 2021-07-09 TUN-4821: Make quick tunnels the default in cloudflared 2021.8.3 - 2021-08-23 TUN-4889: Add back appendtagheaders function - 2021-08-21 TUN-4940: Fix cloudflared not picking up correct NextProtos for quic - 2021-08-21 TUN-4613: Add a no-op protocol version slot - 2021-08-13 TUN-4922: Downgrade quic-go library to 0.20.0 - 2021-08-17 TUN-4866: Add Control Stream for QUIC - 2021-08-17 TUN-4927: Parameterize region in edge discovery code - 2021-08-06 TUN-4602: Added UDP resolves to Edge discovery 2021.8.2 - 2021-08-03 TUN-4597: Added HTTPProxy for QUIC - 2021-08-04 TUN-4795: Remove Equinox releases - 2021-08-09 TUN-4911: Append Environment variable to Path instead of overwriting it 2021.8.1 - 2021-08-02 TUN-4855: Added CHANGES.md for release 2021.8.0 - 2021-08-03 TUN-4597: Add a QUIC server skeleton - 2021-08-03 TUN-4873: Disable unix domain socket test for windows unit tests - 2021-08-04 TUN-4875: Added amd64-linux builds back to releases 2021.8.0 - 2021-07-30 TUN-4847: Allow to list tunnels by prefix name or exclusion prefix name - 2021-07-30 TUN-4772: Release built executables with packages - 2021-07-30 TUN-4851: Component tests to smoke test that Proxy DNS and Tunnel are only run when expected - 2021-07-28 TUN-4811: Publish quick tunnels' hostname in /metrics under `userHostname` for backwards-compatibility - 2021-07-29 TUN-4832: Prevent tunnel from running accidentally when only proxy-dns should run - 2021-07-28 TUN-4819: Tolerate protocol TXT record lookup failing 2021.7.4 - 2021-07-28 TUN-4814: Revert "TUN-4699: Make quick tunnels the default in cloudflared" - 2021-07-28 TUN-4812: Disable CGO for cloudflared builds 2021.7.3 - 2021-07-27 TUN-4799: Build deb, msi and rpm packages with fips 2021.7.2 - 2021-07-27 Fixed a syntax error with python logging. 2021.7.1 - 2021-07-21 TUN-4755: Add a windows msi release option to Make - 2021-07-22 TUN-4761: Added a build-all-packages target to cfsetup - 2021-07-26 TUN-4771: Upload deb, rpm and msi packages to github - 2021-07-14 TUN-4714: Name nightly package cloudflared-nightly to avoid apt conflict - 2021-07-16 TUN-4701: Split Proxy into ProxyHTTP and ProxyTCP - 2021-07-08 TUN-4596: Add QUIC application protocol for QUIC stream handshake - 2021-07-09 TUN-4699: Make quick tunnels the default in cloudflared 2021.7.0 - 2021-07-01 TUN-4626: Proxy non-stream based origin websockets with http Roundtrip. - 2021-07-01 TUN-4655: ingress.StreamBasedProxy.EstablishConnection takes dest input - 2021-07-09 TUN-4698: Add cloudflared metrics endpoint to serve quick tunnel hostname - 2021-06-21 TUN-4521: Modify cloudflared to use zoneless-tunnels-worker for free tunnels - 2021-04-05 AUTH-3475: Updated GetAppInfo error message 2021.6.0 - 2021-06-21 TUN-4571: Changelog for 2021.6.0 - 2021-06-18 TUN-4571: Fix proxying to unix sockets when using HTTP2 transport to Cloudflare Edge - 2021-06-07 TUN-4502: Make `cloudflared tunnel route` subcommands described consistently - 2021-06-08 TUN-4504: Fix component tests in windows - 2021-05-27 TUN-4461: Log resulting DNS hostname if one is received from Cloudflare API 2021.5.10 - 2021-05-25 TUN-4456: Replaced instances of Tick() with Ticker() in h2mux paths 2021.5.9 - 2021-05-20 TUN-4426: Fix centos builds - 2021-05-20 Update changelog - 2021-04-30 AUTH-3426: Point to new transfer service URL and eliminate PUT /ok 2021.5.8 - 2021-05-14 TUN-4419: Improve error message when cloudflared cannot reach origin - 2021-05-19 TUN-4425: --overwrite-dns flag for in adhoc and route dns cmds 2021.5.7 - 2021-05-17 Fix typo in Changes.md - 2021-05-17 TUN-4421: Named Tunnels will automatically select the protocol to connect to Cloudflare's edge network 2021.5.6 - 2021-05-14 TUN-4418: Downgrade to Go 1.16.3 2021.5.5 2021.5.4 - Fix release pipeline 2021.5.1 - 2021-05-10 TUN-4342: Fix false positive warning about unused hostname property - 2021-05-10 Release 2021.5.0 2021.5.0 - 2021-05-10 TUN-4384: Silence log from automaxprocs - 2021-05-10 AUTH-3537: AUDs in JWTs are now always arrays - 2021-05-10 Update changelog for 2021.5.0 - 2021-05-03 TUN-4343: Fix broken build by setting debug field correctly - 2021-05-06 TUN-4356: Set AUTOMAXPROCS to the CPU limit when running in a Linux container - 2021-05-06 TUN-4357: Bump Go to 1.16 - 2021-05-06 TUN-4359: Warn about unused keys in 'tunnel ingress validate' - 2021-04-30 debug: log host / path - 2021-04-20 AUTH-3513: Checks header for app info in case response is a 403/401 from the edge - 2021-04-29 TUN-4000: Release notes for cloudflared replica model - 2021-04-09 TUN-2853: rename STDIN-CONTROL env var to STDIN_CONTROL - 2021-04-09 TUN-4206: Better error message when user is only using one ingress rule 2021.4.0 - 2021-04-05 TUN-4178: Fix component test for running as a service in MacOS to not assume a named tunnel - 2021-04-05 TUN-4177: Running with proxy-dns should not prevent running Named Tunnels - 2021-04-02 TUN-4168: Transparently proxy websocket connections using stdlib HTTP client instead of gorilla/websocket; move websocket client code into carrier package since it's only used by access subcommands now (#345). - 2021-04-07 Publish change log for 2021.4.0 2021.3.6 - 2021-03-30 TUN-4150: Only show the connector table in 'tunnel info' if there are connectors. Don't show rows with zero connections. - 2021-03-31 TUN-4153: Revert best-effort HTTP2 usage when talking to origins - 2021-03-26 TUN-4141: Better error messages for tunnel info subcommand. - 2021-03-29 TUN-4146: Unhide and document grace-period - 2021-03-25 TUN-3863: Consolidate header handling logic in the connection package; move headers definitions from h2mux to packages that manage them; cleanup header conversions 2021.3.5 - 2021-03-26 TUN-3896: http-service and tunnelstore client use http2 transport. - 2021-03-25 TUN-4125: Change component tests to run in CI with its own dedicated resources - 2021-03-26 Publish change log for 2021.3.5 2021.3.4 2021.3.3 - 2021-03-23 TUN-4111: Warn the user if both properties "tunnel" and "hostname" are used - 2021-03-23 TUN-4082: Test logging when running as a service - 2021-03-23 TUN-4112: Skip testing graceful shutdown with SIGINT on Windows - 2021-03-23 TUN-4116: Ingore credentials-file setting in configuration file during tunnel create and delete opeations. - 2021-03-23 TUN-4118: Don't overwrite existing file with tunnel credentials. For ad-hoc tunnels, this means tunnel won't start if there's a file in the way. - 2021-03-24 TUN-4123: Don't capture output in reconnect componet test - 2021-03-23 TUN-4067: Reformat code for consistent import order, grouping, and fix formatting. Added goimports target to the Makefile to make this easier in the future. - 2021-03-24 AUTH-3455: Generate short-lived ssh cert per hostname - 2021-03-25 Update changelog 2021.3.3 2021.3.2 - 2021-03-23 TUN-4042: Capture cloudflared output to debug component tests - 2021-03-23 Publish changelog for 2021.3.2 - 2021-03-16 TUN-4089: Address flakiness in component tests for termination - 2021-03-16 TUN-4060: Fix Go Vet warnings (new with go 1.16) where t.Fatalf is called from a test goroutine - 2021-03-16 TUN-4091: Use flaky decorator to rerun reconnect component tests when they fail - 2021-03-12 TUN-4081: Update log severities to use Zerolog's levels - 2021-03-16 TUN-4094: Don't read configuration file for access commands - 2021-03-15 TUN-3993: New `cloudflared tunnel info` to obtain details about the active connectors for a tunnel - 2021-03-17 TUN-3715: Apply input source to the correct context - 2021-03-17 AUTH-3394: Ensure scheme on token command - 2021-03-18 TUN-4096: Reduce tunnel not connected assertion backoff to address flaky termination tests - 2021-03-19 TUN-3998: Allow to cleanup the connections of a tunnel limited to a single client - 2021-02-04 TUN-3715: Only read config file once, right before invoking the command 2021.3.1 - 2021-03-11 TUN-4051: Add component-tests to test graceful shutdown - 2021-03-12 TUN-4052: Add component tests to assert service mode behavior 2021.3.0 - 2021-03-10 TUN-4075: Dedup test should not compare order of list - 2021-03-10 Revert "AUTH-3394: Creates a token per app instead of per path" - 2021-03-11 TUN-4066: Remove unnecessary chmod during package publish to CF_PKG_HOSTS - 2021-03-11 TUN-4066: Set permissions in build agent before 'scp'-ing to machine hosting package repo - 2021-03-11 TUN-4050: Add component tests to assert reconnect behavior - 2021-03-10 AUTH-3394: Creates a token per app instead of per path - with fix for free tunnels - 2021-03-15 Publish change log for 2021.3.0 - 2021-03-01 Issue #285 - Makefile does not detect TARGET_ARCH correctly on FreeBSD (#325) - 2021-03-01 TUN-3988: Log why it cannot check if origin cert exists - 2021-03-02 TUN-3995: Optional --features flag for tunnel run. - 2021-03-02 TUN-3994: Log client_id when running a named tunnel - 2021-03-04 TUN-4026: Fix regression where HTTP2 edge transport was no longer propagating control plane errors - 2021-03-05 TUN-4055: Skeleton for component tests - 2021-03-08 TUN-4047: Add cfsetup target to run component test - 2021-03-08 TUN-4016: Delegate decision to update for Worker service - 2021-03-02 TUN-3905: Cannot run go mod vendor in cloudflared due to fips - 2021-03-08 TUN-4063: Cleanup dependencies between packages. - 2021-03-09 Allow partial reads from a GorillaConn; add SetDeadline (from net.Conn) (#330) - 2021-03-09 TUN-4069: Fix regression on support for websocket over proxy - 2021-03-02 AUTH-3394: Creates a token per app instead of per path - 2021-03-01 TUN-4017: Add support for using cloudflared as a full socks proxy. - 2021-03-08 TUN-4062: Read component tests config from yaml file - 2021-03-08 TUN-4049: Add component tests to assert logging behavior when running from terminal - 2021-02-23 TUN-3963: Repoint urfave/cli/v2 library at patched branch at github.com/ipostelnik/cli/v2@fixed which correctly handles reading flags declared at multiple levels of subcommands. - 2021-02-25 TUN-3970: Route ip show has alias route ip list - 2021-02-26 TUN-3978: Unhide teamnet commands and improve their help - 2021-02-26 TUN-3983: Renew CA certs in cloudflared - 2021-02-28 TUN-3989: Check in with Updater service in more situations and convey messages to user - 2021-02-11 TUN-3819: Remove client-side check that deleted tunnels have no connections 2021.2.5 - 2021-02-23 Publish change notes for 2021.2.5 - 2021-02-11 TUN-3838: ResponseWriter no longer reads and origin error tests - 2021-02-10 TUN-3895: Tests for socks stream handler - 2021-02-19 TUN-3939: Add logging that shows that Warp-routing is enabled - 2021-02-02 TUN-3817: Adds tests for websocket based streaming regression - 2021-02-04 TUN-3799: extended the Stream interface to take a logger and added debug logs for io.Copy errors - 2021-02-03 TUN-3855: Add ability to override target of 'access ssh' command to a different host for testing - 2021-02-04 TUN-3853: Respond with ws headers from the origin service rather than generating our own - 2021-02-08 TUN-3889: Move host header override logic to httpService - 2021-02-05 TUN-3868: Refactor singleTCPService and bridgeService to tcpOverWSService and rawTCPService - 2021-01-21 TUN-3753: Select http2 protocol when warp routing is enabled - 2021-01-26 TUN-3809: Allow routes ip show to output as JSON or YAML - 2021-01-11 TUN-3615: added support to proxy tcp streams - 2021-01-17 TUN-3725: Warp-routing is independent of ingress - 2021-01-15 TUN-3764: Actively flush data for TCP streams - 2020-12-09 TUN-3617: Separate service from client, and implement different client for http vs. tcp origins 2021.2.4 - 2021-02-22 TUN-3948: Log error when retrying connection - 2021-02-23 TUN-3964: Revert "TUN-3922: Repoint urfave/cli/v2 library at patched branch at github.com/ipostelnik/cli/v2@fixed which correctly handles reading flags declared at multiple levels of subcommands." - 2021-02-23 Publish release notes for 2021.2.4 2021.2.3 - 2021-02-23 Publish release notes for 2021.2.3 - 2021-02-10 TUN-3902: Add jitter to backoffhandler - 2021-02-11 TUN-3913: Help gives wrong exit code for autoupdate - 2021-02-12 Add max upstream connections dns-proxy option (#290) - 2021-02-16 TUN-3924: Removed db-connect command. Added a placeholder handler for this command that informs users that command is no longer supported. - 2021-02-12 TUN-3922: Repoint urfave/cli/v2 library at patched branch at github.com/ipostelnik/cli/v2@fixed which correctly handles reading flags declared at multiple levels of subcommands. - 2021-02-19 Added support for proxy (#318) - 2021-02-19 TUN-3945: Fix runApp signature for generic service - 2021-02-09 Update README.md - 2021-02-09 Update the TryCloudflare link 2021.2.2 - 2021-02-04 TUN-3864: Users can choose where credentials file is written after creating a tunnel - 2021-02-04 TUN-3869: Improve reliability of graceful shutdown. - 2021-02-07 TUN-3878: Do not supply -tags when none are specified - 2021-02-04 TUN-3635: Send event when unregistering tunnel for gracful shutdown so /ready endpoint reports down status befoe connections finish handling pending requests. - 2021-02-08 TUN-3890: Code coverage for cloudflared in CI - 2021-02-09 AUTH-3375 exchangeOrgToken deleted cookie fix - 2020-11-18 Update error message to use login command 2021.2.1 - 2021-02-04 TUN-3858: Do not suffix cloudflared version with -fips 2021.2.0 - 2021-02-01 TUN-3837: Remove automation_email from cloudflared status page test - 2021-02-03 TUN-3848: Use transport logger for h2mux - 2021-02-03 TUN-3854: cloudflared tunnel list flags to sort output - 2021-01-21 TUN-3195: Don't colorize console logs when stderr is not a terminal - 2021-01-20 Fixed connection error handling by removing duplicated errors, standardizing on non-pointer error types - 2021-01-20 TUN-3118: Changed graceful shutdown to immediately unregister tunnel from the edge, keep the connection open until the edge drops it or grace period expires - 2021-01-25 TUN-3165: Add reference to Argo Tunnel documentation in the help output - 2021-01-25 TUN-3806: Use a .dockerignore - 2021-01-21 TUN-3795: Use RFC-3339 style date format for logs, produce timestamp in UTC - 2021-01-26 TUN-3795: Removed errant test - 2021-01-25 TUN-3792: Handle graceful shutdown correctly when running as a windows service. Only expose one shutdown channel globally, which now triggers the graceful shutdown sequence across all modes. Removed separate handling of zero-duration grace period, instead it's checked only when we need to wait for exit. - 2021-01-27 TUN-3811: Better error reporting on http2 connection termination. Registration errors from control loop are now propagated out of the connection server code. Unified error handling between h2mux and http2 connections so we log and retry errors the same way, regardless of underlying transport. - 2021-01-28 TUN-3830: Use Go 1.15.7 - 2021-01-28 TUN-3826: Use go-fips when building cloudflared for linux/amd64 - 2021-01-19 TUN-3777: Fix /ready endpoint for classic tunnels - 2021-01-19 TUN-3773: Add back pprof endpoints 2021.1.5 - 2021-01-15 TUN-3594: Log ingress response at debug level - 2021-01-15 TUN-3765: Fix doubly nested log output by `logfile` option - 2021-01-16 TUN-3767: Tolerate logging errors - 2021-01-17 TUN-3768: Reuse file loggers - 2021-01-14 TUN-3738: Refactor observer to avoid potential of blocking on tunnel notifications - 2021-01-15 TUN-3766: Print flags defined at all levels of command hierarchy, not just locally defined flags for a command. This fixes output of overriden settings for subcommand. 2021.1.4 - 2021-01-14 TUN-3759: Single file logging output should always append 2021.1.3 - 2021-01-14 TUN-3756: File logging output must consider the directory - 2021-01-14 TUN-3757: Fix legacy Uint flags that are incorrectly handled by ufarve library 2021.1.2 - 2021-01-13 TUN-3747: Fix logging in Windows 2021.1.1 - 2021-01-13 TUN-3744: Fix compilation error in windows service 2021.1.0 - 2021-01-11 TUN-3670: Update Teamnet API gateway prefixes - 2021-01-13 TUN-3738: Consume UI events even when UI is disabled - 2021-01-06 TUN-3722: Teamnet API paths include /network - 2021-01-05 TUN-3688: Subcommand for users to check which route an IP proxies through - 2021-01-08 TUN-3691: Edit Teamnet help text - 2020-12-30 TUN-3706: Quit if any origin service fails to start - 2020-12-31 TUN-3708: Better info message about system root certpool on Windows - 2020-12-21 TUN-3669: Teamnet commands to add/show Teamnet routes. - 2020-12-29 TUN-3689: Delete routes via cloudflared CLI - 2020-12-28 TUN-3471: Add structured log context to logs - 2020-12-15 TUN-3650: Remove unused awsuploader package - 2020-12-03 Update to add deprecated version note (#271) - 2020-12-02 TUN-3472: Set up rolling logger with zerolog and lumberjack - 2020-12-08 TUN-3607: Set up single-file logger with zerolog - 2020-12-03 Update to add deprecated version note (#271) - 2020-11-25 TUN-3470: Replace in-house logger calls with zerolog 2020.12.0 - 2020-12-04 TUN-3599: improved delete if credentials isnt found. - 2020-12-04 TUN-3612: Upgrade to Go 1.15.6 - 2020-11-30 TUN-3593: /ready endpoint for k8s readiness. Move tunnel events out of UI package, into connection package. - 2020-11-27 TUN-3594: Log response status at debug level 2020.11.11 - 2020-11-20 TUN-3578: cloudflared tunnel route dns should allow wildcard subdomains - 2020-11-21 EDGEPLAT-2958 remove deb-compression, defaulting to gzip - 2020-11-23 TUN-3581: Tunnels can be run by name using only --credentials-file, no origin cert necessary. - 2020-11-15 TUN-3561: Unified logger configuration - 2020-11-08 AUTH-3221: Saves org token to disk and uses it to refresh the app token 2020.11.10 - 2020-11-20 TUN-3562: Fix panic when using bastion mode ingress rule - 2020-11-20 EDGEPLAT-2958 build cloudflared for Bullseye 2020.11.9 - 2020-11-18 TUN-3557: Detect SSE if content-type starts with text/event-stream - 2020-11-18 TUN-3559: Share response meta header with other packages - 2020-11-18 DEVTOOLS-7936: Remove redundant chgrp from publish - 2020-11-18 TUN-3558: cloudflared allows empty config files - 2020-11-18 TUN-3544: Upgrade to Go 1.15.5 2020.11.8 - 2020-11-17 TUN-3555: Single origin service should default to localhost:8080 2020.11.7 - 2020-11-13 TUN-3514: Stop setting --is-autoupdated flag after autoupdate because it can break named tunnel running in k8s - 2020-11-15 TUN-3548, TUN-3547: Bastion mode can be specified as a service, doesn't require URL. - 2020-11-16 TUN-3549: Use a separate handler for each websocket proxy 2020.11.6 - 2020-11-14 TUN-3546: Fix panic in tlsconfig.LoadOriginCA 2020.11.5 - 2020-11-12 TUN-3540: Better copy in ingress rules error messages - 2020-11-12 DEVTOOLS-7936: Set permissions on public packages - 2020-11-13 TUN-3543: ProxyAddress not using default in single-origin mode 2020.11.4 - 2020-11-11 TUN-3534: Specific error message when credentials file is a .pem not .json - 2020-11-02 TUN-3500: Integrate replace h2mux by http2 work with multiple origin support - 2020-11-09 TUN-3514: Transport logger write to UI when UI is enabled - 2020-10-30 TUN-3490: Make sure OriginClient implementation doesn't write after Proxy return - 2020-10-20 TUN-3403: Unit test for origin/proxy to test serving HTTP and Websocket - 2020-10-23 TUN-3480: Support SSE with http2 connection, and add SSE handler to hello-world server - 2020-10-27 TUN-3489: Add unit tests to cover proxy logic in connection package of cloudflared - 2020-10-16 TUN-3467: Serialize cf-cloudflared-response-meta during package initialization using jsoniter - 2020-10-14 TUN-3456: New protocol option auto to automatically select between http2 and h2mux - 2020-10-14 TUN-3458: Upgrade to http2 when available, fallback to h2mux when we reach max retries - 2020-10-08 TUN-3449: Use flag to select transport protocol implementation - 2020-10-08 TUN-3462: Refactor cloudflared to separate origin from connection - 2020-09-21 TUN-3406: Proxy websocket requests over Go http2 - 2020-09-25 TUN-3420: Establish control plane and send RPC over control plane - 2020-09-11 TUN-3400: Use Go HTTP2 library as transport to connect with the edge 2020.11.3 - 2020-11-11 TUN-3533: Set config for single origin ingress 2020.11.2 2020.11.1 - 2020-11-10 TUN-3527: More specific error for invalid YAML/JSON - 2020-11-06 Update README.md (#256) 2020.11.0 - 2020-11-04 TUN-3484: OriginService that responds with configured HTTP status - 2020-11-05 TUN-3505: Response body for status code origin returns EOF on Read - 2020-11-04 TUN-3503: Matching ingress rule should not take port into account - 2020-11-05 TUN-3506: OriginService needs to set request host and scheme for websocket requests - 2020-11-09 TUN-3516: Better error message when parsing invalid YAML config - 2020-11-09 TUN-3522: ingress validate checks that the config file exists - 2020-11-09 TUN-3524: Don't ignore errors from app-level action handler (#248) - 2020-11-09 TUN-3461: Show all origin services in the UI - 2020-10-30 TUN-3494: Proceed to create tunnel if at least one edge address can be resolved - 2020-10-30 TUN-3492: Refactor OriginService, shrink its interface - 2020-10-22 TUN-3478: Increase download timeout to 60s - 2020-10-15 TUN-2640: Users can configure per-origin config. Unify single-rule CLI flow with multi-rule config file code. 2020.10.2 - 2020-10-21 Release 2020.10.1 - 2020-10-21 AUTH-3185 fixed indention error - 2020-10-19 TUN-3459: Make service install on linux use named tunnels 2020.10.1 - 2020-10-20 Split out typed config from legacy command-line switches; refactor ingress commands and fix tests - 2020-10-20 Move raw ingress rules to config package - 2020-10-21 TUN-3476: Fix conversion to string and int slice - 2020-10-12 TUN-3441: Multiple-origin routing via ingress rules - 2020-10-15 TUN-3464: Newtype to wrap []ingress.Rule - 2020-10-15 TUN-3465: Use Go 1.15.3 - 2020-10-15 TUN-3463: Let users run a named tunnel via config file setting - 2020-10-19 TUN-3475: Unify config file handling with typed config for new fields - 2020-10-19 TUN-3459: Make service install on linux use named tunnels - 2020-10-06 AUTH-3148 fixed cloudflared copy and match all the files in the checksum upload - 2020-10-06 TUN-3436, TUN-3437: Parse ingress from YAML, ensure last rule catches everything - 2020-10-06 TUN-3446: Use go 1.15.2 and add a step to build cloudflared in the dev Dockerfile - 2020-10-07 TUN-3439: 'tunnel validate' command to check ingress rules - 2020-10-07 TUN-3440: 'tunnel rule' command to test ingress rules - 2020-10-08 TUN-3451: Cloudflared tunnel ingress command - 2020-10-09 TUN-3452: Fix loading of flags from config file for tunnel run subcommand. This change also cleans up building of tunnel subcommand list, hides deprecated subcommands and improves help. - 2020-10-08 TUN-3438: move ingress into own package, read into TunnelConfig 2020.10.0 - 2020-10-02 AUTH-2993 cleaned up worker service tests - 2020-10-02 TUN-3443: Decode as v4api response on non-200 status - 2020-09-24 TRAFFIC-448: allow the user to specify the proxy address and port to bind to, falling back to 127.0.0.1 and random port if not specified - 2020-09-28 TUN-3427: Define a struct that only implements RegistrationServer in tunnelpogs - 2020-09-29 TUN-3430: Copy flags to configure proxy to run subcommand, print relevant tunnel flags in help - 2020-08-12 AUTH-2993 added workers updater logic 2020.9.3 - 2020-09-22 TRAFFIC-448: build cloudflare for junos and publish to s3 - 2020-09-22 TUN-3410: Request the v1 Tunnelstore API - 2020-09-23 Release 2020.9.2 - 2020-09-17 updater service exit code should be 11 - 2020-09-18 AUTH-3109 upload the checksum to workers kv on github releases 2020.9.2 - 2020-09-22 TRAFFIC-448: build cloudflare for junos and publish to s3 - 2020-09-22 TUN-3410: Request the v1 Tunnelstore API - 2020-09-17 AUTH-3103 CI build fixes - 2020-09-18 AUTH-3110-use-cfsetup-precache - 2020-09-17 TUN-3295: Show route command results - 2020-09-16 TUN-3291: cloudflared tunnel run -h explains how to use flags from parent command - 2020-09-18 AUTH-3109 upload the checksum to workers kv on github releases - 2020-09-17 updater service exit code should be 11 - 2020-09-01 TUN-3216: UI improvements - 2020-08-25 Rebased and passed TunnelEventChan to LogServerInfo in new ReconnectTunnel function - 2020-08-25 TUN-3321: Add box around logs on UI - 2020-08-26 TUN-3328: Filter out free tunnel has started log from UI - 2020-08-27 TUN-3333: Add text to UI explaining how to exit - 2020-08-27 TUN-3335: Dynamically set connection table size for UI - 2020-08-10 TUN-3238: Update UI when connection re-connects - 2020-08-17 TUN-3261: Display connections on UI for free classic tunnels - 2020-07-24 TUN-3201: Create base cloudflared UI structure - 2020-07-29 TUN-3200: Add connection information to UI - 2020-07-24 TUN-3255: Update UI to display URL instead of hostname - 2020-07-29 TUN-3198: Handle errors while running tunnel UI 2020.9.1 - 2020-09-14 TUN-3395: Unhide named tunnel subcommands, tweak help - 2020-09-15 TUN-3395: Improve help for list command - 2020-09-14 TUN-3294: Perform basic validation on arguments of route command; remove default pool name which wasn't valid - 2020-09-15 TUN-3395: Improve help for list command - 2020-09-16 Use Go 1.15.2 2020.9.0 - 2020-09-11 TUN-3293: Try to use error information from the body of a failed tunnelstore reresponse if available - 2020-09-04 AUTH-2653 renabled signing - 2020-09-04 TUN-3377: Tunnel route should check dns/lb before checking tunnel ID - 2020-09-04 AUTH-2653 changed to proper file extension - 2020-09-04 AUTH-2653 handle duplicate key import errors - 2020-09-04 TUN-3345: tunnel run accepts name of tunnel as argument - 2020-09-08 AUTH-2653 disble error pipe to see what is failing - 2020-09-08 AUTH-2653 search for the certificate and not the identity - 2020-09-09 TUN-3284: Use cloudflared/ as user agent of tunnelstore client - 2020-09-09 TUN-3375: Upgrade x/text and gorilla websocket deps - 2020-09-09 TUN-3375: Upgrade coredns and prometheus dependencies - 2020-09-09 AUTH-2653 add notarization to mac build - 2020-09-08 TUN-3292: Mention cleanup in tunnel run help. - 2020-08-20 AUTH-2016 fixed variable fail - 2020-08-12 TUN-3352 extra debug logging for websockets 2020.8.2 - 2020-08-20 AUTH-3021 fixed the git version call by using the older flag - 2020-08-18 TUN-3268: Each connection has its own event digest to reconnect 2020.8.1 - 2020-08-14 AUTH-2975 don't check /etc on windows - 2020-08-14 AUTH-2977 log file protection - 2020-08-18 TUN-3286: Use either ID or name in Named Tunnel subcommands. - 2020-08-18 AUTH-2712 fixed the mac build script - 2020-08-19 AUTH-2653 disabling signing until we can get the keys - 2020-08-05 TUN-3233: List tunnels support filtering by deleted, name, existed at and id - 2020-08-05 TUN-3237: By default, don't show connections that are pending reconnect - 2020-08-06 TUN-3242: Build with go 1.14 - 2020-08-07 TUN-3243: Refactor tunnel subcommands to allow commands to compose better - 2020-08-07 AUTH-2864 - add macos build to github release - 2020-07-31 AUTH-2857 update homebrew script to use new url - 2020-07-30 TUN-3213: Create, route and run named tunnels in one command - 2020-07-07 AUTH-2712 added MSI build for a windows agent 2020.8.0 - 2020-07-30 TUN-3220: tunnel route reports created route - 2020-07-31 TUN-3221: ConnectionOptions tracks numPreviousAttempts. - 2020-07-20 TUN-3190: Initialize logger using command line flags in tunnels subcommands - 2020-07-21 TUN-3192: Use zone ID in tunnelstore request path; improve debug logging - 2020-07-23 TUN-3194: Don't render log output when level is not enabled - 2020-07-22 AUTH-2016 adds sha256 hashes to releases - 2020-07-27 Removes centos 6 build - 2020-07-27 TUN-3209: Add benchmark for header serialization - 2020-07-24 TUN-3209: improve performance and reduce allocations during user header serialization from h1 to h2 - 2020-07-26 TUN-3208: Add benchmark for large response write - 2020-07-27 TUN-3208: Reduce copies and allocations on h2mux write path. Pre-allocate 16KB write buffer on the first write if possible. Use explicit byte array for chunks on write thread to avoid copying through intermediate buffer due to io.CopyN. - 2020-07-28 AUTH-2714: Adds arm64 cloudflared build - 2020-07-29 AUTH-2927 run message update after all github builds are done - 2020-07-17 AUTH-2902 redirect with just the root host on curl commands 2020.7.4 - 2020-07-20 Build cloudflared for arm64 on native agents - 2020-07-10 TUN-3048: Handle error when user tries to delete active tunnel - 2020-07-14 AUTH-2890: adds error handler to cli actions - 2020-07-06 TUN-3156: Add route subcommand under tunnel 2020.7.3 - 2020-07-13 Change scp command to use file glob that matches both cloudflared rpms and debs 2020.7.2 - 2020-07-02 AUTH-2644: Change install location and add man page - 2020-07-02 TUN-3131: Allow user to specify tunnel credentials path, and remove it in tunnel delete command - 2020-07-03 TUN-3008: Implement cloudflared tunnel cleanup command - 2020-07-02 TUN-3150: cloudflared tunnel list's table should use intelligent column width - 2020-07-07 TUN-3169: Move on to the next address when edge returns duplicate connection. There's no point in trying to connect to the same address since it will be hashed to the same metal. Improve logging of errors from serve tunnel loop, hide useless context cancelled error. - 2020-07-08 beautify package meta information generated by fpm (#218) - 2020-07-06 AUTH-2871: fix rpm builds - 2020-07-08 AUTH-2858: Set file to disable autoupdate - 2020-07-09 AUTH-2872: Adds centos-6 build 2020.7.1 - 2020-07-02 DEVTOOLS-7321: Push GitHub homebrew updates to master - 2020-07-06 TUN-3161: Upgrade golang.org/x/ deps - 2020-06-30 AUTH-2854: Create cloudflared RPMs - 2020-06-26 AUTH-2850 log config file path 2020.7.0 - 2020-06-30 TUN-3140: Add timestamps to terminal log entries - 2020-06-30 AUTH-2860: Fix builds - 2020-06-25 TUN-3007: Implement named tunnel connection registration and unregistration. 2020.6.6 - 2020-06-23 AUTH-2685: Adds script to create release - 2020-06-25 AUTH-2652: Update cloudflare repo - 2020-06-26 AUTH-2718: Add target for publishing deb to pkg.cloudflare repo - 2020-06-26 AUTH-2849 all log output to stderr - 2020-06-17 TUN-3106: Pass NamedTunnel config to StartServer - 2020-06-18 TUN-3107: UnregisterConnection shouldn't wrap nil error as RPC error - 2020-06-17 AUTH-2652: Adds .docker-images to push images to docker hub - 2020-06-18 AUTH-2712 mac package build script and better config file handling when started as a service 2020.6.5 - 2020-06-16 DEVTOOLS-7321: Don't skip macOS builds based on tag - 2020-06-16 fix for a flaky test - 2020-06-16 AUTH-2815 flag check was wrong. stupid oversight - 2020-06-16 TUN-3101: Tunnel list command should only show non-deleted, by default - 2020-06-16 TUN-3066: Command line action for tunnel run - 2020-06-16 TUN-3100 make updater report the right text 2020.6.4 - 2020-06-11 TUN-3085: Pass connection authentication information using TunnelAuth struct - 2020-06-15 TUN-3084: Generate and store tunnel_secret value during tunnel creation - 2020-06-16 AUTH-2815 add the log file to support the config.yaml file - 2020-06-02 TUN-3015: Add a new cap'n'proto RPC interface for connection registration as well as matching client and server implementations. The old interface extends the new one for backward compatibility. 2020.6.3 - 2020-06-15 DEVTOOLS-7321: Add openssh-client pkg for missing ssh-keyscan - 2020-06-15 AUTH-2813 adds back a single file support a cloudflared log file 2020.6.2 - 2020-06-11 AUTH-2648 updated usage text - 2020-06-11 AUTH-2763 don't redirect from curl command - 2020-06-12 TUN-3090: Upgrade crypto dep - 2020-06-11 TUN-3038: Add connections to tunnel list table - 2020-06-12 AUTH-2810 added warn for backwards compatibility sake 2020.6.1 - 2020-06-09 AUTH-2796 fixed windows build 2020.6.0 - 2020-06-05 AUTH-2645 protect against user mistaken flag input - 2020-06-05 AUTH-2687 don't copy config unnecessarily - 2020-06-05 AUTH-2169 make access login page more generic - 2020-06-05 AUTH-2729 added log file and level to cmd flags to match config file settings - 2020-06-08 AUTH-2785 service token flag fix and logger fix - 2020-05-20 AUTH-2682: Create buster build - 2020-05-21 TUN-2928, TUN-2929, TUN-2930: Add tunnel subcommands to interact with tunnel store service - 2020-05-29 Adding support for multi-architecture images and binaries (#184) - 2020-05-29 TUN-3019: Remove declarative tunnel entry code - 2020-05-29 TUN-3020: Remove declarative tunnel related RPC code - 2020-05-13 AUTH-2505 added aliases - 2020-05-14 AUTH-2529 added deprecation text to db-connect command - 2020-05-18 AUTH-2686: Added error handling to tunnel subcommand - 2020-05-04 AUTH-2369: RDP Bastion prototype - 2020-04-29 AUTH-2596 added new logger package and replaced logrus - 2020-04-25 DEVTOOLS-7321: Use SSH key from env for pushing to GitHub - 2020-04-25 DEVTOOLS-7321: Push to a test branch instead of to master - 2020-03-30 DEVTOOLS-7321: Add scripts for macOS builds and homebrew uploads 2020.5.1 - 2020-05-07 TUN-2860: Enable quick reconnect feature by default - 2020-05-07 AUTH-2564: error handling and minor fixes - 2020-05-01 AUTH-2588 add DoH to service mode 2020.5.0 - 2020-05-01 TUN-2943: Copy certutil from edge into cloudflared - 2020-05-05 TUN-2955: Fix connection and goroutine leaks when tunnel conection is terminated on error. Only unregister tunnels that had connected successfully. Close edge connection used to unregister the tunnel. Use buffered channels for error channels where receiver may quit early on context cancellation. - 2020-04-30 TUN-2940: Added delay parameter to stdin reconnect command. - 2020-04-27 TUN-2921: Rework address selection logic to avoid corner cases - 2020-04-28 TUN-2872: Exit with non-0 status code when the binary is updated so launchd will restart the service - 2020-04-13 AUTH-2587 add config watcher and reload logic for access client forwarder 2020.4.0 - 2020-04-10 TUN-2881: Parameterize response meta information header name in the generating function - 2020-04-11 TUN-2894: ResponseMetaHeader should be public - 2020-04-09 TUN-2880: Return metadata about source of the response from cloudflared - 2020-04-04 ARES-899: Fixes DoH client as system resolver. Fixes #91 - 2020-03-31 AUTH-2394 added socks5 proxy - 2020-02-24 AUTH-2235 GetTokenIfExists now parses JWT payload for json expiry field to detect if the cached access token is expired 2020.3.2 - 2020-03-31 TUN-2854: Quick Reconnects should be an optional supported feature - 2020-03-30 TUN-2850: Tunnel stripping Cloudflare headers 2020.3.1 - 2020-03-27 TUN-2846: Trigger debug reconnects from stdin commands, not SIGUSR1 2020.3.0 - 2020-03-23 AUTH-2394 fixed header for websockets. Added TCP alias - 2020-03-10 TUN-2797: Fix panic in SetConnDigest by making mutexes values. - 2020-03-13 TUN-2807: cloudflared hello-world shouldn't assume it's my first tunnel - 2020-03-13 TUN-2756: Set connection digest after reconnect. - 2020-03-16 TUN-2812: Tunnel proxies and RPCs can share an edge address - 2020-03-18 TUN-2816: cloudflared metrics server should be more discoverable - 2020-03-19 TUN-2820: Serialized headers for Websockets - 2020-03-19 TUN-2819: cloudflared should close its connections when a signal is sent - 2020-03-19 TUN-2823: Bugfix. cloudflared would hang forever if error occurred. - 2020-03-10 TUN-2796: Implement HTTP2 CONTINUATION headers correctly - 2020-03-02 TUN-2779: update sample HTML pages - 2020-03-04 TUN-2785: Use reconnect token by default - 2020-03-05 TUN-2754: Add ConnDigest to cloudflared and its RPCs - 2020-03-06 TUN-2755: ReconnectTunnel RPC now transmits ConnectionDigest - 2020-03-06 TUN-2761: Use the new header management functions in cloudflared - 2020-03-06 TUN-2788: cloudflared should store one ConnDigest per HA connection - 2020-02-26 TUN-2767: Test for large headers - 2020-02-28 do not terminate tunnel if origin is not reachable on start-up (#177) - 2020-02-28 TUN-2776: Add header serialization feature in cloudflared - 2020-02-21 TUN-2748: Insecure randomness vulnerability in github.com/miekg/dns 2020.2.1 - 2020-02-20 TUN-2745: Rename existing header management functions - 2020-02-21 TUN-2746: Add the new header management functions - 2020-02-25 perf(cloudflared): reuse memory from buffer pool to get better throughput (#161) - 2020-02-25 Tweak HTTP host header. Fixes #107 (#168) - 2020-02-25 TUN-2765: Add list of features to tunnelrpc - 2020-02-19 TUN-2725: Specify in code that --edge is for internal testing only - 2020-02-19 TUN-2703: Muxer.Serve terminates when its context is Done - 2020-02-09 TUN-2717: Function to serialize/deserialize HTTP headers - 2020-02-05 TUN-2714: New edge discovery. Connections try to reconnect to the same edge IP. 2020.2.0 - 2020-01-30 TUN-2651: Fix panic in h2mux reader when a stream error is encountered - 2020-01-27 TUN-2645: Revert "TUN-2645: Turn on reconnect tokens" - 2020-01-28 TUN-2693: Metrics for ReconnectTunnel - 2020-01-28 TUN-2696: Add unknown registerRPCName - 2020-01-28 TUN-2699: Metrics for Authenticate RPCs - 2020-01-28 TUN-2690: cloudflared reconnect uses wrong context - 2020-01-29 TUN-2707: Inconsistent cardinality in tunnel error metrics - 2020-01-13 TUN-2645: Turn on reconnect tokens - 2019-12-23 TUN-2646: Make --edge flag work again for local development 2019.12.0 - 2019-12-11 TUN-2631: only notify that activeStreamMap is closed if ignoreNewStreams=true - 2019-12-17 bug(cloudflared): Set the MaxIdleConnsPerHost of http.Transport to proxy-keepalive-connections (#155) - 2019-12-17 refactor(docker): optimize Dockerfile (#126) - 2019-12-19 Fix timer scheduling for systemd update service (#159) - 2019-12-13 TUN-2637: Manage edge IPs in a region-aware manner - 2019-12-03 bug(cloudflared): nil pointer deference on h2DictWriter Close() (#154) - 2019-12-03 TUN-2608: h2mux.Muxer.Shutdown always returns a non-nil channel - 2019-12-04 TUN-2555: origin/supervisor.go calls Authenticate - 2019-12-06 TUN-2554: cloudflared calls ReconnectTunnel - 2019-11-20 TUN-2575: Constructors + simpler conversions for AuthOutcome - 2019-11-22 Fix Docker build failure (#149) - 2019-11-21 TUN-2573: Refactor TunnelRegistration into PermanentRegistrationError, RetryableRegistrationError and SuccessfulTunnelRegistration - 2019-11-22 TUN-2582: EventDigest field in tunnelrpc - 2019-11-22 Fix "happy eyeballs" not being disabled since Golang 1.12 upgrade * The Dialer.DualStack setting is now ignored and deprecated; RFC 6555 Fast Fallback ("Happy Eyeballs") is now enabled by default. To disable, set Dialer.FallbackDelay to a negative value. - 2019-11-25 TUN-2591: ReconnectTunnel now sends EventDigest - 2019-11-21 TUN-2606: add DialEdge helpers - 2019-11-21 TUN-2607: add RPC stream helpers 2019.11.3 - 2019-11-20 TUN-2562: Update Cloudflare Origin CA RSA root 2019.11.2 - 2019-11-18 TUN-2567: AuthOutcome can be turned back into AuthResponse - 2019-11-18 TUN-2563: Exposes config_version metrics 2019.11.1 - 2019-11-12 Add db-connect, a SQL over HTTPS server - 2019-11-12 TUN-2053: Add a /healthcheck endpoint to the metrics server - 2019-11-13 TUN-2178: public API to create new h2mux.MuxedStreamRequest - 2019-11-13 TUN-2490: respect original representation of HTTP request path - 2019-11-18 TUN-2547: TunnelRPC definitions for Authenticate flow - 2019-11-18 TUN-2551: TunnelRPC definitions for ReconnectTunnel flow - 2019-11-05 TUN-2506: Expose active streams metrics 2019.11.0 - 2019-11-04 TUN-2502: Switch to go modules - 2019-11-04 TUN-2500: Don't send client registration errors to Sentry - 2019-11-04 TUN-2489: Delete stream from activestreammap when read and write are both closed - 2019-11-05 TUN-2505: Terminate stream on receipt of RST_STREAM; MuxedStream.CloseWrite() should terminate the MuxedStream.Write() loop - 2019-10-30 TUN-2451: Log inavlid path - 2019-10-22 TUN-2425: Enable cloudflared to serve multiple Hello World servers by having each of them create its own ServeMux - 2019-10-22 AUTH-2173: Prepends access login url with scheme if one doesnt exist - 2019-10-23 TUN-2460: Configure according to the ClientConfig recevied from a successful Connect - 2019-10-23 AUTH-2177: Reads and writes error streams 2019.10.4 - 2019-10-21 TUN-2450: Remove Brew publishing formula 2019.10.3 - 2019-10-18 Fix #129: Excessive memory usage streaming large files (#142) 2019.10.2 - 2019-10-17 AUTH-2167: Adds CLI option for host key directory 2019.10.1 - 2019-10-17 Adds variable to fix windows build 2019.10.0 - 2019-10-11 AUTH-2105: Dont require --destination arg - 2019-10-14 TUN-2344: log more details: http2.Framer.ErrorDetail() if available, connectionID - 2019-10-16 AUTH-2159: Moves shutdownC close into error handling AUTH-2161: Lowers size of preamble length AUTH-2160: Fixes url parsing logic - 2019-10-16 AUTH-2135: Adds support for IPv6 and tests - 2019-10-02 AUTH-2105: Adds support for local forwarding. Refactor auditlogger creation. AUTH-2088: Adds dynamic destination routing - 2019-10-09 AUTH-2114: Uses short lived cert auth for outgoing client connection - 2019-09-30 AUTH-2089: Revise ssh server to function as a proxy 2019.9.2 - 2019-09-26 TUN-2355: Roll back TUN-2276 2019.9.1 - 2019-09-23 TUN-2334: remove tlsConfig.ServerName special case - 2019-09-23 AUTH-2077: Quotes open browser command in windows - 2019-09-11 AUTH-2050: Adds time.sleep to temporarily avoid hitting tunnel muxer dealock issue - 2019-09-10 AUTH-2056: Writes stderr to its own stream for non-pty connections - 2019-09-16 TUN-2307: Capnp is the only serialization format used in tunnelpogs - 2019-09-18 TUN-2315: Replace Scope with IntentLabel - 2019-09-17 TUN-2309: Split ConnectResult into ConnectError and ConnectSuccess, each implementing its own capnp serialization logic - 2019-09-18 AUTH-2052: Adds tests for SSH server - 2019-09-18 AUTH-2067: Log commands correctly - 2019-09-19 AUTH-2055: Verifies token at edge on access login - 2019-09-04 TUN-2201: change SRV records used by cloudflared - 2019-09-06 TUN-2280: Revert "TUN-2260: add name/group to CapnpConnectParameters, remove Scope" - 2019-09-03 AUTH-1943 hooked up uploader to logger, added timestamp to session logs, add tests - 2019-09-04 AUTH-2036: Refactor user retrieval, shutdown after ssh server stops, add custom version string - 2019-09-06 AUTH-1942 added event log to ssh server - 2019-09-04 AUTH-2037: Adds support for ssh port forwarding - 2019-09-05 TUN-2276: Path encoding broken 2019.9.0 - 2019-09-05 TUN-2279: Revert path encoding fix - 2019-08-30 AUTH-2021 - check error for failing tests - 2019-08-29 AUTH-2030: Support both authorized_key and short lived cert authentication simultaniously without specifiying at start time - 2019-08-29 AUTH-2026: Adds support for non-pty sessions and inline command exec - 2019-08-26 AUTH-1943: Adds session logging - 2019-08-26 TUN-2162: Decomplect OpenStream to allow finer-grained timeouts - 2019-08-29 TUN-2260: add name/group to CapnpConnectParameters, remove Scope 2019.8.4 - 2019-08-30 Fix #111: Add support for specifying a specific HTTP Host: header on the origin. (#114) - 2019-08-22 TUN-2165: Add ClientConfig to tunnelrpc.ConnectResult - 2019-08-20 AUTH-2014: Checks users login shell - 2019-08-26 TUN-2243: Revert "STOR-519: Add db-connect, a SQL over HTTPS server" - 2019-08-27 TUN-2244: Add NO_AUTOUPDATE env var - 2019-08-22 AUTH-2018: Adds support for authorized keys and short lived certs - 2019-08-28 AUTH-2022: Adds ssh timeout configuration - 2019-08-28 TUN-1968: Gracefully diff StreamHandler.UpdateConfig - 2019-08-26 AUTH-2021 - s3 bucket uploading for SSH logs - 2019-08-19 AUTH-2004: Adds static host key support - 2019-07-18 AUTH-1941: Adds initial SSH server implementation 2019.8.3 - 2019-08-20 STOR-519: Add db-connect, a SQL over HTTPS server - 2019-08-20 Release 2019.8.2 - 2019-08-20 Revert "AUTH-1941: Adds initial SSH server implementation" - 2019-08-11 TUN-2163: Add GrapQLType method to Scope interface - 2019-08-06 TUN-2152: Requests with a query in the URL are erroneously escaped - 2019-07-18 AUTH-1941: Adds initial SSH server implementation 2019.8.1 - 2019-08-05 TUN-2111: Implement custom serialization logic for FallibleConfig and OriginConfig - 2019-08-06 Revert "TUN-1736: Missing headers when passing an invalid path" 2019.8.0 - 2019-07-11 TUN-1956: Go 1.12 update - 2019-07-24 TUN-1736: Missing headers when passing an invalid path - 2019-07-30 TUN-2117: read group/system-name from CLI, send it to edge - 2019-08-02 TUN-2125: Add PostgresType() to Scope - 2019-08-05 TUN-2147: Implemented ScopeUnmarshaler - 2019-07-31 TUN-2110: Implement custom deserialization logic for OriginConfig - 2019-07-31 AUTH-1972: Deletes token lock file if backoff retry attempts exceeded and intercepts signals until lock is released 2019.7.0 - 2019-05-28 TUN-1913: Define OriginService for each type of origin - 2019-04-29 Build a docker container - 2019-06-12 TUN-1952: Group ClientConfig fields by the component that uses the config, and return the part of the config that failed to be applied - 2019-06-05 TUN-1893: Proxy requests to the origin based on tunnel hostname - 2019-06-17 TUN-1961: Create EdgeConnectionManager to maintain outbound connections to the edge - 2019-06-18 TUN-1885: Reconfigure cloudflared on receiving new ClientConfig - 2019-06-19 TUN-1976: Pass tunnel hostname through header - 2019-06-20 TUN-1982: Load custom origin CA when OriginCAPool is specified - 2019-06-26 TUN-2005: Upgrade logrus - 2019-06-20 TUN-1981: Write response header & body on proxy error to notify eyeballs of failure category - 2019-06-20 TUN-1977: Validate OriginConfig has valid URL, and use scheme to determine if a HTTPOriginService is expecting HTTP or Unix - 2019-06-13 DoH: change the media type to application/dns-message - 2019-06-26 AUTH-1736: Better handling of token revocation 2019.6.0 - 2019-05-17 TUN-1828: Update declarative tunnel config struct - 2019-05-29 Handle exit code on err - 2019-05-29 AUTH-1802: Fixed ssh-config templating - 2019-05-30 TUN-1914: Conflate HTTP and Unix OriginConfig, and add TLS config to WebSocketOriginConfig - 2019-06-03 AUTH-1811: ssh-gen config fixes 2019.5.0 - 2019-04-25 TUN-1781: ServeStream should return early on error - 2019-04-30 TUN-1786: Remove low-level Windows service logging - 2019-05-03 TUN-1807: Send cloudflared version in Connect RPC - 2019-01-23 AUTH-1557: Short Lived Certs - 2019-05-13 TUN-1847: Log a distinct message when OpenStream fails while waiting for response headers - 2019-05-13 AUTH-1706: fixes and testing - 2019-05-22 TUN-1880: Save debug and warn level log to logfile - 2019-05-22 AUTH-1781: fixed race condition for short lived certs, doc required config 2019.4.1 - 2019-03-18 TUN-1626: Create new supervisor to establish connection with origintunneld - 2019-04-04 TUN-1619: Add flag to test declarative tunnels. - 2019-04-05 TUN-1577: decompose carrier.StartServer to make TestStartServer less flappy - 2019-03-29 TUN-1606: Define CloudflaredConfig RPC structure, interface for cloudflared's RPC server - 2019-04-02 TUN-1682: Add context to OpenStream to prevent it from blocking indefinitely. - 2019-04-16 TUN-1732: cloudflared metrics should track userHostnames - 2019-04-17 TUN-1734: Pin packages at exact versions - 2019-04-18 TUN-1669: Update license message in help text. Also fix test 2019.4.0 - 2019-03-28 TUN-1648: ConnectionID is now a UUID - 2019-04-01 TUN-1673: Conflate Hello and Connect RPCs 2019.3.2 - 2019-03-22 TUN-1637: Free tunnels shouldn't require cert.pem - 2019-03-18 TUN-1604: Define Connect RPC call 2019.3.1 - 2019-03-09 Add rdp as a supported protocol in URL validation (#76) - 2019-03-15 TUN-1613: improved cloudflared RegisterTunnel fail metrics - 2019-03-17 TUN-1615: revert miekg/dns to last known working revision 2019.3.0 - 2018-12-28 make http transport aware of proxy from envvar - 2019-02-28 TUN-1559: fix nil dereference in TunnelConfig.CloseConnOnce - 2019-03-04 TUN-1451: Make non-interactive, non-service execution possible on Windows - 2019-03-04 TUN-1562: Refactor connectedSignal to be safe to close multiple times - 2019-02-27 TUN-1550: Add validation timeout for non-responsive origins - 2019-03-06 AUTH-1531: Named flags for ssh service tokens - 2019-02-14 Support unix sockets. - 2019-03-08 TUN-1389: Non-scalar flags in a cloudflared config.yml don't get logged - 2019-03-07 TUN-1522: If we can't get SRV from default resolver, get them from 1.1.1.1 DoT 2019.2.1 - 2019-02-14 TUN-1381: should tell you if you're on the latest version rather than just exiting silently - 2019-02-15 TUN-1467: build with Go 1.11 - 2019-02-15 AUTH-1519: Added logging - 2019-02-19 TUN-1525: cloudflared metrics for registration success/fail - 2019-02-19 TUN-1510: Wrap the close() in sync.Once.Do 2019.2.0 - 2019-01-24 AUTH-1462: better curl arg parsing - 2019-02-01 TUN-1456: Only make one UUID - 2019-01-30 cloudflared/linux_service: Add missing /etc/init.d shebang - 2019-02-07 AUTH-1511: Add custom headers for ssh command - 2019-02-01 AUTH-1503: Added RDP support - 2019-02-01 AUTH-1403: Print the paths in the ssh-config instructions 2019.1.0 - 2018-12-10 TUN-1231: Horizontal overflow wrapping on the Hello page - 2018-12-17 TUN-1140: Show usage if invoked with no args or config - 2018-11-06 TUN-632 Filter out common network exceptions from going to Sentry on StartServer - 2019-01-07 TUN-1138: Install cloudflared service directory with 755 permissions - 2019-01-07 TUN-1265: Silent exit when failing to parse config - 2019-01-10 TUN-1350: Enhance error messages with cloudflarestatus.com link, if relevant - 2019-01-16 TUN-1358: Close readyList after Muxer.Serve() has stopped running - 2019-01-24 AUTH-1423: move from stdout to stderr - 2019-01-24 AUTH-1404: reauth if the token is about to expire within 15 minutes - 2019-01-24 AUTH-1459: improved ssh streaming error message - 2019-01-24 AUTH-1211: print all the versions - 2019-01-24 AUTH-1337: fix url path - 2019-01-28 TUN-1418: Rename ProtocolLogger to TransportLogger, and use TransportLogger to log RPC events. - 2019-01-28 TUN-1419: Identify request/response headers/content length with ray ID 2018.12.1 - 2018-12-11 TUN-1270: cloudflared panic (HA metrics missing label) 2018.12.0 - 2018-11-15 TUN-1196: Allow TLS config client CA and root CA to be constructed from multiple certificates - 2018-11-20 TUN-1209: TLS Config Certificates and GetCertificate can both be set - 2018-11-26 TUN-1212: Expose tunnel_id in metrics - 2018-11-30 TUN-1204: remove 'cloudflared hello' command - 2018-12-04 Fix license URL typo - 2018-12-07 TUN-1250: ValidateHTTPService shouldn't follow 302s 2018.11.0 - 2018-10-31 AUTH-1282: Fixed an issue where we were receiving as opposed sending on the channel. - 2018-11-06 TUN-1179: Fix log message in cmd/cloudflared/transfer.Run - 2018-11-13 AUTH-1308: get jwt even when you are already logged in - 2018-11-12 TUN-1190: check URL parse error when starting SSH proxy server - 2018-11-15 AUTH-1320: Fixed request issue and unhide the ssh command 2018.10.5 - 2018-10-18 TUN-968: Flow control for large requests/responses - 2018-10-26 TUN-1158: Windows: use process arguments rather than trivial service arguments - 2018-10-20 #30: Fix the Content-Length header for HTTP2->HTTP1 - 2018-10-29 TUN-1160: pass Host header during origin url validation 2018.10.4 - 2018-09-21 AUTH-1070: added SSH/protocol forwarding - 2018-10-19 AUTH-1235: fixed packaging of deb dev file - 2018-10-19 TUN-1097: Host missing from WebSocket request - 2018-10-19 AUTH-1188: UX Review and Changes for CLI SSH Access 2018.10.3 - 2018-10-08 TUN-1099: Bring back changes in 2018.10.1 - 2018-10-08 TUN-1098: removed deprecation error - 2018-10-08 TUN-1101: False negatives in Cloudflared error reporting 2018.10.2 - 2018-10-06 TUN-1093: Revert cloudflared to 2018.8.0 2018.10.1 - 2018-10-03 TUN-1012: Normalize config filename for Linux services - 2018-10-05 TUN-1081: cloudflared now generates UUIDs - 2018-10-05 TUN-1083: fixed incorrect help menu - 2018-10-05 TUN-1086: fixed config option 2018.10.0 - 2018-08-15 AUTH-910, AUTH-1049, AUTH-1068, AUTH-1056: Generate and store Access tokens with E2EE option, curl/cmd wrapper - 2018-09-11 TUN-890: To support free tunnels, hostname can now be "" - 2018-09-12 TUN-810: Cloudflared should open dash/argotunnel not dash/warp - 2018-09-12 TUN-985: Don't display tunnel ID if it's empty string - 2018-09-11 TUN-881: Display trial zone URL upon successful registration - 2018-09-11 TUN-868: HTTP/HTTPS mismatch should have a better error message - 2018-09-19 TUN-1028: Unhide cloudflared compression flag - 2018-09-20 AUTH-1139: refactored cloudflared help menu - 2018-09-20 TUN-1035: New text for cloudflared tunnel --help - 2018-09-18 AUTH-1136: addressing beta feedback - 2018-09-26 AUTH-1165: hide access command - 2018-09-26 TUN-1046: Document that delta compression is a beta feature - 2018-09-28 TUN-1056: Lint error broke build - 2018-09-27 TUN-1052: Origintunneld can send back an Origincert to Cloudflared - 2018-09-28 TUN-1052: Changing type of OriginCert to :Data - 2018-10-01 TUN-1062: Makefile target for regenerating Capn Proto definitions - 2018-10-02 TUN-1064: Revert OriginCert capnp changes in Cloudflared. Reverts commits a1ee2342e97 and 8c756c45785. - 2018-10-03 TUN-1076: Pin capnproto2 to version 2.17.1 - 2018-10-03 AUTH-1199: unhide access command, added beta label 2018.8.0 - 2018-05-01 Initial commit - 2018-05-03 TUN-595: Add License/Readme files to cloudflared - 2018-05-01 TUN-528: Move cloudflared into a separate repo - 2018-07-24 TUN-813: Clean up cloudflared dependencies - 2018-07-25 TUN-814: Handle error in CreateTLSListener before closing listener - 2018-07-24 TUN-804: create Makefile recipe to build cloudflared and run tests - 2018-07-26 TUN-817: Increase the log time precision - 2018-07-30 TUN-828: Added Connection: keep-alive header - 2018-07-30 TUN-829: prefer p256 curve - 2018-07-31 TUN-834: Enable tracing on cloudflared - 2018-08-07 TUN-820: Fix caddyfile gitignore - 2018-07-25 TUN-804: create make recipe for building deb package - 2018-08-07 TUN-861: Disable cloudflared tracing by default; preserve the latest tracefile - 2018-08-07 TUN-857: Pull the brotli-go dependency from Github - 2018-08-14 TUN-897: Bring back missing Brotli files - 2018-07-26 TUN-804: create makefile recipe to release cloudflared using equinox - 2018-08-15 TUN-901: makefile target for homebrew release - 2018-07-30 TUN-801: Rapid SQL Proxy - 2018-08-27 TUN-833: Don't log system root certificate loading failure on Windows ================================================ FILE: carrier/carrier.go ================================================ // Package carrier provides a WebSocket proxy to carry or proxy a connection // from the local client to the edge. See it as a wrapper around any protocol // that it packages up in a WebSocket connection to the edge. package carrier import ( "crypto/tls" "fmt" "io" "net" "net/http" "net/url" "os" "strings" "github.com/pkg/errors" "github.com/rs/zerolog" "github.com/cloudflare/cloudflared/token" ) const ( LogFieldOriginURL = "originURL" CFAccessTokenHeader = "Cf-Access-Token" cfJumpDestinationHeader = "Cf-Access-Jump-Destination" ) type StartOptions struct { AppInfo *token.AppInfo OriginURL string Headers http.Header Host string TLSClientConfig *tls.Config AutoCloseInterstitial bool IsFedramp bool } // Connection wraps up all the needed functions to forward over the tunnel type Connection interface { // ServeStream is used to forward data from the client to the edge ServeStream(*StartOptions, io.ReadWriter) error } // StdinoutStream is empty struct for wrapping stdin/stdout // into a single ReadWriter type StdinoutStream struct{} // Read will read from Stdin func (c *StdinoutStream) Read(p []byte) (int, error) { return os.Stdin.Read(p) } // Write will write to Stdout func (c *StdinoutStream) Write(p []byte) (int, error) { return os.Stdout.Write(p) } // Helper to allow deferring the response close with a check that the resp is not nil func closeRespBody(resp *http.Response) { if resp != nil { _ = resp.Body.Close() } } // StartForwarder will setup a listener on a specified address/port and then // forward connections to the origin by calling `Serve()`. func StartForwarder(conn Connection, address string, shutdownC <-chan struct{}, options *StartOptions) error { listener, err := net.Listen("tcp", address) if err != nil { return errors.Wrap(err, "failed to start forwarding server") } return Serve(conn, listener, shutdownC, options) } // StartClient will copy the data from stdin/stdout over a WebSocket connection // to the edge (originURL) func StartClient(conn Connection, stream io.ReadWriter, options *StartOptions) error { return conn.ServeStream(options, stream) } // Serve accepts incoming connections on the specified net.Listener. // Each connection is handled in a new goroutine: its data is copied over a // WebSocket connection to the edge (originURL). // `Serve` always closes `listener`. func Serve(remoteConn Connection, listener net.Listener, shutdownC <-chan struct{}, options *StartOptions) error { defer listener.Close() errChan := make(chan error) go func() { for { conn, err := listener.Accept() if err != nil { // don't block if parent goroutine quit early select { case errChan <- err: default: } return } go serveConnection(remoteConn, conn, options) } }() select { case <-shutdownC: return nil case err := <-errChan: return err } } // serveConnection handles connections for the Serve() call func serveConnection(remoteConn Connection, c net.Conn, options *StartOptions) { defer c.Close() _ = remoteConn.ServeStream(options, c) } // IsAccessResponse checks the http Response to see if the url location // contains the Access structure. func IsAccessResponse(resp *http.Response) bool { if resp == nil || resp.StatusCode != http.StatusFound { return false } location, err := resp.Location() if err != nil || location == nil { return false } if strings.HasPrefix(location.Path, token.AccessLoginWorkerPath) { return true } return false } // BuildAccessRequest builds an HTTP request with the Access token set func BuildAccessRequest(options *StartOptions, log *zerolog.Logger) (*http.Request, error) { req, err := http.NewRequest(http.MethodGet, options.OriginURL, nil) if err != nil { return nil, err } token, err := token.FetchTokenWithRedirect(req.URL, options.AppInfo, options.AutoCloseInterstitial, options.IsFedramp, log) if err != nil { return nil, err } // We need to create a new request as FetchToken will modify req (boo mutable) // as it has to follow redirect on the API and such, so here we init a new one originRequest, err := http.NewRequest(http.MethodGet, options.OriginURL, nil) if err != nil { return nil, err } originRequest.Header.Set(CFAccessTokenHeader, token) for k, v := range options.Headers { if len(v) >= 1 { originRequest.Header.Set(k, v[0]) } } return originRequest, nil } func SetBastionDest(header http.Header, destination string) { if destination != "" { header.Set(cfJumpDestinationHeader, destination) } } func ResolveBastionDest(r *http.Request) (string, error) { jumpDestination := r.Header.Get(cfJumpDestinationHeader) if jumpDestination == "" { return "", fmt.Errorf("Did not receive final destination from client. The --destination flag is likely not set on the client side") } // Strip scheme and path set by client. Without a scheme // Parsing a hostname and path without scheme might not return an error due to parsing ambiguities if jumpURL, err := url.Parse(jumpDestination); err == nil && jumpURL.Host != "" { return removePath(jumpURL.Host), nil } return removePath(jumpDestination), nil } func removePath(dest string) string { return strings.SplitN(dest, "/", 2)[0] } ================================================ FILE: carrier/carrier_test.go ================================================ package carrier import ( "bytes" "io" "net" "net/http" "net/http/httptest" "sync" "testing" ws "github.com/gorilla/websocket" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" ) const ( // example in Sec-Websocket-Key in rfc6455 testSecWebsocketKey = "dGhlIHNhbXBsZSBub25jZQ==" ) type testStreamer struct { buf *bytes.Buffer l sync.RWMutex } func newTestStream() *testStreamer { return &testStreamer{buf: new(bytes.Buffer)} } func (s *testStreamer) Read(p []byte) (int, error) { s.l.RLock() defer s.l.RUnlock() return s.buf.Read(p) } func (s *testStreamer) Write(p []byte) (int, error) { s.l.Lock() defer s.l.Unlock() return s.buf.Write(p) } func TestStartClient(t *testing.T) { message := "Good morning Austin! Time for another sunny day in the great state of Texas." log := zerolog.Nop() wsConn := NewWSConnection(&log) ts := newTestWebSocketServer() defer ts.Close() buf := newTestStream() options := &StartOptions{ OriginURL: "http://" + ts.Listener.Addr().String(), Headers: nil, } err := StartClient(wsConn, buf, options) assert.NoError(t, err) _, _ = buf.Write([]byte(message)) readBuffer := make([]byte, len(message)) _, _ = buf.Read(readBuffer) assert.Equal(t, message, string(readBuffer)) } func TestStartServer(t *testing.T) { listener, err := net.Listen("tcp", "localhost:") if err != nil { t.Fatalf("Error starting listener: %v", err) } message := "Good morning Austin! Time for another sunny day in the great state of Texas." log := zerolog.Nop() shutdownC := make(chan struct{}) wsConn := NewWSConnection(&log) ts := newTestWebSocketServer() defer ts.Close() options := &StartOptions{ OriginURL: "http://" + ts.Listener.Addr().String(), Headers: nil, } go func() { err := Serve(wsConn, listener, shutdownC, options) if err != nil { t.Errorf("Error running server: %v", err) return } }() conn, err := net.Dial("tcp", listener.Addr().String()) _, _ = conn.Write([]byte(message)) readBuffer := make([]byte, len(message)) _, _ = conn.Read(readBuffer) assert.Equal(t, string(readBuffer), message) } func TestIsAccessResponse(t *testing.T) { validLocationHeader := http.Header{} validLocationHeader.Add("location", "https://test.cloudflareaccess.com/cdn-cgi/access/login/blahblah") invalidLocationHeader := http.Header{} invalidLocationHeader.Add("location", "https://google.com") testCases := []struct { Description string In *http.Response ExpectedOut bool }{ {"nil response", nil, false}, {"redirect with no location", &http.Response{StatusCode: http.StatusFound}, false}, {"200 ok", &http.Response{StatusCode: http.StatusOK}, false}, {"redirect with location", &http.Response{StatusCode: http.StatusFound, Header: validLocationHeader}, true}, {"redirect with invalid location", &http.Response{StatusCode: http.StatusFound, Header: invalidLocationHeader}, false}, } for i, tc := range testCases { if IsAccessResponse(tc.In) != tc.ExpectedOut { t.Fatalf("Failed case %d -- %s", i, tc.Description) } } } func newTestWebSocketServer() *httptest.Server { upgrader := ws.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, } return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { conn, _ := upgrader.Upgrade(w, r, nil) defer conn.Close() for { mt, message, err := conn.ReadMessage() if err != nil { break } if err := conn.WriteMessage(mt, []byte(message)); err != nil { break } } })) } func testRequest(t *testing.T, url string, stream io.ReadWriter) *http.Request { req, err := http.NewRequest("GET", url, stream) if err != nil { t.Fatalf("testRequestHeader error") } req.Header.Add("Connection", "Upgrade") req.Header.Add("Upgrade", "WebSocket") req.Header.Add("Sec-Websocket-Key", testSecWebsocketKey) req.Header.Add("Sec-Websocket-Protocol", "tunnel-protocol") req.Header.Add("Sec-Websocket-Version", "13") req.Header.Add("User-Agent", "curl/7.59.0") return req } func TestBastionDestination(t *testing.T) { tests := []struct { name string header http.Header expectedDest string wantErr bool }{ { name: "hostname destination", header: http.Header{ cfJumpDestinationHeader: []string{"localhost"}, }, expectedDest: "localhost", }, { name: "hostname destination with port", header: http.Header{ cfJumpDestinationHeader: []string{"localhost:9000"}, }, expectedDest: "localhost:9000", }, { name: "hostname destination with scheme and port", header: http.Header{ cfJumpDestinationHeader: []string{"ssh://localhost:9000"}, }, expectedDest: "localhost:9000", }, { name: "full hostname url", header: http.Header{ cfJumpDestinationHeader: []string{"ssh://localhost:9000/metrics"}, }, expectedDest: "localhost:9000", }, { name: "hostname destination with port and path", header: http.Header{ cfJumpDestinationHeader: []string{"localhost:9000/metrics"}, }, expectedDest: "localhost:9000", }, { name: "ip destination", header: http.Header{ cfJumpDestinationHeader: []string{"127.0.0.1"}, }, expectedDest: "127.0.0.1", }, { name: "ip destination with port", header: http.Header{ cfJumpDestinationHeader: []string{"127.0.0.1:9000"}, }, expectedDest: "127.0.0.1:9000", }, { name: "ip destination with port and path", header: http.Header{ cfJumpDestinationHeader: []string{"127.0.0.1:9000/metrics"}, }, expectedDest: "127.0.0.1:9000", }, { name: "ip destination with schem and port", header: http.Header{ cfJumpDestinationHeader: []string{"tcp://127.0.0.1:9000"}, }, expectedDest: "127.0.0.1:9000", }, { name: "full ip url", header: http.Header{ cfJumpDestinationHeader: []string{"ssh://127.0.0.1:9000/metrics"}, }, expectedDest: "127.0.0.1:9000", }, { name: "no destination", wantErr: true, }, } for _, test := range tests { r := &http.Request{ Header: test.header, } dest, err := ResolveBastionDest(r) if test.wantErr { assert.Error(t, err, "Test %s expects error", test.name) } else { assert.NoError(t, err, "Test %s expects no error, got error %v", test.name, err) assert.Equal(t, test.expectedDest, dest, "Test %s expect dest %s, got %s", test.name, test.expectedDest, dest) } } } ================================================ FILE: carrier/websocket.go ================================================ package carrier import ( "io" "net/http" "net/http/httputil" "net/url" "github.com/gorilla/websocket" "github.com/rs/zerolog" "github.com/cloudflare/cloudflared/stream" "github.com/cloudflare/cloudflared/token" cfwebsocket "github.com/cloudflare/cloudflared/websocket" ) // Websocket is used to carry data via WS binary frames over the tunnel from client to the origin // This implements the functions for glider proxy (sock5) and the carrier interface type Websocket struct { log *zerolog.Logger isSocks bool } // NewWSConnection returns a new connection object func NewWSConnection(log *zerolog.Logger) Connection { return &Websocket{ log: log, } } // ServeStream will create a Websocket client stream connection to the edge // it blocks and writes the raw data from conn over the tunnel func (ws *Websocket) ServeStream(options *StartOptions, conn io.ReadWriter) error { wsConn, err := createWebsocketStream(options, ws.log) if err != nil { ws.log.Err(err).Str(LogFieldOriginURL, options.OriginURL).Msg("failed to connect to origin") return err } defer wsConn.Close() stream.Pipe(wsConn, conn, ws.log) return nil } // createWebsocketStream will create a WebSocket connection to stream data over // It also handles redirects from Access and will present that flow if // the token is not present on the request func createWebsocketStream(options *StartOptions, log *zerolog.Logger) (*cfwebsocket.GorillaConn, error) { req, err := http.NewRequest(http.MethodGet, options.OriginURL, nil) if err != nil { return nil, err } req.Header = options.Headers if options.Host != "" { req.Host = options.Host } dump, err := httputil.DumpRequest(req, false) if err != nil { return nil, err } log.Debug().Msgf("Websocket request: %s", string(dump)) dialer := &websocket.Dialer{ TLSClientConfig: options.TLSClientConfig, Proxy: http.ProxyFromEnvironment, } wsConn, resp, err := clientConnect(req, dialer) defer closeRespBody(resp) if err != nil && IsAccessResponse(resp) { // Only get Access app info if we know the origin is protected by Access originReq, err := http.NewRequest(http.MethodGet, options.OriginURL, nil) if err != nil { return nil, err } appInfo, err := token.GetAppInfo(originReq.URL) if err != nil { return nil, err } options.AppInfo = appInfo wsConn, err = createAccessAuthenticatedStream(options, log) if err != nil { return nil, err } } else if err != nil { return nil, err } return &cfwebsocket.GorillaConn{Conn: wsConn}, nil } var stripWebsocketHeaders = []string{ "Upgrade", "Connection", "Sec-Websocket-Key", "Sec-Websocket-Version", "Sec-Websocket-Extensions", } // the gorilla websocket library sets its own Upgrade, Connection, Sec-WebSocket-Key, // Sec-WebSocket-Version and Sec-Websocket-Extensions headers. // https://github.com/gorilla/websocket/blob/master/client.go#L189-L194. func websocketHeaders(req *http.Request) http.Header { wsHeaders := make(http.Header) for key, val := range req.Header { wsHeaders[key] = val } // Assume the header keys are in canonical format. for _, header := range stripWebsocketHeaders { wsHeaders.Del(header) } wsHeaders.Set("Host", req.Host) // See TUN-1097 return wsHeaders } // clientConnect creates a WebSocket client connection for provided request. Caller is responsible for closing // the connection. The response body may not contain the entire response and does // not need to be closed by the application. func clientConnect(req *http.Request, dialler *websocket.Dialer) (*websocket.Conn, *http.Response, error) { req.URL.Scheme = changeRequestScheme(req.URL) wsHeaders := websocketHeaders(req) if dialler == nil { dialler = &websocket.Dialer{ Proxy: http.ProxyFromEnvironment, } } conn, response, err := dialler.Dial(req.URL.String(), wsHeaders) if err != nil { return nil, response, err } return conn, response, nil } // changeRequestScheme is needed as the gorilla websocket library requires the ws scheme. // (even though it changes it back to http/https, but ¯\_(ツ)_/¯.) func changeRequestScheme(reqURL *url.URL) string { switch reqURL.Scheme { case "https": return "wss" case "http": return "ws" case "": return "ws" default: return reqURL.Scheme } } // createAccessAuthenticatedStream will try load a token from storage and make // a connection with the token set on the request. If it still get redirect, // this probably means the token in storage is invalid (expired/revoked). If that // happens it deletes the token and runs the connection again, so the user can // login again and generate a new one. func createAccessAuthenticatedStream(options *StartOptions, log *zerolog.Logger) (*websocket.Conn, error) { wsConn, resp, err := createAccessWebSocketStream(options, log) defer closeRespBody(resp) if err == nil { return wsConn, nil } if !IsAccessResponse(resp) { return nil, err } // Access Token is invalid for some reason. Go through regen flow if err := token.RemoveTokenIfExists(options.AppInfo); err != nil { return nil, err } wsConn, resp, err = createAccessWebSocketStream(options, log) defer closeRespBody(resp) if err != nil { return nil, err } return wsConn, nil } // createAccessWebSocketStream builds an Access request and makes a connection func createAccessWebSocketStream(options *StartOptions, log *zerolog.Logger) (*websocket.Conn, *http.Response, error) { req, err := BuildAccessRequest(options, log) if err != nil { return nil, nil, err } dump, err := httputil.DumpRequest(req, false) if err != nil { return nil, nil, err } log.Debug().Msgf("Access Websocket request: %s", string(dump)) conn, resp, err := clientConnect(req, nil) if resp != nil { r, err := httputil.DumpResponse(resp, true) if r != nil { log.Debug().Msgf("Websocket response: %q", r) } else if err != nil { log.Debug().Msgf("Websocket response error: %v", err) } } return conn, resp, err } ================================================ FILE: carrier/websocket_test.go ================================================ package carrier import ( "context" "crypto/tls" "crypto/x509" "fmt" "math/rand" "testing" "time" gws "github.com/gorilla/websocket" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/net/websocket" "github.com/cloudflare/cloudflared/hello" "github.com/cloudflare/cloudflared/tlsconfig" cfwebsocket "github.com/cloudflare/cloudflared/websocket" ) func websocketClientTLSConfig(t *testing.T) *tls.Config { certPool := x509.NewCertPool() helloCert, err := tlsconfig.GetHelloCertificateX509() assert.NoError(t, err) certPool.AddCert(helloCert) assert.NotNil(t, certPool) return &tls.Config{RootCAs: certPool} } func TestWebsocketHeaders(t *testing.T) { req := testRequest(t, "http://example.com", nil) wsHeaders := websocketHeaders(req) for _, header := range stripWebsocketHeaders { assert.Empty(t, wsHeaders[header]) } assert.Equal(t, "curl/7.59.0", wsHeaders.Get("User-Agent")) } func TestServe(t *testing.T) { log := zerolog.Nop() shutdownC := make(chan struct{}) errC := make(chan error) listener, err := hello.CreateTLSListener("localhost:1111") assert.NoError(t, err) defer listener.Close() go func() { errC <- hello.StartHelloWorldServer(&log, listener, shutdownC) }() req := testRequest(t, "https://localhost:1111/ws", nil) tlsConfig := websocketClientTLSConfig(t) assert.NotNil(t, tlsConfig) d := gws.Dialer{TLSClientConfig: tlsConfig} conn, resp, err := clientConnect(req, &d) assert.NoError(t, err) assert.Equal(t, "websocket", resp.Header.Get("Upgrade")) for i := 0; i < 1000; i++ { messageSize := rand.Int()%2048 + 1 clientMessage := make([]byte, messageSize) // rand.Read always returns len(clientMessage) and a nil error rand.Read(clientMessage) err = conn.WriteMessage(websocket.BinaryFrame, clientMessage) assert.NoError(t, err) messageType, message, err := conn.ReadMessage() assert.NoError(t, err) assert.Equal(t, websocket.BinaryFrame, messageType) assert.Equal(t, clientMessage, message) } _ = conn.Close() close(shutdownC) <-errC } func TestWebsocketWrapper(t *testing.T) { listener, err := hello.CreateTLSListener("localhost:0") require.NoError(t, err) serverErrorChan := make(chan error) helloSvrCtx, cancelHelloSvr := context.WithCancel(context.Background()) defer func() { <-serverErrorChan }() defer cancelHelloSvr() go func() { log := zerolog.Nop() serverErrorChan <- hello.StartHelloWorldServer(&log, listener, helloSvrCtx.Done()) }() tlsConfig := websocketClientTLSConfig(t) d := gws.Dialer{TLSClientConfig: tlsConfig, HandshakeTimeout: time.Minute} testAddr := fmt.Sprintf("https://%s/ws", listener.Addr().String()) req := testRequest(t, testAddr, nil) conn, resp, err := clientConnect(req, &d) require.NoError(t, err) assert.Equal(t, "websocket", resp.Header.Get("Upgrade")) // Websocket now connected to test server so lets check our wrapper wrapper := cfwebsocket.GorillaConn{Conn: conn} buf := make([]byte, 100) wrapper.Write([]byte("abc")) n, err := wrapper.Read(buf) require.NoError(t, err) require.Equal(t, n, 3) require.Equal(t, "abc", string(buf[:n])) // Test partial read, read 1 of 3 bytes in one read and the other 2 in another read wrapper.Write([]byte("abc")) buf = buf[:1] n, err = wrapper.Read(buf) require.NoError(t, err) require.Equal(t, n, 1) require.Equal(t, "a", string(buf[:n])) buf = buf[:cap(buf)] n, err = wrapper.Read(buf) require.NoError(t, err) require.Equal(t, n, 2) require.Equal(t, "bc", string(buf[:n])) } ================================================ FILE: catalog-info.yaml ================================================ apiVersion: backstage.io/v1alpha1 kind: Component metadata: name: cloudflared description: Client for Cloudflare Tunnels annotations: cloudflare.com/software-excellence-opt-in: "true" cloudflare.com/jira-project-key: "TUN" cloudflare.com/jira-project-component: "Cloudflare Tunnel" tags: - internal spec: type: "service" lifecycle: "Active" owner: "teams/tunnel-teams-routing" cf: compliance: fedramp-high: "pending" fedramp-moderate: "yes" FIPS: "required" ================================================ FILE: cfapi/base_client.go ================================================ package cfapi import ( "bytes" "encoding/json" "fmt" "io" "net/http" "net/url" "strings" "time" "github.com/pkg/errors" "github.com/rs/zerolog" "golang.org/x/net/http2" ) const ( defaultTimeout = 15 * time.Second jsonContentType = "application/json" ) var ( ErrUnauthorized = errors.New("unauthorized") ErrBadRequest = errors.New("incorrect request parameters") ErrNotFound = errors.New("not found") ErrAPINoSuccess = errors.New("API call failed") ) type RESTClient struct { baseEndpoints *baseEndpoints authToken string userAgent string client http.Client log *zerolog.Logger } type baseEndpoints struct { accountLevel url.URL zoneLevel url.URL accountRoutes url.URL accountVnets url.URL } var _ Client = (*RESTClient)(nil) func NewRESTClient(baseURL, accountTag, zoneTag, authToken, userAgent string, log *zerolog.Logger) (*RESTClient, error) { baseURL = strings.TrimSuffix(baseURL, "/") accountLevelEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/cfd_tunnel", baseURL, accountTag)) if err != nil { return nil, errors.Wrap(err, "failed to create account level endpoint") } accountRoutesEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/teamnet/routes", baseURL, accountTag)) if err != nil { return nil, errors.Wrap(err, "failed to create route account-level endpoint") } accountVnetsEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/teamnet/virtual_networks", baseURL, accountTag)) if err != nil { return nil, errors.Wrap(err, "failed to create virtual network account-level endpoint") } zoneLevelEndpoint, err := url.Parse(fmt.Sprintf("%s/zones/%s/tunnels", baseURL, zoneTag)) if err != nil { return nil, errors.Wrap(err, "failed to create account level endpoint") } httpTransport := http.Transport{ TLSHandshakeTimeout: defaultTimeout, ResponseHeaderTimeout: defaultTimeout, } _ = http2.ConfigureTransport(&httpTransport) return &RESTClient{ baseEndpoints: &baseEndpoints{ accountLevel: *accountLevelEndpoint, zoneLevel: *zoneLevelEndpoint, accountRoutes: *accountRoutesEndpoint, accountVnets: *accountVnetsEndpoint, }, authToken: authToken, userAgent: userAgent, client: http.Client{ Transport: &httpTransport, Timeout: defaultTimeout, }, log: log, }, nil } func (r *RESTClient) sendRequest(method string, url url.URL, body interface{}) (*http.Response, error) { var bodyReader io.Reader if body != nil { if bodyBytes, err := json.Marshal(body); err != nil { return nil, errors.Wrap(err, "failed to serialize json body") } else { bodyReader = bytes.NewBuffer(bodyBytes) } } req, err := http.NewRequest(method, url.String(), bodyReader) if err != nil { return nil, errors.Wrapf(err, "can't create %s request", method) } req.Header.Set("User-Agent", r.userAgent) if bodyReader != nil { req.Header.Set("Content-Type", jsonContentType) } req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.authToken)) req.Header.Add("Accept", "application/json;version=1") return r.client.Do(req) } func parseResponseEnvelope(reader io.Reader) (*response, error) { // Schema for Tunnelstore responses in the v1 API. // Roughly, it's a wrapper around a particular result that adds failures/errors/etc var result response // First, parse the wrapper and check the API call succeeded if err := json.NewDecoder(reader).Decode(&result); err != nil { return nil, errors.Wrap(err, "failed to decode response") } if err := result.checkErrors(); err != nil { return nil, err } if !result.Success { return nil, ErrAPINoSuccess } return &result, nil } func parseResponse(reader io.Reader, data interface{}) error { result, err := parseResponseEnvelope(reader) if err != nil { return err } return parseResponseBody(result, data) } func parseResponseBody(result *response, data interface{}) error { // At this point we know the API call succeeded, so, parse out the inner // result into the datatype provided as a parameter. if err := json.Unmarshal(result.Result, &data); err != nil { return errors.Wrap(err, "the Cloudflare API response was an unexpected type") } return nil } func fetchExhaustively[T any](requestFn func(int) (*http.Response, error)) ([]*T, error) { page := 0 var fullResponse []*T for { page += 1 envelope, parsedBody, err := fetchPage[T](requestFn, page) if err != nil { return nil, errors.Wrap(err, fmt.Sprintf("Error Parsing page %d", page)) } fullResponse = append(fullResponse, parsedBody...) if envelope.Pagination.Count < envelope.Pagination.PerPage || len(fullResponse) >= envelope.Pagination.TotalCount { break } } return fullResponse, nil } func fetchPage[T any](requestFn func(int) (*http.Response, error), page int) (*response, []*T, error) { pageResp, err := requestFn(page) if err != nil { return nil, nil, errors.Wrap(err, "REST request failed") } defer pageResp.Body.Close() if pageResp.StatusCode == http.StatusOK { envelope, err := parseResponseEnvelope(pageResp.Body) if err != nil { return nil, nil, err } var parsedRspBody []*T return envelope, parsedRspBody, parseResponseBody(envelope, &parsedRspBody) } return nil, nil, errors.New(fmt.Sprintf("Failed to fetch page. Server returned: %d", pageResp.StatusCode)) } type response struct { Success bool `json:"success,omitempty"` Errors []apiError `json:"errors,omitempty"` Messages []string `json:"messages,omitempty"` Result json.RawMessage `json:"result,omitempty"` Pagination Pagination `json:"result_info,omitempty"` } type Pagination struct { Count int `json:"count,omitempty"` Page int `json:"page,omitempty"` PerPage int `json:"per_page,omitempty"` TotalCount int `json:"total_count,omitempty"` } func (r *response) checkErrors() error { if len(r.Errors) == 0 { return nil } if len(r.Errors) == 1 { return r.Errors[0] } var messagesBuilder strings.Builder for _, e := range r.Errors { messagesBuilder.WriteString(fmt.Sprintf("%s; ", e)) } return fmt.Errorf("API errors: %s", messagesBuilder.String()) } type apiError struct { Code json.Number `json:"code,omitempty"` Message string `json:"message,omitempty"` } func (e apiError) Error() string { return fmt.Sprintf("code: %v, reason: %s", e.Code, e.Message) } func (r *RESTClient) statusCodeToError(op string, resp *http.Response) error { if resp.Header.Get("Content-Type") == "application/json" { var errorsResp response if json.NewDecoder(resp.Body).Decode(&errorsResp) == nil { if err := errorsResp.checkErrors(); err != nil { return errors.Errorf("Failed to %s: %s", op, err) } } } switch resp.StatusCode { case http.StatusOK: return nil case http.StatusBadRequest: return ErrBadRequest case http.StatusUnauthorized, http.StatusForbidden: return ErrUnauthorized case http.StatusNotFound: return ErrNotFound } return errors.Errorf("API call to %s failed with status %d: %s", op, resp.StatusCode, http.StatusText(resp.StatusCode)) } ================================================ FILE: cfapi/client.go ================================================ package cfapi import ( "github.com/google/uuid" ) type TunnelClient interface { CreateTunnel(name string, tunnelSecret []byte) (*TunnelWithToken, error) GetTunnel(tunnelID uuid.UUID) (*Tunnel, error) GetTunnelToken(tunnelID uuid.UUID) (string, error) GetManagementToken(tunnelID uuid.UUID, resource ManagementResource) (string, error) DeleteTunnel(tunnelID uuid.UUID, cascade bool) error ListTunnels(filter *TunnelFilter) ([]*Tunnel, error) ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error) CleanupConnections(tunnelID uuid.UUID, params *CleanupParams) error } type HostnameClient interface { RouteTunnel(tunnelID uuid.UUID, route HostnameRoute) (HostnameRouteResult, error) } type IPRouteClient interface { ListRoutes(filter *IpRouteFilter) ([]*DetailedRoute, error) AddRoute(newRoute NewRoute) (Route, error) DeleteRoute(id uuid.UUID) error GetByIP(params GetRouteByIpParams) (DetailedRoute, error) } type VnetClient interface { CreateVirtualNetwork(newVnet NewVirtualNetwork) (VirtualNetwork, error) ListVirtualNetworks(filter *VnetFilter) ([]*VirtualNetwork, error) DeleteVirtualNetwork(id uuid.UUID, force bool) error UpdateVirtualNetwork(id uuid.UUID, updates UpdateVirtualNetwork) error } type Client interface { TunnelClient HostnameClient IPRouteClient VnetClient } ================================================ FILE: cfapi/hostname.go ================================================ package cfapi import ( "encoding/json" "fmt" "io" "net/http" "path" "github.com/google/uuid" "github.com/pkg/errors" ) type Change = string const ( ChangeNew = "new" ChangeUpdated = "updated" ChangeUnchanged = "unchanged" ) // HostnameRoute represents a record type that can route to a tunnel type HostnameRoute interface { json.Marshaler RecordType() string UnmarshalResult(body io.Reader) (HostnameRouteResult, error) String() string } type HostnameRouteResult interface { // SuccessSummary explains what will route to this tunnel when it's provisioned successfully SuccessSummary() string } type DNSRoute struct { userHostname string overwriteExisting bool } type DNSRouteResult struct { route *DNSRoute CName Change `json:"cname"` Name string `json:"name"` } func NewDNSRoute(userHostname string, overwriteExisting bool) HostnameRoute { return &DNSRoute{ userHostname: userHostname, overwriteExisting: overwriteExisting, } } func (dr *DNSRoute) MarshalJSON() ([]byte, error) { s := struct { Type string `json:"type"` UserHostname string `json:"user_hostname"` OverwriteExisting bool `json:"overwrite_existing"` }{ Type: dr.RecordType(), UserHostname: dr.userHostname, OverwriteExisting: dr.overwriteExisting, } return json.Marshal(&s) } func (dr *DNSRoute) UnmarshalResult(body io.Reader) (HostnameRouteResult, error) { var result DNSRouteResult err := parseResponse(body, &result) result.route = dr return &result, err } func (dr *DNSRoute) RecordType() string { return "dns" } func (dr *DNSRoute) String() string { return fmt.Sprintf("%s %s", dr.RecordType(), dr.userHostname) } func (res *DNSRouteResult) SuccessSummary() string { var msgFmt string switch res.CName { case ChangeNew: msgFmt = "Added CNAME %s which will route to this tunnel" case ChangeUpdated: // this is not currently returned by tunnelsore msgFmt = "%s updated to route to your tunnel" case ChangeUnchanged: msgFmt = "%s is already configured to route to your tunnel" } return fmt.Sprintf(msgFmt, res.hostname()) } // hostname yields the resulting name for the DNS route; if that is not available from Cloudflare API, then the // requested name is returned instead (should not be the common path, it is just a fall-back). func (res *DNSRouteResult) hostname() string { if res.Name != "" { return res.Name } return res.route.userHostname } type LBRoute struct { lbName string lbPool string } type LBRouteResult struct { route *LBRoute LoadBalancer Change `json:"load_balancer"` Pool Change `json:"pool"` } func NewLBRoute(lbName, lbPool string) HostnameRoute { return &LBRoute{ lbName: lbName, lbPool: lbPool, } } func (lr *LBRoute) MarshalJSON() ([]byte, error) { s := struct { Type string `json:"type"` LBName string `json:"lb_name"` LBPool string `json:"lb_pool"` }{ Type: lr.RecordType(), LBName: lr.lbName, LBPool: lr.lbPool, } return json.Marshal(&s) } func (lr *LBRoute) RecordType() string { return "lb" } func (lb *LBRoute) String() string { return fmt.Sprintf("%s %s %s", lb.RecordType(), lb.lbName, lb.lbPool) } func (lr *LBRoute) UnmarshalResult(body io.Reader) (HostnameRouteResult, error) { var result LBRouteResult err := parseResponse(body, &result) result.route = lr return &result, err } func (res *LBRouteResult) SuccessSummary() string { var msg string switch res.LoadBalancer + "," + res.Pool { case "new,new": msg = "Created load balancer %s and added a new pool %s with this tunnel as an origin" case "new,updated": msg = "Created load balancer %s with an existing pool %s which was updated to use this tunnel as an origin" case "new,unchanged": msg = "Created load balancer %s with an existing pool %s which already has this tunnel as an origin" case "updated,new": msg = "Added new pool %[2]s with this tunnel as an origin to load balancer %[1]s" case "updated,updated": msg = "Updated pool %[2]s to use this tunnel as an origin and added it to load balancer %[1]s" case "updated,unchanged": msg = "Added pool %[2]s, which already has this tunnel as an origin, to load balancer %[1]s" case "unchanged,updated": msg = "Added this tunnel as an origin in pool %[2]s which is already used by load balancer %[1]s" case "unchanged,unchanged": msg = "Load balancer %s already uses pool %s which has this tunnel as an origin" case "unchanged,new": // this state is not possible fallthrough default: msg = "Something went wrong: failed to modify load balancer %s with pool %s; please check traffic manager configuration in the dashboard" } return fmt.Sprintf(msg, res.route.lbName, res.route.lbPool) } func (r *RESTClient) RouteTunnel(tunnelID uuid.UUID, route HostnameRoute) (HostnameRouteResult, error) { endpoint := r.baseEndpoints.zoneLevel endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/routes", tunnelID)) resp, err := r.sendRequest("PUT", endpoint, route) if err != nil { return nil, errors.Wrap(err, "REST request failed") } defer resp.Body.Close() if resp.StatusCode == http.StatusOK { return route.UnmarshalResult(resp.Body) } return nil, r.statusCodeToError("add route", resp) } ================================================ FILE: cfapi/hostname_test.go ================================================ package cfapi import ( "strings" "testing" "github.com/stretchr/testify/assert" ) func TestDNSRouteUnmarshalResult(t *testing.T) { route := &DNSRoute{ userHostname: "example.com", } result, err := route.UnmarshalResult(strings.NewReader(`{"success": true, "result": {"cname": "new"}}`)) assert.NoError(t, err) assert.Equal(t, &DNSRouteResult{ route: route, CName: ChangeNew, }, result) badJSON := []string{ `abc`, `{"success": false, "result": {"cname": "new"}}`, `{"errors": [{"code": 1003, "message":"An A, AAAA or CNAME record already exists with that host"}], "result": {"cname": "new"}}`, `{"errors": [{"code": 1003, "message":"An A, AAAA or CNAME record already exists with that host"}, {"code": 1004, "message":"Cannot use tunnel as origin for non-proxied load balancer"}], "result": {"cname": "new"}}`, `{"result": {"cname": "new"}}`, `{"result": {"cname": "new"}}`, } for _, j := range badJSON { _, err = route.UnmarshalResult(strings.NewReader(j)) assert.NotNil(t, err) } } func TestLBRouteUnmarshalResult(t *testing.T) { route := &LBRoute{ lbName: "lb.example.com", lbPool: "pool", } result, err := route.UnmarshalResult(strings.NewReader(`{"success": true, "result": {"pool": "unchanged", "load_balancer": "updated"}}`)) assert.NoError(t, err) assert.Equal(t, &LBRouteResult{ route: route, LoadBalancer: ChangeUpdated, Pool: ChangeUnchanged, }, result) badJSON := []string{ `abc`, `{"success": false, "result": {"pool": "unchanged", "load_balancer": "updated"}}`, `{"errors": [{"code": 1003, "message":"An A, AAAA or CNAME record already exists with that host"}], "result": {"pool": "unchanged", "load_balancer": "updated"}}`, `{"errors": [{"code": 1003, "message":"An A, AAAA or CNAME record already exists with that host"}, {"code": 1004, "message":"Cannot use tunnel as origin for non-proxied load balancer"}], "result": {"pool": "unchanged", "load_balancer": "updated"}}`, `{"result": {"pool": "unchanged", "load_balancer": "updated"}}`, } for _, j := range badJSON { _, err = route.UnmarshalResult(strings.NewReader(j)) assert.NotNil(t, err) } } func TestLBRouteResultSuccessSummary(t *testing.T) { route := &LBRoute{ lbName: "lb.example.com", lbPool: "POOL", } tests := []struct { lb Change pool Change expected string }{ {ChangeNew, ChangeNew, "Created load balancer lb.example.com and added a new pool POOL with this tunnel as an origin"}, {ChangeNew, ChangeUpdated, "Created load balancer lb.example.com with an existing pool POOL which was updated to use this tunnel as an origin"}, {ChangeNew, ChangeUnchanged, "Created load balancer lb.example.com with an existing pool POOL which already has this tunnel as an origin"}, {ChangeUpdated, ChangeNew, "Added new pool POOL with this tunnel as an origin to load balancer lb.example.com"}, {ChangeUpdated, ChangeUpdated, "Updated pool POOL to use this tunnel as an origin and added it to load balancer lb.example.com"}, {ChangeUpdated, ChangeUnchanged, "Added pool POOL, which already has this tunnel as an origin, to load balancer lb.example.com"}, {ChangeUnchanged, ChangeNew, "Something went wrong: failed to modify load balancer lb.example.com with pool POOL; please check traffic manager configuration in the dashboard"}, {ChangeUnchanged, ChangeUpdated, "Added this tunnel as an origin in pool POOL which is already used by load balancer lb.example.com"}, {ChangeUnchanged, ChangeUnchanged, "Load balancer lb.example.com already uses pool POOL which has this tunnel as an origin"}, {"", "", "Something went wrong: failed to modify load balancer lb.example.com with pool POOL; please check traffic manager configuration in the dashboard"}, {"a", "b", "Something went wrong: failed to modify load balancer lb.example.com with pool POOL; please check traffic manager configuration in the dashboard"}, } for i, tt := range tests { res := &LBRouteResult{ route: route, LoadBalancer: tt.lb, Pool: tt.pool, } actual := res.SuccessSummary() assert.Equal(t, tt.expected, actual, "case %d", i+1) } } ================================================ FILE: cfapi/ip_route.go ================================================ package cfapi import ( "encoding/json" "fmt" "io" "net" "net/http" "net/url" "path" "time" "github.com/google/uuid" "github.com/pkg/errors" ) // Route is a mapping from customer's IP space to a tunnel. // Each route allows the customer to route eyeballs in their corporate network // to certain private IP ranges. Each Route represents an IP range in their // network, and says that eyeballs can reach that route using the corresponding // tunnel. type Route struct { Network CIDR `json:"network"` TunnelID uuid.UUID `json:"tunnel_id"` // Optional field. When unset, it means the Route belongs to the default virtual network. VNetID *uuid.UUID `json:"virtual_network_id,omitempty"` Comment string `json:"comment"` CreatedAt time.Time `json:"created_at"` DeletedAt time.Time `json:"deleted_at"` } // CIDR is just a newtype wrapper around net.IPNet. It adds JSON unmarshalling. type CIDR net.IPNet func (c CIDR) String() string { n := net.IPNet(c) return n.String() } func (c CIDR) MarshalJSON() ([]byte, error) { str := c.String() json, err := json.Marshal(str) if err != nil { return nil, errors.Wrap(err, "error serializing CIDR into JSON") } return json, nil } // UnmarshalJSON parses a JSON string into net.IPNet func (c *CIDR) UnmarshalJSON(data []byte) error { var s string if err := json.Unmarshal(data, &s); err != nil { return errors.Wrap(err, "error parsing cidr string") } _, network, err := net.ParseCIDR(s) if err != nil { return errors.Wrap(err, "error parsing invalid network from backend") } if network == nil { return fmt.Errorf("backend returned invalid network %s", s) } *c = CIDR(*network) return nil } // NewRoute has all the parameters necessary to add a new route to the table. type NewRoute struct { Network net.IPNet TunnelID uuid.UUID Comment string // Optional field. If unset, backend will assume the default vnet for the account. VNetID *uuid.UUID } // MarshalJSON handles fields with non-JSON types (e.g. net.IPNet). func (r NewRoute) MarshalJSON() ([]byte, error) { return json.Marshal(&struct { Network string `json:"network"` TunnelID uuid.UUID `json:"tunnel_id"` Comment string `json:"comment"` VNetID *uuid.UUID `json:"virtual_network_id,omitempty"` }{ Network: r.Network.String(), TunnelID: r.TunnelID, Comment: r.Comment, VNetID: r.VNetID, }) } // DetailedRoute is just a Route with some extra fields, e.g. TunnelName. type DetailedRoute struct { ID uuid.UUID `json:"id"` Network CIDR `json:"network"` TunnelID uuid.UUID `json:"tunnel_id"` // Optional field. When unset, it means the DetailedRoute belongs to the default virtual network. VNetID *uuid.UUID `json:"virtual_network_id,omitempty"` Comment string `json:"comment"` CreatedAt time.Time `json:"created_at"` DeletedAt time.Time `json:"deleted_at"` TunnelName string `json:"tunnel_name"` } // IsZero checks if DetailedRoute is the zero value. func (r *DetailedRoute) IsZero() bool { return r.TunnelID == uuid.Nil } // TableString outputs a table row summarizing the route, to be used // when showing the user their routing table. func (r DetailedRoute) TableString() string { deletedColumn := "-" if !r.DeletedAt.IsZero() { deletedColumn = r.DeletedAt.Format(time.RFC3339) } vnetColumn := "default" if r.VNetID != nil { vnetColumn = r.VNetID.String() } return fmt.Sprintf( "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t", r.ID, r.Network.String(), vnetColumn, r.Comment, r.TunnelID, r.TunnelName, r.CreatedAt.Format(time.RFC3339), deletedColumn, ) } type GetRouteByIpParams struct { Ip net.IP // Optional field. If unset, backend will assume the default vnet for the account. VNetID *uuid.UUID } // ListRoutes calls the Tunnelstore GET endpoint for all routes under an account. // Due to pagination on the server side it will call the endpoint multiple times if needed. func (r *RESTClient) ListRoutes(filter *IpRouteFilter) ([]*DetailedRoute, error) { fetchFn := func(page int) (*http.Response, error) { endpoint := r.baseEndpoints.accountRoutes filter.Page(page) endpoint.RawQuery = filter.Encode() rsp, err := r.sendRequest("GET", endpoint, nil) if err != nil { return nil, errors.Wrap(err, "REST request failed") } if rsp.StatusCode != http.StatusOK { rsp.Body.Close() return nil, r.statusCodeToError("list routes", rsp) } return rsp, nil } return fetchExhaustively[DetailedRoute](fetchFn) } // AddRoute calls the Tunnelstore POST endpoint for a given route. func (r *RESTClient) AddRoute(newRoute NewRoute) (Route, error) { endpoint := r.baseEndpoints.accountRoutes endpoint.Path = path.Join(endpoint.Path) resp, err := r.sendRequest("POST", endpoint, newRoute) if err != nil { return Route{}, errors.Wrap(err, "REST request failed") } defer resp.Body.Close() if resp.StatusCode == http.StatusOK { return parseRoute(resp.Body) } return Route{}, r.statusCodeToError("add route", resp) } // DeleteRoute calls the Tunnelstore DELETE endpoint for a given route. func (r *RESTClient) DeleteRoute(id uuid.UUID) error { endpoint := r.baseEndpoints.accountRoutes endpoint.Path = path.Join(endpoint.Path, url.PathEscape(id.String())) resp, err := r.sendRequest("DELETE", endpoint, nil) if err != nil { return errors.Wrap(err, "REST request failed") } defer resp.Body.Close() if resp.StatusCode == http.StatusOK { _, err := parseRoute(resp.Body) return err } return r.statusCodeToError("delete route", resp) } // GetByIP checks which route will proxy a given IP. func (r *RESTClient) GetByIP(params GetRouteByIpParams) (DetailedRoute, error) { endpoint := r.baseEndpoints.accountRoutes endpoint.Path = path.Join(endpoint.Path, "ip", url.PathEscape(params.Ip.String())) setVnetParam(&endpoint, params.VNetID) resp, err := r.sendRequest("GET", endpoint, nil) if err != nil { return DetailedRoute{}, errors.Wrap(err, "REST request failed") } defer resp.Body.Close() if resp.StatusCode == http.StatusOK { return parseDetailedRoute(resp.Body) } return DetailedRoute{}, r.statusCodeToError("get route by IP", resp) } func parseRoute(body io.ReadCloser) (Route, error) { var route Route err := parseResponse(body, &route) return route, err } func parseDetailedRoute(body io.ReadCloser) (DetailedRoute, error) { var route DetailedRoute err := parseResponse(body, &route) return route, err } // setVnetParam overwrites the URL's query parameters with a query param to scope the HostnameRoute action to a certain // virtual network (if one is provided). func setVnetParam(endpoint *url.URL, vnetID *uuid.UUID) { queryParams := url.Values{} if vnetID != nil { queryParams.Set("virtual_network_id", vnetID.String()) } endpoint.RawQuery = queryParams.Encode() } ================================================ FILE: cfapi/ip_route_filter.go ================================================ package cfapi import ( "fmt" "net" "net/url" "strconv" "time" "github.com/google/uuid" "github.com/pkg/errors" "github.com/urfave/cli/v2" ) var ( filterIpRouteDeleted = cli.BoolFlag{ Name: "filter-is-deleted", Usage: "If false (default), only show non-deleted routes. If true, only show deleted routes.", } filterIpRouteTunnelID = cli.StringFlag{ Name: "filter-tunnel-id", Usage: "Show only routes with the given tunnel ID.", } filterSubsetIpRoute = cli.StringFlag{ Name: "filter-network-is-subset-of", Aliases: []string{"nsub"}, Usage: "Show only routes whose network is a subset of the given network.", } filterSupersetIpRoute = cli.StringFlag{ Name: "filter-network-is-superset-of", Aliases: []string{"nsup"}, Usage: "Show only routes whose network is a superset of the given network.", } filterIpRouteComment = cli.StringFlag{ Name: "filter-comment-is", Usage: "Show only routes with this comment.", } filterIpRouteByVnet = cli.StringFlag{ Name: "filter-vnet-id", Usage: "Show only routes that are attached to the given virtual network ID.", } // Flags contains all filter flags. IpRouteFilterFlags = []cli.Flag{ &filterIpRouteDeleted, &filterIpRouteTunnelID, &filterSubsetIpRoute, &filterSupersetIpRoute, &filterIpRouteComment, &filterIpRouteByVnet, } ) // IpRouteFilter which routes get queried. type IpRouteFilter struct { queryParams url.Values } // NewIpRouteFilterFromCLI parses CLI flags to discover which filters should get applied. func NewIpRouteFilterFromCLI(c *cli.Context) (*IpRouteFilter, error) { f := NewIPRouteFilter() // Set deletion filter if flag := filterIpRouteDeleted.Name; c.IsSet(flag) && c.Bool(flag) { f.Deleted() } else { f.NotDeleted() } if subset, err := cidrFromFlag(c, filterSubsetIpRoute); err != nil { return nil, err } else if subset != nil { f.NetworkIsSupersetOf(*subset) } if superset, err := cidrFromFlag(c, filterSupersetIpRoute); err != nil { return nil, err } else if superset != nil { f.NetworkIsSupersetOf(*superset) } if comment := c.String(filterIpRouteComment.Name); comment != "" { f.CommentIs(comment) } if tunnelID := c.String(filterIpRouteTunnelID.Name); tunnelID != "" { u, err := uuid.Parse(tunnelID) if err != nil { return nil, errors.Wrapf(err, "Couldn't parse UUID from %s", filterIpRouteTunnelID.Name) } f.TunnelID(u) } if vnetId := c.String(filterIpRouteByVnet.Name); vnetId != "" { u, err := uuid.Parse(vnetId) if err != nil { return nil, errors.Wrapf(err, "Couldn't parse UUID from %s", filterIpRouteByVnet.Name) } f.VNetID(u) } if maxFetch := c.Int("max-fetch-size"); maxFetch > 0 { f.MaxFetchSize(uint(maxFetch)) } return f, nil } // Parses a CIDR from the flag. If the flag was unset, returns (nil, nil). func cidrFromFlag(c *cli.Context, flag cli.StringFlag) (*net.IPNet, error) { if !c.IsSet(flag.Name) { return nil, nil } _, subset, err := net.ParseCIDR(c.String(flag.Name)) if err != nil { return nil, err } else if subset == nil { return nil, fmt.Errorf("Invalid CIDR supplied for %s", flag.Name) } return subset, nil } func NewIPRouteFilter() *IpRouteFilter { values := &IpRouteFilter{queryParams: url.Values{}} // always list cfd_tunnel routes only values.queryParams.Set("tun_types", "cfd_tunnel") return values } func (f *IpRouteFilter) CommentIs(comment string) { f.queryParams.Set("comment", comment) } func (f *IpRouteFilter) NotDeleted() { f.queryParams.Set("is_deleted", "false") } func (f *IpRouteFilter) Deleted() { f.queryParams.Set("is_deleted", "true") } func (f *IpRouteFilter) NetworkIsSubsetOf(superset net.IPNet) { f.queryParams.Set("network_subset", superset.String()) } func (f *IpRouteFilter) NetworkIsSupersetOf(subset net.IPNet) { f.queryParams.Set("network_superset", subset.String()) } func (f *IpRouteFilter) ExistedAt(existedAt time.Time) { f.queryParams.Set("existed_at", existedAt.Format(time.RFC3339)) } func (f *IpRouteFilter) TunnelID(id uuid.UUID) { f.queryParams.Set("tunnel_id", id.String()) } func (f *IpRouteFilter) VNetID(id uuid.UUID) { f.queryParams.Set("virtual_network_id", id.String()) } func (f *IpRouteFilter) MaxFetchSize(max uint) { f.queryParams.Set("per_page", strconv.Itoa(int(max))) } func (f *IpRouteFilter) Page(page int) { f.queryParams.Set("page", strconv.Itoa(page)) } func (f IpRouteFilter) Encode() string { return f.queryParams.Encode() } ================================================ FILE: cfapi/ip_route_test.go ================================================ package cfapi import ( "encoding/json" "fmt" "net" "strings" "testing" "github.com/google/uuid" "github.com/stretchr/testify/require" ) func TestUnmarshalRoute(t *testing.T) { testCases := []struct { Json string HasVnet bool }{ { `{ "network":"10.1.2.40/29", "tunnel_id":"fba6ffea-807f-4e7a-a740-4184ee1b82c8", "comment":"test", "created_at":"2020-12-22T02:00:15.587008Z", "deleted_at":null }`, false, }, { `{ "network":"10.1.2.40/29", "tunnel_id":"fba6ffea-807f-4e7a-a740-4184ee1b82c8", "comment":"test", "created_at":"2020-12-22T02:00:15.587008Z", "deleted_at":null, "virtual_network_id":"38c95083-8191-4110-8339-3f438d44fdb9" }`, true, }, } for _, testCase := range testCases { data := testCase.Json var r Route err := json.Unmarshal([]byte(data), &r) // Check everything worked require.NoError(t, err) require.Equal(t, uuid.MustParse("fba6ffea-807f-4e7a-a740-4184ee1b82c8"), r.TunnelID) require.Equal(t, "test", r.Comment) _, cidr, err := net.ParseCIDR("10.1.2.40/29") require.NoError(t, err) require.Equal(t, CIDR(*cidr), r.Network) require.Equal(t, "test", r.Comment) if testCase.HasVnet { require.Equal(t, uuid.MustParse("38c95083-8191-4110-8339-3f438d44fdb9"), *r.VNetID) } else { require.Nil(t, r.VNetID) } } } func TestDetailedRouteJsonRoundtrip(t *testing.T) { testCases := []struct { Json string HasVnet bool }{ { `{ "id":"91ebc578-cc99-4641-9937-0fb630505fa0", "network":"10.1.2.40/29", "tunnel_id":"fba6ffea-807f-4e7a-a740-4184ee1b82c8", "comment":"test", "created_at":"2020-12-22T02:00:15.587008Z", "deleted_at":"2021-01-14T05:01:42.183002Z", "tunnel_name":"Mr. Tun" }`, false, }, { `{ "id":"91ebc578-cc99-4641-9937-0fb630505fa0", "network":"10.1.2.40/29", "tunnel_id":"fba6ffea-807f-4e7a-a740-4184ee1b82c8", "virtual_network_id":"38c95083-8191-4110-8339-3f438d44fdb9", "comment":"test", "created_at":"2020-12-22T02:00:15.587008Z", "deleted_at":"2021-01-14T05:01:42.183002Z", "tunnel_name":"Mr. Tun" }`, true, }, } for _, testCase := range testCases { data := testCase.Json var r DetailedRoute err := json.Unmarshal([]byte(data), &r) // Check everything worked require.NoError(t, err) require.Equal(t, uuid.MustParse("fba6ffea-807f-4e7a-a740-4184ee1b82c8"), r.TunnelID) require.Equal(t, "test", r.Comment) _, cidr, err := net.ParseCIDR("10.1.2.40/29") require.NoError(t, err) require.Equal(t, CIDR(*cidr), r.Network) require.Equal(t, "test", r.Comment) require.Equal(t, "Mr. Tun", r.TunnelName) if testCase.HasVnet { require.Equal(t, uuid.MustParse("38c95083-8191-4110-8339-3f438d44fdb9"), *r.VNetID) } else { require.Nil(t, r.VNetID) } bytes, err := json.Marshal(r) require.NoError(t, err) obtainedJson := string(bytes) data = strings.Replace(data, "\t", "", -1) data = strings.Replace(data, "\n", "", -1) require.Equal(t, data, obtainedJson) } } func TestMarshalNewRoute(t *testing.T) { _, network, err := net.ParseCIDR("1.2.3.4/32") require.NoError(t, err) require.NotNil(t, network) vnetId := uuid.New() newRoutes := []NewRoute{ { Network: *network, TunnelID: uuid.New(), Comment: "hi", }, { Network: *network, TunnelID: uuid.New(), Comment: "hi", VNetID: &vnetId, }, } for _, newRoute := range newRoutes { // Test where receiver is struct serialized, err := json.Marshal(newRoute) require.NoError(t, err) require.True(t, strings.Contains(string(serialized), "tunnel_id")) // Test where receiver is pointer to struct serialized, err = json.Marshal(&newRoute) require.NoError(t, err) require.True(t, strings.Contains(string(serialized), "tunnel_id")) if newRoute.VNetID == nil { require.False(t, strings.Contains(string(serialized), "virtual_network_id")) } else { require.True(t, strings.Contains(string(serialized), "virtual_network_id")) } } } func TestRouteTableString(t *testing.T) { _, network, err := net.ParseCIDR("1.2.3.4/32") require.NoError(t, err) require.NotNil(t, network) r := DetailedRoute{ ID: uuid.Nil, Network: CIDR(*network), } row := r.TableString() fmt.Println(row) require.True(t, strings.HasPrefix(row, "00000000-0000-0000-0000-000000000000\t1.2.3.4/32")) } ================================================ FILE: cfapi/tunnel.go ================================================ package cfapi import ( "fmt" "io" "net" "net/http" "net/url" "path" "time" "github.com/google/uuid" "github.com/pkg/errors" ) var ErrTunnelNameConflict = errors.New("tunnel with name already exists") type ManagementResource int const ( Logs ManagementResource = iota Admin HostDetails ) func (r ManagementResource) String() string { switch r { case Logs: return "logs" case Admin: return "admin" case HostDetails: return "host_details" default: return "" } } type Tunnel struct { ID uuid.UUID `json:"id"` Name string `json:"name"` CreatedAt time.Time `json:"created_at"` DeletedAt time.Time `json:"deleted_at"` Connections []Connection `json:"connections"` } type TunnelWithToken struct { Tunnel Token string `json:"token"` } type Connection struct { ColoName string `json:"colo_name"` ID uuid.UUID `json:"id"` IsPendingReconnect bool `json:"is_pending_reconnect"` OriginIP net.IP `json:"origin_ip"` OpenedAt time.Time `json:"opened_at"` } type ActiveClient struct { ID uuid.UUID `json:"id"` Features []string `json:"features"` Version string `json:"version"` Arch string `json:"arch"` RunAt time.Time `json:"run_at"` Connections []Connection `json:"conns"` } type newTunnel struct { Name string `json:"name"` TunnelSecret []byte `json:"tunnel_secret"` } type CleanupParams struct { queryParams url.Values } func NewCleanupParams() *CleanupParams { return &CleanupParams{ queryParams: url.Values{}, } } func (cp *CleanupParams) ForClient(clientID uuid.UUID) { cp.queryParams.Set("client_id", clientID.String()) } func (cp CleanupParams) encode() string { return cp.queryParams.Encode() } func (r *RESTClient) CreateTunnel(name string, tunnelSecret []byte) (*TunnelWithToken, error) { if name == "" { return nil, errors.New("tunnel name required") } if _, err := uuid.Parse(name); err == nil { return nil, errors.New("you cannot use UUIDs as tunnel names") } body := &newTunnel{ Name: name, TunnelSecret: tunnelSecret, } resp, err := r.sendRequest("POST", r.baseEndpoints.accountLevel, body) if err != nil { return nil, errors.Wrap(err, "REST request failed") } defer resp.Body.Close() switch resp.StatusCode { case http.StatusOK: var tunnel TunnelWithToken if serdeErr := parseResponse(resp.Body, &tunnel); serdeErr != nil { return nil, serdeErr } return &tunnel, nil case http.StatusConflict: return nil, ErrTunnelNameConflict } return nil, r.statusCodeToError("create tunnel", resp) } func (r *RESTClient) GetTunnel(tunnelID uuid.UUID) (*Tunnel, error) { endpoint := r.baseEndpoints.accountLevel endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v", tunnelID)) resp, err := r.sendRequest("GET", endpoint, nil) if err != nil { return nil, errors.Wrap(err, "REST request failed") } defer resp.Body.Close() if resp.StatusCode == http.StatusOK { return unmarshalTunnel(resp.Body) } return nil, r.statusCodeToError("get tunnel", resp) } func (r *RESTClient) GetTunnelToken(tunnelID uuid.UUID) (token string, err error) { endpoint := r.baseEndpoints.accountLevel endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/token", tunnelID)) resp, err := r.sendRequest("GET", endpoint, nil) if err != nil { return "", errors.Wrap(err, "REST request failed") } defer resp.Body.Close() if resp.StatusCode == http.StatusOK { err = parseResponse(resp.Body, &token) return token, err } return "", r.statusCodeToError("get tunnel token", resp) } // managementEndpointPath returns the path segment for a management resource endpoint func managementEndpointPath(tunnelID uuid.UUID, res ManagementResource) string { return fmt.Sprintf("%v/management/%s", tunnelID, res.String()) } func (r *RESTClient) GetManagementToken(tunnelID uuid.UUID, res ManagementResource) (token string, err error) { endpoint := r.baseEndpoints.accountLevel endpoint.Path = path.Join(endpoint.Path, managementEndpointPath(tunnelID, res)) resp, err := r.sendRequest("POST", endpoint, nil) if err != nil { return "", errors.Wrap(err, "REST request failed") } defer resp.Body.Close() if resp.StatusCode == http.StatusOK { err = parseResponse(resp.Body, &token) return token, err } return "", r.statusCodeToError("get tunnel token", resp) } func (r *RESTClient) DeleteTunnel(tunnelID uuid.UUID, cascade bool) error { endpoint := r.baseEndpoints.accountLevel endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v", tunnelID)) // Cascade will delete all tunnel dependencies (connections, routes, etc.) that // are linked to the deleted tunnel. if cascade { endpoint.RawQuery = "cascade=true" } resp, err := r.sendRequest("DELETE", endpoint, nil) if err != nil { return errors.Wrap(err, "REST request failed") } defer resp.Body.Close() return r.statusCodeToError("delete tunnel", resp) } func (r *RESTClient) ListTunnels(filter *TunnelFilter) ([]*Tunnel, error) { fetchFn := func(page int) (*http.Response, error) { endpoint := r.baseEndpoints.accountLevel filter.Page(page) endpoint.RawQuery = filter.encode() rsp, err := r.sendRequest("GET", endpoint, nil) if err != nil { return nil, errors.Wrap(err, "REST request failed") } if rsp.StatusCode != http.StatusOK { rsp.Body.Close() return nil, r.statusCodeToError("list tunnels", rsp) } return rsp, nil } return fetchExhaustively[Tunnel](fetchFn) } func (r *RESTClient) ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error) { endpoint := r.baseEndpoints.accountLevel endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/connections", tunnelID)) resp, err := r.sendRequest("GET", endpoint, nil) if err != nil { return nil, errors.Wrap(err, "REST request failed") } defer resp.Body.Close() if resp.StatusCode == http.StatusOK { return parseConnectionsDetails(resp.Body) } return nil, r.statusCodeToError("list connection details", resp) } func parseConnectionsDetails(reader io.Reader) ([]*ActiveClient, error) { var clients []*ActiveClient err := parseResponse(reader, &clients) return clients, err } func (r *RESTClient) CleanupConnections(tunnelID uuid.UUID, params *CleanupParams) error { endpoint := r.baseEndpoints.accountLevel endpoint.RawQuery = params.encode() endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/connections", tunnelID)) resp, err := r.sendRequest("DELETE", endpoint, nil) if err != nil { return errors.Wrap(err, "REST request failed") } defer resp.Body.Close() return r.statusCodeToError("cleanup connections", resp) } func unmarshalTunnel(reader io.Reader) (*Tunnel, error) { var tunnel Tunnel err := parseResponse(reader, &tunnel) return &tunnel, err } ================================================ FILE: cfapi/tunnel_filter.go ================================================ package cfapi import ( "net/url" "strconv" "time" "github.com/google/uuid" ) const ( TimeLayout = time.RFC3339 ) type TunnelFilter struct { queryParams url.Values } func NewTunnelFilter() *TunnelFilter { return &TunnelFilter{ queryParams: url.Values{}, } } func (f *TunnelFilter) ByName(name string) { f.queryParams.Set("name", name) } func (f *TunnelFilter) ByNamePrefix(namePrefix string) { f.queryParams.Set("name_prefix", namePrefix) } func (f *TunnelFilter) ExcludeNameWithPrefix(excludePrefix string) { f.queryParams.Set("exclude_prefix", excludePrefix) } func (f *TunnelFilter) NoDeleted() { f.queryParams.Set("is_deleted", "false") } func (f *TunnelFilter) ByExistedAt(existedAt time.Time) { f.queryParams.Set("existed_at", existedAt.Format(TimeLayout)) } func (f *TunnelFilter) ByTunnelID(tunnelID uuid.UUID) { f.queryParams.Set("uuid", tunnelID.String()) } func (f *TunnelFilter) MaxFetchSize(max uint) { f.queryParams.Set("per_page", strconv.Itoa(int(max))) } func (f *TunnelFilter) Page(page int) { f.queryParams.Set("page", strconv.Itoa(page)) } func (f TunnelFilter) encode() string { return f.queryParams.Encode() } ================================================ FILE: cfapi/tunnel_test.go ================================================ package cfapi import ( "bytes" "net" "reflect" "strings" "testing" "time" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var loc, _ = time.LoadLocation("UTC") func Test_unmarshalTunnel(t *testing.T) { type args struct { body string } tests := []struct { name string args args want *Tunnel wantErr bool }{ { name: "empty list", args: args{body: `{"success": true, "result": {"id":"b34cc7ce-925b-46ee-bc23-4cb5c18d8292","created_at":"2021-07-29T13:46:14.090955Z","deleted_at":"2021-07-29T14:07:27.559047Z","name":"qt-bIWWN7D662ogh61pCPfu5s2XgqFY1OyV","account_id":6946212,"account_tag":"5ab4e9dfbd435d24068829fda0077963","conns_active_at":null,"conns_inactive_at":"2021-07-29T13:47:22.548482Z","tun_type":"cfd_tunnel","metadata":{"qtid":"a6fJROgkXutNruBGaJjD"}}}`}, want: &Tunnel{ ID: uuid.MustParse("b34cc7ce-925b-46ee-bc23-4cb5c18d8292"), Name: "qt-bIWWN7D662ogh61pCPfu5s2XgqFY1OyV", CreatedAt: time.Date(2021, 07, 29, 13, 46, 14, 90955000, loc), DeletedAt: time.Date(2021, 07, 29, 14, 7, 27, 559047000, loc), Connections: nil, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := unmarshalTunnel(strings.NewReader(tt.args.body)) if (err != nil) != tt.wantErr { t.Errorf("unmarshalTunnel() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { t.Errorf("unmarshalTunnel() = %v, want %v", got, tt.want) } }) } } func TestUnmarshalTunnelOk(t *testing.T) { jsonBody := `{"success": true, "result": {"id": "00000000-0000-0000-0000-000000000000","name":"test","created_at":"0001-01-01T00:00:00Z","connections":[]}}` expected := Tunnel{ ID: uuid.Nil, Name: "test", CreatedAt: time.Time{}, Connections: []Connection{}, } actual, err := unmarshalTunnel(bytes.NewReader([]byte(jsonBody))) require.NoError(t, err) require.Equal(t, &expected, actual) } func TestUnmarshalTunnelErr(t *testing.T) { tests := []string{ `abc`, `{"success": true, "result": abc}`, `{"success": false, "result": {"id": "00000000-0000-0000-0000-000000000000","name":"test","created_at":"0001-01-01T00:00:00Z","connections":[]}}}`, `{"errors": [{"code": 1003, "message":"An A, AAAA or CNAME record already exists with that host"}], "result": {"id": "00000000-0000-0000-0000-000000000000","name":"test","created_at":"0001-01-01T00:00:00Z","connections":[]}}}`, } for i, test := range tests { _, err := unmarshalTunnel(bytes.NewReader([]byte(test))) assert.Error(t, err, "Test #%v failed", i) } } func TestManagementResource_String(t *testing.T) { tests := []struct { name string resource ManagementResource want string }{ { name: "Logs", resource: Logs, want: "logs", }, { name: "Admin", resource: Admin, want: "admin", }, { name: "HostDetails", resource: HostDetails, want: "host_details", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert.Equal(t, tt.want, tt.resource.String()) }) } } func TestManagementResource_String_Unknown(t *testing.T) { unknown := ManagementResource(999) assert.Equal(t, "", unknown.String()) } func TestManagementEndpointPath(t *testing.T) { tunnelID := uuid.MustParse("b34cc7ce-925b-46ee-bc23-4cb5c18d8292") tests := []struct { name string resource ManagementResource want string }{ { name: "Logs resource", resource: Logs, want: "b34cc7ce-925b-46ee-bc23-4cb5c18d8292/management/logs", }, { name: "Admin resource", resource: Admin, want: "b34cc7ce-925b-46ee-bc23-4cb5c18d8292/management/admin", }, { name: "HostDetails resource", resource: HostDetails, want: "b34cc7ce-925b-46ee-bc23-4cb5c18d8292/management/host_details", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := managementEndpointPath(tunnelID, tt.resource) assert.Equal(t, tt.want, got) }) } } func TestUnmarshalConnections(t *testing.T) { jsonBody := `{"success":true,"messages":[],"errors":[],"result":[{"id":"d4041254-91e3-4deb-bd94-b46e11680b1e","features":["ha-origin"],"version":"2021.2.5","arch":"darwin_amd64","conns":[{"colo_name":"LIS","id":"ac2286e5-c708-4588-a6a0-ba6b51940019","is_pending_reconnect":false,"origin_ip":"148.38.28.2","opened_at":"0001-01-01T00:00:00Z"}],"run_at":"0001-01-01T00:00:00Z"}]}` expected := ActiveClient{ ID: uuid.MustParse("d4041254-91e3-4deb-bd94-b46e11680b1e"), Features: []string{"ha-origin"}, Version: "2021.2.5", Arch: "darwin_amd64", RunAt: time.Time{}, Connections: []Connection{{ ID: uuid.MustParse("ac2286e5-c708-4588-a6a0-ba6b51940019"), ColoName: "LIS", IsPendingReconnect: false, OriginIP: net.ParseIP("148.38.28.2"), OpenedAt: time.Time{}, }}, } actual, err := parseConnectionsDetails(bytes.NewReader([]byte(jsonBody))) require.NoError(t, err) assert.Equal(t, []*ActiveClient{&expected}, actual) } ================================================ FILE: cfapi/virtual_network.go ================================================ package cfapi import ( "fmt" "io" "net/http" "net/url" "path" "strconv" "time" "github.com/google/uuid" "github.com/pkg/errors" ) type NewVirtualNetwork struct { Name string `json:"name"` Comment string `json:"comment"` IsDefault bool `json:"is_default_network"` } type VirtualNetwork struct { ID uuid.UUID `json:"id"` Comment string `json:"comment"` Name string `json:"name"` IsDefault bool `json:"is_default_network"` CreatedAt time.Time `json:"created_at"` DeletedAt time.Time `json:"deleted_at"` } type UpdateVirtualNetwork struct { Name *string `json:"name,omitempty"` Comment *string `json:"comment,omitempty"` IsDefault *bool `json:"is_default_network,omitempty"` } func (virtualNetwork VirtualNetwork) TableString() string { deletedColumn := "-" if !virtualNetwork.DeletedAt.IsZero() { deletedColumn = virtualNetwork.DeletedAt.Format(time.RFC3339) } return fmt.Sprintf( "%s\t%s\t%s\t%s\t%s\t%s\t", virtualNetwork.ID, virtualNetwork.Name, strconv.FormatBool(virtualNetwork.IsDefault), virtualNetwork.Comment, virtualNetwork.CreatedAt.Format(time.RFC3339), deletedColumn, ) } func (r *RESTClient) CreateVirtualNetwork(newVnet NewVirtualNetwork) (VirtualNetwork, error) { resp, err := r.sendRequest("POST", r.baseEndpoints.accountVnets, newVnet) if err != nil { return VirtualNetwork{}, errors.Wrap(err, "REST request failed") } defer resp.Body.Close() if resp.StatusCode == http.StatusOK { return parseVnet(resp.Body) } return VirtualNetwork{}, r.statusCodeToError("add virtual network", resp) } func (r *RESTClient) ListVirtualNetworks(filter *VnetFilter) ([]*VirtualNetwork, error) { endpoint := r.baseEndpoints.accountVnets endpoint.RawQuery = filter.Encode() resp, err := r.sendRequest("GET", endpoint, nil) if err != nil { return nil, errors.Wrap(err, "REST request failed") } defer resp.Body.Close() if resp.StatusCode == http.StatusOK { return parseListVnets(resp.Body) } return nil, r.statusCodeToError("list virtual networks", resp) } func (r *RESTClient) DeleteVirtualNetwork(id uuid.UUID, force bool) error { endpoint := r.baseEndpoints.accountVnets endpoint.Path = path.Join(endpoint.Path, url.PathEscape(id.String())) queryParams := url.Values{} if force { queryParams.Set("force", strconv.FormatBool(force)) } endpoint.RawQuery = queryParams.Encode() resp, err := r.sendRequest("DELETE", endpoint, nil) if err != nil { return errors.Wrap(err, "REST request failed") } defer resp.Body.Close() if resp.StatusCode == http.StatusOK { _, err := parseVnet(resp.Body) return err } return r.statusCodeToError("delete virtual network", resp) } func (r *RESTClient) UpdateVirtualNetwork(id uuid.UUID, updates UpdateVirtualNetwork) error { endpoint := r.baseEndpoints.accountVnets endpoint.Path = path.Join(endpoint.Path, url.PathEscape(id.String())) resp, err := r.sendRequest("PATCH", endpoint, updates) if err != nil { return errors.Wrap(err, "REST request failed") } defer resp.Body.Close() if resp.StatusCode == http.StatusOK { _, err := parseVnet(resp.Body) return err } return r.statusCodeToError("update virtual network", resp) } func parseListVnets(body io.ReadCloser) ([]*VirtualNetwork, error) { var vnets []*VirtualNetwork err := parseResponse(body, &vnets) return vnets, err } func parseVnet(body io.ReadCloser) (VirtualNetwork, error) { var vnet VirtualNetwork err := parseResponse(body, &vnet) return vnet, err } ================================================ FILE: cfapi/virtual_network_filter.go ================================================ package cfapi import ( "net/url" "strconv" "github.com/google/uuid" "github.com/pkg/errors" "github.com/urfave/cli/v2" ) var ( filterVnetId = cli.StringFlag{ Name: "id", Usage: "List virtual networks with the given `ID`", } filterVnetByName = cli.StringFlag{ Name: "name", Usage: "List virtual networks with the given `NAME`", } filterDefaultVnet = cli.BoolFlag{ Name: "is-default", Usage: "If true, lists the virtual network that is the default one. If false, lists all non-default virtual networks for the account. If absent, all are included in the results regardless of their default status.", } filterDeletedVnet = cli.BoolFlag{ Name: "show-deleted", Usage: "If false (default), only show non-deleted virtual networks. If true, only show deleted virtual networks.", } VnetFilterFlags = []cli.Flag{ &filterVnetId, &filterVnetByName, &filterDefaultVnet, &filterDeletedVnet, } ) // VnetFilter which virtual networks get queried. type VnetFilter struct { queryParams url.Values } func NewVnetFilter() *VnetFilter { return &VnetFilter{ queryParams: url.Values{}, } } func (f *VnetFilter) ById(vnetId uuid.UUID) { f.queryParams.Set("id", vnetId.String()) } func (f *VnetFilter) ByName(name string) { f.queryParams.Set("name", name) } func (f *VnetFilter) ByDefaultStatus(isDefault bool) { f.queryParams.Set("is_default", strconv.FormatBool(isDefault)) } func (f *VnetFilter) WithDeleted(isDeleted bool) { f.queryParams.Set("is_deleted", strconv.FormatBool(isDeleted)) } func (f *VnetFilter) MaxFetchSize(max uint) { f.queryParams.Set("per_page", strconv.Itoa(int(max))) } func (f VnetFilter) Encode() string { return f.queryParams.Encode() } // NewFromCLI parses CLI flags to discover which filters should get applied to list virtual networks. func NewFromCLI(c *cli.Context) (*VnetFilter, error) { f := NewVnetFilter() if id := c.String("id"); id != "" { vnetId, err := uuid.Parse(id) if err != nil { return nil, errors.Wrapf(err, "%s is not a valid virtual network ID", id) } f.ById(vnetId) } if name := c.String("name"); name != "" { f.ByName(name) } if c.IsSet("is-default") { f.ByDefaultStatus(c.Bool("is-default")) } f.WithDeleted(c.Bool("show-deleted")) if maxFetch := c.Int("max-fetch-size"); maxFetch > 0 { f.MaxFetchSize(uint(maxFetch)) } return f, nil } ================================================ FILE: cfapi/virtual_network_test.go ================================================ package cfapi import ( "encoding/json" "strings" "testing" "time" "github.com/google/uuid" "github.com/stretchr/testify/require" ) func TestVirtualNetworkJsonRoundtrip(t *testing.T) { data := `{ "id":"74fce949-351b-4752-b261-81a56cfd3130", "comment":"New York DC1", "name":"us-east-1", "is_default_network":true, "created_at":"2021-11-26T14:40:02.600673Z", "deleted_at":"2021-12-01T10:23:13.102645Z" }` var v VirtualNetwork err := json.Unmarshal([]byte(data), &v) require.NoError(t, err) require.Equal(t, uuid.MustParse("74fce949-351b-4752-b261-81a56cfd3130"), v.ID) require.Equal(t, "us-east-1", v.Name) require.Equal(t, "New York DC1", v.Comment) require.Equal(t, true, v.IsDefault) bytes, err := json.Marshal(v) require.NoError(t, err) obtainedJson := string(bytes) data = strings.Replace(data, "\t", "", -1) data = strings.Replace(data, "\n", "", -1) require.Equal(t, data, obtainedJson) } func TestMarshalNewVnet(t *testing.T) { newVnet := NewVirtualNetwork{ Name: "eu-west-1", Comment: "London office", IsDefault: true, } serialized, err := json.Marshal(newVnet) require.NoError(t, err) require.True(t, strings.Contains(string(serialized), newVnet.Name)) } func TestMarshalUpdateVnet(t *testing.T) { newName := "bulgaria-1" updates := UpdateVirtualNetwork{ Name: &newName, } // Test where receiver is struct serialized, err := json.Marshal(updates) require.NoError(t, err) require.True(t, strings.Contains(string(serialized), newName)) } func TestVnetTableString(t *testing.T) { virtualNet := VirtualNetwork{ ID: uuid.New(), Name: "us-east-1", Comment: "New York DC1", IsDefault: true, CreatedAt: time.Now(), DeletedAt: time.Time{}, } row := virtualNet.TableString() require.True(t, strings.HasPrefix(row, virtualNet.ID.String())) require.True(t, strings.Contains(row, virtualNet.Name)) require.True(t, strings.Contains(row, virtualNet.Comment)) require.True(t, strings.Contains(row, "true")) require.True(t, strings.HasSuffix(row, "-\t")) } ================================================ FILE: cfio/copy.go ================================================ package cfio import ( "io" "sync" ) const defaultBufferSize = 16 * 1024 var bufferPool = sync.Pool{ New: func() interface{} { return make([]byte, defaultBufferSize) }, } func Copy(dst io.Writer, src io.Reader) (written int64, err error) { _, okWriteTo := src.(io.WriterTo) _, okReadFrom := dst.(io.ReaderFrom) var buffer []byte = nil if !(okWriteTo || okReadFrom) { buffer = bufferPool.Get().([]byte) defer bufferPool.Put(buffer) } return io.CopyBuffer(dst, src, buffer) } ================================================ FILE: cfsetup.yaml ================================================ # A valid cfsetup.yaml is required but we dont have any real config to specify dummy_key: true ================================================ FILE: check-fips.sh ================================================ # Pass the path to the executable to check for FIPS compliance exe=$1 if [ "$(go tool nm "${exe}" | grep -c '_Cfunc__goboringcrypto_')" -eq 0 ]; then # Asserts that executable is using FIPS-compliant boringcrypto echo "${exe}: missing goboring symbols" >&2 exit 1 fi if [ "$(go tool nm "${exe}" | grep -c 'crypto/internal/boring/sig.FIPSOnly')" -eq 0 ]; then # Asserts that executable is using FIPS-only schemes echo "${exe}: missing fipsonly symbols" >&2 exit 1 fi echo "${exe} is FIPS-compliant" ================================================ FILE: client/config.go ================================================ package client import ( "fmt" "net" "github.com/google/uuid" "github.com/rs/zerolog" "github.com/cloudflare/cloudflared/features" "github.com/cloudflare/cloudflared/tunnelrpc/pogs" ) // Config captures the local client runtime configuration. type Config struct { ConnectorID uuid.UUID Version string Arch string featureSelector features.FeatureSelector } func NewConfig(version string, arch string, featureSelector features.FeatureSelector) (*Config, error) { connectorID, err := uuid.NewRandom() if err != nil { return nil, fmt.Errorf("unable to generate a connector UUID: %w", err) } return &Config{ ConnectorID: connectorID, Version: version, Arch: arch, featureSelector: featureSelector, }, nil } // ConnectionOptionsSnapshot is a snapshot of the current client information used to initialize a connection. // // The FeatureSnapshot is the features that are available for this connection. At the client level they may // change, but they will not change within the scope of this struct. type ConnectionOptionsSnapshot struct { client pogs.ClientInfo originLocalIP net.IP numPreviousAttempts uint8 FeatureSnapshot features.FeatureSnapshot } func (c *Config) ConnectionOptionsSnapshot(originIP net.IP, previousAttempts uint8) *ConnectionOptionsSnapshot { snapshot := c.featureSelector.Snapshot() return &ConnectionOptionsSnapshot{ client: pogs.ClientInfo{ ClientID: c.ConnectorID[:], Version: c.Version, Arch: c.Arch, Features: snapshot.FeaturesList, }, originLocalIP: originIP, numPreviousAttempts: previousAttempts, FeatureSnapshot: snapshot, } } func (c ConnectionOptionsSnapshot) ConnectionOptions() *pogs.ConnectionOptions { return &pogs.ConnectionOptions{ Client: c.client, OriginLocalIP: c.originLocalIP, ReplaceExisting: false, CompressionQuality: 0, NumPreviousAttempts: c.numPreviousAttempts, } } func (c ConnectionOptionsSnapshot) LogFields(event *zerolog.Event) *zerolog.Event { return event.Strs("features", c.client.Features) } ================================================ FILE: client/config_test.go ================================================ package client import ( "net" "testing" "github.com/stretchr/testify/require" "github.com/cloudflare/cloudflared/features" ) func TestGenerateConnectionOptions(t *testing.T) { version := "1234" arch := "linux_amd64" originIP := net.ParseIP("192.168.1.1") var previousAttempts uint8 = 4 config, err := NewConfig(version, arch, &mockFeatureSelector{}) require.NoError(t, err) require.Equal(t, version, config.Version) require.Equal(t, arch, config.Arch) // Validate ConnectionOptionsSnapshot fields connOptions := config.ConnectionOptionsSnapshot(originIP, previousAttempts) require.Equal(t, version, connOptions.client.Version) require.Equal(t, arch, connOptions.client.Arch) require.Equal(t, config.ConnectorID[:], connOptions.client.ClientID) // Vaidate snapshot feature fields against the connOptions generated snapshot := config.featureSelector.Snapshot() require.Equal(t, features.DatagramV3, snapshot.DatagramVersion) require.Equal(t, features.DatagramV3, connOptions.FeatureSnapshot.DatagramVersion) pogsConnOptions := connOptions.ConnectionOptions() require.Equal(t, connOptions.client, pogsConnOptions.Client) require.Equal(t, originIP, pogsConnOptions.OriginLocalIP) require.False(t, pogsConnOptions.ReplaceExisting) require.Equal(t, uint8(0), pogsConnOptions.CompressionQuality) require.Equal(t, previousAttempts, pogsConnOptions.NumPreviousAttempts) } type mockFeatureSelector struct{} func (m *mockFeatureSelector) Snapshot() features.FeatureSnapshot { return features.FeatureSnapshot{ PostQuantum: features.PostQuantumPrefer, DatagramVersion: features.DatagramV3, FeaturesList: []string{features.FeaturePostQuantum, features.FeatureDatagramV3_2}, } } ================================================ FILE: cloudflared.wxs ================================================ NOT NEWERVERSIONDETECTED ================================================ FILE: cloudflared_man_template ================================================ .\" Manpage for cloudflared. .TH man 1 ${DATE} "${VERSION}" "cloudflared man page" .SH NAME cloudflared \- creates a connection to the cloudflare edge network .SH DESCRIPTION cloudflared creates a persistent connection between a local service and the Cloudflare network. Once the daemon is running and the Tunnel has been configured, the local service can be locked down to only allow connections from Cloudflare. ================================================ FILE: cmd/cloudflared/access/carrier.go ================================================ package access import ( "crypto/tls" "fmt" "io" "net/http" "strings" "github.com/pkg/errors" "github.com/rs/zerolog" "github.com/urfave/cli/v2" "github.com/cloudflare/cloudflared/carrier" "github.com/cloudflare/cloudflared/config" "github.com/cloudflare/cloudflared/logger" "github.com/cloudflare/cloudflared/stream" "github.com/cloudflare/cloudflared/validation" ) const ( LogFieldHost = "host" cfAccessClientIDHeader = "Cf-Access-Client-Id" cfAccessClientSecretHeader = "Cf-Access-Client-Secret" ) // StartForwarder starts a client side websocket forward func StartForwarder(forwarder config.Forwarder, shutdown <-chan struct{}, log *zerolog.Logger) error { validURL, err := validation.ValidateUrl(forwarder.Listener) if err != nil { return errors.Wrap(err, "error validating origin URL") } // get the headers from the config file and add to the request headers := make(http.Header) if forwarder.TokenClientID != "" { headers.Set(cfAccessClientIDHeader, forwarder.TokenClientID) } if forwarder.TokenSecret != "" { headers.Set(cfAccessClientSecretHeader, forwarder.TokenSecret) } headers.Set("User-Agent", userAgent) carrier.SetBastionDest(headers, forwarder.Destination) options := &carrier.StartOptions{ OriginURL: forwarder.URL, Headers: headers, //TODO: TUN-2688 support custom headers from config file IsFedramp: forwarder.IsFedramp, } // we could add a cmd line variable for this bool if we want the SOCK5 server to be on the client side wsConn := carrier.NewWSConnection(log) log.Info().Str(LogFieldHost, validURL.Host).Msg("Start Websocket listener") return carrier.StartForwarder(wsConn, validURL.Host, shutdown, options) } // ssh will start a WS proxy server for server mode // or copy from stdin/stdout for client mode // useful for proxying other protocols (like ssh) over websockets // (which you can put Access in front of) func ssh(c *cli.Context) error { // If not running as a forwarder, disable terminal logs as it collides with the stdin/stdout of the parent process outputTerminal := logger.DisableTerminalLog if c.IsSet(sshURLFlag) { outputTerminal = logger.EnableTerminalLog } log := logger.CreateSSHLoggerFromContext(c, outputTerminal) // get the hostname from the cmdline and error out if its not provided rawHostName := c.String(sshHostnameFlag) url, err := parseURL(rawHostName) if err != nil { log.Err(err).Send() return cli.ShowCommandHelp(c, "ssh") } // get the headers from the cmdline and add them headers := parseRequestHeaders(c.StringSlice(sshHeaderFlag)) if c.IsSet(sshTokenIDFlag) { headers.Set(cfAccessClientIDHeader, c.String(sshTokenIDFlag)) } if c.IsSet(sshTokenSecretFlag) { headers.Set(cfAccessClientSecretHeader, c.String(sshTokenSecretFlag)) } headers.Set("User-Agent", userAgent) carrier.SetBastionDest(headers, c.String(sshDestinationFlag)) options := &carrier.StartOptions{ OriginURL: url.String(), Headers: headers, Host: url.Host, IsFedramp: c.Bool(fedrampFlag), } if connectTo := c.String(sshConnectTo); connectTo != "" { parts := strings.Split(connectTo, ":") switch len(parts) { case 1: options.OriginURL = fmt.Sprintf("https://%s", parts[0]) case 2: options.OriginURL = fmt.Sprintf("https://%s:%s", parts[0], parts[1]) case 3: options.OriginURL = fmt.Sprintf("https://%s:%s", parts[2], parts[1]) options.TLSClientConfig = &tls.Config{ InsecureSkipVerify: true, // #nosec G402 ServerName: parts[0], } log.Warn().Msgf("Using insecure SSL connection because SNI overridden to %s", parts[0]) default: return fmt.Errorf("invalid connection override: %s", connectTo) } } // we could add a cmd line variable for this bool if we want the SOCK5 server to be on the client side wsConn := carrier.NewWSConnection(log) if c.NArg() > 0 || c.IsSet(sshURLFlag) { forwarder, err := config.ValidateUrl(c, true) if err != nil { log.Err(err).Msg("Error validating origin URL") return errors.Wrap(err, "error validating origin URL") } log.Info().Str(LogFieldHost, forwarder.Host).Msg("Start Websocket listener") err = carrier.StartForwarder(wsConn, forwarder.Host, shutdownC, options) if err != nil { log.Err(err).Msg("Error on Websocket listener") } return err } var s io.ReadWriter s = &carrier.StdinoutStream{} if c.IsSet(sshDebugStream) { maxMessages := c.Uint64(sshDebugStream) if maxMessages == 0 { // default to 10 if provided but unset maxMessages = 10 } logger := log.With().Str("host", url.Host).Logger() s = stream.NewDebugStream(s, &logger, maxMessages) } return carrier.StartClient(wsConn, s, options) } ================================================ FILE: cmd/cloudflared/access/cmd.go ================================================ package access import ( "fmt" "io" "net/http" "net/url" "os" "os/exec" "strings" "text/template" "time" "github.com/getsentry/sentry-go" "github.com/pkg/errors" "github.com/rs/zerolog" "github.com/urfave/cli/v2" "golang.org/x/net/idna" "github.com/cloudflare/cloudflared/carrier" "github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil" cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags" "github.com/cloudflare/cloudflared/logger" "github.com/cloudflare/cloudflared/sshgen" "github.com/cloudflare/cloudflared/token" "github.com/cloudflare/cloudflared/validation" ) const ( appURLFlag = "app" loginQuietFlag = "quiet" sshHostnameFlag = "hostname" sshDestinationFlag = "destination" sshURLFlag = "url" sshHeaderFlag = "header" sshTokenIDFlag = "service-token-id" sshTokenSecretFlag = "service-token-secret" sshGenCertFlag = "short-lived-cert" sshConnectTo = "connect-to" sshDebugStream = "debug-stream" sshConfigTemplate = ` Add to your {{.Home}}/.ssh/config: {{- if .ShortLivedCerts}} Match host {{.Hostname}} exec "{{.Cloudflared}} access ssh-gen --hostname %h" ProxyCommand {{.Cloudflared}} access ssh --hostname %h IdentityFile ~/.cloudflared/%h-cf_key CertificateFile ~/.cloudflared/%h-cf_key-cert.pub {{- else}} Host {{.Hostname}} ProxyCommand {{.Cloudflared}} access ssh --hostname %h {{end}} ` fedrampFlag = "fedramp" ) const sentryDSN = "https://56a9c9fa5c364ab28f34b14f35ea0f1b@sentry.io/189878" var ( shutdownC chan struct{} userAgent = "DEV" ) // Init will initialize and store vars from the main program func Init(shutdown chan struct{}, version string) { shutdownC = shutdown userAgent = fmt.Sprintf("cloudflared/%s", version) } // Flags return the global flags for Access related commands (hopefully none) func Flags() []cli.Flag { return []cli.Flag{} // no flags yet. } // Commands returns all the Access related subcommands func Commands() []*cli.Command { return []*cli.Command{ { Name: "access", Aliases: []string{"forward"}, Category: "Access", Usage: "access ", Flags: []cli.Flag{&cli.BoolFlag{ Name: fedrampFlag, Usage: "use when performing operations in fedramp account", }}, Description: `Cloudflare Access protects internal resources by securing, authenticating and monitoring access per-user and by application. With Cloudflare Access, only authenticated users with the required permissions are able to reach sensitive resources. The commands provided here allow you to interact with Access protected applications from the command line.`, Subcommands: []*cli.Command{ { Name: "login", Action: cliutil.Action(login), Usage: "login ", ArgsUsage: "url of Access application", Description: `The login subcommand initiates an authentication flow with your identity provider. The subcommand will launch a browser. For headless systems, a url is provided. Once authenticated with your identity provider, the login command will generate a JSON Web Token (JWT) scoped to your identity, the application you intend to reach, and valid for a session duration set by your administrator. cloudflared stores the token in local storage.`, Flags: []cli.Flag{ &cli.BoolFlag{ Name: loginQuietFlag, Aliases: []string{"q"}, Usage: "do not print the jwt to the command line", }, &cli.BoolFlag{ Name: "no-verbose", Usage: "print only the jwt to stdout", }, &cli.BoolFlag{ Name: "auto-close", Usage: "automatically close the auth interstitial after action", }, &cli.StringFlag{ Name: appURLFlag, }, }, }, { Name: "curl", Action: cliutil.Action(curl), Usage: "curl [--allow-request, -ar] [...]", Description: `The curl subcommand wraps curl and automatically injects the JWT into a cf-access-token header when using curl to reach an application behind Access.`, ArgsUsage: "allow-request will allow the curl request to continue even if the jwt is not present.", SkipFlagParsing: true, }, { Name: "token", Action: cliutil.Action(generateToken), Usage: "token ", ArgsUsage: "url of Access application", Description: `The token subcommand produces a JWT which can be used to authenticate requests.`, Flags: []cli.Flag{ &cli.StringFlag{ Name: appURLFlag, }, }, }, { Name: "tcp", Action: cliutil.Action(ssh), Aliases: []string{"rdp", "ssh", "smb"}, Usage: "", ArgsUsage: "", Description: `The tcp subcommand sends data over a proxy to the Cloudflare edge.`, Flags: []cli.Flag{ &cli.StringFlag{ Name: sshHostnameFlag, Aliases: []string{"tunnel-host", "T"}, Usage: "specify the hostname of your application.", EnvVars: []string{"TUNNEL_SERVICE_HOSTNAME"}, }, &cli.StringFlag{ Name: sshDestinationFlag, Usage: "specify the destination address of your SSH server.", EnvVars: []string{"TUNNEL_SERVICE_DESTINATION"}, }, &cli.StringFlag{ Name: sshURLFlag, Aliases: []string{"listener", "L"}, Usage: "specify the host:port to forward data to Cloudflare edge.", EnvVars: []string{"TUNNEL_SERVICE_URL"}, }, &cli.StringSliceFlag{ Name: sshHeaderFlag, Aliases: []string{"H"}, Usage: "specify additional headers you wish to send.", }, &cli.StringFlag{ Name: sshTokenIDFlag, Aliases: []string{"id"}, Usage: "specify an Access service token ID you wish to use.", EnvVars: []string{"TUNNEL_SERVICE_TOKEN_ID"}, }, &cli.StringFlag{ Name: sshTokenSecretFlag, Aliases: []string{"secret"}, Usage: "specify an Access service token secret you wish to use.", EnvVars: []string{"TUNNEL_SERVICE_TOKEN_SECRET"}, }, &cli.StringFlag{ Name: cfdflags.LogFile, Usage: "Save application log to this file for reporting issues.", }, &cli.StringFlag{ Name: cfdflags.LogDirectory, Usage: "Save application log to this directory for reporting issues.", }, &cli.StringFlag{ Name: cfdflags.LogLevelSSH, Aliases: []string{"loglevel"}, //added to match the tunnel side Usage: "Application logging level {debug, info, warn, error, fatal}. ", }, &cli.StringFlag{ Name: sshConnectTo, Hidden: true, Usage: "Connect to alternate location for testing, value is host, host:port, or sni:port:host", }, &cli.Uint64Flag{ Name: sshDebugStream, Hidden: true, Usage: "Writes up-to the max provided stream payloads to the logger as debug statements.", }, }, }, { Name: "ssh-config", Action: cliutil.Action(sshConfig), Usage: "", Description: `Prints an example configuration ~/.ssh/config`, Flags: []cli.Flag{ &cli.StringFlag{ Name: sshHostnameFlag, Usage: "specify the hostname of your application.", }, &cli.BoolFlag{ Name: sshGenCertFlag, Usage: "specify if you wish to generate short lived certs.", }, }, }, { Name: "ssh-gen", Action: cliutil.Action(sshGen), Usage: "", Description: `Generates a short lived certificate for given hostname`, Flags: []cli.Flag{ &cli.StringFlag{ Name: sshHostnameFlag, Usage: "specify the hostname of your application.", }, }, }, }, }, } } // login pops up the browser window to do the actual login and JWT generation func login(c *cli.Context) error { err := sentry.Init(sentry.ClientOptions{ Dsn: sentryDSN, Release: c.App.Version, }) if err != nil { return err } log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog) appURL, err := getAppURLFromArgs(c) if err != nil { log.Error().Msg("Please provide the url of the Access application") return err } appInfo, err := token.GetAppInfo(appURL) if err != nil { return err } if err := verifyTokenAtEdge(appURL, appInfo, c, log); err != nil { log.Err(err).Msg("Could not verify token") return err } cfdToken, err := token.GetAppTokenIfExists(appInfo) if err != nil { fmt.Fprintln(os.Stderr, "Unable to find token for provided application.") return err } else if cfdToken == "" { fmt.Fprintln(os.Stderr, "token for provided application was empty.") return errors.New("empty application token") } if c.Bool(loginQuietFlag) { return nil } // Chatty by default for backward compat. The new --app flag // is an implicit opt-out of the backwards-compatible chatty output. if c.Bool("no-verbose") || c.IsSet(appURLFlag) { fmt.Fprint(os.Stdout, cfdToken) } else { fmt.Fprintf(os.Stdout, "Successfully fetched your token:\n\n%s\n\n", cfdToken) } return nil } // curl provides a wrapper around curl, passing Access JWT along in request func curl(c *cli.Context) error { err := sentry.Init(sentry.ClientOptions{ Dsn: sentryDSN, Release: c.App.Version, }) if err != nil { return err } log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog) args := c.Args() if args.Len() < 1 { log.Error().Msg("Please provide the access app and command you wish to run.") return errors.New("incorrect args") } cmdArgs, allowRequest := parseAllowRequest(args.Slice()) appURL, err := getAppURL(cmdArgs, log) if err != nil { return err } appInfo, err := token.GetAppInfo(appURL) if err != nil { return err } // Verify that the existing token is still good; if not fetch a new one if err := verifyTokenAtEdge(appURL, appInfo, c, log); err != nil { log.Err(err).Msg("Could not verify token") return err } tok, err := token.GetAppTokenIfExists(appInfo) if err != nil || tok == "" { if allowRequest { log.Info().Msg("You don't have an Access token set. Please run access token to fetch one.") return run("curl", cmdArgs...) } tok, err = token.FetchToken(appURL, appInfo, c.Bool(cfdflags.AutoCloseInterstitial), c.Bool(fedrampFlag), log) if err != nil { log.Err(err).Msg("Failed to refresh token") return err } } cmdArgs = append(cmdArgs, "-H") cmdArgs = append(cmdArgs, fmt.Sprintf("%s: %s", carrier.CFAccessTokenHeader, tok)) return run("curl", cmdArgs...) } // run kicks off a shell task and pipe the results to the respective std pipes func run(cmd string, args ...string) error { c := exec.Command(cmd, args...) c.Stdin = os.Stdin stderr, err := c.StderrPipe() if err != nil { return err } go func() { _, _ = io.Copy(os.Stderr, stderr) }() stdout, err := c.StdoutPipe() if err != nil { return err } go func() { _, _ = io.Copy(os.Stdout, stdout) }() return c.Run() } func getAppURLFromArgs(c *cli.Context) (*url.URL, error) { var appURLStr string args := c.Args() if args.Len() < 1 { appURLStr = c.String(appURLFlag) } else { appURLStr = args.First() } return parseURL(appURLStr) } // token dumps provided token to stdout func generateToken(c *cli.Context) error { err := sentry.Init(sentry.ClientOptions{ Dsn: sentryDSN, Release: c.App.Version, }) if err != nil { return err } appURL, err := getAppURLFromArgs(c) if err != nil { fmt.Fprintln(os.Stderr, "Please provide a url.") return err } appInfo, err := token.GetAppInfo(appURL) if err != nil { return err } tok, err := token.GetAppTokenIfExists(appInfo) if err != nil || tok == "" { fmt.Fprintln(os.Stderr, "Unable to find token for provided application. Please run login command to generate token.") return err } if _, err := fmt.Fprint(os.Stdout, tok); err != nil { fmt.Fprintln(os.Stderr, "Failed to write token to stdout.") return err } return nil } // sshConfig prints an example SSH config to stdout func sshConfig(c *cli.Context) error { genCertBool := c.Bool(sshGenCertFlag) hostname := c.String(sshHostnameFlag) if hostname == "" { hostname = "[your hostname]" } type config struct { Home string ShortLivedCerts bool Hostname string Cloudflared string } t := template.Must(template.New("sshConfig").Parse(sshConfigTemplate)) return t.Execute(os.Stdout, config{Home: os.Getenv("HOME"), ShortLivedCerts: genCertBool, Hostname: hostname, Cloudflared: cloudflaredPath()}) } // sshGen generates a short lived certificate for provided hostname func sshGen(c *cli.Context) error { log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog) // get the hostname from the cmdline and error out if its not provided rawHostName := c.String(sshHostnameFlag) hostname, err := validation.ValidateHostname(rawHostName) if err != nil || rawHostName == "" { return cli.ShowCommandHelp(c, "ssh-gen") } originURL, err := parseURL(hostname) if err != nil { return err } // this fetchToken function mutates the appURL param. We should refactor that fetchTokenURL := &url.URL{} *fetchTokenURL = *originURL appInfo, err := token.GetAppInfo(fetchTokenURL) if err != nil { return err } cfdToken, err := token.FetchTokenWithRedirect(fetchTokenURL, appInfo, c.Bool(cfdflags.AutoCloseInterstitial), c.Bool(fedrampFlag), log) if err != nil { return err } if err := sshgen.GenerateShortLivedCertificate(originURL, cfdToken); err != nil { return err } return nil } // getAppURL will pull the request URL needed for fetching a user's Access token func getAppURL(cmdArgs []string, log *zerolog.Logger) (*url.URL, error) { if len(cmdArgs) < 1 { log.Error().Msg("Please provide a valid URL as the first argument to curl.") return nil, errors.New("not a valid url") } u, err := processURL(cmdArgs[0]) if err != nil { log.Error().Msg("Please provide a valid URL as the first argument to curl.") return nil, err } return u, err } // parseAllowRequest will parse cmdArgs and return a copy of the args and result // of the allow request was present func parseAllowRequest(cmdArgs []string) ([]string, bool) { if len(cmdArgs) > 1 { if cmdArgs[0] == "--allow-request" || cmdArgs[0] == "-ar" { return cmdArgs[1:], true } } return cmdArgs, false } // processURL will preprocess the string (parse to a url, convert to punycode, etc). func processURL(s string) (*url.URL, error) { u, err := url.ParseRequestURI(s) if err != nil { return nil, err } if u.Host == "" { return nil, errors.New("not a valid host") } host, err := idna.ToASCII(u.Hostname()) if err != nil { // we fail to convert to punycode, just return the url we parsed. return u, nil } if u.Port() != "" { u.Host = fmt.Sprintf("%s:%s", host, u.Port()) } else { u.Host = host } return u, nil } // cloudflaredPath pulls the full path of cloudflared on disk func cloudflaredPath() string { path, err := os.Executable() if err == nil && isFileThere(path) { return path } for _, p := range strings.Split(os.Getenv("PATH"), ":") { path := fmt.Sprintf("%s/%s", p, "cloudflared") if isFileThere(path) { return path } } return "cloudflared" } // isFileThere will check for the presence of candidate path func isFileThere(candidate string) bool { fi, err := os.Stat(candidate) if err != nil || fi.IsDir() || !fi.Mode().IsRegular() { return false } return true } // verifyTokenAtEdge checks for a token on disk, or generates a new one. // Then makes a request to the origin with the token to ensure it is valid. // Returns nil if token is valid. func verifyTokenAtEdge(appUrl *url.URL, appInfo *token.AppInfo, c *cli.Context, log *zerolog.Logger) error { headers := parseRequestHeaders(c.StringSlice(sshHeaderFlag)) if c.IsSet(sshTokenIDFlag) { headers.Add(cfAccessClientIDHeader, c.String(sshTokenIDFlag)) } if c.IsSet(sshTokenSecretFlag) { headers.Add(cfAccessClientSecretHeader, c.String(sshTokenSecretFlag)) } options := &carrier.StartOptions{AppInfo: appInfo, OriginURL: appUrl.String(), Headers: headers, AutoCloseInterstitial: c.Bool(cfdflags.AutoCloseInterstitial), IsFedramp: c.Bool(fedrampFlag)} if valid, err := isTokenValid(options, log); err != nil { return err } else if valid { return nil } if err := token.RemoveTokenIfExists(appInfo); err != nil { return err } if valid, err := isTokenValid(options, log); err != nil { return err } else if !valid { return errors.New("failed to verify token") } return nil } // isTokenValid makes a request to the origin and returns true if the response was not a 302. func isTokenValid(options *carrier.StartOptions, log *zerolog.Logger) (bool, error) { req, err := carrier.BuildAccessRequest(options, log) if err != nil { return false, errors.Wrap(err, "Could not create access request") } req.Header.Set("User-Agent", userAgent) query := req.URL.Query() query.Set("cloudflared_token_check", "true") req.URL.RawQuery = query.Encode() // Do not follow redirects client := &http.Client{ CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, Timeout: time.Second * 5, } resp, err := client.Do(req) if err != nil { return false, err } defer resp.Body.Close() // A redirect to login means the token was invalid. return !carrier.IsAccessResponse(resp), nil } ================================================ FILE: cmd/cloudflared/access/validation.go ================================================ package access import ( "errors" "fmt" "net/http" "net/url" "strings" "golang.org/x/net/http/httpguts" ) // parseRequestHeaders will take user-provided header values as strings "Content-Type: application/json" and create // a http.Header object. func parseRequestHeaders(values []string) http.Header { headers := make(http.Header) for _, valuePair := range values { header, value, found := strings.Cut(valuePair, ":") if found { headers.Add(strings.TrimSpace(header), strings.TrimSpace(value)) } } return headers } // parseHostname will attempt to convert a user provided URL string into a string with some light error checking on // certain expectations from the URL. // Will convert all HTTP URLs to HTTPS func parseURL(input string) (*url.URL, error) { if input == "" { return nil, errors.New("no input provided") } if !strings.HasPrefix(input, "https://") && !strings.HasPrefix(input, "http://") { input = fmt.Sprintf("https://%s", input) } url, err := url.ParseRequestURI(input) if err != nil { return nil, fmt.Errorf("failed to parse as URL: %w", err) } if url.Scheme != "https" { url.Scheme = "https" } if url.Host == "" { return nil, errors.New("failed to parse Host") } host, err := httpguts.PunycodeHostPort(url.Host) if err != nil || host == "" { return nil, err } if !httpguts.ValidHostHeader(host) { return nil, errors.New("invalid Host provided") } url.Host = host return url, nil } ================================================ FILE: cmd/cloudflared/access/validation_test.go ================================================ package access import ( "fmt" "testing" "github.com/stretchr/testify/assert" ) func TestParseRequestHeaders(t *testing.T) { values := parseRequestHeaders([]string{"client: value", "secret: safe-value", "trash", "cf-trace-id: 000:000:0:1:asd"}) assert.Len(t, values, 3) assert.Equal(t, "value", values.Get("client")) assert.Equal(t, "safe-value", values.Get("secret")) assert.Equal(t, "000:000:0:1:asd", values.Get("cf-trace-id")) } func TestParseURL(t *testing.T) { schemes := []string{ "http://", "https://", "", } hosts := []struct { input string expected string }{ {"localhost", "localhost"}, {"127.0.0.1", "127.0.0.1"}, {"127.0.0.1:9090", "127.0.0.1:9090"}, {"::1", "::1"}, {"::1:8080", "::1:8080"}, {"[::1]", "[::1]"}, {"[::1]:8080", "[::1]:8080"}, {":8080", ":8080"}, {"example.com", "example.com"}, {"hello.example.com", "hello.example.com"}, {"bücher.example.com", "xn--bcher-kva.example.com"}, } paths := []string{ "", "/test", "/example.com?qwe=123", } for i, scheme := range schemes { for j, host := range hosts { for k, path := range paths { t.Run(fmt.Sprintf("%d_%d_%d", i, j, k), func(t *testing.T) { input := fmt.Sprintf("%s%s%s", scheme, host.input, path) expected := fmt.Sprintf("%s%s%s", "https://", host.expected, path) url, err := parseURL(input) assert.NoError(t, err, "input: %s\texpected: %s", input, expected) assert.Equal(t, expected, url.String()) assert.Equal(t, host.expected, url.Host) assert.Equal(t, "https", url.Scheme) }) } } } t.Run("no input", func(t *testing.T) { _, err := parseURL("") assert.ErrorContains(t, err, "no input provided") }) t.Run("missing host", func(t *testing.T) { _, err := parseURL("https:///host") assert.ErrorContains(t, err, "failed to parse Host") }) t.Run("invalid path only", func(t *testing.T) { _, err := parseURL("/host") assert.ErrorContains(t, err, "failed to parse Host") }) t.Run("invalid parse URL", func(t *testing.T) { _, err := parseURL("https://host\\host") assert.ErrorContains(t, err, "failed to parse as URL") }) } ================================================ FILE: cmd/cloudflared/app_forward_service.go ================================================ package main import ( "github.com/rs/zerolog" "github.com/cloudflare/cloudflared/cmd/cloudflared/access" "github.com/cloudflare/cloudflared/config" ) // ForwardServiceType is used to identify what kind of overwatch service this is const ForwardServiceType = "forward" // ForwarderService is used to wrap the access package websocket forwarders // into a service model for the overwatch package. // it also holds a reference to the config object that represents its state type ForwarderService struct { forwarder config.Forwarder shutdown chan struct{} log *zerolog.Logger } // NewForwardService creates a new forwarder service func NewForwardService(f config.Forwarder, log *zerolog.Logger) *ForwarderService { return &ForwarderService{forwarder: f, shutdown: make(chan struct{}, 1), log: log} } // Name is used to figure out this service is related to the others (normally the addr it binds to) // e.g. localhost:78641 or 127.0.0.1:2222 since this is a websocket forwarder func (s *ForwarderService) Name() string { return s.forwarder.Listener } // Type is used to identify what kind of overwatch service this is func (s *ForwarderService) Type() string { return ForwardServiceType } // Hash is used to figure out if this forwarder is the unchanged or not from the config file updates func (s *ForwarderService) Hash() string { return s.forwarder.Hash() } // Shutdown stops the websocket listener func (s *ForwarderService) Shutdown() { s.shutdown <- struct{}{} } // Run is the run loop that is started by the overwatch service func (s *ForwarderService) Run() error { return access.StartForwarder(s.forwarder, s.shutdown, s.log) } ================================================ FILE: cmd/cloudflared/app_service.go ================================================ package main import ( "github.com/rs/zerolog" "github.com/cloudflare/cloudflared/config" "github.com/cloudflare/cloudflared/overwatch" ) // AppService is the main service that runs when no command lines flags are passed to cloudflared // it manages all the running services such as tunnels, forwarders, etc type AppService struct { configManager config.Manager serviceManager overwatch.Manager shutdownC chan struct{} configUpdateChan chan config.Root log *zerolog.Logger } // NewAppService creates a new AppService with needed supporting services func NewAppService(configManager config.Manager, serviceManager overwatch.Manager, shutdownC chan struct{}, log *zerolog.Logger) *AppService { return &AppService{ configManager: configManager, serviceManager: serviceManager, shutdownC: shutdownC, configUpdateChan: make(chan config.Root), log: log, } } // Run starts the run loop to handle config updates and run forwarders, tunnels, etc func (s *AppService) Run() error { go s.actionLoop() return s.configManager.Start(s) } // Shutdown kills all the running services func (s *AppService) Shutdown() error { s.configManager.Shutdown() s.shutdownC <- struct{}{} return nil } // ConfigDidUpdate is a delegate notification from the config manager // it is trigger when the config file has been updated and now the service needs // to update its services accordingly func (s *AppService) ConfigDidUpdate(c config.Root) { s.configUpdateChan <- c } // actionLoop handles the actions from running processes func (s *AppService) actionLoop() { for { select { case c := <-s.configUpdateChan: s.handleConfigUpdate(c) case <-s.shutdownC: for _, service := range s.serviceManager.Services() { service.Shutdown() } return } } } func (s *AppService) handleConfigUpdate(c config.Root) { // handle the client forward listeners activeServices := map[string]struct{}{} for _, f := range c.Forwarders { service := NewForwardService(f, s.log) s.serviceManager.Add(service) activeServices[service.Name()] = struct{}{} } // TODO: TUN-1451 - tunnels // remove any services that are no longer active for _, service := range s.serviceManager.Services() { if _, ok := activeServices[service.Name()]; !ok { s.serviceManager.Remove(service.Name()) } } } ================================================ FILE: cmd/cloudflared/cliutil/build_info.go ================================================ package cliutil import ( "crypto/sha256" "fmt" "io" "os" "runtime" "github.com/rs/zerolog" ) type BuildInfo struct { GoOS string `json:"go_os"` GoVersion string `json:"go_version"` GoArch string `json:"go_arch"` BuildType string `json:"build_type"` CloudflaredVersion string `json:"cloudflared_version"` Checksum string `json:"checksum"` } func GetBuildInfo(buildType, version string) *BuildInfo { return &BuildInfo{ GoOS: runtime.GOOS, GoVersion: runtime.Version(), GoArch: runtime.GOARCH, BuildType: buildType, CloudflaredVersion: version, Checksum: currentBinaryChecksum(), } } func (bi *BuildInfo) Log(log *zerolog.Logger) { log.Info().Msgf("Version %s (Checksum %s)", bi.CloudflaredVersion, bi.Checksum) if bi.BuildType != "" { log.Info().Msgf("Built%s", bi.GetBuildTypeMsg()) } log.Info().Msgf("GOOS: %s, GOVersion: %s, GoArch: %s", bi.GoOS, bi.GoVersion, bi.GoArch) } func (bi *BuildInfo) OSArch() string { return fmt.Sprintf("%s_%s", bi.GoOS, bi.GoArch) } func (bi *BuildInfo) Version() string { return bi.CloudflaredVersion } func (bi *BuildInfo) GetBuildTypeMsg() string { if bi.BuildType == "" { return "" } return fmt.Sprintf(" with %s", bi.BuildType) } func (bi *BuildInfo) UserAgent() string { return fmt.Sprintf("cloudflared/%s", bi.CloudflaredVersion) } // FileChecksum opens a file and returns the SHA256 checksum. func FileChecksum(filePath string) (string, error) { f, err := os.Open(filePath) if err != nil { return "", err } defer f.Close() h := sha256.New() if _, err := io.Copy(h, f); err != nil { return "", err } return fmt.Sprintf("%x", h.Sum(nil)), nil } func currentBinaryChecksum() string { currentPath, err := os.Executable() if err != nil { return "" } sum, _ := FileChecksum(currentPath) return sum } ================================================ FILE: cmd/cloudflared/cliutil/deprecated.go ================================================ package cliutil import ( "fmt" "github.com/urfave/cli/v2" ) func RemovedCommand(name string) *cli.Command { return &cli.Command{ Name: name, Action: func(context *cli.Context) error { return cli.Exit( fmt.Sprintf("%s command is no longer supported by cloudflared. Consult Cloudflare Tunnel documentation for possible alternative solutions.", name), -1, ) }, Description: fmt.Sprintf("%s is deprecated", name), Hidden: true, } } ================================================ FILE: cmd/cloudflared/cliutil/errors.go ================================================ package cliutil import ( "fmt" "github.com/urfave/cli/v2" ) type usageError string func (ue usageError) Error() string { return string(ue) } func UsageError(format string, args ...interface{}) error { if len(args) == 0 { return usageError(format) } else { msg := fmt.Sprintf(format, args...) return usageError(msg) } } // Ensures exit with error code if actionFunc returns an error func WithErrorHandler(actionFunc cli.ActionFunc) cli.ActionFunc { return func(ctx *cli.Context) error { err := actionFunc(ctx) if err != nil { if _, ok := err.(usageError); ok { msg := fmt.Sprintf("%s\nSee 'cloudflared %s --help'.", err.Error(), ctx.Command.FullName()) err = cli.Exit(msg, -1) } else if _, ok := err.(cli.ExitCoder); !ok { err = cli.Exit(err.Error(), 1) } } return err } } ================================================ FILE: cmd/cloudflared/cliutil/handler.go ================================================ package cliutil import ( "github.com/urfave/cli/v2" "github.com/urfave/cli/v2/altsrc" "github.com/cloudflare/cloudflared/config" "github.com/cloudflare/cloudflared/logger" ) func Action(actionFunc cli.ActionFunc) cli.ActionFunc { return WithErrorHandler(actionFunc) } func ConfiguredAction(actionFunc cli.ActionFunc) cli.ActionFunc { // Adapt actionFunc to the type signature required by ConfiguredActionWithWarnings f := func(context *cli.Context, _ string) error { return actionFunc(context) } return ConfiguredActionWithWarnings(f) } // Just like ConfiguredAction, but accepts a second parameter with configuration warnings. func ConfiguredActionWithWarnings(actionFunc func(*cli.Context, string) error) cli.ActionFunc { return WithErrorHandler(func(c *cli.Context) error { warnings, err := setFlagsFromConfigFile(c) if err != nil { return err } return actionFunc(c, warnings) }) } func setFlagsFromConfigFile(c *cli.Context) (configWarnings string, err error) { const errorExitCode = 1 log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog) inputSource, warnings, err := config.ReadConfigFile(c, log) if err != nil { if err == config.ErrNoConfigFile { return "", nil } return "", cli.Exit(err, errorExitCode) } if err := altsrc.ApplyInputSource(c, inputSource); err != nil { return "", cli.Exit(err, errorExitCode) } return warnings, nil } ================================================ FILE: cmd/cloudflared/cliutil/logger.go ================================================ package cliutil import ( "github.com/urfave/cli/v2" "github.com/urfave/cli/v2/altsrc" "github.com/cloudflare/cloudflared/cmd/cloudflared/flags" ) var ( debugLevelWarning = "At debug level cloudflared will log request URL, method, protocol, content length, as well as, all request and response headers. " + "This can expose sensitive information in your logs." FlagLogOutput = &cli.StringFlag{ Name: flags.LogFormatOutput, Usage: "Output format for the logs (default, json)", Value: flags.LogFormatOutputValueDefault, EnvVars: []string{"TUNNEL_MANAGEMENT_OUTPUT", "TUNNEL_LOG_OUTPUT"}, } ) func ConfigureLoggingFlags(shouldHide bool) []cli.Flag { return []cli.Flag{ altsrc.NewStringFlag(&cli.StringFlag{ Name: flags.LogLevel, Value: "info", Usage: "Application logging level {debug, info, warn, error, fatal}. " + debugLevelWarning, EnvVars: []string{"TUNNEL_LOGLEVEL"}, Hidden: shouldHide, }), altsrc.NewStringFlag(&cli.StringFlag{ Name: flags.TransportLogLevel, Aliases: []string{"proto-loglevel"}, // This flag used to be called proto-loglevel Value: "info", Usage: "Transport logging level(previously called protocol logging level) {debug, info, warn, error, fatal}", EnvVars: []string{"TUNNEL_PROTO_LOGLEVEL", "TUNNEL_TRANSPORT_LOGLEVEL"}, Hidden: shouldHide, }), altsrc.NewStringFlag(&cli.StringFlag{ Name: flags.LogFile, Usage: "Save application log to this file for reporting issues.", EnvVars: []string{"TUNNEL_LOGFILE"}, Hidden: shouldHide, }), altsrc.NewStringFlag(&cli.StringFlag{ Name: flags.LogDirectory, Usage: "Save application log to this directory for reporting issues.", EnvVars: []string{"TUNNEL_LOGDIRECTORY"}, Hidden: shouldHide, }), altsrc.NewStringFlag(&cli.StringFlag{ Name: flags.TraceOutput, Usage: "Name of trace output file, generated when cloudflared stops.", EnvVars: []string{"TUNNEL_TRACE_OUTPUT"}, Hidden: shouldHide, }), FlagLogOutput, } } ================================================ FILE: cmd/cloudflared/cliutil/management.go ================================================ package cliutil import ( "errors" "fmt" "io" "os" "time" "github.com/google/uuid" "github.com/mattn/go-colorable" "github.com/rs/zerolog" "github.com/urfave/cli/v2" "github.com/cloudflare/cloudflared/cfapi" cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags" "github.com/cloudflare/cloudflared/credentials" ) // Error definitions for management token operations var ( ErrNoTunnelID = errors.New("no tunnel ID provided") ErrInvalidTunnelID = errors.New("unable to parse provided tunnel id as a valid UUID") ) // GetManagementToken acquires a management token from Cloudflare API for the specified resource func GetManagementToken(c *cli.Context, log *zerolog.Logger, res cfapi.ManagementResource, buildInfo *BuildInfo) (string, error) { userCreds, err := credentials.Read(c.String(cfdflags.OriginCert), log) if err != nil { return "", err } var apiURL string if userCreds.IsFEDEndpoint() { apiURL = credentials.FedRampBaseApiURL } else { apiURL = c.String(cfdflags.ApiURL) } client, err := userCreds.Client(apiURL, buildInfo.UserAgent(), log) if err != nil { return "", err } tunnelIDString := c.Args().First() if tunnelIDString == "" { return "", ErrNoTunnelID } tunnelID, err := uuid.Parse(tunnelIDString) if err != nil { return "", fmt.Errorf("%w: %v", ErrInvalidTunnelID, err) } token, err := client.GetManagementToken(tunnelID, res) if err != nil { return "", err } return token, nil } // CreateStderrLogger creates a logger that outputs to stderr to avoid interfering with stdout func CreateStderrLogger(c *cli.Context) *zerolog.Logger { level, levelErr := zerolog.ParseLevel(c.String(cfdflags.LogLevel)) if levelErr != nil { level = zerolog.InfoLevel } var writer io.Writer switch c.String(cfdflags.LogFormatOutput) { case cfdflags.LogFormatOutputValueJSON: // zerolog by default outputs as JSON writer = os.Stderr case cfdflags.LogFormatOutputValueDefault: // "default" and unset use the same logger output format fallthrough default: writer = zerolog.ConsoleWriter{ Out: colorable.NewColorable(os.Stderr), TimeFormat: time.RFC3339, } } log := zerolog.New(writer).With().Timestamp().Logger().Level(level) return &log } ================================================ FILE: cmd/cloudflared/common_service.go ================================================ package main import ( "github.com/rs/zerolog" "github.com/urfave/cli/v2" "github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil" "github.com/cloudflare/cloudflared/cmd/cloudflared/tunnel" ) func buildArgsForToken(c *cli.Context, log *zerolog.Logger) ([]string, error) { token := c.Args().First() if _, err := tunnel.ParseToken(token); err != nil { return nil, cliutil.UsageError("Provided tunnel token is not valid (%s).", err) } return []string{ "tunnel", "run", "--token", token, }, nil } func getServiceExtraArgsFromCliArgs(c *cli.Context, log *zerolog.Logger) ([]string, error) { if c.NArg() > 0 { // currently, we only support extra args for token return buildArgsForToken(c, log) } else { // empty extra args return make([]string, 0), nil } } ================================================ FILE: cmd/cloudflared/flags/flags.go ================================================ package flags const ( // HaConnections specifies how many connections to make to the edge HaConnections = "ha-connections" // SshPort is the port on localhost the cloudflared ssh server will run on SshPort = "local-ssh-port" // SshIdleTimeout defines the duration a SSH session can remain idle before being closed SshIdleTimeout = "ssh-idle-timeout" // SshMaxTimeout defines the max duration a SSH session can remain open for SshMaxTimeout = "ssh-max-timeout" // SshLogUploaderBucketName is the bucket name to use for the SSH log uploader SshLogUploaderBucketName = "bucket-name" // SshLogUploaderRegionName is the AWS region name to use for the SSH log uploader SshLogUploaderRegionName = "region-name" // SshLogUploaderSecretID is the Secret id of SSH log uploader SshLogUploaderSecretID = "secret-id" // SshLogUploaderAccessKeyID is the Access key id of SSH log uploader SshLogUploaderAccessKeyID = "access-key-id" // SshLogUploaderSessionTokenID is the Session token of SSH log uploader SshLogUploaderSessionTokenID = "session-token" // SshLogUploaderS3URL is the S3 URL of SSH log uploader (e.g. don't use AWS s3 and use google storage bucket instead) SshLogUploaderS3URL = "s3-url-host" // HostKeyPath is the path of the dir to save SSH host keys too HostKeyPath = "host-key-path" // RpcTimeout is how long to wait for a Capnp RPC request to the edge RpcTimeout = "rpc-timeout" // WriteStreamTimeout sets if we should have a timeout when writing data to a stream towards the destination (edge/origin). WriteStreamTimeout = "write-stream-timeout" // QuicDisablePathMTUDiscovery sets if QUIC should not perform PTMU discovery and use a smaller (safe) packet size. // Packets will then be at most 1252 (IPv4) / 1232 (IPv6) bytes in size. // Note that this may result in packet drops for UDP proxying, since we expect being able to send at least 1280 bytes of inner packets. QuicDisablePathMTUDiscovery = "quic-disable-pmtu-discovery" // QuicConnLevelFlowControlLimit controls the max flow control limit allocated for a QUIC connection. This controls how much data is the // receiver willing to buffer. Once the limit is reached, the sender will send a DATA_BLOCKED frame to indicate it has more data to write, // but it's blocked by flow control QuicConnLevelFlowControlLimit = "quic-connection-level-flow-control-limit" // QuicStreamLevelFlowControlLimit is similar to quicConnLevelFlowControlLimit but for each QUIC stream. When the sender is blocked, // it will send a STREAM_DATA_BLOCKED frame QuicStreamLevelFlowControlLimit = "quic-stream-level-flow-control-limit" // Ui is to enable launching cloudflared in interactive UI mode Ui = "ui" // ConnectorLabel is the command line flag to give a meaningful label to a specific connector ConnectorLabel = "label" // MaxActiveFlows is the command line flag to set the maximum number of flows that cloudflared can be processing at the same time MaxActiveFlows = "max-active-flows" // Tag is the command line flag to set custom tags used to identify this tunnel via added HTTP request headers to the origin Tag = "tag" // Protocol is the command line flag to set the protocol to use to connect to the Cloudflare Edge Protocol = "protocol" // PostQuantum is the command line flag to force the connection to Cloudflare Edge to use Post Quantum cryptography PostQuantum = "post-quantum" // Features is the command line flag to opt into various features that are still being developed or tested Features = "features" // EdgeIpVersion is the command line flag to set the Cloudflare Edge IP address version to connect with EdgeIpVersion = "edge-ip-version" // EdgeBindAddress is the command line flag to bind to IP address for outgoing connections to Cloudflare Edge EdgeBindAddress = "edge-bind-address" // Force is the command line flag to specify if you wish to force an action Force = "force" // Edge is the command line flag to set the address of the Cloudflare tunnel server. Only works in Cloudflare's internal testing environment Edge = "edge" // Region is the command line flag to set the Cloudflare Edge region to connect to Region = "region" // IsAutoUpdated is the command line flag to signal the new process that cloudflared has been autoupdated IsAutoUpdated = "is-autoupdated" // LBPool is the command line flag to set the name of the load balancing pool to add this origin to LBPool = "lb-pool" // Retries is the command line flag to set the maximum number of retries for connection/protocol errors Retries = "retries" // MaxEdgeAddrRetries is the command line flag to set the maximum number of times to retry on edge addrs before falling back to a lower protocol MaxEdgeAddrRetries = "max-edge-addr-retries" // GracePeriod is the command line flag to set the maximum amount of time that cloudflared waits to shut down if it is still serving requests GracePeriod = "grace-period" // ICMPV4Src is the command line flag to set the source address and the interface name to send/receive ICMPv4 messages ICMPV4Src = "icmpv4-src" // ICMPV6Src is the command line flag to set the source address and the interface name to send/receive ICMPv6 messages ICMPV6Src = "icmpv6-src" // Name is the command line to set the name of the tunnel Name = "name" // AutoUpdateFreq is the command line for setting the frequency that cloudflared checks for updates AutoUpdateFreq = "autoupdate-freq" // NoAutoUpdate is the command line flag to disable cloudflared from checking for updates NoAutoUpdate = "no-autoupdate" // LogLevel is the command line flag for the cloudflared logging level LogLevel = "loglevel" // LogLevelSSH is the command line flag for the cloudflared ssh logging level LogLevelSSH = "log-level" // TransportLogLevel is the command line flag for the transport logging level TransportLogLevel = "transport-loglevel" // LogFile is the command line flag to define the file where application logs will be stored LogFile = "logfile" // LogDirectory is the command line flag to define the directory where application logs will be stored. LogDirectory = "log-directory" // LogFormatOutput allows the command line logs to be output as JSON. LogFormatOutput = "output" LogFormatOutputValueDefault = "default" LogFormatOutputValueJSON = "json" // TraceOutput is the command line flag to set the name of trace output file TraceOutput = "trace-output" // OriginCert is the command line flag to define the path for the origin certificate used by cloudflared OriginCert = "origincert" // Metrics is the command line flag to define the address of the metrics server Metrics = "metrics" // MetricsUpdateFreq is the command line flag to define how frequently tunnel metrics are updated MetricsUpdateFreq = "metrics-update-freq" // ApiURL is the command line flag used to define the base URL of the API ApiURL = "api-url" // Virtual DNS resolver service resolver addresses to use instead of dynamically fetching them from the OS. VirtualDNSServiceResolverAddresses = "dns-resolver-addrs" // Management hostname to signify incoming management requests ManagementHostname = "management-hostname" // Automatically close the login interstitial browser window after the user makes a decision. AutoCloseInterstitial = "auto-close" ) ================================================ FILE: cmd/cloudflared/generic_service.go ================================================ //go:build !windows && !darwin && !linux package main import ( "fmt" "os" cli "github.com/urfave/cli/v2" "github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil" ) func runApp(app *cli.App, graceShutdownC chan struct{}) { app.Commands = append(app.Commands, &cli.Command{ Name: "service", Usage: "Manages the cloudflared system service (not supported on this operating system)", Subcommands: []*cli.Command{ { Name: "install", Usage: "Install cloudflared as a system service (not supported on this operating system)", Action: cliutil.ConfiguredAction(installGenericService), }, { Name: "uninstall", Usage: "Uninstall the cloudflared service (not supported on this operating system)", Action: cliutil.ConfiguredAction(uninstallGenericService), }, }, }) app.Run(os.Args) } func installGenericService(c *cli.Context) error { return fmt.Errorf("service installation is not supported on this operating system") } func uninstallGenericService(c *cli.Context) error { return fmt.Errorf("service uninstallation is not supported on this operating system") } ================================================ FILE: cmd/cloudflared/linux_service.go ================================================ //go:build linux package main import ( "fmt" "io" "os" "github.com/rs/zerolog" "github.com/urfave/cli/v2" "github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil" "github.com/cloudflare/cloudflared/cmd/cloudflared/tunnel" "github.com/cloudflare/cloudflared/config" "github.com/cloudflare/cloudflared/logger" ) func runApp(app *cli.App, _ chan struct{}) { app.Commands = append(app.Commands, &cli.Command{ Name: "service", Usage: "Manages the cloudflared system service", Subcommands: []*cli.Command{ { Name: "install", Usage: "Install cloudflared as a system service", Action: cliutil.ConfiguredAction(installLinuxService), Flags: []cli.Flag{ noUpdateServiceFlag, }, }, { Name: "uninstall", Usage: "Uninstall the cloudflared service", Action: cliutil.ConfiguredAction(uninstallLinuxService), }, }, }) _ = app.Run(os.Args) } // The directory and files that are used by the service. // These are hard-coded in the templates below. const ( serviceConfigDir = "/etc/cloudflared" serviceConfigFile = "config.yml" serviceCredentialFile = "cert.pem" serviceConfigPath = serviceConfigDir + "/" + serviceConfigFile cloudflaredService = "cloudflared.service" cloudflaredUpdateService = "cloudflared-update.service" cloudflaredUpdateTimer = "cloudflared-update.timer" ) var systemdAllTemplates = map[string]ServiceTemplate{ cloudflaredService: { Path: fmt.Sprintf("/etc/systemd/system/%s", cloudflaredService), Content: `[Unit] Description=cloudflared After=network-online.target Wants=network-online.target [Service] TimeoutStartSec=15 Type=notify ExecStart={{ .Path }} --no-autoupdate{{ range .ExtraArgs }} {{ . }}{{ end }} Restart=on-failure RestartSec=5s [Install] WantedBy=multi-user.target `, }, cloudflaredUpdateService: { Path: fmt.Sprintf("/etc/systemd/system/%s", cloudflaredUpdateService), Content: `[Unit] Description=Update cloudflared After=network-online.target Wants=network-online.target [Service] ExecStart=/bin/bash -c '{{ .Path }} update; code=$?; if [ $code -eq 11 ]; then systemctl restart cloudflared; exit 0; fi; exit $code' `, }, cloudflaredUpdateTimer: { Path: fmt.Sprintf("/etc/systemd/system/%s", cloudflaredUpdateTimer), Content: `[Unit] Description=Update cloudflared [Timer] OnCalendar=daily [Install] WantedBy=timers.target `, }, } var sysvTemplate = ServiceTemplate{ Path: "/etc/init.d/cloudflared", FileMode: 0755, // nolint: dupword Content: `#!/bin/sh # For RedHat and cousins: # chkconfig: 2345 99 01 # description: cloudflared # processname: {{.Path}} ### BEGIN INIT INFO # Provides: {{.Path}} # Required-Start: # Required-Stop: # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: cloudflared # Description: cloudflared agent ### END INIT INFO name=$(basename $(readlink -f $0)) cmd="{{.Path}} --pidfile /var/run/$name.pid {{ range .ExtraArgs }} {{ . }}{{ end }}" pid_file="/var/run/$name.pid" stdout_log="/var/log/$name.log" stderr_log="/var/log/$name.err" [ -e /etc/sysconfig/$name ] && . /etc/sysconfig/$name get_pid() { cat "$pid_file" } is_running() { [ -f "$pid_file" ] && ps $(get_pid) > /dev/null 2>&1 } case "$1" in start) if is_running; then echo "Already started" else echo "Starting $name" $cmd >> "$stdout_log" 2>> "$stderr_log" & echo $! > "$pid_file" fi ;; stop) if is_running; then echo -n "Stopping $name.." kill $(get_pid) for i in {1..10} do if ! is_running; then break fi echo -n "." sleep 1 done echo if is_running; then echo "Not stopped; may still be shutting down or shutdown may have failed" exit 1 else echo "Stopped" if [ -f "$pid_file" ]; then rm "$pid_file" fi fi else echo "Not running" fi ;; restart) $0 stop if is_running; then echo "Unable to stop, will not attempt to start" exit 1 fi $0 start ;; status) if is_running; then echo "Running" else echo "Stopped" exit 1 fi ;; *) echo "Usage: $0 {start|stop|restart|status}" exit 1 ;; esac exit 0 `, } var noUpdateServiceFlag = &cli.BoolFlag{ Name: "no-update-service", Usage: "Disable auto-update of the cloudflared linux service, which restarts the server to upgrade for new versions.", Value: false, } func isSystemd() bool { if _, err := os.Stat("/run/systemd/system"); err == nil { return true } return false } func installLinuxService(c *cli.Context) error { log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog) etPath, err := os.Executable() if err != nil { return fmt.Errorf("error determining executable path: %v", err) } templateArgs := ServiceTemplateArgs{ Path: etPath, } // Check if the "no update flag" is set autoUpdate := !c.IsSet(noUpdateServiceFlag.Name) var extraArgsFunc func(c *cli.Context, log *zerolog.Logger) ([]string, error) if c.NArg() == 0 { extraArgsFunc = buildArgsForConfig } else { extraArgsFunc = buildArgsForToken } extraArgs, err := extraArgsFunc(c, log) if err != nil { return err } templateArgs.ExtraArgs = extraArgs switch { case isSystemd(): log.Info().Msgf("Using Systemd") err = installSystemd(&templateArgs, autoUpdate, log) default: log.Info().Msgf("Using SysV") err = installSysv(&templateArgs, autoUpdate, log) } if err == nil { log.Info().Msg("Linux service for cloudflared installed successfully") } return err } func buildArgsForConfig(c *cli.Context, log *zerolog.Logger) ([]string, error) { if err := ensureConfigDirExists(serviceConfigDir); err != nil { return nil, err } src, _, err := config.ReadConfigFile(c, log) if err != nil { return nil, err } // can't use context because this command doesn't define "credentials-file" flag configPresent := func(s string) bool { val, err := src.String(s) return err == nil && val != "" } if src.TunnelID == "" || !configPresent(tunnel.CredFileFlag) { return nil, fmt.Errorf(`Configuration file %s must contain entries for the tunnel to run and its associated credentials: tunnel: TUNNEL-UUID credentials-file: CREDENTIALS-FILE `, src.Source()) } if src.Source() != serviceConfigPath { if exists, err := config.FileExists(serviceConfigPath); err != nil || exists { return nil, fmt.Errorf("Possible conflicting configuration in %[1]s and %[2]s. Either remove %[2]s or run `cloudflared --config %[2]s service install`", src.Source(), serviceConfigPath) } if err := copyFile(src.Source(), serviceConfigPath); err != nil { return nil, fmt.Errorf("failed to copy %s to %s: %w", src.Source(), serviceConfigPath, err) } } return []string{ "--config", "/etc/cloudflared/config.yml", "tunnel", "run", }, nil } func installSystemd(templateArgs *ServiceTemplateArgs, autoUpdate bool, log *zerolog.Logger) error { var systemdTemplates []ServiceTemplate if autoUpdate { systemdTemplates = []ServiceTemplate{ systemdAllTemplates[cloudflaredService], systemdAllTemplates[cloudflaredUpdateService], systemdAllTemplates[cloudflaredUpdateTimer], } } else { systemdTemplates = []ServiceTemplate{ systemdAllTemplates[cloudflaredService], } } for _, serviceTemplate := range systemdTemplates { err := serviceTemplate.Generate(templateArgs) if err != nil { log.Err(err).Msg("error generating service template") return err } } if err := runCommand("systemctl", "enable", cloudflaredService); err != nil { log.Err(err).Msgf("systemctl enable %s error", cloudflaredService) return err } if autoUpdate { if err := runCommand("systemctl", "start", cloudflaredUpdateTimer); err != nil { log.Err(err).Msgf("systemctl start %s error", cloudflaredUpdateTimer) return err } } if err := runCommand("systemctl", "daemon-reload"); err != nil { log.Err(err).Msg("systemctl daemon-reload error") return err } return runCommand("systemctl", "start", cloudflaredService) } func installSysv(templateArgs *ServiceTemplateArgs, autoUpdate bool, log *zerolog.Logger) error { confPath, err := sysvTemplate.ResolvePath() if err != nil { log.Err(err).Msg("error resolving system path") return err } if autoUpdate { templateArgs.ExtraArgs = append([]string{"--autoupdate-freq 24h0m0s"}, templateArgs.ExtraArgs...) } else { templateArgs.ExtraArgs = append([]string{"--no-autoupdate"}, templateArgs.ExtraArgs...) } if err := sysvTemplate.Generate(templateArgs); err != nil { log.Err(err).Msg("error generating system template") return err } for _, i := range [...]string{"2", "3", "4", "5"} { if err := os.Symlink(confPath, "/etc/rc"+i+".d/S50et"); err != nil { continue } } for _, i := range [...]string{"0", "1", "6"} { if err := os.Symlink(confPath, "/etc/rc"+i+".d/K02et"); err != nil { continue } } return runCommand("service", "cloudflared", "start") } func uninstallLinuxService(c *cli.Context) error { log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog) var err error switch { case isSystemd(): log.Info().Msg("Using Systemd") err = uninstallSystemd(log) default: log.Info().Msg("Using SysV") err = uninstallSysv(log) } if err == nil { log.Info().Msg("Linux service for cloudflared uninstalled successfully") } return err } func uninstallSystemd(log *zerolog.Logger) error { // Get only the installed services installedServices := make(map[string]ServiceTemplate) for serviceName, serviceTemplate := range systemdAllTemplates { if err := runCommand("systemctl", "list-units", "--all", "|", "grep", serviceName); err == nil { installedServices[serviceName] = serviceTemplate } else { log.Info().Msgf("Service '%s' not installed, skipping its uninstall", serviceName) } } if _, exists := installedServices[cloudflaredService]; exists { if err := runCommand("systemctl", "disable", cloudflaredService); err != nil { log.Err(err).Msgf("systemctl disable %s error", cloudflaredService) return err } if err := runCommand("systemctl", "stop", cloudflaredService); err != nil { log.Err(err).Msgf("systemctl stop %s error", cloudflaredService) return err } } if _, exists := installedServices[cloudflaredUpdateTimer]; exists { if err := runCommand("systemctl", "stop", cloudflaredUpdateTimer); err != nil { log.Err(err).Msgf("systemctl stop %s error", cloudflaredUpdateTimer) return err } } for _, serviceTemplate := range installedServices { if err := serviceTemplate.Remove(); err != nil { log.Err(err).Msg("error removing service template") return err } } if err := runCommand("systemctl", "daemon-reload"); err != nil { log.Err(err).Msg("systemctl daemon-reload error") return err } return nil } func uninstallSysv(log *zerolog.Logger) error { if err := runCommand("service", "cloudflared", "stop"); err != nil { log.Err(err).Msg("service cloudflared stop error") return err } if err := sysvTemplate.Remove(); err != nil { log.Err(err).Msg("error removing service template") return err } for _, i := range [...]string{"2", "3", "4", "5"} { if err := os.Remove("/etc/rc" + i + ".d/S50et"); err != nil { continue } } for _, i := range [...]string{"0", "1", "6"} { if err := os.Remove("/etc/rc" + i + ".d/K02et"); err != nil { continue } } return nil } func ensureConfigDirExists(configDir string) error { ok, err := config.FileExists(configDir) if !ok && err == nil { err = os.Mkdir(configDir, 0755) } return err } func copyFile(src, dest string) error { srcFile, err := os.Open(src) if err != nil { return err } defer srcFile.Close() destFile, err := os.Create(dest) if err != nil { return err } ok := false defer func() { destFile.Close() if !ok { _ = os.Remove(dest) } }() if _, err := io.Copy(destFile, srcFile); err != nil { return err } ok = true return nil } ================================================ FILE: cmd/cloudflared/macos_service.go ================================================ //go:build darwin package main import ( "fmt" "os" homedir "github.com/mitchellh/go-homedir" "github.com/pkg/errors" "github.com/urfave/cli/v2" "github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil" "github.com/cloudflare/cloudflared/logger" ) const ( launchdIdentifier = "com.cloudflare.cloudflared" ) func runApp(app *cli.App, _ chan struct{}) { app.Commands = append(app.Commands, &cli.Command{ Name: "service", Usage: "Manages the cloudflared launch agent", Subcommands: []*cli.Command{ { Name: "install", Usage: "Install cloudflared as an user launch agent", Action: cliutil.ConfiguredAction(installLaunchd), }, { Name: "uninstall", Usage: "Uninstall the cloudflared launch agent", Action: cliutil.ConfiguredAction(uninstallLaunchd), }, }, }) _ = app.Run(os.Args) } func newLaunchdTemplate(installPath, stdoutPath, stderrPath string) *ServiceTemplate { return &ServiceTemplate{ Path: installPath, Content: fmt.Sprintf(` Label %s ProgramArguments {{ .Path }} {{- range $i, $item := .ExtraArgs}} {{ $item }} {{- end}} RunAtLoad StandardOutPath %s StandardErrorPath %s KeepAlive SuccessfulExit ThrottleInterval 5 `, launchdIdentifier, stdoutPath, stderrPath), } } func isRootUser() bool { return os.Geteuid() == 0 } func installPath() (string, error) { // User is root, use /Library/LaunchDaemons instead of home directory if isRootUser() { return fmt.Sprintf("/Library/LaunchDaemons/%s.plist", launchdIdentifier), nil } userHomeDir, err := userHomeDir() if err != nil { return "", err } return fmt.Sprintf("%s/Library/LaunchAgents/%s.plist", userHomeDir, launchdIdentifier), nil } func stdoutPath() (string, error) { if isRootUser() { return fmt.Sprintf("/Library/Logs/%s.out.log", launchdIdentifier), nil } userHomeDir, err := userHomeDir() if err != nil { return "", err } return fmt.Sprintf("%s/Library/Logs/%s.out.log", userHomeDir, launchdIdentifier), nil } func stderrPath() (string, error) { if isRootUser() { return fmt.Sprintf("/Library/Logs/%s.err.log", launchdIdentifier), nil } userHomeDir, err := userHomeDir() if err != nil { return "", err } return fmt.Sprintf("%s/Library/Logs/%s.err.log", userHomeDir, launchdIdentifier), nil } func installLaunchd(c *cli.Context) error { log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog) if isRootUser() { log.Info().Msg("Installing cloudflared client as a system launch daemon. " + "cloudflared client will run at boot") } else { log.Info().Msg("Installing cloudflared client as an user launch agent. " + "Note that cloudflared client will only run when the user is logged in. " + "If you want to run cloudflared client at boot, install with root permission. " + "For more information, visit https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/configure-tunnels/local-management/as-a-service/macos/") } etPath, err := os.Executable() if err != nil { log.Err(err).Msg("Error determining executable path") return fmt.Errorf("Error determining executable path: %v", err) } installPath, err := installPath() if err != nil { log.Err(err).Msg("Error determining install path") return errors.Wrap(err, "Error determining install path") } extraArgs, err := getServiceExtraArgsFromCliArgs(c, log) if err != nil { errMsg := "Unable to determine extra arguments for launch daemon" log.Err(err).Msg(errMsg) return errors.Wrap(err, errMsg) } stdoutPath, err := stdoutPath() if err != nil { log.Err(err).Msg("error determining stdout path") return errors.Wrap(err, "error determining stdout path") } stderrPath, err := stderrPath() if err != nil { log.Err(err).Msg("error determining stderr path") return errors.Wrap(err, "error determining stderr path") } launchdTemplate := newLaunchdTemplate(installPath, stdoutPath, stderrPath) templateArgs := ServiceTemplateArgs{Path: etPath, ExtraArgs: extraArgs} err = launchdTemplate.Generate(&templateArgs) if err != nil { log.Err(err).Msg("error generating launchd template") return err } plistPath, err := launchdTemplate.ResolvePath() if err != nil { log.Err(err).Msg("error resolving launchd template path") return err } log.Info().Msgf("Outputs are logged to %s and %s", stderrPath, stdoutPath) err = runCommand("launchctl", "load", plistPath) if err == nil { log.Info().Msg("MacOS service for cloudflared installed successfully") } return err } func uninstallLaunchd(c *cli.Context) error { log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog) if isRootUser() { log.Info().Msg("Uninstalling cloudflared as a system launch daemon") } else { log.Info().Msg("Uninstalling cloudflared as a user launch agent") } installPath, err := installPath() if err != nil { return errors.Wrap(err, "error determining install path") } stdoutPath, err := stdoutPath() if err != nil { return errors.Wrap(err, "error determining stdout path") } stderrPath, err := stderrPath() if err != nil { return errors.Wrap(err, "error determining stderr path") } launchdTemplate := newLaunchdTemplate(installPath, stdoutPath, stderrPath) plistPath, err := launchdTemplate.ResolvePath() if err != nil { log.Err(err).Msg("error resolving launchd template path") return err } err = runCommand("launchctl", "unload", plistPath) if err != nil { log.Err(err).Msg("error unloading launchd") return err } err = launchdTemplate.Remove() if err == nil { log.Info().Msg("Launchd for cloudflared was uninstalled successfully") } return err } func userHomeDir() (string, error) { // This returns the home dir of the executing user using OS-specific method // for discovering the home dir. It's not recommended to call this function // when the user has root permission as $HOME depends on what options the user // use with sudo. homeDir, err := homedir.Dir() if err != nil { return "", errors.Wrap(err, "Cannot determine home directory for the user") } return homeDir, nil } ================================================ FILE: cmd/cloudflared/main.go ================================================ package main import ( "fmt" "os" "strings" "time" "github.com/getsentry/sentry-go" "github.com/urfave/cli/v2" "go.uber.org/automaxprocs/maxprocs" "github.com/cloudflare/cloudflared/cmd/cloudflared/access" "github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil" cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags" "github.com/cloudflare/cloudflared/cmd/cloudflared/management" "github.com/cloudflare/cloudflared/cmd/cloudflared/proxydns" "github.com/cloudflare/cloudflared/cmd/cloudflared/tail" "github.com/cloudflare/cloudflared/cmd/cloudflared/tunnel" "github.com/cloudflare/cloudflared/cmd/cloudflared/updater" "github.com/cloudflare/cloudflared/config" "github.com/cloudflare/cloudflared/logger" "github.com/cloudflare/cloudflared/metrics" "github.com/cloudflare/cloudflared/overwatch" "github.com/cloudflare/cloudflared/token" "github.com/cloudflare/cloudflared/tracing" "github.com/cloudflare/cloudflared/watcher" ) const ( versionText = "Print the version" ) var ( Version = "DEV" BuildTime = "unknown" BuildType = "" // Mostly network errors that we don't want reported back to Sentry, this is done by substring match. ignoredErrors = []string{ "connection reset by peer", "An existing connection was forcibly closed by the remote host.", "use of closed connection", "You need to enable Argo Smart Routing", "3001 connection closed", "3002 connection dropped", "rpc exception: dial tcp", "rpc exception: EOF", } ) func main() { // FIXME: TUN-8148: Disable QUIC_GO ECN due to bugs in proper detection if supported os.Setenv("QUIC_GO_DISABLE_ECN", "1") metrics.RegisterBuildInfo(BuildType, BuildTime, Version) _, _ = maxprocs.Set() bInfo := cliutil.GetBuildInfo(BuildType, Version) // Graceful shutdown channel used by the app. When closed, app must terminate gracefully. // Windows service manager closes this channel when it receives stop command. graceShutdownC := make(chan struct{}) cli.VersionFlag = &cli.BoolFlag{ Name: "version", Aliases: []string{"v", "V"}, Usage: versionText, } app := &cli.App{} app.Name = "cloudflared" app.Usage = "Cloudflare's command-line tool and agent" app.UsageText = "cloudflared [global options] [command] [command options]" app.Copyright = fmt.Sprintf( `(c) %d Cloudflare Inc. Your installation of cloudflared software constitutes a symbol of your signature indicating that you accept the terms of the Apache License Version 2.0 (https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/license), Terms (https://www.cloudflare.com/terms/) and Privacy Policy (https://www.cloudflare.com/privacypolicy/).`, time.Now().Year(), ) app.Version = fmt.Sprintf("%s (built %s%s)", Version, BuildTime, bInfo.GetBuildTypeMsg()) app.Description = `cloudflared connects your machine or user identity to Cloudflare's global network. You can use it to authenticate a session to reach an API behind Access, route web traffic to this machine, and configure access control. See https://developers.cloudflare.com/cloudflare-one/connections/connect-apps for more in-depth documentation.` app.Flags = flags() app.Action = action(graceShutdownC) app.Commands = commands(cli.ShowVersion) tunnel.Init(bInfo, graceShutdownC) // we need this to support the tunnel sub command... access.Init(graceShutdownC, Version) updater.Init(bInfo) tracing.Init(Version) token.Init(Version) tail.Init(bInfo) management.Init(bInfo) runApp(app, graceShutdownC) } func commands(version func(c *cli.Context)) []*cli.Command { cmds := []*cli.Command{ { Name: "update", Action: cliutil.ConfiguredAction(updater.Update), Usage: "Update the agent if a new version exists", Flags: []cli.Flag{ &cli.BoolFlag{ Name: "beta", Usage: "specify if you wish to update to the latest beta version", }, &cli.BoolFlag{ Name: cfdflags.Force, Usage: "specify if you wish to force an upgrade to the latest version regardless of the current version", Hidden: true, }, &cli.BoolFlag{ Name: "staging", Usage: "specify if you wish to use the staging url for updating", Hidden: true, }, &cli.StringFlag{ Name: "version", Usage: "specify a version you wish to upgrade or downgrade to", Hidden: false, }, }, Description: `Looks for a new version on the official download server. If a new version exists, updates the agent binary and quits. Otherwise, does nothing. To determine if an update happened in a script, check for error code 11.`, }, { Name: "version", Action: func(c *cli.Context) (err error) { if c.Bool("short") { fmt.Println(strings.Split(c.App.Version, " ")[0]) return nil } version(c) return nil }, Usage: versionText, Description: versionText, Flags: []cli.Flag{ &cli.BoolFlag{ Name: "short", Aliases: []string{"s"}, Usage: "print just the version number", }, }, }, } cmds = append(cmds, tunnel.Commands()...) cmds = append(cmds, proxydns.Command()) // removed feature, only here for error message cmds = append(cmds, access.Commands()...) cmds = append(cmds, tail.Command()) cmds = append(cmds, management.Command()) return cmds } func flags() []cli.Flag { flags := tunnel.Flags() return append(flags, access.Flags()...) } func isEmptyInvocation(c *cli.Context) bool { return c.NArg() == 0 && c.NumFlags() == 0 } func action(graceShutdownC chan struct{}) cli.ActionFunc { return cliutil.ConfiguredAction(func(c *cli.Context) (err error) { if isEmptyInvocation(c) { return handleServiceMode(c, graceShutdownC) } func() { defer sentry.Recover() err = tunnel.TunnelCommand(c) }() if err != nil { captureError(err) } return err }) } // In order to keep the amount of noise sent to Sentry low, typical network errors can be filtered out here by a substring match. func captureError(err error) { errorMessage := err.Error() for _, ignoredErrorMessage := range ignoredErrors { if strings.Contains(errorMessage, ignoredErrorMessage) { return } } sentry.CaptureException(err) } // cloudflared was started without any flags func handleServiceMode(c *cli.Context, shutdownC chan struct{}) error { log := logger.CreateLoggerFromContext(c, logger.DisableTerminalLog) // start the main run loop that reads from the config file f, err := watcher.NewFile() if err != nil { log.Err(err).Msg("Cannot load config file") return err } configPath := config.FindOrCreateConfigPath() configManager, err := config.NewFileManager(f, configPath, log) if err != nil { log.Err(err).Msg("Cannot setup config file for monitoring") return err } log.Info().Msgf("monitoring config file at: %s", configPath) serviceCallback := func(t string, name string, err error) { if err != nil { log.Err(err).Msgf("%s service: %s encountered an error", t, name) } } serviceManager := overwatch.NewAppManager(serviceCallback) appService := NewAppService(configManager, serviceManager, shutdownC, log) if err := appService.Run(); err != nil { log.Err(err).Msg("Failed to start app service") return err } return nil } ================================================ FILE: cmd/cloudflared/management/cmd.go ================================================ package management import ( "encoding/json" "fmt" "os" "github.com/urfave/cli/v2" "github.com/cloudflare/cloudflared/cfapi" "github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil" cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags" "github.com/cloudflare/cloudflared/credentials" ) var buildInfo *cliutil.BuildInfo // Init initializes the management package with build info func Init(bi *cliutil.BuildInfo) { buildInfo = bi } // Command returns the management command with its subcommands func Command() *cli.Command { return &cli.Command{ Name: "management", Usage: "Monitor cloudflared tunnels via management API", Category: "Management", Hidden: true, Subcommands: []*cli.Command{ buildTokenSubcommand(), }, } } // buildTokenSubcommand creates the token subcommand func buildTokenSubcommand() *cli.Command { return &cli.Command{ Name: "token", Action: cliutil.ConfiguredAction(tokenCommand), Usage: "Get management access jwt for a specific resource", UsageText: "cloudflared management token --resource TUNNEL_ID", Description: "Get management access jwt for a tunnel with specified resource permissions (logs, admin, host_details)", Hidden: true, Flags: []cli.Flag{ &cli.StringFlag{ Name: "resource", Usage: "Resource type for token permissions: logs, admin, or host_details", Required: true, }, &cli.StringFlag{ Name: cfdflags.OriginCert, Usage: "Path to the certificate generated for your origin when you run cloudflared login.", EnvVars: []string{"TUNNEL_ORIGIN_CERT"}, Value: credentials.FindDefaultOriginCertPath(), }, &cli.StringFlag{ Name: cfdflags.LogLevel, Value: "info", Usage: "Application logging level {debug, info, warn, error, fatal}", EnvVars: []string{"TUNNEL_LOGLEVEL"}, }, cliutil.FlagLogOutput, }, } } // tokenCommand handles the token subcommand execution func tokenCommand(c *cli.Context) error { log := cliutil.CreateStderrLogger(c) // Parse and validate resource flag resourceStr := c.String("resource") resource, err := parseResource(resourceStr) if err != nil { return fmt.Errorf("invalid resource '%s': %w", resourceStr, err) } // Get management token token, err := cliutil.GetManagementToken(c, log, resource, buildInfo) if err != nil { return err } // Output JSON to stdout tokenResponse := struct { Token string `json:"token"` }{Token: token} return json.NewEncoder(os.Stdout).Encode(tokenResponse) } // parseResource converts resource string to ManagementResource enum func parseResource(resource string) (cfapi.ManagementResource, error) { switch resource { case "logs": return cfapi.Logs, nil case "admin": return cfapi.Admin, nil case "host_details": return cfapi.HostDetails, nil default: return 0, fmt.Errorf("must be one of: logs, admin, host_details") } } ================================================ FILE: cmd/cloudflared/management/cmd_test.go ================================================ package management import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/cloudflare/cloudflared/cfapi" ) func TestParseResource_ValidResources(t *testing.T) { t.Parallel() tests := []struct { input string expected cfapi.ManagementResource }{ {"logs", cfapi.Logs}, {"admin", cfapi.Admin}, {"host_details", cfapi.HostDetails}, } for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { t.Parallel() result, err := parseResource(tt.input) require.NoError(t, err) assert.Equal(t, tt.expected, result) }) } } func TestParseResource_InvalidResource(t *testing.T) { t.Parallel() invalid := []string{"invalid", "LOGS", "Admin", "", "metrics", "host-details"} for _, input := range invalid { t.Run(input, func(t *testing.T) { t.Parallel() _, err := parseResource(input) require.Error(t, err) assert.Contains(t, err.Error(), "must be one of") }) } } func TestCommandStructure(t *testing.T) { t.Parallel() cmd := Command() assert.Equal(t, "management", cmd.Name) assert.True(t, cmd.Hidden) assert.Len(t, cmd.Subcommands, 1) tokenCmd := cmd.Subcommands[0] assert.Equal(t, "token", tokenCmd.Name) assert.True(t, tokenCmd.Hidden) // Verify required flags exist var hasResourceFlag bool for _, flag := range tokenCmd.Flags { if flag.Names()[0] == "resource" { hasResourceFlag = true break } } assert.True(t, hasResourceFlag, "token command should have --resource flag") } ================================================ FILE: cmd/cloudflared/proxydns/cmd.go ================================================ package proxydns import ( "errors" "github.com/urfave/cli/v2" "github.com/urfave/cli/v2/altsrc" "github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil" "github.com/cloudflare/cloudflared/logger" ) const removedMessage = "dns-proxy feature is no longer supported" func Command() *cli.Command { return &cli.Command{ Name: "proxy-dns", Action: cliutil.ConfiguredAction(Run), Usage: removedMessage, SkipFlagParsing: true, } } func Run(c *cli.Context) error { log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog) err := errors.New(removedMessage) log.Error().Msg("DNS Proxy is no longer supported since version 2026.2.0 (https://developers.cloudflare.com/changelog/2025-11-11-cloudflared-proxy-dns/). As an alternative consider using https://developers.cloudflare.com/1.1.1.1/encryption/dns-over-https/dns-over-https-client/") return err } // Old flags used by the proxy-dns command, only kept to not break any script that might be setting these flags func ConfigureProxyDNSFlags(shouldHide bool) []cli.Flag { return []cli.Flag{ altsrc.NewBoolFlag(&cli.BoolFlag{ Name: "proxy-dns", }), altsrc.NewIntFlag(&cli.IntFlag{ Name: "proxy-dns-port", }), altsrc.NewStringFlag(&cli.StringFlag{ Name: "proxy-dns-address", }), altsrc.NewStringSliceFlag(&cli.StringSliceFlag{ Name: "proxy-dns-upstream", }), altsrc.NewIntFlag(&cli.IntFlag{ Name: "proxy-dns-max-upstream-conns", }), altsrc.NewStringSliceFlag(&cli.StringSliceFlag{ Name: "proxy-dns-bootstrap", }), } } ================================================ FILE: cmd/cloudflared/service_template.go ================================================ package main import ( "bytes" "errors" "fmt" "io" "os" "os/exec" "path/filepath" "text/template" homedir "github.com/mitchellh/go-homedir" ) type ServiceTemplate struct { Path string Content string FileMode os.FileMode } type ServiceTemplateArgs struct { Path string ExtraArgs []string } func (st *ServiceTemplate) ResolvePath() (string, error) { resolvedPath, err := homedir.Expand(st.Path) if err != nil { return "", fmt.Errorf("error resolving path %s: %v", st.Path, err) } return resolvedPath, nil } func (st *ServiceTemplate) Generate(args *ServiceTemplateArgs) error { tmpl, err := template.New(st.Path).Parse(st.Content) if err != nil { return fmt.Errorf("error generating %s template: %v", st.Path, err) } resolvedPath, err := st.ResolvePath() if err != nil { return err } if _, err = os.Stat(resolvedPath); err == nil { return errors.New(serviceAlreadyExistsWarn(resolvedPath)) } var buffer bytes.Buffer err = tmpl.Execute(&buffer, args) if err != nil { return fmt.Errorf("error generating %s: %v", st.Path, err) } fileMode := os.FileMode(0o644) if st.FileMode != 0 { fileMode = st.FileMode } plistFolder := filepath.Dir(resolvedPath) err = os.MkdirAll(plistFolder, 0o755) if err != nil { return fmt.Errorf("error creating %s: %v", plistFolder, err) } err = os.WriteFile(resolvedPath, buffer.Bytes(), fileMode) if err != nil { return fmt.Errorf("error writing %s: %v", resolvedPath, err) } return nil } func (st *ServiceTemplate) Remove() error { resolvedPath, err := st.ResolvePath() if err != nil { return err } err = os.Remove(resolvedPath) if err != nil { return fmt.Errorf("error deleting %s: %v", resolvedPath, err) } return nil } func serviceAlreadyExistsWarn(service string) string { return fmt.Sprintf("cloudflared service is already installed at %s; if you are running a cloudflared tunnel, you "+ "can point it to multiple origins, avoiding the need to run more than one cloudflared service in the "+ "same machine; otherwise if you are really sure, you can do `cloudflared service uninstall` to clean "+ "up the existing service and then try again this command", service, ) } func runCommand(command string, args ...string) error { cmd := exec.Command(command, args...) stderr, err := cmd.StderrPipe() if err != nil { return fmt.Errorf("error getting stderr pipe: %v", err) } err = cmd.Start() if err != nil { return fmt.Errorf("error starting %s: %v", command, err) } output, _ := io.ReadAll(stderr) err = cmd.Wait() if err != nil { return fmt.Errorf("%s %v returned with error code %v due to: %v", command, args, err, string(output)) } return nil } ================================================ FILE: cmd/cloudflared/tail/cmd.go ================================================ package tail import ( "encoding/json" "errors" "fmt" "net/http" "net/url" "os" "os/signal" "syscall" "time" "github.com/google/uuid" "github.com/rs/zerolog" "github.com/urfave/cli/v2" "nhooyr.io/websocket" "github.com/cloudflare/cloudflared/cfapi" "github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil" cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags" "github.com/cloudflare/cloudflared/credentials" "github.com/cloudflare/cloudflared/management" ) var buildInfo *cliutil.BuildInfo func Init(bi *cliutil.BuildInfo) { buildInfo = bi } func Command() *cli.Command { subcommands := []*cli.Command{ buildTailManagementTokenSubcommand(), } return buildTailCommand(subcommands) } func buildTailManagementTokenSubcommand() *cli.Command { return &cli.Command{ Name: "token", Action: cliutil.ConfiguredAction(managementTokenCommand), Usage: "Get management access jwt", UsageText: "cloudflared tail token TUNNEL_ID", Description: `Get management access jwt for a tunnel`, Hidden: true, } } func managementTokenCommand(c *cli.Context) error { log := cliutil.CreateStderrLogger(c) token, err := cliutil.GetManagementToken(c, log, cfapi.Logs, buildInfo) if err != nil { return err } tokenResponse := struct { Token string `json:"token"` }{Token: token} return json.NewEncoder(os.Stdout).Encode(tokenResponse) } func buildTailCommand(subcommands []*cli.Command) *cli.Command { return &cli.Command{ Name: "tail", Action: Run, Usage: "Stream logs from a remote cloudflared", UsageText: "cloudflared tail [tail command options] [TUNNEL-ID]", Flags: []cli.Flag{ &cli.StringFlag{ Name: "connector-id", Usage: "Access a specific cloudflared instance by connector id (for when a tunnel has multiple cloudflared's)", Value: "", EnvVars: []string{"TUNNEL_MANAGEMENT_CONNECTOR"}, }, &cli.StringSliceFlag{ Name: "event", Usage: "Filter by specific Events (cloudflared, http, tcp, udp) otherwise, defaults to send all events", EnvVars: []string{"TUNNEL_MANAGEMENT_FILTER_EVENTS"}, }, &cli.StringFlag{ Name: "level", Usage: "Filter by specific log levels (debug, info, warn, error). Filters by debug log level by default.", EnvVars: []string{"TUNNEL_MANAGEMENT_FILTER_LEVEL"}, Value: "debug", }, &cli.Float64Flag{ Name: "sample", Usage: "Sample log events by percentage (0.0 .. 1.0). No sampling by default.", EnvVars: []string{"TUNNEL_MANAGEMENT_FILTER_SAMPLE"}, Value: 1.0, }, &cli.StringFlag{ Name: "token", Usage: "Access token for a specific tunnel", Value: "", EnvVars: []string{"TUNNEL_MANAGEMENT_TOKEN"}, }, &cli.StringFlag{ Name: cfdflags.ManagementHostname, Usage: "Management hostname to signify incoming management requests", EnvVars: []string{"TUNNEL_MANAGEMENT_HOSTNAME"}, Hidden: true, Value: "management.argotunnel.com", }, &cli.StringFlag{ Name: "trace", Usage: "Set a cf-trace-id for the request", Hidden: true, Value: "", }, &cli.StringFlag{ Name: cfdflags.LogLevel, Value: "info", Usage: "Application logging level {debug, info, warn, error, fatal}", EnvVars: []string{"TUNNEL_LOGLEVEL"}, }, &cli.StringFlag{ Name: cfdflags.OriginCert, Usage: "Path to the certificate generated for your origin when you run cloudflared login.", EnvVars: []string{"TUNNEL_ORIGIN_CERT"}, Value: credentials.FindDefaultOriginCertPath(), }, cliutil.FlagLogOutput, }, Subcommands: subcommands, } } // Middleware validation error struct for returning to the eyeball type managementError struct { Code int `json:"code,omitempty"` Message string `json:"message,omitempty"` } // Middleware validation error HTTP response JSON for returning to the eyeball type managementErrorResponse struct { Success bool `json:"success,omitempty"` Errors []managementError `json:"errors,omitempty"` } func handleValidationError(resp *http.Response, log *zerolog.Logger) { if resp.StatusCode == 530 { log.Error().Msgf("no cloudflared connector available or reachable via management request (a recent version of cloudflared is required to use streaming logs)") } var managementErr managementErrorResponse err := json.NewDecoder(resp.Body).Decode(&managementErr) if err != nil { log.Error().Msgf("unable to start management log streaming session: http response code returned %d", resp.StatusCode) return } if managementErr.Success || len(managementErr.Errors) == 0 { log.Error().Msgf("management tunnel validation returned success with invalid HTTP response code to convert to a WebSocket request") return } for _, e := range managementErr.Errors { log.Error().Msgf("management request failed validation: (%d) %s", e.Code, e.Message) } } // parseFilters will attempt to parse provided filters to send to with the EventStartStreaming func parseFilters(c *cli.Context) (*management.StreamingFilters, error) { var level *management.LogLevel var sample float64 events := make([]management.LogEventType, 0) argLevel := c.String("level") argEvents := c.StringSlice("event") argSample := c.Float64("sample") if argLevel != "" { l, ok := management.ParseLogLevel(argLevel) if !ok { return nil, fmt.Errorf("invalid --level filter provided, please use one of the following Log Levels: debug, info, warn, error") } level = &l } for _, v := range argEvents { t, ok := management.ParseLogEventType(v) if !ok { return nil, fmt.Errorf("invalid --event filter provided, please use one of the following EventTypes: cloudflared, http, tcp, udp") } events = append(events, t) } if argSample <= 0.0 || argSample > 1.0 { return nil, fmt.Errorf("invalid --sample value provided, please make sure it is in the range (0.0 .. 1.0)") } sample = argSample if level == nil && len(events) == 0 && argSample != 1.0 { // When no filters are provided, do not return a StreamingFilters struct return nil, nil } return &management.StreamingFilters{ Level: level, Events: events, Sampling: sample, }, nil } // buildURL will build the management url to contain the required query parameters to authenticate the request. func buildURL(c *cli.Context, log *zerolog.Logger, res cfapi.ManagementResource) (url.URL, error) { var err error token := c.String("token") if token == "" { token, err = cliutil.GetManagementToken(c, log, res, buildInfo) if err != nil { return url.URL{}, fmt.Errorf("unable to acquire management token for requested tunnel id: %w", err) } } claims, err := management.ParseToken(token) if err != nil { return url.URL{}, fmt.Errorf("failed to determine if token is FED: %w", err) } var managementHostname string if claims.IsFed() { managementHostname = credentials.FedRampHostname } else { managementHostname = c.String(cfdflags.ManagementHostname) } query := url.Values{} query.Add("access_token", token) connector := c.String("connector-id") if connector != "" { connectorID, err := uuid.Parse(connector) if err != nil { return url.URL{}, fmt.Errorf("unabled to parse 'connector-id' flag into a valid UUID: %w", err) } query.Add("connector_id", connectorID.String()) } return url.URL{Scheme: "wss", Host: managementHostname, Path: "/logs", RawQuery: query.Encode()}, nil } func printLine(log *management.Log, logger *zerolog.Logger) { fields, err := json.Marshal(log.Fields) if err != nil { fields = []byte("unable to parse fields") logger.Debug().Msgf("unable to parse fields from event %+v", log) } fmt.Printf("%s %s %s %s %s\n", log.Time, log.Level, log.Event, log.Message, fields) } func printJSON(log *management.Log, logger *zerolog.Logger) { output, err := json.Marshal(log) if err != nil { logger.Debug().Msgf("unable to parse event to json %+v", log) } else { fmt.Println(string(output)) } } // Run implements a foreground runner func Run(c *cli.Context) error { log := cliutil.CreateStderrLogger(c) signals := make(chan os.Signal, 10) signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT) defer signal.Stop(signals) output := "default" switch c.String("output") { case "default", "": output = "default" case "json": output = "json" default: log.Err(errors.New("invalid --output value provided, please make sure it is one of: default, json")).Send() } filters, err := parseFilters(c) if err != nil { log.Error().Err(err).Msgf("invalid filters provided") return nil } u, err := buildURL(c, log, cfapi.Logs) if err != nil { log.Err(err).Msg("unable to construct management request URL") return nil } header := make(http.Header) header.Add("User-Agent", buildInfo.UserAgent()) trace := c.String("trace") if trace != "" { header["cf-trace-id"] = []string{trace} } ctx := c.Context // nolint: bodyclose conn, resp, err := websocket.Dial(ctx, u.String(), &websocket.DialOptions{ HTTPHeader: header, }) if err != nil { if resp != nil && resp.StatusCode != http.StatusSwitchingProtocols { handleValidationError(resp, log) return nil } log.Error().Err(err).Msgf("unable to start management log streaming session") return nil } defer conn.Close(websocket.StatusInternalError, "management connection was closed abruptly") // Once connection is established, send start_streaming event to begin receiving logs err = management.WriteEvent(conn, ctx, &management.EventStartStreaming{ ClientEvent: management.ClientEvent{Type: management.StartStreaming}, Filters: filters, }) if err != nil { log.Error().Err(err).Msg("unable to request logs from management tunnel") return nil } log.Debug(). Str("tunnel-id", c.Args().First()). Str("connector-id", c.String("connector-id")). Interface("filters", filters). Msg("connected") readerDone := make(chan struct{}) go func() { defer close(readerDone) for { select { case <-ctx.Done(): return default: event, err := management.ReadServerEvent(conn, ctx) if err != nil { if closeErr := management.AsClosed(err); closeErr != nil { // If the client (or the server) already closed the connection, don't continue to // attempt to read from the client. if closeErr.Code == websocket.StatusNormalClosure { return } // Only log abnormal closures log.Error().Msgf("received remote closure: (%d) %s", closeErr.Code, closeErr.Reason) return } log.Err(err).Msg("unable to read event from server") return } switch event.Type { case management.Logs: logs, ok := management.IntoServerEvent(event, management.Logs) if !ok { log.Error().Msgf("invalid logs event") continue } // Output all the logs received to stdout for _, l := range logs.Logs { if output == "json" { printJSON(l, log) } else { printLine(l, log) } } case management.UnknownServerEventType: fallthrough default: log.Debug().Msgf("unexpected log event type: %s", event.Type) } } } }() for { select { case <-ctx.Done(): return nil case <-readerDone: return nil case <-signals: log.Debug().Msg("closing management connection") // Cleanly close the connection by sending a close message and then // waiting (with timeout) for the server to close the connection. conn.Close(websocket.StatusNormalClosure, "") select { case <-readerDone: case <-time.After(time.Second): } return nil } } } ================================================ FILE: cmd/cloudflared/tunnel/cmd.go ================================================ package tunnel import ( "bufio" "context" "fmt" "net/url" "os" "path/filepath" "runtime/trace" "strings" "sync" "time" "github.com/coreos/go-systemd/v22/daemon" "github.com/facebookgo/grace/gracenet" "github.com/getsentry/sentry-go" "github.com/mitchellh/go-homedir" "github.com/pkg/errors" "github.com/rs/zerolog" "github.com/urfave/cli/v2" "github.com/urfave/cli/v2/altsrc" "github.com/cloudflare/cloudflared/cfapi" "github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil" cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags" "github.com/cloudflare/cloudflared/cmd/cloudflared/proxydns" "github.com/cloudflare/cloudflared/cmd/cloudflared/updater" "github.com/cloudflare/cloudflared/config" "github.com/cloudflare/cloudflared/connection" "github.com/cloudflare/cloudflared/credentials" "github.com/cloudflare/cloudflared/diagnostic" "github.com/cloudflare/cloudflared/edgediscovery" "github.com/cloudflare/cloudflared/ingress" "github.com/cloudflare/cloudflared/logger" "github.com/cloudflare/cloudflared/management" "github.com/cloudflare/cloudflared/metrics" "github.com/cloudflare/cloudflared/orchestration" "github.com/cloudflare/cloudflared/signal" "github.com/cloudflare/cloudflared/supervisor" "github.com/cloudflare/cloudflared/tlsconfig" "github.com/cloudflare/cloudflared/tunnelstate" "github.com/cloudflare/cloudflared/validation" ) const ( sentryDSN = "https://56a9c9fa5c364ab28f34b14f35ea0f1b:3e8827f6f9f740738eb11138f7bebb68@sentry.io/189878" LogFieldCommand = "command" LogFieldExpandedPath = "expandedPath" LogFieldPIDPathname = "pidPathname" LogFieldTmpTraceFilename = "tmpTraceFilename" LogFieldTraceOutputFilepath = "traceOutputFilepath" tunnelCmdErrorMessage = `You did not specify any valid additional argument to the cloudflared tunnel command. If you are trying to run a Quick Tunnel then you need to explicitly pass the --url flag. Eg. cloudflared tunnel --url localhost:8080/. Please note that Quick Tunnels are meant to be ephemeral and should only be used for testing purposes. For production usage, we recommend creating Named Tunnels. (https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/tunnel-guide/) ` ) var ( graceShutdownC chan struct{} buildInfo *cliutil.BuildInfo routeFailMsg = fmt.Sprintf("failed to provision routing, please create it manually via Cloudflare dashboard or UI; "+ "most likely you already have a conflicting record there. You can also rerun this command with --%s to overwrite "+ "any existing DNS records for this hostname.", overwriteDNSFlag) errDeprecatedClassicTunnel = errors.New("Classic tunnels have been deprecated, please use Named Tunnels. (https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/tunnel-guide/)") // TODO: TUN-8756 the list below denotes the flags that do not possess any kind of sensitive information // however this approach is not maintainble in the long-term. nonSecretFlagsList = []string{ "config", cfdflags.AutoUpdateFreq, cfdflags.NoAutoUpdate, cfdflags.Metrics, "pidfile", "url", "hello-world", "socks5", "proxy-connect-timeout", "proxy-tls-timeout", "proxy-tcp-keepalive", "proxy-no-happy-eyeballs", "proxy-keepalive-connections", "proxy-keepalive-timeout", "proxy-connection-timeout", "proxy-expect-continue-timeout", "http-host-header", "origin-server-name", "unix-socket", "origin-ca-pool", "no-tls-verify", "no-chunked-encoding", "http2-origin", cfdflags.ManagementHostname, "service-op-ip", "local-ssh-port", "ssh-idle-timeout", "ssh-max-timeout", "bucket-name", "region-name", "s3-url-host", "host-key-path", "ssh-server", "bastion", "proxy-address", "proxy-port", cfdflags.LogLevel, cfdflags.TransportLogLevel, cfdflags.LogFile, cfdflags.LogDirectory, cfdflags.TraceOutput, cfdflags.IsAutoUpdated, cfdflags.Edge, cfdflags.Region, cfdflags.EdgeIpVersion, cfdflags.EdgeBindAddress, "cacert", "hostname", "id", cfdflags.LBPool, cfdflags.ApiURL, cfdflags.MetricsUpdateFreq, cfdflags.Tag, "heartbeat-interval", "heartbeat-count", cfdflags.MaxEdgeAddrRetries, cfdflags.Retries, "ha-connections", "rpc-timeout", "write-stream-timeout", "quic-disable-pmtu-discovery", "quic-connection-level-flow-control-limit", "quic-stream-level-flow-control-limit", cfdflags.ConnectorLabel, cfdflags.GracePeriod, "compression-quality", "use-reconnect-token", "dial-edge-timeout", "stdin-control", cfdflags.Name, cfdflags.Ui, "quick-service", "max-fetch-size", cfdflags.PostQuantum, "management-diagnostics", cfdflags.Protocol, "overwrite-dns", "help", cfdflags.MaxActiveFlows, } ) func Flags() []cli.Flag { return tunnelFlags(true) } func Commands() []*cli.Command { subcommands := []*cli.Command{ buildLoginSubcommand(false), buildCreateCommand(), buildRouteCommand(), buildVirtualNetworkSubcommand(false), buildRunCommand(), buildListCommand(), buildReadyCommand(), buildInfoCommand(), buildIngressSubcommand(), buildDeleteCommand(), buildCleanupCommand(), buildTokenCommand(), buildDiagCommand(), proxydns.Command(), // removed feature, only here for error message cliutil.RemovedCommand("db-connect"), } return []*cli.Command{ buildTunnelCommand(subcommands), // for compatibility, allow following as top-level subcommands buildLoginSubcommand(true), cliutil.RemovedCommand("db-connect"), } } func buildTunnelCommand(subcommands []*cli.Command) *cli.Command { return &cli.Command{ Name: "tunnel", Action: cliutil.ConfiguredAction(TunnelCommand), Category: "Tunnel", Usage: "Use Cloudflare Tunnel to expose private services to the Internet or to Cloudflare connected private users.", ArgsUsage: " ", Description: ` Cloudflare Tunnel allows to expose private services without opening any ingress port on this machine. It can expose: A) Locally reachable HTTP-based private services to the Internet on DNS with Cloudflare as authority (which you can then protect with Cloudflare Access). B) Locally reachable TCP/UDP-based private services to Cloudflare connected private users in the same account, e.g., those enrolled to a Zero Trust WARP Client. You can manage your Tunnels via one.dash.cloudflare.com. This approach will only require you to run a single command later in each machine where you wish to run a Tunnel. Alternatively, you can manage your Tunnels via the command line. Begin by obtaining a certificate to be able to do so: $ cloudflared tunnel login With your certificate installed you can then get started with Tunnels: $ cloudflared tunnel create my-first-tunnel $ cloudflared tunnel route dns my-first-tunnel my-first-tunnel.mydomain.com $ cloudflared tunnel run --hello-world my-first-tunnel You can now access my-first-tunnel.mydomain.com and be served an example page by your local cloudflared process. For exposing local TCP/UDP services by IP to your privately connected users, check out: $ cloudflared tunnel route ip --help See https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/tunnel-guide/ for more info.`, Subcommands: subcommands, Flags: tunnelFlags(false), } } func TunnelCommand(c *cli.Context) error { sc, err := newSubcommandContext(c) if err != nil { return err } // Run a adhoc named tunnel // Allows for the creation, routing (optional), and startup of a tunnel in one command // --name required // --url or --hello-world required // --hostname optional if name := c.String(cfdflags.Name); name != "" { hostname, err := validation.ValidateHostname(c.String("hostname")) if err != nil { return errors.Wrap(err, "Invalid hostname provided") } url := c.String("url") if url == hostname && url != "" && hostname != "" { return fmt.Errorf("hostname and url shouldn't match. See --help for more information") } return runAdhocNamedTunnel(sc, name, c.String(CredFileFlag)) } // Run a quick tunnel // A unauthenticated named tunnel hosted on ..com shouldRunQuickTunnel := c.IsSet("url") || c.IsSet(ingress.HelloWorldFlag) if c.String("quick-service") != "" && shouldRunQuickTunnel { return RunQuickTunnel(sc) } // If user provides a config, check to see if they meant to use `tunnel run` instead if ref := config.GetConfiguration().TunnelID; ref != "" { return fmt.Errorf("Use `cloudflared tunnel run` to start tunnel %s", ref) } // Classic tunnel usage is no longer supported if c.String("hostname") != "" { return errDeprecatedClassicTunnel } return errors.New(tunnelCmdErrorMessage) } func Init(info *cliutil.BuildInfo, gracefulShutdown chan struct{}) { buildInfo, graceShutdownC = info, gracefulShutdown } // runAdhocNamedTunnel create, route and run a named tunnel in one command func runAdhocNamedTunnel(sc *subcommandContext, name, credentialsOutputPath string) error { tunnel, ok, err := sc.tunnelActive(name) if err != nil || !ok { // pass empty string as secret to generate one tunnel, err = sc.create(name, credentialsOutputPath, "") if err != nil { return errors.Wrap(err, "failed to create tunnel") } } else { sc.log.Info().Str(LogFieldTunnelID, tunnel.ID.String()).Msg("Reusing existing tunnel with this name") } if r, ok := routeFromFlag(sc.c); ok { if res, err := sc.route(tunnel.ID, r); err != nil { sc.log.Err(err).Str("route", r.String()).Msg(routeFailMsg) } else { sc.log.Info().Msg(res.SuccessSummary()) } } if err := sc.run(tunnel.ID); err != nil { return errors.Wrap(err, "error running tunnel") } return nil } func routeFromFlag(c *cli.Context) (route cfapi.HostnameRoute, ok bool) { if hostname := c.String("hostname"); hostname != "" { if lbPool := c.String(cfdflags.LBPool); lbPool != "" { return cfapi.NewLBRoute(hostname, lbPool), true } return cfapi.NewDNSRoute(hostname, c.Bool(overwriteDNSFlagName)), true } return nil, false } func StartServer( c *cli.Context, info *cliutil.BuildInfo, namedTunnel *connection.TunnelProperties, log *zerolog.Logger, ) error { err := sentry.Init(sentry.ClientOptions{ Dsn: sentryDSN, Release: c.App.Version, }) if err != nil { return err } var wg sync.WaitGroup listeners := gracenet.Net{} errC := make(chan error) // Only log for locally configured tunnels (Token is blank). if config.GetConfiguration().Source() == "" && c.String(TunnelTokenFlag) == "" { log.Info().Msg(config.ErrNoConfigFile.Error()) } if c.IsSet(cfdflags.TraceOutput) { tmpTraceFile, err := os.CreateTemp("", "trace") if err != nil { log.Err(err).Msg("Failed to create new temporary file to save trace output") } traceLog := log.With().Str(LogFieldTmpTraceFilename, tmpTraceFile.Name()).Logger() defer func() { if err := tmpTraceFile.Close(); err != nil { traceLog.Err(err).Msg("Failed to close temporary trace output file") } traceOutputFilepath := c.String(cfdflags.TraceOutput) if err := os.Rename(tmpTraceFile.Name(), traceOutputFilepath); err != nil { traceLog. Err(err). Str(LogFieldTraceOutputFilepath, traceOutputFilepath). Msg("Failed to rename temporary trace output file") } else { err := os.Remove(tmpTraceFile.Name()) if err != nil { traceLog.Err(err).Msg("Failed to remove the temporary trace file") } } }() if err := trace.Start(tmpTraceFile); err != nil { traceLog.Err(err).Msg("Failed to start trace") return errors.Wrap(err, "Error starting tracing") } defer trace.Stop() } info.Log(log) logClientOptions(c, log) // this context drives the server, when it's cancelled tunnel and all other components (origins, dns, etc...) should stop ctx, cancel := context.WithCancel(c.Context) defer cancel() go waitForSignal(graceShutdownC, log) connectedSignal := signal.New(make(chan struct{})) go notifySystemd(connectedSignal) if c.IsSet("pidfile") { go writePidFile(connectedSignal, c.String("pidfile"), log) } wg.Add(1) go func() { defer wg.Done() autoupdater := updater.NewAutoUpdater( c.Bool(cfdflags.NoAutoUpdate), c.Duration(cfdflags.AutoUpdateFreq), &listeners, log, ) errC <- autoupdater.Run(ctx) }() if namedTunnel == nil { return fmt.Errorf("namedTunnel is nil") } logTransport := logger.CreateTransportLoggerFromContext(c, logger.EnableTerminalLog) observer := connection.NewObserver(log, logTransport) // Send Quick Tunnel URL to UI if applicable quickTunnelURL := namedTunnel.QuickTunnelUrl if quickTunnelURL != "" { observer.SendURL(quickTunnelURL) } tunnelConfig, orchestratorConfig, err := prepareTunnelConfig(ctx, c, info, log, logTransport, observer, namedTunnel) if err != nil { log.Err(err).Msg("Couldn't start tunnel") return err } connectorID := tunnelConfig.ClientConfig.ConnectorID // Disable ICMP packet routing for quick tunnels if quickTunnelURL != "" { tunnelConfig.ICMPRouterServer = nil } serviceIP := c.String("service-op-ip") if edgeAddrs, err := edgediscovery.ResolveEdge(log, tunnelConfig.Region, tunnelConfig.EdgeIPVersion); err == nil { if serviceAddr, err := edgeAddrs.GetAddrForRPC(); err == nil { serviceIP = serviceAddr.TCP.String() } } isFEDEndpoint := namedTunnel.Credentials.Endpoint == credentials.FedEndpoint var managementHostname string if isFEDEndpoint { managementHostname = credentials.FedRampHostname } else { managementHostname = c.String(cfdflags.ManagementHostname) } mgmt := management.New( managementHostname, c.Bool("management-diagnostics"), serviceIP, connectorID, c.String(cfdflags.ConnectorLabel), logger.ManagementLogger.Log, logger.ManagementLogger, ) internalRules := []ingress.Rule{ingress.NewManagementRule(mgmt)} orchestrator, err := orchestration.NewOrchestrator(ctx, orchestratorConfig, tunnelConfig.Tags, internalRules, tunnelConfig.Log) if err != nil { return err } metricsListener, err := metrics.CreateMetricsListener(&listeners, c.String("metrics")) if err != nil { log.Err(err).Msg("Error opening metrics server listener") return errors.Wrap(err, "Error opening metrics server listener") } defer metricsListener.Close() wg.Add(1) go func() { defer wg.Done() tracker := tunnelstate.NewConnTracker(log) observer.RegisterSink(tracker) ipv4, ipv6, err := determineICMPSources(c, log) sources := make([]string, 0) if err == nil { sources = append(sources, ipv4.String()) sources = append(sources, ipv6.String()) } readinessServer := metrics.NewReadyServer(connectorID, tracker) cliFlags := nonSecretCliFlags(log, c, nonSecretFlagsList) diagnosticHandler := diagnostic.NewDiagnosticHandler( log, 0, diagnostic.NewSystemCollectorImpl(buildInfo.CloudflaredVersion), tunnelConfig.NamedTunnel.Credentials.TunnelID, connectorID, tracker, cliFlags, sources, ) metricsConfig := metrics.Config{ ReadyServer: readinessServer, DiagnosticHandler: diagnosticHandler, QuickTunnelHostname: quickTunnelURL, Orchestrator: orchestrator, } errC <- metrics.ServeMetrics(metricsListener, ctx, metricsConfig, log) }() reconnectCh := make(chan supervisor.ReconnectSignal, c.Int(cfdflags.HaConnections)) if c.IsSet("stdin-control") { log.Info().Msg("Enabling control through stdin") go stdinControl(reconnectCh, log) } wg.Add(1) go func() { defer func() { wg.Done() log.Info().Msg("Tunnel server stopped") }() errC <- supervisor.StartTunnelDaemon(ctx, tunnelConfig, orchestrator, connectedSignal, reconnectCh, graceShutdownC) }() gracePeriod, err := gracePeriod(c) if err != nil { return err } return waitToShutdown(&wg, cancel, errC, graceShutdownC, gracePeriod, log) } func waitToShutdown(wg *sync.WaitGroup, cancelServerContext func(), errC <-chan error, graceShutdownC <-chan struct{}, gracePeriod time.Duration, log *zerolog.Logger, ) error { var err error select { case err = <-errC: log.Error().Err(err).Msg("Initiating shutdown") case <-graceShutdownC: log.Debug().Msg("Graceful shutdown signalled") if gracePeriod > 0 { // wait for either grace period or service termination ticker := time.NewTicker(gracePeriod) defer ticker.Stop() select { case <-ticker.C: case <-errC: } } } // stop server context cancelServerContext() // Wait for clean exit, discarding all errors while we wait stopDiscarding := make(chan struct{}) go func() { for { select { case <-errC: // ignore case <-stopDiscarding: return } } }() wg.Wait() close(stopDiscarding) return err } func notifySystemd(waitForSignal *signal.Signal) { <-waitForSignal.Wait() _, _ = daemon.SdNotify(false, "READY=1") } func writePidFile(waitForSignal *signal.Signal, pidPathname string, log *zerolog.Logger) { <-waitForSignal.Wait() expandedPath, err := homedir.Expand(pidPathname) if err != nil { log.Err(err).Str(LogFieldPIDPathname, pidPathname).Msg("Unable to expand the path, try to use absolute path in --pidfile") return } file, err := os.Create(expandedPath) if err != nil { log.Err(err).Str(LogFieldExpandedPath, expandedPath).Msg("Unable to write pid") return } defer file.Close() fmt.Fprintf(file, "%d", os.Getpid()) } func hostnameFromURI(uri string) string { u, err := url.Parse(uri) if err != nil { return "" } switch u.Scheme { case "ssh": return addPortIfMissing(u, 22) case "rdp": return addPortIfMissing(u, 3389) case "smb": return addPortIfMissing(u, 445) case "tcp": return addPortIfMissing(u, 7864) // just a random port since there isn't a default in this case } return "" } func addPortIfMissing(uri *url.URL, port int) string { if uri.Port() != "" { return uri.Host } return fmt.Sprintf("%s:%d", uri.Hostname(), port) } func tunnelFlags(shouldHide bool) []cli.Flag { flags := configureCloudflaredFlags(shouldHide) flags = append(flags, configureProxyFlags(shouldHide)...) flags = append(flags, cliutil.ConfigureLoggingFlags(shouldHide)...) flags = append(flags, proxydns.ConfigureProxyDNSFlags(shouldHide)...) // removed feature, only kept to not break any script that might be setting these flags flags = append(flags, []cli.Flag{ credentialsFileFlag, altsrc.NewBoolFlag(&cli.BoolFlag{ Name: cfdflags.IsAutoUpdated, Usage: "Signal the new process that Cloudflare Tunnel connector has been autoupdated", Value: false, Hidden: true, }), altsrc.NewStringSliceFlag(&cli.StringSliceFlag{ Name: cfdflags.Edge, Usage: "Address of the Cloudflare tunnel server. Only works in Cloudflare's internal testing environment.", EnvVars: []string{"TUNNEL_EDGE"}, Hidden: true, }), altsrc.NewStringFlag(&cli.StringFlag{ Name: cfdflags.Region, Usage: "Cloudflare Edge region to connect to. Omit or set to empty to connect to the global region.", EnvVars: []string{"TUNNEL_REGION"}, }), altsrc.NewStringFlag(&cli.StringFlag{ Name: cfdflags.EdgeIpVersion, Usage: "Cloudflare Edge IP address version to connect with. {4, 6, auto}", EnvVars: []string{"TUNNEL_EDGE_IP_VERSION"}, Value: "4", Hidden: false, }), altsrc.NewStringFlag(&cli.StringFlag{ Name: cfdflags.EdgeBindAddress, Usage: "Bind to IP address for outgoing connections to Cloudflare Edge.", EnvVars: []string{"TUNNEL_EDGE_BIND_ADDRESS"}, Hidden: false, }), altsrc.NewStringFlag(&cli.StringFlag{ Name: tlsconfig.CaCertFlag, Usage: "Certificate Authority authenticating connections with Cloudflare's edge network.", EnvVars: []string{"TUNNEL_CACERT"}, Hidden: true, }), altsrc.NewStringFlag(&cli.StringFlag{ Name: "hostname", Usage: "Set a hostname on a Cloudflare zone to route traffic through this tunnel.", EnvVars: []string{"TUNNEL_HOSTNAME"}, Hidden: shouldHide, }), altsrc.NewStringFlag(&cli.StringFlag{ Name: "id", Usage: "A unique identifier used to tie connections to this tunnel instance.", EnvVars: []string{"TUNNEL_ID"}, Hidden: true, }), altsrc.NewStringFlag(&cli.StringFlag{ Name: cfdflags.LBPool, Usage: "The name of a (new/existing) load balancing pool to add this origin to.", EnvVars: []string{"TUNNEL_LB_POOL"}, Hidden: shouldHide, }), altsrc.NewStringFlag(&cli.StringFlag{ Name: "api-key", Usage: "This parameter has been deprecated since version 2017.10.1.", EnvVars: []string{"TUNNEL_API_KEY"}, Hidden: true, }), altsrc.NewStringFlag(&cli.StringFlag{ Name: "api-email", Usage: "This parameter has been deprecated since version 2017.10.1.", EnvVars: []string{"TUNNEL_API_EMAIL"}, Hidden: true, }), altsrc.NewStringFlag(&cli.StringFlag{ Name: "api-ca-key", Usage: "This parameter has been deprecated since version 2017.10.1.", EnvVars: []string{"TUNNEL_API_CA_KEY"}, Hidden: true, }), altsrc.NewStringFlag(&cli.StringFlag{ Name: cfdflags.ApiURL, Usage: "Base URL for Cloudflare API v4", EnvVars: []string{"TUNNEL_API_URL"}, Value: "https://api.cloudflare.com/client/v4", Hidden: true, }), altsrc.NewDurationFlag(&cli.DurationFlag{ Name: cfdflags.MetricsUpdateFreq, Usage: "Frequency to update tunnel metrics", Value: time.Second * 5, EnvVars: []string{"TUNNEL_METRICS_UPDATE_FREQ"}, Hidden: shouldHide, }), altsrc.NewStringSliceFlag(&cli.StringSliceFlag{ Name: cfdflags.Tag, Usage: "Custom tags used to identify this tunnel via added HTTP request headers to the origin, in format `KEY=VALUE`. Multiple tags may be specified.", EnvVars: []string{"TUNNEL_TAG"}, Hidden: true, }), altsrc.NewDurationFlag(&cli.DurationFlag{ Name: "heartbeat-interval", Usage: "Minimum idle time before sending a heartbeat.", Value: time.Second * 5, Hidden: true, }), // Note TUN-3758 , we use Int because UInt is not supported with altsrc altsrc.NewIntFlag(&cli.IntFlag{ Name: "heartbeat-count", Usage: "Minimum number of unacked heartbeats to send before closing the connection.", Value: 5, Hidden: true, }), altsrc.NewIntFlag(&cli.IntFlag{ Name: cfdflags.MaxEdgeAddrRetries, Usage: "Maximum number of times to retry on edge addrs before falling back to a lower protocol", Value: 8, Hidden: true, }), // Note TUN-3758 , we use Int because UInt is not supported with altsrc altsrc.NewIntFlag(&cli.IntFlag{ Name: cfdflags.Retries, Value: 5, Usage: "Maximum number of retries for connection/protocol errors.", EnvVars: []string{"TUNNEL_RETRIES"}, Hidden: shouldHide, }), altsrc.NewIntFlag(&cli.IntFlag{ Name: cfdflags.HaConnections, Value: 4, Hidden: true, }), altsrc.NewDurationFlag(&cli.DurationFlag{ Name: cfdflags.RpcTimeout, Value: 5 * time.Second, Hidden: true, }), altsrc.NewDurationFlag(&cli.DurationFlag{ Name: cfdflags.WriteStreamTimeout, EnvVars: []string{"TUNNEL_STREAM_WRITE_TIMEOUT"}, Usage: "Use this option to add a stream write timeout for connections when writing towards the origin or edge. Default is 0 which disables the write timeout.", Value: 0 * time.Second, Hidden: true, }), altsrc.NewBoolFlag(&cli.BoolFlag{ Name: cfdflags.QuicDisablePathMTUDiscovery, EnvVars: []string{"TUNNEL_DISABLE_QUIC_PMTU"}, Usage: "Use this option to disable PTMU discovery for QUIC connections. This will result in lower packet sizes. Not however, that this may cause instability for UDP proxying.", Value: false, Hidden: true, }), altsrc.NewIntFlag(&cli.IntFlag{ Name: cfdflags.QuicConnLevelFlowControlLimit, EnvVars: []string{"TUNNEL_QUIC_CONN_LEVEL_FLOW_CONTROL_LIMIT"}, Usage: "Use this option to change the connection-level flow control limit for QUIC transport.", Value: 30 * (1 << 20), // 30 MB Hidden: true, }), altsrc.NewIntFlag(&cli.IntFlag{ Name: cfdflags.QuicStreamLevelFlowControlLimit, EnvVars: []string{"TUNNEL_QUIC_STREAM_LEVEL_FLOW_CONTROL_LIMIT"}, Usage: "Use this option to change the connection-level flow control limit for QUIC transport.", Value: 6 * (1 << 20), // 6 MB Hidden: true, }), altsrc.NewStringFlag(&cli.StringFlag{ Name: cfdflags.ConnectorLabel, Usage: "Use this option to give a meaningful label to a specific connector. When a tunnel starts up, a connector id unique to the tunnel is generated. This is a uuid. To make it easier to identify a connector, we will use the hostname of the machine the tunnel is running on along with the connector ID. This option exists if one wants to have more control over what their individual connectors are called.", Value: "", }), altsrc.NewDurationFlag(&cli.DurationFlag{ Name: cfdflags.GracePeriod, Usage: "When cloudflared receives SIGINT/SIGTERM it will stop accepting new requests, wait for in-progress requests to terminate, then shutdown. Waiting for in-progress requests will timeout after this grace period, or when a second SIGTERM/SIGINT is received.", Value: time.Second * 30, EnvVars: []string{"TUNNEL_GRACE_PERIOD"}, Hidden: shouldHide, }), // Note TUN-3758 , we use Int because UInt is not supported with altsrc altsrc.NewIntFlag(&cli.IntFlag{ Name: "compression-quality", Value: 0, Usage: "(beta) Use cross-stream compression instead HTTP compression. 0-off, 1-low, 2-medium, >=3-high.", EnvVars: []string{"TUNNEL_COMPRESSION_LEVEL"}, Hidden: shouldHide, }), altsrc.NewBoolFlag(&cli.BoolFlag{ Name: "use-reconnect-token", Usage: "Test reestablishing connections with the new 'reconnect token' flow.", Value: true, EnvVars: []string{"TUNNEL_USE_RECONNECT_TOKEN"}, Hidden: true, }), altsrc.NewDurationFlag(&cli.DurationFlag{ Name: "dial-edge-timeout", Usage: "Maximum wait time to set up a connection with the edge", Value: time.Second * 15, EnvVars: []string{"DIAL_EDGE_TIMEOUT"}, Hidden: true, }), altsrc.NewBoolFlag(&cli.BoolFlag{ Name: "stdin-control", Usage: "Control the process using commands sent through stdin", EnvVars: []string{"STDIN_CONTROL"}, Hidden: true, Value: false, }), altsrc.NewStringFlag(&cli.StringFlag{ Name: cfdflags.Name, Aliases: []string{"n"}, EnvVars: []string{"TUNNEL_NAME"}, Usage: "Stable name to identify the tunnel. Using this flag will create, route and run a tunnel. For production usage, execute each command separately", Hidden: shouldHide, }), altsrc.NewBoolFlag(&cli.BoolFlag{ Name: cfdflags.Ui, Usage: "(depreciated) Launch tunnel UI. Tunnel logs are scrollable via 'j', 'k', or arrow keys.", Value: false, Hidden: true, }), altsrc.NewStringFlag(&cli.StringFlag{ Name: "quick-service", Usage: "URL for a service which manages unauthenticated 'quick' tunnels.", Value: "https://api.trycloudflare.com", Hidden: true, }), altsrc.NewIntFlag(&cli.IntFlag{ Name: "max-fetch-size", Usage: `The maximum number of results that cloudflared can fetch from Cloudflare API for any listing operations needed`, EnvVars: []string{"TUNNEL_MAX_FETCH_SIZE"}, Hidden: true, }), altsrc.NewBoolFlag(&cli.BoolFlag{ Name: cfdflags.PostQuantum, Usage: "When given creates an experimental post-quantum secure tunnel", Aliases: []string{"pq"}, EnvVars: []string{"TUNNEL_POST_QUANTUM"}, }), altsrc.NewBoolFlag(&cli.BoolFlag{ Name: "management-diagnostics", Usage: "Enables the in-depth diagnostic routes to be made available over the management service (/debug/pprof, /metrics, etc.)", EnvVars: []string{"TUNNEL_MANAGEMENT_DIAGNOSTICS"}, Value: true, }), selectProtocolFlag, overwriteDNSFlag, }...) return flags } // Flags in tunnel command that is relevant to run subcommand func configureCloudflaredFlags(shouldHide bool) []cli.Flag { return []cli.Flag{ &cli.StringFlag{ Name: "config", Usage: "Specifies a config file in YAML format.", Value: config.FindDefaultConfigPath(), Hidden: shouldHide, }, altsrc.NewStringFlag(&cli.StringFlag{ Name: cfdflags.OriginCert, Usage: "Path to the certificate generated for your origin when you run cloudflared login.", EnvVars: []string{"TUNNEL_ORIGIN_CERT"}, Value: credentials.FindDefaultOriginCertPath(), Hidden: shouldHide, }), altsrc.NewDurationFlag(&cli.DurationFlag{ Name: cfdflags.AutoUpdateFreq, Usage: fmt.Sprintf("Autoupdate frequency. Default is %v.", updater.DefaultCheckUpdateFreq), Value: updater.DefaultCheckUpdateFreq, Hidden: shouldHide, }), altsrc.NewBoolFlag(&cli.BoolFlag{ Name: cfdflags.NoAutoUpdate, Usage: "Disable periodic check for updates, restarting the server with the new version.", EnvVars: []string{"NO_AUTOUPDATE"}, Value: false, Hidden: shouldHide, }), altsrc.NewStringFlag(&cli.StringFlag{ Name: cfdflags.Metrics, Value: metrics.GetMetricsDefaultAddress(metrics.Runtime), Usage: fmt.Sprintf( `Listen address for metrics reporting. If no address is passed cloudflared will try to bind to %v. If all are unavailable, a random port will be used. Note that when running cloudflared from an virtual environment the default address binds to all interfaces, hence, it is important to isolate the host and virtualized host network stacks from each other`, metrics.GetMetricsKnownAddresses(metrics.Runtime), ), EnvVars: []string{"TUNNEL_METRICS"}, Hidden: shouldHide, }), altsrc.NewStringFlag(&cli.StringFlag{ Name: "pidfile", Usage: "Write the application's PID to this file after first successful connection.", EnvVars: []string{"TUNNEL_PIDFILE"}, Hidden: shouldHide, }), } } func configureProxyFlags(shouldHide bool) []cli.Flag { flags := []cli.Flag{ altsrc.NewStringFlag(&cli.StringFlag{ Name: "url", Value: "http://localhost:8080", Usage: "Connect to the local webserver at `URL`.", EnvVars: []string{"TUNNEL_URL"}, Hidden: shouldHide, }), altsrc.NewBoolFlag(&cli.BoolFlag{ Name: ingress.HelloWorldFlag, Value: false, Usage: "Run Hello World Server", EnvVars: []string{"TUNNEL_HELLO_WORLD"}, Hidden: shouldHide, }), altsrc.NewBoolFlag(&cli.BoolFlag{ Name: ingress.Socks5Flag, Usage: legacyTunnelFlag("specify if this tunnel is running as a SOCK5 Server"), EnvVars: []string{"TUNNEL_SOCKS"}, Value: false, Hidden: shouldHide, }), altsrc.NewDurationFlag(&cli.DurationFlag{ Name: ingress.ProxyConnectTimeoutFlag, Usage: legacyTunnelFlag("HTTP proxy timeout for establishing a new connection"), Value: time.Second * 30, Hidden: shouldHide, }), altsrc.NewDurationFlag(&cli.DurationFlag{ Name: ingress.ProxyTLSTimeoutFlag, Usage: legacyTunnelFlag("HTTP proxy timeout for completing a TLS handshake"), Value: time.Second * 10, Hidden: shouldHide, }), altsrc.NewDurationFlag(&cli.DurationFlag{ Name: ingress.ProxyTCPKeepAliveFlag, Usage: legacyTunnelFlag("HTTP proxy TCP keepalive duration"), Value: time.Second * 30, Hidden: shouldHide, }), altsrc.NewBoolFlag(&cli.BoolFlag{ Name: ingress.ProxyNoHappyEyeballsFlag, Usage: legacyTunnelFlag("HTTP proxy should disable \"happy eyeballs\" for IPv4/v6 fallback"), Hidden: shouldHide, }), altsrc.NewIntFlag(&cli.IntFlag{ Name: ingress.ProxyKeepAliveConnectionsFlag, Usage: legacyTunnelFlag("HTTP proxy maximum keepalive connection pool size"), Value: 100, Hidden: shouldHide, }), altsrc.NewDurationFlag(&cli.DurationFlag{ Name: ingress.ProxyKeepAliveTimeoutFlag, Usage: legacyTunnelFlag("HTTP proxy timeout for closing an idle connection"), Value: time.Second * 90, Hidden: shouldHide, }), altsrc.NewDurationFlag(&cli.DurationFlag{ Name: "proxy-connection-timeout", Usage: "DEPRECATED. No longer has any effect.", Value: time.Second * 90, Hidden: shouldHide, }), altsrc.NewDurationFlag(&cli.DurationFlag{ Name: "proxy-expect-continue-timeout", Usage: "DEPRECATED. No longer has any effect.", Value: time.Second * 90, Hidden: shouldHide, }), altsrc.NewStringFlag(&cli.StringFlag{ Name: ingress.HTTPHostHeaderFlag, Usage: legacyTunnelFlag("Sets the HTTP Host header for the local webserver."), EnvVars: []string{"TUNNEL_HTTP_HOST_HEADER"}, Hidden: shouldHide, }), altsrc.NewStringFlag(&cli.StringFlag{ Name: ingress.OriginServerNameFlag, Usage: legacyTunnelFlag("Hostname on the origin server certificate."), EnvVars: []string{"TUNNEL_ORIGIN_SERVER_NAME"}, Hidden: shouldHide, }), altsrc.NewStringFlag(&cli.StringFlag{ Name: "unix-socket", Usage: "Path to unix socket to use instead of --url", EnvVars: []string{"TUNNEL_UNIX_SOCKET"}, Hidden: shouldHide, }), altsrc.NewStringFlag(&cli.StringFlag{ Name: tlsconfig.OriginCAPoolFlag, Usage: legacyTunnelFlag("Path to the CA for the certificate of your origin. This option should be used only if your certificate is not signed by Cloudflare."), EnvVars: []string{"TUNNEL_ORIGIN_CA_POOL"}, Hidden: shouldHide, }), altsrc.NewBoolFlag(&cli.BoolFlag{ Name: ingress.NoTLSVerifyFlag, Usage: legacyTunnelFlag("Disables TLS verification of the certificate presented by your origin. Will allow any certificate from the origin to be accepted. Note: The connection from your machine to Cloudflare's Edge is still encrypted."), EnvVars: []string{"NO_TLS_VERIFY"}, Hidden: shouldHide, }), altsrc.NewBoolFlag(&cli.BoolFlag{ Name: ingress.NoChunkedEncodingFlag, Usage: legacyTunnelFlag("Disables chunked transfer encoding; useful if you are running a WSGI server."), EnvVars: []string{"TUNNEL_NO_CHUNKED_ENCODING"}, Hidden: shouldHide, }), altsrc.NewBoolFlag(&cli.BoolFlag{ Name: ingress.Http2OriginFlag, Usage: "Enables HTTP/2 origin servers.", EnvVars: []string{"TUNNEL_ORIGIN_ENABLE_HTTP2"}, Hidden: shouldHide, Value: false, }), altsrc.NewStringFlag(&cli.StringFlag{ Name: cfdflags.ManagementHostname, Usage: "Management hostname to signify incoming management requests", EnvVars: []string{"TUNNEL_MANAGEMENT_HOSTNAME"}, Hidden: true, Value: "management.argotunnel.com", }), altsrc.NewStringFlag(&cli.StringFlag{ Name: "service-op-ip", Usage: "Fallback IP for service operations run by the management service.", EnvVars: []string{"TUNNEL_SERVICE_OP_IP"}, Hidden: true, Value: "198.41.200.113:80", }), } return append(flags, sshFlags(shouldHide)...) } func legacyTunnelFlag(msg string) string { return fmt.Sprintf( "%s This flag only takes effect if you define your origin with `--url` and if you do not use ingress rules."+ " The recommended way is to rely on ingress rules and define this property under `originRequest` as per"+ " https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/configuration/configuration-file/ingress", msg, ) } func sshFlags(shouldHide bool) []cli.Flag { return []cli.Flag{ altsrc.NewStringFlag(&cli.StringFlag{ Name: cfdflags.SshPort, Usage: "Localhost port that cloudflared SSH server will run on", Value: "2222", EnvVars: []string{"LOCAL_SSH_PORT"}, Hidden: true, }), altsrc.NewDurationFlag(&cli.DurationFlag{ Name: cfdflags.SshIdleTimeout, Usage: "Connection timeout after no activity", EnvVars: []string{"SSH_IDLE_TIMEOUT"}, Hidden: true, }), altsrc.NewDurationFlag(&cli.DurationFlag{ Name: cfdflags.SshMaxTimeout, Usage: "Absolute connection timeout", EnvVars: []string{"SSH_MAX_TIMEOUT"}, Hidden: true, }), altsrc.NewStringFlag(&cli.StringFlag{ Name: cfdflags.SshLogUploaderBucketName, Usage: "Bucket name of where to upload SSH logs", EnvVars: []string{"BUCKET_ID"}, Hidden: true, }), altsrc.NewStringFlag(&cli.StringFlag{ Name: cfdflags.SshLogUploaderRegionName, Usage: "Region name of where to upload SSH logs", EnvVars: []string{"REGION_ID"}, Hidden: true, }), altsrc.NewStringFlag(&cli.StringFlag{ Name: cfdflags.SshLogUploaderSecretID, Usage: "Secret ID of where to upload SSH logs", EnvVars: []string{"SECRET_ID"}, Hidden: true, }), altsrc.NewStringFlag(&cli.StringFlag{ Name: cfdflags.SshLogUploaderAccessKeyID, Usage: "Access Key ID of where to upload SSH logs", EnvVars: []string{"ACCESS_CLIENT_ID"}, Hidden: true, }), altsrc.NewStringFlag(&cli.StringFlag{ Name: cfdflags.SshLogUploaderSessionTokenID, Usage: "Session Token to use in the configuration of SSH logs uploading", EnvVars: []string{"SESSION_TOKEN_ID"}, Hidden: true, }), altsrc.NewStringFlag(&cli.StringFlag{ Name: cfdflags.SshLogUploaderS3URL, Usage: "S3 url of where to upload SSH logs", EnvVars: []string{"S3_URL"}, Hidden: true, }), altsrc.NewPathFlag(&cli.PathFlag{ Name: cfdflags.HostKeyPath, Usage: "Absolute path of directory to save SSH host keys in", EnvVars: []string{"HOST_KEY_PATH"}, Hidden: true, }), altsrc.NewBoolFlag(&cli.BoolFlag{ Name: ingress.SSHServerFlag, Value: false, Usage: "Run an SSH Server", EnvVars: []string{"TUNNEL_SSH_SERVER"}, Hidden: true, // TODO: remove when feature is complete }), altsrc.NewBoolFlag(&cli.BoolFlag{ Name: config.BastionFlag, Value: false, Usage: "Runs as jump host", EnvVars: []string{"TUNNEL_BASTION"}, Hidden: shouldHide, }), altsrc.NewStringFlag(&cli.StringFlag{ Name: ingress.ProxyAddressFlag, Usage: "Listen address for the proxy.", Value: "127.0.0.1", EnvVars: []string{"TUNNEL_PROXY_ADDRESS"}, Hidden: shouldHide, }), // Note TUN-3758 , we use Int because UInt is not supported with altsrc altsrc.NewIntFlag(&cli.IntFlag{ Name: ingress.ProxyPortFlag, Usage: "Listen port for the proxy.", Value: 0, EnvVars: []string{"TUNNEL_PROXY_PORT"}, Hidden: shouldHide, }), } } func stdinControl(reconnectCh chan supervisor.ReconnectSignal, log *zerolog.Logger) { for { scanner := bufio.NewScanner(os.Stdin) for scanner.Scan() { command := scanner.Text() parts := strings.SplitN(command, " ", 2) switch parts[0] { case "": break case "reconnect": var reconnect supervisor.ReconnectSignal if len(parts) > 1 { var err error if reconnect.Delay, err = time.ParseDuration(parts[1]); err != nil { log.Error().Msg(err.Error()) continue } } log.Info().Msgf("Sending %+v", reconnect) reconnectCh <- reconnect default: log.Info().Str(LogFieldCommand, command).Msg("Unknown command") fallthrough case "help": log.Info().Msg(`Supported command: reconnect [delay] - restarts one randomly chosen connection with optional delay before reconnect`) } } } } func nonSecretCliFlags(log *zerolog.Logger, cli *cli.Context, flagInclusionList []string) map[string]string { flagsNames := cli.FlagNames() flags := make(map[string]string, len(flagsNames)) for _, flag := range flagsNames { value := cli.String(flag) if value == "" { continue } isIncluded := isFlagIncluded(flagInclusionList, flag) if !isIncluded { continue } switch flag { case cfdflags.LogDirectory, cfdflags.LogFile: { absolute, err := filepath.Abs(value) if err != nil { log.Error().Err(err).Msgf("could not convert %s path to absolute", flag) } else { flags[flag] = absolute } } default: flags[flag] = value } } return flags } func isFlagIncluded(flagInclusionList []string, flag string) bool { for _, include := range flagInclusionList { if include == flag { return true } } return false } ================================================ FILE: cmd/cloudflared/tunnel/cmd_test.go ================================================ package tunnel import ( "testing" "github.com/stretchr/testify/assert" ) func TestHostnameFromURI(t *testing.T) { assert.Equal(t, "awesome.warptunnels.horse:22", hostnameFromURI("ssh://awesome.warptunnels.horse:22")) assert.Equal(t, "awesome.warptunnels.horse:22", hostnameFromURI("ssh://awesome.warptunnels.horse")) assert.Equal(t, "awesome.warptunnels.horse:2222", hostnameFromURI("ssh://awesome.warptunnels.horse:2222")) assert.Equal(t, "localhost:3389", hostnameFromURI("rdp://localhost")) assert.Equal(t, "localhost:3390", hostnameFromURI("rdp://localhost:3390")) assert.Equal(t, "", hostnameFromURI("trash")) assert.Equal(t, "", hostnameFromURI("https://awesomesauce.com")) } ================================================ FILE: cmd/cloudflared/tunnel/configuration.go ================================================ package tunnel import ( "context" "crypto/tls" "fmt" "net" "net/netip" "os" "strings" "time" "github.com/pkg/errors" "github.com/prometheus/client_golang/prometheus" "github.com/rs/zerolog" "github.com/urfave/cli/v2" "github.com/urfave/cli/v2/altsrc" "golang.org/x/term" "github.com/cloudflare/cloudflared/client" "github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil" "github.com/cloudflare/cloudflared/cmd/cloudflared/flags" "github.com/cloudflare/cloudflared/config" "github.com/cloudflare/cloudflared/connection" "github.com/cloudflare/cloudflared/edgediscovery" "github.com/cloudflare/cloudflared/edgediscovery/allregions" "github.com/cloudflare/cloudflared/features" "github.com/cloudflare/cloudflared/ingress" "github.com/cloudflare/cloudflared/ingress/origins" "github.com/cloudflare/cloudflared/orchestration" "github.com/cloudflare/cloudflared/supervisor" "github.com/cloudflare/cloudflared/tlsconfig" "github.com/cloudflare/cloudflared/tunnelrpc/pogs" ) const ( secretValue = "*****" icmpFunnelTimeout = time.Second * 10 ) var ( secretFlags = [2]*altsrc.StringFlag{credentialsContentsFlag, tunnelTokenFlag} configFlags = []string{ flags.AutoUpdateFreq, flags.NoAutoUpdate, flags.Retries, flags.Protocol, flags.LogLevel, flags.TransportLogLevel, flags.OriginCert, flags.Metrics, flags.MetricsUpdateFreq, flags.EdgeIpVersion, flags.EdgeBindAddress, flags.MaxActiveFlows, } ) func logClientOptions(c *cli.Context, log *zerolog.Logger) { flags := make(map[string]interface{}) for _, flag := range c.FlagNames() { if isSecretFlag(flag) { flags[flag] = secretValue } else { flags[flag] = c.Generic(flag) } } if len(flags) > 0 { log.Info().Msgf("Settings: %v", flags) } envs := make(map[string]string) // Find env variables for Argo Tunnel for _, env := range os.Environ() { // All Argo Tunnel env variables start with TUNNEL_ if strings.Contains(env, "TUNNEL_") { vars := strings.Split(env, "=") if len(vars) == 2 { if isSecretEnvVar(vars[0]) { envs[vars[0]] = secretValue } else { envs[vars[0]] = vars[1] } } } } if len(envs) > 0 { log.Info().Msgf("Environmental variables %v", envs) } } func isSecretFlag(key string) bool { for _, flag := range secretFlags { if flag.Name == key { return true } } return false } func isSecretEnvVar(key string) bool { for _, flag := range secretFlags { for _, secretEnvVar := range flag.EnvVars { if secretEnvVar == key { return true } } } return false } func prepareTunnelConfig( ctx context.Context, c *cli.Context, info *cliutil.BuildInfo, log, logTransport *zerolog.Logger, observer *connection.Observer, namedTunnel *connection.TunnelProperties, ) (*supervisor.TunnelConfig, *orchestration.Config, error) { transportProtocol := c.String(flags.Protocol) isPostQuantumEnforced := c.Bool(flags.PostQuantum) featureSelector, err := features.NewFeatureSelector(ctx, namedTunnel.Credentials.AccountTag, c.StringSlice(flags.Features), isPostQuantumEnforced, log) if err != nil { return nil, nil, errors.Wrap(err, "Failed to create feature selector") } clientConfig, err := client.NewConfig(info.Version(), info.OSArch(), featureSelector) if err != nil { return nil, nil, err } log.Info().Msgf("Generated Connector ID: %s", clientConfig.ConnectorID) tags, err := NewTagSliceFromCLI(c.StringSlice(flags.Tag)) if err != nil { log.Err(err).Msg("Tag parse failure") return nil, nil, errors.Wrap(err, "Tag parse failure") } tags = append(tags, pogs.Tag{Name: "ID", Value: clientConfig.ConnectorID.String()}) clientFeatures := featureSelector.Snapshot() pqMode := clientFeatures.PostQuantum if pqMode == features.PostQuantumStrict { // Error if the user tries to force a non-quic transport protocol if transportProtocol != connection.AutoSelectFlag && transportProtocol != connection.QUIC.String() { return nil, nil, fmt.Errorf("post-quantum is only supported with the quic transport") } transportProtocol = connection.QUIC.String() } cfg := config.GetConfiguration() ingressRules, err := ingress.ParseIngressFromConfigAndCLI(cfg, c, log) if err != nil { return nil, nil, err } protocolSelector, err := connection.NewProtocolSelector(transportProtocol, namedTunnel.Credentials.AccountTag, c.IsSet(TunnelTokenFlag), isPostQuantumEnforced, edgediscovery.ProtocolPercentage, connection.ResolveTTL, log) if err != nil { return nil, nil, err } log.Info().Msgf("Initial protocol %s", protocolSelector.Current()) edgeTLSConfigs := make(map[connection.Protocol]*tls.Config, len(connection.ProtocolList)) for _, p := range connection.ProtocolList { tlsSettings := p.TLSSettings() if tlsSettings == nil { return nil, nil, fmt.Errorf("%s has unknown TLS settings", p) } edgeTLSConfig, err := tlsconfig.CreateTunnelConfig(c, tlsSettings.ServerName) if err != nil { return nil, nil, errors.Wrap(err, "unable to create TLS config to connect with edge") } if len(tlsSettings.NextProtos) > 0 { edgeTLSConfig.NextProtos = tlsSettings.NextProtos } edgeTLSConfigs[p] = edgeTLSConfig } gracePeriod, err := gracePeriod(c) if err != nil { return nil, nil, err } edgeIPVersion, err := parseConfigIPVersion(c.String(flags.EdgeIpVersion)) if err != nil { return nil, nil, err } edgeBindAddr, err := parseConfigBindAddress(c.String(flags.EdgeBindAddress)) if err != nil { return nil, nil, err } if err := testIPBindable(edgeBindAddr); err != nil { return nil, nil, fmt.Errorf("invalid edge-bind-address %s: %v", edgeBindAddr, err) } edgeIPVersion, err = adjustIPVersionByBindAddress(edgeIPVersion, edgeBindAddr) if err != nil { // This is not a fatal error, we just overrode edgeIPVersion log.Warn().Str("edgeIPVersion", edgeIPVersion.String()).Err(err).Msg("Overriding edge-ip-version") } region := c.String(flags.Region) endpoint := namedTunnel.Credentials.Endpoint var resolvedRegion string // set resolvedRegion to either the region passed as argument // or to the endpoint in the credentials. // Region and endpoint are interchangeable if region != "" && endpoint != "" { return nil, nil, fmt.Errorf("region provided with a token that has an endpoint") } else if region != "" { resolvedRegion = region } else if endpoint != "" { resolvedRegion = endpoint } warpRoutingConfig := ingress.NewWarpRoutingConfig(&cfg.WarpRouting) // Setup origin dialer service and virtual services originDialerService := ingress.NewOriginDialer(ingress.OriginConfig{ DefaultDialer: ingress.NewDialer(warpRoutingConfig), TCPWriteTimeout: c.Duration(flags.WriteStreamTimeout), }, log) // Setup DNS Resolver Service originMetrics := origins.NewMetrics(prometheus.DefaultRegisterer) dnsResolverAddrs := c.StringSlice(flags.VirtualDNSServiceResolverAddresses) dnsService := origins.NewDNSResolverService(origins.NewDNSDialer(), log, originMetrics) if len(dnsResolverAddrs) > 0 { addrs, err := parseResolverAddrPorts(dnsResolverAddrs) if err != nil { return nil, nil, fmt.Errorf("invalid %s provided: %w", flags.VirtualDNSServiceResolverAddresses, err) } dnsService = origins.NewStaticDNSResolverService(addrs, origins.NewDNSDialer(), log, originMetrics) } originDialerService.AddReservedService(dnsService, []netip.AddrPort{origins.VirtualDNSServiceAddr}) tunnelConfig := &supervisor.TunnelConfig{ ClientConfig: clientConfig, GracePeriod: gracePeriod, EdgeAddrs: c.StringSlice(flags.Edge), Region: resolvedRegion, EdgeIPVersion: edgeIPVersion, EdgeBindAddr: edgeBindAddr, HAConnections: c.Int(flags.HaConnections), IsAutoupdated: c.Bool(flags.IsAutoUpdated), LBPool: c.String(flags.LBPool), Tags: tags, Log: log, LogTransport: logTransport, Observer: observer, ReportedVersion: info.Version(), // Note TUN-3758 , we use Int because UInt is not supported with altsrc Retries: uint(c.Int(flags.Retries)), // nolint: gosec RunFromTerminal: isRunningFromTerminal(), NamedTunnel: namedTunnel, ProtocolSelector: protocolSelector, EdgeTLSConfigs: edgeTLSConfigs, MaxEdgeAddrRetries: uint8(c.Int(flags.MaxEdgeAddrRetries)), // nolint: gosec RPCTimeout: c.Duration(flags.RpcTimeout), WriteStreamTimeout: c.Duration(flags.WriteStreamTimeout), DisableQUICPathMTUDiscovery: c.Bool(flags.QuicDisablePathMTUDiscovery), QUICConnectionLevelFlowControlLimit: c.Uint64(flags.QuicConnLevelFlowControlLimit), QUICStreamLevelFlowControlLimit: c.Uint64(flags.QuicStreamLevelFlowControlLimit), OriginDNSService: dnsService, OriginDialerService: originDialerService, } icmpRouter, err := newICMPRouter(c, log) if err != nil { log.Warn().Err(err).Msg("ICMP proxy feature is disabled") } else { tunnelConfig.ICMPRouterServer = icmpRouter } orchestratorConfig := &orchestration.Config{ Ingress: &ingressRules, WarpRouting: warpRoutingConfig, OriginDialerService: originDialerService, ConfigurationFlags: parseConfigFlags(c), } return tunnelConfig, orchestratorConfig, nil } func parseConfigFlags(c *cli.Context) map[string]string { result := make(map[string]string) for _, flag := range configFlags { if v := c.String(flag); c.IsSet(flag) && v != "" { result[flag] = v } } return result } func gracePeriod(c *cli.Context) (time.Duration, error) { period := c.Duration(flags.GracePeriod) if period > connection.MaxGracePeriod { return time.Duration(0), fmt.Errorf("%s must be equal or less than %v", flags.GracePeriod, connection.MaxGracePeriod) } return period, nil } func isRunningFromTerminal() bool { return term.IsTerminal(int(os.Stdout.Fd())) } // ParseConfigIPVersion returns the IP version from possible expected values from config func parseConfigIPVersion(version string) (v allregions.ConfigIPVersion, err error) { switch version { case "4": v = allregions.IPv4Only case "6": v = allregions.IPv6Only case "auto": v = allregions.Auto default: // unspecified or invalid err = fmt.Errorf("invalid value for edge-ip-version: %s", version) } return } func parseConfigBindAddress(ipstr string) (net.IP, error) { // Unspecified - it's fine if ipstr == "" { return nil, nil } ip := net.ParseIP(ipstr) if ip == nil { return nil, fmt.Errorf("invalid value for edge-bind-address: %s", ipstr) } return ip, nil } func testIPBindable(ip net.IP) error { // "Unspecified" = let OS choose, so always bindable if ip == nil { return nil } addr := &net.UDPAddr{IP: ip, Port: 0} listener, err := net.ListenUDP("udp", addr) if err != nil { return err } listener.Close() return nil } func adjustIPVersionByBindAddress(ipVersion allregions.ConfigIPVersion, ip net.IP) (allregions.ConfigIPVersion, error) { if ip == nil { return ipVersion, nil } // https://pkg.go.dev/net#IP.To4: "If ip is not an IPv4 address, To4 returns nil." if ip.To4() != nil { if ipVersion == allregions.IPv6Only { return allregions.IPv4Only, fmt.Errorf("IPv4 bind address is specified, but edge-ip-version is IPv6") } return allregions.IPv4Only, nil } else { if ipVersion == allregions.IPv4Only { return allregions.IPv6Only, fmt.Errorf("IPv6 bind address is specified, but edge-ip-version is IPv4") } return allregions.IPv6Only, nil } } func newICMPRouter(c *cli.Context, logger *zerolog.Logger) (ingress.ICMPRouterServer, error) { ipv4Src, ipv6Src, err := determineICMPSources(c, logger) if err != nil { return nil, err } icmpRouter, err := ingress.NewICMPRouter(ipv4Src, ipv6Src, logger, icmpFunnelTimeout) if err != nil { return nil, err } return icmpRouter, nil } func determineICMPSources(c *cli.Context, logger *zerolog.Logger) (netip.Addr, netip.Addr, error) { ipv4Src, err := determineICMPv4Src(c.String(flags.ICMPV4Src), logger) if err != nil { return netip.Addr{}, netip.Addr{}, errors.Wrap(err, "failed to determine IPv4 source address for ICMP proxy") } logger.Info().Msgf("ICMP proxy will use %s as source for IPv4", ipv4Src) ipv6Src, zone, err := determineICMPv6Src(c.String(flags.ICMPV6Src), logger, ipv4Src) if err != nil { return netip.Addr{}, netip.Addr{}, errors.Wrap(err, "failed to determine IPv6 source address for ICMP proxy") } if zone != "" { logger.Info().Msgf("ICMP proxy will use %s in zone %s as source for IPv6", ipv6Src, zone) } else { logger.Info().Msgf("ICMP proxy will use %s as source for IPv6", ipv6Src) } return ipv4Src, ipv6Src, nil } func determineICMPv4Src(userDefinedSrc string, logger *zerolog.Logger) (netip.Addr, error) { if userDefinedSrc != "" { addr, err := netip.ParseAddr(userDefinedSrc) if err != nil { return netip.Addr{}, err } if addr.Is4() { return addr, nil } return netip.Addr{}, fmt.Errorf("expect IPv4, but %s is IPv6", userDefinedSrc) } addr, err := findLocalAddr(net.ParseIP("192.168.0.1"), 53) if err != nil { addr = netip.IPv4Unspecified() logger.Debug().Err(err).Msgf("Failed to determine the IPv4 for this machine. It will use %s to send/listen for ICMPv4 echo", addr) } return addr, nil } type interfaceIP struct { name string ip net.IP } func determineICMPv6Src(userDefinedSrc string, logger *zerolog.Logger, ipv4Src netip.Addr) (addr netip.Addr, zone string, err error) { if userDefinedSrc != "" { addr, err := netip.ParseAddr(userDefinedSrc) if err != nil { return netip.Addr{}, "", err } if addr.Is6() { return addr, addr.Zone(), nil } return netip.Addr{}, "", fmt.Errorf("expect IPv6, but %s is IPv4", userDefinedSrc) } // Loop through all the interfaces, the preference is // 1. The interface where ipv4Src is in // 2. Interface with IPv6 address // 3. Unspecified interface interfaces, err := net.Interfaces() if err != nil { return netip.IPv6Unspecified(), "", nil } interfacesWithIPv6 := make([]interfaceIP, 0) for _, interf := range interfaces { interfaceAddrs, err := interf.Addrs() if err != nil { continue } foundIPv4SrcInterface := false for _, interfaceAddr := range interfaceAddrs { if ipnet, ok := interfaceAddr.(*net.IPNet); ok { ip := ipnet.IP if ip.Equal(ipv4Src.AsSlice()) { foundIPv4SrcInterface = true } if ip.To4() == nil { interfacesWithIPv6 = append(interfacesWithIPv6, interfaceIP{ name: interf.Name, ip: ip, }) } } } // Found the interface of ipv4Src. Loop through the addresses to see if there is an IPv6 if foundIPv4SrcInterface { for _, interfaceAddr := range interfaceAddrs { if ipnet, ok := interfaceAddr.(*net.IPNet); ok { ip := ipnet.IP if ip.To4() == nil { addr, err := netip.ParseAddr(ip.String()) if err == nil { return addr, interf.Name, nil } } } } } } for _, interf := range interfacesWithIPv6 { addr, err := netip.ParseAddr(interf.ip.String()) if err == nil { return addr, interf.name, nil } } logger.Debug().Err(err).Msgf("Failed to determine the IPv6 for this machine. It will use %s to send/listen for ICMPv6 echo", netip.IPv6Unspecified()) return netip.IPv6Unspecified(), "", nil } // FindLocalAddr tries to dial UDP and returns the local address picked by the OS func findLocalAddr(dst net.IP, port int) (netip.Addr, error) { udpConn, err := net.DialUDP("udp", nil, &net.UDPAddr{ IP: dst, Port: port, }) if err != nil { return netip.Addr{}, err } defer udpConn.Close() localAddrPort, err := netip.ParseAddrPort(udpConn.LocalAddr().String()) if err != nil { return netip.Addr{}, err } localAddr := localAddrPort.Addr() return localAddr, nil } func parseResolverAddrPorts(input []string) ([]netip.AddrPort, error) { // We don't allow more than 10 resolvers to be provided statically for the resolver service. if len(input) > 10 { return nil, errors.New("too many addresses provided, max: 10") } addrs := make([]netip.AddrPort, 0, len(input)) for _, val := range input { addr, err := netip.ParseAddrPort(val) if err != nil { return nil, err } addrs = append(addrs, addr) } return addrs, nil } ================================================ FILE: cmd/cloudflared/tunnel/configuration_test.go ================================================ //go:build ignore // TODO: Remove the above build tag and include this test when we start compiling with Golang 1.10.0+ package tunnel import ( "crypto/x509" "crypto/x509/pkix" "encoding/asn1" "net" "os" "testing" "github.com/stretchr/testify/assert" ) // Generated using `openssl req -newkey rsa:512 -nodes -x509 -days 3650` var samplePEM = []byte(` -----BEGIN CERTIFICATE----- MIIB4DCCAYoCCQCb/H0EUrdXEjANBgkqhkiG9w0BAQsFADB3MQswCQYDVQQGEwJV UzEOMAwGA1UECAwFVGV4YXMxDzANBgNVBAcMBkF1c3RpbjEZMBcGA1UECgwQQ2xv dWRmbGFyZSwgSW5jLjEZMBcGA1UECwwQUHJvZHVjdCBTdHJhdGVneTERMA8GA1UE AwwIVGVzdCBPbmUwHhcNMTgwNDI2MTYxMDUxWhcNMjgwNDIzMTYxMDUxWjB3MQsw CQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxDzANBgNVBAcMBkF1c3RpbjEZMBcG A1UECgwQQ2xvdWRmbGFyZSwgSW5jLjEZMBcGA1UECwwQUHJvZHVjdCBTdHJhdGVn eTERMA8GA1UEAwwIVGVzdCBPbmUwXDANBgkqhkiG9w0BAQEFAANLADBIAkEAwVQD K0SJ25UFLznm2pU3zhzMEvpDEofHVNnCjk4mlDrtVop7PkKZ8pDEmuQANltUrxC8 yHBE2wXMv+GlH+bDtwIDAQABMA0GCSqGSIb3DQEBCwUAA0EAjVYQzozIFPkt/HRY uUoZ8zEHIDICb0syFf5VAjm9AgTwIPzUmD+c5vl6LWDnxq7L45nLCzhhQ6YmiwDz X7Wcyg== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIB4DCCAYoCCQDZfCdAJ+mwzDANBgkqhkiG9w0BAQsFADB3MQswCQYDVQQGEwJV UzEOMAwGA1UECAwFVGV4YXMxDzANBgNVBAcMBkF1c3RpbjEZMBcGA1UECgwQQ2xv dWRmbGFyZSwgSW5jLjEZMBcGA1UECwwQUHJvZHVjdCBTdHJhdGVneTERMA8GA1UE AwwIVGVzdCBUd28wHhcNMTgwNDI2MTYxMTIwWhcNMjgwNDIzMTYxMTIwWjB3MQsw CQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxDzANBgNVBAcMBkF1c3RpbjEZMBcG A1UECgwQQ2xvdWRmbGFyZSwgSW5jLjEZMBcGA1UECwwQUHJvZHVjdCBTdHJhdGVn eTERMA8GA1UEAwwIVGVzdCBUd28wXDANBgkqhkiG9w0BAQEFAANLADBIAkEAoHKp ROVK3zCSsH7ocYeyRAML4V7SFAbZcb4WIwDnE08oMBVRkQVcW5tqEkvG3RiClfzV wZIJ3CfqKIeSNSDU9wIDAQABMA0GCSqGSIb3DQEBCwUAA0EAJw2gUbnPiq4C2p5b iWzlA9Q7aKo+VQ4H7IZS7tTccr59nVjvH/TG3eWujpnocr4TOqW9M3CK1DF9mUGP 3pQ3Jg== -----END CERTIFICATE----- `) var systemCertPoolSubjects []*pkix.Name type certificateFixture struct { ou string cn string } func TestMain(m *testing.M) { systemCertPool, err := x509.SystemCertPool() if isUnrecoverableError(err) { os.Exit(1) } if systemCertPool == nil { // On Windows, let's just assume the system cert pool was empty systemCertPool = x509.NewCertPool() } systemCertPoolSubjects, err = getCertPoolSubjects(systemCertPool) if err != nil { os.Exit(1) } os.Exit(m.Run()) } func TestLoadOriginCertPoolJustSystemPool(t *testing.T) { certPoolSubjects := loadCertPoolSubjects(t, nil) extraSubjects := subjectSubtract(systemCertPoolSubjects, certPoolSubjects) // Remove extra subjects from the cert pool var filteredSystemCertPoolSubjects []*pkix.Name t.Log(extraSubjects) OUTER: for _, subject := range certPoolSubjects { for _, extraSubject := range extraSubjects { if subject == extraSubject { t.Log(extraSubject) continue OUTER } } filteredSystemCertPoolSubjects = append(filteredSystemCertPoolSubjects, subject) } assert.Equal(t, len(filteredSystemCertPoolSubjects), len(systemCertPoolSubjects)) difference := subjectSubtract(systemCertPoolSubjects, filteredSystemCertPoolSubjects) assert.Equal(t, 0, len(difference)) } func TestLoadOriginCertPoolCFCertificates(t *testing.T) { certPoolSubjects := loadCertPoolSubjects(t, nil) extraSubjects := subjectSubtract(systemCertPoolSubjects, certPoolSubjects) expected := []*certificateFixture{ {ou: "CloudFlare Origin SSL ECC Certificate Authority"}, {ou: "CloudFlare Origin SSL Certificate Authority"}, {cn: "origin-pull.cloudflare.net"}, {cn: "Argo Tunnel Sample Hello Server Certificate"}, } assertFixturesMatchSubjects(t, expected, extraSubjects) } func TestLoadOriginCertPoolWithExtraPEMs(t *testing.T) { certPoolWithoutPEMSubjects := loadCertPoolSubjects(t, nil) certPoolWithPEMSubjects := loadCertPoolSubjects(t, samplePEM) difference := subjectSubtract(certPoolWithoutPEMSubjects, certPoolWithPEMSubjects) assert.Equal(t, 2, len(difference)) expected := []*certificateFixture{ {cn: "Test One"}, {cn: "Test Two"}, } assertFixturesMatchSubjects(t, expected, difference) } func loadCertPoolSubjects(t *testing.T, originCAPoolPEM []byte) []*pkix.Name { certPool, err := loadOriginCertPool(originCAPoolPEM) if isUnrecoverableError(err) { t.Fatal(err) } assert.NotEmpty(t, certPool.Subjects()) certPoolSubjects, err := getCertPoolSubjects(certPool) if err != nil { t.Fatal(err) } return certPoolSubjects } func assertFixturesMatchSubjects(t *testing.T, fixtures []*certificateFixture, subjects []*pkix.Name) { assert.Equal(t, len(fixtures), len(subjects)) for _, fixture := range fixtures { found := false for _, subject := range subjects { found = found || fixtureMatchesSubjectPredicate(fixture, subject) } if !found { t.Fail() } } } func fixtureMatchesSubjectPredicate(fixture *certificateFixture, subject *pkix.Name) bool { cnMatch := true if fixture.cn != "" { cnMatch = fixture.cn == subject.CommonName } ouMatch := true if fixture.ou != "" { ouMatch = len(subject.OrganizationalUnit) > 0 && fixture.ou == subject.OrganizationalUnit[0] } return cnMatch && ouMatch } func subjectSubtract(left []*pkix.Name, right []*pkix.Name) []*pkix.Name { var difference []*pkix.Name var found bool for _, r := range right { found = false for _, l := range left { if (*l).String() == (*r).String() { found = true } } if !found { difference = append(difference, r) } } return difference } func getCertPoolSubjects(certPool *x509.CertPool) ([]*pkix.Name, error) { var subjects []*pkix.Name for _, subject := range certPool.Subjects() { var sequence pkix.RDNSequence _, err := asn1.Unmarshal(subject, &sequence) if err != nil { return nil, err } name := pkix.Name{} name.FillFromRDNSequence(&sequence) subjects = append(subjects, &name) } return subjects, nil } func isUnrecoverableError(err error) bool { return err != nil && err.Error() != "crypto/x509: system root pool is not available on Windows" } func TestTestIPBindable(t *testing.T) { assert.Nil(t, testIPBindable(nil)) // Public services - if one of these IPs is on the machine, the test environment is too weird assert.NotNil(t, testIPBindable(net.ParseIP("8.8.8.8"))) assert.NotNil(t, testIPBindable(net.ParseIP("1.1.1.1"))) addrs, err := net.InterfaceAddrs() if err != nil { t.Fatal(err) } for i, addr := range addrs { if i >= 3 { break } ip := addr.(*net.IPNet).IP assert.Nil(t, testIPBindable(ip)) } } ================================================ FILE: cmd/cloudflared/tunnel/credential_finder.go ================================================ package tunnel import ( "fmt" "path/filepath" cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags" "github.com/cloudflare/cloudflared/config" "github.com/cloudflare/cloudflared/credentials" "github.com/google/uuid" "github.com/rs/zerolog" "github.com/urfave/cli/v2" ) // CredFinder can find the tunnel credentials file. type CredFinder interface { Path() (string, error) } // Implements CredFinder and looks for the credentials file at the given // filepath. type staticPath struct { filePath string fs fileSystem } func newStaticPath(filePath string, fs fileSystem) CredFinder { return staticPath{ filePath: filePath, fs: fs, } } func (a staticPath) Path() (string, error) { if a.filePath != "" && a.fs.validFilePath(a.filePath) { return a.filePath, nil } return "", fmt.Errorf("Tunnel credentials file '%s' doesn't exist or is not a file", a.filePath) } // Implements CredFinder and looks for the credentials file in several directories // searching for a file named .json type searchByID struct { id uuid.UUID c *cli.Context log *zerolog.Logger fs fileSystem } func newSearchByID(id uuid.UUID, c *cli.Context, log *zerolog.Logger, fs fileSystem) CredFinder { return searchByID{ id: id, c: c, log: log, fs: fs, } } func (s searchByID) Path() (string, error) { originCertPath := s.c.String(cfdflags.OriginCert) originCertLog := s.log.With(). Str("originCertPath", originCertPath). Logger() if originCertPath != "" { // Look for tunnel credentials in the origin cert directory if the flag is provided if originCertPath, err := credentials.FindOriginCert(originCertPath, &originCertLog); err == nil { originCertDir := filepath.Dir(originCertPath) if filePath, err := tunnelFilePath(s.id, originCertDir); err == nil { if s.fs.validFilePath(filePath) { return filePath, nil } } } } // Last resort look under default config directories for _, configDir := range config.DefaultConfigSearchDirectories() { if filePath, err := tunnelFilePath(s.id, configDir); err == nil { if s.fs.validFilePath(filePath) { return filePath, nil } } } return "", fmt.Errorf("tunnel credentials file not found") } ================================================ FILE: cmd/cloudflared/tunnel/filesystem.go ================================================ package tunnel import ( "os" ) // Abstract away details of reading files, so that SubcommandContext can read // from either the real filesystem, or a mock (when running unit tests). type fileSystem interface { readFile(filePath string) ([]byte, error) validFilePath(path string) bool } type realFileSystem struct{} func (fs realFileSystem) validFilePath(path string) bool { fileStat, err := os.Stat(path) if err != nil { return false } return !fileStat.IsDir() } func (fs realFileSystem) readFile(filePath string) ([]byte, error) { return os.ReadFile(filePath) } ================================================ FILE: cmd/cloudflared/tunnel/info.go ================================================ package tunnel import ( "time" "github.com/google/uuid" "github.com/cloudflare/cloudflared/cfapi" ) type Info struct { ID uuid.UUID `json:"id"` Name string `json:"name"` CreatedAt time.Time `json:"createdAt"` Connectors []*cfapi.ActiveClient `json:"conns"` } ================================================ FILE: cmd/cloudflared/tunnel/ingress_subcommands.go ================================================ package tunnel import ( "encoding/json" "fmt" "net/url" "github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil" "github.com/cloudflare/cloudflared/config" "github.com/cloudflare/cloudflared/ingress" "github.com/pkg/errors" "github.com/urfave/cli/v2" ) const ingressDataJSONFlagName = "json" var ingressDataJSON = &cli.StringFlag{ Name: ingressDataJSONFlagName, Aliases: []string{"j"}, Usage: `Accepts data in the form of json as an input rather than read from a file`, EnvVars: []string{"TUNNEL_INGRESS_VALIDATE_JSON"}, } func buildIngressSubcommand() *cli.Command { return &cli.Command{ Name: "ingress", Category: "Tunnel", Usage: "Validate and test cloudflared tunnel's ingress configuration", UsageText: "cloudflared tunnel [--config FILEPATH] ingress COMMAND [arguments...]", Hidden: true, Description: ` Cloudflared lets you route traffic from the internet to multiple different addresses on your origin. Multiple-origin routing is configured by a set of rules. Each rule matches traffic by its hostname or path, and routes it to an address. These rules are configured under the 'ingress' key of your config.yaml, for example: ingress: - hostname: www.example.com service: https://localhost:8000 - hostname: *.example.xyz path: /[a-zA-Z]+.html service: https://localhost:8001 - hostname: * service: https://localhost:8002 To ensure cloudflared can route all incoming requests, the last rule must be a catch-all rule that matches all traffic. You can validate these rules with the 'ingress validate' command, and test which rule matches a particular URL with 'ingress rule '. Multiple-origin routing is incompatible with the --url flag.`, Subcommands: []*cli.Command{buildValidateIngressCommand(), buildTestURLCommand()}, } } func buildValidateIngressCommand() *cli.Command { return &cli.Command{ Name: "validate", Action: cliutil.ConfiguredActionWithWarnings(validateIngressCommand), Usage: "Validate the ingress configuration ", UsageText: "cloudflared tunnel [--config FILEPATH] ingress validate", Description: "Validates the configuration file, ensuring your ingress rules are OK.", Flags: []cli.Flag{ingressDataJSON}, } } func buildTestURLCommand() *cli.Command { return &cli.Command{ Name: "rule", Action: cliutil.ConfiguredAction(testURLCommand), Usage: "Check which ingress rule matches a given request URL", UsageText: "cloudflared tunnel [--config FILEPATH] ingress rule URL", ArgsUsage: "URL", Description: "Check which ingress rule matches a given request URL. " + "Ingress rules match a request's hostname and path. Hostname is " + "optional and is either a full hostname like `www.example.com` or a " + "hostname with a `*` for its subdomains, e.g. `*.example.com`. Path " + "is optional and matches a regular expression, like `/[a-zA-Z0-9_]+.html`", } } // validateIngressCommand check the syntax of the ingress rules in the cloudflared config file func validateIngressCommand(c *cli.Context, warnings string) error { conf, err := getConfiguration(c) if err != nil { return err } if _, err := ingress.ParseIngress(conf); err != nil { return errors.Wrap(err, "Validation failed") } if c.IsSet("url") { return ingress.ErrURLIncompatibleWithIngress } if warnings != "" { fmt.Println("Warning: unused keys detected in your config file. Here is a list of unused keys:") fmt.Println(warnings) return nil } fmt.Println("OK") return nil } func getConfiguration(c *cli.Context) (*config.Configuration, error) { var conf *config.Configuration if c.IsSet(ingressDataJSONFlagName) { ingressJSON := c.String(ingressDataJSONFlagName) fmt.Println("Validating rules from cmdline flag --json") err := json.Unmarshal([]byte(ingressJSON), &conf) return conf, err } conf = config.GetConfiguration() if conf.Source() == "" { return nil, errors.New("No configuration file was found. Please create one, or use the --config flag to specify its filepath. You can use the help command to learn more about configuration files") } fmt.Println("Validating rules from", conf.Source()) return conf, nil } // testURLCommand checks which ingress rule matches the given URL. func testURLCommand(c *cli.Context) error { requestArg := c.Args().First() if requestArg == "" { return errors.New("cloudflared tunnel rule expects a single argument, the URL to test") } requestURL, err := url.Parse(requestArg) if err != nil { return fmt.Errorf("%s is not a valid URL", requestArg) } if requestURL.Hostname() == "" && requestURL.Scheme == "" { return fmt.Errorf("%s doesn't have a hostname, consider adding a scheme", requestArg) } conf := config.GetConfiguration() fmt.Println("Using rules from", conf.Source()) ing, err := ingress.ParseIngress(conf) if err != nil { return errors.Wrap(err, "Validation failed") } _, i := ing.FindMatchingRule(requestURL.Hostname(), requestURL.Path) fmt.Printf("Matched rule #%d\n", i) fmt.Println(ing.Rules[i].MultiLineString()) return nil } ================================================ FILE: cmd/cloudflared/tunnel/login.go ================================================ package tunnel import ( "fmt" "net/url" "os" "path/filepath" "syscall" homedir "github.com/mitchellh/go-homedir" "github.com/pkg/errors" "github.com/urfave/cli/v2" "github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil" cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags" "github.com/cloudflare/cloudflared/config" "github.com/cloudflare/cloudflared/credentials" "github.com/cloudflare/cloudflared/logger" "github.com/cloudflare/cloudflared/token" ) const ( baseLoginURL = "https://dash.cloudflare.com/argotunnel" callbackURL = "https://login.cloudflareaccess.org/" fedBaseLoginURL = "https://dash.fed.cloudflare.com/argotunnel" fedCallbackStoreURL = "https://login.fed.cloudflareaccess.org/" fedRAMPParamName = "fedramp" loginURLParamName = "loginURL" callbackURLParamName = "callbackURL" ) var ( loginURL = &cli.StringFlag{ Name: loginURLParamName, Value: baseLoginURL, Usage: "The URL used to login (default is https://dash.cloudflare.com/argotunnel)", } callbackStore = &cli.StringFlag{ Name: callbackURLParamName, Value: callbackURL, Usage: "The URL used for the callback (default is https://login.cloudflareaccess.org/)", } fedramp = &cli.BoolFlag{ Name: fedRAMPParamName, Aliases: []string{"f"}, Usage: "Login with FedRAMP High environment.", } ) func buildLoginSubcommand(hidden bool) *cli.Command { return &cli.Command{ Name: "login", Action: cliutil.ConfiguredAction(login), Usage: "Generate a configuration file with your login details", ArgsUsage: " ", Hidden: hidden, Flags: []cli.Flag{ loginURL, callbackStore, fedramp, }, } } func login(c *cli.Context) error { log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog) path, ok, err := checkForExistingCert() if ok { log.Error().Err(err).Msgf("You have an existing certificate at %s which login would overwrite.\nIf this is intentional, please move or delete that file then run this command again.\n", path) return nil } else if err != nil { return err } var ( baseloginURL = c.String(loginURLParamName) callbackStoreURL = c.String(callbackURLParamName) ) isFEDRamp := c.Bool(fedRAMPParamName) if isFEDRamp { baseloginURL = fedBaseLoginURL callbackStoreURL = fedCallbackStoreURL } loginURL, err := url.Parse(baseloginURL) if err != nil { return err } resourceData, err := token.RunTransfer( loginURL, "", "cert", "callback", callbackStoreURL, false, false, c.Bool(cfdflags.AutoCloseInterstitial), isFEDRamp, log, ) if err != nil { log.Error().Err(err).Msgf("Failed to write the certificate.\n\nYour browser will download the certificate instead. You will have to manually\ncopy it to the following path:\n\n%s\n", path) return err } cert, err := credentials.DecodeOriginCert(resourceData) if err != nil { log.Error().Err(err).Msg("failed to decode origin certificate") return err } if isFEDRamp { cert.Endpoint = credentials.FedEndpoint } resourceData, err = cert.EncodeOriginCert() if err != nil { log.Error().Err(err).Msg("failed to encode origin certificate") return err } if err := os.WriteFile(path, resourceData, 0600); err != nil { return errors.Wrap(err, fmt.Sprintf("error writing cert to %s", path)) } log.Info().Msgf("You have successfully logged in.\nIf you wish to copy your credentials to a server, they have been saved to:\n%s\n", path) return nil } func checkForExistingCert() (string, bool, error) { configPath, err := homedir.Expand(config.DefaultConfigSearchDirectories()[0]) if err != nil { return "", false, err } ok, err := config.FileExists(configPath) if !ok && err == nil { // create config directory if doesn't already exist err = os.Mkdir(configPath, 0700) } if err != nil { return "", false, err } path := filepath.Join(configPath, credentials.DefaultCredentialFile) fileInfo, err := os.Stat(path) if err == nil && fileInfo.Size() > 0 { return path, true, nil } if err != nil && err.(*os.PathError).Err != syscall.ENOENT { return path, false, err } return path, false, nil } ================================================ FILE: cmd/cloudflared/tunnel/quick_tunnel.go ================================================ package tunnel import ( "encoding/json" "fmt" "io" "net/http" "strings" "time" "github.com/google/uuid" "github.com/pkg/errors" "github.com/cloudflare/cloudflared/cmd/cloudflared/flags" "github.com/cloudflare/cloudflared/connection" ) const httpTimeout = 15 * time.Second const disclaimer = "Thank you for trying Cloudflare Tunnel. Doing so, without a Cloudflare account, is a quick way to experiment and try it out. However, be aware that these account-less Tunnels have no uptime guarantee, are subject to the Cloudflare Online Services Terms of Use (https://www.cloudflare.com/website-terms/), and Cloudflare reserves the right to investigate your use of Tunnels for violations of such terms. If you intend to use Tunnels in production you should use a pre-created named tunnel by following: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps" // RunQuickTunnel requests a tunnel from the specified service. // We use this to power quick tunnels on trycloudflare.com, but the // service is open-source and could be used by anyone. func RunQuickTunnel(sc *subcommandContext) error { sc.log.Info().Msg(disclaimer) sc.log.Info().Msg("Requesting new quick Tunnel on trycloudflare.com...") client := http.Client{ Transport: &http.Transport{ TLSHandshakeTimeout: httpTimeout, ResponseHeaderTimeout: httpTimeout, }, Timeout: httpTimeout, } req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/tunnel", sc.c.String("quick-service")), nil) if err != nil { return errors.Wrap(err, "failed to build quick tunnel request") } req.Header.Add("Content-Type", "application/json") req.Header.Add("User-Agent", buildInfo.UserAgent()) resp, err := client.Do(req) if err != nil { return errors.Wrap(err, "failed to request quick Tunnel") } defer resp.Body.Close() // This will read the entire response into memory so we can print it in case of error rsp_body, err := io.ReadAll(resp.Body) if err != nil { return errors.Wrap(err, "failed to read quick-tunnel response") } var data QuickTunnelResponse if err := json.Unmarshal(rsp_body, &data); err != nil { rsp_string := string(rsp_body) fields := map[string]interface{}{"status_code": resp.Status} sc.log.Err(err).Fields(fields).Msgf("Error unmarshaling QuickTunnel response: %s", rsp_string) return errors.Wrap(err, "failed to unmarshal quick Tunnel") } tunnelID, err := uuid.Parse(data.Result.ID) if err != nil { return errors.Wrap(err, "failed to parse quick Tunnel ID") } credentials := connection.Credentials{ AccountTag: data.Result.AccountTag, TunnelSecret: data.Result.Secret, TunnelID: tunnelID, } url := data.Result.Hostname if !strings.HasPrefix(url, "https://") { url = "https://" + url } for _, line := range AsciiBox([]string{ "Your quick Tunnel has been created! Visit it at (it may take some time to be reachable):", url, }, 2) { sc.log.Info().Msg(line) } if !sc.c.IsSet(flags.Protocol) { _ = sc.c.Set(flags.Protocol, "quic") } // Override the number of connections used. Quick tunnels shouldn't be used for production usage, // so, use a single connection instead. _ = sc.c.Set(flags.HaConnections, "1") return StartServer( sc.c, buildInfo, &connection.TunnelProperties{Credentials: credentials, QuickTunnelUrl: data.Result.Hostname}, sc.log, ) } type QuickTunnelResponse struct { Success bool Result QuickTunnel Errors []QuickTunnelError } type QuickTunnelError struct { Code int Message string } type QuickTunnel struct { ID string `json:"id"` Name string `json:"name"` Hostname string `json:"hostname"` AccountTag string `json:"account_tag"` Secret []byte `json:"secret"` } // Print out the given lines in a nice ASCII box. func AsciiBox(lines []string, padding int) (box []string) { maxLen := maxLen(lines) spacer := strings.Repeat(" ", padding) border := "+" + strings.Repeat("-", maxLen+(padding*2)) + "+" box = append(box, border) for _, line := range lines { box = append(box, "|"+spacer+line+strings.Repeat(" ", maxLen-len(line))+spacer+"|") } box = append(box, border) return } func maxLen(lines []string) int { max := 0 for _, line := range lines { if len(line) > max { max = len(line) } } return max } ================================================ FILE: cmd/cloudflared/tunnel/signal.go ================================================ package tunnel import ( "os" "os/signal" "syscall" "github.com/rs/zerolog" ) // waitForSignal closes graceShutdownC to indicate that we should start graceful shutdown sequence func waitForSignal(graceShutdownC chan struct{}, logger *zerolog.Logger) { signals := make(chan os.Signal, 10) signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT) defer signal.Stop(signals) select { case s := <-signals: logger.Info().Msgf("Initiating graceful shutdown due to signal %s ...", s) close(graceShutdownC) case <-graceShutdownC: } } ================================================ FILE: cmd/cloudflared/tunnel/signal_test.go ================================================ //go:build !windows package tunnel import ( "fmt" "sync" "syscall" "testing" "time" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" ) const tick = 100 * time.Millisecond var ( serverErr = fmt.Errorf("server error") shutdownErr = fmt.Errorf("receive shutdown") graceShutdownErr = fmt.Errorf("receive grace shutdown") ) func channelClosed(c chan struct{}) bool { select { case <-c: return true default: return false } } func TestSignalShutdown(t *testing.T) { log := zerolog.Nop() // Test handling SIGTERM & SIGINT for _, sig := range []syscall.Signal{syscall.SIGTERM, syscall.SIGINT} { graceShutdownC := make(chan struct{}) go func(sig syscall.Signal) { // sleep for a tick to prevent sending signal before calling waitForSignal time.Sleep(tick) _ = syscall.Kill(syscall.Getpid(), sig) }(sig) time.AfterFunc(time.Second, func() { select { case <-graceShutdownC: default: close(graceShutdownC) t.Fatal("waitForSignal timed out") } }) waitForSignal(graceShutdownC, &log) assert.True(t, channelClosed(graceShutdownC)) } } func TestWaitForShutdown(t *testing.T) { log := zerolog.Nop() errC := make(chan error) graceShutdownC := make(chan struct{}) const gracePeriod = 5 * time.Second contextCancelled := false cancel := func() { contextCancelled = true } var wg sync.WaitGroup // on, error stop immediately contextCancelled = false startTime := time.Now() go func() { errC <- serverErr }() err := waitToShutdown(&wg, cancel, errC, graceShutdownC, gracePeriod, &log) assert.Equal(t, serverErr, err) assert.True(t, contextCancelled) assert.False(t, channelClosed(graceShutdownC)) assert.True(t, time.Now().Sub(startTime) < time.Second) // check that wait ended early // on graceful shutdown, ignore error but stop as soon as an error arrives contextCancelled = false startTime = time.Now() go func() { close(graceShutdownC) time.Sleep(tick) errC <- serverErr }() err = waitToShutdown(&wg, cancel, errC, graceShutdownC, gracePeriod, &log) assert.Nil(t, err) assert.True(t, contextCancelled) assert.True(t, time.Now().Sub(startTime) < time.Second) // check that wait ended early // with graceShutdownC closed stop right away without grace period contextCancelled = false startTime = time.Now() err = waitToShutdown(&wg, cancel, errC, graceShutdownC, 0, &log) assert.Nil(t, err) assert.True(t, contextCancelled) assert.True(t, time.Now().Sub(startTime) < time.Second) // check that wait ended early } ================================================ FILE: cmd/cloudflared/tunnel/subcommand_context.go ================================================ package tunnel import ( "encoding/base64" "encoding/json" "fmt" "os" "path/filepath" "strings" "github.com/google/uuid" "github.com/mitchellh/go-homedir" "github.com/pkg/errors" "github.com/rs/zerolog" "github.com/urfave/cli/v2" "github.com/cloudflare/cloudflared/cfapi" cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags" "github.com/cloudflare/cloudflared/connection" "github.com/cloudflare/cloudflared/credentials" "github.com/cloudflare/cloudflared/logger" ) const fedRampBaseApiURL = "https://api.fed.cloudflare.com/client/v4" type invalidJSONCredentialError struct { err error path string } func (e invalidJSONCredentialError) Error() string { return "Invalid JSON when parsing tunnel credentials file" } // subcommandContext carries structs shared between subcommands, to reduce number of arguments needed to // pass between subcommands, and make sure they are only initialized once type subcommandContext struct { c *cli.Context log *zerolog.Logger fs fileSystem // These fields should be accessed using their respective Getter tunnelstoreClient cfapi.Client userCredential *credentials.User } func newSubcommandContext(c *cli.Context) (*subcommandContext, error) { return &subcommandContext{ c: c, log: logger.CreateLoggerFromContext(c, logger.EnableTerminalLog), fs: realFileSystem{}, }, nil } // Returns something that can find the given tunnel's credentials file. func (sc *subcommandContext) credentialFinder(tunnelID uuid.UUID) CredFinder { if path := sc.c.String(CredFileFlag); path != "" { // Expand path if CredFileFlag contains `~` absPath, err := homedir.Expand(path) if err != nil { return newStaticPath(path, sc.fs) } return newStaticPath(absPath, sc.fs) } return newSearchByID(tunnelID, sc.c, sc.log, sc.fs) } func (sc *subcommandContext) client() (cfapi.Client, error) { if sc.tunnelstoreClient != nil { return sc.tunnelstoreClient, nil } cred, err := sc.credential() if err != nil { return nil, err } var apiURL string if cred.IsFEDEndpoint() { sc.log.Info().Str("api-url", fedRampBaseApiURL).Msg("using fedramp base api") apiURL = fedRampBaseApiURL } else { apiURL = sc.c.String(cfdflags.ApiURL) } sc.tunnelstoreClient, err = cred.Client(apiURL, buildInfo.UserAgent(), sc.log) if err != nil { return nil, err } return sc.tunnelstoreClient, nil } func (sc *subcommandContext) credential() (*credentials.User, error) { if sc.userCredential == nil { uc, err := credentials.Read(sc.c.String(cfdflags.OriginCert), sc.log) if err != nil { return nil, err } sc.userCredential = uc } return sc.userCredential, nil } func (sc *subcommandContext) readTunnelCredentials(credFinder CredFinder) (connection.Credentials, error) { filePath, err := credFinder.Path() if err != nil { return connection.Credentials{}, err } body, err := sc.fs.readFile(filePath) if err != nil { return connection.Credentials{}, errors.Wrapf(err, "couldn't read tunnel credentials from %v", filePath) } var credentials connection.Credentials if err = json.Unmarshal(body, &credentials); err != nil { if filepath.Ext(filePath) == ".pem" { return connection.Credentials{}, fmt.Errorf("The tunnel credentials file should be .json but you gave a .pem. " + "The tunnel credentials file was originally created by `cloudflared tunnel create`. " + "You may have accidentally used the filepath to cert.pem, which is generated by `cloudflared tunnel " + "login`.") } return connection.Credentials{}, invalidJSONCredentialError{path: filePath, err: err} } return credentials, nil } func (sc *subcommandContext) create(name string, credentialsFilePath string, secret string) (*cfapi.Tunnel, error) { client, err := sc.client() if err != nil { return nil, errors.Wrap(err, "couldn't create client to talk to Cloudflare Tunnel backend") } var tunnelSecret []byte if secret == "" { tunnelSecret, err = generateTunnelSecret() if err != nil { return nil, errors.Wrap(err, "couldn't generate the secret for your new tunnel") } } else { decodedSecret, err := base64.StdEncoding.DecodeString(secret) if err != nil { return nil, errors.Wrap(err, "Couldn't decode tunnel secret from base64") } tunnelSecret = decodedSecret if len(tunnelSecret) < 32 { return nil, errors.New("Decoded tunnel secret must be at least 32 bytes long") } } tunnel, err := client.CreateTunnel(name, tunnelSecret) if err != nil { return nil, errors.Wrap(err, "Create Tunnel API call failed") } credential, err := sc.credential() if err != nil { return nil, err } tunnelCredentials := connection.Credentials{ AccountTag: credential.AccountID(), TunnelSecret: tunnelSecret, TunnelID: tunnel.ID, Endpoint: credential.Endpoint(), } usedCertPath := false if credentialsFilePath == "" { originCertDir := filepath.Dir(credential.CertPath()) credentialsFilePath, err = tunnelFilePath(tunnelCredentials.TunnelID, originCertDir) if err != nil { return nil, err } usedCertPath = true } writeFileErr := writeTunnelCredentials(credentialsFilePath, &tunnelCredentials) if writeFileErr != nil { var errorLines []string errorLines = append(errorLines, fmt.Sprintf("Your tunnel '%v' was created with ID %v. However, cloudflared couldn't write tunnel credentials to %s.", tunnel.Name, tunnel.ID, credentialsFilePath)) errorLines = append(errorLines, fmt.Sprintf("The file-writing error is: %v", writeFileErr)) if deleteErr := client.DeleteTunnel(tunnel.ID, true); deleteErr != nil { errorLines = append(errorLines, fmt.Sprintf("Cloudflared tried to delete the tunnel for you, but encountered an error. You should use `cloudflared tunnel delete %v` to delete the tunnel yourself, because the tunnel can't be run without the tunnelfile.", tunnel.ID)) errorLines = append(errorLines, fmt.Sprintf("The delete tunnel error is: %v", deleteErr)) } else { errorLines = append(errorLines, "The tunnel was deleted, because the tunnel can't be run without the credentials file") } errorMsg := strings.Join(errorLines, "\n") return nil, errors.New(errorMsg) } if outputFormat := sc.c.String(outputFormatFlag.Name); outputFormat != "" { return nil, renderOutput(outputFormat, &tunnel) } fmt.Printf("Tunnel credentials written to %v.", credentialsFilePath) if usedCertPath { fmt.Print(" cloudflared chose this file based on where your origin certificate was found.") } fmt.Println(" Keep this file secret. To revoke these credentials, delete the tunnel.") fmt.Printf("\nCreated tunnel %s with id %s\n", tunnel.Name, tunnel.ID) return &tunnel.Tunnel, nil } func (sc *subcommandContext) list(filter *cfapi.TunnelFilter) ([]*cfapi.Tunnel, error) { client, err := sc.client() if err != nil { return nil, err } return client.ListTunnels(filter) } func (sc *subcommandContext) delete(tunnelIDs []uuid.UUID) error { forceFlagSet := sc.c.Bool(cfdflags.Force) client, err := sc.client() if err != nil { return err } for _, id := range tunnelIDs { tunnel, err := client.GetTunnel(id) if err != nil { return errors.Wrapf(err, "Can't get tunnel information. Please check tunnel id: %s", id) } // Check if tunnel DeletedAt field has already been set if !tunnel.DeletedAt.IsZero() { return fmt.Errorf("Tunnel %s has already been deleted", tunnel.ID) } if err := client.DeleteTunnel(tunnel.ID, forceFlagSet); err != nil { return errors.Wrapf(err, "Error deleting tunnel %s", tunnel.ID) } credFinder := sc.credentialFinder(id) if tunnelCredentialsPath, err := credFinder.Path(); err == nil { if err = os.Remove(tunnelCredentialsPath); err != nil { sc.log.Info().Msgf("Tunnel %v was deleted, but we could not remove its credentials file %s: %s. Consider deleting this file manually.", id, tunnelCredentialsPath, err) } } } return nil } // findCredentials will choose the right way to find the credentials file, find it, // and add the TunnelID into any old credentials (generated before TUN-3581 added the `TunnelID` // field to credentials files) func (sc *subcommandContext) findCredentials(tunnelID uuid.UUID) (connection.Credentials, error) { var credentials connection.Credentials var err error if credentialsContents := sc.c.String(CredContentsFlag); credentialsContents != "" { if err = json.Unmarshal([]byte(credentialsContents), &credentials); err != nil { err = invalidJSONCredentialError{path: "TUNNEL_CRED_CONTENTS", err: err} } } else { credFinder := sc.credentialFinder(tunnelID) credentials, err = sc.readTunnelCredentials(credFinder) } // This line ensures backwards compatibility with credentials files generated before // TUN-3581. Those old credentials files don't have a TunnelID field, so we enrich the struct // with the ID, which we have already resolved from the user input. credentials.TunnelID = tunnelID return credentials, err } func (sc *subcommandContext) run(tunnelID uuid.UUID) error { credentials, err := sc.findCredentials(tunnelID) if err != nil { if e, ok := err.(invalidJSONCredentialError); ok { sc.log.Error().Msgf("The credentials file at %s contained invalid JSON. This is probably caused by passing the wrong filepath. Reminder: the credentials file is a .json file created via `cloudflared tunnel create`.", e.path) sc.log.Error().Msgf("Invalid JSON when parsing credentials file: %s", e.err.Error()) } return err } return sc.runWithCredentials(credentials) } func (sc *subcommandContext) runWithCredentials(credentials connection.Credentials) error { sc.log.Info().Str(LogFieldTunnelID, credentials.TunnelID.String()).Msg("Starting tunnel") return StartServer( sc.c, buildInfo, &connection.TunnelProperties{Credentials: credentials}, sc.log, ) } func (sc *subcommandContext) cleanupConnections(tunnelIDs []uuid.UUID) error { params := cfapi.NewCleanupParams() extraLog := "" if connector := sc.c.String("connector-id"); connector != "" { connectorID, err := uuid.Parse(connector) if err != nil { return errors.Wrapf(err, "%s is not a valid client ID (must be a UUID)", connector) } params.ForClient(connectorID) extraLog = fmt.Sprintf(" for connector-id %s", connectorID.String()) } client, err := sc.client() if err != nil { return err } for _, tunnelID := range tunnelIDs { sc.log.Info().Msgf("Cleanup connection for tunnel %s%s", tunnelID, extraLog) if err := client.CleanupConnections(tunnelID, params); err != nil { sc.log.Error().Msgf("Error cleaning up connections for tunnel %v, error :%v", tunnelID, err) } } return nil } func (sc *subcommandContext) getTunnelTokenCredentials(tunnelID uuid.UUID) (*connection.TunnelToken, error) { client, err := sc.client() if err != nil { return nil, err } token, err := client.GetTunnelToken(tunnelID) if err != nil { sc.log.Err(err).Msgf("Could not get the Token for the given Tunnel %v", tunnelID) return nil, err } return ParseToken(token) } func (sc *subcommandContext) route(tunnelID uuid.UUID, r cfapi.HostnameRoute) (cfapi.HostnameRouteResult, error) { client, err := sc.client() if err != nil { return nil, err } return client.RouteTunnel(tunnelID, r) } // Query Tunnelstore to find the active tunnel with the given name. func (sc *subcommandContext) tunnelActive(name string) (*cfapi.Tunnel, bool, error) { filter := cfapi.NewTunnelFilter() filter.NoDeleted() filter.ByName(name) tunnels, err := sc.list(filter) if err != nil { return nil, false, err } if len(tunnels) == 0 { return nil, false, nil } // There should only be 1 active tunnel for a given name return tunnels[0], true, nil } // findID parses the input. If it's a UUID, return the UUID. // Otherwise, assume it's a name, and look up the ID of that tunnel. func (sc *subcommandContext) findID(input string) (uuid.UUID, error) { if u, err := uuid.Parse(input); err == nil { return u, nil } // Look up name in the credentials file. credFinder := newStaticPath(sc.c.String(CredFileFlag), sc.fs) if credentials, err := sc.readTunnelCredentials(credFinder); err == nil { if credentials.TunnelID != uuid.Nil { return credentials.TunnelID, nil } } // Fall back to querying Tunnelstore. if tunnel, found, err := sc.tunnelActive(input); err != nil { return uuid.Nil, err } else if found { return tunnel.ID, nil } return uuid.Nil, fmt.Errorf("%s is neither the ID nor the name of any of your tunnels", input) } // findIDs is just like mapping `findID` over a slice, but it only uses // one Tunnelstore API call per non-UUID input provided. func (sc *subcommandContext) findIDs(inputs []string) ([]uuid.UUID, error) { uuids, names := splitUuids(inputs) for _, name := range names { filter := cfapi.NewTunnelFilter() filter.NoDeleted() filter.ByName(name) tunnels, err := sc.list(filter) if err != nil { return nil, err } if len(tunnels) != 1 { return nil, fmt.Errorf("there should only be 1 non-deleted Tunnel named %s", name) } uuids = append(uuids, tunnels[0].ID) } return uuids, nil } func splitUuids(inputs []string) ([]uuid.UUID, []string) { uuids := make([]uuid.UUID, 0) names := make([]string, 0) for _, input := range inputs { id, err := uuid.Parse(input) if err != nil { names = append(names, input) } else { uuids = append(uuids, id) } } return uuids, names } ================================================ FILE: cmd/cloudflared/tunnel/subcommand_context_teamnet.go ================================================ package tunnel import ( "net" "github.com/google/uuid" "github.com/pkg/errors" "github.com/cloudflare/cloudflared/cfapi" ) const noClientMsg = "error while creating backend client" func (sc *subcommandContext) listRoutes(filter *cfapi.IpRouteFilter) ([]*cfapi.DetailedRoute, error) { client, err := sc.client() if err != nil { return nil, errors.Wrap(err, noClientMsg) } return client.ListRoutes(filter) } func (sc *subcommandContext) addRoute(newRoute cfapi.NewRoute) (cfapi.Route, error) { client, err := sc.client() if err != nil { return cfapi.Route{}, errors.Wrap(err, noClientMsg) } return client.AddRoute(newRoute) } func (sc *subcommandContext) deleteRoute(id uuid.UUID) error { client, err := sc.client() if err != nil { return errors.Wrap(err, noClientMsg) } return client.DeleteRoute(id) } func (sc *subcommandContext) getRouteByIP(params cfapi.GetRouteByIpParams) (cfapi.DetailedRoute, error) { client, err := sc.client() if err != nil { return cfapi.DetailedRoute{}, errors.Wrap(err, noClientMsg) } return client.GetByIP(params) } func (sc *subcommandContext) getRouteId(network net.IPNet, vnetId *uuid.UUID) (uuid.UUID, error) { filters := cfapi.NewIPRouteFilter() filters.NotDeleted() filters.NetworkIsSubsetOf(network) filters.NetworkIsSupersetOf(network) if vnetId != nil { filters.VNetID(*vnetId) } result, err := sc.listRoutes(filters) if err != nil { return uuid.Nil, err } if len(result) != 1 { return uuid.Nil, errors.New("unable to find route for provided network and vnet") } return result[0].ID, nil } ================================================ FILE: cmd/cloudflared/tunnel/subcommand_context_test.go ================================================ package tunnel import ( "encoding/base64" "flag" "fmt" "reflect" "testing" "time" "github.com/google/uuid" "github.com/pkg/errors" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" "github.com/urfave/cli/v2" "github.com/cloudflare/cloudflared/cfapi" "github.com/cloudflare/cloudflared/connection" "github.com/cloudflare/cloudflared/credentials" ) type mockFileSystem struct { rf func(string) ([]byte, error) vfp func(string) bool } func (fs mockFileSystem) validFilePath(path string) bool { return fs.vfp(path) } func (fs mockFileSystem) readFile(filePath string) ([]byte, error) { return fs.rf(filePath) } func Test_subcommandContext_findCredentials(t *testing.T) { type fields struct { c *cli.Context log *zerolog.Logger fs fileSystem tunnelstoreClient cfapi.Client userCredential *credentials.User } type args struct { tunnelID uuid.UUID } oldCertPath := "old_cert.json" newCertPath := "new_cert.json" accountTag := "0000d4d14e84bd4ae5a6a02e0000ac63" secret := []byte{211, 79, 177, 245, 179, 194, 152, 127, 140, 71, 18, 46, 183, 209, 10, 24, 192, 150, 55, 249, 211, 16, 167, 30, 113, 51, 152, 168, 72, 100, 205, 144} secretB64 := base64.StdEncoding.EncodeToString(secret) tunnelID := uuid.MustParse("df5ed608-b8b4-4109-89f3-9f2cf199df64") name := "mytunnel" fs := mockFileSystem{ rf: func(filePath string) ([]byte, error) { if filePath == oldCertPath { // An old credentials file created before TUN-3581 added the new fields return []byte(fmt.Sprintf(`{"AccountTag":"%s","TunnelSecret":"%s"}`, accountTag, secretB64)), nil } if filePath == newCertPath { // A new credentials file created after TUN-3581 with its new fields. return []byte(fmt.Sprintf(`{"AccountTag":"%s","TunnelSecret":"%s","TunnelID":"%s","TunnelName":"%s"}`, accountTag, secretB64, tunnelID, name)), nil } return nil, errors.New("file not found") }, vfp: func(string) bool { return true }, } log := zerolog.Nop() tests := []struct { name string fields fields args args want connection.Credentials wantErr bool }{ { name: "Filepath given leads to old credentials file", fields: fields{ log: &log, fs: fs, c: func() *cli.Context { flagSet := flag.NewFlagSet("test0", flag.PanicOnError) flagSet.String(CredFileFlag, oldCertPath, "") c := cli.NewContext(cli.NewApp(), flagSet, nil) _ = c.Set(CredFileFlag, oldCertPath) return c }(), }, args: args{ tunnelID: tunnelID, }, want: connection.Credentials{ AccountTag: accountTag, TunnelID: tunnelID, TunnelSecret: secret, }, }, { name: "Filepath given leads to new credentials file", fields: fields{ log: &log, fs: fs, c: func() *cli.Context { flagSet := flag.NewFlagSet("test0", flag.PanicOnError) flagSet.String(CredFileFlag, newCertPath, "") c := cli.NewContext(cli.NewApp(), flagSet, nil) _ = c.Set(CredFileFlag, newCertPath) return c }(), }, args: args{ tunnelID: tunnelID, }, want: connection.Credentials{ AccountTag: accountTag, TunnelID: tunnelID, TunnelSecret: secret, }, }, { name: "TUNNEL_CRED_CONTENTS given contains old credentials contents", fields: fields{ log: &log, fs: fs, c: func() *cli.Context { flagSet := flag.NewFlagSet("test0", flag.PanicOnError) flagSet.String(CredContentsFlag, "", "") c := cli.NewContext(cli.NewApp(), flagSet, nil) _ = c.Set(CredContentsFlag, fmt.Sprintf(`{"AccountTag":"%s","TunnelSecret":"%s"}`, accountTag, secretB64)) return c }(), }, args: args{ tunnelID: tunnelID, }, want: connection.Credentials{ AccountTag: accountTag, TunnelID: tunnelID, TunnelSecret: secret, }, }, { name: "TUNNEL_CRED_CONTENTS given contains new credentials contents", fields: fields{ log: &log, fs: fs, c: func() *cli.Context { flagSet := flag.NewFlagSet("test0", flag.PanicOnError) flagSet.String(CredContentsFlag, "", "") c := cli.NewContext(cli.NewApp(), flagSet, nil) _ = c.Set(CredContentsFlag, fmt.Sprintf(`{"AccountTag":"%s","TunnelSecret":"%s","TunnelID":"%s","TunnelName":"%s"}`, accountTag, secretB64, tunnelID, name)) return c }(), }, args: args{ tunnelID: tunnelID, }, want: connection.Credentials{ AccountTag: accountTag, TunnelID: tunnelID, TunnelSecret: secret, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { sc := &subcommandContext{ c: tt.fields.c, log: tt.fields.log, fs: tt.fields.fs, tunnelstoreClient: tt.fields.tunnelstoreClient, userCredential: tt.fields.userCredential, } got, err := sc.findCredentials(tt.args.tunnelID) if (err != nil) != tt.wantErr { t.Errorf("subcommandContext.findCredentials() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { t.Errorf("subcommandContext.findCredentials() = %v, want %v", got, tt.want) } }) } } type deleteMockTunnelStore struct { cfapi.Client mockTunnels map[uuid.UUID]mockTunnelBehaviour deletedTunnelIDs []uuid.UUID } type mockTunnelBehaviour struct { tunnel cfapi.Tunnel deleteErr error cleanupErr error } func newDeleteMockTunnelStore(tunnels ...mockTunnelBehaviour) *deleteMockTunnelStore { mockTunnels := make(map[uuid.UUID]mockTunnelBehaviour) for _, tunnel := range tunnels { mockTunnels[tunnel.tunnel.ID] = tunnel } return &deleteMockTunnelStore{ mockTunnels: mockTunnels, deletedTunnelIDs: make([]uuid.UUID, 0), } } func (d *deleteMockTunnelStore) GetTunnel(tunnelID uuid.UUID) (*cfapi.Tunnel, error) { tunnel, ok := d.mockTunnels[tunnelID] if !ok { return nil, fmt.Errorf("Couldn't find tunnel: %v", tunnelID) } return &tunnel.tunnel, nil } func (d *deleteMockTunnelStore) GetTunnelToken(tunnelID uuid.UUID) (string, error) { return "token", nil } func (d *deleteMockTunnelStore) DeleteTunnel(tunnelID uuid.UUID, cascade bool) error { tunnel, ok := d.mockTunnels[tunnelID] if !ok { return fmt.Errorf("Couldn't find tunnel: %v", tunnelID) } if tunnel.deleteErr != nil { return tunnel.deleteErr } d.deletedTunnelIDs = append(d.deletedTunnelIDs, tunnelID) tunnel.tunnel.DeletedAt = time.Now() delete(d.mockTunnels, tunnelID) return nil } func (d *deleteMockTunnelStore) CleanupConnections(tunnelID uuid.UUID, _ *cfapi.CleanupParams) error { tunnel, ok := d.mockTunnels[tunnelID] if !ok { return fmt.Errorf("Couldn't find tunnel: %v", tunnelID) } return tunnel.cleanupErr } func Test_subcommandContext_Delete(t *testing.T) { type fields struct { c *cli.Context log *zerolog.Logger isUIEnabled bool fs fileSystem tunnelstoreClient *deleteMockTunnelStore userCredential *credentials.User } type args struct { tunnelIDs []uuid.UUID } newCertPath := "new_cert.json" tunnelID1 := uuid.MustParse("df5ed608-b8b4-4109-89f3-9f2cf199df64") tunnelID2 := uuid.MustParse("af5ed608-b8b4-4109-89f3-9f2cf199df64") log := zerolog.Nop() var tests = []struct { name string fields fields args args want []uuid.UUID wantErr bool }{ { name: "clean up continues if credentials are not found", fields: fields{ log: &log, fs: mockFileSystem{ rf: func(filePath string) ([]byte, error) { return nil, errors.New("file not found") }, vfp: func(string) bool { return true }, }, c: func() *cli.Context { flagSet := flag.NewFlagSet("test0", flag.PanicOnError) flagSet.String(CredFileFlag, newCertPath, "") c := cli.NewContext(cli.NewApp(), flagSet, nil) _ = c.Set(CredFileFlag, newCertPath) return c }(), tunnelstoreClient: newDeleteMockTunnelStore( mockTunnelBehaviour{ tunnel: cfapi.Tunnel{ID: tunnelID1}, }, mockTunnelBehaviour{ tunnel: cfapi.Tunnel{ID: tunnelID2}, }, ), }, args: args{ tunnelIDs: []uuid.UUID{tunnelID1, tunnelID2}, }, want: []uuid.UUID{tunnelID1, tunnelID2}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { sc := &subcommandContext{ c: tt.fields.c, log: tt.fields.log, fs: tt.fields.fs, tunnelstoreClient: tt.fields.tunnelstoreClient, userCredential: tt.fields.userCredential, } err := sc.delete(tt.args.tunnelIDs) if (err != nil) != tt.wantErr { t.Errorf("subcommandContext.findCredentials() error = %v, wantErr %v", err, tt.wantErr) return } got := tt.fields.tunnelstoreClient.deletedTunnelIDs if !reflect.DeepEqual(got, tt.want) { t.Errorf("subcommandContext.findCredentials() = %v, want %v", got, tt.want) return } }) } } func Test_subcommandContext_ValidateIngressCommand(t *testing.T) { var tests = []struct { name string c *cli.Context wantErr bool expectedErr error }{ { name: "read a valid configuration from data", c: func() *cli.Context { data := `{ "warp-routing": {"enabled": true}, "originRequest" : {"connectTimeout": 10}, "ingress" : [ {"hostname": "test", "service": "https://localhost:8000" } , {"service": "http_status:404"} ]}` flagSet := flag.NewFlagSet("json", flag.PanicOnError) flagSet.String(ingressDataJSONFlagName, data, "") c := cli.NewContext(cli.NewApp(), flagSet, nil) _ = c.Set(ingressDataJSONFlagName, data) return c }(), }, { name: "read an invalid configuration with multiple mistakes", c: func() *cli.Context { data := `{ "ingress" : [ {"hostname": "test", "service": "localhost:8000" } , {"service": "http_status:invalid_status"} ]}` flagSet := flag.NewFlagSet("json", flag.PanicOnError) flagSet.String(ingressDataJSONFlagName, data, "") c := cli.NewContext(cli.NewApp(), flagSet, nil) _ = c.Set(ingressDataJSONFlagName, data) return c }(), wantErr: true, expectedErr: errors.New("Validation failed: localhost:8000 is an invalid address, please make sure it has a scheme and a hostname"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := validateIngressCommand(tt.c, "") if tt.wantErr { assert.Equal(t, tt.expectedErr.Error(), err.Error()) } else { assert.Nil(t, err) } }) } } ================================================ FILE: cmd/cloudflared/tunnel/subcommand_context_vnets.go ================================================ package tunnel import ( "github.com/google/uuid" "github.com/pkg/errors" "github.com/cloudflare/cloudflared/cfapi" ) func (sc *subcommandContext) addVirtualNetwork(newVnet cfapi.NewVirtualNetwork) (cfapi.VirtualNetwork, error) { client, err := sc.client() if err != nil { return cfapi.VirtualNetwork{}, errors.Wrap(err, noClientMsg) } return client.CreateVirtualNetwork(newVnet) } func (sc *subcommandContext) listVirtualNetworks(filter *cfapi.VnetFilter) ([]*cfapi.VirtualNetwork, error) { client, err := sc.client() if err != nil { return nil, errors.Wrap(err, noClientMsg) } return client.ListVirtualNetworks(filter) } func (sc *subcommandContext) deleteVirtualNetwork(vnetId uuid.UUID, force bool) error { client, err := sc.client() if err != nil { return errors.Wrap(err, noClientMsg) } return client.DeleteVirtualNetwork(vnetId, force) } func (sc *subcommandContext) updateVirtualNetwork(vnetId uuid.UUID, updates cfapi.UpdateVirtualNetwork) error { client, err := sc.client() if err != nil { return errors.Wrap(err, noClientMsg) } return client.UpdateVirtualNetwork(vnetId, updates) } ================================================ FILE: cmd/cloudflared/tunnel/subcommands.go ================================================ package tunnel import ( "crypto/rand" "encoding/base64" "encoding/json" "fmt" "io" "net/http" "os" "path/filepath" "regexp" "sort" "strings" "text/tabwriter" "time" "github.com/google/uuid" "github.com/mitchellh/go-homedir" "github.com/pkg/errors" "github.com/urfave/cli/v2" "github.com/urfave/cli/v2/altsrc" "golang.org/x/net/idna" "gopkg.in/yaml.v3" "github.com/cloudflare/cloudflared/cfapi" "github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil" "github.com/cloudflare/cloudflared/cmd/cloudflared/flags" "github.com/cloudflare/cloudflared/cmd/cloudflared/updater" "github.com/cloudflare/cloudflared/config" "github.com/cloudflare/cloudflared/connection" "github.com/cloudflare/cloudflared/diagnostic" "github.com/cloudflare/cloudflared/fips" "github.com/cloudflare/cloudflared/metrics" ) const ( allSortByOptions = "name, id, createdAt, deletedAt, numConnections" connsSortByOptions = "id, startedAt, numConnections, version" CredFileFlagAlias = "cred-file" CredFileFlag = "credentials-file" CredContentsFlag = "credentials-contents" TunnelTokenFlag = "token" TunnelTokenFileFlag = "token-file" overwriteDNSFlagName = "overwrite-dns" noDiagLogsFlagName = "no-diag-logs" noDiagMetricsFlagName = "no-diag-metrics" noDiagSystemFlagName = "no-diag-system" noDiagRuntimeFlagName = "no-diag-runtime" noDiagNetworkFlagName = "no-diag-network" diagContainerIDFlagName = "diag-container-id" diagPodFlagName = "diag-pod-id" LogFieldTunnelID = "tunnelID" ) var ( showDeletedFlag = &cli.BoolFlag{ Name: "show-deleted", Aliases: []string{"d"}, Usage: "Include deleted tunnels in the list", } listNameFlag = &cli.StringFlag{ Name: flags.Name, Aliases: []string{"n"}, Usage: "List tunnels with the given `NAME`", } listNamePrefixFlag = &cli.StringFlag{ Name: "name-prefix", Aliases: []string{"np"}, Usage: "List tunnels that start with the give `NAME` prefix", } listExcludeNamePrefixFlag = &cli.StringFlag{ Name: "exclude-name-prefix", Aliases: []string{"enp"}, Usage: "List tunnels whose `NAME` does not start with the given prefix", } listExistedAtFlag = &cli.TimestampFlag{ Name: "when", Aliases: []string{"w"}, Usage: "List tunnels that are active at the given `TIME` in RFC3339 format", Layout: cfapi.TimeLayout, DefaultText: fmt.Sprintf("current time, %s", time.Now().Format(cfapi.TimeLayout)), } listIDFlag = &cli.StringFlag{ Name: "id", Aliases: []string{"i"}, Usage: "List tunnel by `ID`", } showRecentlyDisconnected = &cli.BoolFlag{ Name: "show-recently-disconnected", Aliases: []string{"rd"}, Usage: "Include connections that have recently disconnected in the list", } outputFormatFlag = &cli.StringFlag{ Name: "output", Aliases: []string{"o"}, Usage: "Render output using given `FORMAT`. Valid options are 'json' or 'yaml'", } sortByFlag = &cli.StringFlag{ Name: "sort-by", Value: "name", Usage: fmt.Sprintf("Sorts the list of tunnels by the given field. Valid options are {%s}", allSortByOptions), EnvVars: []string{"TUNNEL_LIST_SORT_BY"}, } invertSortFlag = &cli.BoolFlag{ Name: "invert-sort", Usage: "Inverts the sort order of the tunnel list.", EnvVars: []string{"TUNNEL_LIST_INVERT_SORT"}, } featuresFlag = altsrc.NewStringSliceFlag(&cli.StringSliceFlag{ Name: flags.Features, Aliases: []string{"F"}, Usage: "Opt into various features that are still being developed or tested.", }) credentialsFileFlagCLIOnly = &cli.StringFlag{ Name: CredFileFlag, Aliases: []string{CredFileFlagAlias}, Usage: "Filepath at which to read/write the tunnel credentials", EnvVars: []string{"TUNNEL_CRED_FILE"}, } credentialsFileFlag = altsrc.NewStringFlag(credentialsFileFlagCLIOnly) credentialsContentsFlag = altsrc.NewStringFlag(&cli.StringFlag{ Name: CredContentsFlag, Usage: "Contents of the tunnel credentials JSON file to use. When provided along with credentials-file, this will take precedence.", EnvVars: []string{"TUNNEL_CRED_CONTENTS"}, }) tunnelTokenFlag = altsrc.NewStringFlag(&cli.StringFlag{ Name: TunnelTokenFlag, Usage: "The Tunnel token. When provided along with credentials, this will take precedence. Also takes precedence over token-file", EnvVars: []string{"TUNNEL_TOKEN"}, }) tunnelTokenFileFlag = altsrc.NewStringFlag(&cli.StringFlag{ Name: TunnelTokenFileFlag, Usage: "Filepath at which to read the tunnel token. When provided along with credentials, this will take precedence.", EnvVars: []string{"TUNNEL_TOKEN_FILE"}, }) forceDeleteFlag = &cli.BoolFlag{ Name: flags.Force, Aliases: []string{"f"}, Usage: "Deletes a tunnel even if tunnel is connected and it has dependencies associated to it. (eg. IP routes)." + " It is not possible to delete tunnels that have connections or non-deleted dependencies, without this flag.", EnvVars: []string{"TUNNEL_RUN_FORCE_OVERWRITE"}, } selectProtocolFlag = altsrc.NewStringFlag(&cli.StringFlag{ Name: flags.Protocol, Value: connection.AutoSelectFlag, Aliases: []string{"p"}, Usage: fmt.Sprintf("Protocol implementation to connect with Cloudflare's edge network. %s", connection.AvailableProtocolFlagMessage), EnvVars: []string{"TUNNEL_TRANSPORT_PROTOCOL"}, Hidden: true, }) postQuantumFlag = altsrc.NewBoolFlag(&cli.BoolFlag{ Name: flags.PostQuantum, Usage: "When given creates an experimental post-quantum secure tunnel", Aliases: []string{"pq"}, EnvVars: []string{"TUNNEL_POST_QUANTUM"}, Hidden: fips.IsFipsEnabled(), }) sortInfoByFlag = &cli.StringFlag{ Name: "sort-by", Value: "createdAt", Usage: fmt.Sprintf("Sorts the list of connections of a tunnel by the given field. Valid options are {%s}", connsSortByOptions), EnvVars: []string{"TUNNEL_INFO_SORT_BY"}, } invertInfoSortFlag = &cli.BoolFlag{ Name: "invert-sort", Usage: "Inverts the sort order of the tunnel info.", EnvVars: []string{"TUNNEL_INFO_INVERT_SORT"}, } cleanupClientFlag = &cli.StringFlag{ Name: "connector-id", Aliases: []string{"c"}, Usage: `Constraints the cleanup to stop the connections of a single Connector (by its ID). You can find the various Connectors (and their IDs) currently connected to your tunnel via 'cloudflared tunnel info '.`, EnvVars: []string{"TUNNEL_CLEANUP_CONNECTOR"}, } overwriteDNSFlag = &cli.BoolFlag{ Name: overwriteDNSFlagName, Aliases: []string{"f"}, Usage: `Overwrites existing DNS records with this hostname`, EnvVars: []string{"TUNNEL_FORCE_PROVISIONING_DNS"}, } createSecretFlag = &cli.StringFlag{ Name: "secret", Aliases: []string{"s"}, Usage: "Base64 encoded secret to set for the tunnel. The decoded secret must be at least 32 bytes long. If not specified, a random 32-byte secret will be generated.", EnvVars: []string{"TUNNEL_CREATE_SECRET"}, } icmpv4SrcFlag = &cli.StringFlag{ Name: flags.ICMPV4Src, Usage: "Source address to send/receive ICMPv4 messages. If not provided cloudflared will dial a local address to determine the source IP or fallback to 0.0.0.0.", EnvVars: []string{"TUNNEL_ICMPV4_SRC"}, } icmpv6SrcFlag = &cli.StringFlag{ Name: flags.ICMPV6Src, Usage: "Source address and the interface name to send/receive ICMPv6 messages. If not provided cloudflared will dial a local address to determine the source IP or fallback to ::.", EnvVars: []string{"TUNNEL_ICMPV6_SRC"}, } metricsFlag = &cli.StringFlag{ Name: flags.Metrics, Usage: "The metrics server address i.e.: 127.0.0.1:12345. If your instance is running in a Docker/Kubernetes environment you need to setup port forwarding for your application.", Value: "", } diagContainerFlag = &cli.StringFlag{ Name: diagContainerIDFlagName, Usage: "Container ID or Name to collect logs from", Value: "", } diagPodFlag = &cli.StringFlag{ Name: diagPodFlagName, Usage: "Kubernetes POD to collect logs from", Value: "", } noDiagLogsFlag = &cli.BoolFlag{ Name: noDiagLogsFlagName, Usage: "Log collection will not be performed", Value: false, } noDiagMetricsFlag = &cli.BoolFlag{ Name: noDiagMetricsFlagName, Usage: "Metric collection will not be performed", Value: false, } noDiagSystemFlag = &cli.BoolFlag{ Name: noDiagSystemFlagName, Usage: "System information collection will not be performed", Value: false, } noDiagRuntimeFlag = &cli.BoolFlag{ Name: noDiagRuntimeFlagName, Usage: "Runtime information collection will not be performed", Value: false, } noDiagNetworkFlag = &cli.BoolFlag{ Name: noDiagNetworkFlagName, Usage: "Network diagnostics won't be performed", Value: false, } maxActiveFlowsFlag = &cli.Uint64Flag{ Name: flags.MaxActiveFlows, Usage: "Overrides the remote configuration for max active private network flows (TCP/UDP) that this cloudflared instance supports", EnvVars: []string{"TUNNEL_MAX_ACTIVE_FLOWS"}, } dnsResolverAddrsFlag = &cli.StringSliceFlag{ Name: flags.VirtualDNSServiceResolverAddresses, Usage: "Overrides the dynamic DNS resolver resolution to use these address:port's instead.", EnvVars: []string{"TUNNEL_DNS_RESOLVER_ADDRS"}, } ) func buildCreateCommand() *cli.Command { return &cli.Command{ Name: "create", Action: cliutil.ConfiguredAction(createCommand), Usage: "Create a new tunnel with given name", UsageText: "cloudflared tunnel [tunnel command options] create [subcommand options] NAME", Description: `Creates a tunnel, registers it with Cloudflare edge and generates credential file used to run this tunnel. Use "cloudflared tunnel route" subcommand to map a DNS name to this tunnel and "cloudflared tunnel run" to start the connection. For example, to create a tunnel named 'my-tunnel' run: $ cloudflared tunnel create my-tunnel`, Flags: []cli.Flag{outputFormatFlag, credentialsFileFlagCLIOnly, createSecretFlag}, CustomHelpTemplate: commandHelpTemplate(), } } // generateTunnelSecret as an array of 32 bytes using secure random number generator func generateTunnelSecret() ([]byte, error) { randomBytes := make([]byte, 32) _, err := rand.Read(randomBytes) return randomBytes, err } func createCommand(c *cli.Context) error { sc, err := newSubcommandContext(c) if err != nil { return errors.Wrap(err, "error setting up logger") } if c.NArg() != 1 { return cliutil.UsageError(`"cloudflared tunnel create" requires exactly 1 argument, the name of tunnel to create.`) } name := c.Args().First() warningChecker := updater.StartWarningCheck(c) defer warningChecker.LogWarningIfAny(sc.log) _, err = sc.create(name, c.String(CredFileFlag), c.String(createSecretFlag.Name)) return errors.Wrap(err, "failed to create tunnel") } func tunnelFilePath(tunnelID uuid.UUID, directory string) (string, error) { fileName := fmt.Sprintf("%v.json", tunnelID) filePath := filepath.Clean(fmt.Sprintf("%s/%s", directory, fileName)) return homedir.Expand(filePath) } // writeTunnelCredentials saves `credentials` as a JSON into `filePath`, only if // the file does not exist already func writeTunnelCredentials(filePath string, credentials *connection.Credentials) error { if _, err := os.Stat(filePath); !os.IsNotExist(err) { if err == nil { return fmt.Errorf("%s already exists", filePath) } return err } body, err := json.Marshal(credentials) if err != nil { return errors.Wrap(err, "Unable to marshal tunnel credentials to JSON") } return os.WriteFile(filePath, body, 0400) } func buildListCommand() *cli.Command { return &cli.Command{ Name: "list", Action: cliutil.ConfiguredAction(listCommand), Usage: "List existing tunnels", UsageText: "cloudflared tunnel [tunnel command options] list [subcommand options]", Description: "cloudflared tunnel list will display all active tunnels, their created time and associated connections. Use -d flag to include deleted tunnels. See the list of options to filter the list", Flags: []cli.Flag{ outputFormatFlag, showDeletedFlag, listNameFlag, listNamePrefixFlag, listExcludeNamePrefixFlag, listExistedAtFlag, listIDFlag, showRecentlyDisconnected, sortByFlag, invertSortFlag, }, CustomHelpTemplate: commandHelpTemplate(), } } func listCommand(c *cli.Context) error { sc, err := newSubcommandContext(c) if err != nil { return err } warningChecker := updater.StartWarningCheck(c) defer warningChecker.LogWarningIfAny(sc.log) filter := cfapi.NewTunnelFilter() if !c.Bool("show-deleted") { filter.NoDeleted() } if name := c.String(flags.Name); name != "" { filter.ByName(name) } if namePrefix := c.String("name-prefix"); namePrefix != "" { filter.ByNamePrefix(namePrefix) } if excludePrefix := c.String("exclude-name-prefix"); excludePrefix != "" { filter.ExcludeNameWithPrefix(excludePrefix) } if existedAt := c.Timestamp("time"); existedAt != nil { filter.ByExistedAt(*existedAt) } if id := c.String("id"); id != "" { tunnelID, err := uuid.Parse(id) if err != nil { return errors.Wrapf(err, "%s is not a valid tunnel ID", id) } filter.ByTunnelID(tunnelID) } if maxFetch := c.Int("max-fetch-size"); maxFetch > 0 { filter.MaxFetchSize(uint(maxFetch)) } tunnels, err := sc.list(filter) if err != nil { return err } // Sort the tunnels sortBy := c.String("sort-by") invalidSortField := false sort.Slice(tunnels, func(i, j int) bool { cmp := func() bool { switch sortBy { case "name": return tunnels[i].Name < tunnels[j].Name case "id": return tunnels[i].ID.String() < tunnels[j].ID.String() case "createdAt": return tunnels[i].CreatedAt.Unix() < tunnels[j].CreatedAt.Unix() case "deletedAt": return tunnels[i].DeletedAt.Unix() < tunnels[j].DeletedAt.Unix() case "numConnections": return len(tunnels[i].Connections) < len(tunnels[j].Connections) default: invalidSortField = true return tunnels[i].Name < tunnels[j].Name } }() if c.Bool("invert-sort") { return !cmp } return cmp }) if invalidSortField { sc.log.Error().Msgf("%s is not a valid sort field. Valid sort fields are %s. Defaulting to 'name'.", sortBy, allSortByOptions) } if outputFormat := c.String(outputFormatFlag.Name); outputFormat != "" { return renderOutput(outputFormat, tunnels) } if len(tunnels) > 0 { formatAndPrintTunnelList(tunnels, c.Bool("show-recently-disconnected")) } else { fmt.Println("No tunnels were found for the given filter flags. You can use 'cloudflared tunnel create' to create a tunnel.") } return nil } func formatAndPrintTunnelList(tunnels []*cfapi.Tunnel, showRecentlyDisconnected bool) { writer := tabWriter() defer writer.Flush() _, _ = fmt.Fprintln(writer, "You can obtain more detailed information for each tunnel with `cloudflared tunnel info `") // Print column headers with tabbed columns _, _ = fmt.Fprintln(writer, "ID\tNAME\tCREATED\tCONNECTIONS\t") // Loop through tunnels, create formatted string for each, and print using tabwriter for _, t := range tunnels { formattedStr := fmt.Sprintf( "%s\t%s\t%s\t%s\t", t.ID, t.Name, t.CreatedAt.Format(time.RFC3339), fmtConnections(t.Connections, showRecentlyDisconnected), ) _, _ = fmt.Fprintln(writer, formattedStr) } } func fmtConnections(connections []cfapi.Connection, showRecentlyDisconnected bool) string { // Count connections per colo numConnsPerColo := make(map[string]uint, len(connections)) for _, connection := range connections { if !connection.IsPendingReconnect || showRecentlyDisconnected { numConnsPerColo[connection.ColoName]++ } } // Get sorted list of colos sortedColos := []string{} for coloName := range numConnsPerColo { sortedColos = append(sortedColos, coloName) } sort.Strings(sortedColos) // Map each colo to its frequency, combine into output string. output := make([]string, 0, len(sortedColos)) for _, coloName := range sortedColos { output = append(output, fmt.Sprintf("%dx%s", numConnsPerColo[coloName], coloName)) } return strings.Join(output, ", ") } func buildReadyCommand() *cli.Command { return &cli.Command{ Name: "ready", Action: cliutil.ConfiguredAction(readyCommand), Usage: "Call /ready endpoint and return proper exit code", UsageText: "cloudflared tunnel [tunnel command options] ready [subcommand options]", Description: "cloudflared tunnel ready will return proper exit code based on the /ready endpoint", Flags: []cli.Flag{}, CustomHelpTemplate: commandHelpTemplate(), } } func readyCommand(c *cli.Context) error { metricsOpts := c.String(flags.Metrics) if !c.IsSet(flags.Metrics) { return errors.New("--metrics has to be provided") } requestURL := fmt.Sprintf("http://%s/ready", metricsOpts) req, err := http.NewRequest(http.MethodGet, requestURL, nil) if err != nil { return err } res, err := http.DefaultClient.Do(req) if err != nil { return err } defer res.Body.Close() if res.StatusCode != 200 { body, err := io.ReadAll(res.Body) if err != nil { return err } return fmt.Errorf("http://%s/ready endpoint returned status code %d\n%s", metricsOpts, res.StatusCode, body) } return nil } func buildInfoCommand() *cli.Command { return &cli.Command{ Name: "info", Action: cliutil.ConfiguredAction(tunnelInfo), Usage: "List details about the active connectors for a tunnel", UsageText: "cloudflared tunnel [tunnel command options] info [subcommand options] [TUNNEL]", Description: "cloudflared tunnel info displays details about the active connectors for a given tunnel (identified by name or uuid).", Flags: []cli.Flag{ outputFormatFlag, showRecentlyDisconnected, sortInfoByFlag, invertInfoSortFlag, }, CustomHelpTemplate: commandHelpTemplate(), } } func tunnelInfo(c *cli.Context) error { sc, err := newSubcommandContext(c) if err != nil { return err } warningChecker := updater.StartWarningCheck(c) defer warningChecker.LogWarningIfAny(sc.log) if c.NArg() != 1 { return cliutil.UsageError(`"cloudflared tunnel info" accepts exactly one argument, the ID or name of the tunnel to get info about.`) } tunnelID, err := sc.findID(c.Args().First()) if err != nil { return errors.Wrap(err, "error parsing tunnel ID") } client, err := sc.client() if err != nil { return err } clients, err := client.ListActiveClients(tunnelID) if err != nil { return err } sortBy := c.String("sort-by") invalidSortField := false sort.Slice(clients, func(i, j int) bool { cmp := func() bool { switch sortBy { case "id": return clients[i].ID.String() < clients[j].ID.String() case "createdAt": return clients[i].RunAt.Unix() < clients[j].RunAt.Unix() case "numConnections": return len(clients[i].Connections) < len(clients[j].Connections) case "version": return clients[i].Version < clients[j].Version default: invalidSortField = true return clients[i].RunAt.Unix() < clients[j].RunAt.Unix() } }() if c.Bool("invert-sort") { return !cmp } return cmp }) if invalidSortField { sc.log.Error().Msgf("%s is not a valid sort field. Valid sort fields are %s. Defaulting to 'name'.", sortBy, connsSortByOptions) } tunnel, err := getTunnel(sc, tunnelID) if err != nil { return err } info := Info{ tunnel.ID, tunnel.Name, tunnel.CreatedAt, clients, } if outputFormat := c.String(outputFormatFlag.Name); outputFormat != "" { return renderOutput(outputFormat, info) } if len(clients) > 0 { formatAndPrintConnectionsList(info, c.Bool("show-recently-disconnected")) } else { fmt.Printf("Your tunnel %s does not have any active connection.\n", tunnelID) } return nil } func getTunnel(sc *subcommandContext, tunnelID uuid.UUID) (*cfapi.Tunnel, error) { filter := cfapi.NewTunnelFilter() filter.ByTunnelID(tunnelID) tunnels, err := sc.list(filter) if err != nil { return nil, err } if len(tunnels) != 1 { return nil, errors.Errorf("Expected to find a single tunnel with uuid %v but found %d tunnels.", tunnelID, len(tunnels)) } return tunnels[0], nil } func formatAndPrintConnectionsList(tunnelInfo Info, showRecentlyDisconnected bool) { writer := tabWriter() defer writer.Flush() // Print the general tunnel info table _, _ = fmt.Fprintf(writer, "NAME: %s\nID: %s\nCREATED: %s\n\n", tunnelInfo.Name, tunnelInfo.ID, tunnelInfo.CreatedAt) // Determine whether to print the connector table shouldDisplayTable := false for _, c := range tunnelInfo.Connectors { conns := fmtConnections(c.Connections, showRecentlyDisconnected) if len(conns) > 0 { shouldDisplayTable = true } } if !shouldDisplayTable { fmt.Println("This tunnel has no active connectors.") return } // Print the connector table _, _ = fmt.Fprintln(writer, "CONNECTOR ID\tCREATED\tARCHITECTURE\tVERSION\tORIGIN IP\tEDGE\t") for _, c := range tunnelInfo.Connectors { conns := fmtConnections(c.Connections, showRecentlyDisconnected) if len(conns) == 0 { continue } originIp := c.Connections[0].OriginIP.String() formattedStr := fmt.Sprintf( "%s\t%s\t%s\t%s\t%s\t%s\t", c.ID, c.RunAt.Format(time.RFC3339), c.Arch, c.Version, originIp, conns, ) _, _ = fmt.Fprintln(writer, formattedStr) } } func tabWriter() *tabwriter.Writer { const ( minWidth = 0 tabWidth = 8 padding = 1 padChar = ' ' flags = 0 ) writer := tabwriter.NewWriter(os.Stdout, minWidth, tabWidth, padding, padChar, flags) return writer } func buildDeleteCommand() *cli.Command { return &cli.Command{ Name: "delete", Action: cliutil.ConfiguredAction(deleteCommand), Usage: "Delete existing tunnel by UUID or name", UsageText: "cloudflared tunnel [tunnel command options] delete [subcommand options] TUNNEL", Description: "cloudflared tunnel delete will delete tunnels with the given tunnel UUIDs or names. A tunnel cannot be deleted if it has active connections. To delete the tunnel unconditionally, use -f flag.", Flags: []cli.Flag{credentialsFileFlagCLIOnly, forceDeleteFlag}, CustomHelpTemplate: commandHelpTemplate(), } } func deleteCommand(c *cli.Context) error { sc, err := newSubcommandContext(c) if err != nil { return err } if c.NArg() < 1 { return cliutil.UsageError(`"cloudflared tunnel delete" requires at least 1 argument, the ID or name of the tunnel to delete.`) } warningChecker := updater.StartWarningCheck(c) defer warningChecker.LogWarningIfAny(sc.log) tunnelIDs, err := sc.findIDs(c.Args().Slice()) if err != nil { return err } return sc.delete(tunnelIDs) } func renderOutput(format string, v interface{}) error { switch format { case "json": encoder := json.NewEncoder(os.Stdout) encoder.SetIndent("", " ") return encoder.Encode(v) case "yaml": return yaml.NewEncoder(os.Stdout).Encode(v) default: return errors.Errorf("Unknown output format '%s'", format) } } func buildRunCommand() *cli.Command { flags := []cli.Flag{ credentialsFileFlag, credentialsContentsFlag, postQuantumFlag, selectProtocolFlag, featuresFlag, tunnelTokenFlag, tunnelTokenFileFlag, icmpv4SrcFlag, icmpv6SrcFlag, maxActiveFlowsFlag, dnsResolverAddrsFlag, } flags = append(flags, configureProxyFlags(false)...) return &cli.Command{ Name: "run", Action: cliutil.ConfiguredAction(runCommand), Usage: "Proxy a local web server by running the given tunnel", UsageText: "cloudflared tunnel [tunnel command options] run [subcommand options] [TUNNEL]", Description: `Runs the tunnel identified by name or UUID, creating highly available connections between your server and the Cloudflare edge. You can provide name or UUID of tunnel to run either as the last command line argument or in the configuration file using "tunnel: TUNNEL". This command requires the tunnel credentials file created when "cloudflared tunnel create" was run, however it does not need access to cert.pem from "cloudflared login" if you identify the tunnel by UUID. If you experience other problems running the tunnel, "cloudflared tunnel cleanup" may help by removing any old connection records. `, Flags: flags, CustomHelpTemplate: commandHelpTemplate(), } } func runCommand(c *cli.Context) error { sc, err := newSubcommandContext(c) if err != nil { return err } if c.NArg() > 1 { return cliutil.UsageError(`"cloudflared tunnel run" accepts only one argument, the ID or name of the tunnel to run.`) } if c.String("hostname") != "" { sc.log.Warn().Msg("The property `hostname` in your configuration is ignored because you configured a Named Tunnel " + "in the property `tunnel` to run. Make sure to provision the routing (e.g. via `cloudflared tunnel route dns/lb`) or else " + "your origin will not be reachable. You should remove the `hostname` property to avoid this warning.") } tokenStr := c.String(TunnelTokenFlag) // Check if tokenStr is blank before checking for tokenFile if tokenStr == "" { if tokenFile := c.String(TunnelTokenFileFlag); tokenFile != "" { data, err := os.ReadFile(tokenFile) if err != nil { return cliutil.UsageError("Failed to read token file: %s", err.Error()) } tokenStr = strings.TrimSpace(string(data)) } } // Check if token is provided and if not use default tunnelID flag method if tokenStr != "" { if token, err := ParseToken(tokenStr); err == nil { return sc.runWithCredentials(token.Credentials()) } return cliutil.UsageError("Provided Tunnel token is not valid.") } else { tunnelRef := c.Args().First() if tunnelRef == "" { // see if tunnel id was in the config file tunnelRef = config.GetConfiguration().TunnelID if tunnelRef == "" { return cliutil.UsageError(`"cloudflared tunnel run" requires the ID or name of the tunnel to run as the last command line argument or in the configuration file.`) } } return runNamedTunnel(sc, tunnelRef) } } func ParseToken(tokenStr string) (*connection.TunnelToken, error) { content, err := base64.StdEncoding.DecodeString(tokenStr) if err != nil { return nil, err } var token connection.TunnelToken if err := json.Unmarshal(content, &token); err != nil { return nil, err } return &token, nil } func runNamedTunnel(sc *subcommandContext, tunnelRef string) error { tunnelID, err := sc.findID(tunnelRef) if err != nil { return errors.Wrap(err, "error parsing tunnel ID") } return sc.run(tunnelID) } func buildCleanupCommand() *cli.Command { return &cli.Command{ Name: "cleanup", Action: cliutil.ConfiguredAction(cleanupCommand), Usage: "Cleanup tunnel connections", UsageText: "cloudflared tunnel [tunnel command options] cleanup [subcommand options] TUNNEL", Description: "Delete connections for tunnels with the given UUIDs or names.", Flags: []cli.Flag{cleanupClientFlag}, CustomHelpTemplate: commandHelpTemplate(), } } func cleanupCommand(c *cli.Context) error { if c.NArg() < 1 { return cliutil.UsageError(`"cloudflared tunnel cleanup" requires at least 1 argument, the IDs of the tunnels to cleanup connections.`) } sc, err := newSubcommandContext(c) if err != nil { return err } tunnelIDs, err := sc.findIDs(c.Args().Slice()) if err != nil { return err } return sc.cleanupConnections(tunnelIDs) } func buildTokenCommand() *cli.Command { return &cli.Command{ Name: "token", Action: cliutil.ConfiguredAction(tokenCommand), Usage: "Fetch the credentials token for an existing tunnel (by name or UUID) that allows to run it", UsageText: "cloudflared tunnel [tunnel command options] token [subcommand options] TUNNEL", Description: "cloudflared tunnel token will fetch the credentials token for a given tunnel (by its name or UUID), which is then used to run the tunnel. This command fails if the tunnel does not exist or has been deleted. Use the flag `cloudflared tunnel token --cred-file /my/path/file.json TUNNEL` to output the token to the credentials JSON file. Note: this command only works for Tunnels created since cloudflared version 2022.3.0", Flags: []cli.Flag{credentialsFileFlagCLIOnly}, CustomHelpTemplate: commandHelpTemplate(), } } func tokenCommand(c *cli.Context) error { sc, err := newSubcommandContext(c) if err != nil { return errors.Wrap(err, "error setting up logger") } warningChecker := updater.StartWarningCheck(c) defer warningChecker.LogWarningIfAny(sc.log) if c.NArg() != 1 { return cliutil.UsageError(`"cloudflared tunnel token" requires exactly 1 argument, the name or UUID of tunnel to fetch the credentials token for.`) } tunnelID, err := sc.findID(c.Args().First()) if err != nil { return errors.Wrap(err, "error parsing tunnel ID") } token, err := sc.getTunnelTokenCredentials(tunnelID) if err != nil { return err } if path := c.String(CredFileFlag); path != "" { credentials := token.Credentials() err := writeTunnelCredentials(path, &credentials) if err != nil { return errors.Wrapf(err, "error writing token credentials to JSON file in path %s", path) } return nil } encodedToken, err := token.Encode() if err != nil { return err } fmt.Println(encodedToken) return nil } func buildRouteCommand() *cli.Command { return &cli.Command{ Name: "route", Usage: "Define which traffic routed from Cloudflare edge to this tunnel: requests to a DNS hostname, to a Cloudflare Load Balancer, or traffic originating from Cloudflare WARP clients", UsageText: "cloudflared tunnel [tunnel command options] route [subcommand options] [dns TUNNEL HOSTNAME]|[lb TUNNEL HOSTNAME LB-POOL]|[ip NETWORK TUNNEL]", Description: `The route command defines how Cloudflare will proxy requests to this tunnel. To route a hostname by creating a DNS CNAME record to a tunnel: cloudflared tunnel route dns You can read more at: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/routing-to-tunnel/dns To use this tunnel as a load balancer origin, creating pool and load balancer if necessary: cloudflared tunnel route lb You can read more at: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/routing-to-tunnel/lb For Cloudflare WARP traffic to be routed to your private network, reachable from this tunnel as origins, use: cloudflared tunnel route ip Further information about managing Cloudflare WARP traffic to your tunnel is available at: cloudflared tunnel route ip --help `, CustomHelpTemplate: commandHelpTemplate(), Subcommands: []*cli.Command{ { Name: "dns", Action: cliutil.ConfiguredAction(routeDnsCommand), Usage: "HostnameRoute a hostname by creating a DNS CNAME record to a tunnel", UsageText: "cloudflared tunnel route dns [TUNNEL] [HOSTNAME]", Description: `Creates a DNS CNAME record hostname that points to the tunnel.`, Flags: []cli.Flag{overwriteDNSFlag}, }, { Name: "lb", Action: cliutil.ConfiguredAction(routeLbCommand), Usage: "Use this tunnel as a load balancer origin, creating pool and load balancer if necessary", UsageText: "cloudflared tunnel route lb [TUNNEL] [HOSTNAME] [LB-POOL-NAME]", Description: `Creates Load Balancer with an origin pool that points to the tunnel.`, }, buildRouteIPSubcommand(), }, } } func dnsRouteFromArg(c *cli.Context, overwriteExisting bool) (cfapi.HostnameRoute, error) { const ( userHostnameIndex = 1 expectedNArgs = 2 ) if c.NArg() != expectedNArgs { return nil, cliutil.UsageError("Expected %d arguments, got %d", expectedNArgs, c.NArg()) } userHostname := c.Args().Get(userHostnameIndex) if userHostname == "" { return nil, cliutil.UsageError("The third argument should be the hostname") } else if !validateHostname(userHostname, true) { return nil, errors.Errorf("%s is not a valid hostname", userHostname) } return cfapi.NewDNSRoute(userHostname, overwriteExisting), nil } func lbRouteFromArg(c *cli.Context) (cfapi.HostnameRoute, error) { const ( lbNameIndex = 1 lbPoolIndex = 2 expectedNArgs = 3 ) if c.NArg() != expectedNArgs { return nil, cliutil.UsageError("Expected %d arguments, got %d", expectedNArgs, c.NArg()) } lbName := c.Args().Get(lbNameIndex) if lbName == "" { return nil, cliutil.UsageError("The third argument should be the load balancer name") } else if !validateHostname(lbName, true) { return nil, errors.Errorf("%s is not a valid load balancer name", lbName) } lbPool := c.Args().Get(lbPoolIndex) if lbPool == "" { return nil, cliutil.UsageError("The fourth argument should be the pool name") } else if !validateName(lbPool, false) { return nil, errors.Errorf("%s is not a valid pool name", lbPool) } return cfapi.NewLBRoute(lbName, lbPool), nil } var ( nameRegex = regexp.MustCompile("^[_a-zA-Z0-9][-_.a-zA-Z0-9]*$") hostNameRegex = regexp.MustCompile("^[*_a-zA-Z0-9][-_.a-zA-Z0-9]*$") ) func validateName(s string, allowWildcardSubdomain bool) bool { if allowWildcardSubdomain { return hostNameRegex.MatchString(s) } return nameRegex.MatchString(s) } func validateHostname(s string, allowWildcardSubdomain bool) bool { // Slightly stricter than PunyCodeProfile idnaProfile := idna.New( idna.ValidateLabels(true), idna.VerifyDNSLength(true)) puny, err := idnaProfile.ToASCII(s) return err == nil && validateName(puny, allowWildcardSubdomain) } func routeDnsCommand(c *cli.Context) error { if c.NArg() != 2 { return cliutil.UsageError(`This command expects the format "cloudflared tunnel route dns "`) } return routeCommand(c, "dns") } func routeLbCommand(c *cli.Context) error { if c.NArg() != 3 { return cliutil.UsageError(`This command expects the format "cloudflared tunnel route lb "`) } return routeCommand(c, "lb") } func routeCommand(c *cli.Context, routeType string) error { sc, err := newSubcommandContext(c) if err != nil { return err } tunnelID, err := sc.findID(c.Args().Get(0)) if err != nil { return err } var route cfapi.HostnameRoute switch routeType { case "dns": route, err = dnsRouteFromArg(c, c.Bool(overwriteDNSFlagName)) case "lb": route, err = lbRouteFromArg(c) } if err != nil { return err } res, err := sc.route(tunnelID, route) if err != nil { return err } sc.log.Info().Str(LogFieldTunnelID, tunnelID.String()).Msg(res.SuccessSummary()) return nil } func commandHelpTemplate() string { var parentFlagsHelp string for _, f := range configureCloudflaredFlags(false) { parentFlagsHelp += fmt.Sprintf(" %s\n\t", f) } for _, f := range cliutil.ConfigureLoggingFlags(false) { parentFlagsHelp += fmt.Sprintf(" %s\n\t", f) } const template = `NAME: {{.HelpName}} - {{.Usage}} USAGE: {{.UsageText}} DESCRIPTION: {{.Description}} TUNNEL COMMAND OPTIONS: %s SUBCOMMAND OPTIONS: {{range .VisibleFlags}}{{.}} {{end}} ` return fmt.Sprintf(template, parentFlagsHelp) } func buildDiagCommand() *cli.Command { return &cli.Command{ Name: "diag", Action: cliutil.ConfiguredAction(diagCommand), Usage: "Creates a diagnostic report from a local cloudflared instance", UsageText: "cloudflared tunnel [tunnel command options] diag [subcommand options]", Description: "cloudflared tunnel diag will create a diagnostic report of a local cloudflared instance. The diagnostic procedure collects: logs, metrics, system information, traceroute to Cloudflare Edge, and runtime information. Since there may be multiple instances of cloudflared running the --metrics option may be provided to target a specific instance.", Flags: []cli.Flag{ metricsFlag, diagContainerFlag, diagPodFlag, noDiagLogsFlag, noDiagMetricsFlag, noDiagSystemFlag, noDiagRuntimeFlag, noDiagNetworkFlag, }, CustomHelpTemplate: commandHelpTemplate(), } } func diagCommand(ctx *cli.Context) error { sctx, err := newSubcommandContext(ctx) if err != nil { return err } log := sctx.log options := diagnostic.Options{ KnownAddresses: metrics.GetMetricsKnownAddresses(metrics.Runtime), Address: sctx.c.String(flags.Metrics), ContainerID: sctx.c.String(diagContainerIDFlagName), PodID: sctx.c.String(diagPodFlagName), Toggles: diagnostic.Toggles{ NoDiagLogs: sctx.c.Bool(noDiagLogsFlagName), NoDiagMetrics: sctx.c.Bool(noDiagMetricsFlagName), NoDiagSystem: sctx.c.Bool(noDiagSystemFlagName), NoDiagRuntime: sctx.c.Bool(noDiagRuntimeFlagName), NoDiagNetwork: sctx.c.Bool(noDiagNetworkFlagName), }, } if options.Address == "" { log.Info().Msg("If your instance is running in a Docker/Kubernetes environment you need to setup port forwarding for your application.") } states, err := diagnostic.RunDiagnostic(log, options) if errors.Is(err, diagnostic.ErrMetricsServerNotFound) { log.Warn().Msg("No instances found") return nil } if errors.Is(err, diagnostic.ErrMultipleMetricsServerFound) { if states != nil { log.Info().Msgf("Found multiple instances running:") for _, state := range states { log.Info().Msgf("Instance: tunnel-id=%s connector-id=%s metrics-address=%s", state.TunnelID, state.ConnectorID, state.URL.String()) } log.Info().Msgf("To select one instance use the option --metrics") } return nil } if errors.Is(err, diagnostic.ErrLogConfigurationIsInvalid) { log.Info().Msg("Couldn't extract logs from the instance. If the instance is running in a containerized environment use the option --diag-container-id or --diag-pod-id. If there is no logging configuration use --no-diag-logs.") } if err != nil { log.Warn().Msg("Diagnostic completed with one or more errors") } else { log.Info().Msg("Diagnostic completed") } return nil } ================================================ FILE: cmd/cloudflared/tunnel/subcommands_test.go ================================================ package tunnel import ( "encoding/base64" "encoding/json" "path/filepath" "testing" "github.com/google/uuid" homedir "github.com/mitchellh/go-homedir" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/cloudflare/cloudflared/cfapi" "github.com/cloudflare/cloudflared/connection" ) func Test_fmtConnections(t *testing.T) { type args struct { connections []cfapi.Connection } tests := []struct { name string args args want string }{ { name: "empty", args: args{ connections: []cfapi.Connection{}, }, want: "", }, { name: "trivial", args: args{ connections: []cfapi.Connection{ { ColoName: "DFW", ID: uuid.MustParse("ea550130-57fd-4463-aab1-752822231ddd"), }, }, }, want: "1xDFW", }, { name: "with a pending reconnect", args: args{ connections: []cfapi.Connection{ { ColoName: "DFW", ID: uuid.MustParse("ea550130-57fd-4463-aab1-752822231ddd"), IsPendingReconnect: true, }, }, }, want: "", }, { name: "many colos", args: args{ connections: []cfapi.Connection{ { ColoName: "YRV", ID: uuid.MustParse("ea550130-57fd-4463-aab1-752822231ddd"), }, { ColoName: "DFW", ID: uuid.MustParse("c13c0b3b-0fbf-453c-8169-a1990fced6d0"), }, { ColoName: "ATL", ID: uuid.MustParse("70c90639-e386-4e8d-9a4e-7f046d70e63f"), }, { ColoName: "DFW", ID: uuid.MustParse("30ad6251-0305-4635-a670-d3994f474981"), }, }, }, want: "1xATL, 2xDFW, 1xYRV", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := fmtConnections(tt.args.connections, false); got != tt.want { t.Errorf("fmtConnections() = %v, want %v", got, tt.want) } }) } } func TestTunnelfilePath(t *testing.T) { tunnelID, err := uuid.Parse("f48d8918-bc23-4647-9d48-082c5b76de65") assert.NoError(t, err) originCertDir := filepath.Dir("~/.cloudflared/cert.pem") actual, err := tunnelFilePath(tunnelID, originCertDir) assert.NoError(t, err) homeDir, err := homedir.Dir() assert.NoError(t, err) expected := filepath.Join(homeDir, ".cloudflared", tunnelID.String()+".json") assert.Equal(t, expected, actual) } func TestValidateName(t *testing.T) { tests := []struct { name string want bool }{ {name: "", want: false}, {name: "-", want: false}, {name: ".", want: false}, {name: "a b", want: false}, {name: "a+b", want: false}, {name: "-ab", want: false}, {name: "ab", want: true}, {name: "ab-c", want: true}, {name: "abc.def", want: true}, {name: "_ab_c.-d-ef", want: true}, } for _, tt := range tests { if got := validateName(tt.name, false); got != tt.want { t.Errorf("validateName() = %v, want %v", got, tt.want) } } } func Test_validateHostname(t *testing.T) { type args struct { s string allowWildcardSubdomain bool } tests := []struct { name string args args want bool }{ { name: "Normal", args: args{ s: "example.com", allowWildcardSubdomain: true, }, want: true, }, { name: "wildcard subdomain for TUN-358", args: args{ s: "*.ehrig.io", allowWildcardSubdomain: true, }, want: true, }, { name: "Misplaced wildcard", args: args{ s: "subdomain.*.ehrig.io", allowWildcardSubdomain: true, }, }, { name: "Invalid domain", args: args{ s: "..", allowWildcardSubdomain: true, }, }, { name: "Invalid domain", args: args{ s: "..", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := validateHostname(tt.args.s, tt.args.allowWildcardSubdomain); got != tt.want { t.Errorf("validateHostname() = %v, want %v", got, tt.want) } }) } } func Test_TunnelToken(t *testing.T) { token, err := ParseToken("aabc") require.Error(t, err) require.Nil(t, token) expectedToken := &connection.TunnelToken{ AccountTag: "abc", TunnelSecret: []byte("secret"), TunnelID: uuid.New(), } tokenJsonStr, err := json.Marshal(expectedToken) require.NoError(t, err) token64 := base64.StdEncoding.EncodeToString(tokenJsonStr) token, err = ParseToken(token64) require.NoError(t, err) require.Equal(t, token, expectedToken) } ================================================ FILE: cmd/cloudflared/tunnel/tag.go ================================================ package tunnel import ( "fmt" "regexp" "github.com/cloudflare/cloudflared/tunnelrpc/pogs" ) // Restrict key names to characters allowed in an HTTP header name. // Restrict key values to printable characters (what is recognised as data in an HTTP header value). var tagRegexp = regexp.MustCompile("^([a-zA-Z0-9!#$%&'*+\\-.^_`|~]+)=([[:print:]]+)$") func NewTagFromCLI(compoundTag string) (pogs.Tag, bool) { matches := tagRegexp.FindStringSubmatch(compoundTag) if len(matches) == 0 { return pogs.Tag{}, false } return pogs.Tag{Name: matches[1], Value: matches[2]}, true } func NewTagSliceFromCLI(tags []string) ([]pogs.Tag, error) { var tagSlice []pogs.Tag for _, compoundTag := range tags { if tag, ok := NewTagFromCLI(compoundTag); ok { tagSlice = append(tagSlice, tag) } else { return nil, fmt.Errorf("Cannot parse tag value %s", compoundTag) } } return tagSlice, nil } ================================================ FILE: cmd/cloudflared/tunnel/tag_test.go ================================================ package tunnel import ( "testing" "github.com/cloudflare/cloudflared/tunnelrpc/pogs" "github.com/stretchr/testify/assert" ) func TestSingleTag(t *testing.T) { testCases := []struct { Input string Output pogs.Tag Fail bool }{ {Input: "x=y", Output: pogs.Tag{Name: "x", Value: "y"}}, {Input: "More-Complex=Tag Values", Output: pogs.Tag{Name: "More-Complex", Value: "Tag Values"}}, {Input: "First=Equals=Wins", Output: pogs.Tag{Name: "First", Value: "Equals=Wins"}}, {Input: "x=", Fail: true}, {Input: "=y", Fail: true}, {Input: "=", Fail: true}, {Input: "No spaces allowed=in key names", Fail: true}, {Input: "omg\nwtf=bbq", Fail: true}, } for i, testCase := range testCases { tag, ok := NewTagFromCLI(testCase.Input) assert.Equalf(t, !testCase.Fail, ok, "mismatched success for test case %d", i) assert.Equalf(t, testCase.Output, tag, "mismatched output for test case %d", i) } } func TestTagSlice(t *testing.T) { tagSlice, err := NewTagSliceFromCLI([]string{"a=b", "c=d", "e=f"}) assert.NoError(t, err) assert.Len(t, tagSlice, 3) assert.Equal(t, "a", tagSlice[0].Name) assert.Equal(t, "b", tagSlice[0].Value) assert.Equal(t, "c", tagSlice[1].Name) assert.Equal(t, "d", tagSlice[1].Value) assert.Equal(t, "e", tagSlice[2].Name) assert.Equal(t, "f", tagSlice[2].Value) tagSlice, err = NewTagSliceFromCLI([]string{"a=b", "=", "e=f"}) assert.Error(t, err) } ================================================ FILE: cmd/cloudflared/tunnel/teamnet_subcommands.go ================================================ package tunnel import ( "fmt" "net" "os" "text/tabwriter" "github.com/google/uuid" "github.com/pkg/errors" "github.com/urfave/cli/v2" "github.com/cloudflare/cloudflared/cfapi" "github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil" "github.com/cloudflare/cloudflared/cmd/cloudflared/updater" ) var ( vnetFlag = &cli.StringFlag{ Name: "vnet", Aliases: []string{"vn"}, Usage: "The ID or name of the virtual network to which the route is associated to.", } errAddRoute = errors.New("You must supply exactly one argument, the ID or CIDR of the route you want to delete") ) func buildRouteIPSubcommand() *cli.Command { return &cli.Command{ Name: "ip", Usage: "Configure and query Cloudflare WARP routing to private IP networks made available through Cloudflare Tunnels.", UsageText: "cloudflared tunnel [--config FILEPATH] route COMMAND [arguments...]", Description: `cloudflared can provision routes for any IP space in your corporate network. Users enrolled in your Cloudflare for Teams organization can reach those IPs through the Cloudflare WARP client. You can then configure L7/L4 filtering on https://one.dash.cloudflare.com to determine who can reach certain routes. By default IP routes all exist within a single virtual network. If you use the same IP space(s) in different physical private networks, all meant to be reachable via IP routes, then you have to manage the ambiguous IP routes by associating them to virtual networks. See "cloudflared tunnel vnet --help" for more information.`, Subcommands: []*cli.Command{ { Name: "add", Action: cliutil.ConfiguredAction(addRouteCommand), Usage: "Add a new network to the routing table reachable via a Tunnel", UsageText: "cloudflared tunnel [--config FILEPATH] route ip add [flags] [CIDR] [TUNNEL] [COMMENT?]", Description: `Adds a network IP route space (represented as a CIDR) to your routing table. That network IP space becomes reachable for requests egressing from a user's machine as long as it is using Cloudflare WARP client and is enrolled in the same account that is running the Tunnel chosen here. Further, those requests will be proxied to the specified Tunnel, and reach an IP in the given CIDR, as long as that IP is reachable from cloudflared. If the CIDR exists in more than one private network, to be connected with Cloudflare Tunnels, then you have to manage those IP routes with virtual networks (see "cloudflared tunnel vnet --help)". In those cases, you then have to tell which virtual network's routing table you want to add the route to with: "cloudflared tunnel route ip add --vnet [ID/name] [CIDR] [TUNNEL]".`, Flags: []cli.Flag{vnetFlag}, }, { Name: "show", Aliases: []string{"list"}, Action: cliutil.ConfiguredAction(showRoutesCommand), Usage: "Show the routing table", UsageText: "cloudflared tunnel [--config FILEPATH] route ip show [flags]", Description: `Shows your organization private routing table. You can use flags to filter the results.`, Flags: showRoutesFlags(), }, { Name: "delete", Action: cliutil.ConfiguredAction(deleteRouteCommand), Usage: "Delete a row from your organization's private routing table", UsageText: "cloudflared tunnel [--config FILEPATH] route ip delete [flags] [Route ID or CIDR]", Description: `Deletes the row for the given route ID from your routing table. That portion of your network will no longer be reachable.`, Flags: []cli.Flag{vnetFlag}, }, { Name: "get", Action: cliutil.ConfiguredAction(getRouteByIPCommand), Usage: "Check which row of the routing table matches a given IP.", UsageText: "cloudflared tunnel [--config FILEPATH] route ip get [flags] [IP]", Description: `Checks which row of the routing table will be used to proxy a given IP. This helps check and validate your config. Note that if you use virtual networks, then you have to tell which virtual network whose routing table you want to use.`, Flags: []cli.Flag{vnetFlag}, }, }, } } func showRoutesFlags() []cli.Flag { flags := make([]cli.Flag, 0) flags = append(flags, cfapi.IpRouteFilterFlags...) flags = append(flags, outputFormatFlag) return flags } func showRoutesCommand(c *cli.Context) error { sc, err := newSubcommandContext(c) if err != nil { return err } filter, err := cfapi.NewIpRouteFilterFromCLI(c) if err != nil { return errors.Wrap(err, "invalid config for routing filters") } warningChecker := updater.StartWarningCheck(c) defer warningChecker.LogWarningIfAny(sc.log) routes, err := sc.listRoutes(filter) if err != nil { return err } if outputFormat := c.String(outputFormatFlag.Name); outputFormat != "" { return renderOutput(outputFormat, routes) } if len(routes) > 0 { formatAndPrintRouteList(routes) } else { fmt.Println("No routes were found for the given filter flags. You can use 'cloudflared tunnel route ip add' to add a route.") } return nil } func addRouteCommand(c *cli.Context) error { sc, err := newSubcommandContext(c) if err != nil { return err } if c.NArg() < 2 { return errors.New("You must supply at least 2 arguments, first the network you wish to route (in CIDR form e.g. 1.2.3.4/32) and then the tunnel ID to proxy with") } args := c.Args() _, network, err := net.ParseCIDR(args.Get(0)) if err != nil { return errors.Wrap(err, "Invalid network CIDR") } if network == nil { return errors.New("Invalid network CIDR") } tunnelRef := args.Get(1) tunnelID, err := sc.findID(tunnelRef) if err != nil { return errors.Wrap(err, "Invalid tunnel") } comment := "" if c.NArg() >= 3 { comment = args.Get(2) } var vnetId *uuid.UUID if c.IsSet(vnetFlag.Name) { id, err := getVnetId(sc, c.String(vnetFlag.Name)) if err != nil { return err } vnetId = &id } _, err = sc.addRoute(cfapi.NewRoute{ Comment: comment, Network: *network, TunnelID: tunnelID, VNetID: vnetId, }) if err != nil { return errors.Wrap(err, "API error") } fmt.Printf("Successfully added route for %s over tunnel %s\n", network, tunnelID) return nil } func deleteRouteCommand(c *cli.Context) error { sc, err := newSubcommandContext(c) if err != nil { return err } if c.NArg() != 1 { return errAddRoute } var routeId uuid.UUID routeId, err = uuid.Parse(c.Args().First()) if err != nil { _, network, err := net.ParseCIDR(c.Args().First()) if err != nil || network == nil { return errAddRoute } var vnetId *uuid.UUID if c.IsSet(vnetFlag.Name) { id, err := getVnetId(sc, c.String(vnetFlag.Name)) if err != nil { return err } vnetId = &id } routeId, err = sc.getRouteId(*network, vnetId) if err != nil { return err } } if err := sc.deleteRoute(routeId); err != nil { return errors.Wrap(err, "API error") } fmt.Printf("Successfully deleted route with ID %s\n", routeId) return nil } func getRouteByIPCommand(c *cli.Context) error { sc, err := newSubcommandContext(c) if err != nil { return err } if c.NArg() != 1 { return errors.New("You must supply exactly one argument, an IP whose route will be queried (e.g. 1.2.3.4 or 2001:0db8:::7334)") } ipInput := c.Args().First() ip := net.ParseIP(ipInput) if ip == nil { return fmt.Errorf("Invalid IP %s", ipInput) } params := cfapi.GetRouteByIpParams{ Ip: ip, } if c.IsSet(vnetFlag.Name) { vnetId, err := getVnetId(sc, c.String(vnetFlag.Name)) if err != nil { return err } params.VNetID = &vnetId } route, err := sc.getRouteByIP(params) if err != nil { return errors.Wrap(err, "API error") } if route.IsZero() { fmt.Printf("No route matches the IP %s\n", ip) } else { formatAndPrintRouteList([]*cfapi.DetailedRoute{&route}) } return nil } func formatAndPrintRouteList(routes []*cfapi.DetailedRoute) { const ( minWidth = 0 tabWidth = 8 padding = 1 padChar = ' ' flags = 0 ) writer := tabwriter.NewWriter(os.Stdout, minWidth, tabWidth, padding, padChar, flags) defer writer.Flush() // Print column headers with tabbed columns _, _ = fmt.Fprintln(writer, "ID\tNETWORK\tVIRTUAL NET ID\tCOMMENT\tTUNNEL ID\tTUNNEL NAME\tCREATED\tDELETED\t") // Loop through routes, create formatted string for each, and print using tabwriter for _, route := range routes { formattedStr := route.TableString() _, _ = fmt.Fprintln(writer, formattedStr) } } ================================================ FILE: cmd/cloudflared/tunnel/vnets_subcommands.go ================================================ package tunnel import ( "fmt" "os" "text/tabwriter" "github.com/google/uuid" "github.com/pkg/errors" "github.com/urfave/cli/v2" "github.com/cloudflare/cloudflared/cfapi" "github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil" "github.com/cloudflare/cloudflared/cmd/cloudflared/updater" ) var ( makeDefaultFlag = &cli.BoolFlag{ Name: "default", Aliases: []string{"d"}, Usage: "The virtual network becomes the default one for the account. This means that all operations that " + "omit a virtual network will now implicitly be using this virtual network (i.e., the default one) such " + "as new IP routes that are created. When this flag is not set, the virtual network will not become the " + "default one in the account.", } newNameFlag = &cli.StringFlag{ Name: "name", Aliases: []string{"n"}, Usage: "The new name for the virtual network.", } newCommentFlag = &cli.StringFlag{ Name: "comment", Aliases: []string{"c"}, Usage: "A new comment describing the purpose of the virtual network.", } vnetForceDeleteFlag = &cli.BoolFlag{ Name: "force", Aliases: []string{"f"}, Usage: "Force the deletion of the virtual network even if it is being relied upon by other resources. Those" + "resources will either be deleted (e.g. IP Routes) or moved to the current default virutal network.", } ) func buildVirtualNetworkSubcommand(hidden bool) *cli.Command { return &cli.Command{ Name: "vnet", Usage: "Configure and query virtual networks to manage private IP routes with overlapping IPs.", UsageText: "cloudflared tunnel [--config FILEPATH] network COMMAND [arguments...]", Description: `cloudflared allows to manage IP routes that expose origins in your private network space via their IP directly to clients outside (e.g. using WARP client) --- those are configurable via "cloudflared tunnel route ip" commands. By default, all those IP routes live in the same virtual network. Managing virtual networks (e.g. by creating a new one) becomes relevant when you have different private networks that have overlapping IPs. E.g.: if you have a private network A running Tunnel 1, and private network B running Tunnel 2, it is possible that both Tunnels expose the same IP space (say 10.0.0.0/8); to handle that, you have to add each IP Route (one that points to Tunnel 1 and another that points to Tunnel 2) in different Virtual Networks. That way, if your clients are on Virtual Network X, they will see Tunnel 1 (via Route A) and not see Tunnel 2 (since its Route B is associated to another Virtual Network Y).`, Hidden: hidden, Subcommands: []*cli.Command{ { Name: "add", Action: cliutil.ConfiguredAction(addVirtualNetworkCommand), Usage: "Add a new virtual network to which IP routes can be attached", UsageText: "cloudflared tunnel [--config FILEPATH] network add [flags] NAME [\"comment\"]", Description: `Adds a new virtual network. You can then attach IP routes to this virtual network with "cloudflared tunnel route ip" commands. By doing so, such route(s) become segregated from route(s) in another virtual networks. Note that all routes exist within some virtual network. If you do not specify any, then the system pre-creates a default virtual network to which all routes belong. That is fine if you do not have overlapping IPs within different physical private networks in your infrastructure exposed via Cloudflare Tunnel. Note: if a virtual network is added as the new default, then the previous existing default virtual network will be automatically modified to no longer be the current default.`, Flags: []cli.Flag{makeDefaultFlag}, Hidden: hidden, }, { Name: "list", Action: cliutil.ConfiguredAction(listVirtualNetworksCommand), Usage: "Lists the virtual networks", UsageText: "cloudflared tunnel [--config FILEPATH] network list [flags]", Description: "Lists the virtual networks based on the given filter flags.", Flags: listVirtualNetworksFlags(), Hidden: hidden, }, { Name: "delete", Action: cliutil.ConfiguredAction(deleteVirtualNetworkCommand), Usage: "Delete a virtual network", UsageText: "cloudflared tunnel [--config FILEPATH] network delete VIRTUAL_NETWORK", Description: `Deletes the virtual network (given its ID or name). This is only possible if that virtual network is unused. A virtual network may be used by IP routes or by WARP devices.`, Flags: []cli.Flag{vnetForceDeleteFlag}, Hidden: hidden, }, { Name: "update", Action: cliutil.ConfiguredAction(updateVirtualNetworkCommand), Usage: "Update a virtual network", UsageText: "cloudflared tunnel [--config FILEPATH] network update [flags] VIRTUAL_NETWORK", Description: `Updates the virtual network (given its ID or name). If this virtual network is updated to become the new default, then the previously existing default virtual network will also be modified to no longer be the default. You cannot update a virtual network to not be the default anymore directly. Instead, you should create a new default or update an existing one to become the default.`, Flags: []cli.Flag{newNameFlag, newCommentFlag, makeDefaultFlag}, Hidden: hidden, }, }, } } func listVirtualNetworksFlags() []cli.Flag { flags := make([]cli.Flag, 0) flags = append(flags, cfapi.VnetFilterFlags...) flags = append(flags, outputFormatFlag) return flags } func addVirtualNetworkCommand(c *cli.Context) error { sc, err := newSubcommandContext(c) if err != nil { return err } if c.NArg() < 1 { return errors.New("You must supply at least 1 argument, the name of the virtual network you wish to add.") } warningChecker := updater.StartWarningCheck(c) defer warningChecker.LogWarningIfAny(sc.log) args := c.Args() name := args.Get(0) comment := "" if c.NArg() >= 2 { comment = args.Get(1) } newVnet := cfapi.NewVirtualNetwork{ Name: name, Comment: comment, IsDefault: c.Bool(makeDefaultFlag.Name), } createdVnet, err := sc.addVirtualNetwork(newVnet) if err != nil { return errors.Wrap(err, "Could not add virtual network") } extraMsg := "" if createdVnet.IsDefault { extraMsg = " (as the new default for this account) " } fmt.Printf( "Successfully added virtual 'network' %s with ID: %s%s\n"+ "You can now add IP routes attached to this virtual network. See `cloudflared tunnel route ip add -help`\n", name, createdVnet.ID, extraMsg, ) return nil } func listVirtualNetworksCommand(c *cli.Context) error { sc, err := newSubcommandContext(c) if err != nil { return err } warningChecker := updater.StartWarningCheck(c) defer warningChecker.LogWarningIfAny(sc.log) filter, err := cfapi.NewFromCLI(c) if err != nil { return errors.Wrap(err, "invalid flags for filtering virtual networks") } vnets, err := sc.listVirtualNetworks(filter) if err != nil { return err } if outputFormat := c.String(outputFormatFlag.Name); outputFormat != "" { return renderOutput(outputFormat, vnets) } if len(vnets) > 0 { formatAndPrintVnetsList(vnets) } else { fmt.Println("No virtual networks were found for the given filter flags. You can use 'cloudflared tunnel vnet add' to add a virtual network.") } return nil } func deleteVirtualNetworkCommand(c *cli.Context) error { sc, err := newSubcommandContext(c) if err != nil { return err } if c.NArg() < 1 { return errors.New("You must supply exactly one argument, either the ID or name of the virtual network to delete") } input := c.Args().Get(0) vnetId, err := getVnetId(sc, input) if err != nil { return err } forceDelete := false if c.IsSet(vnetForceDeleteFlag.Name) { forceDelete = c.Bool(vnetForceDeleteFlag.Name) } if err := sc.deleteVirtualNetwork(vnetId, forceDelete); err != nil { return errors.Wrap(err, "API error") } fmt.Printf("Successfully deleted virtual network '%s'\n", input) return nil } func updateVirtualNetworkCommand(c *cli.Context) error { sc, err := newSubcommandContext(c) if err != nil { return err } if c.NArg() != 1 { return errors.New(" You must supply exactly one argument, either the ID or (current) name of the virtual network to update") } input := c.Args().Get(0) vnetId, err := getVnetId(sc, input) if err != nil { return err } updates := cfapi.UpdateVirtualNetwork{} if c.IsSet(newNameFlag.Name) { newName := c.String(newNameFlag.Name) updates.Name = &newName } if c.IsSet(newCommentFlag.Name) { newComment := c.String(newCommentFlag.Name) updates.Comment = &newComment } if c.IsSet(makeDefaultFlag.Name) { isDefault := c.Bool(makeDefaultFlag.Name) updates.IsDefault = &isDefault } if err := sc.updateVirtualNetwork(vnetId, updates); err != nil { return errors.Wrap(err, "API error") } fmt.Printf("Successfully updated virtual network '%s'\n", input) return nil } func getVnetId(sc *subcommandContext, input string) (uuid.UUID, error) { val, err := uuid.Parse(input) if err == nil { return val, nil } filter := cfapi.NewVnetFilter() filter.WithDeleted(false) filter.ByName(input) vnets, err := sc.listVirtualNetworks(filter) if err != nil { return uuid.Nil, err } if len(vnets) != 1 { return uuid.Nil, fmt.Errorf("there should only be 1 non-deleted virtual network named %s", input) } return vnets[0].ID, nil } func formatAndPrintVnetsList(vnets []*cfapi.VirtualNetwork) { const ( minWidth = 0 tabWidth = 8 padding = 1 padChar = ' ' flags = 0 ) writer := tabwriter.NewWriter(os.Stdout, minWidth, tabWidth, padding, padChar, flags) defer writer.Flush() _, _ = fmt.Fprintln(writer, "ID\tNAME\tIS DEFAULT\tCOMMENT\tCREATED\tDELETED\t") for _, virtualNetwork := range vnets { formattedStr := virtualNetwork.TableString() _, _ = fmt.Fprintln(writer, formattedStr) } } ================================================ FILE: cmd/cloudflared/updater/check.go ================================================ package updater import ( "github.com/rs/zerolog" "github.com/urfave/cli/v2" ) type VersionWarningChecker struct { warningChan chan string } func StartWarningCheck(c *cli.Context) VersionWarningChecker { checker := VersionWarningChecker{ warningChan: make(chan string), } go func() { options := updateOptions{ updateDisabled: true, isBeta: c.Bool("beta"), isStaging: c.Bool("staging"), isForced: false, intendedVersion: "", } checkResult, err := CheckForUpdate(options) if err == nil { checker.warningChan <- checkResult.UserMessage() } close(checker.warningChan) }() return checker } func (checker VersionWarningChecker) getWarning() string { select { case message := <-checker.warningChan: return message default: // No feedback on time, we don't wait for it, since this is best-effort. return "" } } func (checker VersionWarningChecker) LogWarningIfAny(log *zerolog.Logger) { if warning := checker.getWarning(); warning != "" { log.Warn().Msg(warning) } } ================================================ FILE: cmd/cloudflared/updater/service.go ================================================ package updater // CheckResult is the behaviour resulting from checking in with the Update Service type CheckResult interface { Apply() error Version() string UserMessage() string } // Service is the functions to get check for new updates type Service interface { Check() (CheckResult, error) } const ( // OSKeyName is the url parameter key to send to the checkin API for the operating system of the local cloudflared (e.g. windows, darwin, linux) OSKeyName = "os" // ArchitectureKeyName is the url parameter key to send to the checkin API for the architecture of the local cloudflared (e.g. amd64, x86) ArchitectureKeyName = "arch" // BetaKeyName is the url parameter key to send to the checkin API to signal if the update should be a beta version or not BetaKeyName = "beta" // VersionKeyName is the url parameter key to send to the checkin API to specific what version to upgrade or downgrade to VersionKeyName = "version" // ClientVersionName is the url parameter key to send the version that this cloudflared is currently running with ClientVersionName = "clientVersion" ) ================================================ FILE: cmd/cloudflared/updater/update.go ================================================ package updater import ( "context" "fmt" "os" "path/filepath" "runtime" "strings" "time" "github.com/facebookgo/grace/gracenet" "github.com/rs/zerolog" "github.com/urfave/cli/v2" "golang.org/x/term" "github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil" cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags" "github.com/cloudflare/cloudflared/config" "github.com/cloudflare/cloudflared/logger" ) const ( DefaultCheckUpdateFreq = time.Hour * 24 noUpdateInShellMessage = "cloudflared will not automatically update when run from the shell. To enable auto-updates, run cloudflared as a service: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/configure-tunnels/local-management/as-a-service/" noUpdateOnWindowsMessage = "cloudflared will not automatically update on Windows systems." noUpdateManagedPackageMessage = "cloudflared will not automatically update if installed by a package manager." isManagedInstallFile = ".installedFromPackageManager" UpdateURL = "https://update.argotunnel.com" StagingUpdateURL = "https://staging-update.argotunnel.com" LogFieldVersion = "version" ) var ( buildInfo *cliutil.BuildInfo BuiltForPackageManager = "" ) // BinaryUpdated implements ExitCoder interface, the app will exit with status code 11 // https://pkg.go.dev/github.com/urfave/cli/v2?tab=doc#ExitCoder // nolint: errname type statusSuccess struct { newVersion string } func (u *statusSuccess) Error() string { return fmt.Sprintf("cloudflared has been updated to version %s", u.newVersion) } func (u *statusSuccess) ExitCode() int { return 11 } // statusError implements ExitCoder interface, the app will exit with status code 10 type statusError struct { err error } func (e *statusError) Error() string { return fmt.Sprintf("failed to update cloudflared: %v", e.err) } func (e *statusError) ExitCode() int { return 10 } type updateOptions struct { updateDisabled bool isBeta bool isStaging bool isForced bool intendedVersion string } type UpdateOutcome struct { Updated bool Version string UserMessage string Error error } func (uo *UpdateOutcome) noUpdate() bool { return uo.Error == nil && !uo.Updated } func Init(info *cliutil.BuildInfo) { buildInfo = info } func CheckForUpdate(options updateOptions) (CheckResult, error) { cfdPath, err := os.Executable() if err != nil { return nil, err } url := UpdateURL if options.isStaging { url = StagingUpdateURL } if runtime.GOOS == "windows" { cfdPath = encodeWindowsPath(cfdPath) } s := NewWorkersService(buildInfo.CloudflaredVersion, url, cfdPath, Options{IsBeta: options.isBeta, IsForced: options.isForced, RequestedVersion: options.intendedVersion}) return s.Check() } func encodeWindowsPath(path string) string { // We do this because Windows allows spaces in directories such as // Program Files but does not allow these directories to be spaced in batch files. targetPath := strings.Replace(path, "Program Files (x86)", "PROGRA~2", -1) // This is to do the same in 32 bit systems. We do this second so that the first // replace is for x86 dirs. targetPath = strings.Replace(targetPath, "Program Files", "PROGRA~1", -1) return targetPath } func applyUpdate(options updateOptions, update CheckResult) UpdateOutcome { if update.Version() == "" || options.updateDisabled { return UpdateOutcome{UserMessage: update.UserMessage()} } err := update.Apply() if err != nil { return UpdateOutcome{Error: err} } return UpdateOutcome{Updated: true, Version: update.Version(), UserMessage: update.UserMessage()} } // Update is the handler for the update command from the command line func Update(c *cli.Context) error { log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog) if wasInstalledFromPackageManager() { packageManagerName := "a package manager" if BuiltForPackageManager != "" { packageManagerName = BuiltForPackageManager } log.Error().Msg(fmt.Sprintf("cloudflared was installed by %s. Please update using the same method.", packageManagerName)) return nil } isBeta := c.Bool("beta") if isBeta { log.Info().Msg("cloudflared is set to update to the latest beta version") } isStaging := c.Bool("staging") if isStaging { log.Info().Msg("cloudflared is set to update from staging") } isForced := c.Bool(cfdflags.Force) if isForced { log.Info().Msg("cloudflared is set to upgrade to the latest publish version regardless of the current version") } updateOutcome := loggedUpdate(log, updateOptions{ updateDisabled: false, isBeta: isBeta, isStaging: isStaging, isForced: isForced, intendedVersion: c.String("version"), }) if updateOutcome.Error != nil { return &statusError{updateOutcome.Error} } if updateOutcome.noUpdate() { log.Info().Str(LogFieldVersion, updateOutcome.Version).Msg("cloudflared is up to date") return nil } return &statusSuccess{newVersion: updateOutcome.Version} } // Checks for an update and applies it if one is available func loggedUpdate(log *zerolog.Logger, options updateOptions) UpdateOutcome { checkResult, err := CheckForUpdate(options) if err != nil { log.Err(err).Msg("update check failed") return UpdateOutcome{Error: err} } updateOutcome := applyUpdate(options, checkResult) if updateOutcome.Updated { log.Info().Str(LogFieldVersion, updateOutcome.Version).Msg("cloudflared has been updated") } if updateOutcome.Error != nil { log.Err(updateOutcome.Error).Msg("update failed to apply") } return updateOutcome } // AutoUpdater periodically checks for new version of cloudflared. type AutoUpdater struct { configurable *configurable listeners *gracenet.Net log *zerolog.Logger } // AutoUpdaterConfigurable is the attributes of AutoUpdater that can be reconfigured during runtime type configurable struct { enabled bool freq time.Duration } func NewAutoUpdater(updateDisabled bool, freq time.Duration, listeners *gracenet.Net, log *zerolog.Logger) *AutoUpdater { return &AutoUpdater{ configurable: createUpdateConfig(updateDisabled, freq, log), listeners: listeners, log: log, } } func createUpdateConfig(updateDisabled bool, freq time.Duration, log *zerolog.Logger) *configurable { if isAutoupdateEnabled(log, updateDisabled, freq) { log.Info().Dur("autoupdateFreq", freq).Msg("Autoupdate frequency is set") return &configurable{ enabled: true, freq: freq, } } else { return &configurable{ enabled: false, freq: DefaultCheckUpdateFreq, } } } // Run will perodically check for cloudflared updates, download them, and then restart the current cloudflared process // to use the new version. It delays the first update check by the configured frequency as to not attempt a // download immediately and restart after starting (in the case that there is an upgrade available). func (a *AutoUpdater) Run(ctx context.Context) error { ticker := time.NewTicker(a.configurable.freq) for { select { case <-ctx.Done(): return ctx.Err() case <-ticker.C: } updateOutcome := loggedUpdate(a.log, updateOptions{updateDisabled: !a.configurable.enabled}) if updateOutcome.Updated { buildInfo.CloudflaredVersion = updateOutcome.Version if IsSysV() { // SysV doesn't have a mechanism to keep service alive, we have to restart the process a.log.Info().Msg("Restarting service managed by SysV...") pid, err := a.listeners.StartProcess() if err != nil { a.log.Err(err).Msg("Unable to restart server automatically") return &statusError{err: err} } // stop old process after autoupdate. Otherwise we create a new process // after each update a.log.Info().Msgf("PID of the new process is %d", pid) } return &statusSuccess{newVersion: updateOutcome.Version} } else if updateOutcome.UserMessage != "" { a.log.Warn().Msg(updateOutcome.UserMessage) } } } func isAutoupdateEnabled(log *zerolog.Logger, updateDisabled bool, updateFreq time.Duration) bool { if !supportAutoUpdate(log) { return false } return !updateDisabled && updateFreq != 0 } func supportAutoUpdate(log *zerolog.Logger) bool { if runtime.GOOS == "windows" { log.Info().Msg(noUpdateOnWindowsMessage) return false } if wasInstalledFromPackageManager() { log.Info().Msg(noUpdateManagedPackageMessage) return false } if isRunningFromTerminal() { log.Info().Msg(noUpdateInShellMessage) return false } return true } func wasInstalledFromPackageManager() bool { ok, _ := config.FileExists(filepath.Join(config.DefaultUnixConfigLocation, isManagedInstallFile)) return len(BuiltForPackageManager) != 0 || ok } func isRunningFromTerminal() bool { return term.IsTerminal(int(os.Stdout.Fd())) } func IsSysV() bool { if runtime.GOOS != "linux" { return false } if _, err := os.Stat("/run/systemd/system"); err == nil { return false } return true } ================================================ FILE: cmd/cloudflared/updater/update_test.go ================================================ package updater import ( "context" "flag" "testing" "github.com/facebookgo/grace/gracenet" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" "github.com/urfave/cli/v2" "github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil" ) func init() { Init(cliutil.GetBuildInfo("TEST", "TEST")) } func TestDisabledAutoUpdater(t *testing.T) { listeners := &gracenet.Net{} log := zerolog.Nop() autoupdater := NewAutoUpdater(false, 0, listeners, &log) ctx, cancel := context.WithCancel(context.Background()) errC := make(chan error) go func() { errC <- autoupdater.Run(ctx) }() assert.False(t, autoupdater.configurable.enabled) assert.Equal(t, DefaultCheckUpdateFreq, autoupdater.configurable.freq) cancel() // Make sure that autoupdater terminates after canceling the context assert.Equal(t, context.Canceled, <-errC) } func TestCheckInWithUpdater(t *testing.T) { flagSet := flag.NewFlagSet(t.Name(), flag.PanicOnError) cliCtx := cli.NewContext(cli.NewApp(), flagSet, nil) warningChecker := StartWarningCheck(cliCtx) warning := warningChecker.getWarning() // Assuming this runs either on a release or development version, then the Worker will never have anything to tell us. assert.Empty(t, warning) } ================================================ FILE: cmd/cloudflared/updater/workers_service.go ================================================ package updater import ( "encoding/json" "errors" "fmt" "net/http" "runtime" ) // Options are the update options supported by the type Options struct { // IsBeta is for beta updates to be installed if available IsBeta bool // IsForced is to forcibly download the latest version regardless of the current version IsForced bool // RequestedVersion is the specific version to upgrade or downgrade to RequestedVersion string } // VersionResponse is the JSON response from the Workers API endpoint type VersionResponse struct { URL string `json:"url"` Version string `json:"version"` Checksum string `json:"checksum"` IsCompressed bool `json:"compressed"` UserMessage string `json:"userMessage"` ShouldUpdate bool `json:"shouldUpdate"` Error string `json:"error"` } // WorkersService implements Service. // It contains everything needed to check in with the WorkersAPI and download and apply the updates type WorkersService struct { currentVersion string url string targetPath string opts Options } // NewWorkersService creates a new updater Service object. func NewWorkersService(currentVersion, url, targetPath string, opts Options) Service { return &WorkersService{ currentVersion: currentVersion, url: url, targetPath: targetPath, opts: opts, } } // Check does a check in with the Workers API to get a new version update func (s *WorkersService) Check() (CheckResult, error) { client := &http.Client{ Timeout: clientTimeout, } req, err := http.NewRequest(http.MethodGet, s.url, nil) if err != nil { return nil, err } q := req.URL.Query() q.Add(OSKeyName, runtime.GOOS) q.Add(ArchitectureKeyName, runtime.GOARCH) q.Add(ClientVersionName, s.currentVersion) if s.opts.IsBeta { q.Add(BetaKeyName, "true") } if s.opts.RequestedVersion != "" { q.Add(VersionKeyName, s.opts.RequestedVersion) } req.URL.RawQuery = q.Encode() resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != 200 { return nil, fmt.Errorf("unable to check for update: %d", resp.StatusCode) } var v VersionResponse if err := json.NewDecoder(resp.Body).Decode(&v); err != nil { return nil, err } if v.Error != "" { return nil, errors.New(v.Error) } versionToUpdate := "" if v.ShouldUpdate { versionToUpdate = v.Version } return NewWorkersVersion(v.URL, versionToUpdate, v.Checksum, s.targetPath, v.UserMessage, v.IsCompressed), nil } ================================================ FILE: cmd/cloudflared/updater/workers_service_test.go ================================================ //go:build !windows package updater import ( "archive/tar" "bytes" "compress/gzip" "crypto/sha256" "encoding/json" "errors" "fmt" "log" "net/http" "net/http/httptest" "os" "path/filepath" "runtime" "strconv" "strings" "testing" "github.com/stretchr/testify/require" ) var testFilePath = filepath.Join(os.TempDir(), "test") func respondWithJSON(w http.ResponseWriter, v interface{}, status int) { data, _ := json.Marshal(v) w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) w.Write(data) } func respondWithData(w http.ResponseWriter, b []byte, status int) { w.Header().Set("Content-Type", "application/octet-stream") w.WriteHeader(status) w.Write(b) } const mostRecentVersion = "2021.2.5" const mostRecentBetaVersion = "2021.3.0" const knownBuggyVersion = "2020.12.0" const expectedUserMsg = "This message is expected when running a known buggy version" func updateHandler(w http.ResponseWriter, r *http.Request) { version := mostRecentVersion host := fmt.Sprintf("http://%s", r.Host) url := host + "/download" query := r.URL.Query() if query.Get(BetaKeyName) == "true" { version = mostRecentBetaVersion url = host + "/beta" } requestedVersion := query.Get(VersionKeyName) if requestedVersion != "" { version = requestedVersion url = fmt.Sprintf("%s?version=%s", url, requestedVersion) } if query.Get(ArchitectureKeyName) != runtime.GOARCH || query.Get(OSKeyName) != runtime.GOOS { respondWithJSON(w, VersionResponse{Error: "unsupported os and architecture"}, http.StatusBadRequest) return } h := sha256.New() fmt.Fprint(h, version) checksum := fmt.Sprintf("%x", h.Sum(nil)) var userMessage = "" if query.Get(ClientVersionName) == knownBuggyVersion { userMessage = expectedUserMsg } shouldUpdate := requestedVersion != "" || IsNewerVersion(query.Get(ClientVersionName), version) v := VersionResponse{ URL: url, Version: version, Checksum: checksum, UserMessage: userMessage, ShouldUpdate: shouldUpdate, } respondWithJSON(w, v, http.StatusOK) } func gzipUpdateHandler(w http.ResponseWriter, r *http.Request) { log.Println("got a request!") version := "2020.09.02" h := sha256.New() fmt.Fprint(h, version) checksum := fmt.Sprintf("%x", h.Sum(nil)) url := fmt.Sprintf("http://%s/gzip-download.tgz", r.Host) v := VersionResponse{URL: url, Version: version, Checksum: checksum, ShouldUpdate: true} respondWithJSON(w, v, http.StatusOK) } func compressedDownloadHandler(w http.ResponseWriter, r *http.Request) { version := "2020.09.02" buf := new(bytes.Buffer) gw := gzip.NewWriter(buf) tw := tar.NewWriter(gw) header := &tar.Header{ Size: int64(len(version)), Name: "download", } tw.WriteHeader(header) tw.Write([]byte(version)) tw.Close() gw.Close() respondWithData(w, buf.Bytes(), http.StatusOK) } func downloadHandler(w http.ResponseWriter, r *http.Request) { version := mostRecentVersion requestedVersion := r.URL.Query().Get(VersionKeyName) if requestedVersion != "" { version = requestedVersion } respondWithData(w, []byte(version), http.StatusOK) } func betaHandler(w http.ResponseWriter, r *http.Request) { respondWithData(w, []byte(mostRecentBetaVersion), http.StatusOK) } func failureHandler(w http.ResponseWriter, r *http.Request) { respondWithJSON(w, VersionResponse{Error: "unsupported os and architecture"}, http.StatusBadRequest) } func IsNewerVersion(current string, check string) bool { if current == "" || check == "" { return false } if strings.Contains(strings.ToLower(current), "dev") { return false // dev builds shouldn't update } cMajor, cMinor, cPatch, err := SemanticParts(current) if err != nil { return false } nMajor, nMinor, nPatch, err := SemanticParts(check) if err != nil { return false } if nMajor > cMajor { return true } if nMajor == cMajor && nMinor > cMinor { return true } if nMajor == cMajor && nMinor == cMinor && nPatch > cPatch { return true } return false } func SemanticParts(version string) (major int, minor int, patch int, err error) { major = 0 minor = 0 patch = 0 parts := strings.Split(version, ".") if len(parts) != 3 { err = errors.New("invalid version") return } major, err = strconv.Atoi(parts[0]) if err != nil { return } minor, err = strconv.Atoi(parts[1]) if err != nil { return } patch, err = strconv.Atoi(parts[2]) if err != nil { return } return } func createServer() *httptest.Server { mux := http.NewServeMux() mux.HandleFunc("/updater", updateHandler) mux.HandleFunc("/download", downloadHandler) mux.HandleFunc("/beta", betaHandler) mux.HandleFunc("/fail", failureHandler) mux.HandleFunc("/compressed", gzipUpdateHandler) mux.HandleFunc("/gzip-download.tgz", compressedDownloadHandler) return httptest.NewServer(mux) } func createTestFile(t *testing.T, path string) { f, err := os.Create(path) require.NoError(t, err) fmt.Fprint(f, "2020.08.04") f.Close() } func TestUpdateService(t *testing.T) { ts := createServer() defer ts.Close() createTestFile(t, testFilePath) defer os.Remove(testFilePath) log.Println("server url: ", ts.URL) s := NewWorkersService("2020.8.2", fmt.Sprintf("%s/updater", ts.URL), testFilePath, Options{}) v, err := s.Check() require.NoError(t, err) require.Equal(t, v.Version(), mostRecentVersion) require.NoError(t, v.Apply()) dat, err := os.ReadFile(testFilePath) require.NoError(t, err) require.Equal(t, string(dat), mostRecentVersion) } func TestBetaUpdateService(t *testing.T) { ts := createServer() defer ts.Close() createTestFile(t, testFilePath) defer os.Remove(testFilePath) s := NewWorkersService("2020.8.2", fmt.Sprintf("%s/updater", ts.URL), testFilePath, Options{IsBeta: true}) v, err := s.Check() require.NoError(t, err) require.Equal(t, v.Version(), mostRecentBetaVersion) require.NoError(t, v.Apply()) dat, err := os.ReadFile(testFilePath) require.NoError(t, err) require.Equal(t, string(dat), mostRecentBetaVersion) } func TestFailUpdateService(t *testing.T) { ts := createServer() defer ts.Close() createTestFile(t, testFilePath) defer os.Remove(testFilePath) s := NewWorkersService("2020.8.2", fmt.Sprintf("%s/fail", ts.URL), testFilePath, Options{}) v, err := s.Check() require.Error(t, err) require.Nil(t, v) } func TestNoUpdateService(t *testing.T) { ts := createServer() defer ts.Close() createTestFile(t, testFilePath) defer os.Remove(testFilePath) s := NewWorkersService(mostRecentVersion, fmt.Sprintf("%s/updater", ts.URL), testFilePath, Options{}) v, err := s.Check() require.NoError(t, err) require.NotNil(t, v) require.Empty(t, v.Version()) } func TestForcedUpdateService(t *testing.T) { ts := createServer() defer ts.Close() createTestFile(t, testFilePath) defer os.Remove(testFilePath) s := NewWorkersService("2020.8.5", fmt.Sprintf("%s/updater", ts.URL), testFilePath, Options{IsForced: true}) v, err := s.Check() require.NoError(t, err) require.Equal(t, v.Version(), mostRecentVersion) require.NoError(t, v.Apply()) dat, err := os.ReadFile(testFilePath) require.NoError(t, err) require.Equal(t, string(dat), mostRecentVersion) } func TestUpdateSpecificVersionService(t *testing.T) { ts := createServer() defer ts.Close() createTestFile(t, testFilePath) defer os.Remove(testFilePath) reqVersion := "2020.9.1" s := NewWorkersService("2020.8.2", fmt.Sprintf("%s/updater", ts.URL), testFilePath, Options{RequestedVersion: reqVersion}) v, err := s.Check() require.NoError(t, err) require.Equal(t, reqVersion, v.Version()) require.NoError(t, v.Apply()) dat, err := os.ReadFile(testFilePath) require.NoError(t, err) require.Equal(t, reqVersion, string(dat)) } func TestCompressedUpdateService(t *testing.T) { ts := createServer() defer ts.Close() createTestFile(t, testFilePath) defer os.Remove(testFilePath) s := NewWorkersService("2020.8.2", fmt.Sprintf("%s/compressed", ts.URL), testFilePath, Options{}) v, err := s.Check() require.NoError(t, err) require.Equal(t, "2020.09.02", v.Version()) require.NoError(t, v.Apply()) dat, err := os.ReadFile(testFilePath) require.NoError(t, err) require.Equal(t, "2020.09.02", string(dat)) } func TestUpdateWhenRunningKnownBuggyVersion(t *testing.T) { ts := createServer() defer ts.Close() createTestFile(t, testFilePath) defer os.Remove(testFilePath) s := NewWorkersService(knownBuggyVersion, fmt.Sprintf("%s/updater", ts.URL), testFilePath, Options{}) v, err := s.Check() require.NoError(t, err) require.Equal(t, v.Version(), mostRecentVersion) require.Equal(t, v.UserMessage(), expectedUserMsg) } ================================================ FILE: cmd/cloudflared/updater/workers_update.go ================================================ package updater import ( "archive/tar" "compress/gzip" "errors" "fmt" "io" "net/http" "net/url" "os" "os/exec" "path" "path/filepath" "runtime" "text/template" "time" "github.com/getsentry/sentry-go" "github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil" ) const ( clientTimeout = time.Second * 60 // stop the service // rename cloudflared.exe to cloudflared.exe.old // rename cloudflared.exe.new to cloudflared.exe // delete cloudflared.exe.old // start the service // exit with code 0 if we've reached this point indicating success. windowsUpdateCommandTemplate = `sc stop cloudflared >nul 2>&1 del "{{.OldPath}}" rename "{{.TargetPath}}" {{.OldName}} rename "{{.NewPath}}" {{.BinaryName}} sc start cloudflared >nul 2>&1 exit /b 0` batchFileName = "cfd_update.bat" ) // Prepare some data to insert into the template. type batchData struct { TargetPath string OldName string NewPath string OldPath string BinaryName string BatchName string } // WorkersVersion implements the Version interface. // It contains everything needed to perform a version upgrade type WorkersVersion struct { downloadURL string checksum string version string targetPath string isCompressed bool userMessage string } // NewWorkersVersion creates a new Version object. This is normally created by a WorkersService JSON checkin response // url is where to download the file // version is the version of this update // checksum is the expected checksum of the downloaded file // target path is where the file should be replace. Normally this the running cloudflared's path // userMessage is a possible message to convey back to the user after having checked in with the Updater Service // isCompressed tells whether the asset to update cloudflared is compressed or not func NewWorkersVersion(url, version, checksum, targetPath, userMessage string, isCompressed bool) CheckResult { return &WorkersVersion{ downloadURL: url, version: version, checksum: checksum, targetPath: targetPath, isCompressed: isCompressed, userMessage: userMessage, } } // Apply does the actual verification and update logic. // This includes signature and checksum validation, // replacing the binary, etc func (v *WorkersVersion) Apply() error { newFilePath := fmt.Sprintf("%s.new", v.targetPath) os.Remove(newFilePath) //remove any failed updates before download // download the file if err := download(v.downloadURL, newFilePath, v.isCompressed); err != nil { return err } downloadSum, err := cliutil.FileChecksum(newFilePath) if err != nil { return err } // Check that the file downloaded matches what is expected. if v.checksum != downloadSum { return errors.New("checksum validation failed") } // Check if the currently running version has the same checksum if downloadSum == buildInfo.Checksum { // Currently running binary matches the downloaded binary so we have no reason to update. This is // typically unexpected, as such we emit a sentry event. localHub := sentry.CurrentHub().Clone() err := errors.New("checksum validation matches currently running process") localHub.CaptureException(err) // Make sure to cleanup the new downloaded file since we aren't upgrading versions. os.Remove(newFilePath) return err } oldFilePath := fmt.Sprintf("%s.old", v.targetPath) // Windows requires more effort to self update, especially when it is running as a service: // you have to stop the service (if running as one) in order to move/rename the binary // but now the binary isn't running though, so an external process // has to move the old binary out and the new one in then start the service // the easiest way to do this is with a batch file (or with a DLL, but that gets ugly for a cross compiled binary like cloudflared) // a batch file isn't ideal, but it is the simplest path forward for the constraints Windows creates if runtime.GOOS == "windows" { if err := writeBatchFile(v.targetPath, newFilePath, oldFilePath); err != nil { return err } rootDir := filepath.Dir(v.targetPath) batchPath := filepath.Join(rootDir, batchFileName) return runWindowsBatch(batchPath) } // now move the current file out, move the new file in and delete the old file if err := os.Rename(v.targetPath, oldFilePath); err != nil { return err } if err := os.Rename(newFilePath, v.targetPath); err != nil { //attempt rollback _ = os.Rename(oldFilePath, v.targetPath) return err } os.Remove(oldFilePath) return nil } // String returns the version number of this update/release (e.g. 2020.08.05) func (v *WorkersVersion) Version() string { return v.version } // String returns a possible message to convey back to user after having checked in with the Updater Service. E.g. // it can warn about the need to update the version currently running. func (v *WorkersVersion) UserMessage() string { return v.userMessage } // download the file from the link in the json func download(url, filepath string, isCompressed bool) error { client := &http.Client{ Timeout: clientTimeout, } resp, err := client.Get(url) if err != nil { return err } defer resp.Body.Close() var r io.Reader r = resp.Body // compressed macos binary, need to decompress if isCompressed || isCompressedFile(url) { // first the gzip reader gr, err := gzip.NewReader(resp.Body) if err != nil { return err } defer gr.Close() // now the tar tr := tar.NewReader(gr) // advance the reader pass the header, which will be the single binary file _, _ = tr.Next() r = tr } out, err := os.OpenFile(filepath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755) if err != nil { return err } defer out.Close() _, err = io.Copy(out, r) return err } // isCompressedFile is a really simple file extension check to see if this is a macos tar and gzipped func isCompressedFile(urlstring string) bool { if path.Ext(urlstring) == ".tgz" { return true } u, err := url.Parse(urlstring) if err != nil { return false } return path.Ext(u.Path) == ".tgz" } // writeBatchFile writes a batch file out to disk // see the dicussion on why it has to be done this way func writeBatchFile(targetPath string, newPath string, oldPath string) error { batchFilePath := filepath.Join(filepath.Dir(targetPath), batchFileName) os.Remove(batchFilePath) //remove any failed updates before download f, err := os.Create(batchFilePath) if err != nil { return err } defer f.Close() cfdName := filepath.Base(targetPath) oldName := filepath.Base(oldPath) data := batchData{ TargetPath: targetPath, OldName: oldName, NewPath: newPath, OldPath: oldPath, BinaryName: cfdName, BatchName: batchFileName, } t, err := template.New("batch").Parse(windowsUpdateCommandTemplate) if err != nil { return err } return t.Execute(f, data) } // run each OS command for windows func runWindowsBatch(batchFile string) error { defer os.Remove(batchFile) cmd := exec.Command("cmd", "/C", batchFile) _, err := cmd.Output() // Remove the batch file we created. Don't let this interfere with the error // we report. if err != nil { if exitError, ok := err.(*exec.ExitError); ok { return fmt.Errorf("Error during update : %s;", string(exitError.Stderr)) } } return err } ================================================ FILE: cmd/cloudflared/windows_service.go ================================================ //go:build windows package main // Copypasta from the example files: // https://github.com/golang/sys/blob/master/windows/svc/example import ( "fmt" "os" "syscall" "time" "unsafe" "github.com/pkg/errors" "github.com/urfave/cli/v2" "golang.org/x/sys/windows" "golang.org/x/sys/windows/svc" "golang.org/x/sys/windows/svc/eventlog" "golang.org/x/sys/windows/svc/mgr" "github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil" "github.com/cloudflare/cloudflared/logger" ) const ( windowsServiceName = "Cloudflared" windowsServiceDescription = "Cloudflared agent" windowsServiceUrl = "https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/configure-tunnels/local-management/as-a-service/windows/" recoverActionDelay = time.Second * 20 failureCountResetPeriod = time.Hour * 24 // not defined in golang.org/x/sys/windows package // https://msdn.microsoft.com/en-us/library/windows/desktop/ms681988(v=vs.85).aspx serviceConfigFailureActionsFlag = 4 // ERROR_FAILED_SERVICE_CONTROLLER_CONNECT // https://docs.microsoft.com/en-us/windows/desktop/debug/system-error-codes--1000-1299- serviceControllerConnectionFailure = 1063 LogFieldWindowsServiceName = "windowsServiceName" ) func runApp(app *cli.App, graceShutdownC chan struct{}) { app.Commands = append(app.Commands, &cli.Command{ Name: "service", Usage: "Manages the cloudflared Windows service", Subcommands: []*cli.Command{ { Name: "install", Usage: "Install cloudflared as a Windows service", Action: cliutil.ConfiguredAction(installWindowsService), }, { Name: "uninstall", Usage: "Uninstall the cloudflared service", Action: cliutil.ConfiguredAction(uninstallWindowsService), }, }, }) // `IsAnInteractiveSession()` isn't exactly equivalent to "should the // process run as a normal EXE?" There are legitimate non-service cases, // like running cloudflared in a GCP startup script, for which // `IsAnInteractiveSession()` returns false. For more context, see: // https://github.com/judwhite/go-svc/issues/6 // It seems that the "correct way" to check "is this a normal EXE?" is: // 1. attempt to connect to the Service Control Manager // 2. get ERROR_FAILED_SERVICE_CONTROLLER_CONNECT // This involves actually trying to start the service. log := logger.Create(nil) isIntSess, err := svc.IsAnInteractiveSession() if err != nil { log.Fatal().Err(err).Msg("failed to determine if we are running in an interactive session") } if isIntSess { app.Run(os.Args) return } // Run executes service name by calling windowsService which is a Handler // interface that implements Execute method. // It will set service status to stop after Execute returns err = svc.Run(windowsServiceName, &windowsService{app: app, graceShutdownC: graceShutdownC}) if err != nil { if errno, ok := err.(syscall.Errno); ok && int(errno) == serviceControllerConnectionFailure { // Hack: assume this is a false negative from the IsAnInteractiveSession() check above. // Run the app in "interactive" mode anyway. app.Run(os.Args) return } log.Fatal().Err(err).Msgf("%s service failed", windowsServiceName) } } type windowsService struct { app *cli.App graceShutdownC chan struct{} } // Execute is called by the service manager when service starts, the state // of the service will be set to Stopped when this function returns. func (s *windowsService) Execute(serviceArgs []string, r <-chan svc.ChangeRequest, statusChan chan<- svc.Status) (ssec bool, errno uint32) { log := logger.Create(nil) elog, err := eventlog.Open(windowsServiceName) if err != nil { log.Err(err).Msgf("Cannot open event log for %s", windowsServiceName) return } defer elog.Close() elog.Info(1, fmt.Sprintf("%s service starting", windowsServiceName)) defer func() { elog.Info(1, fmt.Sprintf("%s service stopped", windowsServiceName)) }() // the arguments passed here are only meaningful if they were manually // specified by the user, e.g. using the Services console or `sc start`. // https://docs.microsoft.com/en-us/windows/desktop/services/service-entry-point // https://stackoverflow.com/a/6235139 var args []string if len(serviceArgs) > 1 { args = serviceArgs } else { // fall back to the arguments from ImagePath (or, as sc calls it, binPath) args = os.Args } elog.Info(1, fmt.Sprintf("%s service arguments: %v", windowsServiceName, args)) statusChan <- svc.Status{State: svc.StartPending} errC := make(chan error) go func() { errC <- s.app.Run(args) }() statusChan <- svc.Status{State: svc.Running, Accepts: svc.AcceptStop | svc.AcceptShutdown} for { select { case c := <-r: switch c.Cmd { case svc.Interrogate: statusChan <- c.CurrentStatus case svc.Stop, svc.Shutdown: if s.graceShutdownC != nil { // start graceful shutdown elog.Info(1, "cloudflared starting graceful shutdown") close(s.graceShutdownC) s.graceShutdownC = nil statusChan <- svc.Status{State: svc.StopPending} continue } // repeated attempts at graceful shutdown forces immediate stop elog.Info(1, "cloudflared terminating immediately") statusChan <- svc.Status{State: svc.StopPending} return false, 0 default: elog.Error(1, fmt.Sprintf("unexpected control request #%d", c)) } case err := <-errC: if err != nil { elog.Error(1, fmt.Sprintf("cloudflared terminated with error %v", err)) ssec = true errno = 1 } else { elog.Info(1, "cloudflared terminated without error") errno = 0 } return } } } func installWindowsService(c *cli.Context) error { zeroLogger := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog) zeroLogger.Info().Msg("Installing cloudflared Windows service") exepath, err := os.Executable() if err != nil { return errors.Wrap(err, "Cannot find path name that start the process") } m, err := mgr.Connect() if err != nil { return errors.Wrap(err, "Cannot establish a connection to the service control manager") } defer m.Disconnect() s, err := m.OpenService(windowsServiceName) log := zeroLogger.With().Str(LogFieldWindowsServiceName, windowsServiceName).Logger() if err == nil { s.Close() return errors.New(serviceAlreadyExistsWarn(windowsServiceName)) } extraArgs, err := getServiceExtraArgsFromCliArgs(c, &log) if err != nil { errMsg := "Unable to determine extra arguments for windows service" log.Err(err).Msg(errMsg) return errors.Wrap(err, errMsg) } config := mgr.Config{StartType: mgr.StartAutomatic, DisplayName: windowsServiceDescription} s, err = m.CreateService(windowsServiceName, exepath, config, extraArgs...) if err != nil { return errors.Wrap(err, "Cannot install service") } defer s.Close() log.Info().Msg("cloudflared agent service is installed") err = eventlog.InstallAsEventCreate(windowsServiceName, eventlog.Error|eventlog.Warning|eventlog.Info) if err != nil { s.Delete() return errors.Wrap(err, "Cannot install event logger") } err = configRecoveryOption(s.Handle) if err != nil { log.Err(err).Msg("Cannot set service recovery actions") log.Info().Msgf("See %s to manually configure service recovery actions", windowsServiceUrl) } err = s.Start() if err == nil { log.Info().Msg("Agent service for cloudflared installed successfully") } return err } func uninstallWindowsService(c *cli.Context) error { log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog). With(). Str(LogFieldWindowsServiceName, windowsServiceName).Logger() log.Info().Msg("Uninstalling cloudflared agent service") m, err := mgr.Connect() if err != nil { return errors.Wrap(err, "Cannot establish a connection to the service control manager") } defer m.Disconnect() s, err := m.OpenService(windowsServiceName) if err != nil { return fmt.Errorf("agent service %s is not installed, so it could not be uninstalled", windowsServiceName) } defer s.Close() if status, err := s.Query(); err == nil && status.State == svc.Running { log.Info().Msg("Stopping cloudflared agent service") if _, err := s.Control(svc.Stop); err != nil { log.Info().Err(err).Msg("Failed to stop cloudflared agent service, you may need to stop it manually to complete uninstall.") } } err = s.Delete() if err != nil { return errors.Wrap(err, "Cannot delete agent service") } log.Info().Msg("Agent service for cloudflared was uninstalled successfully") err = eventlog.Remove(windowsServiceName) if err != nil { return errors.Wrap(err, "Cannot remove event logger") } return nil } // defined in https://msdn.microsoft.com/en-us/library/windows/desktop/ms685126(v=vs.85).aspx type scAction int // https://msdn.microsoft.com/en-us/library/windows/desktop/ms685126(v=vs.85).aspx const ( scActionNone scAction = iota scActionRestart scActionReboot scActionRunCommand ) // defined in https://msdn.microsoft.com/en-us/library/windows/desktop/ms685939(v=vs.85).aspx type serviceFailureActions struct { // time to wait to reset the failure count to zero if there are no failures in seconds resetPeriod uint32 rebootMsg *uint16 command *uint16 // If failure count is greater than actionCount, the service controller repeats // the last action in actions actionCount uint32 actions uintptr } // https://msdn.microsoft.com/en-us/library/windows/desktop/ms685937(v=vs.85).aspx // Not supported in Windows Server 2003 and Windows XP type serviceFailureActionsFlag struct { // enableActionsForStopsWithErr is of type BOOL, which is declared as // typedef int BOOL in C enableActionsForStopsWithErr int } type recoveryAction struct { recoveryType uint32 // The time to wait before performing the specified action, in milliseconds delay uint32 } // until https://github.com/golang/go/issues/23239 is release, we will need to // configure through ChangeServiceConfig2 func configRecoveryOption(handle windows.Handle) error { actions := []recoveryAction{ {recoveryType: uint32(scActionRestart), delay: uint32(recoverActionDelay / time.Millisecond)}, } serviceRecoveryActions := serviceFailureActions{ resetPeriod: uint32(failureCountResetPeriod / time.Second), actionCount: uint32(len(actions)), actions: uintptr(unsafe.Pointer(&actions[0])), } if err := windows.ChangeServiceConfig2(handle, windows.SERVICE_CONFIG_FAILURE_ACTIONS, (*byte)(unsafe.Pointer(&serviceRecoveryActions))); err != nil { return err } serviceFailureActionsFlag := serviceFailureActionsFlag{enableActionsForStopsWithErr: 1} return windows.ChangeServiceConfig2(handle, serviceConfigFailureActionsFlag, (*byte)(unsafe.Pointer(&serviceFailureActionsFlag))) } ================================================ FILE: component-tests/.gitignore ================================================ __pycache__ .pytest_cache ================================================ FILE: component-tests/README.md ================================================ # Requirements 1. Python 3.10 or later with packages in the given `requirements.txt` - E.g. with venv: - `python3 -m venv ./.venv` - `source ./.venv/bin/activate` - `python3 -m pip install -r requirements.txt` 2. Create a config yaml file, for example: ``` cloudflared_binary: "cloudflared" tunnel: "3d539f97-cd3a-4d8e-c33b-65e9099c7a8d" credentials_file: "/Users/tunnel/.cloudflared/3d539f97-cd3a-4d8e-c33b-65e9099c7a8d.json" origincert: "/Users/tunnel/.cloudflared/cert.pem" ingress: - hostname: named-tunnel-component-tests.example.com service: hello_world - service: http_status:404 ``` 3. Route hostname to the tunnel. For the example config above, we can do that via ``` cloudflared tunnel route dns 3d539f97-cd3a-4d8e-c33b-65e9099c7a8d named-tunnel-component-tests.example.com ``` 4. Turn on linter If you are using Visual Studio, follow https://code.visualstudio.com/docs/python/linting to turn on linter. 5. Turn on formatter If you are using Visual Studio, follow https://code.visualstudio.com/docs/python/editing#_formatting to turn on formatter and https://marketplace.visualstudio.com/items?itemName=cbrevik.toggle-format-on-save to turn on format on save. 6. If you have cloudflared running as a service on your machine, you can either stop the service or ignore the service tests via `--ignore test_service.py` # How to run Specify path to config file via env var `COMPONENT_TESTS_CONFIG`. This is required. ## All tests Run `pytest` inside this(component-tests) folder ## Specific files Run `pytest .py .py` ## Specific tests Run `pytest file.py -k -k ` ## Live Logging Running with `-o log_cli=true` outputs logging to CLI as the tests are. By default the log level is WARN. `--log-cli-level` control logging level. For example, to log at info level, run `pytest -o log_cli=true --log-cli-level=INFO`. See https://docs.pytest.org/en/latest/logging.html#live-logs for more documentation on logging. ================================================ FILE: component-tests/cli.py ================================================ import json import subprocess from time import sleep from constants import MANAGEMENT_HOST_NAME from setup import get_config_from_file from util import get_tunnel_connector_id SINGLE_CASE_TIMEOUT = 600 class CloudflaredCli: def __init__(self, config, config_path, logger): self.basecmd = [config.cloudflared_binary, "tunnel"] if config_path is not None: self.basecmd += ["--config", str(config_path)] origincert = get_config_from_file()["origincert"] if origincert: self.basecmd += ["--origincert", origincert] self.logger = logger def _run_command(self, subcmd, subcmd_name, needs_to_pass=True): cmd = self.basecmd + subcmd # timeout limits the time a subprocess can run. This is useful to guard against running a tunnel when # command/args are in wrong order. result = run_subprocess(cmd, subcmd_name, self.logger, check=needs_to_pass, capture_output=True, timeout=15) return result def list_tunnels(self): cmd_args = ["list", "--output", "json"] listed = self._run_command(cmd_args, "list") return json.loads(listed.stdout) def get_management_token(self, config, config_path, resource): basecmd = [config.cloudflared_binary] if config_path is not None: basecmd += ["--config", str(config_path)] origincert = get_config_from_file()["origincert"] if origincert: basecmd += ["--origincert", origincert] cmd_args = ["management", "token", "--resource", resource, config.get_tunnel_id()] cmd = basecmd + cmd_args result = run_subprocess(cmd, "token", self.logger, check=True, capture_output=True, timeout=15) return json.loads(result.stdout.decode("utf-8").strip())["token"] def get_tail_token(self, config, config_path): """ Get management token using the 'tail token' command. Returns a token scoped for 'logs' resource. """ basecmd = [config.cloudflared_binary] if config_path is not None: basecmd += ["--config", str(config_path)] origincert = get_config_from_file()["origincert"] if origincert: basecmd += ["--origincert", origincert] cmd_args = ["tail", "token", config.get_tunnel_id()] cmd = basecmd + cmd_args result = run_subprocess(cmd, "tail-token", self.logger, check=True, capture_output=True, timeout=15) return json.loads(result.stdout.decode("utf-8").strip())["token"] def get_management_url(self, path, config, config_path, resource): access_jwt = self.get_management_token(config, config_path, resource) connector_id = get_tunnel_connector_id() return f"https://{MANAGEMENT_HOST_NAME}/{path}?connector_id={connector_id}&access_token={access_jwt}" def get_management_wsurl(self, path, config, config_path, resource): access_jwt = self.get_management_token(config, config_path, resource) connector_id = get_tunnel_connector_id() return f"wss://{MANAGEMENT_HOST_NAME}/{path}?connector_id={connector_id}&access_token={access_jwt}" def get_connector_id(self, config): op = self.get_tunnel_info(config.get_tunnel_id()) connectors = [] for conn in op["conns"]: connectors.append(conn["id"]) return connectors def get_tunnel_info(self, tunnel_id): info = self._run_command(["info", "--output", "json", tunnel_id], "info") return json.loads(info.stdout) def __enter__(self): self.basecmd += ["run"] self.process = subprocess.Popen(self.basecmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) self.logger.info(f"Run cmd {self.basecmd}") return self.process def __exit__(self, exc_type, exc_value, exc_traceback): terminate_gracefully(self.process, self.logger, self.basecmd) self.logger.debug(f"{self.basecmd} logs: {self.process.stderr.read()}") def terminate_gracefully(process, logger, cmd): process.terminate() process_terminated = wait_for_terminate(process) if not process_terminated: process.kill() logger.warning(f"{cmd}: cloudflared did not terminate within wait period. Killing process. logs: \ stdout: {process.stdout.read()}, stderr: {process.stderr.read()}") def wait_for_terminate(opened_subprocess, attempts=10, poll_interval=1): """ wait_for_terminate polls the opened_subprocess every x seconds for a given number of attempts. It returns true if the subprocess was terminated and false if it didn't. """ for _ in range(attempts): if _is_process_stopped(opened_subprocess): return True sleep(poll_interval) return False def _is_process_stopped(process): return process.poll() is not None def cert_path(): return get_config_from_file()["origincert"] class SubprocessError(Exception): def __init__(self, program, exit_code, cause): self.program = program self.exit_code = exit_code self.cause = cause def run_subprocess(cmd, cmd_name, logger, timeout=SINGLE_CASE_TIMEOUT, **kargs): kargs["timeout"] = timeout try: result = subprocess.run(cmd, **kargs) logger.debug(f"{cmd} log: {result.stdout}", extra={"cmd": cmd_name}) return result except subprocess.CalledProcessError as e: err = f"{cmd} return exit code {e.returncode}, stderr" + e.stderr.decode("utf-8") logger.error(err, extra={"cmd": cmd_name, "return_code": e.returncode}) raise SubprocessError(cmd[0], e.returncode, e) except subprocess.TimeoutExpired as e: err = f"{cmd} timeout after {e.timeout} seconds, stdout: {e.stdout}, stderr: {e.stderr}" logger.error(err, extra={"cmd": cmd_name, "return_code": "timeout"}) raise e ================================================ FILE: component-tests/config.py ================================================ #!/usr/bin/env python import copy import json import base64 from dataclasses import dataclass, InitVar from constants import METRICS_PORT # frozen=True raises exception when assigning to fields. This emulates immutability @dataclass(frozen=True) class BaseConfig: cloudflared_binary: str no_autoupdate: bool = True metrics: str = f'localhost:{METRICS_PORT}' def merge_config(self, additional): config = copy.copy(additional) config['no-autoupdate'] = self.no_autoupdate config['metrics'] = self.metrics return config @dataclass(frozen=True) class NamedTunnelBaseConfig(BaseConfig): # The attributes of the parent class are ordered before attributes in this class, # so we have to use default values here and check if they are set in __post_init__ tunnel: str = None credentials_file: str = None ingress: list = None hostname: str = None def __post_init__(self): if self.tunnel is None: raise TypeError("Field tunnel is not set") if self.credentials_file is None: raise TypeError("Field credentials_file is not set") if self.ingress is None: raise TypeError("Field ingress is not set") def merge_config(self, additional): config = super(NamedTunnelBaseConfig, self).merge_config(additional) if 'tunnel' not in config: config['tunnel'] = self.tunnel if 'credentials-file' not in config: config['credentials-file'] = self.credentials_file # In some cases we want to override default ingress, such as in config tests if 'ingress' not in config: config['ingress'] = self.ingress return config @dataclass(frozen=True) class NamedTunnelConfig(NamedTunnelBaseConfig): full_config: dict = None additional_config: InitVar[dict] = {} def __post_init__(self, additional_config): # Cannot call set self.full_config because the class is frozen, instead, we can use __setattr__ # https://docs.python.org/3/library/dataclasses.html#frozen-instances object.__setattr__(self, 'full_config', self.merge_config(additional_config)) def get_url(self): return "https://" + self.hostname def base_config(self): config = self.full_config.copy() # removes the tunnel reference del(config["tunnel"]) del(config["credentials-file"]) return config def get_tunnel_id(self): return self.full_config["tunnel"] def get_token(self): creds = self.get_credentials_json() token_dict = {"a": creds["AccountTag"], "t": creds["TunnelID"], "s": creds["TunnelSecret"]} token_json_str = json.dumps(token_dict) return base64.b64encode(token_json_str.encode('utf-8')) def get_credentials_json(self): with open(self.credentials_file) as json_file: return json.load(json_file) @dataclass(frozen=True) class QuickTunnelConfig(BaseConfig): full_config: dict = None additional_config: InitVar[dict] = {} def __post_init__(self, additional_config): # Cannot call set self.full_config because the class is frozen, instead, we can use __setattr__ # https://docs.python.org/3/library/dataclasses.html#frozen-instances object.__setattr__(self, 'full_config', self.merge_config(additional_config)) ================================================ FILE: component-tests/config.yaml ================================================ cloudflared_binary: "cloudflared" tunnel: "ae21a96c-24d1-4ce8-a6ba-962cba5976d3" credentials_file: "/Users/sudarsan/.cloudflared/ae21a96c-24d1-4ce8-a6ba-962cba5976d3.json" origincert: "/Users/sudarsan/.cloudflared/cert.pem" ingress: - hostname: named-tunnel-component-tests.example.com service: hello_world - service: http_status:404 ================================================ FILE: component-tests/conftest.py ================================================ import os from enum import Enum, auto from time import sleep import pytest import yaml from config import NamedTunnelConfig, QuickTunnelConfig from constants import BACKOFF_SECS from util import LOGGER class CfdModes(Enum): NAMED = auto() QUICK = auto() @pytest.fixture(scope="session") def component_tests_config(): config_file = os.getenv("COMPONENT_TESTS_CONFIG") if config_file is None: raise Exception( "Need to provide path to config file in COMPONENT_TESTS_CONFIG") with open(config_file, 'r') as stream: config = yaml.safe_load(stream) LOGGER.info(f"component tests base config {config}") def _component_tests_config(additional_config={}, cfd_mode=CfdModes.NAMED, provide_ingress=True): # Allows the ingress rules to be omitted from the provided config ingress = [] if provide_ingress: ingress = config['ingress'] # Provide the hostname to allow routing to the tunnel even if the ingress rule isn't defined in the config hostname = config['ingress'][0]['hostname'] if cfd_mode is CfdModes.NAMED: return NamedTunnelConfig(additional_config=additional_config, cloudflared_binary=config['cloudflared_binary'], tunnel=config['tunnel'], credentials_file=config['credentials_file'], ingress=ingress, hostname=hostname) elif cfd_mode is CfdModes.QUICK: return QuickTunnelConfig(additional_config=additional_config, cloudflared_binary=config['cloudflared_binary']) else: raise Exception(f"Unknown cloudflared mode {cfd_mode}") return _component_tests_config # This fixture is automatically called before each tests to make sure the previous cloudflared has been shutdown @pytest.fixture(autouse=True) def wait_previous_cloudflared(): sleep(BACKOFF_SECS) ================================================ FILE: component-tests/constants.py ================================================ METRICS_PORT = 51000 MAX_RETRIES = 5 BACKOFF_SECS = 7 MAX_LOG_LINES = 50 MANAGEMENT_HOST_NAME = "management.argotunnel.com" def protocols(): return ["http2", "quic"] ================================================ FILE: component-tests/requirements.txt ================================================ cloudflare==2.14.3 flaky==3.7.0 pytest==7.3.1 pytest-asyncio==0.21.0 pyyaml==6.0.1 requests==2.28.2 retrying==1.3.4 websockets==11.0.1 ================================================ FILE: component-tests/setup.py ================================================ #!/usr/bin/env python import argparse import base64 import json import os import subprocess import uuid import CloudFlare import yaml from retrying import retry from constants import MAX_RETRIES, BACKOFF_SECS from util import LOGGER def get_config_from_env(): config_content = base64.b64decode(get_env("COMPONENT_TESTS_CONFIG_CONTENT")).decode('utf-8') return yaml.safe_load(config_content) def get_config_from_file(): config_path = get_env("COMPONENT_TESTS_CONFIG") with open(config_path, 'r') as infile: return yaml.safe_load(infile) def persist_config(config): config_path = get_env("COMPONENT_TESTS_CONFIG") with open(config_path, 'w') as outfile: yaml.safe_dump(config, outfile) def persist_origin_cert(config): origincert = get_env("COMPONENT_TESTS_ORIGINCERT") path = config["origincert"] with open(path, 'w') as outfile: outfile.write(origincert) return path @retry(stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000) def create_tunnel(config, origincert_path, random_uuid): # Delete any previous existing credentials file. If the agent keeps files around (that's the case in Windows) then # cloudflared tunnel create will refuse to create the tunnel because it does not want to overwrite credentials # files. credentials_path = config["credentials_file"] try: os.remove(credentials_path) except OSError: pass tunnel_name = "cfd_component_test-" + random_uuid create_cmd = [config["cloudflared_binary"], "tunnel", "--origincert", origincert_path, "create", "--credentials-file", credentials_path, tunnel_name] LOGGER.info(f"Creating tunnel with {create_cmd}") subprocess.run(create_cmd, check=True) list_cmd = [config["cloudflared_binary"], "tunnel", "--origincert", origincert_path, "list", "--name", tunnel_name, "--output", "json"] LOGGER.info(f"Listing tunnel with {list_cmd}") cloudflared = subprocess.run(list_cmd, check=True, capture_output=True) return json.loads(cloudflared.stdout)[0]["id"] @retry(stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000) def delete_tunnel(config): credentials_path = config["credentials_file"] delete_cmd = [config["cloudflared_binary"], "tunnel", "--origincert", config["origincert"], "delete", "--credentials-file", credentials_path, "-f", config["tunnel"]] LOGGER.info(f"Deleting tunnel with {delete_cmd}") subprocess.run(delete_cmd, check=True) @retry(stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000) def create_dns(config, hostname, type, content): cf = CloudFlare.CloudFlare(debug=False, token=get_env("DNS_API_TOKEN")) cf.zones.dns_records.post( config["zone_tag"], data={'name': hostname, 'type': type, 'content': content, 'proxied': True} ) def create_named_dns(config, random_uuid): hostname = "named-" + random_uuid + "." + config["zone_domain"] create_dns(config, hostname, "CNAME", config["tunnel"] + ".cfargotunnel.com") return hostname @retry(stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000) def delete_dns(config, hostname): cf = CloudFlare.CloudFlare(debug=False, token=get_env("DNS_API_TOKEN")) zone_tag = config["zone_tag"] dns_records = cf.zones.dns_records.get(zone_tag, params={'name': hostname}) if len(dns_records) > 0: cf.zones.dns_records.delete(zone_tag, dns_records[0]['id']) def write_file(content, path): with open(path, 'w') as outfile: outfile.write(content) def get_env(env_name): val = os.getenv(env_name) if val is None: raise Exception(f"{env_name} is not set") return val def create(): """ Creates the necessary resources for the components test to run. - Creates a named tunnel with a random name. - Creates a random CNAME DNS entry for that tunnel. Those created resources are added to the config (obtained from an environment variable). The resulting configuration is persisted for the tests to use. """ config = get_config_from_env() origincert_path = persist_origin_cert(config) random_uuid = str(uuid.uuid4()) config["tunnel"] = create_tunnel(config, origincert_path, random_uuid) config["ingress"] = [ { "hostname": create_named_dns(config, random_uuid), "service": "hello_world" }, { "service": "http_status:404" } ] persist_config(config) def cleanup(): """ Reads the persisted configuration that was created previously. Deletes the resources that were created there. """ config = get_config_from_file() delete_tunnel(config) delete_dns(config, config["ingress"][0]["hostname"]) if __name__ == '__main__': parser = argparse.ArgumentParser(description='setup component tests') parser.add_argument('--type', choices=['create', 'cleanup'], default='create') args = parser.parse_args() if args.type == 'create': create() else: cleanup() ================================================ FILE: component-tests/test_config.py ================================================ #!/usr/bin/env python from util import start_cloudflared class TestConfig: # tmp_path is a fixture provides a temporary directory unique to the test invocation def test_validate_ingress_rules(self, tmp_path, component_tests_config): extra_config = { 'ingress': [ { "hostname": "example.com", "service": "https://localhost:8000", "originRequest": { "originServerName": "test.example.com", "caPool": "/etc/certs/ca.pem" }, }, { "hostname": "api.example.com", "path": "login", "service": "https://localhost:9000", }, { "hostname": "wss.example.com", "service": "wss://localhost:8000", }, { "hostname": "ssh.example.com", "service": "ssh://localhost:8000", }, {"service": "http_status:404"} ], } config = component_tests_config(extra_config) validate_args = ["ingress", "validate"] _ = start_cloudflared(tmp_path, config, validate_args) self.match_rule(tmp_path, config, "http://example.com/index.html", 0) self.match_rule(tmp_path, config, "https://example.com/index.html", 0) self.match_rule(tmp_path, config, "https://api.example.com/login", 1) self.match_rule(tmp_path, config, "https://wss.example.com", 2) self.match_rule(tmp_path, config, "https://ssh.example.com", 3) self.match_rule(tmp_path, config, "https://api.example.com", 4) # This is used to check that the command tunnel ingress url matches rule number . Note that rule number uses 1-based indexing def match_rule(self, tmp_path, config, url, rule_num): args = ["ingress", "rule", url] match_rule = start_cloudflared(tmp_path, config, args) assert f"Matched rule #{rule_num}" .encode() in match_rule.stdout ================================================ FILE: component-tests/test_edge_discovery.py ================================================ import ipaddress import socket import pytest from constants import protocols from cli import CloudflaredCli from util import get_tunnel_connector_id, LOGGER, wait_tunnel_ready, write_config class TestEdgeDiscovery: def _extra_config(self, protocol, edge_ip_version): config = { "protocol": protocol, } if edge_ip_version: config["edge-ip-version"] = edge_ip_version return config @pytest.mark.parametrize("protocol", protocols()) def test_default_only(self, tmp_path, component_tests_config, protocol): """ This test runs a tunnel to connect via IPv4-only edge addresses (default is unset "--edge-ip-version 4") """ if self.has_ipv6_only(): pytest.skip("Host has IPv6 only support and current default is IPv4 only") self.expect_address_connections( tmp_path, component_tests_config, protocol, None, self.expect_ipv4_address) @pytest.mark.parametrize("protocol", protocols()) def test_ipv4_only(self, tmp_path, component_tests_config, protocol): """ This test runs a tunnel to connect via IPv4-only edge addresses """ if self.has_ipv6_only(): pytest.skip("Host has IPv6 only support") self.expect_address_connections( tmp_path, component_tests_config, protocol, "4", self.expect_ipv4_address) @pytest.mark.parametrize("protocol", protocols()) def test_ipv6_only(self, tmp_path, component_tests_config, protocol): """ This test runs a tunnel to connect via IPv6-only edge addresses """ if self.has_ipv4_only(): pytest.skip("Host has IPv4 only support") self.expect_address_connections( tmp_path, component_tests_config, protocol, "6", self.expect_ipv6_address) @pytest.mark.parametrize("protocol", protocols()) def test_auto_ip64(self, tmp_path, component_tests_config, protocol): """ This test runs a tunnel to connect via auto with a preference of IPv6 then IPv4 addresses for a dual stack host This test also assumes that the host has IPv6 preference. """ if not self.has_dual_stack(address_family_preference=socket.AddressFamily.AF_INET6): pytest.skip("Host does not support dual stack with IPv6 preference") self.expect_address_connections( tmp_path, component_tests_config, protocol, "auto", self.expect_ipv6_address) @pytest.mark.parametrize("protocol", protocols()) def test_auto_ip46(self, tmp_path, component_tests_config, protocol): """ This test runs a tunnel to connect via auto with a preference of IPv4 then IPv6 addresses for a dual stack host This test also assumes that the host has IPv4 preference. """ if not self.has_dual_stack(address_family_preference=socket.AddressFamily.AF_INET): pytest.skip("Host does not support dual stack with IPv4 preference") self.expect_address_connections( tmp_path, component_tests_config, protocol, "auto", self.expect_ipv4_address) def expect_address_connections(self, tmp_path, component_tests_config, protocol, edge_ip_version, assert_address_type): config = component_tests_config( self._extra_config(protocol, edge_ip_version)) config_path = write_config(tmp_path, config.full_config) LOGGER.debug(config) with CloudflaredCli(config, config_path, LOGGER): wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=4) cfd_cli = CloudflaredCli(config, config_path, LOGGER) tunnel_id = config.get_tunnel_id() info = cfd_cli.get_tunnel_info(tunnel_id) connector_id = get_tunnel_connector_id() connector = next( (c for c in info["conns"] if c["id"] == connector_id), None) assert connector, f"Expected connection info from get tunnel info for the connected instance: {info}" conns = connector["conns"] assert conns == None or len( conns) == 4, f"There should be 4 connections registered: {conns}" for conn in conns: origin_ip = conn["origin_ip"] assert origin_ip, f"No available origin_ip for this connection: {conn}" assert_address_type(origin_ip) def expect_ipv4_address(self, address): assert type(ipaddress.ip_address( address)) is ipaddress.IPv4Address, f"Expected connection from origin to be a valid IPv4 address: {address}" def expect_ipv6_address(self, address): assert type(ipaddress.ip_address( address)) is ipaddress.IPv6Address, f"Expected connection from origin to be a valid IPv6 address: {address}" def get_addresses(self): """ Returns a list of addresses for the host. """ host_addresses = socket.getaddrinfo( "region1.v2.argotunnel.com", 7844, socket.AF_UNSPEC, socket.SOCK_STREAM) assert len( host_addresses) > 0, "No addresses returned from getaddrinfo" return host_addresses def has_dual_stack(self, address_family_preference=None): """ Returns true if the host has dual stack support and can optionally check the provided IP family preference. """ dual_stack = not self.has_ipv6_only() and not self.has_ipv4_only() if address_family_preference: address = self.get_addresses()[0] return dual_stack and address[0] == address_family_preference return dual_stack def has_ipv6_only(self): """ Returns True if the host has only IPv6 address support. """ return self.attempt_connection(socket.AddressFamily.AF_INET6) and not self.attempt_connection(socket.AddressFamily.AF_INET) def has_ipv4_only(self): """ Returns True if the host has only IPv4 address support. """ return self.attempt_connection(socket.AddressFamily.AF_INET) and not self.attempt_connection(socket.AddressFamily.AF_INET6) def attempt_connection(self, address_family): """ Returns True if a successful socket connection can be made to the remote host with the provided address family to validate host support for the provided address family. """ address = None for a in self.get_addresses(): if a[0] == address_family: address = a break if address is None: # Couldn't even lookup the address family so we can't connect return False af, socktype, proto, canonname, sockaddr = address s = None try: s = socket.socket(af, socktype, proto) except OSError: return False try: s.connect(sockaddr) except OSError: s.close() return False s.close() return True ================================================ FILE: component-tests/test_logging.py ================================================ #!/usr/bin/env python import json import os from constants import MAX_LOG_LINES from util import start_cloudflared, wait_tunnel_ready, send_requests # Rolling logger rotate log files after 1 MB rotate_after_size = 1000 * 1000 default_log_file = "cloudflared.log" expect_message = "Starting Hello" def assert_log_to_terminal(cloudflared): for _ in range(0, MAX_LOG_LINES): line = cloudflared.stderr.readline() if not line: break if expect_message.encode() in line: return raise Exception(f"terminal log doesn't contain {expect_message}") def assert_log_in_file(file): with open(file, "r") as f: for _ in range(0, MAX_LOG_LINES): line = f.readline() if not line: break if expect_message in line: return raise Exception(f"log file doesn't contain {expect_message}") def assert_json_log(file): def assert_in_json(j, key): assert key in j, f"{key} is not in j" with open(file, "r") as f: line = f.readline() json_log = json.loads(line) assert_in_json(json_log, "level") assert_in_json(json_log, "time") assert_in_json(json_log, "message") def assert_log_to_dir(config, log_dir): max_batches = 3 batch_requests = 1000 for _ in range(max_batches): send_requests(config.get_url(), batch_requests, require_ok=False) files = os.listdir(log_dir) if len(files) == 2: current_log_file_index = files.index(default_log_file) current_file = log_dir / files[current_log_file_index] stats = os.stat(current_file) assert stats.st_size > 0 assert_json_log(current_file) # One file is the current log file, the other is the rotated log file rotated_log_file_index = 0 if current_log_file_index == 1 else 1 rotated_file = log_dir / files[rotated_log_file_index] stats = os.stat(rotated_file) assert stats.st_size > rotate_after_size assert_log_in_file(rotated_file) assert_json_log(current_file) return raise Exception( f"Log file isn't rotated after sending {max_batches * batch_requests} requests") class TestLogging: def test_logging_to_terminal(self, tmp_path, component_tests_config): config = component_tests_config() with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], new_process=True) as cloudflared: wait_tunnel_ready(tunnel_url=config.get_url()) assert_log_to_terminal(cloudflared) def test_logging_to_file(self, tmp_path, component_tests_config): log_file = tmp_path / default_log_file extra_config = { # Convert from pathlib.Path to str "logfile": str(log_file), } config = component_tests_config(extra_config) with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], new_process=True, capture_output=False): wait_tunnel_ready(tunnel_url=config.get_url(), cfd_logs=str(log_file)) assert_log_in_file(log_file) assert_json_log(log_file) def test_logging_to_dir(self, tmp_path, component_tests_config): log_dir = tmp_path / "logs" extra_config = { "loglevel": "debug", # Convert from pathlib.Path to str "log-directory": str(log_dir), } config = component_tests_config(extra_config) with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], new_process=True, capture_output=False): wait_tunnel_ready(tunnel_url=config.get_url(), cfd_logs=str(log_dir)) assert_log_to_dir(config, log_dir) ================================================ FILE: component-tests/test_management.py ================================================ #!/usr/bin/env python import json import requests from conftest import CfdModes from constants import METRICS_PORT, MAX_RETRIES, BACKOFF_SECS from retrying import retry from cli import CloudflaredCli from util import LOGGER, write_config, start_cloudflared, wait_tunnel_ready, send_requests, decode_jwt_payload import platform """ Each test in TestManagement will: 1. Acquire a management token from Cloudflare public API 2. Make a request against the management service for the running tunnel """ class TestManagement: """ test_get_host_details does the following: 1. It gets a management token from Tunnelstore using cloudflared tail token 2. It gets the connector_id after starting a cloudflare tunnel 3. It sends a request to the management host with the connector_id and management token 4. Asserts that the response has a hostname and ip. """ def test_get_host_details(self, tmp_path, component_tests_config): # TUN-7377 : wait_tunnel_ready does not work properly in windows. # Skipping this test for windows for now and will address it as part of tun-7377 if platform.system() == "Windows": return config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False) LOGGER.debug(config) headers = {} headers["Content-Type"] = "application/json" config_path = write_config(tmp_path, config.full_config) with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1", "--label" , "test"], cfd_args=["run", "--hello-world"], new_process=True): wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=1) cfd_cli = CloudflaredCli(config, config_path, LOGGER) connector_id = cfd_cli.get_connector_id(config)[0] url = cfd_cli.get_management_url("host_details", config, config_path, resource="host_details") resp = send_request(url, headers=headers) # Assert response json. assert resp.status_code == 200, "Expected cloudflared to return 200 for host details" assert resp.json()["hostname"] == "custom:test", "Expected cloudflared to return hostname" assert resp.json()["ip"] != "", "Expected cloudflared to return ip" assert resp.json()["connector_id"] == connector_id, "Expected cloudflared to return connector_id" """ test_get_metrics will verify that the /metrics endpoint returns the prometheus metrics dump """ def test_get_metrics(self, tmp_path, component_tests_config): # TUN-7377 : wait_tunnel_ready does not work properly in windows. # Skipping this test for windows for now and will address it as part of tun-7377 if platform.system() == "Windows": return config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False) LOGGER.debug(config) config_path = write_config(tmp_path, config.full_config) with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], new_process=True): wait_tunnel_ready(require_min_connections=1) cfd_cli = CloudflaredCli(config, config_path, LOGGER) url = cfd_cli.get_management_url("metrics", config, config_path, resource="admin") resp = send_request(url) # Assert response. assert resp.status_code == 200, "Expected cloudflared to return 200 for /metrics" assert "# HELP build_info Build and version information" in resp.text, "Expected /metrics to have with the build_info details" """ test_get_pprof_heap will verify that the /debug/pprof/heap endpoint returns a pprof/heap dump response """ def test_get_pprof_heap(self, tmp_path, component_tests_config): # TUN-7377 : wait_tunnel_ready does not work properly in windows. # Skipping this test for windows for now and will address it as part of tun-7377 if platform.system() == "Windows": return config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False) LOGGER.debug(config) config_path = write_config(tmp_path, config.full_config) with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], new_process=True): wait_tunnel_ready(require_min_connections=1) cfd_cli = CloudflaredCli(config, config_path, LOGGER) url = cfd_cli.get_management_url("debug/pprof/heap", config, config_path, resource="admin") resp = send_request(url) # Assert response. assert resp.status_code == 200, "Expected cloudflared to return 200 for /debug/pprof/heap" assert resp.headers["Content-Type"] == "application/octet-stream", "Expected /debug/pprof/heap to have return a binary response" """ test_get_metrics_when_disabled will verify that diagnostic endpoints (such as /metrics) return 404 and are unmounted. """ def test_get_metrics_when_disabled(self, tmp_path, component_tests_config): # TUN-7377 : wait_tunnel_ready does not work properly in windows. # Skipping this test for windows for now and will address it as part of tun-7377 if platform.system() == "Windows": return config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False) LOGGER.debug(config) config_path = write_config(tmp_path, config.full_config) with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1", "--management-diagnostics=false"], new_process=True): wait_tunnel_ready(require_min_connections=1) cfd_cli = CloudflaredCli(config, config_path, LOGGER) url = cfd_cli.get_management_url("metrics", config, config_path, resource="admin") resp = send_request(url) # Assert response. assert resp.status_code == 404, "Expected cloudflared to return 404 for /metrics" def test_tail_token_command(self, tmp_path, component_tests_config): """ Validates that 'cloudflared tail token' command returns a token scoped for 'logs' and 'ping' resources. """ # TUN-7377: wait_tunnel_ready does not work properly in windows if platform.system() == "Windows": return config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False) LOGGER.debug(config) config_path = write_config(tmp_path, config.full_config) cfd_cli = CloudflaredCli(config, config_path, LOGGER) token = cfd_cli.get_tail_token(config, config_path) # Verify token was returned assert token, "Expected non-empty token to be returned" # Decode JWT payload to verify resource claims claims = decode_jwt_payload(token) resource_tag = 'res' # Verify the token has 'logs' and 'ping' in resource array assert resource_tag in claims, f"Expected {resource_tag} claim in token" assert isinstance(claims['res'], list), f"Expected {resource_tag} to be an array" assert 'logs' in claims[resource_tag], \ f"Expected 'logs' in resource array, got: {claims[resource_tag]}" assert 'ping' in claims[resource_tag], \ f"Expected 'ping' in resource array, got: {claims[resource_tag]}" LOGGER.info(f"Tail token successfully verified with resources: {claims[resource_tag]}") @retry(stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000) def send_request(url, headers={}): with requests.Session() as s: resp = s.get(url, timeout=BACKOFF_SECS, headers=headers) if resp.status_code == 530: LOGGER.debug(f"Received 530 status, retrying request to {url}") raise Exception(f"Received 530 status code from {url}") return resp ================================================ FILE: component-tests/test_pq.py ================================================ from util import LOGGER, start_cloudflared, wait_tunnel_ready class TestPostQuantum: def _extra_config(self): config = { "protocol": "quic", } return config def test_post_quantum(self, tmp_path, component_tests_config): config = component_tests_config(self._extra_config()) LOGGER.debug(config) with start_cloudflared( tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["run", "--post-quantum"], new_process=True, ): wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=1) ================================================ FILE: component-tests/test_quicktunnels.py ================================================ #!/usr/bin/env python from conftest import CfdModes from constants import METRICS_PORT import time from util import LOGGER, start_cloudflared, wait_tunnel_ready, get_quicktunnel_url, send_requests class TestQuickTunnels: def test_quick_tunnel(self, tmp_path, component_tests_config): config = component_tests_config(cfd_mode=CfdModes.QUICK) LOGGER.debug(config) with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["--hello-world"], new_process=True): wait_tunnel_ready(require_min_connections=1) time.sleep(10) url = get_quicktunnel_url() send_requests(url, 3, True) def test_quick_tunnel_url(self, tmp_path, component_tests_config): config = component_tests_config(cfd_mode=CfdModes.QUICK) LOGGER.debug(config) with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["--url", f"http://localhost:{METRICS_PORT}/"], new_process=True): wait_tunnel_ready(require_min_connections=1) time.sleep(10) url = get_quicktunnel_url() send_requests(url+"/ready", 3, True) ================================================ FILE: component-tests/test_reconnect.py ================================================ #!/usr/bin/env python import copy import platform from time import sleep import pytest from flaky import flaky from conftest import CfdModes from constants import protocols from util import start_cloudflared, wait_tunnel_ready, check_tunnel_not_connected @flaky(max_runs=3, min_passes=1) class TestReconnect: default_ha_conns = 1 default_reconnect_secs = 15 extra_config = { "stdin-control": True, } def _extra_config(self, protocol): return { "stdin-control": True, "protocol": protocol, } @pytest.mark.skipif(platform.system() == "Windows", reason=f"Currently buggy on Windows TUN-4584") @pytest.mark.parametrize("protocol", protocols()) def test_named_reconnect(self, tmp_path, component_tests_config, protocol): config = component_tests_config(self._extra_config(protocol)) with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], new_process=True, allow_input=True, capture_output=False) as cloudflared: # Repeat the test multiple times because some issues only occur after multiple reconnects self.assert_reconnect(config, cloudflared, 5) def send_reconnect(self, cloudflared, secs): # Although it is recommended to use the Popen.communicate method, we cannot # use it because it blocks on reading stdout and stderr until EOF is reached cloudflared.stdin.write(f"reconnect {secs}s\n".encode()) cloudflared.stdin.flush() def assert_reconnect(self, config, cloudflared, repeat): wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=self.default_ha_conns) for _ in range(repeat): for _ in range(self.default_ha_conns): self.send_reconnect(cloudflared, self.default_reconnect_secs) check_tunnel_not_connected() sleep(self.default_reconnect_secs * 2) wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=self.default_ha_conns) ================================================ FILE: component-tests/test_service.py ================================================ #!/usr/bin/env python import os import pathlib import subprocess from contextlib import contextmanager from pathlib import Path import pytest import test_logging from conftest import CfdModes from util import select_platform, skip_on_ci, start_cloudflared, wait_tunnel_ready, write_config def default_config_dir(): return os.path.join(Path.home(), ".cloudflared") def default_config_file(): return os.path.join(default_config_dir(), "config.yml") class TestServiceMode: @select_platform("Darwin") @pytest.mark.skipif(os.path.exists(default_config_file()), reason=f"There is already a config file in default path") def test_launchd_service_log_to_file(self, tmp_path, component_tests_config): log_file = tmp_path / test_logging.default_log_file additional_config = { # On Darwin cloudflared service defaults to run classic tunnel command "hello-world": True, "logfile": str(log_file), } config = component_tests_config(additional_config=additional_config, cfd_mode=CfdModes.CLASSIC) def assert_log_file(): test_logging.assert_log_in_file(log_file) test_logging.assert_json_log(log_file) self.launchd_service_scenario(config, assert_log_file) @select_platform("Darwin") @pytest.mark.skipif(os.path.exists(default_config_file()), reason=f"There is already a config file in default path") def test_launchd_service_with_token(self, tmp_path, component_tests_config): log_file = tmp_path / test_logging.default_log_file additional_config = { "logfile": str(log_file), } config = component_tests_config(additional_config=additional_config) # service install doesn't install the config file but in this case we want to use some default settings # so we write the base config without the tunnel credentials and ID write_config(pathlib.Path(default_config_dir()), config.base_config()) self.launchd_service_scenario(config, use_token=True) @select_platform("Darwin") @pytest.mark.skipif(os.path.exists(default_config_file()), reason=f"There is already a config file in default path") def test_launchd_service_rotating_log(self, tmp_path, component_tests_config): log_dir = tmp_path / "logs" additional_config = { # On Darwin cloudflared service defaults to run classic tunnel command "hello-world": True, "loglevel": "debug", "log-directory": str(log_dir), } config = component_tests_config(additional_config=additional_config, cfd_mode=CfdModes.CLASSIC) def assert_rotating_log(): test_logging.assert_log_to_dir(config, log_dir) self.launchd_service_scenario(config, assert_rotating_log) def launchd_service_scenario(self, config, extra_assertions=None, use_token=False): with self.run_service(Path(default_config_dir()), config, use_token=use_token): self.launchctl_cmd("list") self.launchctl_cmd("start") wait_tunnel_ready(tunnel_url=config.get_url()) if extra_assertions is not None: extra_assertions() self.launchctl_cmd("stop") os.remove(default_config_file()) self.launchctl_cmd("list", success=False) @skip_on_ci("we can't run sudo command on CI") @select_platform("Linux") @pytest.mark.skipif(os.path.exists("/etc/cloudflared/config.yml"), reason=f"There is already a config file in default path") def test_sysv_service_log_to_file(self, tmp_path, component_tests_config): log_file = tmp_path / test_logging.default_log_file additional_config = { "logfile": str(log_file), } config = component_tests_config(additional_config=additional_config) def assert_log_file(): test_logging.assert_log_in_file(log_file) test_logging.assert_json_log(log_file) self.sysv_service_scenario(config, tmp_path, assert_log_file) @skip_on_ci("we can't run sudo command on CI") @select_platform("Linux") @pytest.mark.skipif(os.path.exists("/etc/cloudflared/config.yml"), reason=f"There is already a config file in default path") def test_sysv_service_rotating_log(self, tmp_path, component_tests_config): log_dir = tmp_path / "logs" additional_config = { "loglevel": "debug", "log-directory": str(log_dir), } config = component_tests_config(additional_config=additional_config) def assert_rotating_log(): # We need the folder to have executable permissions for the "stat" command in the assertions to work. subprocess.check_call(['sudo', 'chmod', 'o+x', log_dir]) test_logging.assert_log_to_dir(config, log_dir) self.sysv_service_scenario(config, tmp_path, assert_rotating_log) @skip_on_ci("we can't run sudo command on CI") @select_platform("Linux") @pytest.mark.skipif(os.path.exists("/etc/cloudflared/config.yml"), reason=f"There is already a config file in default path") def test_sysv_service_with_token(self, tmp_path, component_tests_config): additional_config = { "loglevel": "debug", } config = component_tests_config(additional_config=additional_config) # service install doesn't install the config file but in this case we want to use some default settings # so we write the base config without the tunnel credentials and ID config_path = write_config(tmp_path, config.base_config()) subprocess.run(["sudo", "cp", config_path, "/etc/cloudflared/config.yml"], check=True) self.sysv_service_scenario(config, tmp_path, use_token=True) def sysv_service_scenario(self, config, tmp_path, extra_assertions=None, use_token=False): with self.run_service(tmp_path, config, root=True, use_token=use_token): self.sysv_cmd("status") wait_tunnel_ready(tunnel_url=config.get_url()) if extra_assertions is not None: extra_assertions() # Service install copies config file to /etc/cloudflared/config.yml subprocess.run(["sudo", "rm", "/etc/cloudflared/config.yml"]) self.sysv_cmd("status", success=False) @contextmanager def run_service(self, tmp_path, config, root=False, use_token=False): args = ["service", "install"] if use_token: args.append(config.get_token()) try: service = start_cloudflared( tmp_path, config, cfd_args=args, cfd_pre_args=[], capture_output=False, root=root, skip_config_flag=use_token) yield service finally: start_cloudflared( tmp_path, config, cfd_args=["service", "uninstall"], cfd_pre_args=[], capture_output=False, root=root, skip_config_flag=use_token) def launchctl_cmd(self, action, success=True): cmd = subprocess.run( ["launchctl", action, "com.cloudflare.cloudflared"], check=success) if not success: assert cmd.returncode != 0, f"Expect {cmd.args} to fail, but it succeed" def sysv_cmd(self, action, success=True): cmd = subprocess.run( ["sudo", "service", "cloudflared", action], check=success) if not success: assert cmd.returncode != 0, f"Expect {cmd.args} to fail, but it succeed" ================================================ FILE: component-tests/test_tail.py ================================================ #!/usr/bin/env python import asyncio import json import pytest import requests import websockets from websockets.client import connect, WebSocketClientProtocol from conftest import CfdModes from constants import MAX_RETRIES, BACKOFF_SECS from retrying import retry from cli import CloudflaredCli from util import LOGGER, start_cloudflared, write_config, wait_tunnel_ready class TestTail: @pytest.mark.asyncio async def test_start_stop_streaming(self, tmp_path, component_tests_config): """ Validates that a websocket connection to management.argotunnel.com/logs can be opened with the access token and start and stop streaming on-demand. """ print("test_start_stop_streaming") config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False) LOGGER.debug(config) config_path = write_config(tmp_path, config.full_config) with start_cloudflared(tmp_path, config, cfd_args=["run", "--hello-world"], new_process=True): wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=1) cfd_cli = CloudflaredCli(config, config_path, LOGGER) url = cfd_cli.get_management_wsurl("logs", config, config_path, resource="logs") async with connect(url, open_timeout=5, close_timeout=3) as websocket: await websocket.send('{"type": "start_streaming"}') await websocket.send('{"type": "stop_streaming"}') await websocket.send('{"type": "start_streaming"}') await websocket.send('{"type": "stop_streaming"}') @pytest.mark.asyncio async def test_streaming_logs(self, tmp_path, component_tests_config): """ Validates that a streaming logs connection will stream logs """ print("test_streaming_logs") config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False) LOGGER.debug(config) config_path = write_config(tmp_path, config.full_config) with start_cloudflared(tmp_path, config, cfd_args=["run", "--hello-world"], new_process=True): wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=1) cfd_cli = CloudflaredCli(config, config_path, LOGGER) url = cfd_cli.get_management_wsurl("logs", config, config_path, resource="logs") async with connect(url, open_timeout=5, close_timeout=5) as websocket: # send start_streaming await websocket.send(json.dumps({ "type": "start_streaming", "filters": { "events": ["http"] } })) # send some http requests to the tunnel to trigger some logs await generate_and_validate_http_events(websocket, config.get_url(), 10) # send stop_streaming await websocket.send('{"type": "stop_streaming"}') @pytest.mark.asyncio async def test_streaming_logs_filters(self, tmp_path, component_tests_config): """ Validates that a streaming logs connection will stream logs but not http when filters applied. """ print("test_streaming_logs_filters") config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False) LOGGER.debug(config) config_path = write_config(tmp_path, config.full_config) with start_cloudflared(tmp_path, config, cfd_args=["run", "--hello-world"], new_process=True): wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=1) cfd_cli = CloudflaredCli(config, config_path, LOGGER) url = cfd_cli.get_management_wsurl("logs", config, config_path, resource="logs") async with connect(url, open_timeout=5, close_timeout=5) as websocket: # send start_streaming with tcp logs only await websocket.send(json.dumps({ "type": "start_streaming", "filters": { "events": ["tcp"], "level": "debug" } })) # don't expect any http logs await generate_and_validate_no_log_event(websocket, config.get_url()) # send stop_streaming await websocket.send('{"type": "stop_streaming"}') @pytest.mark.asyncio async def test_streaming_logs_sampling(self, tmp_path, component_tests_config): """ Validates that a streaming logs connection will stream logs with sampling. """ print("test_streaming_logs_sampling") config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False) LOGGER.debug(config) config_path = write_config(tmp_path, config.full_config) with start_cloudflared(tmp_path, config, cfd_args=["run", "--hello-world"], new_process=True): wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=1) cfd_cli = CloudflaredCli(config, config_path, LOGGER) url = cfd_cli.get_management_wsurl("logs", config, config_path, resource="logs") async with connect(url, open_timeout=5, close_timeout=5) as websocket: # send start_streaming with info logs only await websocket.send(json.dumps({ "type": "start_streaming", "filters": { "sampling": 0.5, "events": ["http"] } })) # don't expect any http logs count = await generate_and_validate_http_events(websocket, config.get_url(), 10) assert count < (10 * 2) # There are typically always two log lines for http requests (request and response) # send stop_streaming await websocket.send('{"type": "stop_streaming"}') @pytest.mark.asyncio async def test_streaming_logs_actor_override(self, tmp_path, component_tests_config): """ Validates that a streaming logs session can be overriden by the same actor """ print("test_streaming_logs_actor_override") config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False) LOGGER.debug(config) config_path = write_config(tmp_path, config.full_config) with start_cloudflared(tmp_path, config, cfd_args=["run", "--hello-world"], new_process=True): wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=1) cfd_cli = CloudflaredCli(config, config_path, LOGGER) url = cfd_cli.get_management_wsurl("logs", config, config_path, resource="logs") task = asyncio.ensure_future(start_streaming_to_be_remotely_closed(url)) override_task = asyncio.ensure_future(start_streaming_override(url)) await asyncio.wait([task, override_task]) assert task.exception() == None, task.exception() assert override_task.exception() == None, override_task.exception() async def start_streaming_to_be_remotely_closed(url): async with connect(url, open_timeout=5, close_timeout=5) as websocket: try: await websocket.send(json.dumps({"type": "start_streaming"})) await asyncio.sleep(10) assert websocket.closed, "expected this request to be forcibly closed by the override" except websockets.ConnectionClosed: # we expect the request to be closed pass async def start_streaming_override(url): # wait for the first connection to be established await asyncio.sleep(1) async with connect(url, open_timeout=5, close_timeout=5) as websocket: await websocket.send(json.dumps({"type": "start_streaming"})) await asyncio.sleep(1) await websocket.send(json.dumps({"type": "stop_streaming"})) await asyncio.sleep(1) # Every http request has two log lines sent async def generate_and_validate_http_events(websocket: WebSocketClientProtocol, url: str, count_send: int): for i in range(count_send): send_request(url) # There are typically always two log lines for http requests (request and response) count = 0 while True: try: req_line = await asyncio.wait_for(websocket.recv(), 2) log_line = json.loads(req_line) assert log_line["type"] == "logs" assert log_line["logs"][0]["event"] == "http" count += 1 except asyncio.TimeoutError: # ignore timeout from waiting for recv break return count # Every http request has two log lines sent async def generate_and_validate_no_log_event(websocket: WebSocketClientProtocol, url: str): send_request(url) try: # wait for 5 seconds and make sure we hit the timeout and not recv any events req_line = await asyncio.wait_for(websocket.recv(), 5) assert req_line == None, "expected no logs for the specified filters" except asyncio.TimeoutError: pass @retry(stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000) def send_request(url, headers={}): with requests.Session() as s: resp = s.get(url, timeout=BACKOFF_SECS, headers=headers) assert resp.status_code == 200, f"{url} returned {resp}" return resp.status_code == 200 ================================================ FILE: component-tests/test_termination.py ================================================ #!/usr/bin/env python from contextlib import contextmanager import platform import signal import threading import time import pytest import requests from constants import protocols from util import start_cloudflared, wait_tunnel_ready, check_tunnel_not_connected def supported_signals(): if platform.system() == "Windows": return [signal.SIGTERM] return [signal.SIGTERM, signal.SIGINT] class TestTermination: grace_period = 5 timeout = 10 sse_endpoint = "/sse?freq=1s" def _extra_config(self, protocol): return { "grace-period": f"{self.grace_period}s", "protocol": protocol, } @pytest.mark.parametrize("signal", supported_signals()) @pytest.mark.parametrize("protocol", protocols()) def test_graceful_shutdown(self, tmp_path, component_tests_config, signal, protocol): config = component_tests_config(self._extra_config(protocol)) with start_cloudflared( tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], new_process=True, capture_output=False) as cloudflared: wait_tunnel_ready(tunnel_url=config.get_url()) connected = threading.Condition() in_flight_req = threading.Thread( target=self.stream_request, args=(config, connected, False, )) in_flight_req.start() with connected: connected.wait(self.timeout) # Send signal after the SSE connection is established with self.within_grace_period(): self.terminate_by_signal(cloudflared, signal) self.wait_eyeball_thread( in_flight_req, self.grace_period + self.timeout) # test cloudflared terminates before grace period expires when all eyeball # connections are drained @pytest.mark.parametrize("signal", supported_signals()) @pytest.mark.parametrize("protocol", protocols()) def test_shutdown_once_no_connection(self, tmp_path, component_tests_config, signal, protocol): config = component_tests_config(self._extra_config(protocol)) with start_cloudflared( tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], new_process=True, capture_output=False) as cloudflared: wait_tunnel_ready(tunnel_url=config.get_url()) connected = threading.Condition() in_flight_req = threading.Thread( target=self.stream_request, args=(config, connected, True, )) in_flight_req.start() with connected: connected.wait(self.timeout) with self.within_grace_period(has_connection=False): # Send signal after the SSE connection is established self.terminate_by_signal(cloudflared, signal) self.wait_eyeball_thread(in_flight_req, self.grace_period) @pytest.mark.parametrize("signal", supported_signals()) @pytest.mark.parametrize("protocol", protocols()) def test_no_connection_shutdown(self, tmp_path, component_tests_config, signal, protocol): config = component_tests_config(self._extra_config(protocol)) with start_cloudflared( tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], new_process=True, capture_output=False) as cloudflared: wait_tunnel_ready(tunnel_url=config.get_url()) with self.within_grace_period(has_connection=False): self.terminate_by_signal(cloudflared, signal) def terminate_by_signal(self, cloudflared, sig): cloudflared.send_signal(sig) check_tunnel_not_connected() cloudflared.wait() def wait_eyeball_thread(self, thread, timeout): thread.join(timeout) assert thread.is_alive() == False, "eyeball thread is still alive" # Using this context asserts logic within the context is executed within grace period @contextmanager def within_grace_period(self, has_connection=True): try: start = time.time() yield finally: # If the request takes longer than the grace period then we need to wait at most the grace period. # If the request fell within the grace period cloudflared can close earlier, but to ensure that it doesn't # close immediately we add a minimum boundary. If cloudflared shutdown in less than 1s it's likely that # it shutdown as soon as it received SIGINT. The only way cloudflared can close immediately is if it has no # in-flight requests minimum = 1 if has_connection else 0 duration = time.time() - start # Here we truncate to ensure that we don't fail on minute differences like 10.1 instead of 10 assert minimum <= int(duration) <= self.grace_period def stream_request(self, config, connected, early_terminate): expected_terminate_message = "502 Bad Gateway" url = config.get_url() + self.sse_endpoint with requests.get(url, timeout=5, stream=True) as resp: with connected: connected.notifyAll() lines = 0 for line in resp.iter_lines(): if expected_terminate_message.encode() == line: break lines += 1 if early_terminate and lines == 2: return # /sse returns count followed by 2 new lines assert lines >= (self.grace_period * 2) ================================================ FILE: component-tests/test_token.py ================================================ import base64 import json from setup import get_config_from_file from util import start_cloudflared class TestToken: def test_get_token(self, tmp_path, component_tests_config): config = component_tests_config() tunnel_id = config.get_tunnel_id() token_args = ["--origincert", cert_path(), "token", tunnel_id] output = start_cloudflared(tmp_path, config, token_args) assert parse_token(config.get_token()) == parse_token(output.stdout) def test_get_credentials_file(self, tmp_path, component_tests_config): config = component_tests_config() tunnel_id = config.get_tunnel_id() cred_file = tmp_path / "test_get_credentials_file.json" token_args = ["--origincert", cert_path(), "token", "--cred-file", cred_file, tunnel_id] start_cloudflared(tmp_path, config, token_args) with open(cred_file) as json_file: assert config.get_credentials_json() == json.load(json_file) def cert_path(): return get_config_from_file()["origincert"] def parse_token(token): return json.loads(base64.b64decode(token)) ================================================ FILE: component-tests/test_tunnel.py ================================================ #!/usr/bin/env python import requests from conftest import CfdModes from constants import METRICS_PORT, MAX_RETRIES, BACKOFF_SECS from retrying import retry from cli import CloudflaredCli from util import LOGGER, write_config, start_cloudflared, wait_tunnel_ready, send_requests import platform class TestTunnel: '''Test tunnels with no ingress rules from config.yaml but ingress rules from CLI only''' def test_tunnel_hello_world(self, tmp_path, component_tests_config): config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False) LOGGER.debug(config) with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["run", "--hello-world"], new_process=True): wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=1) def test_tunnel_url(self, tmp_path, component_tests_config): config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False) LOGGER.debug(config) with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["run", "--url", f"http://localhost:{METRICS_PORT}/"], new_process=True): wait_tunnel_ready(require_min_connections=1) send_requests(config.get_url()+"/ready", 3, True) def test_tunnel_no_ingress(self, tmp_path, component_tests_config): ''' Running a tunnel with no ingress rules provided from either config.yaml or CLI will still work but return 503 for all incoming requests. ''' config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False) LOGGER.debug(config) with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["run"], new_process=True): wait_tunnel_ready(require_min_connections=1) expected_status_code = 503 resp = send_request(config.get_url()+"/", expected_status_code) assert resp.status_code == expected_status_code, "Expected cloudflared to return 503 for all requests with no ingress defined" resp = send_request(config.get_url()+"/test", expected_status_code) assert resp.status_code == expected_status_code, "Expected cloudflared to return 503 for all requests with no ingress defined" def retry_if_result_none(result): ''' Returns True if the result is None, indicating that the function should be retried. ''' return result is None @retry(retry_on_result=retry_if_result_none, stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000) def send_request(url, expected_status_code=200): with requests.Session() as s: resp = s.get(url, timeout=BACKOFF_SECS) return resp if resp.status_code == expected_status_code else None ================================================ FILE: component-tests/util.py ================================================ import logging import os import platform import subprocess from contextlib import contextmanager from time import sleep import sys import pytest import requests import yaml from retrying import retry from constants import METRICS_PORT, MAX_RETRIES, BACKOFF_SECS def configure_logger(): logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) handler = logging.StreamHandler(sys.stdout) logger.addHandler(handler) return logger LOGGER = configure_logger() def select_platform(plat): return pytest.mark.skipif( platform.system() != plat, reason=f"Only runs on {plat}") def fips_enabled(): env_fips = os.getenv("COMPONENT_TESTS_FIPS") return env_fips is not None and env_fips != "0" nofips = pytest.mark.skipif( fips_enabled(), reason=f"Only runs without FIPS (COMPONENT_TESTS_FIPS=0)") def skip_on_ci(reason): env_ci = os.getenv("CI") running_in_ci = env_ci is not None and env_ci != "0" return pytest.mark.skipif( running_in_ci, reason=f"This test can't run on CI due to: {reason}") def write_config(directory, config): config_path = directory / "config.yml" with open(config_path, 'w') as outfile: yaml.dump(config, outfile) return config_path def start_cloudflared(directory, config, cfd_args=["run"], cfd_pre_args=["tunnel"], new_process=False, allow_input=False, capture_output=True, root=False, skip_config_flag=False, expect_success=True): config_path = None if not skip_config_flag: config_path = write_config(directory, config.full_config) cmd = cloudflared_cmd(config, config_path, cfd_args, cfd_pre_args, root) if new_process: return run_cloudflared_background(cmd, allow_input, capture_output) # By setting check=True, it will raise an exception if the process exits with non-zero exit code return subprocess.run(cmd, check=expect_success, capture_output=capture_output) def cloudflared_cmd(config, config_path, cfd_args, cfd_pre_args, root): cmd = [] if root: cmd += ["sudo"] cmd += [config.cloudflared_binary] cmd += cfd_pre_args if config_path is not None: cmd += ["--config", str(config_path)] cmd += cfd_args LOGGER.info(f"Run cmd {cmd} with config {config}") return cmd @contextmanager def run_cloudflared_background(cmd, allow_input, capture_output): output = subprocess.PIPE if capture_output else subprocess.DEVNULL stdin = subprocess.PIPE if allow_input else None cfd = None try: cfd = subprocess.Popen(cmd, stdin=stdin, stdout=output, stderr=output) yield cfd finally: if cfd: cfd.terminate() if capture_output: LOGGER.info(f"cloudflared log: {cfd.stderr.read()}") def get_quicktunnel_url(): quicktunnel_url = f'http://localhost:{METRICS_PORT}/quicktunnel' with requests.Session() as s: resp = send_request(s, quicktunnel_url, True) hostname = resp.json()["hostname"] assert hostname, \ f"Quicktunnel endpoint returned {hostname} but we expected a url" return f"https://{hostname}" def wait_tunnel_ready(tunnel_url=None, require_min_connections=1, cfd_logs=None): try: inner_wait_tunnel_ready(tunnel_url, require_min_connections) except Exception as e: if cfd_logs is not None: _log_cloudflared_logs(cfd_logs) raise e @retry(stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000) def inner_wait_tunnel_ready(tunnel_url=None, require_min_connections=1): metrics_url = f'http://localhost:{METRICS_PORT}/ready' with requests.Session() as s: LOGGER.debug("Waiting for tunnel to be ready...") resp = send_request(s, metrics_url, True) ready_connections = resp.json()["readyConnections"] assert ready_connections >= require_min_connections, \ f"Ready endpoint returned {resp.json()} but we expect at least {require_min_connections} connections" if tunnel_url is not None: send_request(s, tunnel_url, True) def _log_cloudflared_logs(cfd_logs): log_file = cfd_logs if os.path.isdir(cfd_logs): files = os.listdir(cfd_logs) if len(files) == 0: return log_file = os.path.join(cfd_logs, files[0]) with open(log_file, "r") as f: LOGGER.warning("Cloudflared Tunnel was not ready:") for line in f.readlines(): LOGGER.warning(line) @retry(stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000) def check_tunnel_not_connected(): url = f'http://localhost:{METRICS_PORT}/ready' try: resp = requests.get(url, timeout=BACKOFF_SECS) assert resp.status_code == 503, f"Expect {url} returns 503, got {resp.status_code}" assert resp.json()[ "readyConnections"] == 0, "Expected all connections to be terminated (pending reconnect)" # cloudflared might already terminate except requests.exceptions.ConnectionError as e: LOGGER.warning(f"Failed to connect to {url}, error: {e}") def get_tunnel_connector_id(): url = f'http://localhost:{METRICS_PORT}/ready' try: resp = requests.get(url, timeout=1) return resp.json()["connectorId"] # cloudflared might already terminated except requests.exceptions.ConnectionError as e: LOGGER.warning(f"Failed to connect to {url}, error: {e}") # In some cases we don't need to check response status, such as when sending batch requests to generate logs def send_requests(url, count, require_ok=True): errors = 0 with requests.Session() as s: for _ in range(count): resp = send_request(s, url, require_ok) if resp is None: errors += 1 sleep(0.01) if errors > 0: LOGGER.warning( f"{errors} out of {count} requests to {url} return non-200 status") @retry(stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000) def send_request(session, url, require_ok): resp = session.get(url, timeout=BACKOFF_SECS) if require_ok: assert resp.status_code == 200, f"{url} returned {resp}" return resp if resp.status_code == 200 else None def decode_jwt_payload(token): """ Decode the payload section of a JWT token without signature verification. JWT Structure: ============== A JWT consists of three Base64URL-encoded parts separated by dots: HEADER.PAYLOAD.SIGNATURE The payload contains the JWT claims (the actual data/permissions). Args: token (str): The complete JWT token string Returns: dict: The decoded payload as a dictionary containing JWT claims Raises: ValueError: If the token doesn't have exactly 3 parts Note: This function does NOT verify the signature - it only decodes the payload. Use this only when you trust the token source (e.g., tokens you just generated). """ import base64 import json # Split JWT into its three components parts = token.split('.') if len(parts) != 3: raise ValueError(f"Invalid JWT format: expected 3 parts, got {len(parts)}") # Extract and decode the payload (middle section) # Base64 requires padding to be a multiple of 4 characters payload_encoded = parts[1] remainder = len(payload_encoded) % 4 if remainder != 0: payload_padded = payload_encoded + '=' * (4 - remainder) else: payload_padded = payload_encoded # Decode from Base64URL format and parse JSON decoded_payload = base64.urlsafe_b64decode(payload_padded) return json.loads(decoded_payload) ================================================ FILE: config/configuration.go ================================================ package config import ( "encoding/json" "fmt" "io" "net/url" "os" "path/filepath" "runtime" "strconv" "time" homedir "github.com/mitchellh/go-homedir" "github.com/pkg/errors" "github.com/rs/zerolog" "github.com/urfave/cli/v2" yaml "gopkg.in/yaml.v3" "github.com/cloudflare/cloudflared/validation" ) var ( // DefaultConfigFiles is the file names from which we attempt to read configuration. DefaultConfigFiles = []string{"config.yml", "config.yaml"} // DefaultUnixConfigLocation is the primary location to find a config file DefaultUnixConfigLocation = "/usr/local/etc/cloudflared" // DefaultUnixLogLocation is the primary location to find log files DefaultUnixLogLocation = "/var/log/cloudflared" // Launchd doesn't set root env variables, so there is default // Windows default config dir was ~/cloudflare-warp in documentation; let's keep it compatible defaultUserConfigDirs = []string{"~/.cloudflared", "~/.cloudflare-warp", "~/cloudflare-warp"} defaultNixConfigDirs = []string{"/etc/cloudflared", DefaultUnixConfigLocation} ErrNoConfigFile = fmt.Errorf("Cannot determine default configuration path. No file %v in %v", DefaultConfigFiles, DefaultConfigSearchDirectories()) ) const ( // BastionFlag is to enable bastion, or jump host, operation BastionFlag = "bastion" ) // DefaultConfigDirectory returns the default directory of the config file func DefaultConfigDirectory() string { if runtime.GOOS == "windows" { path := os.Getenv("CFDPATH") if path == "" { path = filepath.Join(os.Getenv("ProgramFiles(x86)"), "cloudflared") if _, err := os.Stat(path); os.IsNotExist(err) { // doesn't exist, so return an empty failure string return "" } } return path } return DefaultUnixConfigLocation } // DefaultLogDirectory returns the default directory for log files func DefaultLogDirectory() string { if runtime.GOOS == "windows" { return DefaultConfigDirectory() } return DefaultUnixLogLocation } // DefaultConfigPath returns the default location of a config file func DefaultConfigPath() string { dir := DefaultConfigDirectory() if dir == "" { return DefaultConfigFiles[0] } return filepath.Join(dir, DefaultConfigFiles[0]) } // DefaultConfigSearchDirectories returns the default folder locations of the config func DefaultConfigSearchDirectories() []string { dirs := make([]string, len(defaultUserConfigDirs)) copy(dirs, defaultUserConfigDirs) if runtime.GOOS != "windows" { dirs = append(dirs, defaultNixConfigDirs...) } return dirs } // FileExists checks to see if a file exist at the provided path. func FileExists(path string) (bool, error) { f, err := os.Open(path) if err != nil { if os.IsNotExist(err) { // ignore missing files return false, nil } return false, err } _ = f.Close() return true, nil } // FindDefaultConfigPath returns the first path that contains a config file. // If none of the combination of DefaultConfigSearchDirectories() and DefaultConfigFiles // contains a config file, return empty string. func FindDefaultConfigPath() string { for _, configDir := range DefaultConfigSearchDirectories() { for _, configFile := range DefaultConfigFiles { dirPath, err := homedir.Expand(configDir) if err != nil { continue } path := filepath.Join(dirPath, configFile) if ok, _ := FileExists(path); ok { return path } } } return "" } // FindOrCreateConfigPath returns the first path that contains a config file // or creates one in the primary default path if it doesn't exist func FindOrCreateConfigPath() string { path := FindDefaultConfigPath() if path == "" { // create the default directory if it doesn't exist path = DefaultConfigPath() if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil { return "" } // write a new config file out file, err := os.Create(path) if err != nil { return "" } defer file.Close() logDir := DefaultLogDirectory() _ = os.MkdirAll(logDir, os.ModePerm) // try and create it. Doesn't matter if it succeed or not, only byproduct will be no logs c := Root{ LogDirectory: logDir, } if err := yaml.NewEncoder(file).Encode(&c); err != nil { return "" } } return path } // ValidateUnixSocket ensures --unix-socket param is used exclusively // i.e. it fails if a user specifies both --url and --unix-socket func ValidateUnixSocket(c *cli.Context) (string, error) { if c.IsSet("unix-socket") && (c.IsSet("url") || c.NArg() > 0) { return "", errors.New("--unix-socket must be used exclusively.") } return c.String("unix-socket"), nil } // ValidateUrl will validate url flag correctness. It can be either from --url or argument // Notice ValidateUnixSocket, it will enforce --unix-socket is not used with --url or argument func ValidateUrl(c *cli.Context, allowURLFromArgs bool) (*url.URL, error) { var url = c.String("url") if allowURLFromArgs && c.NArg() > 0 { if c.IsSet("url") { return nil, errors.New("Specified origin urls using both --url and argument. Decide which one you want, I can only support one.") } url = c.Args().Get(0) } validUrl, err := validation.ValidateUrl(url) return validUrl, err } type UnvalidatedIngressRule struct { Hostname string `json:"hostname,omitempty"` Path string `json:"path,omitempty"` Service string `json:"service,omitempty"` OriginRequest OriginRequestConfig `yaml:"originRequest" json:"originRequest"` } // OriginRequestConfig is a set of optional fields that users may set to // customize how cloudflared sends requests to origin services. It is used to set // up general config that apply to all rules, and also, specific per-rule // config. // Note: // - To specify a time.Duration in go-yaml, use e.g. "3s" or "24h". // - To specify a time.Duration in json, use int64 of the nanoseconds type OriginRequestConfig struct { // HTTP proxy timeout for establishing a new connection ConnectTimeout *CustomDuration `yaml:"connectTimeout" json:"connectTimeout,omitempty"` // HTTP proxy timeout for completing a TLS handshake TLSTimeout *CustomDuration `yaml:"tlsTimeout" json:"tlsTimeout,omitempty"` // HTTP proxy TCP keepalive duration TCPKeepAlive *CustomDuration `yaml:"tcpKeepAlive" json:"tcpKeepAlive,omitempty"` // HTTP proxy should disable "happy eyeballs" for IPv4/v6 fallback NoHappyEyeballs *bool `yaml:"noHappyEyeballs" json:"noHappyEyeballs,omitempty"` // HTTP proxy maximum keepalive connection pool size KeepAliveConnections *int `yaml:"keepAliveConnections" json:"keepAliveConnections,omitempty"` // HTTP proxy timeout for closing an idle connection KeepAliveTimeout *CustomDuration `yaml:"keepAliveTimeout" json:"keepAliveTimeout,omitempty"` // Sets the HTTP Host header for the local webserver. HTTPHostHeader *string `yaml:"httpHostHeader" json:"httpHostHeader,omitempty"` // Hostname on the origin server certificate. OriginServerName *string `yaml:"originServerName" json:"originServerName,omitempty"` // Auto configure the Hostname on the origin server certificate. MatchSNIToHost *bool `yaml:"matchSNItoHost" json:"matchSNItoHost,omitempty"` // Path to the CA for the certificate of your origin. // This option should be used only if your certificate is not signed by Cloudflare. CAPool *string `yaml:"caPool" json:"caPool,omitempty"` // Disables TLS verification of the certificate presented by your origin. // Will allow any certificate from the origin to be accepted. // Note: The connection from your machine to Cloudflare's Edge is still encrypted. NoTLSVerify *bool `yaml:"noTLSVerify" json:"noTLSVerify,omitempty"` // Disables chunked transfer encoding. // Useful if you are running a WSGI server. DisableChunkedEncoding *bool `yaml:"disableChunkedEncoding" json:"disableChunkedEncoding,omitempty"` // Runs as jump host BastionMode *bool `yaml:"bastionMode" json:"bastionMode,omitempty"` // Listen address for the proxy. ProxyAddress *string `yaml:"proxyAddress" json:"proxyAddress,omitempty"` // Listen port for the proxy. ProxyPort *uint `yaml:"proxyPort" json:"proxyPort,omitempty"` // Valid options are 'socks' or empty. ProxyType *string `yaml:"proxyType" json:"proxyType,omitempty"` // IP rules for the proxy service IPRules []IngressIPRule `yaml:"ipRules" json:"ipRules,omitempty"` // Attempt to connect to origin with HTTP/2 Http2Origin *bool `yaml:"http2Origin" json:"http2Origin,omitempty"` // Access holds all access related configs Access *AccessConfig `yaml:"access" json:"access,omitempty"` } type AccessConfig struct { // Required when set to true will fail every request that does not arrive through an access authenticated endpoint. Required bool `yaml:"required" json:"required,omitempty"` // TeamName is the organization team name to get the public key certificates for. TeamName string `yaml:"teamName" json:"teamName"` // AudTag is the AudTag to verify access JWT against. AudTag []string `yaml:"audTag" json:"audTag"` Environment string `yaml:"environment" json:"environment,omitempty"` } type IngressIPRule struct { Prefix *string `yaml:"prefix" json:"prefix"` Ports []int `yaml:"ports" json:"ports"` Allow bool `yaml:"allow" json:"allow"` } type Configuration struct { TunnelID string `yaml:"tunnel"` Ingress []UnvalidatedIngressRule WarpRouting WarpRoutingConfig `yaml:"warp-routing"` OriginRequest OriginRequestConfig `yaml:"originRequest"` sourceFile string } type WarpRoutingConfig struct { ConnectTimeout *CustomDuration `yaml:"connectTimeout" json:"connectTimeout,omitempty"` MaxActiveFlows *uint64 `yaml:"maxActiveFlows" json:"maxActiveFlows,omitempty"` TCPKeepAlive *CustomDuration `yaml:"tcpKeepAlive" json:"tcpKeepAlive,omitempty"` } type configFileSettings struct { Configuration `yaml:",inline"` // older settings will be aggregated into the generic map, should be read via cli.Context Settings map[string]interface{} `yaml:",inline"` } func (c *Configuration) Source() string { return c.sourceFile } func (c *configFileSettings) Int(name string) (int, error) { if raw, ok := c.Settings[name]; ok { if v, ok := raw.(int); ok { return v, nil } return 0, fmt.Errorf("expected int found %T for %s", raw, name) } return 0, nil } func (c *configFileSettings) Duration(name string) (time.Duration, error) { if raw, ok := c.Settings[name]; ok { switch v := raw.(type) { case time.Duration: return v, nil case string: return time.ParseDuration(v) } return 0, fmt.Errorf("expected duration found %T for %s", raw, name) } return 0, nil } func (c *configFileSettings) Float64(name string) (float64, error) { if raw, ok := c.Settings[name]; ok { if v, ok := raw.(float64); ok { return v, nil } return 0, fmt.Errorf("expected float found %T for %s", raw, name) } return 0, nil } func (c *configFileSettings) String(name string) (string, error) { if raw, ok := c.Settings[name]; ok { if v, ok := raw.(string); ok { return v, nil } return "", fmt.Errorf("expected string found %T for %s", raw, name) } return "", nil } func (c *configFileSettings) StringSlice(name string) ([]string, error) { if raw, ok := c.Settings[name]; ok { if slice, ok := raw.([]interface{}); ok { strSlice := make([]string, len(slice)) for i, v := range slice { str, ok := v.(string) if !ok { return nil, fmt.Errorf("expected string, found %T for %v", i, v) } strSlice[i] = str } return strSlice, nil } return nil, fmt.Errorf("expected string slice found %T for %s", raw, name) } return nil, nil } func (c *configFileSettings) IntSlice(name string) ([]int, error) { if raw, ok := c.Settings[name]; ok { if slice, ok := raw.([]interface{}); ok { intSlice := make([]int, len(slice)) for i, v := range slice { str, ok := v.(int) if !ok { return nil, fmt.Errorf("expected int, found %T for %v ", v, v) } intSlice[i] = str } return intSlice, nil } if v, ok := raw.([]int); ok { return v, nil } return nil, fmt.Errorf("expected int slice found %T for %s", raw, name) } return nil, nil } func (c *configFileSettings) Generic(name string) (cli.Generic, error) { return nil, errors.New("option type Generic not supported") } func (c *configFileSettings) Bool(name string) (bool, error) { if raw, ok := c.Settings[name]; ok { if v, ok := raw.(bool); ok { return v, nil } return false, fmt.Errorf("expected boolean found %T for %s", raw, name) } return false, nil } var configuration configFileSettings func GetConfiguration() *Configuration { return &configuration.Configuration } // ReadConfigFile returns InputSourceContext initialized from the configuration file. // On repeat calls returns with the same file, returns without reading the file again; however, // if value of "config" flag changes, will read the new config file func ReadConfigFile(c *cli.Context, log *zerolog.Logger) (settings *configFileSettings, warnings string, err error) { configFile := c.String("config") if configuration.Source() == configFile || configFile == "" { if configuration.Source() == "" { return nil, "", ErrNoConfigFile } return &configuration, "", nil } log.Debug().Msgf("Loading configuration from %s", configFile) file, err := os.Open(configFile) if err != nil { // If does not exist and config file was not specificly specified then return ErrNoConfigFile found. if os.IsNotExist(err) && !c.IsSet("config") { err = ErrNoConfigFile } return nil, "", err } defer file.Close() if err := yaml.NewDecoder(file).Decode(&configuration); err != nil { if err == io.EOF { log.Error().Msgf("Configuration file %s was empty", configFile) return &configuration, "", nil } return nil, "", errors.Wrap(err, "error parsing YAML in config file at "+configFile) } configuration.sourceFile = configFile // Parse it again, with strict mode, to find warnings. if file, err := os.Open(configFile); err == nil { decoder := yaml.NewDecoder(file) decoder.KnownFields(true) var unusedConfig configFileSettings if err := decoder.Decode(&unusedConfig); err != nil { warnings = err.Error() } } return &configuration, warnings, nil } // A CustomDuration is a Duration that has custom serialization for JSON. // JSON in Javascript assumes that int fields are 32 bits and Duration fields are deserialized assuming that numbers // are in nanoseconds, which in 32bit integers limits to just 2 seconds. // This type assumes that when serializing/deserializing from JSON, that the number is in seconds, while it maintains // the YAML serde assumptions. type CustomDuration struct { time.Duration } func (s CustomDuration) MarshalJSON() ([]byte, error) { return json.Marshal(s.Duration.Seconds()) } func (s *CustomDuration) UnmarshalJSON(data []byte) error { seconds, err := strconv.ParseInt(string(data), 10, 64) if err != nil { return err } s.Duration = time.Duration(seconds * int64(time.Second)) return nil } func (s *CustomDuration) MarshalYAML() (interface{}, error) { return s.Duration.String(), nil } func (s *CustomDuration) UnmarshalYAML(unmarshal func(interface{}) error) error { return unmarshal(&s.Duration) } ================================================ FILE: config/configuration_test.go ================================================ package config import ( "encoding/json" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" yaml "gopkg.in/yaml.v3" ) func TestConfigFileSettings(t *testing.T) { var ( firstIngress = UnvalidatedIngressRule{ Hostname: "tunnel1.example.com", Path: "/id", Service: "https://localhost:8000", } secondIngress = UnvalidatedIngressRule{ Hostname: "*", Path: "", Service: "https://localhost:8001", } warpRouting = WarpRoutingConfig{ ConnectTimeout: &CustomDuration{Duration: 2 * time.Second}, TCPKeepAlive: &CustomDuration{Duration: 10 * time.Second}, } ) rawYAML := ` tunnel: config-file-test originRequest: ipRules: - prefix: "10.0.0.0/8" ports: - 80 - 8080 allow: false - prefix: "fc00::/7" ports: - 443 - 4443 allow: true ingress: - hostname: tunnel1.example.com path: /id service: https://localhost:8000 - hostname: "*" service: https://localhost:8001 warp-routing: enabled: true connectTimeout: 2s tcpKeepAlive: 10s retries: 5 grace-period: 30s percentage: 3.14 hostname: example.com tag: - test - central-1 counters: - 123 - 456 ` var config configFileSettings err := yaml.Unmarshal([]byte(rawYAML), &config) assert.NoError(t, err) assert.Equal(t, "config-file-test", config.TunnelID) assert.Equal(t, firstIngress, config.Ingress[0]) assert.Equal(t, secondIngress, config.Ingress[1]) assert.Equal(t, warpRouting, config.WarpRouting) privateV4 := "10.0.0.0/8" privateV6 := "fc00::/7" ipRules := []IngressIPRule{ { Prefix: &privateV4, Ports: []int{80, 8080}, Allow: false, }, { Prefix: &privateV6, Ports: []int{443, 4443}, Allow: true, }, } assert.Equal(t, ipRules, config.OriginRequest.IPRules) retries, err := config.Int("retries") assert.NoError(t, err) assert.Equal(t, 5, retries) gracePeriod, err := config.Duration("grace-period") assert.NoError(t, err) assert.Equal(t, time.Second*30, gracePeriod) percentage, err := config.Float64("percentage") assert.NoError(t, err) assert.Equal(t, 3.14, percentage) hostname, err := config.String("hostname") assert.NoError(t, err) assert.Equal(t, "example.com", hostname) tags, err := config.StringSlice("tag") assert.NoError(t, err) assert.Equal(t, "test", tags[0]) assert.Equal(t, "central-1", tags[1]) counters, err := config.IntSlice("counters") assert.NoError(t, err) assert.Equal(t, 123, counters[0]) assert.Equal(t, 456, counters[1]) } var rawJsonConfig = []byte(` { "connectTimeout": 10, "tlsTimeout": 30, "tcpKeepAlive": 30, "noHappyEyeballs": true, "keepAliveTimeout": 60, "keepAliveConnections": 10, "httpHostHeader": "app.tunnel.com", "originServerName": "app.tunnel.com", "caPool": "/etc/capool", "noTLSVerify": true, "disableChunkedEncoding": true, "bastionMode": true, "proxyAddress": "127.0.0.3", "proxyPort": 9000, "proxyType": "socks", "ipRules": [ { "prefix": "10.0.0.0/8", "ports": [80, 8080], "allow": false }, { "prefix": "fc00::/7", "ports": [443, 4443], "allow": true } ], "http2Origin": true } `) func TestMarshalUnmarshalOriginRequest(t *testing.T) { testCases := []struct { name string marshalFunc func(in interface{}) (out []byte, err error) unMarshalFunc func(in []byte, out interface{}) (err error) }{ {"json", json.Marshal, json.Unmarshal}, {"yaml", yaml.Marshal, yaml.Unmarshal}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { assertConfig(t, tc.marshalFunc, tc.unMarshalFunc) }) } } func assertConfig( t *testing.T, marshalFunc func(in interface{}) (out []byte, err error), unMarshalFunc func(in []byte, out interface{}) (err error), ) { var config OriginRequestConfig var config2 OriginRequestConfig assert.NoError(t, json.Unmarshal(rawJsonConfig, &config)) assert.Equal(t, time.Second*10, config.ConnectTimeout.Duration) assert.Equal(t, time.Second*30, config.TLSTimeout.Duration) assert.Equal(t, time.Second*30, config.TCPKeepAlive.Duration) assert.Equal(t, true, *config.NoHappyEyeballs) assert.Equal(t, time.Second*60, config.KeepAliveTimeout.Duration) assert.Equal(t, 10, *config.KeepAliveConnections) assert.Equal(t, "app.tunnel.com", *config.HTTPHostHeader) assert.Equal(t, "app.tunnel.com", *config.OriginServerName) assert.Equal(t, "/etc/capool", *config.CAPool) assert.Equal(t, true, *config.NoTLSVerify) assert.Equal(t, true, *config.DisableChunkedEncoding) assert.Equal(t, true, *config.BastionMode) assert.Equal(t, "127.0.0.3", *config.ProxyAddress) assert.Equal(t, true, *config.NoTLSVerify) assert.Equal(t, uint(9000), *config.ProxyPort) assert.Equal(t, "socks", *config.ProxyType) assert.Equal(t, true, *config.Http2Origin) privateV4 := "10.0.0.0/8" privateV6 := "fc00::/7" ipRules := []IngressIPRule{ { Prefix: &privateV4, Ports: []int{80, 8080}, Allow: false, }, { Prefix: &privateV6, Ports: []int{443, 4443}, Allow: true, }, } assert.Equal(t, ipRules, config.IPRules) // validate that serializing and deserializing again matches the deserialization from raw string result, err := marshalFunc(config) require.NoError(t, err) err = unMarshalFunc(result, &config2) require.NoError(t, err) require.Equal(t, config2, config) } ================================================ FILE: config/manager.go ================================================ package config import ( "io" "os" "github.com/pkg/errors" "github.com/rs/zerolog" yaml "gopkg.in/yaml.v3" "github.com/cloudflare/cloudflared/watcher" ) // Notifier sends out config updates type Notifier interface { ConfigDidUpdate(Root) } // Manager is the base functions of the config manager type Manager interface { Start(Notifier) error Shutdown() } // FileManager watches the yaml config for changes // sends updates to the service to reconfigure to match the updated config type FileManager struct { watcher watcher.Notifier notifier Notifier configPath string log *zerolog.Logger ReadConfig func(string, *zerolog.Logger) (Root, error) } // NewFileManager creates a config manager func NewFileManager(watcher watcher.Notifier, configPath string, log *zerolog.Logger) (*FileManager, error) { m := &FileManager{ watcher: watcher, configPath: configPath, log: log, ReadConfig: readConfigFromPath, } err := watcher.Add(configPath) return m, err } // Start starts the runloop to watch for config changes func (m *FileManager) Start(notifier Notifier) error { m.notifier = notifier // update the notifier with a fresh config on start config, err := m.GetConfig() if err != nil { return err } notifier.ConfigDidUpdate(config) m.watcher.Start(m) return nil } // GetConfig reads the yaml file from the disk func (m *FileManager) GetConfig() (Root, error) { return m.ReadConfig(m.configPath, m.log) } // Shutdown stops the watcher func (m *FileManager) Shutdown() { m.watcher.Shutdown() } func readConfigFromPath(configPath string, log *zerolog.Logger) (Root, error) { if configPath == "" { return Root{}, errors.New("unable to find config file") } file, err := os.Open(configPath) if err != nil { return Root{}, err } defer file.Close() var config Root if err := yaml.NewDecoder(file).Decode(&config); err != nil { if err == io.EOF { log.Error().Msgf("Configuration file %s was empty", configPath) return Root{}, nil } return Root{}, errors.Wrap(err, "error parsing YAML in config file at "+configPath) } return config, nil } // File change notifications from the watcher // WatcherItemDidChange triggers when the yaml config is updated // sends the updated config to the service to reload its state func (m *FileManager) WatcherItemDidChange(filepath string) { config, err := m.GetConfig() if err != nil { m.log.Err(err).Msg("Failed to read new config") return } m.log.Info().Msg("Config file has been updated") m.notifier.ConfigDidUpdate(config) } // WatcherDidError notifies of errors with the file watcher func (m *FileManager) WatcherDidError(err error) { m.log.Err(err).Msg("Config watcher encountered an error") } ================================================ FILE: config/manager_test.go ================================================ package config import ( "os" "testing" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" "github.com/cloudflare/cloudflared/watcher" ) type mockNotifier struct { configs []Root } func (n *mockNotifier) ConfigDidUpdate(c Root) { n.configs = append(n.configs, c) } type mockFileWatcher struct { path string notifier watcher.Notification ready chan struct{} } func (w *mockFileWatcher) Start(n watcher.Notification) { w.notifier = n w.ready <- struct{}{} } func (w *mockFileWatcher) Add(string) error { return nil } func (w *mockFileWatcher) Shutdown() { } func (w *mockFileWatcher) TriggerChange() { w.notifier.WatcherItemDidChange(w.path) } func TestConfigChanged(t *testing.T) { filePath := "config.yaml" f, err := os.Create(filePath) assert.NoError(t, err) defer func() { _ = f.Close() _ = os.Remove(filePath) }() c := &Root{ Forwarders: []Forwarder{ { URL: "test.daltoniam.com", Listener: "127.0.0.1:8080", }, }, } configRead := func(configPath string, log *zerolog.Logger) (Root, error) { return *c, nil } wait := make(chan struct{}) w := &mockFileWatcher{path: filePath, ready: wait} log := zerolog.Nop() service, err := NewFileManager(w, filePath, &log) service.ReadConfig = configRead assert.NoError(t, err) n := &mockNotifier{} go service.Start(n) <-wait c.Forwarders = append(c.Forwarders, Forwarder{URL: "add.daltoniam.com", Listener: "127.0.0.1:8081"}) w.TriggerChange() service.Shutdown() assert.Len(t, n.configs, 2, "did not get 2 config updates as expected") assert.Len(t, n.configs[0].Forwarders, 1, "not the amount of forwarders expected") assert.Len(t, n.configs[1].Forwarders, 2, "not the amount of forwarders expected") assert.Equal(t, n.configs[0].Forwarders[0].Hash(), c.Forwarders[0].Hash(), "forwarder hashes don't match") assert.Equal(t, n.configs[1].Forwarders[0].Hash(), c.Forwarders[0].Hash(), "forwarder hashes don't match") assert.Equal(t, n.configs[1].Forwarders[1].Hash(), c.Forwarders[1].Hash(), "forwarder hashes don't match") } ================================================ FILE: config/model.go ================================================ package config import ( "crypto/sha256" "fmt" "io" ) // Forwarder represents a client side listener to forward traffic to the edge type Forwarder struct { URL string `json:"url"` Listener string `json:"listener"` TokenClientID string `json:"service_token_id" yaml:"serviceTokenID"` TokenSecret string `json:"secret_token_id" yaml:"serviceTokenSecret"` Destination string `json:"destination"` IsFedramp bool `json:"is_fedramp" yaml:"isFedramp"` } // Tunnel represents a tunnel that should be started type Tunnel struct { URL string `json:"url"` Origin string `json:"origin"` ProtocolType string `json:"type"` } // Root is the base options to configure the service. type Root struct { LogDirectory string `json:"log_directory" yaml:"logDirectory,omitempty"` LogLevel string `json:"log_level" yaml:"logLevel,omitempty"` Forwarders []Forwarder `json:"forwarders,omitempty" yaml:"forwarders,omitempty"` Tunnels []Tunnel `json:"tunnels,omitempty" yaml:"tunnels,omitempty"` // `resolver` key is reserved for a removed feature (proxy-dns) and should not be used. } // Hash returns the computed values to see if the forwarder values change func (f *Forwarder) Hash() string { h := sha256.New() _, _ = io.WriteString(h, f.URL) _, _ = io.WriteString(h, f.Listener) _, _ = io.WriteString(h, f.TokenClientID) _, _ = io.WriteString(h, f.TokenSecret) _, _ = io.WriteString(h, f.Destination) return fmt.Sprintf("%x", h.Sum(nil)) } ================================================ FILE: connection/connection.go ================================================ package connection import ( "context" "encoding/base64" "fmt" "io" "math" "net" "net/http" "strconv" "strings" "time" "github.com/google/uuid" "github.com/pkg/errors" "github.com/cloudflare/cloudflared/tracing" "github.com/cloudflare/cloudflared/tunnelrpc/pogs" "github.com/cloudflare/cloudflared/websocket" ) const ( lbProbeUserAgentPrefix = "Mozilla/5.0 (compatible; Cloudflare-Traffic-Manager/1.0; +https://www.cloudflare.com/traffic-manager/;" LogFieldConnIndex = "connIndex" MaxGracePeriod = time.Minute * 3 MaxConcurrentStreams = math.MaxUint32 contentTypeHeader = "content-type" contentLengthHeader = "content-length" transferEncodingHeader = "transfer-encoding" sseContentType = "text/event-stream" grpcContentType = "application/grpc" sseJsonContentType = "application/x-ndjson" chunkTransferEncoding = "chunked" ) var ( switchingProtocolText = fmt.Sprintf("%d %s", http.StatusSwitchingProtocols, http.StatusText(http.StatusSwitchingProtocols)) flushableContentTypes = []string{sseContentType, grpcContentType, sseJsonContentType} ) // TunnelConnection represents the connection to the edge. // The Serve method is provided to allow clients to handle any errors from the connection encountered during // processing of the connection. Cancelling of the context provided to Serve will close the connection. type TunnelConnection interface { Serve(ctx context.Context) error } type Orchestrator interface { UpdateConfig(version int32, config []byte) *pogs.UpdateConfigurationResponse GetConfigJSON() ([]byte, error) GetOriginProxy() (OriginProxy, error) } type TunnelProperties struct { Credentials Credentials QuickTunnelUrl string } // Credentials are stored in the credentials file and contain all info needed to run a tunnel. type Credentials struct { AccountTag string TunnelSecret []byte TunnelID uuid.UUID Endpoint string } func (c *Credentials) Auth() pogs.TunnelAuth { return pogs.TunnelAuth{ AccountTag: c.AccountTag, TunnelSecret: c.TunnelSecret, } } // TunnelToken are Credentials but encoded with custom fields namings. type TunnelToken struct { AccountTag string `json:"a"` TunnelSecret []byte `json:"s"` TunnelID uuid.UUID `json:"t"` Endpoint string `json:"e,omitempty"` } func (t TunnelToken) Credentials() Credentials { // nolint: gosimple return Credentials{ AccountTag: t.AccountTag, TunnelSecret: t.TunnelSecret, TunnelID: t.TunnelID, Endpoint: t.Endpoint, } } func (t TunnelToken) Encode() (string, error) { val, err := json.Marshal(t) if err != nil { return "", errors.Wrap(err, "could not JSON encode token") } return base64.StdEncoding.EncodeToString(val), nil } type ClassicTunnelProperties struct { Hostname string OriginCert []byte // feature-flag to use new edge reconnect tokens UseReconnectToken bool } // Type indicates the connection type of the connection. type Type int const ( TypeWebsocket Type = iota TypeTCP TypeControlStream TypeHTTP TypeConfiguration ) // ShouldFlush returns whether this kind of connection should actively flush data func (t Type) shouldFlush() bool { switch t { case TypeWebsocket, TypeTCP, TypeControlStream: return true default: return false } } func (t Type) String() string { switch t { case TypeWebsocket: return "websocket" case TypeTCP: return "tcp" case TypeControlStream: return "control stream" case TypeHTTP: return "http" default: return fmt.Sprintf("Unknown Type %d", t) } } // OriginProxy is how data flows from cloudflared to the origin services running behind it. type OriginProxy interface { ProxyHTTP(w ResponseWriter, tr *tracing.TracedHTTPRequest, isWebsocket bool) error ProxyTCP(ctx context.Context, rwa ReadWriteAcker, req *TCPRequest) error } // TCPRequest defines the input format needed to perform a TCP proxy. type TCPRequest struct { Dest string CFRay string LBProbe bool FlowID string CfTraceID string ConnIndex uint8 } // ReadWriteAcker is a readwriter with the ability to Acknowledge to the downstream (edge) that the origin has // accepted the connection. type ReadWriteAcker interface { io.ReadWriter AckConnection(tracePropagation string) error } // HTTPResponseReadWriteAcker is an HTTP implementation of ReadWriteAcker. type HTTPResponseReadWriteAcker struct { r io.Reader w ResponseWriter f http.Flusher req *http.Request } // NewHTTPResponseReadWriterAcker returns a new instance of HTTPResponseReadWriteAcker. func NewHTTPResponseReadWriterAcker(w ResponseWriter, flusher http.Flusher, req *http.Request) *HTTPResponseReadWriteAcker { return &HTTPResponseReadWriteAcker{ r: req.Body, w: w, f: flusher, req: req, } } func (h *HTTPResponseReadWriteAcker) Read(p []byte) (int, error) { return h.r.Read(p) } func (h *HTTPResponseReadWriteAcker) Write(p []byte) (int, error) { n, err := h.w.Write(p) if n > 0 { h.f.Flush() } return n, err } // AckConnection acks an HTTP connection by sending a switch protocols status code that enables the caller to // upgrade to streams. func (h *HTTPResponseReadWriteAcker) AckConnection(tracePropagation string) error { resp := &http.Response{ Status: switchingProtocolText, StatusCode: http.StatusSwitchingProtocols, ContentLength: -1, Header: http.Header{}, } if secWebsocketKey := h.req.Header.Get("Sec-WebSocket-Key"); secWebsocketKey != "" { resp.Header = websocket.NewResponseHeader(h.req) } if tracePropagation != "" { resp.Header.Add(tracing.CanonicalCloudflaredTracingHeader, tracePropagation) } return h.w.WriteRespHeaders(resp.StatusCode, resp.Header) } // localProxyConnection emulates an incoming connection to cloudflared as a net.Conn. // Used when handling a "hijacked" connection from connection.ResponseWriter type localProxyConnection struct { io.ReadWriteCloser } func (c *localProxyConnection) Read(b []byte) (int, error) { return c.ReadWriteCloser.Read(b) } func (c *localProxyConnection) Write(b []byte) (int, error) { return c.ReadWriteCloser.Write(b) } func (c *localProxyConnection) Close() error { return c.ReadWriteCloser.Close() } func (c *localProxyConnection) LocalAddr() net.Addr { // Unused LocalAddr return &net.TCPAddr{IP: net.IPv6loopback, Port: 0, Zone: ""} } func (c *localProxyConnection) RemoteAddr() net.Addr { // Unused RemoteAddr return &net.TCPAddr{IP: net.IPv6loopback, Port: 0, Zone: ""} } func (c *localProxyConnection) SetDeadline(t time.Time) error { // ignored since we can't set the read/write Deadlines for the tunnel back to origintunneld return nil } func (c *localProxyConnection) SetReadDeadline(t time.Time) error { // ignored since we can't set the read/write Deadlines for the tunnel back to origintunneld return nil } func (c *localProxyConnection) SetWriteDeadline(t time.Time) error { // ignored since we can't set the read/write Deadlines for the tunnel back to origintunneld return nil } // ResponseWriter is the response path for a request back through cloudflared's tunnel. type ResponseWriter interface { WriteRespHeaders(status int, header http.Header) error AddTrailer(trailerName, trailerValue string) http.ResponseWriter http.Hijacker io.Writer } type ConnectedFuse interface { Connected() IsConnected() bool } // Helper method to let the caller know what content-types should require a flush on every // write to a ResponseWriter. func shouldFlush(headers http.Header) bool { // When doing Server Side Events (SSE), some frameworks don't respect the `Content-Type` header. // Therefore, we need to rely on other ways to know whether we should flush on write or not. A good // approach is to assume that responses without `Content-Length` or with `Transfer-Encoding: chunked` // are streams, and therefore, should be flushed right away to the eyeball. // References: // - https://datatracker.ietf.org/doc/html/rfc7230#section-4.1 // - https://datatracker.ietf.org/doc/html/rfc9112#section-6.1 if contentLength := headers.Get(contentLengthHeader); contentLength == "" { return true } if transferEncoding := headers.Get(transferEncodingHeader); transferEncoding != "" { transferEncoding = strings.ToLower(transferEncoding) if strings.Contains(transferEncoding, chunkTransferEncoding) { return true } } if contentType := headers.Get(contentTypeHeader); contentType != "" { contentType = strings.ToLower(contentType) for _, c := range flushableContentTypes { if strings.HasPrefix(contentType, c) { return true } } } return false } func uint8ToString(input uint8) string { return strconv.FormatUint(uint64(input), 10) } func FindCfRayHeader(req *http.Request) string { return req.Header.Get("Cf-Ray") } func IsLBProbeRequest(req *http.Request) bool { return strings.HasPrefix(req.UserAgent(), lbProbeUserAgentPrefix) } ================================================ FILE: connection/connection_test.go ================================================ package connection import ( "context" "crypto/rand" "fmt" "io" "math/big" "net/http" "testing" "time" pkgerrors "github.com/pkg/errors" "github.com/rs/zerolog" "github.com/stretchr/testify/require" cfdflow "github.com/cloudflare/cloudflared/flow" "github.com/cloudflare/cloudflared/stream" "github.com/cloudflare/cloudflared/tracing" tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs" "github.com/cloudflare/cloudflared/websocket" ) const ( largeFileSize = 2 * 1024 * 1024 testGracePeriod = time.Millisecond * 100 ) var ( testOrchestrator = &mockOrchestrator{ originProxy: &mockOriginProxy{}, } log = zerolog.Nop() testLargeResp = make([]byte, largeFileSize) ) var _ ReadWriteAcker = (*HTTPResponseReadWriteAcker)(nil) type testRequest struct { name string endpoint string expectedStatus int expectedBody []byte isProxyError bool } type mockOrchestrator struct { originProxy OriginProxy } func (mcr *mockOrchestrator) GetConfigJSON() ([]byte, error) { return nil, fmt.Errorf("not implemented") } func (*mockOrchestrator) UpdateConfig(version int32, config []byte) *tunnelpogs.UpdateConfigurationResponse { return &tunnelpogs.UpdateConfigurationResponse{ LastAppliedVersion: version, } } func (mcr *mockOrchestrator) GetOriginProxy() (OriginProxy, error) { return mcr.originProxy, nil } func (mcr *mockOrchestrator) WarpRoutingEnabled() (enabled bool) { return true } type mockOriginProxy struct{} func (moc *mockOriginProxy) ProxyHTTP( w ResponseWriter, tr *tracing.TracedHTTPRequest, isWebsocket bool, ) error { req := tr.Request if isWebsocket { switch req.URL.Path { case "/ws/echo": return wsEchoEndpoint(w, req) case "/ws/flaky": return wsFlakyEndpoint(w, req) default: originRespEndpoint(w, http.StatusNotFound, []byte("ws endpoint not found")) return fmt.Errorf("unknown websocket endpoint %s", req.URL.Path) } } switch req.URL.Path { case "/ok": originRespEndpoint(w, http.StatusOK, []byte(http.StatusText(http.StatusOK))) case "/large_file": originRespEndpoint(w, http.StatusOK, testLargeResp) case "/400": originRespEndpoint(w, http.StatusBadRequest, []byte(http.StatusText(http.StatusBadRequest))) case "/500": originRespEndpoint(w, http.StatusInternalServerError, []byte(http.StatusText(http.StatusInternalServerError))) case "/error": return fmt.Errorf("Failed to proxy to origin") default: originRespEndpoint(w, http.StatusNotFound, []byte("page not found")) } return nil } func (moc *mockOriginProxy) ProxyTCP( ctx context.Context, rwa ReadWriteAcker, r *TCPRequest, ) error { if r.CfTraceID == "flow-rate-limited" { return pkgerrors.Wrap(cfdflow.ErrTooManyActiveFlows, "tcp flow rate limited") } return nil } type echoPipe struct { reader *io.PipeReader writer *io.PipeWriter } func (ep *echoPipe) Read(p []byte) (int, error) { return ep.reader.Read(p) } func (ep *echoPipe) Write(p []byte) (int, error) { return ep.writer.Write(p) } // A mock origin that echos data by streaming like a tcpOverWSConnection // https://github.com/cloudflare/cloudflared/blob/master/ingress/origin_connection.go func wsEchoEndpoint(w ResponseWriter, r *http.Request) error { resp := &http.Response{ StatusCode: http.StatusSwitchingProtocols, } if err := w.WriteRespHeaders(resp.StatusCode, resp.Header); err != nil { return err } wsCtx, cancel := context.WithCancel(r.Context()) readPipe, writePipe := io.Pipe() wsConn := websocket.NewConn(wsCtx, NewHTTPResponseReadWriterAcker(w, w.(http.Flusher), r), &log) go func() { select { case <-wsCtx.Done(): case <-r.Context().Done(): } readPipe.Close() writePipe.Close() }() originConn := &echoPipe{reader: readPipe, writer: writePipe} stream.Pipe(wsConn, originConn, &log) cancel() wsConn.Close() return nil } type flakyConn struct { closeAt time.Time } func (fc *flakyConn) Read(p []byte) (int, error) { if time.Now().After(fc.closeAt) { return 0, io.EOF } n := copy(p, "Read from flaky connection") return n, nil } func (fc *flakyConn) Write(p []byte) (int, error) { if time.Now().After(fc.closeAt) { return 0, fmt.Errorf("flaky connection closed") } return len(p), nil } func wsFlakyEndpoint(w ResponseWriter, r *http.Request) error { resp := &http.Response{ StatusCode: http.StatusSwitchingProtocols, } if err := w.WriteRespHeaders(resp.StatusCode, resp.Header); err != nil { return err } wsCtx, cancel := context.WithCancel(r.Context()) wsConn := websocket.NewConn(wsCtx, NewHTTPResponseReadWriterAcker(w, w.(http.Flusher), r), &log) rInt, _ := rand.Int(rand.Reader, big.NewInt(50)) closedAfter := time.Millisecond * time.Duration(rInt.Int64()) originConn := &flakyConn{closeAt: time.Now().Add(closedAfter)} stream.Pipe(wsConn, originConn, &log) cancel() wsConn.Close() return nil } func originRespEndpoint(w ResponseWriter, status int, data []byte) { resp := &http.Response{ StatusCode: status, } _ = w.WriteRespHeaders(resp.StatusCode, resp.Header) _, _ = w.Write(data) } type mockConnectedFuse struct{} func (mcf mockConnectedFuse) Connected() {} func (mcf mockConnectedFuse) IsConnected() bool { return true } func TestShouldFlushHeaders(t *testing.T) { tests := []struct { headers map[string]string shouldFlush bool }{ { headers: map[string]string{contentTypeHeader: "application/json", contentLengthHeader: "1"}, shouldFlush: false, }, { headers: map[string]string{contentTypeHeader: "text/html", contentLengthHeader: "1"}, shouldFlush: false, }, { headers: map[string]string{contentTypeHeader: "text/event-stream", contentLengthHeader: "1"}, shouldFlush: true, }, { headers: map[string]string{contentTypeHeader: "application/grpc", contentLengthHeader: "1"}, shouldFlush: true, }, { headers: map[string]string{contentTypeHeader: "application/x-ndjson", contentLengthHeader: "1"}, shouldFlush: true, }, { headers: map[string]string{contentTypeHeader: "application/json"}, shouldFlush: true, }, { headers: map[string]string{contentTypeHeader: "application/json", contentLengthHeader: "-1", transferEncodingHeader: "chunked"}, shouldFlush: true, }, } for _, test := range tests { headers := http.Header{} for k, v := range test.headers { headers.Add(k, v) } require.Equal(t, test.shouldFlush, shouldFlush(headers)) } } ================================================ FILE: connection/control.go ================================================ package connection import ( "context" "io" "net" "time" "github.com/pkg/errors" "github.com/cloudflare/cloudflared/management" "github.com/cloudflare/cloudflared/tunnelrpc" "github.com/cloudflare/cloudflared/tunnelrpc/pogs" ) // registerClient derives a named tunnel rpc client that can then be used to register and unregister connections. type registerClientFunc func(context.Context, io.ReadWriteCloser, time.Duration) tunnelrpc.RegistrationClient type controlStream struct { observer *Observer connectedFuse ConnectedFuse tunnelProperties *TunnelProperties connIndex uint8 edgeAddress net.IP protocol Protocol registerClientFunc registerClientFunc registerTimeout time.Duration gracefulShutdownC <-chan struct{} gracePeriod time.Duration stoppedGracefully bool } // ControlStreamHandler registers connections with origintunneld and initiates graceful shutdown. type ControlStreamHandler interface { // ServeControlStream handles the control plane of the transport in the current goroutine calling this ServeControlStream(ctx context.Context, rw io.ReadWriteCloser, connOptions *pogs.ConnectionOptions, tunnelConfigGetter TunnelConfigJSONGetter) error // IsStopped tells whether the method above has finished IsStopped() bool } type TunnelConfigJSONGetter interface { GetConfigJSON() ([]byte, error) } // NewControlStream returns a new instance of ControlStreamHandler func NewControlStream( observer *Observer, connectedFuse ConnectedFuse, tunnelProperties *TunnelProperties, connIndex uint8, edgeAddress net.IP, registerClientFunc registerClientFunc, registerTimeout time.Duration, gracefulShutdownC <-chan struct{}, gracePeriod time.Duration, protocol Protocol, ) ControlStreamHandler { if registerClientFunc == nil { registerClientFunc = tunnelrpc.NewRegistrationClient } return &controlStream{ observer: observer, connectedFuse: connectedFuse, tunnelProperties: tunnelProperties, registerClientFunc: registerClientFunc, registerTimeout: registerTimeout, connIndex: connIndex, edgeAddress: edgeAddress, gracefulShutdownC: gracefulShutdownC, gracePeriod: gracePeriod, protocol: protocol, } } func (c *controlStream) ServeControlStream( ctx context.Context, rw io.ReadWriteCloser, connOptions *pogs.ConnectionOptions, tunnelConfigGetter TunnelConfigJSONGetter, ) error { registrationClient := c.registerClientFunc(ctx, rw, c.registerTimeout) c.observer.logConnecting(c.connIndex, c.edgeAddress, c.protocol) registrationDetails, err := registrationClient.RegisterConnection( ctx, c.tunnelProperties.Credentials.Auth(), c.tunnelProperties.Credentials.TunnelID, connOptions, c.connIndex, c.edgeAddress) if err != nil { defer registrationClient.Close() if err.Error() == DuplicateConnectionError { c.observer.metrics.regFail.WithLabelValues("dup_edge_conn", "registerConnection").Inc() return errDuplicationConnection } c.observer.metrics.regFail.WithLabelValues("server_error", "registerConnection").Inc() return serverRegistrationErrorFromRPC(err) } c.observer.metrics.regSuccess.WithLabelValues("registerConnection").Inc() c.observer.logConnected(registrationDetails.UUID, c.connIndex, registrationDetails.Location, c.edgeAddress, c.protocol) c.observer.sendConnectedEvent(c.connIndex, c.protocol, registrationDetails.Location, c.edgeAddress) c.connectedFuse.Connected() // if conn index is 0 and tunnel is not remotely managed, then send local ingress rules configuration if c.connIndex == 0 && !registrationDetails.TunnelIsRemotelyManaged { if tunnelConfig, err := tunnelConfigGetter.GetConfigJSON(); err == nil { if err := registrationClient.SendLocalConfiguration(ctx, tunnelConfig); err != nil { c.observer.metrics.localConfigMetrics.pushesErrors.Inc() c.observer.log.Err(err).Msg("unable to send local configuration") } c.observer.metrics.localConfigMetrics.pushes.Inc() } else { c.observer.log.Err(err).Msg("failed to obtain current configuration") } } return c.waitForUnregister(ctx, registrationClient) } func (c *controlStream) waitForUnregister(ctx context.Context, registrationClient tunnelrpc.RegistrationClient) error { // wait for connection termination or start of graceful shutdown defer registrationClient.Close() var shutdownError error select { case <-ctx.Done(): shutdownError = ctx.Err() break case <-c.gracefulShutdownC: c.stoppedGracefully = true } c.observer.sendUnregisteringEvent(c.connIndex) err := registrationClient.GracefulShutdown(ctx, c.gracePeriod) if err != nil { return errors.Wrap(err, "Error shutting down control stream") } c.observer.log.Info(). Int(management.EventTypeKey, int(management.Cloudflared)). Uint8(LogFieldConnIndex, c.connIndex). IPAddr(LogFieldIPAddress, c.edgeAddress). Msg("Unregistered tunnel connection") return shutdownError } func (c *controlStream) IsStopped() bool { return c.stoppedGracefully } ================================================ FILE: connection/errors.go ================================================ package connection import ( tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs" ) const ( DuplicateConnectionError = "EDUPCONN" ) type DupConnRegisterTunnelError struct{} var errDuplicationConnection = DupConnRegisterTunnelError{} func (e DupConnRegisterTunnelError) Error() string { return "already connected to this server, trying another address" } // Dial to edge server with quic failed type EdgeQuicDialError struct { Cause error } func (e *EdgeQuicDialError) Error() string { return "failed to dial to edge with quic: " + e.Cause.Error() } func (e *EdgeQuicDialError) Unwrap() error { return e.Cause } // RegisterTunnel error from server type ServerRegisterTunnelError struct { Cause error Permanent bool } func (e ServerRegisterTunnelError) Error() string { return e.Cause.Error() } func serverRegistrationErrorFromRPC(err error) ServerRegisterTunnelError { if retryable, ok := err.(*tunnelpogs.RetryableError); ok { return ServerRegisterTunnelError{ Cause: retryable.Unwrap(), Permanent: false, } } return ServerRegisterTunnelError{ Cause: err, Permanent: true, } } type ControlStreamError struct{} var _ error = &ControlStreamError{} func (e *ControlStreamError) Error() string { return "control stream encountered a failure while serving" } type StreamListenerError struct{} var _ error = &StreamListenerError{} func (e *StreamListenerError) Error() string { return "accept stream listener encountered a failure while serving" } type DatagramManagerError struct{} var _ error = &DatagramManagerError{} func (e *DatagramManagerError) Error() string { return "datagram manager encountered a failure while serving" } ================================================ FILE: connection/event.go ================================================ package connection import "net" // Event is something that happened to a connection, e.g. disconnection or registration. type Event struct { Index uint8 EventType Status Location string Protocol Protocol URL string EdgeAddress net.IP } // Status is the status of a connection. type Status int const ( // Disconnected means the connection to the edge was broken. Disconnected Status = iota // Connected means the connection to the edge was successfully established. Connected // Reconnecting means the connection to the edge is being re-established. Reconnecting // SetURL means this connection's tunnel was given a URL by the edge. Used for quick tunnels. SetURL // RegisteringTunnel means the non-named tunnel is registering its connection. RegisteringTunnel // We're unregistering tunnel from the edge in preparation for a disconnect Unregistering ) ================================================ FILE: connection/header.go ================================================ package connection import ( "encoding/base64" "fmt" "net/http" "strings" "github.com/pkg/errors" ) var ( // internal special headers RequestUserHeaders = "cf-cloudflared-request-headers" ResponseUserHeaders = "cf-cloudflared-response-headers" ResponseMetaHeader = "cf-cloudflared-response-meta" // internal special headers CanonicalResponseUserHeaders = http.CanonicalHeaderKey(ResponseUserHeaders) CanonicalResponseMetaHeader = http.CanonicalHeaderKey(ResponseMetaHeader) ) var ( // pre-generate possible values for res responseMetaHeaderCfd = mustInitRespMetaHeader("cloudflared", false) responseMetaHeaderCfdFlowRateLimited = mustInitRespMetaHeader("cloudflared", true) responseMetaHeaderOrigin = mustInitRespMetaHeader("origin", false) ) // HTTPHeader is a custom header struct that expects only ever one value for the header. // This structure is used to serialize the headers and attach them to the HTTP2 request when proxying. type HTTPHeader struct { Name string Value string } type responseMetaHeader struct { Source string `json:"src"` FlowRateLimited bool `json:"flow_rate_limited,omitempty"` } func mustInitRespMetaHeader(src string, flowRateLimited bool) string { header, err := json.Marshal(responseMetaHeader{Source: src, FlowRateLimited: flowRateLimited}) if err != nil { panic(fmt.Sprintf("Failed to serialize response meta header = %s, err: %v", src, err)) } return string(header) } var headerEncoding = base64.RawStdEncoding // IsControlResponseHeader is called in the direction of eyeball <- origin. func IsControlResponseHeader(headerName string) bool { return strings.HasPrefix(headerName, ":") || strings.HasPrefix(headerName, "cf-int-") || strings.HasPrefix(headerName, "cf-cloudflared-") || strings.HasPrefix(headerName, "cf-proxy-") } // isWebsocketClientHeader returns true if the header name is required by the client to upgrade properly func IsWebsocketClientHeader(headerName string) bool { return headerName == "sec-websocket-accept" || headerName == "connection" || headerName == "upgrade" } // Serialize HTTP1.x headers by base64-encoding each header name and value, // and then joining them in the format of [key:value;] func SerializeHeaders(h1Headers http.Header) string { // compute size of the fully serialized value and largest temp buffer we will need serializedLen := 0 maxTempLen := 0 for headerName, headerValues := range h1Headers { for _, headerValue := range headerValues { nameLen := headerEncoding.EncodedLen(len(headerName)) valueLen := headerEncoding.EncodedLen(len(headerValue)) const delims = 2 serializedLen += delims + nameLen + valueLen if nameLen > maxTempLen { maxTempLen = nameLen } if valueLen > maxTempLen { maxTempLen = valueLen } } } var buf strings.Builder buf.Grow(serializedLen) temp := make([]byte, maxTempLen) writeB64 := func(s string) { n := headerEncoding.EncodedLen(len(s)) if n > len(temp) { temp = make([]byte, n) } headerEncoding.Encode(temp[:n], []byte(s)) buf.Write(temp[:n]) } for headerName, headerValues := range h1Headers { for _, headerValue := range headerValues { if buf.Len() > 0 { buf.WriteByte(';') } writeB64(headerName) buf.WriteByte(':') writeB64(headerValue) } } return buf.String() } // Deserialize headers serialized by `SerializeHeader` func DeserializeHeaders(serializedHeaders string) ([]HTTPHeader, error) { const unableToDeserializeErr = "Unable to deserialize headers" deserialized := make([]HTTPHeader, 0) for _, serializedPair := range strings.Split(serializedHeaders, ";") { if len(serializedPair) == 0 { continue } serializedHeaderParts := strings.Split(serializedPair, ":") if len(serializedHeaderParts) != 2 { return nil, errors.New(unableToDeserializeErr) } serializedName := serializedHeaderParts[0] serializedValue := serializedHeaderParts[1] deserializedName := make([]byte, headerEncoding.DecodedLen(len(serializedName))) deserializedValue := make([]byte, headerEncoding.DecodedLen(len(serializedValue))) if _, err := headerEncoding.Decode(deserializedName, []byte(serializedName)); err != nil { return nil, errors.Wrap(err, unableToDeserializeErr) } if _, err := headerEncoding.Decode(deserializedValue, []byte(serializedValue)); err != nil { return nil, errors.Wrap(err, unableToDeserializeErr) } deserialized = append(deserialized, HTTPHeader{ Name: string(deserializedName), Value: string(deserializedValue), }) } return deserialized, nil } ================================================ FILE: connection/header_test.go ================================================ package connection import ( "net/http" "reflect" "sort" "testing" "github.com/stretchr/testify/require" ) func TestSerializeHeaders(t *testing.T) { request, err := http.NewRequest(http.MethodGet, "http://example.com", nil) require.NoError(t, err) mockHeaders := http.Header{ "Mock-Header-One": {"Mock header one value", "three"}, "Mock-Header-Two-Long": {"Mock header two value\nlong"}, ":;": {":;", ";:"}, ":": {":"}, ";": {";"}, ";;": {";;"}, "Empty values": {"", ""}, "": {"Empty key"}, "control\tcharacter\b\n": {"value\n\b\t"}, ";\v:": {":\v;"}, } for header, values := range mockHeaders { for _, value := range values { // Note that Golang's http library is opinionated; // at this point every header name will be title-cased in order to comply with the HTTP RFC // This means our proxy is not completely transparent when it comes to proxying headers request.Header.Add(header, value) } } serializedHeaders := SerializeHeaders(request.Header) // Sanity check: the headers serialized to something that's not an empty string require.NotEqual(t, "", serializedHeaders) // Deserialize back, and ensure we get the same set of headers deserializedHeaders, err := DeserializeHeaders(serializedHeaders) require.NoError(t, err) require.Len(t, deserializedHeaders, 13) expectedHeaders := headerToReqHeader(mockHeaders) sort.Sort(ByName(deserializedHeaders)) sort.Sort(ByName(expectedHeaders)) require.True( t, reflect.DeepEqual(expectedHeaders, deserializedHeaders), "got = %#v, want = %#v\n", deserializedHeaders, expectedHeaders, ) } type ByName []HTTPHeader func (a ByName) Len() int { return len(a) } func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a ByName) Less(i, j int) bool { if a[i].Name == a[j].Name { return a[i].Value < a[j].Value } return a[i].Name < a[j].Name } func headerToReqHeader(headers http.Header) (reqHeaders []HTTPHeader) { for name, values := range headers { for _, value := range values { reqHeaders = append(reqHeaders, HTTPHeader{Name: name, Value: value}) } } return reqHeaders } func TestSerializeNoHeaders(t *testing.T) { request, err := http.NewRequest(http.MethodGet, "http://example.com", nil) require.NoError(t, err) serializedHeaders := SerializeHeaders(request.Header) deserializedHeaders, err := DeserializeHeaders(serializedHeaders) require.NoError(t, err) require.Empty(t, deserializedHeaders) } func TestDeserializeMalformed(t *testing.T) { var err error malformedData := []string{ "malformed data", "bW9jawo=", // "mock" "bW9jawo=:ZGF0YQo=:bW9jawo=", // "mock:data:mock" "::", } for _, malformedValue := range malformedData { _, err = DeserializeHeaders(malformedValue) require.Error(t, err) } } func TestIsControlResponseHeader(t *testing.T) { controlResponseHeaders := []string{ // Anything that begins with cf-int-, cf-cloudflared- or cf-proxy- "cf-int-sample-header", "cf-cloudflared-sample-header", "cf-proxy-sample-header", // Any http2 pseudoheader ":sample-pseudo-header", } for _, header := range controlResponseHeaders { require.True(t, IsControlResponseHeader(header)) } } func TestIsNotControlResponseHeader(t *testing.T) { notControlResponseHeaders := []string{ "mock-header", "another-sample-header", "upgrade", "connection", "cf-whatever", // On the response path, we only want to filter cf-int- and cf-cloudflared- } for _, header := range notControlResponseHeaders { require.False(t, IsControlResponseHeader(header)) } } ================================================ FILE: connection/http2.go ================================================ package connection import ( "bufio" "context" gojson "encoding/json" "fmt" "io" "net" "net/http" "runtime/debug" "strings" "sync" "github.com/pkg/errors" "github.com/rs/zerolog" "golang.org/x/net/http2" "github.com/cloudflare/cloudflared/client" cfdflow "github.com/cloudflare/cloudflared/flow" "github.com/cloudflare/cloudflared/tracing" ) // note: these constants are exported so we can reuse them in the edge-side code const ( InternalUpgradeHeader = "Cf-Cloudflared-Proxy-Connection-Upgrade" InternalTCPProxySrcHeader = "Cf-Cloudflared-Proxy-Src" WebsocketUpgrade = "websocket" ControlStreamUpgrade = "control-stream" ConfigurationUpdate = "update-configuration" ) var errEdgeConnectionClosed = fmt.Errorf("connection with edge closed") // HTTP2Connection represents a net.Conn that uses HTTP2 frames to proxy traffic from the edge to cloudflared on the // origin. type HTTP2Connection struct { conn net.Conn server *http2.Server orchestrator Orchestrator connOptions *client.ConnectionOptionsSnapshot observer *Observer connIndex uint8 log *zerolog.Logger activeRequestsWG sync.WaitGroup controlStreamHandler ControlStreamHandler stoppedGracefully bool controlStreamErr error // result of running control stream handler } // NewHTTP2Connection returns a new instance of HTTP2Connection. func NewHTTP2Connection( conn net.Conn, orchestrator Orchestrator, connOptions *client.ConnectionOptionsSnapshot, observer *Observer, connIndex uint8, controlStreamHandler ControlStreamHandler, log *zerolog.Logger, ) *HTTP2Connection { return &HTTP2Connection{ conn: conn, server: &http2.Server{ MaxConcurrentStreams: MaxConcurrentStreams, }, orchestrator: orchestrator, connOptions: connOptions, observer: observer, connIndex: connIndex, controlStreamHandler: controlStreamHandler, log: log, } } // Serve serves an HTTP2 server that the edge can talk to. func (c *HTTP2Connection) Serve(ctx context.Context) error { go func() { <-ctx.Done() c.close() }() c.server.ServeConn(c.conn, &http2.ServeConnOpts{ Context: ctx, Handler: c, }) switch { case c.controlStreamHandler.IsStopped(): return nil case c.controlStreamErr != nil: return c.controlStreamErr default: c.observer.log.Info().Uint8(LogFieldConnIndex, c.connIndex).Msg("Lost connection with the edge") return errEdgeConnectionClosed } } func (c *HTTP2Connection) ServeHTTP(w http.ResponseWriter, r *http.Request) { c.activeRequestsWG.Add(1) defer c.activeRequestsWG.Done() connType := determineHTTP2Type(r) handleMissingRequestParts(connType, r) respWriter, err := NewHTTP2RespWriter(r, w, connType, c.log) if err != nil { c.observer.log.Error().Msg(err.Error()) return } originProxy, err := c.orchestrator.GetOriginProxy() if err != nil { c.observer.log.Error().Msg(err.Error()) return } var requestErr error switch connType { case TypeControlStream: requestErr = c.controlStreamHandler.ServeControlStream(r.Context(), respWriter, c.connOptions.ConnectionOptions(), c.orchestrator) if requestErr != nil { c.controlStreamErr = requestErr } case TypeConfiguration: requestErr = c.handleConfigurationUpdate(respWriter, r) case TypeWebsocket, TypeHTTP: stripWebsocketUpgradeHeader(r) // Check for tracing on request tr := tracing.NewTracedHTTPRequest(r, c.connIndex, c.log) if err := originProxy.ProxyHTTP(respWriter, tr, connType == TypeWebsocket); err != nil { requestErr = fmt.Errorf("Failed to proxy HTTP: %w", err) } case TypeTCP: host, err := getRequestHost(r) if err != nil { requestErr = fmt.Errorf(`cloudflared received a warp-routing request with an empty host value: %w`, err) break } rws := NewHTTPResponseReadWriterAcker(respWriter, respWriter, r) requestErr = originProxy.ProxyTCP(r.Context(), rws, &TCPRequest{ Dest: host, CFRay: FindCfRayHeader(r), LBProbe: IsLBProbeRequest(r), CfTraceID: r.Header.Get(tracing.TracerContextName), ConnIndex: c.connIndex, }) default: requestErr = fmt.Errorf("Received unknown connection type: %s", connType) } if requestErr != nil { c.log.Error().Err(requestErr).Msg("failed to serve incoming request") // WriteErrorResponse will return false if status was already written. we need to abort handler. if !respWriter.WriteErrorResponse(requestErr) { c.log.Debug().Msg("Handler aborted due to failure to write error response after status already sent") panic(http.ErrAbortHandler) } } } // ConfigurationUpdateBody is the representation followed by the edge to send updates to cloudflared. type ConfigurationUpdateBody struct { Version int32 `json:"version"` Config gojson.RawMessage `json:"config"` } func (c *HTTP2Connection) handleConfigurationUpdate(respWriter *http2RespWriter, r *http.Request) error { var configBody ConfigurationUpdateBody if err := json.NewDecoder(r.Body).Decode(&configBody); err != nil { return err } resp := c.orchestrator.UpdateConfig(configBody.Version, configBody.Config) bdy, err := json.Marshal(resp) if err != nil { return err } _, err = respWriter.Write(bdy) return err } func (c *HTTP2Connection) close() { // Wait for all serve HTTP handlers to return c.activeRequestsWG.Wait() c.conn.Close() } type http2RespWriter struct { r io.Reader w http.ResponseWriter flusher http.Flusher shouldFlush bool statusWritten bool respHeaders http.Header hijackedMutex sync.Mutex hijackedv bool log *zerolog.Logger } func NewHTTP2RespWriter(r *http.Request, w http.ResponseWriter, connType Type, log *zerolog.Logger) (*http2RespWriter, error) { flusher, isFlusher := w.(http.Flusher) if !isFlusher { respWriter := &http2RespWriter{ r: r.Body, w: w, log: log, } err := fmt.Errorf("%T doesn't implement http.Flusher", w) respWriter.WriteErrorResponse(err) return nil, err } return &http2RespWriter{ r: r.Body, w: w, flusher: flusher, shouldFlush: connType.shouldFlush(), respHeaders: make(http.Header), log: log, }, nil } func (rp *http2RespWriter) AddTrailer(trailerName, trailerValue string) { if !rp.statusWritten { rp.log.Warn().Msg("Tried to add Trailer to response before status written. Ignoring...") return } rp.w.Header().Add(http2.TrailerPrefix+trailerName, trailerValue) } func (rp *http2RespWriter) WriteRespHeaders(status int, header http.Header) error { if rp.hijacked() { rp.log.Warn().Msg("WriteRespHeaders after hijack") return nil } dest := rp.w.Header() userHeaders := make(http.Header, len(header)) for name, values := range header { // lowercase headers for simplicity check h2name := strings.ToLower(name) if h2name == "content-length" { // This header has meaning in HTTP/2 and will be used by the edge, // so it should be sent *also* as an HTTP/2 response header. dest[name] = values } if h2name == tracing.IntCloudflaredTracingHeader { // Add cf-int-cloudflared-tracing header outside of serialized userHeaders dest[tracing.CanonicalCloudflaredTracingHeader] = values continue } if !IsControlResponseHeader(h2name) || IsWebsocketClientHeader(h2name) { // User headers, on the other hand, must all be serialized so that // HTTP/2 header validation won't be applied to HTTP/1 header values userHeaders[name] = values } } // Perform user header serialization and set them in the single header dest.Set(CanonicalResponseUserHeaders, SerializeHeaders(userHeaders)) rp.setResponseMetaHeader(responseMetaHeaderOrigin) // HTTP2 removes support for 101 Switching Protocols https://tools.ietf.org/html/rfc7540#section-8.1.1 if status == http.StatusSwitchingProtocols { status = http.StatusOK } rp.w.WriteHeader(status) if shouldFlush(header) { rp.shouldFlush = true } if rp.shouldFlush { rp.flusher.Flush() } rp.statusWritten = true return nil } func (rp *http2RespWriter) Header() http.Header { return rp.respHeaders } func (rp *http2RespWriter) Flush() { rp.flusher.Flush() } func (rp *http2RespWriter) WriteHeader(status int) { if rp.hijacked() { rp.log.Warn().Msg("WriteHeader after hijack") return } _ = rp.WriteRespHeaders(status, rp.respHeaders) } func (rp *http2RespWriter) hijacked() bool { rp.hijackedMutex.Lock() defer rp.hijackedMutex.Unlock() return rp.hijackedv } func (rp *http2RespWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { if !rp.statusWritten { return nil, nil, fmt.Errorf("status not yet written before attempting to hijack connection") } // Make sure to flush anything left in the buffer before hijacking if rp.shouldFlush { rp.flusher.Flush() } rp.hijackedMutex.Lock() defer rp.hijackedMutex.Unlock() if rp.hijackedv { return nil, nil, http.ErrHijacked } rp.hijackedv = true conn := &localProxyConnection{rp} // We return the http2RespWriter here because we want to make sure that we flush after every write // otherwise the HTTP2 write buffer waits a few seconds before sending. readWriter := bufio.NewReadWriter( bufio.NewReader(rp), bufio.NewWriter(rp), ) return conn, readWriter, nil } func (rp *http2RespWriter) WriteErrorResponse(err error) bool { if rp.statusWritten { return false } if errors.Is(err, cfdflow.ErrTooManyActiveFlows) { rp.setResponseMetaHeader(responseMetaHeaderCfdFlowRateLimited) } else { rp.setResponseMetaHeader(responseMetaHeaderCfd) } rp.w.WriteHeader(http.StatusBadGateway) rp.statusWritten = true return true } func (rp *http2RespWriter) setResponseMetaHeader(value string) { rp.w.Header().Set(CanonicalResponseMetaHeader, value) } func (rp *http2RespWriter) Read(p []byte) (n int, err error) { return rp.r.Read(p) } func (rp *http2RespWriter) Write(p []byte) (n int, err error) { defer func() { // Implementer of OriginClient should make sure it doesn't write to the connection after Proxy returns // Register a recover routine just in case. if r := recover(); r != nil { rp.log.Debug().Msgf("Recover from http2 response writer panic, error %s", debug.Stack()) } }() n, err = rp.w.Write(p) if err == nil && rp.shouldFlush { rp.flusher.Flush() } return n, err } func (rp *http2RespWriter) Close() error { return nil } func determineHTTP2Type(r *http.Request) Type { switch { case isConfigurationUpdate(r): return TypeConfiguration case isWebsocketUpgrade(r): return TypeWebsocket case IsTCPStream(r): return TypeTCP case isControlStreamUpgrade(r): return TypeControlStream default: return TypeHTTP } } func handleMissingRequestParts(connType Type, r *http.Request) { if connType == TypeHTTP { // http library has no guarantees that we receive a filled URL. If not, then we fill it, as we reuse the request // for proxying. For proxying they should not matter since we control the dialer on every egress proxied. if len(r.URL.Scheme) == 0 { r.URL.Scheme = "http" } if len(r.URL.Host) == 0 { r.URL.Host = "localhost:8080" } } } func isControlStreamUpgrade(r *http.Request) bool { return r.Header.Get(InternalUpgradeHeader) == ControlStreamUpgrade } func isWebsocketUpgrade(r *http.Request) bool { return r.Header.Get(InternalUpgradeHeader) == WebsocketUpgrade } func isConfigurationUpdate(r *http.Request) bool { return r.Header.Get(InternalUpgradeHeader) == ConfigurationUpdate } // IsTCPStream discerns if the connection request needs a tcp stream proxy. func IsTCPStream(r *http.Request) bool { return r.Header.Get(InternalTCPProxySrcHeader) != "" } func stripWebsocketUpgradeHeader(r *http.Request) { r.Header.Del(InternalUpgradeHeader) } // getRequestHost returns the host of the http.Request. func getRequestHost(r *http.Request) (string, error) { if r.Host != "" { return r.Host, nil } if r.URL != nil { return r.URL.Host, nil } return "", errors.New("host not set in incoming request") } ================================================ FILE: connection/http2_test.go ================================================ package connection import ( "bytes" "context" "errors" "fmt" "io" "net" "net/http" "net/http/httptest" "sync" "testing" "time" "github.com/gobwas/ws/wsutil" "github.com/google/uuid" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/net/http2" "github.com/cloudflare/cloudflared/client" "github.com/cloudflare/cloudflared/tracing" "github.com/cloudflare/cloudflared/tunnelrpc" "github.com/cloudflare/cloudflared/tunnelrpc/pogs" ) var testTransport = http2.Transport{} func newTestHTTP2Connection() (*HTTP2Connection, net.Conn) { edgeConn, cfdConn := net.Pipe() connIndex := uint8(0) log := zerolog.Nop() obs := NewObserver(&log, &log) controlStream := NewControlStream( obs, mockConnectedFuse{}, &TunnelProperties{}, connIndex, nil, nil, 1*time.Second, nil, 1*time.Second, HTTP2, ) return NewHTTP2Connection( cfdConn, // OriginProxy is set in testConfigManager testOrchestrator, &client.ConnectionOptionsSnapshot{}, obs, connIndex, controlStream, &log, ), edgeConn } func TestHTTP2ConfigurationSet(t *testing.T) { http2Conn, edgeConn := newTestHTTP2Connection() ctx, cancel := context.WithCancel(t.Context()) var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() _ = http2Conn.Serve(ctx) }() edgeHTTP2Conn, err := testTransport.NewClientConn(edgeConn) require.NoError(t, err) reqBody := []byte(`{ "version": 2, "config": {"warp-routing": {"enabled": true}, "originRequest" : {"connectTimeout": 10}, "ingress" : [ {"hostname": "test", "service": "https://localhost:8000" } , {"service": "http_status:404"} ]}} `) reader := bytes.NewReader(reqBody) req, err := http.NewRequestWithContext(ctx, http.MethodPut, "http://localhost:8080/ok", reader) require.NoError(t, err) req.Header.Set(InternalUpgradeHeader, ConfigurationUpdate) resp, err := edgeHTTP2Conn.RoundTrip(req) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.StatusCode) bdy, err := io.ReadAll(resp.Body) defer resp.Body.Close() require.NoError(t, err) assert.Equal(t, `{"lastAppliedVersion":2,"err":null}`, string(bdy)) cancel() wg.Wait() } func TestServeHTTP(t *testing.T) { tests := []testRequest{ { name: "ok", endpoint: "ok", expectedStatus: http.StatusOK, expectedBody: []byte(http.StatusText(http.StatusOK)), }, { name: "large_file", endpoint: "large_file", expectedStatus: http.StatusOK, expectedBody: testLargeResp, }, { name: "Bad request", endpoint: "400", expectedStatus: http.StatusBadRequest, expectedBody: []byte(http.StatusText(http.StatusBadRequest)), }, { name: "Internal server error", endpoint: "500", expectedStatus: http.StatusInternalServerError, expectedBody: []byte(http.StatusText(http.StatusInternalServerError)), }, { name: "Proxy error", endpoint: "error", expectedStatus: http.StatusBadGateway, expectedBody: nil, isProxyError: true, }, } http2Conn, edgeConn := newTestHTTP2Connection() ctx, cancel := context.WithCancel(t.Context()) var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() _ = http2Conn.Serve(ctx) }() edgeHTTP2Conn, err := testTransport.NewClientConn(edgeConn) require.NoError(t, err) for _, test := range tests { endpoint := fmt.Sprintf("http://localhost:8080/%s", test.endpoint) req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) require.NoError(t, err) resp, err := edgeHTTP2Conn.RoundTrip(req) require.NoError(t, err) require.Equal(t, test.expectedStatus, resp.StatusCode) if test.expectedBody != nil { respBody, err := io.ReadAll(resp.Body) require.NoError(t, err) require.Equal(t, test.expectedBody, respBody) } _ = resp.Body.Close() if test.isProxyError { require.Equal(t, responseMetaHeaderCfd, resp.Header.Get(ResponseMetaHeader)) } else { require.Equal(t, responseMetaHeaderOrigin, resp.Header.Get(ResponseMetaHeader)) } } cancel() wg.Wait() } type mockNamedTunnelRPCClient struct { shouldFail error registered chan struct{} unregistered chan struct{} } func (mc mockNamedTunnelRPCClient) SendLocalConfiguration(c context.Context, config []byte) error { return nil } func (mc mockNamedTunnelRPCClient) RegisterConnection( ctx context.Context, auth pogs.TunnelAuth, tunnelID uuid.UUID, options *pogs.ConnectionOptions, connIndex uint8, edgeAddress net.IP, ) (*pogs.ConnectionDetails, error) { if mc.shouldFail != nil { return nil, mc.shouldFail } close(mc.registered) return &pogs.ConnectionDetails{ Location: "LIS", UUID: uuid.New(), TunnelIsRemotelyManaged: false, }, nil } func (mc mockNamedTunnelRPCClient) GracefulShutdown(ctx context.Context, gracePeriod time.Duration) error { close(mc.unregistered) return nil } func (mockNamedTunnelRPCClient) Close() {} type mockRPCClientFactory struct { shouldFail error registered chan struct{} unregistered chan struct{} } func (mf *mockRPCClientFactory) newMockRPCClient(context.Context, io.ReadWriteCloser, time.Duration) tunnelrpc.RegistrationClient { return &mockNamedTunnelRPCClient{ shouldFail: mf.shouldFail, registered: mf.registered, unregistered: mf.unregistered, } } type wsRespWriter struct { *httptest.ResponseRecorder readPipe *io.PipeReader writePipe *io.PipeWriter closed bool panicked bool } func newWSRespWriter() *wsRespWriter { readPipe, writePipe := io.Pipe() return &wsRespWriter{ httptest.NewRecorder(), readPipe, writePipe, false, false, } } type nowriter struct { io.Reader } func (nowriter) Write(_ []byte) (int, error) { return 0, fmt.Errorf("writer not implemented") } func (w *wsRespWriter) RespBody() io.ReadWriter { return nowriter{w.readPipe} } func (w *wsRespWriter) Write(data []byte) (n int, err error) { if w.closed { w.panicked = true return 0, errors.New("wsRespWriter panicked") } return w.writePipe.Write(data) } func (w *wsRespWriter) close() { w.closed = true } func TestServeWS(t *testing.T) { http2Conn, _ := newTestHTTP2Connection() ctx, cancel := context.WithCancel(t.Context()) respWriter := newWSRespWriter() readPipe, writePipe := io.Pipe() req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost:8080/ws/echo", readPipe) require.NoError(t, err) req.Header.Set(InternalUpgradeHeader, WebsocketUpgrade) serveDone := make(chan struct{}) go func() { defer close(serveDone) http2Conn.ServeHTTP(respWriter, req) respWriter.close() }() data := []byte("test websocket") err = wsutil.WriteClientBinary(writePipe, data) require.NoError(t, err) respBody, err := wsutil.ReadServerBinary(respWriter.RespBody()) require.NoError(t, err) require.Equal(t, data, respBody, "expect %s, got %s", string(data), string(respBody)) cancel() resp := respWriter.Result() defer resp.Body.Close() // http2RespWriter should rewrite status 101 to 200 require.Equal(t, http.StatusOK, resp.StatusCode) require.Equal(t, responseMetaHeaderOrigin, resp.Header.Get(ResponseMetaHeader)) <-serveDone require.False(t, respWriter.panicked) } // TestNoWriteAfterServeHTTPReturns is a regression test of https://jira.cfdata.org/browse/TUN-5184 // to make sure we don't write to the ResponseWriter after the ServeHTTP method returns func TestNoWriteAfterServeHTTPReturns(t *testing.T) { cfdHTTP2Conn, edgeTCPConn := newTestHTTP2Connection() ctx, cancel := context.WithCancel(t.Context()) var wg sync.WaitGroup serverDone := make(chan struct{}) go func() { defer close(serverDone) _ = cfdHTTP2Conn.Serve(ctx) }() edgeTransport := http2.Transport{} edgeHTTP2Conn, err := edgeTransport.NewClientConn(edgeTCPConn) require.NoError(t, err) message := []byte(t.Name()) for i := 0; i < 100; i++ { wg.Add(1) go func() { defer wg.Done() readPipe, writePipe := io.Pipe() reqCtx, reqCancel := context.WithCancel(ctx) req, err := http.NewRequestWithContext(reqCtx, http.MethodGet, "http://localhost:8080/ws/flaky", readPipe) assert.NoError(t, err) req.Header.Set(InternalUpgradeHeader, WebsocketUpgrade) resp, err := edgeHTTP2Conn.RoundTrip(req) assert.NoError(t, err) _ = resp.Body.Close() // http2RespWriter should rewrite status 101 to 200 assert.Equal(t, http.StatusOK, resp.StatusCode) wg.Add(1) go func() { defer wg.Done() for { select { case <-reqCtx.Done(): return default: } _ = wsutil.WriteClientBinary(writePipe, message) } }() time.Sleep(time.Millisecond * 100) reqCancel() }() } wg.Wait() cancel() <-serverDone } func TestServeControlStream(t *testing.T) { http2Conn, edgeConn := newTestHTTP2Connection() rpcClientFactory := mockRPCClientFactory{ registered: make(chan struct{}), unregistered: make(chan struct{}), } obs := NewObserver(&log, &log) controlStream := NewControlStream( obs, mockConnectedFuse{}, &TunnelProperties{}, 1, nil, rpcClientFactory.newMockRPCClient, 1*time.Second, nil, 1*time.Second, HTTP2, ) http2Conn.controlStreamHandler = controlStream ctx, cancel := context.WithCancel(t.Context()) var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() _ = http2Conn.Serve(ctx) }() req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost:8080/", nil) require.NoError(t, err) req.Header.Set(InternalUpgradeHeader, ControlStreamUpgrade) edgeHTTP2Conn, err := testTransport.NewClientConn(edgeConn) require.NoError(t, err) wg.Add(1) go func() { defer wg.Done() // nolint: bodyclose _, _ = edgeHTTP2Conn.RoundTrip(req) }() <-rpcClientFactory.registered cancel() <-rpcClientFactory.unregistered assert.False(t, http2Conn.stoppedGracefully) wg.Wait() } func TestFailRegistration(t *testing.T) { http2Conn, edgeConn := newTestHTTP2Connection() rpcClientFactory := mockRPCClientFactory{ shouldFail: errDuplicationConnection, registered: make(chan struct{}), unregistered: make(chan struct{}), } obs := NewObserver(&log, &log) controlStream := NewControlStream( obs, mockConnectedFuse{}, &TunnelProperties{}, http2Conn.connIndex, nil, rpcClientFactory.newMockRPCClient, 1*time.Second, nil, 1*time.Second, HTTP2, ) http2Conn.controlStreamHandler = controlStream ctx, cancel := context.WithCancel(t.Context()) var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() _ = http2Conn.Serve(ctx) }() req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost:8080/", nil) require.NoError(t, err) req.Header.Set(InternalUpgradeHeader, ControlStreamUpgrade) edgeHTTP2Conn, err := testTransport.NewClientConn(edgeConn) require.NoError(t, err) resp, err := edgeHTTP2Conn.RoundTrip(req) require.NoError(t, err) defer resp.Body.Close() require.Equal(t, http.StatusBadGateway, resp.StatusCode) require.Error(t, http2Conn.controlStreamErr) cancel() wg.Wait() } func TestGracefulShutdownHTTP2(t *testing.T) { http2Conn, edgeConn := newTestHTTP2Connection() rpcClientFactory := mockRPCClientFactory{ registered: make(chan struct{}), unregistered: make(chan struct{}), } events := &eventCollectorSink{} shutdownC := make(chan struct{}) obs := NewObserver(&log, &log) obs.RegisterSink(events) controlStream := NewControlStream( obs, mockConnectedFuse{}, &TunnelProperties{}, http2Conn.connIndex, nil, rpcClientFactory.newMockRPCClient, 1*time.Second, shutdownC, 1*time.Second, HTTP2, ) http2Conn.controlStreamHandler = controlStream ctx, cancel := context.WithCancel(t.Context()) var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() _ = http2Conn.Serve(ctx) }() req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost:8080/", nil) require.NoError(t, err) req.Header.Set(InternalUpgradeHeader, ControlStreamUpgrade) edgeHTTP2Conn, err := testTransport.NewClientConn(edgeConn) require.NoError(t, err) wg.Add(1) go func() { defer wg.Done() // nolint: bodyclose _, _ = edgeHTTP2Conn.RoundTrip(req) }() select { case <-rpcClientFactory.registered: break // ok case <-time.Tick(time.Second): t.Fatal("timeout out waiting for registration") } // signal graceful shutdown close(shutdownC) select { case <-rpcClientFactory.unregistered: break // ok case <-time.Tick(time.Second): t.Fatal("timeout out waiting for unregistered signal") } assert.True(t, controlStream.IsStopped()) cancel() wg.Wait() events.assertSawEvent(t, Event{ Index: http2Conn.connIndex, EventType: Unregistering, }) } func TestServeTCP_RateLimited(t *testing.T) { ctx, cancel := context.WithCancel(t.Context()) http2Conn, edgeConn := newTestHTTP2Connection() var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() _ = http2Conn.Serve(ctx) }() edgeHTTP2Conn, err := testTransport.NewClientConn(edgeConn) require.NoError(t, err) req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost:8080", nil) require.NoError(t, err) req.Header.Set(InternalTCPProxySrcHeader, "tcp") req.Header.Set(tracing.TracerContextName, "flow-rate-limited") resp, err := edgeHTTP2Conn.RoundTrip(req) require.NoError(t, err) defer resp.Body.Close() require.Equal(t, http.StatusBadGateway, resp.StatusCode) require.Equal(t, responseMetaHeaderCfdFlowRateLimited, resp.Header.Get(ResponseMetaHeader)) cancel() wg.Wait() } func benchmarkServeHTTP(b *testing.B, test testRequest) { http2Conn, edgeConn := newTestHTTP2Connection() ctx, cancel := context.WithCancel(b.Context()) var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() _ = http2Conn.Serve(ctx) }() endpoint := fmt.Sprintf("http://localhost:8080/%s", test.endpoint) req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) require.NoError(b, err) edgeHTTP2Conn, err := testTransport.NewClientConn(edgeConn) require.NoError(b, err) b.ResetTimer() for i := 0; i < b.N; i++ { b.StartTimer() resp, err := edgeHTTP2Conn.RoundTrip(req) b.StopTimer() require.NoError(b, err) require.Equal(b, test.expectedStatus, resp.StatusCode) if test.expectedBody != nil { respBody, err := io.ReadAll(resp.Body) require.NoError(b, err) require.Equal(b, test.expectedBody, respBody) } resp.Body.Close() } cancel() wg.Wait() } func BenchmarkServeHTTPSimple(b *testing.B) { test := testRequest{ name: "ok", endpoint: "ok", expectedStatus: http.StatusOK, expectedBody: []byte(http.StatusText(http.StatusOK)), } benchmarkServeHTTP(b, test) } func BenchmarkServeHTTPLargeFile(b *testing.B) { test := testRequest{ name: "large_file", endpoint: "large_file", expectedStatus: http.StatusOK, expectedBody: testLargeResp, } benchmarkServeHTTP(b, test) } ================================================ FILE: connection/json.go ================================================ package connection import ( jsoniter "github.com/json-iterator/go" ) var json = jsoniter.ConfigFastest ================================================ FILE: connection/metrics.go ================================================ package connection import ( "sync" "github.com/prometheus/client_golang/prometheus" ) const ( MetricsNamespace = "cloudflared" TunnelSubsystem = "tunnel" muxerSubsystem = "muxer" configSubsystem = "config" ) type localConfigMetrics struct { pushes prometheus.Counter pushesErrors prometheus.Counter } type tunnelMetrics struct { serverLocations *prometheus.GaugeVec // locationLock is a mutex for oldServerLocations locationLock sync.Mutex // oldServerLocations stores the last server the tunnel was connected to oldServerLocations map[string]string regSuccess *prometheus.CounterVec regFail *prometheus.CounterVec rpcFail *prometheus.CounterVec tunnelsHA tunnelsForHA userHostnamesCounts *prometheus.CounterVec localConfigMetrics *localConfigMetrics } func newLocalConfigMetrics() *localConfigMetrics { pushesMetric := prometheus.NewCounter( prometheus.CounterOpts{ Namespace: MetricsNamespace, Subsystem: configSubsystem, Name: "local_config_pushes", Help: "Number of local configuration pushes to the edge", }, ) pushesErrorsMetric := prometheus.NewCounter( prometheus.CounterOpts{ Namespace: MetricsNamespace, Subsystem: configSubsystem, Name: "local_config_pushes_errors", Help: "Number of errors occurred during local configuration pushes", }, ) prometheus.MustRegister( pushesMetric, pushesErrorsMetric, ) return &localConfigMetrics{ pushes: pushesMetric, pushesErrors: pushesErrorsMetric, } } // Metrics that can be collected without asking the edge func initTunnelMetrics() *tunnelMetrics { maxConcurrentRequestsPerTunnel := prometheus.NewGaugeVec( prometheus.GaugeOpts{ Namespace: MetricsNamespace, Subsystem: TunnelSubsystem, Name: "max_concurrent_requests_per_tunnel", Help: "Largest number of concurrent requests proxied through each tunnel so far", }, []string{"connection_id"}, ) prometheus.MustRegister(maxConcurrentRequestsPerTunnel) serverLocations := prometheus.NewGaugeVec( prometheus.GaugeOpts{ Namespace: MetricsNamespace, Subsystem: TunnelSubsystem, Name: "server_locations", Help: "Where each tunnel is connected to. 1 means current location, 0 means previous locations.", }, []string{"connection_id", "edge_location"}, ) prometheus.MustRegister(serverLocations) rpcFail := prometheus.NewCounterVec( prometheus.CounterOpts{ Namespace: MetricsNamespace, Subsystem: TunnelSubsystem, Name: "tunnel_rpc_fail", Help: "Count of RPC connection errors by type", }, []string{"error", "rpcName"}, ) prometheus.MustRegister(rpcFail) registerFail := prometheus.NewCounterVec( prometheus.CounterOpts{ Namespace: MetricsNamespace, Subsystem: TunnelSubsystem, Name: "tunnel_register_fail", Help: "Count of tunnel registration errors by type", }, []string{"error", "rpcName"}, ) prometheus.MustRegister(registerFail) userHostnamesCounts := prometheus.NewCounterVec( prometheus.CounterOpts{ Namespace: MetricsNamespace, Subsystem: TunnelSubsystem, Name: "user_hostnames_counts", Help: "Which user hostnames cloudflared is serving", }, []string{"userHostname"}, ) prometheus.MustRegister(userHostnamesCounts) registerSuccess := prometheus.NewCounterVec( prometheus.CounterOpts{ Namespace: MetricsNamespace, Subsystem: TunnelSubsystem, Name: "tunnel_register_success", Help: "Count of successful tunnel registrations", }, []string{"rpcName"}, ) prometheus.MustRegister(registerSuccess) return &tunnelMetrics{ serverLocations: serverLocations, oldServerLocations: make(map[string]string), tunnelsHA: newTunnelsForHA(), regSuccess: registerSuccess, regFail: registerFail, rpcFail: rpcFail, userHostnamesCounts: userHostnamesCounts, localConfigMetrics: newLocalConfigMetrics(), } } func (t *tunnelMetrics) registerServerLocation(connectionID, loc string) { t.locationLock.Lock() defer t.locationLock.Unlock() if oldLoc, ok := t.oldServerLocations[connectionID]; ok && oldLoc == loc { return } else if ok { t.serverLocations.WithLabelValues(connectionID, oldLoc).Dec() } t.serverLocations.WithLabelValues(connectionID, loc).Inc() t.oldServerLocations[connectionID] = loc } var tunnelMetricsInternal struct { sync.Once metrics *tunnelMetrics } func newTunnelMetrics() *tunnelMetrics { tunnelMetricsInternal.Do(func() { tunnelMetricsInternal.metrics = initTunnelMetrics() }) return tunnelMetricsInternal.metrics } ================================================ FILE: connection/observer.go ================================================ package connection import ( "net" "strings" "github.com/google/uuid" "github.com/rs/zerolog" "github.com/cloudflare/cloudflared/management" ) const ( LogFieldConnectionID = "connection" LogFieldLocation = "location" LogFieldIPAddress = "ip" LogFieldProtocol = "protocol" observerChannelBufferSize = 16 ) type Observer struct { log *zerolog.Logger logTransport *zerolog.Logger metrics *tunnelMetrics tunnelEventChan chan Event addSinkChan chan EventSink } type EventSink interface { OnTunnelEvent(event Event) } func NewObserver(log, logTransport *zerolog.Logger) *Observer { o := &Observer{ log: log, logTransport: logTransport, metrics: newTunnelMetrics(), tunnelEventChan: make(chan Event, observerChannelBufferSize), addSinkChan: make(chan EventSink, observerChannelBufferSize), } go o.dispatchEvents() return o } func (o *Observer) RegisterSink(sink EventSink) { o.addSinkChan <- sink } func (o *Observer) logConnecting(connIndex uint8, address net.IP, protocol Protocol) { o.log.Debug(). Int(management.EventTypeKey, int(management.Cloudflared)). Uint8(LogFieldConnIndex, connIndex). IPAddr(LogFieldIPAddress, address). Str(LogFieldProtocol, protocol.String()). Msg("Registering tunnel connection") } func (o *Observer) logConnected(connectionID uuid.UUID, connIndex uint8, location string, address net.IP, protocol Protocol) { o.log.Info(). Int(management.EventTypeKey, int(management.Cloudflared)). Str(LogFieldConnectionID, connectionID.String()). Uint8(LogFieldConnIndex, connIndex). Str(LogFieldLocation, location). IPAddr(LogFieldIPAddress, address). Str(LogFieldProtocol, protocol.String()). Msg("Registered tunnel connection") o.metrics.registerServerLocation(uint8ToString(connIndex), location) } func (o *Observer) sendRegisteringEvent(connIndex uint8) { o.sendEvent(Event{Index: connIndex, EventType: RegisteringTunnel}) } func (o *Observer) sendConnectedEvent(connIndex uint8, protocol Protocol, location string, edgeAddress net.IP) { o.sendEvent(Event{Index: connIndex, EventType: Connected, Protocol: protocol, Location: location, EdgeAddress: edgeAddress}) } func (o *Observer) SendURL(url string) { o.sendEvent(Event{EventType: SetURL, URL: url}) if !strings.HasPrefix(url, "https://") { // We add https:// in the prefix for backwards compatibility as we used to do that with the old free tunnels // and some tools (like `wrangler tail`) are regexp-ing for that specifically. url = "https://" + url } o.metrics.userHostnamesCounts.WithLabelValues(url).Inc() } func (o *Observer) SendReconnect(connIndex uint8) { o.sendEvent(Event{Index: connIndex, EventType: Reconnecting}) } func (o *Observer) sendUnregisteringEvent(connIndex uint8) { o.sendEvent(Event{Index: connIndex, EventType: Unregistering}) } func (o *Observer) SendDisconnect(connIndex uint8) { o.sendEvent(Event{Index: connIndex, EventType: Disconnected}) } func (o *Observer) sendEvent(e Event) { select { case o.tunnelEventChan <- e: break default: o.log.Warn().Msg("observer channel buffer is full") } } func (o *Observer) dispatchEvents() { var sinks []EventSink for { select { case sink := <-o.addSinkChan: sinks = append(sinks, sink) case evt := <-o.tunnelEventChan: for _, sink := range sinks { sink.OnTunnelEvent(evt) } } } } type EventSinkFunc func(event Event) func (f EventSinkFunc) OnTunnelEvent(event Event) { f(event) } ================================================ FILE: connection/observer_test.go ================================================ package connection import ( "strconv" "sync" "testing" "time" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" "github.com/stretchr/testify/assert" ) func TestSendUrl(t *testing.T) { observer := NewObserver(&log, &log) observer.SendURL("my-url.com") assert.Equal(t, 1.0, getCounterValue(t, observer.metrics.userHostnamesCounts, "https://my-url.com")) observer.SendURL("https://another-long-one.com") assert.Equal(t, 1.0, getCounterValue(t, observer.metrics.userHostnamesCounts, "https://another-long-one.com")) } func getCounterValue(t *testing.T, metric *prometheus.CounterVec, val string) float64 { var m = &dto.Metric{} err := metric.WithLabelValues(val).Write(m) assert.NoError(t, err) return m.Counter.GetValue() } func TestRegisterServerLocation(t *testing.T) { m := newTunnelMetrics() tunnels := 20 var wg sync.WaitGroup wg.Add(tunnels) for i := 0; i < tunnels; i++ { go func(i int) { id := strconv.Itoa(i) m.registerServerLocation(id, "LHR") wg.Done() }(i) } wg.Wait() for i := 0; i < tunnels; i++ { id := strconv.Itoa(i) assert.Equal(t, "LHR", m.oldServerLocations[id]) } wg.Add(tunnels) for i := 0; i < tunnels; i++ { go func(i int) { id := strconv.Itoa(i) m.registerServerLocation(id, "AUS") wg.Done() }(i) } wg.Wait() for i := 0; i < tunnels; i++ { id := strconv.Itoa(i) assert.Equal(t, "AUS", m.oldServerLocations[id]) } } func TestObserverEventsDontBlock(t *testing.T) { observer := NewObserver(&log, &log) var mu sync.Mutex observer.RegisterSink(EventSinkFunc(func(_ Event) { // callback will block if lock is already held mu.Lock() mu.Unlock() })) timeout := time.AfterFunc(5*time.Second, func() { mu.Unlock() // release the callback on timer expiration t.Fatal("observer is blocked") }) mu.Lock() // block the callback for i := 0; i < 2*observerChannelBufferSize; i++ { observer.sendRegisteringEvent(0) } if pending := timeout.Stop(); pending { // release the callback if timer hasn't expired yet mu.Unlock() } } type eventCollectorSink struct { observedEvents []Event mu sync.Mutex } func (s *eventCollectorSink) OnTunnelEvent(event Event) { s.mu.Lock() defer s.mu.Unlock() s.observedEvents = append(s.observedEvents, event) } func (s *eventCollectorSink) assertSawEvent(t *testing.T, event Event) { s.mu.Lock() defer s.mu.Unlock() assert.Contains(t, s.observedEvents, event) } ================================================ FILE: connection/protocol.go ================================================ package connection import ( "fmt" "hash/fnv" "sync" "time" "github.com/rs/zerolog" "github.com/cloudflare/cloudflared/edgediscovery" ) const ( AvailableProtocolFlagMessage = "Available protocols: 'auto' - automatically chooses the best protocol over time (the default; and also the recommended one); 'quic' - based on QUIC, relying on UDP egress to Cloudflare edge; 'http2' - using Go's HTTP2 library, relying on TCP egress to Cloudflare edge" // edgeH2muxTLSServerName is the server name to establish h2mux connection with edge (unused, but kept for legacy reference). _ = "cftunnel.com" // edgeH2TLSServerName is the server name to establish http2 connection with edge edgeH2TLSServerName = "h2.cftunnel.com" // edgeQUICServerName is the server name to establish quic connection with edge. edgeQUICServerName = "quic.cftunnel.com" AutoSelectFlag = "auto" // SRV and TXT record resolution TTL ResolveTTL = time.Hour ) // ProtocolList represents a list of supported protocols for communication with the edge // in order of precedence for remote percentage fetcher. var ProtocolList = []Protocol{QUIC, HTTP2} type Protocol int64 const ( // HTTP2 using golang HTTP2 library for edge connections. HTTP2 Protocol = iota // QUIC using quic-go for edge connections. QUIC ) // Fallback returns the fallback protocol and whether the protocol has a fallback func (p Protocol) fallback() (Protocol, bool) { switch p { case HTTP2: return 0, false case QUIC: return HTTP2, true default: return 0, false } } func (p Protocol) String() string { switch p { case HTTP2: return "http2" case QUIC: return "quic" default: return "unknown protocol" } } func (p Protocol) TLSSettings() *TLSSettings { switch p { case HTTP2: return &TLSSettings{ ServerName: edgeH2TLSServerName, } case QUIC: return &TLSSettings{ ServerName: edgeQUICServerName, NextProtos: []string{"argotunnel"}, } default: return nil } } type TLSSettings struct { ServerName string NextProtos []string } type ProtocolSelector interface { Current() Protocol Fallback() (Protocol, bool) } // staticProtocolSelector will not provide a different protocol for Fallback type staticProtocolSelector struct { current Protocol } func (s *staticProtocolSelector) Current() Protocol { return s.current } func (s *staticProtocolSelector) Fallback() (Protocol, bool) { return s.current, false } // remoteProtocolSelector will fetch a list of remote protocols to provide for edge discovery type remoteProtocolSelector struct { lock sync.RWMutex current Protocol // protocolPool is desired protocols in the order of priority they should be picked in. protocolPool []Protocol switchThreshold int32 fetchFunc edgediscovery.PercentageFetcher refreshAfter time.Time ttl time.Duration log *zerolog.Logger } func newRemoteProtocolSelector( current Protocol, protocolPool []Protocol, switchThreshold int32, fetchFunc edgediscovery.PercentageFetcher, ttl time.Duration, log *zerolog.Logger, ) *remoteProtocolSelector { return &remoteProtocolSelector{ current: current, protocolPool: protocolPool, switchThreshold: switchThreshold, fetchFunc: fetchFunc, refreshAfter: time.Now().Add(ttl), ttl: ttl, log: log, } } func (s *remoteProtocolSelector) Current() Protocol { s.lock.Lock() defer s.lock.Unlock() if time.Now().Before(s.refreshAfter) { return s.current } protocol, err := getProtocol(s.protocolPool, s.fetchFunc, s.switchThreshold) if err != nil { s.log.Err(err).Msg("Failed to refresh protocol") return s.current } s.current = protocol s.refreshAfter = time.Now().Add(s.ttl) return s.current } func (s *remoteProtocolSelector) Fallback() (Protocol, bool) { s.lock.RLock() defer s.lock.RUnlock() return s.current.fallback() } func getProtocol(protocolPool []Protocol, fetchFunc edgediscovery.PercentageFetcher, switchThreshold int32) (Protocol, error) { protocolPercentages, err := fetchFunc() if err != nil { return 0, err } for _, protocol := range protocolPool { protocolPercentage := protocolPercentages.GetPercentage(protocol.String()) if protocolPercentage > switchThreshold { return protocol, nil } } // Default to first index in protocolPool list return protocolPool[0], nil } // defaultProtocolSelector will allow for a protocol to have a fallback type defaultProtocolSelector struct { lock sync.RWMutex current Protocol } func newDefaultProtocolSelector( current Protocol, ) *defaultProtocolSelector { return &defaultProtocolSelector{ current: current, } } func (s *defaultProtocolSelector) Current() Protocol { s.lock.Lock() defer s.lock.Unlock() return s.current } func (s *defaultProtocolSelector) Fallback() (Protocol, bool) { s.lock.RLock() defer s.lock.RUnlock() return s.current.fallback() } func NewProtocolSelector( protocolFlag string, accountTag string, tunnelTokenProvided bool, needPQ bool, protocolFetcher edgediscovery.PercentageFetcher, resolveTTL time.Duration, log *zerolog.Logger, ) (ProtocolSelector, error) { // With --post-quantum, we force quic if needPQ { return &staticProtocolSelector{ current: QUIC, }, nil } threshold := switchThreshold(accountTag) fetchedProtocol, err := getProtocol(ProtocolList, protocolFetcher, threshold) log.Debug().Msgf("Fetched protocol: %s", fetchedProtocol) if err != nil { log.Warn().Msg("Unable to lookup protocol percentage.") // Falling through here since 'auto' is handled in the switch and failing // to do the protocol lookup isn't a failure since it can be triggered again // after the TTL. } // If the user picks a protocol, then we stick to it no matter what. switch protocolFlag { case "h2mux": // Any users still requesting h2mux will be upgraded to http2 instead log.Warn().Msg("h2mux is no longer a supported protocol: upgrading edge connection to http2. Please remove '--protocol h2mux' from runtime arguments to remove this warning.") return &staticProtocolSelector{current: HTTP2}, nil case QUIC.String(): return &staticProtocolSelector{current: QUIC}, nil case HTTP2.String(): return &staticProtocolSelector{current: HTTP2}, nil case AutoSelectFlag: // When a --token is provided, we want to start with QUIC but have fallback to HTTP2 if tunnelTokenProvided { return newDefaultProtocolSelector(QUIC), nil } return newRemoteProtocolSelector(fetchedProtocol, ProtocolList, threshold, protocolFetcher, resolveTTL, log), nil } return nil, fmt.Errorf("unknown protocol %s, %s", protocolFlag, AvailableProtocolFlagMessage) } func switchThreshold(accountTag string) int32 { h := fnv.New32a() _, _ = h.Write([]byte(accountTag)) return int32(h.Sum32() % 100) // nolint: gosec } ================================================ FILE: connection/protocol_test.go ================================================ package connection import ( "fmt" "testing" "github.com/stretchr/testify/assert" "github.com/cloudflare/cloudflared/edgediscovery" ) const ( testNoTTL = 0 testAccountTag = "testAccountTag" ) func mockFetcher(getError bool, protocolPercent ...edgediscovery.ProtocolPercent) edgediscovery.PercentageFetcher { return func() (edgediscovery.ProtocolPercents, error) { if getError { return nil, fmt.Errorf("failed to fetch percentage") } return protocolPercent, nil } } type dynamicMockFetcher struct { protocolPercents edgediscovery.ProtocolPercents err error } func (dmf *dynamicMockFetcher) fetch() edgediscovery.PercentageFetcher { return func() (edgediscovery.ProtocolPercents, error) { return dmf.protocolPercents, dmf.err } } func TestNewProtocolSelector(t *testing.T) { tests := []struct { name string protocol string tunnelTokenProvided bool needPQ bool expectedProtocol Protocol hasFallback bool expectedFallback Protocol wantErr bool }{ { name: "named tunnel with unknown protocol", protocol: "unknown", wantErr: true, }, { name: "named tunnel with h2mux: force to http2", protocol: "h2mux", expectedProtocol: HTTP2, }, { name: "named tunnel with http2: no fallback", protocol: "http2", expectedProtocol: HTTP2, }, { name: "named tunnel with auto: quic", protocol: AutoSelectFlag, expectedProtocol: QUIC, hasFallback: true, expectedFallback: HTTP2, }, { name: "named tunnel (post quantum)", protocol: AutoSelectFlag, needPQ: true, expectedProtocol: QUIC, }, { name: "named tunnel (post quantum) w/http2", protocol: "http2", needPQ: true, expectedProtocol: QUIC, }, } fetcher := dynamicMockFetcher{ protocolPercents: edgediscovery.ProtocolPercents{}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { selector, err := NewProtocolSelector(test.protocol, testAccountTag, test.tunnelTokenProvided, test.needPQ, fetcher.fetch(), ResolveTTL, &log) if test.wantErr { assert.Error(t, err, fmt.Sprintf("test %s failed", test.name)) } else { assert.NoError(t, err, fmt.Sprintf("test %s failed", test.name)) assert.Equal(t, test.expectedProtocol, selector.Current(), fmt.Sprintf("test %s failed", test.name)) fallback, ok := selector.Fallback() assert.Equal(t, test.hasFallback, ok, fmt.Sprintf("test %s failed", test.name)) if test.hasFallback { assert.Equal(t, test.expectedFallback, fallback, fmt.Sprintf("test %s failed", test.name)) } } }) } } func TestAutoProtocolSelectorRefresh(t *testing.T) { fetcher := dynamicMockFetcher{} selector, err := NewProtocolSelector(AutoSelectFlag, testAccountTag, false, false, fetcher.fetch(), testNoTTL, &log) assert.NoError(t, err) assert.Equal(t, QUIC, selector.Current()) fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}} assert.Equal(t, HTTP2, selector.Current()) fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 0}} assert.Equal(t, QUIC, selector.Current()) fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}} assert.Equal(t, HTTP2, selector.Current()) fetcher.err = fmt.Errorf("failed to fetch") assert.Equal(t, HTTP2, selector.Current()) fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: -1}} fetcher.err = nil assert.Equal(t, QUIC, selector.Current()) fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 0}} assert.Equal(t, QUIC, selector.Current()) fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "quic", Percentage: 100}} assert.Equal(t, QUIC, selector.Current()) } func TestHTTP2ProtocolSelectorRefresh(t *testing.T) { fetcher := dynamicMockFetcher{} // Since the user chooses http2 on purpose, we always stick to it. selector, err := NewProtocolSelector(HTTP2.String(), testAccountTag, false, false, fetcher.fetch(), testNoTTL, &log) assert.NoError(t, err) assert.Equal(t, HTTP2, selector.Current()) fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}} assert.Equal(t, HTTP2, selector.Current()) fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 0}} assert.Equal(t, HTTP2, selector.Current()) fetcher.err = fmt.Errorf("failed to fetch") assert.Equal(t, HTTP2, selector.Current()) fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: -1}} fetcher.err = nil assert.Equal(t, HTTP2, selector.Current()) fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 0}} assert.Equal(t, HTTP2, selector.Current()) fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}} assert.Equal(t, HTTP2, selector.Current()) fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: -1}} assert.Equal(t, HTTP2, selector.Current()) } func TestAutoProtocolSelectorNoRefreshWithToken(t *testing.T) { fetcher := dynamicMockFetcher{} selector, err := NewProtocolSelector(AutoSelectFlag, testAccountTag, true, false, fetcher.fetch(), testNoTTL, &log) assert.NoError(t, err) assert.Equal(t, QUIC, selector.Current()) fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}} assert.Equal(t, QUIC, selector.Current()) } ================================================ FILE: connection/quic.go ================================================ package connection import ( "context" "crypto/tls" "fmt" "net" "net/netip" "runtime" "sync" "github.com/quic-go/quic-go" "github.com/rs/zerolog" ) var ( portForConnIndex = make(map[uint8]int, 0) portMapMutex sync.Mutex ) func DialQuic( ctx context.Context, quicConfig *quic.Config, tlsConfig *tls.Config, edgeAddr netip.AddrPort, localAddr net.IP, connIndex uint8, logger *zerolog.Logger, ) (quic.Connection, error) { udpConn, err := createUDPConnForConnIndex(connIndex, localAddr, edgeAddr, logger) if err != nil { return nil, err } conn, err := quic.Dial(ctx, udpConn, net.UDPAddrFromAddrPort(edgeAddr), tlsConfig, quicConfig) if err != nil { // close the udp server socket in case of error connecting to the edge udpConn.Close() return nil, &EdgeQuicDialError{Cause: err} } // wrap the session, so that the UDPConn is closed after session is closed. conn = &wrapCloseableConnQuicConnection{ conn, udpConn, } return conn, nil } func createUDPConnForConnIndex(connIndex uint8, localIP net.IP, edgeIP netip.AddrPort, logger *zerolog.Logger) (*net.UDPConn, error) { portMapMutex.Lock() defer portMapMutex.Unlock() listenNetwork := "udp" // https://github.com/quic-go/quic-go/issues/3793 DF bit cannot be set for dual stack listener ("udp") on macOS, // to set the DF bit properly, the network string needs to be specific to the IP family. if runtime.GOOS == "darwin" { if edgeIP.Addr().Is4() { listenNetwork = "udp4" } else { listenNetwork = "udp6" } } // if port was not set yet, it will be zero, so bind will randomly allocate one. if port, ok := portForConnIndex[connIndex]; ok { udpConn, err := net.ListenUDP(listenNetwork, &net.UDPAddr{IP: localIP, Port: port}) // if there wasn't an error, or if port was 0 (independently of error or not, just return) if err == nil { return udpConn, nil } else { logger.Debug().Err(err).Msgf("Unable to reuse port %d for connIndex %d. Falling back to random allocation.", port, connIndex) } } // if we reached here, then there was an error or port as not been allocated it. udpConn, err := net.ListenUDP(listenNetwork, &net.UDPAddr{IP: localIP, Port: 0}) if err == nil { udpAddr, ok := (udpConn.LocalAddr()).(*net.UDPAddr) if !ok { return nil, fmt.Errorf("unable to cast to udpConn") } portForConnIndex[connIndex] = udpAddr.Port } else { delete(portForConnIndex, connIndex) } return udpConn, err } type wrapCloseableConnQuicConnection struct { quic.Connection udpConn *net.UDPConn } func (w *wrapCloseableConnQuicConnection) CloseWithError(errorCode quic.ApplicationErrorCode, reason string) error { err := w.Connection.CloseWithError(errorCode, reason) w.udpConn.Close() return err } ================================================ FILE: connection/quic_connection.go ================================================ package connection import ( "bufio" "context" "errors" "fmt" "io" "net" "net/http" "strconv" "strings" "sync/atomic" "time" "github.com/quic-go/quic-go" "github.com/rs/zerolog" "golang.org/x/sync/errgroup" "github.com/cloudflare/cloudflared/client" cfdflow "github.com/cloudflare/cloudflared/flow" cfdquic "github.com/cloudflare/cloudflared/quic" "github.com/cloudflare/cloudflared/tracing" "github.com/cloudflare/cloudflared/tunnelrpc/pogs" rpcquic "github.com/cloudflare/cloudflared/tunnelrpc/quic" ) const ( // HTTPHeaderKey is used to get or set http headers in QUIC ALPN if the underlying proxy connection type is HTTP. HTTPHeaderKey = "HttpHeader" // HTTPMethodKey is used to get or set http method in QUIC ALPN if the underlying proxy connection type is HTTP. HTTPMethodKey = "HttpMethod" // HTTPHostKey is used to get or set http host in QUIC ALPN if the underlying proxy connection type is HTTP. HTTPHostKey = "HttpHost" // HTTPStatus is used to return http status code in QUIC ALPN if the underlying proxy connection type is HTTP. HTTPStatus = "HttpStatus" QUICMetadataFlowID = "FlowID" ) // quicConnection represents the type that facilitates Proxying via QUIC streams. type quicConnection struct { conn quic.Connection logger *zerolog.Logger orchestrator Orchestrator datagramHandler DatagramSessionHandler controlStreamHandler ControlStreamHandler connOptions *client.ConnectionOptionsSnapshot connIndex uint8 rpcTimeout time.Duration streamWriteTimeout time.Duration gracePeriod time.Duration } // NewTunnelConnection takes a [quic.Connection] to wrap it for use with cloudflared application logic. func NewTunnelConnection( ctx context.Context, conn quic.Connection, connIndex uint8, orchestrator Orchestrator, datagramSessionHandler DatagramSessionHandler, controlStreamHandler ControlStreamHandler, connOptions *client.ConnectionOptionsSnapshot, rpcTimeout time.Duration, streamWriteTimeout time.Duration, gracePeriod time.Duration, logger *zerolog.Logger, ) TunnelConnection { return &quicConnection{ conn: conn, logger: logger, orchestrator: orchestrator, datagramHandler: datagramSessionHandler, controlStreamHandler: controlStreamHandler, connOptions: connOptions, connIndex: connIndex, rpcTimeout: rpcTimeout, streamWriteTimeout: streamWriteTimeout, gracePeriod: gracePeriod, } } // Serve starts a QUIC connection that begins accepting streams. // Returning a nil error means cloudflared will exit for good and will not attempt to reconnect. func (q *quicConnection) Serve(ctx context.Context) error { // The edge assumes the first stream is used for the control plane controlStream, err := q.conn.OpenStream() if err != nil { return fmt.Errorf("failed to open a registration control stream: %w", err) } // If either goroutine returns a non nil error, then the error group cancels the context, thus also canceling the // other goroutines. We enforce returning a not-nil error for each function started in the errgroup by logging // the error returned and returning a custom error type instead. errGroup, ctx := errgroup.WithContext(ctx) // Close the quic connection if any of the following routines return from the errgroup (regardless of their error) // because they are no longer processing requests for the connection. defer q.Close() // Start the control stream routine errGroup.Go(func() error { // err is equal to nil if we exit due to unregistration. If that happens we want to wait the full // amount of the grace period, allowing requests to finish before we cancel the context, which will // make cloudflared exit. if err := q.serveControlStream(ctx, controlStream); err == nil { if q.gracePeriod > 0 { // In Go1.23 this can be removed and replaced with time.Ticker // see https://pkg.go.dev/time#Tick ticker := time.NewTicker(q.gracePeriod) defer ticker.Stop() select { case <-ctx.Done(): case <-ticker.C: } } } if err != nil { q.logger.Error().Err(err).Msg("failed to serve the control stream") } return &ControlStreamError{} }) // Start the accept stream loop routine errGroup.Go(func() error { err := q.acceptStream(ctx) if err != nil { q.logger.Error().Err(err).Msg("failed to accept incoming stream requests") } return &StreamListenerError{} }) // Start the datagram handler routine errGroup.Go(func() error { err := q.datagramHandler.Serve(ctx) if err != nil { q.logger.Error().Err(err).Msg("failed to run the datagram handler") } return &DatagramManagerError{} }) return errGroup.Wait() } // serveControlStream will serve the RPC; blocking until the control plane is done. func (q *quicConnection) serveControlStream(ctx context.Context, controlStream quic.Stream) error { return q.controlStreamHandler.ServeControlStream(ctx, controlStream, q.connOptions.ConnectionOptions(), q.orchestrator) } // Close the connection with no errors specified. func (q *quicConnection) Close() { _ = q.conn.CloseWithError(0, "") } func (q *quicConnection) acceptStream(ctx context.Context) error { for { quicStream, err := q.conn.AcceptStream(ctx) if err != nil { // context.Canceled is usually a user ctrl+c. We don't want to log an error here as it's intentional. if errors.Is(err, context.Canceled) || q.controlStreamHandler.IsStopped() { return nil } return fmt.Errorf("failed to accept QUIC stream: %w", err) } go q.runStream(quicStream) } } func (q *quicConnection) runStream(quicStream quic.Stream) { ctx := quicStream.Context() stream := cfdquic.NewSafeStreamCloser(quicStream, q.streamWriteTimeout, q.logger) defer stream.Close() // we are going to fuse readers/writers from stream <- cloudflared -> origin, and we want to guarantee that // code executed in the code path of handleStream don't trigger an earlier close to the downstream write stream. // So, we wrap the stream with a no-op write closer and only this method can actually close write side of the stream. // A call to close will simulate a close to the read-side, which will fail subsequent reads. noCloseStream := &nopCloserReadWriter{ReadWriteCloser: stream} ss := rpcquic.NewCloudflaredServer(q.handleDataStream, q.datagramHandler, q, q.rpcTimeout) if err := ss.Serve(ctx, noCloseStream); err != nil { q.logger.Debug().Err(err).Msg("Failed to handle QUIC stream") // if we received an error at this level, then close write side of stream with an error, which will result in // RST_STREAM frame. quicStream.CancelWrite(0) } } func (q *quicConnection) handleDataStream(ctx context.Context, stream *rpcquic.RequestServerStream) error { request, err := stream.ReadConnectRequestData() if err != nil { return err } if err, connectResponseSent := q.dispatchRequest(ctx, stream, request); err != nil { q.logger.Err(err).Str("type", request.Type.String()).Str("dest", request.Dest).Msg("Request failed") // if the connectResponse was already sent and we had an error, we need to propagate it up, so that the stream is // closed with an RST_STREAM frame if connectResponseSent { return err } var metadata []pogs.Metadata // Check the type of error that was throw and add metadata that will help identify it on OTD. if errors.Is(err, cfdflow.ErrTooManyActiveFlows) { metadata = append(metadata, pogs.ErrorFlowConnectRateLimitedMetadata) } if writeRespErr := stream.WriteConnectResponseData(err, metadata...); writeRespErr != nil { return writeRespErr } } return nil } // dispatchRequest will dispatch the request to the origin depending on the type and returns an error if it occurs. // Also returns if the connect response was sent to the downstream during processing of the origin request. func (q *quicConnection) dispatchRequest(ctx context.Context, stream *rpcquic.RequestServerStream, request *pogs.ConnectRequest) (err error, connectResponseSent bool) { originProxy, err := q.orchestrator.GetOriginProxy() if err != nil { return err, false } switch request.Type { case pogs.ConnectionTypeHTTP, pogs.ConnectionTypeWebsocket: tracedReq, err := buildHTTPRequest(ctx, request, stream, q.connIndex, q.logger) if err != nil { return err, false } w := newHTTPResponseAdapter(stream) return originProxy.ProxyHTTP(&w, tracedReq, request.Type == pogs.ConnectionTypeWebsocket), w.connectResponseSent case pogs.ConnectionTypeTCP: rwa := &streamReadWriteAcker{RequestServerStream: stream} metadata := request.MetadataMap() return originProxy.ProxyTCP(ctx, rwa, &TCPRequest{ Dest: request.Dest, FlowID: metadata[QUICMetadataFlowID], CfTraceID: metadata[tracing.TracerContextName], ConnIndex: q.connIndex, }), rwa.connectResponseSent default: return fmt.Errorf("unsupported error type: %s", request.Type), false } } // UpdateConfiguration is the RPC method invoked by edge when there is a new configuration func (q *quicConnection) UpdateConfiguration(ctx context.Context, version int32, config []byte) *pogs.UpdateConfigurationResponse { return q.orchestrator.UpdateConfig(version, config) } // streamReadWriteAcker is a light wrapper over QUIC streams with a callback to send response back to // the client. type streamReadWriteAcker struct { *rpcquic.RequestServerStream connectResponseSent bool } // AckConnection acks response back to the proxy. func (s *streamReadWriteAcker) AckConnection(tracePropagation string) error { metadata := []pogs.Metadata{} // Only add tracing if provided by the edge request if tracePropagation != "" { metadata = append(metadata, pogs.Metadata{ Key: tracing.CanonicalCloudflaredTracingHeader, Val: tracePropagation, }) } s.connectResponseSent = true return s.WriteConnectResponseData(nil, metadata...) } // httpResponseAdapter translates responses written by the HTTP Proxy into ones that can be used in QUIC. type httpResponseAdapter struct { *rpcquic.RequestServerStream headers http.Header connectResponseSent bool } func newHTTPResponseAdapter(s *rpcquic.RequestServerStream) httpResponseAdapter { return httpResponseAdapter{RequestServerStream: s, headers: make(http.Header)} } func (hrw *httpResponseAdapter) AddTrailer(trailerName, trailerValue string) { // we do not support trailers over QUIC } func (hrw *httpResponseAdapter) WriteRespHeaders(status int, header http.Header) error { metadata := make([]pogs.Metadata, 0) metadata = append(metadata, pogs.Metadata{Key: HTTPStatus, Val: strconv.Itoa(status)}) for k, vv := range header { for _, v := range vv { httpHeaderKey := fmt.Sprintf("%s:%s", HTTPHeaderKey, k) metadata = append(metadata, pogs.Metadata{Key: httpHeaderKey, Val: v}) } } return hrw.WriteConnectResponseData(nil, metadata...) } func (hrw *httpResponseAdapter) Write(p []byte) (int, error) { // Make sure to send WriteHeader response if not called yet if !hrw.connectResponseSent { _ = hrw.WriteRespHeaders(http.StatusOK, hrw.headers) } return hrw.RequestServerStream.Write(p) } func (hrw *httpResponseAdapter) Header() http.Header { return hrw.headers } // This is a no-op Flush because this adapter is over a quic.Stream and we don't need Flush here. func (hrw *httpResponseAdapter) Flush() {} func (hrw *httpResponseAdapter) WriteHeader(status int) { _ = hrw.WriteRespHeaders(status, hrw.headers) } func (hrw *httpResponseAdapter) Hijack() (net.Conn, *bufio.ReadWriter, error) { conn := &localProxyConnection{hrw.ReadWriteCloser} readWriter := bufio.NewReadWriter( bufio.NewReader(hrw.ReadWriteCloser), bufio.NewWriter(hrw.ReadWriteCloser), ) return conn, readWriter, nil } func (hrw *httpResponseAdapter) WriteErrorResponse(err error) { _ = hrw.WriteConnectResponseData(err, pogs.Metadata{Key: HTTPStatus, Val: strconv.Itoa(http.StatusBadGateway)}) } func (hrw *httpResponseAdapter) WriteConnectResponseData(respErr error, metadata ...pogs.Metadata) error { hrw.connectResponseSent = true return hrw.RequestServerStream.WriteConnectResponseData(respErr, metadata...) } func buildHTTPRequest( ctx context.Context, connectRequest *pogs.ConnectRequest, body io.ReadCloser, connIndex uint8, log *zerolog.Logger, ) (*tracing.TracedHTTPRequest, error) { metadata := connectRequest.MetadataMap() dest := connectRequest.Dest method := metadata[HTTPMethodKey] host := metadata[HTTPHostKey] isWebsocket := connectRequest.Type == pogs.ConnectionTypeWebsocket req, err := http.NewRequestWithContext(ctx, method, dest, body) if err != nil { return nil, err } req.Host = host for _, metadata := range connectRequest.Metadata { if strings.Contains(metadata.Key, HTTPHeaderKey) { // metadata.Key is off the format httpHeaderKey: httpHeaderKey := strings.Split(metadata.Key, ":") if len(httpHeaderKey) != 2 { return nil, fmt.Errorf("header Key: %s malformed", metadata.Key) } req.Header.Add(httpHeaderKey[1], metadata.Val) } } // Go's http.Client automatically sends chunked request body if this value is not set on the // *http.Request struct regardless of header: // https://go.googlesource.com/go/+/go1.8rc2/src/net/http/transfer.go#154. if err := setContentLength(req); err != nil { return nil, fmt.Errorf("Error setting content-length: %w", err) } // Go's client defaults to chunked encoding after a 200ms delay if the following cases are true: // * the request body blocks // * the content length is not set (or set to -1) // * the method doesn't usually have a body (GET, HEAD, DELETE, ...) // * there is no transfer-encoding=chunked already set. // So, if transfer cannot be chunked and content length is 0, we dont set a request body. if !isWebsocket && !isTransferEncodingChunked(req) && req.ContentLength == 0 { req.Body = http.NoBody } stripWebsocketUpgradeHeader(req) // Check for tracing on request tracedReq := tracing.NewTracedHTTPRequest(req, connIndex, log) return tracedReq, err } func setContentLength(req *http.Request) error { var err error if contentLengthStr := req.Header.Get("Content-Length"); contentLengthStr != "" { req.ContentLength, err = strconv.ParseInt(contentLengthStr, 10, 64) } return err } func isTransferEncodingChunked(req *http.Request) bool { transferEncodingVal := req.Header.Get("Transfer-Encoding") // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Transfer-Encoding suggests that this can be a comma // separated value as well. return strings.Contains(strings.ToLower(transferEncodingVal), "chunked") } // A helper struct that guarantees a call to close only affects read side, but not write side. type nopCloserReadWriter struct { io.ReadWriteCloser // for use by Read only // we don't need a memory barrier here because there is an implicit assumption that // Read calls can't happen concurrently by different go-routines. sawEOF bool // should be updated and read using atomic primitives. // value is read in Read method and written in Close method, which could be done by different // go-routines. closed uint32 } func (np *nopCloserReadWriter) Read(p []byte) (n int, err error) { if np.sawEOF { return 0, io.EOF } if atomic.LoadUint32(&np.closed) > 0 { return 0, fmt.Errorf("closed by handler") } n, err = np.ReadWriteCloser.Read(p) if err == io.EOF { np.sawEOF = true } return } func (np *nopCloserReadWriter) Close() error { atomic.StoreUint32(&np.closed, 1) return nil } ================================================ FILE: connection/quic_connection_test.go ================================================ package connection import ( "bytes" "context" "crypto/rand" "crypto/rsa" "crypto/tls" "crypto/x509" "encoding/pem" "errors" "fmt" "io" "math/big" "net" "net/http" "net/netip" "net/url" "strings" "testing" "time" "github.com/gobwas/ws/wsutil" "github.com/google/uuid" pkgerrors "github.com/pkg/errors" "github.com/quic-go/quic-go" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/net/nettest" "github.com/cloudflare/cloudflared/client" "github.com/cloudflare/cloudflared/config" cfdflow "github.com/cloudflare/cloudflared/flow" "github.com/cloudflare/cloudflared/datagramsession" "github.com/cloudflare/cloudflared/ingress" "github.com/cloudflare/cloudflared/packet" cfdquic "github.com/cloudflare/cloudflared/quic" "github.com/cloudflare/cloudflared/tracing" "github.com/cloudflare/cloudflared/tunnelrpc/pogs" rpcquic "github.com/cloudflare/cloudflared/tunnelrpc/quic" ) var ( testTLSServerConfig = GenerateTLSConfig() testQUICConfig = &quic.Config{ KeepAlivePeriod: 5 * time.Second, EnableDatagrams: true, } defaultQUICTimeout = 30 * time.Second ) var _ ReadWriteAcker = (*streamReadWriteAcker)(nil) // TestQUICServer tests if a quic server accepts and responds to a quic client with the acceptance protocol. // It also serves as a demonstration for communication with the QUIC connection started by a cloudflared. func TestQUICServer(t *testing.T) { // This is simply a sample websocket frame message. wsBuf := &bytes.Buffer{} err := wsutil.WriteClientBinary(wsBuf, []byte("Hello")) require.NoError(t, err) tests := []struct { desc string dest string connectionType pogs.ConnectionType metadata []pogs.Metadata message []byte expectedResponse []byte }{ { desc: "test http proxy", dest: "/ok", connectionType: pogs.ConnectionTypeHTTP, metadata: []pogs.Metadata{ { Key: "HttpHeader:Cf-Ray", Val: "123123123", }, { Key: "HttpHost", Val: "cf.host", }, { Key: "HttpMethod", Val: "GET", }, }, expectedResponse: []byte("OK"), }, { desc: "test http body request streaming", dest: "/slow_echo_body", connectionType: pogs.ConnectionTypeHTTP, metadata: []pogs.Metadata{ { Key: "HttpHeader:Cf-Ray", Val: "123123123", }, { Key: "HttpHost", Val: "cf.host", }, { Key: "HttpMethod", Val: "POST", }, { Key: "HttpHeader:Content-Length", Val: "24", }, }, message: []byte("This is the message body"), expectedResponse: []byte("This is the message body"), }, { desc: "test ws proxy", dest: "/ws/echo", connectionType: pogs.ConnectionTypeWebsocket, metadata: []pogs.Metadata{ { Key: "HttpHeader:Cf-Cloudflared-Proxy-Connection-Upgrade", Val: "Websocket", }, { Key: "HttpHeader:Another-Header", Val: "Misc", }, { Key: "HttpHost", Val: "cf.host", }, { Key: "HttpMethod", Val: "get", }, }, message: wsBuf.Bytes(), expectedResponse: []byte{0x82, 0x5, 0x48, 0x65, 0x6c, 0x6c, 0x6f}, }, { desc: "test tcp proxy", connectionType: pogs.ConnectionTypeTCP, metadata: []pogs.Metadata{}, message: []byte("Here is some tcp data"), expectedResponse: []byte("Here is some tcp data"), }, } for i, test := range tests { t.Run(test.desc, func(t *testing.T) { ctx, cancel := context.WithCancel(t.Context()) // Start a UDP Listener for QUIC. udpAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:0") require.NoError(t, err) udpListener, err := net.ListenUDP(udpAddr.Network(), udpAddr) require.NoError(t, err) defer udpListener.Close() quicTransport := &quic.Transport{Conn: udpListener, ConnectionIDLength: 16} quicListener, err := quicTransport.Listen(testTLSServerConfig, testQUICConfig) require.NoError(t, err) serverDone := make(chan struct{}) go func() { // nolint: testifylint quicServer( ctx, t, quicListener, test.dest, test.connectionType, test.metadata, test.message, test.expectedResponse, ) close(serverDone) }() // nolint: gosec tunnelConn, _ := testTunnelConnection(t, netip.MustParseAddrPort(udpListener.LocalAddr().String()), uint8(i)) connDone := make(chan struct{}) go func() { _ = tunnelConn.Serve(ctx) close(connDone) }() <-serverDone cancel() <-connDone }) } } type fakeControlStream struct { ControlStreamHandler } func (fakeControlStream) ServeControlStream(ctx context.Context, rw io.ReadWriteCloser, connOptions *pogs.ConnectionOptions, tunnelConfigGetter TunnelConfigJSONGetter) error { <-ctx.Done() return nil } func (fakeControlStream) IsStopped() bool { return true } func quicServer( ctx context.Context, t *testing.T, listener *quic.Listener, dest string, connectionType pogs.ConnectionType, metadata []pogs.Metadata, message []byte, expectedResponse []byte, ) { session, err := listener.Accept(ctx) require.NoError(t, err) quicStream, err := session.OpenStreamSync(t.Context()) require.NoError(t, err) stream := cfdquic.NewSafeStreamCloser(quicStream, defaultQUICTimeout, &log) reqClientStream := rpcquic.RequestClientStream{ReadWriteCloser: stream} err = reqClientStream.WriteConnectRequestData(dest, connectionType, metadata...) require.NoError(t, err) _, err = reqClientStream.ReadConnectResponseData() require.NoError(t, err) if message != nil { // ALPN successful. Write data. _, err := stream.Write(message) require.NoError(t, err) } response := make([]byte, len(expectedResponse)) _, err = stream.Read(response) if err != io.EOF { require.NoError(t, err) } // For now it is an echo server. Verify if the same data is returned. assert.Equal(t, expectedResponse, response) } type mockOriginProxyWithRequest struct{} func (moc *mockOriginProxyWithRequest) ProxyHTTP(w ResponseWriter, tr *tracing.TracedHTTPRequest, isWebsocket bool) error { // These are a series of crude tests to ensure the headers and http related data is transferred from // metadata. r := tr.Request if r.Method == "" { return errors.New("method not sent") } if r.Host == "" { return errors.New("host not sent") } if len(r.Header) == 0 { return errors.New("headers not set") } if isWebsocket { return wsEchoEndpoint(w, r) } switch r.URL.Path { case "/ok": originRespEndpoint(w, http.StatusOK, []byte(http.StatusText(http.StatusOK))) case "/slow_echo_body": time.Sleep(5 * time.Nanosecond) fallthrough case "/echo_body": resp := &http.Response{ StatusCode: http.StatusOK, } _ = w.WriteRespHeaders(resp.StatusCode, resp.Header) _, _ = io.Copy(w, r.Body) case "/error": return fmt.Errorf("Failed to proxy to origin") default: originRespEndpoint(w, http.StatusNotFound, []byte("page not found")) } return nil } func TestBuildHTTPRequest(t *testing.T) { tests := []struct { name string connectRequest *pogs.ConnectRequest body io.ReadCloser req *http.Request }{ { name: "check if http.Request is built correctly with content length", connectRequest: &pogs.ConnectRequest{ Dest: "http://test.com", Metadata: []pogs.Metadata{ { Key: "HttpHeader:Cf-Cloudflared-Proxy-Connection-Upgrade", Val: "Websocket", }, { Key: "HttpHeader:Content-Length", Val: "514", }, { Key: "HttpHeader:Another-Header", Val: "Misc", }, { Key: "HttpHost", Val: "cf.host", }, { Key: "HttpMethod", Val: "get", }, }, }, req: &http.Request{ Method: "get", URL: &url.URL{ Scheme: "http", Host: "test.com", }, Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, Header: http.Header{ "Another-Header": []string{"Misc"}, "Content-Length": []string{"514"}, }, ContentLength: 514, Host: "cf.host", Body: io.NopCloser(&bytes.Buffer{}), }, body: io.NopCloser(&bytes.Buffer{}), }, { name: "if content length isn't part of request headers, then it's not set", connectRequest: &pogs.ConnectRequest{ Dest: "http://test.com", Metadata: []pogs.Metadata{ { Key: "HttpHeader:Cf-Cloudflared-Proxy-Connection-Upgrade", Val: "Websocket", }, { Key: "HttpHeader:Another-Header", Val: "Misc", }, { Key: "HttpHost", Val: "cf.host", }, { Key: "HttpMethod", Val: "get", }, }, }, req: &http.Request{ Method: "get", URL: &url.URL{ Scheme: "http", Host: "test.com", }, Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, Header: http.Header{ "Another-Header": []string{"Misc"}, }, ContentLength: 0, Host: "cf.host", Body: http.NoBody, }, body: io.NopCloser(&bytes.Buffer{}), }, { name: "if content length is 0, but transfer-encoding is chunked, body is not nil", connectRequest: &pogs.ConnectRequest{ Dest: "http://test.com", Metadata: []pogs.Metadata{ { Key: "HttpHeader:Another-Header", Val: "Misc", }, { Key: "HttpHeader:Transfer-Encoding", Val: "chunked", }, { Key: "HttpHost", Val: "cf.host", }, { Key: "HttpMethod", Val: "get", }, }, }, req: &http.Request{ Method: "get", URL: &url.URL{ Scheme: "http", Host: "test.com", }, Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, Header: http.Header{ "Another-Header": []string{"Misc"}, "Transfer-Encoding": []string{"chunked"}, }, ContentLength: 0, Host: "cf.host", Body: io.NopCloser(&bytes.Buffer{}), }, body: io.NopCloser(&bytes.Buffer{}), }, { name: "if content length is 0, but transfer-encoding is gzip,chunked, body is not nil", connectRequest: &pogs.ConnectRequest{ Dest: "http://test.com", Metadata: []pogs.Metadata{ { Key: "HttpHeader:Another-Header", Val: "Misc", }, { Key: "HttpHeader:Transfer-Encoding", Val: "gzip,chunked", }, { Key: "HttpHost", Val: "cf.host", }, { Key: "HttpMethod", Val: "get", }, }, }, req: &http.Request{ Method: "get", URL: &url.URL{ Scheme: "http", Host: "test.com", }, Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, Header: http.Header{ "Another-Header": []string{"Misc"}, "Transfer-Encoding": []string{"gzip,chunked"}, }, ContentLength: 0, Host: "cf.host", Body: io.NopCloser(&bytes.Buffer{}), }, body: io.NopCloser(&bytes.Buffer{}), }, { name: "if content length is 0, and connect request is a websocket, body is not nil", connectRequest: &pogs.ConnectRequest{ Type: pogs.ConnectionTypeWebsocket, Dest: "http://test.com", Metadata: []pogs.Metadata{ { Key: "HttpHeader:Another-Header", Val: "Misc", }, { Key: "HttpHost", Val: "cf.host", }, { Key: "HttpMethod", Val: "get", }, }, }, req: &http.Request{ Method: "get", URL: &url.URL{ Scheme: "http", Host: "test.com", }, Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, Header: http.Header{ "Another-Header": []string{"Misc"}, }, ContentLength: 0, Host: "cf.host", Body: io.NopCloser(&bytes.Buffer{}), }, body: io.NopCloser(&bytes.Buffer{}), }, } log := zerolog.Nop() for _, test := range tests { t.Run(test.name, func(t *testing.T) { req, err := buildHTTPRequest(t.Context(), test.connectRequest, test.body, 0, &log) require.NoError(t, err) test.req = test.req.WithContext(req.Context()) require.Equal(t, test.req, req.Request) }) } } func (moc *mockOriginProxyWithRequest) ProxyTCP(ctx context.Context, rwa ReadWriteAcker, tcpRequest *TCPRequest) error { if tcpRequest.Dest == "rate-limit-me" { return pkgerrors.Wrap(cfdflow.ErrTooManyActiveFlows, "failed tcp stream") } _ = rwa.AckConnection("") _, _ = io.Copy(rwa, rwa) return nil } func TestServeUDPSession(t *testing.T) { // Start a UDP Listener for QUIC. udpAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:0") require.NoError(t, err) udpListener, err := net.ListenUDP(udpAddr.Network(), udpAddr) require.NoError(t, err) defer udpListener.Close() ctx, cancel := context.WithCancel(t.Context()) // Establish QUIC connection with edge edgeQUICSessionChan := make(chan quic.Connection) go func() { earlyListener, err := quic.Listen(udpListener, testTLSServerConfig, testQUICConfig) assert.NoError(t, err) edgeQUICSession, err := earlyListener.Accept(ctx) assert.NoError(t, err) edgeQUICSessionChan <- edgeQUICSession }() // Random index to avoid reusing port tunnelConn, datagramConn := testTunnelConnection(t, netip.MustParseAddrPort(udpListener.LocalAddr().String()), 28) go func() { _ = tunnelConn.Serve(ctx) }() edgeQUICSession := <-edgeQUICSessionChan serveSession(ctx, datagramConn, edgeQUICSession, closedByOrigin, io.EOF.Error(), t) serveSession(ctx, datagramConn, edgeQUICSession, closedByTimeout, datagramsession.SessionIdleErr(time.Millisecond*50).Error(), t) serveSession(ctx, datagramConn, edgeQUICSession, closedByRemote, "eyeball closed connection", t) cancel() } func TestNopCloserReadWriterCloseBeforeEOF(t *testing.T) { readerWriter := nopCloserReadWriter{ReadWriteCloser: &mockReaderNoopWriter{Reader: strings.NewReader("123456789")}} buffer := make([]byte, 5) n, err := readerWriter.Read(buffer) require.NoError(t, err) require.Equal(t, 5, n) // close require.NoError(t, readerWriter.Close()) // read should get error n, err = readerWriter.Read(buffer) require.Equal(t, 0, n) require.Equal(t, err, fmt.Errorf("closed by handler")) } func TestNopCloserReadWriterCloseAfterEOF(t *testing.T) { readerWriter := nopCloserReadWriter{ReadWriteCloser: &mockReaderNoopWriter{Reader: strings.NewReader("123456789")}} buffer := make([]byte, 20) n, err := readerWriter.Read(buffer) require.NoError(t, err) require.Equal(t, 9, n) // force another read to read eof _, err = readerWriter.Read(buffer) require.Equal(t, err, io.EOF) // close require.NoError(t, readerWriter.Close()) // read should get EOF still n, err = readerWriter.Read(buffer) require.Equal(t, 0, n) require.Equal(t, err, io.EOF) } func TestCreateUDPConnReuseSourcePort(t *testing.T) { edgeIPv4 := netip.MustParseAddrPort("0.0.0.0:0") edgeIPv6 := netip.MustParseAddrPort("[::]:0") // We assume the test environment has access to an IPv4 interface testCreateUDPConnReuseSourcePortForEdgeIP(t, edgeIPv4) if nettest.SupportsIPv6() { testCreateUDPConnReuseSourcePortForEdgeIP(t, edgeIPv6) } } // TestTCPProxy_FlowRateLimited tests if the pogs.ConnectResponse returns the expected error and metadata, when a // new flow is rate limited. func TestTCPProxy_FlowRateLimited(t *testing.T) { ctx, cancel := context.WithCancel(t.Context()) // Start a UDP Listener for QUIC. udpAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:0") require.NoError(t, err) udpListener, err := net.ListenUDP(udpAddr.Network(), udpAddr) require.NoError(t, err) defer udpListener.Close() quicTransport := &quic.Transport{Conn: udpListener, ConnectionIDLength: 16} quicListener, err := quicTransport.Listen(testTLSServerConfig, testQUICConfig) require.NoError(t, err) serverDone := make(chan struct{}) go func() { defer close(serverDone) session, err := quicListener.Accept(ctx) assert.NoError(t, err) quicStream, err := session.OpenStreamSync(t.Context()) assert.NoError(t, err) stream := cfdquic.NewSafeStreamCloser(quicStream, defaultQUICTimeout, &log) reqClientStream := rpcquic.RequestClientStream{ReadWriteCloser: stream} err = reqClientStream.WriteConnectRequestData("rate-limit-me", pogs.ConnectionTypeTCP) assert.NoError(t, err) response, err := reqClientStream.ReadConnectResponseData() assert.NoError(t, err) // Got Rate Limited assert.NotEmpty(t, response.Error) assert.Contains(t, response.Metadata, pogs.ErrorFlowConnectRateLimitedMetadata) }() tunnelConn, _ := testTunnelConnection(t, netip.MustParseAddrPort(udpListener.LocalAddr().String()), uint8(0)) connDone := make(chan struct{}) go func() { defer close(connDone) _ = tunnelConn.Serve(ctx) }() <-serverDone cancel() <-connDone } func testCreateUDPConnReuseSourcePortForEdgeIP(t *testing.T, edgeIP netip.AddrPort) { logger := zerolog.Nop() conn, err := createUDPConnForConnIndex(0, nil, edgeIP, &logger) require.NoError(t, err) getPortFunc := func(conn *net.UDPConn) int { addr := conn.LocalAddr().(*net.UDPAddr) return addr.Port } initialPort := getPortFunc(conn) // close conn conn.Close() // should get the same port as before. conn, err = createUDPConnForConnIndex(0, nil, edgeIP, &logger) require.NoError(t, err) require.Equal(t, initialPort, getPortFunc(conn)) // new index, should get a different port conn1, err := createUDPConnForConnIndex(1, nil, edgeIP, &logger) require.NoError(t, err) require.NotEqual(t, initialPort, getPortFunc(conn1)) // not closing the conn and trying to obtain a new conn for same index should give a different random port conn, err = createUDPConnForConnIndex(0, nil, edgeIP, &logger) require.NoError(t, err) require.NotEqual(t, initialPort, getPortFunc(conn)) } func serveSession(ctx context.Context, datagramConn *datagramV2Connection, edgeQUICSession quic.Connection, closeType closeReason, expectedReason string, t *testing.T) { payload := []byte(t.Name()) sessionID := uuid.New() cfdConn, originConn := net.Pipe() // Registers and run a new session session, err := datagramConn.sessionManager.RegisterSession(ctx, sessionID, cfdConn) require.NoError(t, err) sessionDone := make(chan struct{}) go func() { datagramConn.serveUDPSession(session, time.Millisecond*50) close(sessionDone) }() // Send a message to the quic session on edge side, it should be deumx to this datagram v2 session muxedPayload, err := cfdquic.SuffixSessionID(sessionID, payload) require.NoError(t, err) muxedPayload, err = cfdquic.SuffixType(muxedPayload, cfdquic.DatagramTypeUDP) require.NoError(t, err) err = edgeQUICSession.SendDatagram(muxedPayload) require.NoError(t, err) readBuffer := make([]byte, len(payload)+1) n, err := originConn.Read(readBuffer) require.NoError(t, err) require.Equal(t, len(payload), n) require.True(t, bytes.Equal(payload, readBuffer[:n])) // Close connection to terminate session switch closeType { case closedByOrigin: originConn.Close() case closedByRemote: err = datagramConn.UnregisterUdpSession(ctx, sessionID, expectedReason) require.NoError(t, err) case closedByTimeout: } if closeType != closedByRemote { // Session was not closed by remote, so closeUDPSession should be invoked to unregister from remote unregisterFromEdgeChan := make(chan struct{}) sessionRPCServer := &mockSessionRPCServer{ sessionID: sessionID, unregisterReason: expectedReason, calledUnregisterChan: unregisterFromEdgeChan, } // nolint: testifylint go runRPCServer(ctx, edgeQUICSession, sessionRPCServer, nil, t) <-unregisterFromEdgeChan } <-sessionDone } type closeReason uint8 const ( closedByOrigin closeReason = iota closedByRemote closedByTimeout ) func runRPCServer(ctx context.Context, session quic.Connection, sessionRPCServer pogs.SessionManager, configRPCServer pogs.ConfigurationManager, t *testing.T) { stream, err := session.AcceptStream(ctx) require.NoError(t, err) if stream.StreamID() == 0 { // Skip the first stream, it's the control stream of the QUIC connection stream, err = session.AcceptStream(ctx) require.NoError(t, err) } ss := rpcquic.NewCloudflaredServer( func(_ context.Context, _ *rpcquic.RequestServerStream) error { return nil }, sessionRPCServer, configRPCServer, 10*time.Second, ) err = ss.Serve(ctx, stream) assert.NoError(t, err) } type mockSessionRPCServer struct { sessionID uuid.UUID unregisterReason string calledUnregisterChan chan struct{} } func (s mockSessionRPCServer) RegisterUdpSession(ctx context.Context, sessionID uuid.UUID, dstIP net.IP, dstPort uint16, closeIdleAfter time.Duration, traceContext string) (*pogs.RegisterUdpSessionResponse, error) { return nil, fmt.Errorf("mockSessionRPCServer doesn't implement RegisterUdpSession") } func (s mockSessionRPCServer) UnregisterUdpSession(ctx context.Context, sessionID uuid.UUID, reason string) error { if s.sessionID != sessionID { return fmt.Errorf("expect session ID %s, got %s", s.sessionID, sessionID) } if s.unregisterReason != reason { return fmt.Errorf("expect unregister reason %s, got %s", s.unregisterReason, reason) } close(s.calledUnregisterChan) return nil } func testTunnelConnection(t *testing.T, serverAddr netip.AddrPort, index uint8) (TunnelConnection, *datagramV2Connection) { tlsClientConfig := &tls.Config{ // nolint: gosec InsecureSkipVerify: true, NextProtos: []string{"argotunnel"}, } // Start a mock httpProxy log := zerolog.New(io.Discard) ctx, cancel := context.WithCancel(t.Context()) defer cancel() // Dial the QUIC connection to the edge conn, err := DialQuic( ctx, testQUICConfig, tlsClientConfig, serverAddr, nil, // connect on a random port index, &log, ) require.NoError(t, err) // Start a session manager for the connection sessionDemuxChan := make(chan *packet.Session, 4) datagramMuxer := cfdquic.NewDatagramMuxerV2(conn, &log, sessionDemuxChan) sessionManager := datagramsession.NewManager(&log, datagramMuxer.SendToSession, sessionDemuxChan) var connIndex uint8 = 0 packetRouter := ingress.NewPacketRouter(nil, datagramMuxer, connIndex, &log) testDefaultDialer := ingress.NewDialer(ingress.WarpRoutingConfig{ ConnectTimeout: config.CustomDuration{Duration: 1 * time.Second}, TCPKeepAlive: config.CustomDuration{Duration: 15 * time.Second}, MaxActiveFlows: 0, }) originDialer := ingress.NewOriginDialer(ingress.OriginConfig{ DefaultDialer: testDefaultDialer, TCPWriteTimeout: 1 * time.Second, }, &log) datagramConn := &datagramV2Connection{ conn, index, sessionManager, cfdflow.NewLimiter(0), datagramMuxer, originDialer, packetRouter, 15 * time.Second, 0 * time.Second, &log, } tunnelConn := NewTunnelConnection( ctx, conn, index, &mockOrchestrator{originProxy: &mockOriginProxyWithRequest{}}, datagramConn, fakeControlStream{}, &client.ConnectionOptionsSnapshot{}, 15*time.Second, 0*time.Second, 0*time.Second, &log, ) return tunnelConn, datagramConn } type mockReaderNoopWriter struct { io.Reader } func (m *mockReaderNoopWriter) Write(p []byte) (n int, err error) { return len(p), nil } func (m *mockReaderNoopWriter) Close() error { return nil } // GenerateTLSConfig sets up a bare-bones TLS config for a QUIC server func GenerateTLSConfig() *tls.Config { // nolint: gosec key, err := rsa.GenerateKey(rand.Reader, 1024) if err != nil { panic(err) } template := x509.Certificate{SerialNumber: big.NewInt(1)} certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key) if err != nil { panic(err) } keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}) certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) tlsCert, err := tls.X509KeyPair(certPEM, keyPEM) if err != nil { panic(err) } // nolint: gosec return &tls.Config{ Certificates: []tls.Certificate{tlsCert}, NextProtos: []string{"argotunnel"}, } } ================================================ FILE: connection/quic_datagram_v2.go ================================================ package connection import ( "context" "fmt" "net" "net/netip" "time" "github.com/google/uuid" "github.com/pkg/errors" pkgerrors "github.com/pkg/errors" "github.com/quic-go/quic-go" "github.com/rs/zerolog" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" "golang.org/x/sync/errgroup" cfdflow "github.com/cloudflare/cloudflared/flow" "github.com/cloudflare/cloudflared/datagramsession" "github.com/cloudflare/cloudflared/ingress" "github.com/cloudflare/cloudflared/management" "github.com/cloudflare/cloudflared/packet" cfdquic "github.com/cloudflare/cloudflared/quic" "github.com/cloudflare/cloudflared/tracing" "github.com/cloudflare/cloudflared/tunnelrpc/pogs" tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs" rpcquic "github.com/cloudflare/cloudflared/tunnelrpc/quic" ) const ( // emperically this capacity has been working well demuxChanCapacity = 16 ) var ( errInvalidDestinationIP = errors.New("unable to parse destination IP") ) // DatagramSessionHandler is a service that can serve datagrams for a connection and handle sessions from incoming // connection streams. type DatagramSessionHandler interface { Serve(context.Context) error pogs.SessionManager } type datagramV2Connection struct { conn quic.Connection index uint8 // sessionManager tracks active sessions. It receives datagrams from quic connection via datagramMuxer sessionManager datagramsession.Manager // flowLimiter tracks active sessions across the tunnel and limits new sessions if they are above the limit. flowLimiter cfdflow.Limiter // datagramMuxer mux/demux datagrams from quic connection datagramMuxer *cfdquic.DatagramMuxerV2 // originDialer is the origin dialer for UDP requests originDialer ingress.OriginUDPDialer // packetRouter acts as the origin router for ICMP requests packetRouter *ingress.PacketRouter rpcTimeout time.Duration streamWriteTimeout time.Duration logger *zerolog.Logger } func NewDatagramV2Connection(ctx context.Context, conn quic.Connection, originDialer ingress.OriginUDPDialer, icmpRouter ingress.ICMPRouter, index uint8, rpcTimeout time.Duration, streamWriteTimeout time.Duration, flowLimiter cfdflow.Limiter, logger *zerolog.Logger, ) DatagramSessionHandler { sessionDemuxChan := make(chan *packet.Session, demuxChanCapacity) datagramMuxer := cfdquic.NewDatagramMuxerV2(conn, logger, sessionDemuxChan) sessionManager := datagramsession.NewManager(logger, datagramMuxer.SendToSession, sessionDemuxChan) packetRouter := ingress.NewPacketRouter(icmpRouter, datagramMuxer, index, logger) return &datagramV2Connection{ conn: conn, index: index, sessionManager: sessionManager, flowLimiter: flowLimiter, datagramMuxer: datagramMuxer, originDialer: originDialer, packetRouter: packetRouter, rpcTimeout: rpcTimeout, streamWriteTimeout: streamWriteTimeout, logger: logger, } } func (d *datagramV2Connection) Serve(ctx context.Context) error { // If either goroutine from the errgroup returns at all (error or nil), we rely on its cancellation to make sure // the other goroutines as well. errGroup, ctx := errgroup.WithContext(ctx) errGroup.Go(func() error { return d.sessionManager.Serve(ctx) }) errGroup.Go(func() error { return d.datagramMuxer.ServeReceive(ctx) }) errGroup.Go(func() error { return d.packetRouter.Serve(ctx) }) return errGroup.Wait() } // RegisterUdpSession is the RPC method invoked by edge to register and run a session func (q *datagramV2Connection) RegisterUdpSession(ctx context.Context, sessionID uuid.UUID, dstIP net.IP, dstPort uint16, closeAfterIdleHint time.Duration, traceContext string) (*tunnelpogs.RegisterUdpSessionResponse, error) { traceCtx := tracing.NewTracedContext(ctx, traceContext, q.logger) ctx, registerSpan := traceCtx.Tracer().Start(traceCtx, "register-session", trace.WithAttributes( attribute.String("session-id", sessionID.String()), attribute.String("dst", fmt.Sprintf("%s:%d", dstIP, dstPort)), )) log := q.logger.With().Int(management.EventTypeKey, int(management.UDP)).Logger() // Try to start a new session if err := q.flowLimiter.Acquire(management.UDP.String()); err != nil { log.Warn().Msgf("Too many concurrent sessions being handled, rejecting udp proxy to %s:%d", dstIP, dstPort) err := pkgerrors.Wrap(err, "failed to start udp session due to rate limiting") tracing.EndWithErrorStatus(registerSpan, err) return nil, err } // We need to force the net.IP to IPv4 (if it's an IPv4 address) otherwise the net.IP conversion from capnp // will be a IPv4-mapped-IPv6 address. // In the case that the address is IPv6 we leave it untouched and parse it as normal. ip := dstIP.To4() if ip == nil { ip = dstIP } // Parse the dstIP and dstPort into a netip.AddrPort // This should never fail because the IP was already parsed as a valid net.IP destAddr, ok := netip.AddrFromSlice(ip) if !ok { log.Err(errInvalidDestinationIP).Msgf("Failed to parse destination proxy IP: %s", ip) tracing.EndWithErrorStatus(registerSpan, errInvalidDestinationIP) q.flowLimiter.Release() return nil, errInvalidDestinationIP } dstAddrPort := netip.AddrPortFrom(destAddr, dstPort) // Each session is a series of datagram from an eyeball to a dstIP:dstPort. // (src port, dst IP, dst port) uniquely identifies a session, so it needs a dedicated connected socket. originProxy, err := q.originDialer.DialUDP(dstAddrPort) if err != nil { log.Err(err).Msgf("Failed to create udp proxy to %s", dstAddrPort) tracing.EndWithErrorStatus(registerSpan, err) q.flowLimiter.Release() return nil, err } registerSpan.SetAttributes( attribute.Bool("socket-bind-success", true), attribute.String("src", originProxy.LocalAddr().String()), ) session, err := q.sessionManager.RegisterSession(ctx, sessionID, originProxy) if err != nil { originProxy.Close() log.Err(err).Str(datagramsession.LogFieldSessionID, datagramsession.FormatSessionID(sessionID)).Msgf("Failed to register udp session") tracing.EndWithErrorStatus(registerSpan, err) q.flowLimiter.Release() return nil, err } go func() { defer q.flowLimiter.Release() // we do the release here, instead of inside the `serveUDPSession` just to keep all acquire/release calls in the same method. q.serveUDPSession(session, closeAfterIdleHint) }() log.Debug(). Str(datagramsession.LogFieldSessionID, datagramsession.FormatSessionID(sessionID)). Str("src", originProxy.LocalAddr().String()). Str("dst", fmt.Sprintf("%s:%d", dstIP, dstPort)). Msgf("Registered session") tracing.End(registerSpan) resp := tunnelpogs.RegisterUdpSessionResponse{ Spans: traceCtx.GetProtoSpans(), } return &resp, nil } // UnregisterUdpSession is the RPC method invoked by edge to unregister and terminate a sesssion func (q *datagramV2Connection) UnregisterUdpSession(ctx context.Context, sessionID uuid.UUID, message string) error { return q.sessionManager.UnregisterSession(ctx, sessionID, message, true) } func (q *datagramV2Connection) serveUDPSession(session *datagramsession.Session, closeAfterIdleHint time.Duration) { ctx := q.conn.Context() closedByRemote, err := session.Serve(ctx, closeAfterIdleHint) // If session is terminated by remote, then we know it has been unregistered from session manager and edge if !closedByRemote { if err != nil { q.closeUDPSession(ctx, session.ID, err.Error()) } else { q.closeUDPSession(ctx, session.ID, "terminated without error") } } q.logger.Debug().Err(err). Int(management.EventTypeKey, int(management.UDP)). Str(datagramsession.LogFieldSessionID, datagramsession.FormatSessionID(session.ID)). Msg("Session terminated") } // closeUDPSession first unregisters the session from session manager, then it tries to unregister from edge func (q *datagramV2Connection) closeUDPSession(ctx context.Context, sessionID uuid.UUID, message string) { _ = q.sessionManager.UnregisterSession(ctx, sessionID, message, false) quicStream, err := q.conn.OpenStream() if err != nil { // Log this at debug because this is not an error if session was closed due to lost connection // with edge q.logger.Debug().Err(err). Int(management.EventTypeKey, int(management.UDP)). Str(datagramsession.LogFieldSessionID, datagramsession.FormatSessionID(sessionID)). Msgf("Failed to open quic stream to unregister udp session with edge") return } stream := cfdquic.NewSafeStreamCloser(quicStream, q.streamWriteTimeout, q.logger) defer stream.Close() rpcClientStream, err := rpcquic.NewSessionClient(ctx, stream, q.rpcTimeout) if err != nil { // Log this at debug because this is not an error if session was closed due to lost connection // with edge q.logger.Err(err).Str(datagramsession.LogFieldSessionID, datagramsession.FormatSessionID(sessionID)). Msgf("Failed to open rpc stream to unregister udp session with edge") return } defer rpcClientStream.Close() if err := rpcClientStream.UnregisterUdpSession(ctx, sessionID, message); err != nil { q.logger.Err(err).Str(datagramsession.LogFieldSessionID, datagramsession.FormatSessionID(sessionID)). Msgf("Failed to unregister udp session with edge") } } ================================================ FILE: connection/quic_datagram_v2_test.go ================================================ package connection import ( "context" "net" "testing" "time" "github.com/google/uuid" "github.com/quic-go/quic-go" "github.com/rs/zerolog" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" cfdflow "github.com/cloudflare/cloudflared/flow" "github.com/cloudflare/cloudflared/mocks" ) type mockQuicConnection struct{} func (m *mockQuicConnection) AcceptStream(_ context.Context) (quic.Stream, error) { return nil, nil } func (m *mockQuicConnection) AcceptUniStream(_ context.Context) (quic.ReceiveStream, error) { return nil, nil } func (m *mockQuicConnection) OpenStream() (quic.Stream, error) { return nil, nil } func (m *mockQuicConnection) OpenStreamSync(_ context.Context) (quic.Stream, error) { return nil, nil } func (m *mockQuicConnection) OpenUniStream() (quic.SendStream, error) { return nil, nil } func (m *mockQuicConnection) OpenUniStreamSync(_ context.Context) (quic.SendStream, error) { return nil, nil } func (m *mockQuicConnection) LocalAddr() net.Addr { return nil } func (m *mockQuicConnection) RemoteAddr() net.Addr { return nil } func (m *mockQuicConnection) CloseWithError(_ quic.ApplicationErrorCode, s string) error { return nil } func (m *mockQuicConnection) Context() context.Context { return nil } func (m *mockQuicConnection) ConnectionState() quic.ConnectionState { panic("not meant to be called") } func (m *mockQuicConnection) SendDatagram(_ []byte) error { return nil } func (m *mockQuicConnection) ReceiveDatagram(_ context.Context) ([]byte, error) { return nil, nil } func (m *mockQuicConnection) AddPath(*quic.Transport) (*quic.Path, error) { return nil, nil } func TestRateLimitOnNewDatagramV2UDPSession(t *testing.T) { log := zerolog.Nop() conn := &mockQuicConnection{} ctrl := gomock.NewController(t) flowLimiterMock := mocks.NewMockLimiter(ctrl) datagramConn := NewDatagramV2Connection( t.Context(), conn, nil, nil, 0, 0*time.Second, 0*time.Second, flowLimiterMock, &log, ) flowLimiterMock.EXPECT().Acquire("udp").Return(cfdflow.ErrTooManyActiveFlows) flowLimiterMock.EXPECT().Release().Times(0) _, err := datagramConn.RegisterUdpSession(t.Context(), uuid.New(), net.IPv4(0, 0, 0, 0), 1000, 1*time.Second, "") require.ErrorIs(t, err, cfdflow.ErrTooManyActiveFlows) } ================================================ FILE: connection/quic_datagram_v3.go ================================================ package connection import ( "context" "net" "time" "github.com/google/uuid" "github.com/pkg/errors" "github.com/quic-go/quic-go" "github.com/rs/zerolog" "github.com/cloudflare/cloudflared/ingress" "github.com/cloudflare/cloudflared/management" cfdquic "github.com/cloudflare/cloudflared/quic/v3" "github.com/cloudflare/cloudflared/tunnelrpc/pogs" ) var ( ErrUnsupportedRPCUDPRegistration = errors.New("datagram v3 does not support RegisterUdpSession RPC") ErrUnsupportedRPCUDPUnregistration = errors.New("datagram v3 does not support UnregisterUdpSession RPC") ) type datagramV3Connection struct { conn quic.Connection index uint8 // datagramMuxer mux/demux datagrams from quic connection datagramMuxer cfdquic.DatagramConn metrics cfdquic.Metrics logger *zerolog.Logger } func NewDatagramV3Connection(ctx context.Context, conn quic.Connection, sessionManager cfdquic.SessionManager, icmpRouter ingress.ICMPRouter, index uint8, metrics cfdquic.Metrics, logger *zerolog.Logger, ) DatagramSessionHandler { log := logger. With(). Int(management.EventTypeKey, int(management.UDP)). Uint8(LogFieldConnIndex, index). Logger() datagramMuxer := cfdquic.NewDatagramConn(conn, sessionManager, icmpRouter, index, metrics, &log) return &datagramV3Connection{ conn, index, datagramMuxer, metrics, logger, } } func (d *datagramV3Connection) Serve(ctx context.Context) error { return d.datagramMuxer.Serve(ctx) } func (d *datagramV3Connection) RegisterUdpSession(ctx context.Context, sessionID uuid.UUID, dstIP net.IP, dstPort uint16, closeAfterIdleHint time.Duration, traceContext string) (*pogs.RegisterUdpSessionResponse, error) { d.metrics.UnsupportedRemoteCommand(d.index, "register_udp_session") return nil, ErrUnsupportedRPCUDPRegistration } func (d *datagramV3Connection) UnregisterUdpSession(ctx context.Context, sessionID uuid.UUID, message string) error { d.metrics.UnsupportedRemoteCommand(d.index, "unregister_udp_session") return ErrUnsupportedRPCUDPUnregistration } ================================================ FILE: connection/tunnelsforha.go ================================================ package connection import ( "fmt" "sync" "github.com/prometheus/client_golang/prometheus" ) // tunnelsForHA maps this cloudflared instance's HA connections to the tunnel IDs they serve. type tunnelsForHA struct { sync.Mutex metrics *prometheus.GaugeVec entries map[uint8]string } // NewTunnelsForHA initializes the Prometheus metrics etc for a tunnelsForHA. func newTunnelsForHA() tunnelsForHA { metrics := prometheus.NewGaugeVec( prometheus.GaugeOpts{ Name: "tunnel_ids", Help: "The ID of all tunnels (and their corresponding HA connection ID) running in this instance of cloudflared.", }, []string{"tunnel_id", "ha_conn_id"}, ) prometheus.MustRegister(metrics) return tunnelsForHA{ metrics: metrics, entries: make(map[uint8]string), } } // Track a new tunnel ID, removing the disconnected tunnel (if any) and update metrics. func (t *tunnelsForHA) AddTunnelID(haConn uint8, tunnelID string) { t.Lock() defer t.Unlock() haStr := fmt.Sprintf("%v", haConn) if oldTunnelID, ok := t.entries[haConn]; ok { t.metrics.WithLabelValues(oldTunnelID, haStr).Dec() } t.entries[haConn] = tunnelID t.metrics.WithLabelValues(tunnelID, haStr).Inc() } func (t *tunnelsForHA) String() string { t.Lock() defer t.Unlock() return fmt.Sprintf("%v", t.entries) } ================================================ FILE: credentials/credentials.go ================================================ package credentials import ( "github.com/pkg/errors" "github.com/rs/zerolog" "github.com/cloudflare/cloudflared/cfapi" ) const ( logFieldOriginCertPath = "originCertPath" FedEndpoint = "fed" FedRampBaseApiURL = "https://api.fed.cloudflare.com/client/v4" FedRampHostname = "management.fed.argotunnel.com" ) type User struct { cert *OriginCert certPath string } func (c User) AccountID() string { return c.cert.AccountID } func (c User) Endpoint() string { return c.cert.Endpoint } func (c User) ZoneID() string { return c.cert.ZoneID } func (c User) APIToken() string { return c.cert.APIToken } func (c User) CertPath() string { return c.certPath } func (c User) IsFEDEndpoint() bool { return c.cert.Endpoint == FedEndpoint } // Client uses the user credentials to create a Cloudflare API client func (c *User) Client(apiURL string, userAgent string, log *zerolog.Logger) (cfapi.Client, error) { if apiURL == "" { return nil, errors.New("An api-url was not provided for the Cloudflare API client") } client, err := cfapi.NewRESTClient( apiURL, c.cert.AccountID, c.cert.ZoneID, c.cert.APIToken, userAgent, log, ) if err != nil { return nil, err } return client, nil } // Read will load and read the origin cert.pem to load the user credentials func Read(originCertPath string, log *zerolog.Logger) (*User, error) { originCertLog := log.With(). Str(logFieldOriginCertPath, originCertPath). Logger() originCertPath, err := FindOriginCert(originCertPath, &originCertLog) if err != nil { return nil, errors.Wrap(err, "Error locating origin cert") } blocks, err := readOriginCert(originCertPath) if err != nil { return nil, errors.Wrapf(err, "Can't read origin cert from %s", originCertPath) } cert, err := decodeOriginCert(blocks) if err != nil { return nil, errors.Wrap(err, "Error decoding origin cert") } if cert.AccountID == "" { return nil, errors.Errorf(`Origin certificate needs to be refreshed before creating new tunnels.\nDelete %s and run "cloudflared login" to obtain a new cert.`, originCertPath) } return &User{ cert: cert, certPath: originCertPath, }, nil } ================================================ FILE: credentials/credentials_test.go ================================================ package credentials import ( "io/fs" "os" "path/filepath" "testing" "github.com/stretchr/testify/require" ) func TestCredentialsRead(t *testing.T) { file, err := os.ReadFile("test-cloudflare-tunnel-cert-json.pem") require.NoError(t, err) dir := t.TempDir() certPath := filepath.Join(dir, originCertFile) _ = os.WriteFile(certPath, file, fs.ModePerm) user, err := Read(certPath, &nopLog) require.NoError(t, err) require.Equal(t, certPath, user.CertPath()) require.Equal(t, "test-service-key", user.APIToken()) require.Equal(t, "7b0a4d77dfb881c1a3b7d61ea9443e19", user.ZoneID()) require.Equal(t, "abcdabcdabcdabcd1234567890abcdef", user.AccountID()) } func TestCredentialsClient(t *testing.T) { user := User{ certPath: "/tmp/cert.pem", cert: &OriginCert{ ZoneID: "7b0a4d77dfb881c1a3b7d61ea9443e19", AccountID: "abcdabcdabcdabcd1234567890abcdef", APIToken: "test-service-key", }, } client, err := user.Client("example.com", "cloudflared/test", &nopLog) require.NoError(t, err) require.NotNil(t, client) } ================================================ FILE: credentials/origin_cert.go ================================================ package credentials import ( "bytes" "encoding/json" "encoding/pem" "fmt" "os" "path/filepath" "strings" "github.com/mitchellh/go-homedir" "github.com/rs/zerolog" "github.com/cloudflare/cloudflared/config" ) const ( DefaultCredentialFile = "cert.pem" ) type OriginCert struct { ZoneID string `json:"zoneID"` AccountID string `json:"accountID"` APIToken string `json:"apiToken"` Endpoint string `json:"endpoint,omitempty"` } func (oc *OriginCert) UnmarshalJSON(data []byte) error { var aux struct { ZoneID string `json:"zoneID"` AccountID string `json:"accountID"` APIToken string `json:"apiToken"` Endpoint string `json:"endpoint,omitempty"` } if err := json.Unmarshal(data, &aux); err != nil { return fmt.Errorf("error parsing OriginCert: %v", err) } oc.ZoneID = aux.ZoneID oc.AccountID = aux.AccountID oc.APIToken = aux.APIToken oc.Endpoint = strings.ToLower(aux.Endpoint) return nil } // FindDefaultOriginCertPath returns the first path that contains a cert.pem file. If none of the // DefaultConfigSearchDirectories contains a cert.pem file, return empty string func FindDefaultOriginCertPath() string { for _, defaultConfigDir := range config.DefaultConfigSearchDirectories() { originCertPath, _ := homedir.Expand(filepath.Join(defaultConfigDir, DefaultCredentialFile)) if ok := fileExists(originCertPath); ok { return originCertPath } } return "" } func DecodeOriginCert(blocks []byte) (*OriginCert, error) { return decodeOriginCert(blocks) } func (cert *OriginCert) EncodeOriginCert() ([]byte, error) { if cert == nil { return nil, fmt.Errorf("originCert cannot be nil") } buffer, err := json.Marshal(cert) if err != nil { return nil, fmt.Errorf("originCert marshal failed: %v", err) } block := pem.Block{ Type: "ARGO TUNNEL TOKEN", Headers: map[string]string{}, Bytes: buffer, } var out bytes.Buffer err = pem.Encode(&out, &block) if err != nil { return nil, fmt.Errorf("pem encoding failed: %v", err) } return out.Bytes(), nil } func decodeOriginCert(blocks []byte) (*OriginCert, error) { if len(blocks) == 0 { return nil, fmt.Errorf("cannot decode empty certificate") } originCert := OriginCert{} block, rest := pem.Decode(blocks) for block != nil { switch block.Type { case "PRIVATE KEY", "CERTIFICATE": // this is for legacy purposes. case "ARGO TUNNEL TOKEN": if originCert.ZoneID != "" || originCert.APIToken != "" { return nil, fmt.Errorf("found multiple tokens in the certificate") } // The token is a string, // Try the newer JSON format _ = json.Unmarshal(block.Bytes, &originCert) default: return nil, fmt.Errorf("unknown block %s in the certificate", block.Type) } block, rest = pem.Decode(rest) } if originCert.ZoneID == "" || originCert.APIToken == "" { return nil, fmt.Errorf("missing token in the certificate") } return &originCert, nil } func readOriginCert(originCertPath string) ([]byte, error) { originCert, err := os.ReadFile(originCertPath) if err != nil { return nil, fmt.Errorf("cannot read %s to load origin certificate", originCertPath) } return originCert, nil } // FindOriginCert will check to make sure that the certificate exists at the specified file path. func FindOriginCert(originCertPath string, log *zerolog.Logger) (string, error) { if originCertPath == "" { log.Error().Msgf("Cannot determine default origin certificate path. No file %s in %v. You need to specify the origin certificate path by specifying the origincert option in the configuration file, or set TUNNEL_ORIGIN_CERT environment variable", DefaultCredentialFile, config.DefaultConfigSearchDirectories()) return "", fmt.Errorf("client didn't specify origincert path") } var err error originCertPath, err = homedir.Expand(originCertPath) if err != nil { log.Err(err).Msgf("Cannot resolve origin certificate path") return "", fmt.Errorf("cannot resolve path %s", originCertPath) } // Check that the user has acquired a certificate using the login command ok := fileExists(originCertPath) if !ok { log.Error().Msgf(`Cannot find a valid certificate for your origin at the path: %s If the path above is wrong, specify the path with the -origincert option. If you don't have a certificate signed by Cloudflare, run the command: cloudflared login `, originCertPath) return "", fmt.Errorf("cannot find a valid certificate at the path %s", originCertPath) } return originCertPath, nil } // FileExists checks to see if a file exist at the provided path. func fileExists(path string) bool { fileStat, err := os.Stat(path) if err != nil { return false } return !fileStat.IsDir() } ================================================ FILE: credentials/origin_cert_test.go ================================================ package credentials import ( "fmt" "io/fs" "os" "path/filepath" "testing" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const ( originCertFile = "cert.pem" ) var nopLog = zerolog.Nop().With().Logger() func TestLoadOriginCert(t *testing.T) { cert, err := decodeOriginCert([]byte{}) assert.Equal(t, fmt.Errorf("cannot decode empty certificate"), err) assert.Nil(t, cert) blocks, err := os.ReadFile("test-cert-unknown-block.pem") require.NoError(t, err) cert, err = decodeOriginCert(blocks) assert.Equal(t, fmt.Errorf("unknown block RSA PRIVATE KEY in the certificate"), err) assert.Nil(t, cert) } func TestJSONArgoTunnelTokenEmpty(t *testing.T) { blocks, err := os.ReadFile("test-cert-no-token.pem") require.NoError(t, err) cert, err := decodeOriginCert(blocks) assert.Equal(t, fmt.Errorf("missing token in the certificate"), err) assert.Nil(t, cert) } func TestJSONArgoTunnelToken(t *testing.T) { // The given cert's Argo Tunnel Token was generated by base64 encoding this JSON: // { // "zoneID": "7b0a4d77dfb881c1a3b7d61ea9443e19", // "apiToken": "test-service-key", // "accountID": "abcdabcdabcdabcd1234567890abcdef" // } CloudflareTunnelTokenTest(t, "test-cloudflare-tunnel-cert-json.pem") } func CloudflareTunnelTokenTest(t *testing.T, path string) { blocks, err := os.ReadFile(path) require.NoError(t, err) cert, err := decodeOriginCert(blocks) require.NoError(t, err) assert.NotNil(t, cert) assert.Equal(t, "7b0a4d77dfb881c1a3b7d61ea9443e19", cert.ZoneID) key := "test-service-key" assert.Equal(t, key, cert.APIToken) } func TestFindOriginCert_Valid(t *testing.T) { file, err := os.ReadFile("test-cloudflare-tunnel-cert-json.pem") require.NoError(t, err) dir := t.TempDir() certPath := filepath.Join(dir, originCertFile) _ = os.WriteFile(certPath, file, fs.ModePerm) path, err := FindOriginCert(certPath, &nopLog) require.NoError(t, err) require.Equal(t, certPath, path) } func TestFindOriginCert_Missing(t *testing.T) { dir := t.TempDir() certPath := filepath.Join(dir, originCertFile) _, err := FindOriginCert(certPath, &nopLog) require.Error(t, err) } func TestEncodeDecodeOriginCert(t *testing.T) { cert := OriginCert{ ZoneID: "zone", AccountID: "account", APIToken: "token", Endpoint: "FED", } blocks, err := cert.EncodeOriginCert() require.NoError(t, err) decodedCert, err := DecodeOriginCert(blocks) require.NoError(t, err) assert.NotNil(t, cert) assert.Equal(t, "zone", decodedCert.ZoneID) assert.Equal(t, "account", decodedCert.AccountID) assert.Equal(t, "token", decodedCert.APIToken) assert.Equal(t, FedEndpoint, decodedCert.Endpoint) } func TestEncodeDecodeNilOriginCert(t *testing.T) { var cert *OriginCert blocks, err := cert.EncodeOriginCert() assert.Equal(t, fmt.Errorf("originCert cannot be nil"), err) require.Nil(t, blocks) } ================================================ FILE: credentials/test-cert-no-token.pem ================================================ -----BEGIN PRIVATE KEY----- MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCfGswL16Fz9Ei3 sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng6yHR1H5oX1Lg 1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bxtG0uyrXYh7Mt z0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyXPE6SuDvMHIeX 6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZAzNOxVKrUsyS x7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOglHJ2n0sMcZ+Ja 1Y649mPVAgMBAAECggEAEbPF0ah9fH0IzTU/CPbIeh3flyY8GDuMpR1HvwUurSWB IFI9bLyVAXKb8vYP1TMaTnXi5qmFof+/JShgyZc3+1tZtWTfoaiC8Y1bRfE2yk+D xmwddhDmijYGG7i8uEaeddSdFEh2GKAqkbV/QgBvN2Nl4EVmIOAJXXNe9l5LFyjy sR10aNVJRYV1FahrCTwZ3SovHP4d4AUvHh/3FFZDukHc37CFA0+CcR4uehp5yedi 2UdqaszXqunFo/3h+Tn9dW2C7gTTZx4+mfyaws3p3YOmdYArXvpejxHIc0FGwLBm sb9K7wGVUiF0Bt0ch+C1mdYrCaFNHnPuDswjmm3FwQKBgQDYtxOwwSLA6ZyppozX Doyx9a7PhiMHCFKSdVB4l8rpK545a+AmpG6LRScTtBsMTHBhT3IQ3QPWlVm1AhjF AvXMa1rOeaGbCbDn1xqEoEVPtj4tys8eTfyWmtU73jWTFauOt4/xpf/urEpg91xj m+Gl/8qgBrpm5rQxV5Y4MysRlQKBgQC78jzzlhocXGNvw0wT/K2NsknyeoZXqpIE QYL60FMl4geZn6w9hwxaL1r+g/tUjTnpBPQtS1r2Ed2gXby5zspN1g/PW8U3t3to P7zHIJ/sLBXrCh5RJko3hUgGhDNOOCIQj4IaKUfvHYvEIbIxlyI0vdsXsgXgMuQ8 pb9Yifn5QQKBgQCmGu0EtYQlyOlDP10EGSrN3Dm45l9CrKZdi326cN4eCkikSoLs G2x/YumouItiydP5QiNzuXOPrbmse4bwumwb2s0nJSMw6iSmDsFMlmuJxW2zO5e0 6qGH7fUyhgcaTanJIfk6hrm7/mKkH/S4hGpYCc8NCRsmc/35M+D4AoAoYQKBgQC0 LWpZaxDlF30MbAHHN3l6We2iU+vup0sMYXGb2ZOcwa/fir+ozIr++l8VmJmdWTan OWSM96zgMghx8Os4hhJTxF+rvqK242OfcVsc2x31X94zUaP2z+peh5uhA6Pb3Nxr W+iyA9k+Vujiwhr+h5D3VvtvH++aG6/KpGtoCf5nAQKBgQDXX2+d7bd5CLNLLFNd M2i4QoOFcSKIG+v4SuvgEJHgG8vGvxh2qlSxnMWuPV+7/1P5ATLqDj1PlKms+BNR y7sc5AT9PclkL3Y9MNzOu0LXyBkGYcl8M0EQfLv9VPbWT+NXiMg/O2CHiT02pAAz uQicoQq3yzeQh20wtrtaXzTNmA== -----END PRIVATE KEY----- -----BEGIN CERTIFICATE----- MIID+jCCA6CgAwIBAgIUJhFxUKEGvTRc3CjCok6dbPGH/P4wCgYIKoZIzj0EAwIw gagxCzAJBgNVBAYTAlVTMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMTgwNgYD VQQLEy9DbG91ZEZsYXJlIE9yaWdpbiBTU0wgRUNDIENlcnRpZmljYXRlIEF1dGhv cml0eTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzETMBEGA1UECBMKQ2FsaWZvcm5p YTEXMBUGA1UEAxMOKGRldiB1c2Ugb25seSkwHhcNMTcxMDEzMTM1OTAwWhcNMzIx MDA5MTM1OTAwWjBiMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMR0wGwYDVQQL ExRDbG91ZEZsYXJlIE9yaWdpbiBDQTEmMCQGA1UEAxMdQ2xvdWRGbGFyZSBPcmln aW4gQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCf GswL16Fz9Ei3sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng 6yHR1H5oX1Lg1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bx tG0uyrXYh7Mtz0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyX PE6SuDvMHIeX6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZ AzNOxVKrUsySx7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOgl HJ2n0sMcZ+Ja1Y649mPVAgMBAAGjggEgMIIBHDAOBgNVHQ8BAf8EBAMCBaAwEwYD VR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUzA6f2Ajq zhX67c6piY2a1uTiUkwwHwYDVR0jBBgwFoAU2qfBlqxKMZnf0QeTeYiMelfqJfgw RAYIKwYBBQUHAQEEODA2MDQGCCsGAQUFBzABhihodHRwOi8vb2NzcC5jbG91ZGZs YXJlLmNvbS9vcmlnaW5fZWNjX2NhMCMGA1UdEQQcMBqCDCouYXJub2xkLmNvbYIK YXJub2xkLmNvbTA8BgNVHR8ENTAzMDGgL6AthitodHRwOi8vY3JsLmNsb3VkZmxh cmUuY29tL29yaWdpbl9lY2NfY2EuY3JsMAoGCCqGSM49BAMCA0gAMEUCIDV7HoMj K5rShE/l+90YAOzHC89OH/wUz3I5KYOFuehoAiEA8e92aIf9XBkr0K6EvFCiSsD+ x+Yo/cL8fGfVpPt4UM8= -----END CERTIFICATE----- -----BEGIN ARGO TUNNEL TOKEN----- eyJ6b25lSUQiOiAiN2IwYTRkNzdkZmI4ODFjMWEzYjdkNjFlYTk0NDNlMTkiLCAiYWNjb3VudElE IjogImFiY2RhYmNkYWJjZGFiY2QxMjM0NTY3ODkwYWJjZGVmIn0= -----END ARGO TUNNEL TOKEN----- ================================================ FILE: credentials/test-cert-unknown-block.pem ================================================ -----BEGIN PRIVATE KEY----- MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCfGswL16Fz9Ei3 sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng6yHR1H5oX1Lg 1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bxtG0uyrXYh7Mt z0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyXPE6SuDvMHIeX 6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZAzNOxVKrUsyS x7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOglHJ2n0sMcZ+Ja 1Y649mPVAgMBAAECggEAEbPF0ah9fH0IzTU/CPbIeh3flyY8GDuMpR1HvwUurSWB IFI9bLyVAXKb8vYP1TMaTnXi5qmFof+/JShgyZc3+1tZtWTfoaiC8Y1bRfE2yk+D xmwddhDmijYGG7i8uEaeddSdFEh2GKAqkbV/QgBvN2Nl4EVmIOAJXXNe9l5LFyjy sR10aNVJRYV1FahrCTwZ3SovHP4d4AUvHh/3FFZDukHc37CFA0+CcR4uehp5yedi 2UdqaszXqunFo/3h+Tn9dW2C7gTTZx4+mfyaws3p3YOmdYArXvpejxHIc0FGwLBm sb9K7wGVUiF0Bt0ch+C1mdYrCaFNHnPuDswjmm3FwQKBgQDYtxOwwSLA6ZyppozX Doyx9a7PhiMHCFKSdVB4l8rpK545a+AmpG6LRScTtBsMTHBhT3IQ3QPWlVm1AhjF AvXMa1rOeaGbCbDn1xqEoEVPtj4tys8eTfyWmtU73jWTFauOt4/xpf/urEpg91xj m+Gl/8qgBrpm5rQxV5Y4MysRlQKBgQC78jzzlhocXGNvw0wT/K2NsknyeoZXqpIE QYL60FMl4geZn6w9hwxaL1r+g/tUjTnpBPQtS1r2Ed2gXby5zspN1g/PW8U3t3to P7zHIJ/sLBXrCh5RJko3hUgGhDNOOCIQj4IaKUfvHYvEIbIxlyI0vdsXsgXgMuQ8 pb9Yifn5QQKBgQCmGu0EtYQlyOlDP10EGSrN3Dm45l9CrKZdi326cN4eCkikSoLs G2x/YumouItiydP5QiNzuXOPrbmse4bwumwb2s0nJSMw6iSmDsFMlmuJxW2zO5e0 6qGH7fUyhgcaTanJIfk6hrm7/mKkH/S4hGpYCc8NCRsmc/35M+D4AoAoYQKBgQC0 LWpZaxDlF30MbAHHN3l6We2iU+vup0sMYXGb2ZOcwa/fir+ozIr++l8VmJmdWTan OWSM96zgMghx8Os4hhJTxF+rvqK242OfcVsc2x31X94zUaP2z+peh5uhA6Pb3Nxr W+iyA9k+Vujiwhr+h5D3VvtvH++aG6/KpGtoCf5nAQKBgQDXX2+d7bd5CLNLLFNd M2i4QoOFcSKIG+v4SuvgEJHgG8vGvxh2qlSxnMWuPV+7/1P5ATLqDj1PlKms+BNR y7sc5AT9PclkL3Y9MNzOu0LXyBkGYcl8M0EQfLv9VPbWT+NXiMg/O2CHiT02pAAz uQicoQq3yzeQh20wtrtaXzTNmA== -----END PRIVATE KEY----- -----BEGIN CERTIFICATE----- MIID+jCCA6CgAwIBAgIUJhFxUKEGvTRc3CjCok6dbPGH/P4wCgYIKoZIzj0EAwIw gagxCzAJBgNVBAYTAlVTMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMTgwNgYD VQQLEy9DbG91ZEZsYXJlIE9yaWdpbiBTU0wgRUNDIENlcnRpZmljYXRlIEF1dGhv cml0eTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzETMBEGA1UECBMKQ2FsaWZvcm5p YTEXMBUGA1UEAxMOKGRldiB1c2Ugb25seSkwHhcNMTcxMDEzMTM1OTAwWhcNMzIx MDA5MTM1OTAwWjBiMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMR0wGwYDVQQL ExRDbG91ZEZsYXJlIE9yaWdpbiBDQTEmMCQGA1UEAxMdQ2xvdWRGbGFyZSBPcmln aW4gQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCf GswL16Fz9Ei3sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng 6yHR1H5oX1Lg1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bx tG0uyrXYh7Mtz0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyX PE6SuDvMHIeX6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZ AzNOxVKrUsySx7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOgl HJ2n0sMcZ+Ja1Y649mPVAgMBAAGjggEgMIIBHDAOBgNVHQ8BAf8EBAMCBaAwEwYD VR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUzA6f2Ajq zhX67c6piY2a1uTiUkwwHwYDVR0jBBgwFoAU2qfBlqxKMZnf0QeTeYiMelfqJfgw RAYIKwYBBQUHAQEEODA2MDQGCCsGAQUFBzABhihodHRwOi8vb2NzcC5jbG91ZGZs YXJlLmNvbS9vcmlnaW5fZWNjX2NhMCMGA1UdEQQcMBqCDCouYXJub2xkLmNvbYIK YXJub2xkLmNvbTA8BgNVHR8ENTAzMDGgL6AthitodHRwOi8vY3JsLmNsb3VkZmxh cmUuY29tL29yaWdpbl9lY2NfY2EuY3JsMAoGCCqGSM49BAMCA0gAMEUCIDV7HoMj K5rShE/l+90YAOzHC89OH/wUz3I5KYOFuehoAiEA8e92aIf9XBkr0K6EvFCiSsD+ x+Yo/cL8fGfVpPt4UM8= -----END CERTIFICATE----- -----BEGIN ARGO TUNNEL TOKEN----- N2IwYTRkNzdkZmI4ODFjMWEzYjdkNjFlYTk0NDNlMTkKdjEuMC01OGJkNGY5ZTI4 ZjdiM2MyOGUwNWEzNWZmM2U4MGFiNGZkOTY0NGVmM2ZlY2U1MzdlYjBkMTJlMmU5 MjU4MjE3LTE4MzQ0MmZiYjBiYmRiM2U1NzE1NThmZWM5YjU1ODllYmQ3N2FhZmM4 NzQ5OGVlM2YwOWY2NGE0YWQ3OWZmZTg3OTFlZGJhZTA4YjM2YzFkOGYxZDcwYTg2 NzBkZTU2OTIyZGZmOTJiMTVkMjE0YTUyNGY0ZWJmYTE5NTg4NTllLTdjZTgwZjc5 OTIxMzEyYTYwMjJjNWQyNWUyZDM4MGY4MmNlYWVmZTNmYmRjNDNkZDEzYjA4MGUz ZWYxZTI2Zjc= -----END ARGO TUNNEL TOKEN----- -----BEGIN RSA PRIVATE KEY----- MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCfGswL16Fz9Ei3 sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng6yHR1H5oX1Lg 1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bxtG0uyrXYh7Mt z0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyXPE6SuDvMHIeX 6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZAzNOxVKrUsyS x7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOglHJ2n0sMcZ+Ja 1Y649mPVAgMBAAECggEAEbPF0ah9fH0IzTU/CPbIeh3flyY8GDuMpR1HvwUurSWB IFI9bLyVAXKb8vYP1TMaTnXi5qmFof+/JShgyZc3+1tZtWTfoaiC8Y1bRfE2yk+D xmwddhDmijYGG7i8uEaeddSdFEh2GKAqkbV/QgBvN2Nl4EVmIOAJXXNe9l5LFyjy sR10aNVJRYV1FahrCTwZ3SovHP4d4AUvHh/3FFZDukHc37CFA0+CcR4uehp5yedi 2UdqaszXqunFo/3h+Tn9dW2C7gTTZx4+mfyaws3p3YOmdYArXvpejxHIc0FGwLBm sb9K7wGVUiF0Bt0ch+C1mdYrCaFNHnPuDswjmm3FwQKBgQDYtxOwwSLA6ZyppozX Doyx9a7PhiMHCFKSdVB4l8rpK545a+AmpG6LRScTtBsMTHBhT3IQ3QPWlVm1AhjF AvXMa1rOeaGbCbDn1xqEoEVPtj4tys8eTfyWmtU73jWTFauOt4/xpf/urEpg91xj m+Gl/8qgBrpm5rQxV5Y4MysRlQKBgQC78jzzlhocXGNvw0wT/K2NsknyeoZXqpIE QYL60FMl4geZn6w9hwxaL1r+g/tUjTnpBPQtS1r2Ed2gXby5zspN1g/PW8U3t3to P7zHIJ/sLBXrCh5RJko3hUgGhDNOOCIQj4IaKUfvHYvEIbIxlyI0vdsXsgXgMuQ8 pb9Yifn5QQKBgQCmGu0EtYQlyOlDP10EGSrN3Dm45l9CrKZdi326cN4eCkikSoLs G2x/YumouItiydP5QiNzuXOPrbmse4bwumwb2s0nJSMw6iSmDsFMlmuJxW2zO5e0 6qGH7fUyhgcaTanJIfk6hrm7/mKkH/S4hGpYCc8NCRsmc/35M+D4AoAoYQKBgQC0 LWpZaxDlF30MbAHHN3l6We2iU+vup0sMYXGb2ZOcwa/fir+ozIr++l8VmJmdWTan OWSM96zgMghx8Os4hhJTxF+rvqK242OfcVsc2x31X94zUaP2z+peh5uhA6Pb3Nxr W+iyA9k+Vujiwhr+h5D3VvtvH++aG6/KpGtoCf5nAQKBgQDXX2+d7bd5CLNLLFNd M2i4QoOFcSKIG+v4SuvgEJHgG8vGvxh2qlSxnMWuPV+7/1P5ATLqDj1PlKms+BNR y7sc5AT9PclkL3Y9MNzOu0LXyBkGYcl8M0EQfLv9VPbWT+NXiMg/O2CHiT02pAAz uQicoQq3yzeQh20wtrtaXzTNmA== -----END RSA PRIVATE KEY----- ================================================ FILE: credentials/test-cloudflare-tunnel-cert-json.pem ================================================ -----BEGIN PRIVATE KEY----- MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCfGswL16Fz9Ei3 sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng6yHR1H5oX1Lg 1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bxtG0uyrXYh7Mt z0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyXPE6SuDvMHIeX 6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZAzNOxVKrUsyS x7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOglHJ2n0sMcZ+Ja 1Y649mPVAgMBAAECggEAEbPF0ah9fH0IzTU/CPbIeh3flyY8GDuMpR1HvwUurSWB IFI9bLyVAXKb8vYP1TMaTnXi5qmFof+/JShgyZc3+1tZtWTfoaiC8Y1bRfE2yk+D xmwddhDmijYGG7i8uEaeddSdFEh2GKAqkbV/QgBvN2Nl4EVmIOAJXXNe9l5LFyjy sR10aNVJRYV1FahrCTwZ3SovHP4d4AUvHh/3FFZDukHc37CFA0+CcR4uehp5yedi 2UdqaszXqunFo/3h+Tn9dW2C7gTTZx4+mfyaws3p3YOmdYArXvpejxHIc0FGwLBm sb9K7wGVUiF0Bt0ch+C1mdYrCaFNHnPuDswjmm3FwQKBgQDYtxOwwSLA6ZyppozX Doyx9a7PhiMHCFKSdVB4l8rpK545a+AmpG6LRScTtBsMTHBhT3IQ3QPWlVm1AhjF AvXMa1rOeaGbCbDn1xqEoEVPtj4tys8eTfyWmtU73jWTFauOt4/xpf/urEpg91xj m+Gl/8qgBrpm5rQxV5Y4MysRlQKBgQC78jzzlhocXGNvw0wT/K2NsknyeoZXqpIE QYL60FMl4geZn6w9hwxaL1r+g/tUjTnpBPQtS1r2Ed2gXby5zspN1g/PW8U3t3to P7zHIJ/sLBXrCh5RJko3hUgGhDNOOCIQj4IaKUfvHYvEIbIxlyI0vdsXsgXgMuQ8 pb9Yifn5QQKBgQCmGu0EtYQlyOlDP10EGSrN3Dm45l9CrKZdi326cN4eCkikSoLs G2x/YumouItiydP5QiNzuXOPrbmse4bwumwb2s0nJSMw6iSmDsFMlmuJxW2zO5e0 6qGH7fUyhgcaTanJIfk6hrm7/mKkH/S4hGpYCc8NCRsmc/35M+D4AoAoYQKBgQC0 LWpZaxDlF30MbAHHN3l6We2iU+vup0sMYXGb2ZOcwa/fir+ozIr++l8VmJmdWTan OWSM96zgMghx8Os4hhJTxF+rvqK242OfcVsc2x31X94zUaP2z+peh5uhA6Pb3Nxr W+iyA9k+Vujiwhr+h5D3VvtvH++aG6/KpGtoCf5nAQKBgQDXX2+d7bd5CLNLLFNd M2i4QoOFcSKIG+v4SuvgEJHgG8vGvxh2qlSxnMWuPV+7/1P5ATLqDj1PlKms+BNR y7sc5AT9PclkL3Y9MNzOu0LXyBkGYcl8M0EQfLv9VPbWT+NXiMg/O2CHiT02pAAz uQicoQq3yzeQh20wtrtaXzTNmA== -----END PRIVATE KEY----- -----BEGIN CERTIFICATE----- MIID+jCCA6CgAwIBAgIUJhFxUKEGvTRc3CjCok6dbPGH/P4wCgYIKoZIzj0EAwIw gagxCzAJBgNVBAYTAlVTMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMTgwNgYD VQQLEy9DbG91ZEZsYXJlIE9yaWdpbiBTU0wgRUNDIENlcnRpZmljYXRlIEF1dGhv cml0eTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzETMBEGA1UECBMKQ2FsaWZvcm5p YTEXMBUGA1UEAxMOKGRldiB1c2Ugb25seSkwHhcNMTcxMDEzMTM1OTAwWhcNMzIx MDA5MTM1OTAwWjBiMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMR0wGwYDVQQL ExRDbG91ZEZsYXJlIE9yaWdpbiBDQTEmMCQGA1UEAxMdQ2xvdWRGbGFyZSBPcmln aW4gQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCf GswL16Fz9Ei3sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng 6yHR1H5oX1Lg1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bx tG0uyrXYh7Mtz0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyX PE6SuDvMHIeX6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZ AzNOxVKrUsySx7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOgl HJ2n0sMcZ+Ja1Y649mPVAgMBAAGjggEgMIIBHDAOBgNVHQ8BAf8EBAMCBaAwEwYD VR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUzA6f2Ajq zhX67c6piY2a1uTiUkwwHwYDVR0jBBgwFoAU2qfBlqxKMZnf0QeTeYiMelfqJfgw RAYIKwYBBQUHAQEEODA2MDQGCCsGAQUFBzABhihodHRwOi8vb2NzcC5jbG91ZGZs YXJlLmNvbS9vcmlnaW5fZWNjX2NhMCMGA1UdEQQcMBqCDCouYXJub2xkLmNvbYIK YXJub2xkLmNvbTA8BgNVHR8ENTAzMDGgL6AthitodHRwOi8vY3JsLmNsb3VkZmxh cmUuY29tL29yaWdpbl9lY2NfY2EuY3JsMAoGCCqGSM49BAMCA0gAMEUCIDV7HoMj K5rShE/l+90YAOzHC89OH/wUz3I5KYOFuehoAiEA8e92aIf9XBkr0K6EvFCiSsD+ x+Yo/cL8fGfVpPt4UM8= -----END CERTIFICATE----- -----BEGIN ARGO TUNNEL TOKEN----- eyJ6b25lSUQiOiAiN2IwYTRkNzdkZmI4ODFjMWEzYjdkNjFlYTk0NDNlMTkiLCAiYXBpVG9rZW4i OiAidGVzdC1zZXJ2aWNlLWtleSIsICJhY2NvdW50SUQiOiAiYWJjZGFiY2RhYmNkYWJjZDEyMzQ1 Njc4OTBhYmNkZWYifQ== -----END ARGO TUNNEL TOKEN----- ================================================ FILE: datagramsession/event.go ================================================ package datagramsession import ( "fmt" "io" "github.com/google/uuid" ) // registerSessionEvent is an event to start tracking a new session type registerSessionEvent struct { sessionID uuid.UUID originProxy io.ReadWriteCloser resultChan chan *Session } func newRegisterSessionEvent(sessionID uuid.UUID, originProxy io.ReadWriteCloser) *registerSessionEvent { return ®isterSessionEvent{ sessionID: sessionID, originProxy: originProxy, resultChan: make(chan *Session, 1), } } // unregisterSessionEvent is an event to stop tracking and terminate the session. type unregisterSessionEvent struct { sessionID uuid.UUID err *errClosedSession } // ClosedSessionError represent a condition that closes the session other than I/O // I/O error is not included, because the side that closes the session is ambiguous. type errClosedSession struct { message string byRemote bool } func (sc *errClosedSession) Error() string { if sc.byRemote { return fmt.Sprintf("session closed by remote due to %s", sc.message) } else { return fmt.Sprintf("session closed by local due to %s", sc.message) } } ================================================ FILE: datagramsession/manager.go ================================================ package datagramsession import ( "context" "fmt" "io" "strings" "time" "github.com/google/uuid" "github.com/rs/zerolog" "github.com/cloudflare/cloudflared/management" "github.com/cloudflare/cloudflared/packet" ) const ( requestChanCapacity = 16 defaultReqTimeout = time.Second * 5 ) var ( errSessionManagerClosed = fmt.Errorf("session manager closed") LogFieldSessionID = "sessionID" ) func FormatSessionID(sessionID uuid.UUID) string { sessionIDStr := sessionID.String() sessionIDStr = strings.ReplaceAll(sessionIDStr, "-", "") return sessionIDStr } // Manager defines the APIs to manage sessions from the same transport. type Manager interface { // Serve starts the event loop Serve(ctx context.Context) error // RegisterSession starts tracking a session. Caller is responsible for starting the session RegisterSession(ctx context.Context, sessionID uuid.UUID, dstConn io.ReadWriteCloser) (*Session, error) // UnregisterSession stops tracking the session and terminates it UnregisterSession(ctx context.Context, sessionID uuid.UUID, message string, byRemote bool) error // UpdateLogger updates the logger used by the Manager UpdateLogger(log *zerolog.Logger) } type manager struct { registrationChan chan *registerSessionEvent unregistrationChan chan *unregisterSessionEvent sendFunc transportSender receiveChan <-chan *packet.Session closedChan <-chan struct{} sessions map[uuid.UUID]*Session log *zerolog.Logger // timeout waiting for an API to finish. This can be overriden in test timeout time.Duration } func NewManager(log *zerolog.Logger, sendF transportSender, receiveChan <-chan *packet.Session) *manager { return &manager{ registrationChan: make(chan *registerSessionEvent), unregistrationChan: make(chan *unregisterSessionEvent), sendFunc: sendF, receiveChan: receiveChan, closedChan: make(chan struct{}), sessions: make(map[uuid.UUID]*Session), log: log, timeout: defaultReqTimeout, } } func (m *manager) UpdateLogger(log *zerolog.Logger) { // Benign data race, no problem if the old pointer is read or not concurrently. m.log = log } func (m *manager) Serve(ctx context.Context) error { for { select { case <-ctx.Done(): m.shutdownSessions(ctx.Err()) return ctx.Err() // receiveChan is buffered, so the transport can read more datagrams from transport while the event loop is // processing other events case datagram := <-m.receiveChan: m.sendToSession(datagram) case registration := <-m.registrationChan: m.registerSession(ctx, registration) case unregistration := <-m.unregistrationChan: m.unregisterSession(unregistration) } } } func (m *manager) shutdownSessions(err error) { if err == nil { err = errSessionManagerClosed } closeSessionErr := &errClosedSession{ message: err.Error(), // Usually connection with remote has been closed, so set this to true to skip unregistering from remote byRemote: true, } for _, s := range m.sessions { m.unregisterSession(&unregisterSessionEvent{ sessionID: s.ID, err: closeSessionErr, }) } } func (m *manager) RegisterSession(ctx context.Context, sessionID uuid.UUID, originProxy io.ReadWriteCloser) (*Session, error) { ctx, cancel := context.WithTimeout(ctx, m.timeout) defer cancel() event := newRegisterSessionEvent(sessionID, originProxy) select { case <-ctx.Done(): m.log.Error().Msg("Datagram session registration timeout") return nil, ctx.Err() case m.registrationChan <- event: session := <-event.resultChan return session, nil // Once closedChan is closed, manager won't accept more registration because nothing is // reading from registrationChan and it's an unbuffered channel case <-m.closedChan: return nil, errSessionManagerClosed } } func (m *manager) registerSession(ctx context.Context, registration *registerSessionEvent) { session := m.newSession(registration.sessionID, registration.originProxy) m.sessions[registration.sessionID] = session registration.resultChan <- session incrementUDPSessions() } func (m *manager) newSession(id uuid.UUID, dstConn io.ReadWriteCloser) *Session { logger := m.log.With(). Int(management.EventTypeKey, int(management.UDP)). Str(LogFieldSessionID, FormatSessionID(id)).Logger() return &Session{ ID: id, sendFunc: m.sendFunc, dstConn: dstConn, // activeAtChan has low capacity. It can be full when there are many concurrent read/write. markActive() will // drop instead of blocking because last active time only needs to be an approximation activeAtChan: make(chan time.Time, 2), // capacity is 2 because close() and dstToTransport routine in Serve() can write to this channel closeChan: make(chan error, 2), log: &logger, } } func (m *manager) UnregisterSession(ctx context.Context, sessionID uuid.UUID, message string, byRemote bool) error { ctx, cancel := context.WithTimeout(ctx, m.timeout) defer cancel() event := &unregisterSessionEvent{ sessionID: sessionID, err: &errClosedSession{ message: message, byRemote: byRemote, }, } select { case <-ctx.Done(): m.log.Error().Msg("Datagram session unregistration timeout") return ctx.Err() case m.unregistrationChan <- event: return nil case <-m.closedChan: return errSessionManagerClosed } } func (m *manager) unregisterSession(unregistration *unregisterSessionEvent) { session, ok := m.sessions[unregistration.sessionID] if ok { delete(m.sessions, unregistration.sessionID) session.close(unregistration.err) decrementUDPActiveSessions() } } func (m *manager) sendToSession(datagram *packet.Session) { session, ok := m.sessions[datagram.ID] if !ok { m.log.Error().Str(LogFieldSessionID, FormatSessionID(datagram.ID)).Msg("session not found") return } // session writes to destination over a connected UDP socket, which should not be blocking, so this call doesn't // need to run in another go routine session.transportToDst(datagram.Payload) } ================================================ FILE: datagramsession/manager_test.go ================================================ package datagramsession import ( "bytes" "context" "fmt" "io" "net" "sync" "testing" "time" "github.com/google/uuid" "github.com/rs/zerolog" "github.com/stretchr/testify/require" "golang.org/x/sync/errgroup" "github.com/cloudflare/cloudflared/packet" ) var ( nopLogger = zerolog.Nop() ) func TestManagerServe(t *testing.T) { const ( sessions = 2 msgs = 5 remoteUnregisterMsg = "eyeball closed connection" ) requestChan := make(chan *packet.Session) transport := mockQUICTransport{ sessions: make(map[uuid.UUID]chan []byte), } for i := 0; i < sessions; i++ { transport.sessions[uuid.New()] = make(chan []byte) } mg := NewManager(&nopLogger, transport.MuxSession, requestChan) ctx, cancel := context.WithCancel(context.Background()) serveDone := make(chan struct{}) go func(ctx context.Context) { mg.Serve(ctx) close(serveDone) }(ctx) errGroup, ctx := errgroup.WithContext(ctx) for sessionID, eyeballRespChan := range transport.sessions { // Assign loop variables to local variables sID := sessionID payload := testPayload(sID) expectResp := testResponse(payload) cfdConn, originConn := net.Pipe() origin := mockOrigin{ expectMsgCount: msgs, expectedMsg: payload, expectedResp: expectResp, conn: originConn, } eyeball := mockEyeballSession{ id: sID, expectedMsgCount: msgs, expectedMsg: payload, expectedResponse: expectResp, respReceiver: eyeballRespChan, } // Assign loop variables to local variables errGroup.Go(func() error { session, err := mg.RegisterSession(ctx, sID, cfdConn) require.NoError(t, err) reqErrGroup, reqCtx := errgroup.WithContext(ctx) reqErrGroup.Go(func() error { return origin.serve() }) reqErrGroup.Go(func() error { return eyeball.serve(reqCtx, requestChan) }) sessionDone := make(chan struct{}) go func() { closedByRemote, err := session.Serve(ctx, time.Minute*2) closeSession := &errClosedSession{ message: remoteUnregisterMsg, byRemote: true, } require.Equal(t, closeSession, err) require.True(t, closedByRemote) close(sessionDone) }() // Make sure eyeball and origin have received all messages before unregistering the session require.NoError(t, reqErrGroup.Wait()) require.NoError(t, mg.UnregisterSession(ctx, sID, remoteUnregisterMsg, true)) <-sessionDone return nil }) } require.NoError(t, errGroup.Wait()) cancel() <-serveDone } func TestTimeout(t *testing.T) { const ( testTimeout = time.Millisecond * 50 ) mg := NewManager(&nopLogger, nil, nil) mg.timeout = testTimeout ctx := context.Background() sessionID := uuid.New() // session manager is not running, so event loop is not running and therefore calling the APIs should timeout session, err := mg.RegisterSession(ctx, sessionID, nil) require.ErrorIs(t, err, context.DeadlineExceeded) require.Nil(t, session) err = mg.UnregisterSession(ctx, sessionID, "session gone", true) require.ErrorIs(t, err, context.DeadlineExceeded) } func TestUnregisterSessionCloseSession(t *testing.T) { sessionID := uuid.New() payload := []byte(t.Name()) sender := newMockTransportSender(sessionID, payload) mg := NewManager(&nopLogger, sender.muxSession, nil) ctx, cancel := context.WithCancel(context.Background()) managerDone := make(chan struct{}) go func() { err := mg.Serve(ctx) require.Error(t, err) close(managerDone) }() cfdConn, originConn := net.Pipe() session, err := mg.RegisterSession(ctx, sessionID, cfdConn) require.NoError(t, err) require.NotNil(t, session) unregisteredChan := make(chan struct{}) go func() { _, err := originConn.Write(payload) require.NoError(t, err) err = mg.UnregisterSession(ctx, sessionID, "eyeball closed session", true) require.NoError(t, err) close(unregisteredChan) }() closedByRemote, err := session.Serve(ctx, time.Minute) require.True(t, closedByRemote) require.Error(t, err) <-unregisteredChan cancel() <-managerDone } func TestManagerCtxDoneCloseSessions(t *testing.T) { sessionID := uuid.New() payload := []byte(t.Name()) sender := newMockTransportSender(sessionID, payload) mg := NewManager(&nopLogger, sender.muxSession, nil) ctx, cancel := context.WithCancel(context.Background()) var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() err := mg.Serve(ctx) require.Error(t, err) }() cfdConn, originConn := net.Pipe() session, err := mg.RegisterSession(ctx, sessionID, cfdConn) require.NoError(t, err) require.NotNil(t, session) wg.Add(1) go func() { defer wg.Done() _, err := originConn.Write(payload) require.NoError(t, err) cancel() }() closedByRemote, err := session.Serve(ctx, time.Minute) require.False(t, closedByRemote) require.Error(t, err) wg.Wait() } type mockOrigin struct { expectMsgCount int expectedMsg []byte expectedResp []byte conn io.ReadWriteCloser } func (mo *mockOrigin) serve() error { expectedMsgLen := len(mo.expectedMsg) readBuffer := make([]byte, expectedMsgLen+1) for i := 0; i < mo.expectMsgCount; i++ { n, err := mo.conn.Read(readBuffer) if err != nil { return err } if n != expectedMsgLen { return fmt.Errorf("Expect to read %d bytes, read %d", expectedMsgLen, n) } if !bytes.Equal(readBuffer[:n], mo.expectedMsg) { return fmt.Errorf("Expect %v, read %v", mo.expectedMsg, readBuffer[:n]) } _, err = mo.conn.Write(mo.expectedResp) if err != nil { return err } } return nil } func testPayload(sessionID uuid.UUID) []byte { return []byte(fmt.Sprintf("Message from %s", sessionID)) } func testResponse(msg []byte) []byte { return []byte(fmt.Sprintf("Response to %v", msg)) } type mockQUICTransport struct { sessions map[uuid.UUID]chan []byte } func (me *mockQUICTransport) MuxSession(session *packet.Session) error { s := me.sessions[session.ID] s <- session.Payload return nil } type mockEyeballSession struct { id uuid.UUID expectedMsgCount int expectedMsg []byte expectedResponse []byte respReceiver <-chan []byte } func (me *mockEyeballSession) serve(ctx context.Context, requestChan chan *packet.Session) error { for i := 0; i < me.expectedMsgCount; i++ { requestChan <- &packet.Session{ ID: me.id, Payload: me.expectedMsg, } resp := <-me.respReceiver if !bytes.Equal(resp, me.expectedResponse) { return fmt.Errorf("Expect %v, read %v", me.expectedResponse, resp) } fmt.Println("Resp", resp) } return nil } ================================================ FILE: datagramsession/metrics.go ================================================ package datagramsession import ( "github.com/prometheus/client_golang/prometheus" ) const ( namespace = "cloudflared" ) var ( activeUDPSessions = prometheus.NewGauge(prometheus.GaugeOpts{ Namespace: namespace, Subsystem: "udp", Name: "active_sessions", Help: "Concurrent count of UDP sessions that are being proxied to any origin", }) totalUDPSessions = prometheus.NewCounter(prometheus.CounterOpts{ Namespace: namespace, Subsystem: "udp", Name: "total_sessions", Help: "Total count of UDP sessions that have been proxied to any origin", }) ) func init() { prometheus.MustRegister( activeUDPSessions, totalUDPSessions, ) } func incrementUDPSessions() { totalUDPSessions.Inc() activeUDPSessions.Inc() } func decrementUDPActiveSessions() { activeUDPSessions.Dec() } ================================================ FILE: datagramsession/session.go ================================================ package datagramsession import ( "context" "errors" "fmt" "io" "net" "time" "github.com/google/uuid" "github.com/rs/zerolog" "github.com/cloudflare/cloudflared/packet" ) const ( defaultCloseIdleAfter = time.Second * 210 ) func SessionIdleErr(timeout time.Duration) error { return fmt.Errorf("session idle for %v", timeout) } type transportSender func(session *packet.Session) error // ErrVithVariableSeverity are errors that have variable severity type ErrVithVariableSeverity interface { error // LogLevel return the severity of this error LogLevel() zerolog.Level } // Session is a bidirectional pipe of datagrams between transport and dstConn // Destination can be a connection with origin or with eyeball // When the destination is origin: // - Manager receives datagrams from receiveChan and calls the transportToDst method of the Session to send to origin // - Datagrams from origin are read from conn and Send to transport using the transportSender callback. Transport will return them to eyeball // When the destination is eyeball: // - Datagrams from eyeball are read from conn and Send to transport. Transport will send them to cloudflared using the transportSender callback. // - Manager receives datagrams from receiveChan and calls the transportToDst method of the Session to send to the eyeball type Session struct { ID uuid.UUID sendFunc transportSender dstConn io.ReadWriteCloser // activeAtChan is used to communicate the last read/write time activeAtChan chan time.Time closeChan chan error log *zerolog.Logger } func (s *Session) Serve(ctx context.Context, closeAfterIdle time.Duration) (closedByRemote bool, err error) { go func() { // QUIC implementation copies data to another buffer before returning https://github.com/quic-go/quic-go/blob/v0.24.0/session.go#L1967-L1975 // This makes it safe to share readBuffer between iterations const maxPacketSize = 1500 readBuffer := make([]byte, maxPacketSize) for { if closeSession, err := s.dstToTransport(readBuffer); err != nil { if errors.Is(err, net.ErrClosed) || errors.Is(err, io.EOF) { s.log.Debug().Msg("Destination connection closed") } else { level := zerolog.ErrorLevel if variableErr, ok := err.(ErrVithVariableSeverity); ok { level = variableErr.LogLevel() } s.log.WithLevel(level).Err(err).Msg("Failed to send session payload from destination to transport") } if closeSession { s.closeChan <- err return } } } }() err = s.waitForCloseCondition(ctx, closeAfterIdle) if closeSession, ok := err.(*errClosedSession); ok { closedByRemote = closeSession.byRemote } return closedByRemote, err } func (s *Session) waitForCloseCondition(ctx context.Context, closeAfterIdle time.Duration) error { // Closing dstConn cancels read so dstToTransport routine in Serve() can return defer s.dstConn.Close() if closeAfterIdle == 0 { // provide default is caller doesn't specify one closeAfterIdle = defaultCloseIdleAfter } checkIdleFreq := closeAfterIdle / 8 checkIdleTicker := time.NewTicker(checkIdleFreq) defer checkIdleTicker.Stop() activeAt := time.Now() for { select { case <-ctx.Done(): return ctx.Err() case reason := <-s.closeChan: return reason // TODO: TUN-5423 evaluate if using atomic is more efficient case now := <-checkIdleTicker.C: // The session is considered inactive if current time is after (last active time + allowed idle time) if now.After(activeAt.Add(closeAfterIdle)) { return SessionIdleErr(closeAfterIdle) } case activeAt = <-s.activeAtChan: // Update last active time } } } func (s *Session) dstToTransport(buffer []byte) (closeSession bool, err error) { n, err := s.dstConn.Read(buffer) s.markActive() // https://pkg.go.dev/io#Reader suggests caller should always process n > 0 bytes if n > 0 || err == nil { session := packet.Session{ ID: s.ID, Payload: buffer[:n], } if sendErr := s.sendFunc(&session); sendErr != nil { return false, sendErr } } return err != nil, err } func (s *Session) transportToDst(payload []byte) (int, error) { s.markActive() n, err := s.dstConn.Write(payload) if err != nil { s.log.Err(err).Msg("Failed to write payload to session") } return n, err } // Sends the last active time to the idle checker loop without blocking. activeAtChan will only be full when there // are many concurrent read/write. It is fine to lose some precision func (s *Session) markActive() { select { case s.activeAtChan <- time.Now(): default: } } func (s *Session) close(err *errClosedSession) { s.closeChan <- err } ================================================ FILE: datagramsession/session_test.go ================================================ package datagramsession import ( "bytes" "context" "fmt" "io" "net" "sync" "testing" "time" "github.com/google/uuid" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/sync/errgroup" "github.com/cloudflare/cloudflared/packet" ) // TestCloseSession makes sure a session will stop after context is done func TestSessionCtxDone(t *testing.T) { testSessionReturns(t, closeByContext, time.Minute*2) } // TestCloseSession makes sure a session will stop after close method is called func TestCloseSession(t *testing.T) { testSessionReturns(t, closeByCallingClose, time.Minute*2) } // TestCloseIdle makess sure a session will stop after there is no read/write for a period defined by closeAfterIdle func TestCloseIdle(t *testing.T) { testSessionReturns(t, closeByTimeout, time.Millisecond*100) } func testSessionReturns(t *testing.T, closeBy closeMethod, closeAfterIdle time.Duration) { localCloseReason := &errClosedSession{ message: "connection closed by origin", byRemote: false, } sessionID := uuid.New() cfdConn, originConn := net.Pipe() payload := testPayload(sessionID) log := zerolog.Nop() mg := NewManager(&log, nil, nil) session := mg.newSession(sessionID, cfdConn) ctx, cancel := context.WithCancel(t.Context()) sessionDone := make(chan struct{}) go func() { closedByRemote, err := session.Serve(ctx, closeAfterIdle) switch closeBy { case closeByContext: assert.Equal(t, context.Canceled, err) assert.False(t, closedByRemote) case closeByCallingClose: assert.Equal(t, localCloseReason, err) assert.Equal(t, localCloseReason.byRemote, closedByRemote) case closeByTimeout: assert.Equal(t, SessionIdleErr(closeAfterIdle), err) assert.False(t, closedByRemote) } close(sessionDone) }() go func() { n, err := session.transportToDst(payload) assert.NoError(t, err) assert.Equal(t, len(payload), n) }() readBuffer := make([]byte, len(payload)+1) n, err := originConn.Read(readBuffer) require.NoError(t, err) require.Equal(t, len(payload), n) lastRead := time.Now() switch closeBy { case closeByContext: cancel() case closeByCallingClose: session.close(localCloseReason) default: // ignore } <-sessionDone if closeBy == closeByTimeout { require.True(t, time.Now().After(lastRead.Add(closeAfterIdle))) } // call cancelled again otherwise the linter will warn about possible context leak cancel() } type closeMethod int const ( closeByContext closeMethod = iota closeByCallingClose closeByTimeout ) func TestWriteToDstSessionPreventClosed(t *testing.T) { testActiveSessionNotClosed(t, false, true) } func TestReadFromDstSessionPreventClosed(t *testing.T) { testActiveSessionNotClosed(t, true, false) } func testActiveSessionNotClosed(t *testing.T, readFromDst bool, writeToDst bool) { const closeAfterIdle = time.Millisecond * 100 const activeTime = time.Millisecond * 500 sessionID := uuid.New() cfdConn, originConn := net.Pipe() payload := testPayload(sessionID) respChan := make(chan *packet.Session) sender := newMockTransportSender(sessionID, payload) mg := NewManager(&nopLogger, sender.muxSession, respChan) session := mg.newSession(sessionID, cfdConn) startTime := time.Now() activeUntil := startTime.Add(activeTime) ctx, cancel := context.WithCancel(t.Context()) errGroup, ctx := errgroup.WithContext(ctx) errGroup.Go(func() error { _, _ = session.Serve(ctx, closeAfterIdle) if time.Now().Before(startTime.Add(activeTime)) { return fmt.Errorf("session closed while it's still active") } return nil }) if readFromDst { errGroup.Go(func() error { for { if time.Now().After(activeUntil) { return nil } if _, err := originConn.Write(payload); err != nil { return err } time.Sleep(closeAfterIdle / 2) } }) } if writeToDst { errGroup.Go(func() error { readBuffer := make([]byte, len(payload)) for { n, err := originConn.Read(readBuffer) if err != nil { if err == io.EOF || err == io.ErrClosedPipe { return nil } return err } if !bytes.Equal(payload, readBuffer[:n]) { return fmt.Errorf("payload %v is not equal to %v", readBuffer[:n], payload) } } }) errGroup.Go(func() error { for { if time.Now().After(activeUntil) { return nil } if _, err := session.transportToDst(payload); err != nil { return err } time.Sleep(closeAfterIdle / 2) } }) } require.NoError(t, errGroup.Wait()) cancel() } func TestMarkActiveNotBlocking(t *testing.T) { const concurrentCalls = 50 mg := NewManager(&nopLogger, nil, nil) session := mg.newSession(uuid.New(), nil) var wg sync.WaitGroup wg.Add(concurrentCalls) for i := 0; i < concurrentCalls; i++ { go func() { session.markActive() wg.Done() }() } wg.Wait() } // Some UDP application might send 0-size payload. func TestZeroBytePayload(t *testing.T) { sessionID := uuid.New() cfdConn, originConn := net.Pipe() sender := sendOnceTransportSender{ baseSender: newMockTransportSender(sessionID, make([]byte, 0)), sentChan: make(chan struct{}), } mg := NewManager(&nopLogger, sender.muxSession, nil) session := mg.newSession(sessionID, cfdConn) ctx, cancel := context.WithCancel(t.Context()) errGroup, ctx := errgroup.WithContext(ctx) errGroup.Go(func() error { // Read from underlying conn and send to transport closedByRemote, err := session.Serve(ctx, time.Minute*2) require.Equal(t, context.Canceled, err) require.False(t, closedByRemote) return nil }) errGroup.Go(func() error { // Write to underlying connection n, err := originConn.Write([]byte{}) require.NoError(t, err) require.Equal(t, 0, n) return nil }) <-sender.sentChan cancel() require.NoError(t, errGroup.Wait()) } type mockTransportSender struct { expectedSessionID uuid.UUID expectedPayload []byte } func newMockTransportSender(expectedSessionID uuid.UUID, expectedPayload []byte) *mockTransportSender { return &mockTransportSender{ expectedSessionID: expectedSessionID, expectedPayload: expectedPayload, } } func (mts *mockTransportSender) muxSession(session *packet.Session) error { if session.ID != mts.expectedSessionID { return fmt.Errorf("Expect session %s, got %s", mts.expectedSessionID, session.ID) } if !bytes.Equal(session.Payload, mts.expectedPayload) { return fmt.Errorf("Expect %v, read %v", mts.expectedPayload, session.Payload) } return nil } type sendOnceTransportSender struct { baseSender *mockTransportSender sentChan chan struct{} } func (sots *sendOnceTransportSender) muxSession(session *packet.Session) error { defer close(sots.sentChan) return sots.baseSender.muxSession(session) } ================================================ FILE: diagnostic/client.go ================================================ package diagnostic import ( "context" "encoding/json" "fmt" "io" "net/http" "net/url" "strconv" cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags" ) type httpClient struct { http.Client baseURL *url.URL } func NewHTTPClient() *httpClient { httpTransport := http.Transport{ TLSHandshakeTimeout: defaultTimeout, ResponseHeaderTimeout: defaultTimeout, } return &httpClient{ http.Client{ Transport: &httpTransport, Timeout: defaultTimeout, }, nil, } } func (client *httpClient) SetBaseURL(baseURL *url.URL) { client.baseURL = baseURL } func (client *httpClient) GET(ctx context.Context, endpoint string) (*http.Response, error) { if client.baseURL == nil { return nil, ErrNoBaseURL } url := client.baseURL.JoinPath(endpoint) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url.String(), nil) if err != nil { return nil, fmt.Errorf("error creating GET request: %w", err) } req.Header.Add("Accept", "application/json;version=1") response, err := client.Do(req) if err != nil { return nil, fmt.Errorf("error GET request: %w", err) } return response, nil } type LogConfiguration struct { logFile string logDirectory string uid int // the uid of the user that started cloudflared } func (client *httpClient) GetLogConfiguration(ctx context.Context) (*LogConfiguration, error) { response, err := client.GET(ctx, cliConfigurationEndpoint) if err != nil { return nil, err } defer response.Body.Close() var data map[string]string if err := json.NewDecoder(response.Body).Decode(&data); err != nil { return nil, fmt.Errorf("failed to decode body: %w", err) } uidStr, exists := data[configurationKeyUID] if !exists { return nil, ErrKeyNotFound } uid, err := strconv.Atoi(uidStr) if err != nil { return nil, fmt.Errorf("error convertin pid to int: %w", err) } logFile, exists := data[cfdflags.LogFile] if exists { return &LogConfiguration{logFile, "", uid}, nil } logDirectory, exists := data[cfdflags.LogDirectory] if exists { return &LogConfiguration{"", logDirectory, uid}, nil } // No log configured may happen when cloudflared is executed as a managed service or // when containerized return &LogConfiguration{"", "", uid}, nil } func (client *httpClient) GetMemoryDump(ctx context.Context, writer io.Writer) error { response, err := client.GET(ctx, memoryDumpEndpoint) if err != nil { return err } return copyToWriter(response, writer) } func (client *httpClient) GetGoroutineDump(ctx context.Context, writer io.Writer) error { response, err := client.GET(ctx, goroutineDumpEndpoint) if err != nil { return err } return copyToWriter(response, writer) } func (client *httpClient) GetTunnelState(ctx context.Context) (*TunnelState, error) { response, err := client.GET(ctx, tunnelStateEndpoint) if err != nil { return nil, err } defer response.Body.Close() var state TunnelState if err := json.NewDecoder(response.Body).Decode(&state); err != nil { return nil, fmt.Errorf("failed to decode body: %w", err) } return &state, nil } func (client *httpClient) GetSystemInformation(ctx context.Context, writer io.Writer) error { response, err := client.GET(ctx, systemInformationEndpoint) if err != nil { return err } return copyJSONToWriter(response, writer) } func (client *httpClient) GetMetrics(ctx context.Context, writer io.Writer) error { response, err := client.GET(ctx, metricsEndpoint) if err != nil { return err } return copyToWriter(response, writer) } func (client *httpClient) GetTunnelConfiguration(ctx context.Context, writer io.Writer) error { response, err := client.GET(ctx, tunnelConfigurationEndpoint) if err != nil { return err } return copyJSONToWriter(response, writer) } func (client *httpClient) GetCliConfiguration(ctx context.Context, writer io.Writer) error { response, err := client.GET(ctx, cliConfigurationEndpoint) if err != nil { return err } return copyJSONToWriter(response, writer) } func copyToWriter(response *http.Response, writer io.Writer) error { defer response.Body.Close() _, err := io.Copy(writer, response.Body) if err != nil { return fmt.Errorf("error writing response: %w", err) } return nil } func copyJSONToWriter(response *http.Response, writer io.Writer) error { defer response.Body.Close() var data interface{} decoder := json.NewDecoder(response.Body) err := decoder.Decode(&data) if err != nil { return fmt.Errorf("diagnostic client error whilst reading response: %w", err) } encoder := newFormattedEncoder(writer) err = encoder.Encode(data) if err != nil { return fmt.Errorf("diagnostic client error whilst writing json: %w", err) } return nil } type HTTPClient interface { GetLogConfiguration(ctx context.Context) (*LogConfiguration, error) GetMemoryDump(ctx context.Context, writer io.Writer) error GetGoroutineDump(ctx context.Context, writer io.Writer) error GetTunnelState(ctx context.Context) (*TunnelState, error) GetSystemInformation(ctx context.Context, writer io.Writer) error GetMetrics(ctx context.Context, writer io.Writer) error GetCliConfiguration(ctx context.Context, writer io.Writer) error GetTunnelConfiguration(ctx context.Context, writer io.Writer) error } ================================================ FILE: diagnostic/consts.go ================================================ package diagnostic import "time" const ( defaultCollectorTimeout = time.Second * 10 // This const define the timeout value of a collector operation. collectorField = "collector" // used for logging purposes systemCollectorName = "system" // used for logging purposes tunnelStateCollectorName = "tunnelState" // used for logging purposes configurationCollectorName = "configuration" // used for logging purposes defaultTimeout = 15 * time.Second // timeout for the collectors twoWeeksOffset = -14 * 24 * time.Hour // maximum offset for the logs logFilename = "cloudflared_logs.txt" // name of the output log file configurationKeyUID = "uid" // Key used to set and get the UID value from the configuration map tailMaxNumberOfLines = "10000" // maximum number of log lines from a virtual runtime (docker or kubernetes) // Endpoints used by the diagnostic HTTP Client. cliConfigurationEndpoint = "/diag/configuration" tunnelStateEndpoint = "/diag/tunnel" systemInformationEndpoint = "/diag/system" memoryDumpEndpoint = "debug/pprof/heap" goroutineDumpEndpoint = "debug/pprof/goroutine" metricsEndpoint = "metrics" tunnelConfigurationEndpoint = "/config" // Base for filenames of the diagnostic procedure systemInformationBaseName = "systeminformation.json" metricsBaseName = "metrics.txt" zipName = "cloudflared-diag" heapPprofBaseName = "heap.pprof" goroutinePprofBaseName = "goroutine.pprof" networkBaseName = "network.json" rawNetworkBaseName = "raw-network.txt" tunnelStateBaseName = "tunnelstate.json" cliConfigurationBaseName = "cli-configuration.json" configurationBaseName = "configuration.json" taskResultBaseName = "task-result.json" ) ================================================ FILE: diagnostic/diagnostic.go ================================================ package diagnostic import ( "context" "encoding/json" "errors" "fmt" "io" "net/url" "os" "path/filepath" "strings" "sync" "time" "github.com/rs/zerolog" network "github.com/cloudflare/cloudflared/diagnostic/network" ) const ( taskSuccess = "success" taskFailure = "failure" jobReportName = "job report" tunnelStateJobName = "tunnel state" systemInformationJobName = "system information" goroutineJobName = "goroutine profile" heapJobName = "heap profile" metricsJobName = "metrics" logInformationJobName = "log information" rawNetworkInformationJobName = "raw network information" networkInformationJobName = "network information" cliConfigurationJobName = "cli configuration" configurationJobName = "configuration" ) // Struct used to hold the results of different routines executing the network collection. type taskResult struct { Result string `json:"result,omitempty"` Err error `json:"error,omitempty"` path string } func (result taskResult) MarshalJSON() ([]byte, error) { s := map[string]string{ "result": result.Result, } if result.Err != nil { s["error"] = result.Err.Error() } return json.Marshal(s) } // Struct used to hold the results of different routines executing the network collection. type networkCollectionResult struct { name string info []*network.Hop raw string err error } // This type represents the most common functions from the diagnostic http client // functions. type collectToWriterFunc func(ctx context.Context, writer io.Writer) error // This type represents the common denominator among all the collection procedures. type collectFunc func(ctx context.Context) (string, error) // collectJob is an internal struct that denotes holds the information necessary // to run a collection job. type collectJob struct { jobName string fn collectFunc bypass bool } // The Toggles structure denotes the available toggles for the diagnostic procedure. // Each toggle enables/disables tasks from the diagnostic. type Toggles struct { NoDiagLogs bool NoDiagMetrics bool NoDiagSystem bool NoDiagRuntime bool NoDiagNetwork bool } // The Options structure holds every option necessary for // the diagnostic procedure to work. type Options struct { KnownAddresses []string Address string ContainerID string PodID string Toggles Toggles } func collectLogs( ctx context.Context, client HTTPClient, diagContainer, diagPod string, ) (string, error) { var collector LogCollector if diagPod != "" { collector = NewKubernetesLogCollector(diagContainer, diagPod) } else if diagContainer != "" { collector = NewDockerLogCollector(diagContainer) } else { collector = NewHostLogCollector(client) } logInformation, err := collector.Collect(ctx) if err != nil { return "", fmt.Errorf("error collecting logs: %w", err) } if logInformation.isDirectory { return CopyFilesFromDirectory(logInformation.path) } if logInformation.wasCreated { return logInformation.path, nil } logHandle, err := os.Open(logInformation.path) if err != nil { return "", fmt.Errorf("error opening log file while collecting logs: %w", err) } defer logHandle.Close() outputLogHandle, err := os.Create(filepath.Join(os.TempDir(), logFilename)) if err != nil { return "", ErrCreatingTemporaryFile } defer outputLogHandle.Close() _, err = io.Copy(outputLogHandle, logHandle) if err != nil { return "", fmt.Errorf("error copying logs while collecting logs: %w", err) } return outputLogHandle.Name(), err } func collectNetworkResultRoutine( ctx context.Context, collector network.NetworkCollector, hostname string, useIPv4 bool, results chan networkCollectionResult, ) { const ( hopsNo = 5 timeout = time.Second * 5 ) name := hostname if useIPv4 { name += "-v4" } else { name += "-v6" } hops, raw, err := collector.Collect(ctx, network.NewTraceOptions(hopsNo, timeout, hostname, useIPv4)) results <- networkCollectionResult{name, hops, raw, err} } func gatherNetworkInformation(ctx context.Context) map[string]networkCollectionResult { networkCollector := network.NetworkCollectorImpl{} hostAndIPversionPairs := []struct { host string useV4 bool }{ {"region1.v2.argotunnel.com", true}, {"region1.v2.argotunnel.com", false}, {"region2.v2.argotunnel.com", true}, {"region2.v2.argotunnel.com", false}, } // the number of results is known thus use len to avoid footguns results := make(chan networkCollectionResult, len(hostAndIPversionPairs)) var wgroup sync.WaitGroup for _, item := range hostAndIPversionPairs { wgroup.Add(1) go func() { defer wgroup.Done() collectNetworkResultRoutine(ctx, &networkCollector, item.host, item.useV4, results) }() } // Wait for routines to end. wgroup.Wait() resultMap := make(map[string]networkCollectionResult) for range len(hostAndIPversionPairs) { result := <-results resultMap[result.name] = result } return resultMap } func networkInformationCollectors() (rawNetworkCollector, jsonNetworkCollector collectFunc) { // The network collector is an operation that takes most of the diagnostic time, thus, // the sync.Once is used to memoize the result of the collector and then create different // outputs. var once sync.Once var resultMap map[string]networkCollectionResult rawNetworkCollector = func(ctx context.Context) (string, error) { once.Do(func() { resultMap = gatherNetworkInformation(ctx) }) return rawNetworkInformationWriter(resultMap) } jsonNetworkCollector = func(ctx context.Context) (string, error) { once.Do(func() { resultMap = gatherNetworkInformation(ctx) }) return jsonNetworkInformationWriter(resultMap) } return rawNetworkCollector, jsonNetworkCollector } func rawNetworkInformationWriter(resultMap map[string]networkCollectionResult) (string, error) { networkDumpHandle, err := os.Create(filepath.Join(os.TempDir(), rawNetworkBaseName)) if err != nil { return "", ErrCreatingTemporaryFile } defer networkDumpHandle.Close() var exitErr error for k, v := range resultMap { if v.err != nil { if exitErr == nil { exitErr = v.err } _, err := networkDumpHandle.WriteString(k + "\nno content\n") if err != nil { return networkDumpHandle.Name(), fmt.Errorf("error writing 'no content' to raw network file: %w", err) } } else { _, err := networkDumpHandle.WriteString(k + "\n" + v.raw + "\n") if err != nil { return networkDumpHandle.Name(), fmt.Errorf("error writing raw network information: %w", err) } } } return networkDumpHandle.Name(), exitErr } func jsonNetworkInformationWriter(resultMap map[string]networkCollectionResult) (string, error) { networkDumpHandle, err := os.Create(filepath.Join(os.TempDir(), networkBaseName)) if err != nil { return "", ErrCreatingTemporaryFile } defer networkDumpHandle.Close() encoder := newFormattedEncoder(networkDumpHandle) var exitErr error jsonMap := make(map[string][]*network.Hop, len(resultMap)) for k, v := range resultMap { jsonMap[k] = v.info if exitErr == nil && v.err != nil { exitErr = v.err } } err = encoder.Encode(jsonMap) if err != nil { return networkDumpHandle.Name(), fmt.Errorf("error encoding network information results: %w", err) } return networkDumpHandle.Name(), exitErr } func collectFromEndpointAdapter(collect collectToWriterFunc, fileName string) collectFunc { return func(ctx context.Context) (string, error) { dumpHandle, err := os.Create(filepath.Join(os.TempDir(), fileName)) if err != nil { return "", ErrCreatingTemporaryFile } defer dumpHandle.Close() err = collect(ctx, dumpHandle) if err != nil { return dumpHandle.Name(), fmt.Errorf("error running collector: %w", err) } return dumpHandle.Name(), nil } } func tunnelStateCollectEndpointAdapter(client HTTPClient, tunnel *TunnelState, fileName string) collectFunc { endpointFunc := func(ctx context.Context, writer io.Writer) error { if tunnel == nil { // When the metrics server is not passed the diagnostic will query all known hosts // and get the tunnel state, however, when the metrics server is passed that won't // happen hence the check for nil in this function. tunnelResponse, err := client.GetTunnelState(ctx) if err != nil { return fmt.Errorf("error retrieving tunnel state: %w", err) } tunnel = tunnelResponse } encoder := newFormattedEncoder(writer) err := encoder.Encode(tunnel) if err != nil { return fmt.Errorf("error encoding tunnel state: %w", err) } return nil } return collectFromEndpointAdapter(endpointFunc, fileName) } // resolveInstanceBaseURL is responsible to // resolve the base URL of the instance that should be diagnosed. // To resolve the instance it may be necessary to query the // /diag/tunnel endpoint of the known instances, thus, if a single // instance is found its state is also returned; if multiple instances // are found then their states are returned in an array along with an // error. func resolveInstanceBaseURL( metricsServerAddress string, log *zerolog.Logger, client *httpClient, addresses []string, ) (*url.URL, *TunnelState, []*AddressableTunnelState, error) { if metricsServerAddress != "" { if !strings.HasPrefix(metricsServerAddress, "http://") { metricsServerAddress = "http://" + metricsServerAddress } url, err := url.Parse(metricsServerAddress) if err != nil { return nil, nil, nil, fmt.Errorf("provided address is not valid: %w", err) } return url, nil, nil, nil } tunnelState, foundTunnelStates, err := FindMetricsServer(log, client, addresses) if err != nil { return nil, nil, foundTunnelStates, err } return tunnelState.URL, tunnelState.TunnelState, nil, nil } func createJobs( client *httpClient, tunnel *TunnelState, diagContainer string, diagPod string, noDiagSystem bool, noDiagRuntime bool, noDiagMetrics bool, noDiagLogs bool, noDiagNetwork bool, ) []collectJob { rawNetworkCollectorFunc, jsonNetworkCollectorFunc := networkInformationCollectors() jobs := []collectJob{ { jobName: tunnelStateJobName, fn: tunnelStateCollectEndpointAdapter(client, tunnel, tunnelStateBaseName), bypass: false, }, { jobName: systemInformationJobName, fn: collectFromEndpointAdapter(client.GetSystemInformation, systemInformationBaseName), bypass: noDiagSystem, }, { jobName: goroutineJobName, fn: collectFromEndpointAdapter(client.GetGoroutineDump, goroutinePprofBaseName), bypass: noDiagRuntime, }, { jobName: heapJobName, fn: collectFromEndpointAdapter(client.GetMemoryDump, heapPprofBaseName), bypass: noDiagRuntime, }, { jobName: metricsJobName, fn: collectFromEndpointAdapter(client.GetMetrics, metricsBaseName), bypass: noDiagMetrics, }, { jobName: logInformationJobName, fn: func(ctx context.Context) (string, error) { return collectLogs(ctx, client, diagContainer, diagPod) }, bypass: noDiagLogs, }, { jobName: rawNetworkInformationJobName, fn: rawNetworkCollectorFunc, bypass: noDiagNetwork, }, { jobName: networkInformationJobName, fn: jsonNetworkCollectorFunc, bypass: noDiagNetwork, }, { jobName: cliConfigurationJobName, fn: collectFromEndpointAdapter(client.GetCliConfiguration, cliConfigurationBaseName), bypass: false, }, { jobName: configurationJobName, fn: collectFromEndpointAdapter(client.GetTunnelConfiguration, configurationBaseName), bypass: false, }, } return jobs } func createTaskReport(taskReport map[string]taskResult) (string, error) { dumpHandle, err := os.Create(filepath.Join(os.TempDir(), taskResultBaseName)) if err != nil { return "", ErrCreatingTemporaryFile } defer dumpHandle.Close() encoder := newFormattedEncoder(dumpHandle) err = encoder.Encode(taskReport) if err != nil { return "", fmt.Errorf("error encoding task results: %w", err) } return dumpHandle.Name(), nil } func runJobs(ctx context.Context, jobs []collectJob, log *zerolog.Logger) map[string]taskResult { jobReport := make(map[string]taskResult, len(jobs)) for _, job := range jobs { if job.bypass { continue } log.Info().Msgf("Collecting %s...", job.jobName) path, err := job.fn(ctx) var result taskResult if err != nil { result = taskResult{Result: taskFailure, Err: err, path: path} log.Error().Err(err).Msgf("Job: %s finished with error.", job.jobName) } else { result = taskResult{Result: taskSuccess, Err: nil, path: path} log.Info().Msgf("Collected %s.", job.jobName) } jobReport[job.jobName] = result } taskReportName, err := createTaskReport(jobReport) var result taskResult if err != nil { result = taskResult{ Result: taskFailure, path: taskReportName, Err: err, } } else { result = taskResult{ Result: taskSuccess, path: taskReportName, Err: nil, } } jobReport[jobReportName] = result return jobReport } func RunDiagnostic( log *zerolog.Logger, options Options, ) ([]*AddressableTunnelState, error) { client := NewHTTPClient() baseURL, tunnel, foundTunnels, err := resolveInstanceBaseURL(options.Address, log, client, options.KnownAddresses) if err != nil { return foundTunnels, err } log.Info().Msgf("Selected server %s starting diagnostic...", baseURL.String()) client.SetBaseURL(baseURL) const timeout = 45 * time.Second ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() jobs := createJobs( client, tunnel, options.ContainerID, options.PodID, options.Toggles.NoDiagSystem, options.Toggles.NoDiagRuntime, options.Toggles.NoDiagMetrics, options.Toggles.NoDiagLogs, options.Toggles.NoDiagNetwork, ) jobsReport := runJobs(ctx, jobs, log) paths := make([]string, 0) var gerr error for _, v := range jobsReport { paths = append(paths, v.path) if gerr == nil && v.Err != nil { gerr = v.Err } defer func() { if !errors.Is(v.Err, ErrCreatingTemporaryFile) { os.Remove(v.path) } }() } zipfile, err := CreateDiagnosticZipFile(zipName, paths) if err != nil { return nil, err } log.Info().Msgf("Diagnostic file written: %v", zipfile) return nil, gerr } ================================================ FILE: diagnostic/diagnostic_utils.go ================================================ package diagnostic import ( "archive/zip" "context" "encoding/json" "fmt" "io" "net/url" "os" "path/filepath" "strings" "time" "github.com/google/uuid" "github.com/rs/zerolog" ) // CreateDiagnosticZipFile create a zip file with the contents from the all // files paths. The files will be written in the root of the zip file. // In case of an error occurs after whilst writing to the zip file // this will be removed. func CreateDiagnosticZipFile(base string, paths []string) (zipFileName string, err error) { // Create a zip file with all files from paths added to the root suffix := time.Now().Format(time.RFC3339) zipFileName = base + "-" + suffix + ".zip" zipFileName = strings.ReplaceAll(zipFileName, ":", "-") archive, cerr := os.Create(zipFileName) if cerr != nil { return "", fmt.Errorf("error creating file %s: %w", zipFileName, cerr) } archiveWriter := zip.NewWriter(archive) defer func() { archiveWriter.Close() archive.Close() if err != nil { os.Remove(zipFileName) } }() for _, file := range paths { if file == "" { continue } var handle *os.File handle, err = os.Open(file) if err != nil { return "", fmt.Errorf("error opening file %s: %w", zipFileName, err) } defer handle.Close() // Keep the base only to not create sub directories in the // zip file. var writer io.Writer writer, err = archiveWriter.Create(filepath.Base(file)) if err != nil { return "", fmt.Errorf("error creating archive writer from %s: %w", file, err) } if _, err = io.Copy(writer, handle); err != nil { return "", fmt.Errorf("error copying file %s: %w", file, err) } } zipFileName = archive.Name() return zipFileName, nil } type AddressableTunnelState struct { *TunnelState URL *url.URL } func findMetricsServerPredicate(tunnelID, connectorID uuid.UUID) func(state *TunnelState) bool { if tunnelID != uuid.Nil && connectorID != uuid.Nil { return func(state *TunnelState) bool { return state.ConnectorID == connectorID && state.TunnelID == tunnelID } } else if tunnelID == uuid.Nil && connectorID != uuid.Nil { return func(state *TunnelState) bool { return state.ConnectorID == connectorID } } else if tunnelID != uuid.Nil && connectorID == uuid.Nil { return func(state *TunnelState) bool { return state.TunnelID == tunnelID } } return func(*TunnelState) bool { return true } } // The FindMetricsServer will try to find the metrics server url. // There are two possible error scenarios: // 1. No instance is found which will only return ErrMetricsServerNotFound // 2. Multiple instances are found which will return an array of state and ErrMultipleMetricsServerFound // In case of success, only the state for the instance is returned. func FindMetricsServer( log *zerolog.Logger, client *httpClient, addresses []string, ) (*AddressableTunnelState, []*AddressableTunnelState, error) { instances := make([]*AddressableTunnelState, 0) for _, address := range addresses { url, err := url.Parse("http://" + address) if err != nil { log.Debug().Err(err).Msgf("error parsing address %s", address) continue } client.SetBaseURL(url) state, err := client.GetTunnelState(context.Background()) if err == nil { instances = append(instances, &AddressableTunnelState{state, url}) } else { log.Debug().Err(err).Msgf("error getting tunnel state from address %s", address) } } if len(instances) == 0 { return nil, nil, ErrMetricsServerNotFound } if len(instances) == 1 { return instances[0], nil, nil } return nil, instances, ErrMultipleMetricsServerFound } // newFormattedEncoder return a JSON encoder with identation func newFormattedEncoder(w io.Writer) *json.Encoder { encoder := json.NewEncoder(w) encoder.SetIndent("", " ") return encoder } ================================================ FILE: diagnostic/diagnostic_utils_test.go ================================================ package diagnostic_test import ( "context" "net/http" "net/url" "sync" "testing" "time" "github.com/facebookgo/grace/gracenet" "github.com/google/uuid" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/cloudflare/cloudflared/diagnostic" "github.com/cloudflare/cloudflared/metrics" "github.com/cloudflare/cloudflared/tunnelstate" ) func helperCreateServer(t *testing.T, listeners *gracenet.Net, tunnelID uuid.UUID, connectorID uuid.UUID) func() { t.Helper() listener, err := metrics.CreateMetricsListener(listeners, "localhost:0") require.NoError(t, err) log := zerolog.Nop() tracker := tunnelstate.NewConnTracker(&log) handler := diagnostic.NewDiagnosticHandler(&log, 0, nil, tunnelID, connectorID, tracker, map[string]string{}, []string{}) router := http.NewServeMux() router.HandleFunc("/diag/tunnel", handler.TunnelStateHandler) server := &http.Server{ ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, Handler: router, } var wgroup sync.WaitGroup wgroup.Add(1) go func() { defer wgroup.Done() _ = server.Serve(listener) }() ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) cleanUp := func() { _ = server.Shutdown(ctx) cancel() wgroup.Wait() } return cleanUp } func TestFindMetricsServer_WhenSingleServerIsRunning_ReturnState(t *testing.T) { listeners := gracenet.Net{} tid1 := uuid.New() cid1 := uuid.New() cleanUp := helperCreateServer(t, &listeners, tid1, cid1) defer cleanUp() log := zerolog.Nop() client := diagnostic.NewHTTPClient() addresses := metrics.GetMetricsKnownAddresses("host") url1, err := url.Parse("http://localhost:20241") require.NoError(t, err) tunnel1 := &diagnostic.AddressableTunnelState{ TunnelState: &diagnostic.TunnelState{ TunnelID: tid1, ConnectorID: cid1, Connections: nil, }, URL: url1, } state, tunnels, err := diagnostic.FindMetricsServer(&log, client, addresses[:]) if err != nil { require.ErrorIs(t, err, diagnostic.ErrMultipleMetricsServerFound) } assert.Equal(t, tunnel1, state) assert.Nil(t, tunnels) } func TestFindMetricsServer_WhenMultipleServerAreRunning_ReturnError(t *testing.T) { listeners := gracenet.Net{} tid1 := uuid.New() cid1 := uuid.New() cid2 := uuid.New() cleanUp := helperCreateServer(t, &listeners, tid1, cid1) defer cleanUp() cleanUp = helperCreateServer(t, &listeners, tid1, cid2) defer cleanUp() log := zerolog.Nop() client := diagnostic.NewHTTPClient() addresses := metrics.GetMetricsKnownAddresses("host") url1, err := url.Parse("http://localhost:20241") require.NoError(t, err) url2, err := url.Parse("http://localhost:20242") require.NoError(t, err) tunnel1 := &diagnostic.AddressableTunnelState{ TunnelState: &diagnostic.TunnelState{ TunnelID: tid1, ConnectorID: cid1, Connections: nil, }, URL: url1, } tunnel2 := &diagnostic.AddressableTunnelState{ TunnelState: &diagnostic.TunnelState{ TunnelID: tid1, ConnectorID: cid2, Connections: nil, }, URL: url2, } state, tunnels, err := diagnostic.FindMetricsServer(&log, client, addresses[:]) if err != nil { require.ErrorIs(t, err, diagnostic.ErrMultipleMetricsServerFound) } assert.Nil(t, state) assert.Equal(t, []*diagnostic.AddressableTunnelState{tunnel1, tunnel2}, tunnels) } func TestFindMetricsServer_WhenNoInstanceIsRuning_ReturnError(t *testing.T) { log := zerolog.Nop() client := diagnostic.NewHTTPClient() addresses := metrics.GetMetricsKnownAddresses("host") state, tunnels, err := diagnostic.FindMetricsServer(&log, client, addresses[:]) require.ErrorIs(t, err, diagnostic.ErrMetricsServerNotFound) assert.Nil(t, state) assert.Nil(t, tunnels) } ================================================ FILE: diagnostic/error.go ================================================ package diagnostic import ( "errors" ) var ( // Error used when there is no log directory available. ErrManagedLogNotFound = errors.New("managed log directory not found") // Error used when it is not possible to collect logs using the log configuration. ErrLogConfigurationIsInvalid = errors.New("provided log configuration is invalid") // Error used when parsing the fields of the output of collector. ErrInsufficientLines = errors.New("insufficient lines") // Error used when parsing the lines of the output of collector. ErrInsuficientFields = errors.New("insufficient fields") // Error used when given key is not found while parsing KV. ErrKeyNotFound = errors.New("key not found") // Error used when there is no disk volume information available. ErrNoVolumeFound = errors.New("no disk volume information found") // Error user when the base url of the diagnostic client is not provided. ErrNoBaseURL = errors.New("no base url") // Error used when no metrics server is found listening to the known addresses list (check [metrics.GetMetricsKnownAddresses]). ErrMetricsServerNotFound = errors.New("metrics server not found") // Error used when multiple metrics server are found listening to the known addresses list (check [metrics.GetMetricsKnownAddresses]). ErrMultipleMetricsServerFound = errors.New("multiple metrics server found") // Error used when a temporary file creation fails within the diagnostic procedure ErrCreatingTemporaryFile = errors.New("temporary file creation failed") ) ================================================ FILE: diagnostic/handlers.go ================================================ package diagnostic import ( "context" "encoding/json" "net/http" "os" "strconv" "time" "github.com/google/uuid" "github.com/rs/zerolog" "github.com/cloudflare/cloudflared/tunnelstate" ) type Handler struct { log *zerolog.Logger timeout time.Duration systemCollector SystemCollector tunnelID uuid.UUID connectorID uuid.UUID tracker *tunnelstate.ConnTracker cliFlags map[string]string icmpSources []string } func NewDiagnosticHandler( log *zerolog.Logger, timeout time.Duration, systemCollector SystemCollector, tunnelID uuid.UUID, connectorID uuid.UUID, tracker *tunnelstate.ConnTracker, cliFlags map[string]string, icmpSources []string, ) *Handler { logger := log.With().Logger() if timeout == 0 { timeout = defaultCollectorTimeout } cliFlags[configurationKeyUID] = strconv.Itoa(os.Getuid()) return &Handler{ log: &logger, timeout: timeout, systemCollector: systemCollector, tunnelID: tunnelID, connectorID: connectorID, tracker: tracker, cliFlags: cliFlags, icmpSources: icmpSources, } } func (handler *Handler) InstallEndpoints(router *http.ServeMux) { router.HandleFunc(cliConfigurationEndpoint, handler.ConfigurationHandler) router.HandleFunc(tunnelStateEndpoint, handler.TunnelStateHandler) router.HandleFunc(systemInformationEndpoint, handler.SystemHandler) } type SystemInformationResponse struct { Info *SystemInformation `json:"info"` Err error `json:"errors"` } func (handler *Handler) SystemHandler(writer http.ResponseWriter, request *http.Request) { logger := handler.log.With().Str(collectorField, systemCollectorName).Logger() logger.Info().Msg("Collection started") defer logger.Info().Msg("Collection finished") ctx, cancel := context.WithTimeout(request.Context(), handler.timeout) defer cancel() info, err := handler.systemCollector.Collect(ctx) response := SystemInformationResponse{ Info: info, Err: err, } encoder := json.NewEncoder(writer) err = encoder.Encode(response) if err != nil { logger.Error().Err(err).Msgf("error occurred whilst serializing information") writer.WriteHeader(http.StatusInternalServerError) } } type TunnelState struct { TunnelID uuid.UUID `json:"tunnelID,omitempty"` ConnectorID uuid.UUID `json:"connectorID,omitempty"` Connections []tunnelstate.IndexedConnectionInfo `json:"connections,omitempty"` ICMPSources []string `json:"icmp_sources,omitempty"` } func (handler *Handler) TunnelStateHandler(writer http.ResponseWriter, _ *http.Request) { log := handler.log.With().Str(collectorField, tunnelStateCollectorName).Logger() log.Info().Msg("Collection started") defer log.Info().Msg("Collection finished") body := TunnelState{ handler.tunnelID, handler.connectorID, handler.tracker.GetActiveConnections(), handler.icmpSources, } encoder := json.NewEncoder(writer) err := encoder.Encode(body) if err != nil { handler.log.Error().Err(err).Msgf("error occurred whilst serializing information") writer.WriteHeader(http.StatusInternalServerError) } } func (handler *Handler) ConfigurationHandler(writer http.ResponseWriter, _ *http.Request) { log := handler.log.With().Str(collectorField, configurationCollectorName).Logger() log.Info().Msg("Collection started") defer func() { log.Info().Msg("Collection finished") }() encoder := json.NewEncoder(writer) err := encoder.Encode(handler.cliFlags) if err != nil { handler.log.Error().Err(err).Msgf("error occurred whilst serializing response") writer.WriteHeader(http.StatusInternalServerError) } } func writeResponse(w http.ResponseWriter, bytes []byte, logger *zerolog.Logger) { bytesWritten, err := w.Write(bytes) if err != nil { logger.Error().Err(err).Msg("error occurred writing response") } else if bytesWritten != len(bytes) { logger.Error().Msgf("error incomplete write response %d/%d", bytesWritten, len(bytes)) } } ================================================ FILE: diagnostic/handlers_test.go ================================================ package diagnostic_test import ( "context" "encoding/json" "errors" "net" "net/http" "net/http/httptest" "runtime" "testing" "github.com/google/uuid" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/cloudflare/cloudflared/connection" "github.com/cloudflare/cloudflared/diagnostic" "github.com/cloudflare/cloudflared/tunnelstate" ) type SystemCollectorMock struct { systemInfo *diagnostic.SystemInformation err error } const ( systemInformationKey = "sikey" errorKey = "errkey" ) func newTrackerFromConns(t *testing.T, connections []tunnelstate.IndexedConnectionInfo) *tunnelstate.ConnTracker { t.Helper() log := zerolog.Nop() tracker := tunnelstate.NewConnTracker(&log) for _, conn := range connections { tracker.OnTunnelEvent(connection.Event{ Index: conn.Index, EventType: connection.Connected, Protocol: conn.Protocol, EdgeAddress: conn.EdgeAddress, }) } return tracker } func (collector *SystemCollectorMock) Collect(context.Context) (*diagnostic.SystemInformation, error) { return collector.systemInfo, collector.err } func TestSystemHandler(t *testing.T) { t.Parallel() log := zerolog.Nop() tests := []struct { name string systemInfo *diagnostic.SystemInformation err error statusCode int }{ { name: "happy path", systemInfo: diagnostic.NewSystemInformation( 0, 0, 0, 0, "string", "string", "string", "string", "string", "string", runtime.Version(), runtime.GOARCH, nil, ), err: nil, statusCode: http.StatusOK, }, { name: "on error and no raw info", systemInfo: nil, err: errors.New("an error"), statusCode: http.StatusOK, }, } for _, tCase := range tests { t.Run(tCase.name, func(t *testing.T) { t.Parallel() handler := diagnostic.NewDiagnosticHandler(&log, 0, &SystemCollectorMock{ systemInfo: tCase.systemInfo, err: tCase.err, }, uuid.New(), uuid.New(), nil, map[string]string{}, nil) recorder := httptest.NewRecorder() ctx := context.Background() request, err := http.NewRequestWithContext(ctx, http.MethodGet, "/diag/system", nil) require.NoError(t, err) handler.SystemHandler(recorder, request) assert.Equal(t, tCase.statusCode, recorder.Code) if tCase.statusCode == http.StatusOK && tCase.systemInfo != nil { var response diagnostic.SystemInformationResponse decoder := json.NewDecoder(recorder.Body) err := decoder.Decode(&response) require.NoError(t, err) assert.Equal(t, tCase.systemInfo, response.Info) } }) } } func TestTunnelStateHandler(t *testing.T) { t.Parallel() log := zerolog.Nop() tests := []struct { name string tunnelID uuid.UUID clientID uuid.UUID connections []tunnelstate.IndexedConnectionInfo icmpSources []string }{ { name: "case1", tunnelID: uuid.New(), clientID: uuid.New(), }, { name: "case2", tunnelID: uuid.New(), clientID: uuid.New(), icmpSources: []string{"172.17.0.3", "::1"}, connections: []tunnelstate.IndexedConnectionInfo{{ ConnectionInfo: tunnelstate.ConnectionInfo{ IsConnected: true, Protocol: connection.QUIC, EdgeAddress: net.IPv4(100, 100, 100, 100), }, Index: 0, }}, }, } for _, tCase := range tests { t.Run(tCase.name, func(t *testing.T) { t.Parallel() tracker := newTrackerFromConns(t, tCase.connections) handler := diagnostic.NewDiagnosticHandler( &log, 0, nil, tCase.tunnelID, tCase.clientID, tracker, map[string]string{}, tCase.icmpSources, ) recorder := httptest.NewRecorder() handler.TunnelStateHandler(recorder, nil) decoder := json.NewDecoder(recorder.Body) var response diagnostic.TunnelState err := decoder.Decode(&response) require.NoError(t, err) assert.Equal(t, http.StatusOK, recorder.Code) assert.Equal(t, tCase.tunnelID, response.TunnelID) assert.Equal(t, tCase.clientID, response.ConnectorID) assert.Equal(t, tCase.connections, response.Connections) assert.Equal(t, tCase.icmpSources, response.ICMPSources) }) } } func TestConfigurationHandler(t *testing.T) { t.Parallel() log := zerolog.Nop() tests := []struct { name string flags map[string]string expected map[string]string }{ { name: "empty cli", flags: make(map[string]string), expected: map[string]string{ "uid": "0", }, }, { name: "cli with flags", flags: map[string]string{ "b": "a", "c": "a", "d": "a", "uid": "0", }, expected: map[string]string{ "b": "a", "c": "a", "d": "a", "uid": "0", }, }, } for _, tCase := range tests { t.Run(tCase.name, func(t *testing.T) { t.Parallel() var response map[string]string handler := diagnostic.NewDiagnosticHandler(&log, 0, nil, uuid.New(), uuid.New(), nil, tCase.flags, nil) recorder := httptest.NewRecorder() handler.ConfigurationHandler(recorder, nil) decoder := json.NewDecoder(recorder.Body) err := decoder.Decode(&response) require.NoError(t, err) _, ok := response["uid"] assert.True(t, ok) delete(tCase.expected, "uid") delete(response, "uid") assert.Equal(t, http.StatusOK, recorder.Code) assert.Equal(t, tCase.expected, response) }) } } ================================================ FILE: diagnostic/log_collector.go ================================================ package diagnostic import ( "context" ) // Represents the path of the log file or log directory. // This struct is meant to give some ergonimics regarding // the logging information. type LogInformation struct { path string // path to a file or directory wasCreated bool // denotes if `path` was created isDirectory bool // denotes if `path` is a directory } func NewLogInformation( path string, wasCreated bool, isDirectory bool, ) *LogInformation { return &LogInformation{ path, wasCreated, isDirectory, } } type LogCollector interface { // This function is responsible for returning a path to a single file // whose contents are the logs of a cloudflared instance. // A new file may be create by a LogCollector, thus, its the caller // responsibility to remove the newly create file. Collect(ctx context.Context) (*LogInformation, error) } ================================================ FILE: diagnostic/log_collector_docker.go ================================================ package diagnostic import ( "context" "fmt" "os" "os/exec" "path/filepath" "time" ) type DockerLogCollector struct { containerID string // This member identifies the container by identifier or name } func NewDockerLogCollector(containerID string) *DockerLogCollector { return &DockerLogCollector{ containerID, } } func (collector *DockerLogCollector) Collect(ctx context.Context) (*LogInformation, error) { tmp := os.TempDir() outputHandle, err := os.Create(filepath.Join(tmp, logFilename)) if err != nil { return nil, fmt.Errorf("error opening output file: %w", err) } defer outputHandle.Close() // Calculate 2 weeks ago since := time.Now().Add(twoWeeksOffset).Format(time.RFC3339) command := exec.CommandContext( ctx, "docker", "logs", "--tail", tailMaxNumberOfLines, "--since", since, collector.containerID, ) return PipeCommandOutputToFile(command, outputHandle) } ================================================ FILE: diagnostic/log_collector_host.go ================================================ package diagnostic import ( "context" "fmt" "os" "os/exec" "path/filepath" "runtime" ) const ( linuxManagedLogsPath = "/var/log/cloudflared.err" darwinManagedLogsPath = "/Library/Logs/com.cloudflare.cloudflared.err.log" linuxServiceConfigurationPath = "/etc/systemd/system/cloudflared.service" linuxSystemdPath = "/run/systemd/system" ) type HostLogCollector struct { client HTTPClient } func NewHostLogCollector(client HTTPClient) *HostLogCollector { return &HostLogCollector{ client, } } func extractLogsFromJournalCtl(ctx context.Context) (*LogInformation, error) { tmp := os.TempDir() outputHandle, err := os.Create(filepath.Join(tmp, logFilename)) if err != nil { return nil, fmt.Errorf("error opening output file: %w", err) } defer outputHandle.Close() command := exec.CommandContext( ctx, "journalctl", "--since", "2 weeks ago", "-u", "cloudflared.service", ) return PipeCommandOutputToFile(command, outputHandle) } func getServiceLogPath() (string, error) { switch runtime.GOOS { case "darwin": { path := darwinManagedLogsPath if _, err := os.Stat(path); err == nil { return path, nil } userHomeDir, err := os.UserHomeDir() if err != nil { return "", fmt.Errorf("error getting user home: %w", err) } return filepath.Join(userHomeDir, darwinManagedLogsPath), nil } case "linux": { return linuxManagedLogsPath, nil } default: return "", ErrManagedLogNotFound } } func (collector *HostLogCollector) Collect(ctx context.Context) (*LogInformation, error) { logConfiguration, err := collector.client.GetLogConfiguration(ctx) if err != nil { return nil, fmt.Errorf("error getting log configuration: %w", err) } if logConfiguration.uid == 0 { _, statSystemdErr := os.Stat(linuxServiceConfigurationPath) _, statServiceConfigurationErr := os.Stat(linuxServiceConfigurationPath) if statSystemdErr == nil && statServiceConfigurationErr == nil && runtime.GOOS == "linux" { return extractLogsFromJournalCtl(ctx) } path, err := getServiceLogPath() if err != nil { return nil, err } return NewLogInformation(path, false, false), nil } if logConfiguration.logFile != "" { return NewLogInformation(logConfiguration.logFile, false, false), nil } else if logConfiguration.logDirectory != "" { return NewLogInformation(logConfiguration.logDirectory, false, true), nil } return nil, ErrLogConfigurationIsInvalid } ================================================ FILE: diagnostic/log_collector_kubernetes.go ================================================ package diagnostic import ( "context" "fmt" "os" "os/exec" "path/filepath" "time" ) type KubernetesLogCollector struct { containerID string // This member identifies the container by identifier or name pod string // This member identifies the pod where the container is deployed } func NewKubernetesLogCollector(containerID, pod string) *KubernetesLogCollector { return &KubernetesLogCollector{ containerID, pod, } } func (collector *KubernetesLogCollector) Collect(ctx context.Context) (*LogInformation, error) { tmp := os.TempDir() outputHandle, err := os.Create(filepath.Join(tmp, logFilename)) if err != nil { return nil, fmt.Errorf("error opening output file: %w", err) } defer outputHandle.Close() var command *exec.Cmd // Calculate 2 weeks ago since := time.Now().Add(twoWeeksOffset).Format(time.RFC3339) if collector.containerID != "" { command = exec.CommandContext( ctx, "kubectl", "logs", collector.pod, "--since-time", since, "--tail", tailMaxNumberOfLines, "-c", collector.containerID, ) } else { command = exec.CommandContext( ctx, "kubectl", "logs", collector.pod, "--since-time", since, "--tail", tailMaxNumberOfLines, ) } return PipeCommandOutputToFile(command, outputHandle) } ================================================ FILE: diagnostic/log_collector_utils.go ================================================ package diagnostic import ( "fmt" "io" "os" "os/exec" "path/filepath" ) func PipeCommandOutputToFile(command *exec.Cmd, outputHandle *os.File) (*LogInformation, error) { stdoutReader, err := command.StdoutPipe() if err != nil { return nil, fmt.Errorf( "error retrieving stdout from command '%s': %w", command.String(), err, ) } stderrReader, err := command.StderrPipe() if err != nil { return nil, fmt.Errorf( "error retrieving stderr from command '%s': %w", command.String(), err, ) } if err := command.Start(); err != nil { return nil, fmt.Errorf( "error running command '%s': %w", command.String(), err, ) } _, err = io.Copy(outputHandle, stdoutReader) if err != nil { return nil, fmt.Errorf( "error copying stdout from %s to file %s: %w", command.String(), outputHandle.Name(), err, ) } _, err = io.Copy(outputHandle, stderrReader) if err != nil { return nil, fmt.Errorf( "error copying stderr from %s to file %s: %w", command.String(), outputHandle.Name(), err, ) } if err := command.Wait(); err != nil { return nil, fmt.Errorf( "error waiting from command '%s': %w", command.String(), err, ) } return NewLogInformation(outputHandle.Name(), true, false), nil } func CopyFilesFromDirectory(path string) (string, error) { // rolling logs have as suffix the current date thus // when iterating the path files they are already in // chronological order files, err := os.ReadDir(path) if err != nil { return "", fmt.Errorf("error reading directory %s: %w", path, err) } outputHandle, err := os.Create(filepath.Join(os.TempDir(), logFilename)) if err != nil { return "", fmt.Errorf("creating file %s: %w", outputHandle.Name(), err) } defer outputHandle.Close() for _, file := range files { logHandle, err := os.Open(filepath.Join(path, file.Name())) if err != nil { return "", fmt.Errorf("error opening file %s:%w", file.Name(), err) } defer logHandle.Close() _, err = io.Copy(outputHandle, logHandle) if err != nil { return "", fmt.Errorf("error copying file %s:%w", logHandle.Name(), err) } } logHandle, err := os.Open(filepath.Join(path, "cloudflared.log")) if err != nil { return "", fmt.Errorf("error opening file %s:%w", logHandle.Name(), err) } defer logHandle.Close() _, err = io.Copy(outputHandle, logHandle) if err != nil { return "", fmt.Errorf("error copying file %s:%w", logHandle.Name(), err) } return outputHandle.Name(), nil } ================================================ FILE: diagnostic/network/collector.go ================================================ package diagnostic import ( "context" "errors" "time" ) const MicrosecondsFactor = 1000.0 var ErrEmptyDomain = errors.New("domain must not be empty") // For now only support ICMP is provided. type IPVersion int const ( V4 IPVersion = iota V6 IPVersion = iota ) type Hop struct { Hop uint8 `json:"hop,omitempty"` // hop number along the route Domain string `json:"domain,omitempty"` // domain and/or ip of the hop, this field will be '*' if the hop is a timeout Rtts []time.Duration `json:"rtts,omitempty"` // RTT measurements in microseconds } type TraceOptions struct { ttl uint64 // number of hops to perform timeout time.Duration // wait timeout for each response address string // address to trace useV4 bool } func NewTimeoutHop( hop uint8, ) *Hop { // Whenever there is a hop in the format of 'N * * *' // it means that the hop in the path didn't answer to // any probe. return NewHop( hop, "*", nil, ) } func NewHop(hop uint8, domain string, rtts []time.Duration) *Hop { return &Hop{ hop, domain, rtts, } } func NewTraceOptions( ttl uint64, timeout time.Duration, address string, useV4 bool, ) TraceOptions { return TraceOptions{ ttl, timeout, address, useV4, } } type NetworkCollector interface { // Performs a trace route operation with the specified options. // In case the trace fails, it will return a non-nil error and // it may return a string which represents the raw information // obtained. // In case it is successful it will only return an array of Hops // an empty string and a nil error. Collect(ctx context.Context, options TraceOptions) ([]*Hop, string, error) } ================================================ FILE: diagnostic/network/collector_unix.go ================================================ //go:build darwin || linux package diagnostic import ( "context" "fmt" "os/exec" "strconv" "strings" "time" ) type NetworkCollectorImpl struct{} func (tracer *NetworkCollectorImpl) Collect(ctx context.Context, options TraceOptions) ([]*Hop, string, error) { args := []string{ "-I", "-w", strconv.FormatInt(int64(options.timeout.Seconds()), 10), "-m", strconv.FormatUint(options.ttl, 10), options.address, } var command string switch options.useV4 { case false: command = "traceroute6" default: command = "traceroute" } process := exec.CommandContext(ctx, command, args...) return decodeNetworkOutputToFile(process, DecodeLine) } func DecodeLine(text string) (*Hop, error) { fields := strings.Fields(text) parts := []string{} filter := func(s string) bool { return s != "*" && s != "ms" } for _, field := range fields { if filter(field) { parts = append(parts, field) } } index, err := strconv.ParseUint(parts[0], 10, 8) if err != nil { return nil, fmt.Errorf("couldn't parse index from timeout hop: %w", err) } if len(parts) == 1 { return NewTimeoutHop(uint8(index)), nil } domain := "" rtts := []time.Duration{} for _, part := range parts[1:] { rtt, err := strconv.ParseFloat(part, 64) if err != nil { domain += part + " " } else { rtts = append(rtts, time.Duration(rtt*MicrosecondsFactor)) } } domain, _ = strings.CutSuffix(domain, " ") if domain == "" { return nil, ErrEmptyDomain } return NewHop(uint8(index), domain, rtts), nil } ================================================ FILE: diagnostic/network/collector_unix_test.go ================================================ //go:build darwin || linux package diagnostic_test import ( "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diagnostic "github.com/cloudflare/cloudflared/diagnostic/network" ) func TestDecode(t *testing.T) { t.Parallel() tests := []struct { name string text string expectedHops []*diagnostic.Hop }{ { "repeated hop index parse failure", `1 172.68.101.121 (172.68.101.121) 12.874 ms 15.517 ms 15.311 ms 2 172.68.101.121 (172.68.101.121) 12.874 ms 15.517 ms 15.311 ms someletters * * * 4 172.68.101.121 (172.68.101.121) 12.874 ms 15.517 ms 15.311 ms `, []*diagnostic.Hop{ diagnostic.NewHop( uint8(1), "172.68.101.121 (172.68.101.121)", []time.Duration{ time.Duration(12874), time.Duration(15517), time.Duration(15311), }, ), diagnostic.NewHop( uint8(2), "172.68.101.121 (172.68.101.121)", []time.Duration{ time.Duration(12874), time.Duration(15517), time.Duration(15311), }, ), diagnostic.NewHop( uint8(4), "172.68.101.121 (172.68.101.121)", []time.Duration{ time.Duration(12874), time.Duration(15517), time.Duration(15311), }, ), }, }, { "hop index parse failure", `1 172.68.101.121 (172.68.101.121) 12.874 ms 15.517 ms 15.311 ms 2 172.68.101.121 (172.68.101.121) 12.874 ms 15.517 ms 15.311 ms someletters 8.8.8.8 8.8.8.9 abc ms 0.456 ms 0.789 ms`, []*diagnostic.Hop{ diagnostic.NewHop( uint8(1), "172.68.101.121 (172.68.101.121)", []time.Duration{ time.Duration(12874), time.Duration(15517), time.Duration(15311), }, ), diagnostic.NewHop( uint8(2), "172.68.101.121 (172.68.101.121)", []time.Duration{ time.Duration(12874), time.Duration(15517), time.Duration(15311), }, ), }, }, { "missing rtt", `1 172.68.101.121 (172.68.101.121) 12.874 ms 15.517 ms 15.311 ms 2 * 8.8.8.8 8.8.8.9 0.456 ms 0.789 ms`, []*diagnostic.Hop{ diagnostic.NewHop( uint8(1), "172.68.101.121 (172.68.101.121)", []time.Duration{ time.Duration(12874), time.Duration(15517), time.Duration(15311), }, ), diagnostic.NewHop( uint8(2), "8.8.8.8 8.8.8.9", []time.Duration{ time.Duration(456), time.Duration(789), }, ), }, }, { "simple example ipv4", `1 172.68.101.121 (172.68.101.121) 12.874 ms 15.517 ms 15.311 ms 2 172.68.101.121 (172.68.101.121) 12.874 ms 15.517 ms 15.311 ms 3 * * *`, []*diagnostic.Hop{ diagnostic.NewHop( uint8(1), "172.68.101.121 (172.68.101.121)", []time.Duration{ time.Duration(12874), time.Duration(15517), time.Duration(15311), }, ), diagnostic.NewHop( uint8(2), "172.68.101.121 (172.68.101.121)", []time.Duration{ time.Duration(12874), time.Duration(15517), time.Duration(15311), }, ), diagnostic.NewTimeoutHop(uint8(3)), }, }, { "simple example ipv6", ` 1 2400:cb00:107:1024::ac44:6550 12.780 ms 9.118 ms 10.046 ms 2 2a09:bac1:: 9.945 ms 10.033 ms 11.562 ms`, []*diagnostic.Hop{ diagnostic.NewHop( uint8(1), "2400:cb00:107:1024::ac44:6550", []time.Duration{ time.Duration(12780), time.Duration(9118), time.Duration(10046), }, ), diagnostic.NewHop( uint8(2), "2a09:bac1::", []time.Duration{ time.Duration(9945), time.Duration(10033), time.Duration(11562), }, ), }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { t.Parallel() hops, err := diagnostic.Decode(strings.NewReader(test.text), diagnostic.DecodeLine) require.NoError(t, err) assert.Equal(t, test.expectedHops, hops) }) } } ================================================ FILE: diagnostic/network/collector_utils.go ================================================ package diagnostic import ( "bufio" "bytes" "fmt" "io" "os/exec" ) type DecodeLineFunc func(text string) (*Hop, error) func decodeNetworkOutputToFile(command *exec.Cmd, decodeLine DecodeLineFunc) ([]*Hop, string, error) { stdout, err := command.StdoutPipe() if err != nil { return nil, "", fmt.Errorf("error piping traceroute's output: %w", err) } if err := command.Start(); err != nil { return nil, "", fmt.Errorf("error starting traceroute: %w", err) } // Tee the output to a string to have the raw information // in case the decode call fails // This error is handled only after the Wait call below returns // otherwise the process can become a zombie buf := bytes.NewBuffer([]byte{}) tee := io.TeeReader(stdout, buf) hops, err := Decode(tee, decodeLine) // regardless of success of the decoding // consume all output to have available in buf _, _ = io.ReadAll(tee) if werr := command.Wait(); werr != nil { return nil, "", fmt.Errorf("error finishing traceroute: %w", werr) } if err != nil { return nil, buf.String(), err } return hops, buf.String(), nil } func Decode(reader io.Reader, decodeLine DecodeLineFunc) ([]*Hop, error) { scanner := bufio.NewScanner(reader) scanner.Split(bufio.ScanLines) var hops []*Hop for scanner.Scan() { text := scanner.Text() if text == "" { continue } hop, err := decodeLine(text) if err != nil { // This continue is here on the error case because there are lines at the start and end // that may not be parsable. (check windows tracert output) // The skip is here because aside from the start and end lines the other lines should // always be parsable without errors. continue } hops = append(hops, hop) } if scanner.Err() != nil { return nil, fmt.Errorf("scanner reported an error: %w", scanner.Err()) } return hops, nil } ================================================ FILE: diagnostic/network/collector_windows.go ================================================ //go:build windows package diagnostic import ( "context" "fmt" "os/exec" "strconv" "strings" "time" ) type NetworkCollectorImpl struct{} func (tracer *NetworkCollectorImpl) Collect(ctx context.Context, options TraceOptions) ([]*Hop, string, error) { ipversion := "-4" if !options.useV4 { ipversion = "-6" } args := []string{ ipversion, "-w", strconv.FormatInt(int64(options.timeout.Seconds()), 10), "-h", strconv.FormatUint(options.ttl, 10), // Do not resolve host names (can add 30+ seconds to run time) "-d", options.address, } command := exec.CommandContext(ctx, "tracert.exe", args...) return decodeNetworkOutputToFile(command, DecodeLine) } func DecodeLine(text string) (*Hop, error) { const requestTimedOut = "Request timed out." fields := strings.Fields(text) parts := []string{} filter := func(s string) bool { return s != "*" && s != "ms" } for _, field := range fields { if filter(field) { parts = append(parts, field) } } index, err := strconv.ParseUint(parts[0], 10, 8) if err != nil { return nil, fmt.Errorf("couldn't parse index from timeout hop: %w", err) } domain := "" rtts := []time.Duration{} for _, part := range parts[1:] { rtt, err := strconv.ParseFloat(strings.TrimLeft(part, "<"), 64) if err != nil { domain += part + " " } else { rtts = append(rtts, time.Duration(rtt*MicrosecondsFactor)) } } domain, _ = strings.CutSuffix(domain, " ") // If the domain is equal to "Request timed out." then we build a // timeout hop. if domain == requestTimedOut { return NewTimeoutHop(uint8(index)), nil } if domain == "" { return nil, ErrEmptyDomain } return NewHop(uint8(index), domain, rtts), nil } ================================================ FILE: diagnostic/network/collector_windows_test.go ================================================ //go:build windows package diagnostic_test import ( "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diagnostic "github.com/cloudflare/cloudflared/diagnostic/network" ) func TestDecode(t *testing.T) { t.Parallel() tests := []struct { name string text string expectedHops []*diagnostic.Hop }{ { "tracert output", ` Tracing route to region2.v2.argotunnel.com [198.41.200.73] over a maximum of 5 hops: 1 10 ms <1 ms 1 ms 192.168.64.1 2 27 ms 14 ms 5 ms 192.168.1.254 3 * * * Request timed out. 4 * * * Request timed out. 5 27 ms 5 ms 5 ms 195.8.30.245 Trace complete. `, []*diagnostic.Hop{ diagnostic.NewHop( uint8(1), "192.168.64.1", []time.Duration{ time.Duration(10000), time.Duration(1000), time.Duration(1000), }, ), diagnostic.NewHop( uint8(2), "192.168.1.254", []time.Duration{ time.Duration(27000), time.Duration(14000), time.Duration(5000), }, ), diagnostic.NewTimeoutHop(uint8(3)), diagnostic.NewTimeoutHop(uint8(4)), diagnostic.NewHop( uint8(5), "195.8.30.245", []time.Duration{ time.Duration(27000), time.Duration(5000), time.Duration(5000), }, ), }, }, { "repeated hop index parse failure", `1 12.874 ms 15.517 ms 15.311 ms 172.68.101.121 (172.68.101.121) 2 12.874 ms 15.517 ms 15.311 ms 172.68.101.121 (172.68.101.121) someletters * * *`, []*diagnostic.Hop{ diagnostic.NewHop( uint8(1), "172.68.101.121 (172.68.101.121)", []time.Duration{ time.Duration(12874), time.Duration(15517), time.Duration(15311), }, ), diagnostic.NewHop( uint8(2), "172.68.101.121 (172.68.101.121)", []time.Duration{ time.Duration(12874), time.Duration(15517), time.Duration(15311), }, ), }, }, { "hop index parse failure", `1 12.874 ms 15.517 ms 15.311 ms 172.68.101.121 (172.68.101.121) 2 12.874 ms 15.517 ms 15.311 ms 172.68.101.121 (172.68.101.121) someletters abc ms 0.456 ms 0.789 ms 8.8.8.8 8.8.8.9`, []*diagnostic.Hop{ diagnostic.NewHop( uint8(1), "172.68.101.121 (172.68.101.121)", []time.Duration{ time.Duration(12874), time.Duration(15517), time.Duration(15311), }, ), diagnostic.NewHop( uint8(2), "172.68.101.121 (172.68.101.121)", []time.Duration{ time.Duration(12874), time.Duration(15517), time.Duration(15311), }, ), }, }, { "missing rtt", `1 <12.874 ms <15.517 ms <15.311 ms 172.68.101.121 (172.68.101.121) 2 * 0.456 ms 0.789 ms 8.8.8.8 8.8.8.9`, []*diagnostic.Hop{ diagnostic.NewHop( uint8(1), "172.68.101.121 (172.68.101.121)", []time.Duration{ time.Duration(12874), time.Duration(15517), time.Duration(15311), }, ), diagnostic.NewHop( uint8(2), "8.8.8.8 8.8.8.9", []time.Duration{ time.Duration(456), time.Duration(789), }, ), }, }, { "simple example ipv4", `1 12.874 ms 15.517 ms 15.311 ms 172.68.101.121 (172.68.101.121) 2 12.874 ms 15.517 ms 15.311 ms 172.68.101.121 (172.68.101.121) 3 * * * Request timed out.`, []*diagnostic.Hop{ diagnostic.NewHop( uint8(1), "172.68.101.121 (172.68.101.121)", []time.Duration{ time.Duration(12874), time.Duration(15517), time.Duration(15311), }, ), diagnostic.NewHop( uint8(2), "172.68.101.121 (172.68.101.121)", []time.Duration{ time.Duration(12874), time.Duration(15517), time.Duration(15311), }, ), diagnostic.NewTimeoutHop(uint8(3)), }, }, { "simple example ipv6", ` 1 12.780 ms 9.118 ms 10.046 ms 2400:cb00:107:1024::ac44:6550 2 9.945 ms 10.033 ms 11.562 ms 2a09:bac1::`, []*diagnostic.Hop{ diagnostic.NewHop( uint8(1), "2400:cb00:107:1024::ac44:6550", []time.Duration{ time.Duration(12780), time.Duration(9118), time.Duration(10046), }, ), diagnostic.NewHop( uint8(2), "2a09:bac1::", []time.Duration{ time.Duration(9945), time.Duration(10033), time.Duration(11562), }, ), }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { t.Parallel() hops, err := diagnostic.Decode(strings.NewReader(test.text), diagnostic.DecodeLine) require.NoError(t, err) assert.Equal(t, test.expectedHops, hops) }) } } ================================================ FILE: diagnostic/system_collector.go ================================================ package diagnostic import ( "context" "encoding/json" "errors" "strings" ) type SystemInformationError struct { Err error `json:"error"` RawInfo string `json:"rawInfo"` } func (err SystemInformationError) Error() string { return err.Err.Error() } func (err SystemInformationError) MarshalJSON() ([]byte, error) { s := map[string]string{ "error": err.Err.Error(), "rawInfo": err.RawInfo, } return json.Marshal(s) } type SystemInformationGeneralError struct { OperatingSystemInformationError error MemoryInformationError error FileDescriptorsInformationError error DiskVolumeInformationError error } func (err SystemInformationGeneralError) Error() string { builder := &strings.Builder{} builder.WriteString("errors found:") if err.OperatingSystemInformationError != nil { builder.WriteString(err.OperatingSystemInformationError.Error() + ", ") } if err.MemoryInformationError != nil { builder.WriteString(err.MemoryInformationError.Error() + ", ") } if err.FileDescriptorsInformationError != nil { builder.WriteString(err.FileDescriptorsInformationError.Error() + ", ") } if err.DiskVolumeInformationError != nil { builder.WriteString(err.DiskVolumeInformationError.Error() + ", ") } return builder.String() } func (err SystemInformationGeneralError) MarshalJSON() ([]byte, error) { data := map[string]SystemInformationError{} var sysErr SystemInformationError if errors.As(err.OperatingSystemInformationError, &sysErr) { data["operatingSystemInformationError"] = sysErr } if errors.As(err.MemoryInformationError, &sysErr) { data["memoryInformationError"] = sysErr } if errors.As(err.FileDescriptorsInformationError, &sysErr) { data["fileDescriptorsInformationError"] = sysErr } if errors.As(err.DiskVolumeInformationError, &sysErr) { data["diskVolumeInformationError"] = sysErr } return json.Marshal(data) } type DiskVolumeInformation struct { Name string `json:"name"` // represents the filesystem in linux/macos or device name in windows SizeMaximum uint64 `json:"sizeMaximum"` // represents the maximum size of the disk in kilobytes SizeCurrent uint64 `json:"sizeCurrent"` // represents the current size of the disk in kilobytes } func NewDiskVolumeInformation(name string, maximum, current uint64) *DiskVolumeInformation { return &DiskVolumeInformation{ name, maximum, current, } } type SystemInformation struct { MemoryMaximum uint64 `json:"memoryMaximum,omitempty"` // represents the maximum memory of the system in kilobytes MemoryCurrent uint64 `json:"memoryCurrent,omitempty"` // represents the system's memory in use in kilobytes FileDescriptorMaximum uint64 `json:"fileDescriptorMaximum,omitempty"` // represents the maximum number of file descriptors of the system FileDescriptorCurrent uint64 `json:"fileDescriptorCurrent,omitempty"` // represents the system's file descriptors in use OsSystem string `json:"osSystem,omitempty"` // represents the operating system name i.e.: linux, windows, darwin HostName string `json:"hostName,omitempty"` // represents the system host name OsVersion string `json:"osVersion,omitempty"` // detailed information about the system's release version level OsRelease string `json:"osRelease,omitempty"` // detailed information about the system's release Architecture string `json:"architecture,omitempty"` // represents the system's hardware platform i.e: arm64/amd64 CloudflaredVersion string `json:"cloudflaredVersion,omitempty"` // the runtime version of cloudflared GoVersion string `json:"goVersion,omitempty"` GoArch string `json:"goArch,omitempty"` Disk []*DiskVolumeInformation `json:"disk,omitempty"` } func NewSystemInformation( memoryMaximum, memoryCurrent, filesMaximum, filesCurrent uint64, osystem, name, osVersion, osRelease, architecture, cloudflaredVersion, goVersion, goArchitecture string, disk []*DiskVolumeInformation, ) *SystemInformation { return &SystemInformation{ memoryMaximum, memoryCurrent, filesMaximum, filesCurrent, osystem, name, osVersion, osRelease, architecture, cloudflaredVersion, goVersion, goArchitecture, disk, } } type SystemCollector interface { // If the collection is successful it will return `SystemInformation` struct, // and a nil error. // // This function expects that the caller sets the context timeout to prevent // long-lived collectors. Collect(ctx context.Context) (*SystemInformation, error) } ================================================ FILE: diagnostic/system_collector_linux.go ================================================ //go:build linux package diagnostic import ( "context" "fmt" "os/exec" "runtime" "strconv" "strings" ) type SystemCollectorImpl struct { version string } func NewSystemCollectorImpl( version string, ) *SystemCollectorImpl { return &SystemCollectorImpl{ version, } } func (collector *SystemCollectorImpl) Collect(ctx context.Context) (*SystemInformation, error) { memoryInfo, memoryInfoRaw, memoryInfoErr := collectMemoryInformation(ctx) fdInfo, fdInfoRaw, fdInfoErr := collectFileDescriptorInformation(ctx) disks, disksRaw, diskErr := collectDiskVolumeInformationUnix(ctx) osInfo, osInfoRaw, osInfoErr := collectOSInformationUnix(ctx) var memoryMaximum, memoryCurrent, fileDescriptorMaximum, fileDescriptorCurrent uint64 var osSystem, name, osVersion, osRelease, architecture string gerror := SystemInformationGeneralError{} if memoryInfoErr != nil { gerror.MemoryInformationError = SystemInformationError{ Err: memoryInfoErr, RawInfo: memoryInfoRaw, } } else { memoryMaximum = memoryInfo.MemoryMaximum memoryCurrent = memoryInfo.MemoryCurrent } if fdInfoErr != nil { gerror.FileDescriptorsInformationError = SystemInformationError{ Err: fdInfoErr, RawInfo: fdInfoRaw, } } else { fileDescriptorMaximum = fdInfo.FileDescriptorMaximum fileDescriptorCurrent = fdInfo.FileDescriptorCurrent } if diskErr != nil { gerror.DiskVolumeInformationError = SystemInformationError{ Err: diskErr, RawInfo: disksRaw, } } if osInfoErr != nil { gerror.OperatingSystemInformationError = SystemInformationError{ Err: osInfoErr, RawInfo: osInfoRaw, } } else { osSystem = osInfo.OsSystem name = osInfo.Name osVersion = osInfo.OsVersion osRelease = osInfo.OsRelease architecture = osInfo.Architecture } cloudflaredVersion := collector.version info := NewSystemInformation( memoryMaximum, memoryCurrent, fileDescriptorMaximum, fileDescriptorCurrent, osSystem, name, osVersion, osRelease, architecture, cloudflaredVersion, runtime.Version(), runtime.GOARCH, disks, ) return info, gerror } func collectMemoryInformation(ctx context.Context) (*MemoryInformation, string, error) { // This function relies on the output of `cat /proc/meminfo` to retrieve // memoryMax and memoryCurrent. // The expected output is in the format of `KEY VALUE UNIT`. const ( memTotalPrefix = "MemTotal" memAvailablePrefix = "MemAvailable" ) command := exec.CommandContext(ctx, "cat", "/proc/meminfo") stdout, err := command.Output() if err != nil { return nil, "", fmt.Errorf("error retrieving output from command '%s': %w", command.String(), err) } output := string(stdout) mapper := func(field string) (uint64, error) { field = strings.TrimRight(field, " kB") return strconv.ParseUint(field, 10, 64) } memoryInfo, err := ParseMemoryInformationFromKV(output, memTotalPrefix, memAvailablePrefix, mapper) if err != nil { return nil, output, err } // returning raw output in case other collected information // resulted in errors return memoryInfo, output, nil } func collectFileDescriptorInformation(ctx context.Context) (*FileDescriptorInformation, string, error) { // Command retrieved from https://docs.kernel.org/admin-guide/sysctl/fs.html#file-max-file-nr. // If the sysctl is not available the command with fail. command := exec.CommandContext(ctx, "sysctl", "-n", "fs.file-nr") stdout, err := command.Output() if err != nil { return nil, "", fmt.Errorf("error retrieving output from command '%s': %w", command.String(), err) } output := string(stdout) fileDescriptorInfo, err := ParseSysctlFileDescriptorInformation(output) if err != nil { return nil, output, err } // returning raw output in case other collected information // resulted in errors return fileDescriptorInfo, output, nil } ================================================ FILE: diagnostic/system_collector_macos.go ================================================ //go:build darwin package diagnostic import ( "context" "fmt" "os/exec" "runtime" "strconv" ) type SystemCollectorImpl struct { version string } func NewSystemCollectorImpl( version string, ) *SystemCollectorImpl { return &SystemCollectorImpl{ version, } } func (collector *SystemCollectorImpl) Collect(ctx context.Context) (*SystemInformation, error) { memoryInfo, memoryInfoRaw, memoryInfoErr := collectMemoryInformation(ctx) fdInfo, fdInfoRaw, fdInfoErr := collectFileDescriptorInformation(ctx) disks, disksRaw, diskErr := collectDiskVolumeInformationUnix(ctx) osInfo, osInfoRaw, osInfoErr := collectOSInformationUnix(ctx) var memoryMaximum, memoryCurrent, fileDescriptorMaximum, fileDescriptorCurrent uint64 var osSystem, name, osVersion, osRelease, architecture string err := SystemInformationGeneralError{ OperatingSystemInformationError: nil, MemoryInformationError: nil, FileDescriptorsInformationError: nil, DiskVolumeInformationError: nil, } if memoryInfoErr != nil { err.MemoryInformationError = SystemInformationError{ Err: memoryInfoErr, RawInfo: memoryInfoRaw, } } else { memoryMaximum = memoryInfo.MemoryMaximum memoryCurrent = memoryInfo.MemoryCurrent } if fdInfoErr != nil { err.FileDescriptorsInformationError = SystemInformationError{ Err: fdInfoErr, RawInfo: fdInfoRaw, } } else { fileDescriptorMaximum = fdInfo.FileDescriptorMaximum fileDescriptorCurrent = fdInfo.FileDescriptorCurrent } if diskErr != nil { err.DiskVolumeInformationError = SystemInformationError{ Err: diskErr, RawInfo: disksRaw, } } if osInfoErr != nil { err.OperatingSystemInformationError = SystemInformationError{ Err: osInfoErr, RawInfo: osInfoRaw, } } else { osSystem = osInfo.OsSystem name = osInfo.Name osVersion = osInfo.OsVersion osRelease = osInfo.OsRelease architecture = osInfo.Architecture } cloudflaredVersion := collector.version info := NewSystemInformation( memoryMaximum, memoryCurrent, fileDescriptorMaximum, fileDescriptorCurrent, osSystem, name, osVersion, osRelease, architecture, cloudflaredVersion, runtime.Version(), runtime.GOARCH, disks, ) return info, err } func collectFileDescriptorInformation(ctx context.Context) ( *FileDescriptorInformation, string, error, ) { const ( fileDescriptorMaximumKey = "kern.maxfiles" fileDescriptorCurrentKey = "kern.num_files" ) command := exec.CommandContext(ctx, "sysctl", fileDescriptorMaximumKey, fileDescriptorCurrentKey) stdout, err := command.Output() if err != nil { return nil, "", fmt.Errorf("error retrieving output from command '%s': %w", command.String(), err) } output := string(stdout) fileDescriptorInfo, err := ParseFileDescriptorInformationFromKV( output, fileDescriptorMaximumKey, fileDescriptorCurrentKey, ) if err != nil { return nil, output, err } // returning raw output in case other collected information // resulted in errors return fileDescriptorInfo, output, nil } func collectMemoryInformation(ctx context.Context) ( *MemoryInformation, string, error, ) { const ( memoryMaximumKey = "hw.memsize" memoryAvailableKey = "hw.memsize_usable" ) command := exec.CommandContext( ctx, "sysctl", memoryMaximumKey, memoryAvailableKey, ) stdout, err := command.Output() if err != nil { return nil, "", fmt.Errorf("error retrieving output from command '%s': %w", command.String(), err) } output := string(stdout) mapper := func(field string) (uint64, error) { const kiloBytes = 1024 value, err := strconv.ParseUint(field, 10, 64) return value / kiloBytes, err } memoryInfo, err := ParseMemoryInformationFromKV(output, memoryMaximumKey, memoryAvailableKey, mapper) if err != nil { return nil, output, err } // returning raw output in case other collected information // resulted in errors return memoryInfo, output, nil } ================================================ FILE: diagnostic/system_collector_test.go ================================================ package diagnostic_test import ( "strconv" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/cloudflare/cloudflared/diagnostic" ) func TestParseMemoryInformationFromKV(t *testing.T) { t.Parallel() mapper := func(field string) (uint64, error) { value, err := strconv.ParseUint(field, 10, 64) return value, err } linuxMapper := func(field string) (uint64, error) { field = strings.TrimRight(field, " kB") return strconv.ParseUint(field, 10, 64) } windowsMemoryOutput := ` FreeVirtualMemory : 5350472 TotalVirtualMemorySize : 8903424 ` macosMemoryOutput := `hw.memsize: 38654705664 hw.memsize_usable: 38009012224` memoryOutputWithMissingKey := `hw.memsize: 38654705664` linuxMemoryOutput := `MemTotal: 8028860 kB MemFree: 731396 kB MemAvailable: 4678844 kB Buffers: 472632 kB Cached: 3186492 kB SwapCached: 4196 kB Active: 3088988 kB Inactive: 3468560 kB` tests := []struct { name string output string memoryMaximumKey string memoryAvailableKey string expected *diagnostic.MemoryInformation expectedErr bool mapper func(string) (uint64, error) }{ { name: "parse linux memory values", output: linuxMemoryOutput, memoryMaximumKey: "MemTotal", memoryAvailableKey: "MemAvailable", expected: &diagnostic.MemoryInformation{ 8028860, 8028860 - 4678844, }, expectedErr: false, mapper: linuxMapper, }, { name: "parse memory values with missing key", output: memoryOutputWithMissingKey, memoryMaximumKey: "hw.memsize", memoryAvailableKey: "hw.memsize_usable", expected: nil, expectedErr: true, mapper: mapper, }, { name: "parse macos memory values", output: macosMemoryOutput, memoryMaximumKey: "hw.memsize", memoryAvailableKey: "hw.memsize_usable", expected: &diagnostic.MemoryInformation{ 38654705664, 38654705664 - 38009012224, }, expectedErr: false, mapper: mapper, }, { name: "parse windows memory values", output: windowsMemoryOutput, memoryMaximumKey: "TotalVirtualMemorySize", memoryAvailableKey: "FreeVirtualMemory", expected: &diagnostic.MemoryInformation{ 8903424, 8903424 - 5350472, }, expectedErr: false, mapper: mapper, }, } for _, tCase := range tests { t.Run(tCase.name, func(t *testing.T) { t.Parallel() memoryInfo, err := diagnostic.ParseMemoryInformationFromKV( tCase.output, tCase.memoryMaximumKey, tCase.memoryAvailableKey, tCase.mapper, ) if tCase.expectedErr { assert.Error(t, err) } else { require.NoError(t, err) assert.Equal(t, tCase.expected, memoryInfo) } }) } } func TestParseUnameOutput(t *testing.T) { t.Parallel() tests := []struct { name string output string os string expected *diagnostic.OsInfo expectedErr bool }{ { name: "darwin machine", output: "Darwin APC 23.6.0 Darwin Kernel Version 99.6.0: Wed Jul 31 20:48:04 PDT 1997; root:xnu-66666.666.6.666.6~1/RELEASE_ARM64_T6666 arm64", os: "darwin", expected: &diagnostic.OsInfo{ Architecture: "arm64", Name: "APC", OsSystem: "Darwin", OsRelease: "Darwin Kernel Version 99.6.0: Wed Jul 31 20:48:04 PDT 1997; root:xnu-66666.666.6.666.6~1/RELEASE_ARM64_T6666", OsVersion: "23.6.0", }, expectedErr: false, }, { name: "linux machine", output: "Linux dab00d565591 6.6.31-linuxkit #1 SMP Thu May 23 08:36:57 UTC 2024 aarch64 GNU/Linux", os: "linux", expected: &diagnostic.OsInfo{ Architecture: "aarch64", Name: "dab00d565591", OsSystem: "Linux", OsRelease: "#1 SMP Thu May 23 08:36:57 UTC 2024", OsVersion: "6.6.31-linuxkit", }, expectedErr: false, }, { name: "not enough fields", output: "Linux ", os: "linux", expected: nil, expectedErr: true, }, } for _, tCase := range tests { t.Run(tCase.name, func(t *testing.T) { t.Parallel() memoryInfo, err := diagnostic.ParseUnameOutput( tCase.output, tCase.os, ) if tCase.expectedErr { assert.Error(t, err) } else { require.NoError(t, err) assert.Equal(t, tCase.expected, memoryInfo) } }) } } func TestParseFileDescriptorInformationFromKV(t *testing.T) { const ( fileDescriptorMaximumKey = "kern.maxfiles" fileDescriptorCurrentKey = "kern.num_files" ) t.Parallel() memoryOutput := `kern.maxfiles: 276480 kern.num_files: 11787` memoryOutputWithMissingKey := `kern.maxfiles: 276480` tests := []struct { name string output string expected *diagnostic.FileDescriptorInformation expectedErr bool }{ { name: "parse memory values with missing key", output: memoryOutputWithMissingKey, expected: nil, expectedErr: true, }, { name: "parse macos memory values", output: memoryOutput, expected: &diagnostic.FileDescriptorInformation{ 276480, 11787, }, expectedErr: false, }, } for _, tCase := range tests { t.Run(tCase.name, func(t *testing.T) { t.Parallel() fdInfo, err := diagnostic.ParseFileDescriptorInformationFromKV( tCase.output, fileDescriptorMaximumKey, fileDescriptorCurrentKey, ) if tCase.expectedErr { assert.Error(t, err) } else { require.NoError(t, err) assert.Equal(t, tCase.expected, fdInfo) } }) } } func TestParseSysctlFileDescriptorInformation(t *testing.T) { t.Parallel() tests := []struct { name string output string expected *diagnostic.FileDescriptorInformation expectedErr bool }{ { name: "expected output", output: "111 0 1111111", expected: &diagnostic.FileDescriptorInformation{ FileDescriptorMaximum: 1111111, FileDescriptorCurrent: 111, }, expectedErr: false, }, { name: "not enough fields", output: "111 111 ", expected: nil, expectedErr: true, }, } for _, tCase := range tests { t.Run(tCase.name, func(t *testing.T) { t.Parallel() fdsInfo, err := diagnostic.ParseSysctlFileDescriptorInformation( tCase.output, ) if tCase.expectedErr { assert.Error(t, err) } else { require.NoError(t, err) assert.Equal(t, tCase.expected, fdsInfo) } }) } } func TestParseWinOperatingSystemInfo(t *testing.T) { const ( architecturePrefix = "OSArchitecture" osSystemPrefix = "Caption" osVersionPrefix = "Version" osReleasePrefix = "BuildNumber" namePrefix = "CSName" ) t.Parallel() windowsIncompleteOsInfo := ` OSArchitecture : ARM 64 bits Caption : Microsoft Windows 11 Home Morekeys : 121314 CSName : UTILIZA-QO859QP ` windowsCompleteOsInfo := ` OSArchitecture : ARM 64 bits Caption : Microsoft Windows 11 Home Version : 10.0.22631 BuildNumber : 22631 Morekeys : 121314 CSName : UTILIZA-QO859QP ` tests := []struct { name string output string expected *diagnostic.OsInfo expectedErr bool }{ { name: "expected output", output: windowsCompleteOsInfo, expected: &diagnostic.OsInfo{ Architecture: "ARM 64 bits", Name: "UTILIZA-QO859QP", OsSystem: "Microsoft Windows 11 Home", OsRelease: "22631", OsVersion: "10.0.22631", }, expectedErr: false, }, { name: "missing keys", output: windowsIncompleteOsInfo, expected: nil, expectedErr: true, }, } for _, tCase := range tests { t.Run(tCase.name, func(t *testing.T) { t.Parallel() osInfo, err := diagnostic.ParseWinOperatingSystemInfo( tCase.output, architecturePrefix, osSystemPrefix, osVersionPrefix, osReleasePrefix, namePrefix, ) if tCase.expectedErr { assert.Error(t, err) } else { require.NoError(t, err) assert.Equal(t, tCase.expected, osInfo) } }) } } func TestParseDiskVolumeInformationOutput(t *testing.T) { t.Parallel() invalidUnixDiskVolumeInfo := `Filesystem Size Used Avail Use% Mounted on overlay 59G 19G 38G 33% / tmpfs 64M 0 64M 0% /dev shm 64M 0 64M 0% /dev/shm /run/host_mark/Users 461G 266G 195G 58% /tmp/cloudflared /dev/vda1 59G 19G 38G 33% /etc/hosts tmpfs 3.9G 0 3.9G 0% /sys/firmware ` unixDiskVolumeInfo := `Filesystem Size Used Avail Use% Mounted on overlay 61202244 18881444 39179476 33% / tmpfs 65536 0 65536 0% /dev shm 65536 0 65536 0% /dev/shm /run/host_mark/Users 482797652 278648468 204149184 58% /tmp/cloudflared /dev/vda1 61202244 18881444 39179476 33% /etc/hosts tmpfs 4014428 0 4014428 0% /sys/firmware` missingFields := ` DeviceID Size -------- ---- C: size E: 235563008 Z: 67754782720 ` invalidTypeField := ` DeviceID Size FreeSpace -------- ---- --------- C: size 31318736896 D: E: 235563008 0 Z: 67754782720 31318732800 ` windowsDiskVolumeInfo := ` DeviceID Size FreeSpace -------- ---- --------- C: 67754782720 31318736896 E: 235563008 0 Z: 67754782720 31318732800` tests := []struct { name string output string expected []*diagnostic.DiskVolumeInformation skipLines int expectedErr bool }{ { name: "invalid unix disk volume information (numbers have units)", output: invalidUnixDiskVolumeInfo, expected: []*diagnostic.DiskVolumeInformation{}, skipLines: 1, expectedErr: true, }, { name: "unix disk volume information", output: unixDiskVolumeInfo, skipLines: 1, expected: []*diagnostic.DiskVolumeInformation{ diagnostic.NewDiskVolumeInformation("overlay", 61202244, 18881444), diagnostic.NewDiskVolumeInformation("tmpfs", 65536, 0), diagnostic.NewDiskVolumeInformation("shm", 65536, 0), diagnostic.NewDiskVolumeInformation("/run/host_mark/Users", 482797652, 278648468), diagnostic.NewDiskVolumeInformation("/dev/vda1", 61202244, 18881444), diagnostic.NewDiskVolumeInformation("tmpfs", 4014428, 0), }, expectedErr: false, }, { name: "windows disk volume information", output: windowsDiskVolumeInfo, expected: []*diagnostic.DiskVolumeInformation{ diagnostic.NewDiskVolumeInformation("C:", 67754782720, 31318736896), diagnostic.NewDiskVolumeInformation("E:", 235563008, 0), diagnostic.NewDiskVolumeInformation("Z:", 67754782720, 31318732800), }, skipLines: 4, expectedErr: false, }, { name: "insuficient fields", output: missingFields, expected: nil, skipLines: 2, expectedErr: true, }, { name: "invalid field", output: invalidTypeField, expected: nil, skipLines: 2, expectedErr: true, }, } for _, tCase := range tests { t.Run(tCase.name, func(t *testing.T) { t.Parallel() disks, err := diagnostic.ParseDiskVolumeInformationOutput(tCase.output, tCase.skipLines, 1) if tCase.expectedErr { assert.Error(t, err) } else { require.NoError(t, err) assert.Equal(t, tCase.expected, disks) } }) } } ================================================ FILE: diagnostic/system_collector_utils.go ================================================ package diagnostic import ( "context" "fmt" "os/exec" "runtime" "sort" "strconv" "strings" ) func findColonSeparatedPairs[V any](output string, keys []string, mapper func(string) (V, error)) map[string]V { const ( memoryField = 1 memoryInformationFields = 2 ) lines := strings.Split(output, "\n") pairs := make(map[string]V, 0) // sort keys and lines to allow incremental search sort.Strings(lines) sort.Strings(keys) // keeps track of the last key found lastIndex := 0 for _, line := range lines { if lastIndex == len(keys) { // already found all keys no need to continue iterating // over the other values break } for index, key := range keys[lastIndex:] { line = strings.TrimSpace(line) if strings.HasPrefix(line, key) { fields := strings.Split(line, ":") if len(fields) < memoryInformationFields { lastIndex = index + 1 break } field, err := mapper(strings.TrimSpace(fields[memoryField])) if err != nil { lastIndex = lastIndex + index + 1 break } pairs[key] = field lastIndex = lastIndex + index + 1 break } } } return pairs } func ParseDiskVolumeInformationOutput(output string, skipLines int, scale float64) ([]*DiskVolumeInformation, error) { const ( diskFieldsMinimum = 3 nameField = 0 sizeMaximumField = 1 sizeCurrentField = 2 ) disksRaw := strings.Split(output, "\n") disks := make([]*DiskVolumeInformation, 0) if skipLines > len(disksRaw) || skipLines < 0 { skipLines = 0 } for _, disk := range disksRaw[skipLines:] { if disk == "" { // skip empty line continue } fields := strings.Fields(disk) if len(fields) < diskFieldsMinimum { return nil, fmt.Errorf("expected disk volume to have %d fields got %d: %w", diskFieldsMinimum, len(fields), ErrInsuficientFields, ) } name := fields[nameField] sizeMaximum, err := strconv.ParseUint(fields[sizeMaximumField], 10, 64) if err != nil { continue } sizeCurrent, err := strconv.ParseUint(fields[sizeCurrentField], 10, 64) if err != nil { continue } diskInfo := NewDiskVolumeInformation( name, uint64(float64(sizeMaximum)*scale), uint64(float64(sizeCurrent)*scale), ) disks = append(disks, diskInfo) } if len(disks) == 0 { return nil, ErrNoVolumeFound } return disks, nil } type OsInfo struct { OsSystem string Name string OsVersion string OsRelease string Architecture string } func ParseUnameOutput(output string, system string) (*OsInfo, error) { const ( osystemField = 0 nameField = 1 osVersionField = 2 osReleaseStartField = 3 osInformationFieldsMinimum = 6 darwin = "darwin" ) architectureOffset := 2 if system == darwin { architectureOffset = 1 } fields := strings.Fields(output) if len(fields) < osInformationFieldsMinimum { return nil, fmt.Errorf("expected system information to have %d fields got %d: %w", osInformationFieldsMinimum, len(fields), ErrInsuficientFields, ) } architectureField := len(fields) - architectureOffset osystem := fields[osystemField] name := fields[nameField] osVersion := fields[osVersionField] osRelease := strings.Join(fields[osReleaseStartField:architectureField], " ") architecture := fields[architectureField] return &OsInfo{ osystem, name, osVersion, osRelease, architecture, }, nil } func ParseWinOperatingSystemInfo( output string, architectureKey string, osSystemKey string, osVersionKey string, osReleaseKey string, nameKey string, ) (*OsInfo, error) { identity := func(s string) (string, error) { return s, nil } keys := []string{architectureKey, osSystemKey, osVersionKey, osReleaseKey, nameKey} pairs := findColonSeparatedPairs( output, keys, identity, ) architecture, exists := pairs[architectureKey] if !exists { return nil, fmt.Errorf("parsing os information: %w, key=%s", ErrKeyNotFound, architectureKey) } osSystem, exists := pairs[osSystemKey] if !exists { return nil, fmt.Errorf("parsing os information: %w, key=%s", ErrKeyNotFound, osSystemKey) } osVersion, exists := pairs[osVersionKey] if !exists { return nil, fmt.Errorf("parsing os information: %w, key=%s", ErrKeyNotFound, osVersionKey) } osRelease, exists := pairs[osReleaseKey] if !exists { return nil, fmt.Errorf("parsing os information: %w, key=%s", ErrKeyNotFound, osReleaseKey) } name, exists := pairs[nameKey] if !exists { return nil, fmt.Errorf("parsing os information: %w, key=%s", ErrKeyNotFound, nameKey) } return &OsInfo{osSystem, name, osVersion, osRelease, architecture}, nil } type FileDescriptorInformation struct { FileDescriptorMaximum uint64 FileDescriptorCurrent uint64 } func ParseSysctlFileDescriptorInformation(output string) (*FileDescriptorInformation, error) { const ( openFilesField = 0 maxFilesField = 2 fileDescriptorLimitsFields = 3 ) fields := strings.Fields(output) if len(fields) != fileDescriptorLimitsFields { return nil, fmt.Errorf( "expected file descriptor information to have %d fields got %d: %w", fileDescriptorLimitsFields, len(fields), ErrInsuficientFields, ) } fileDescriptorCurrent, err := strconv.ParseUint(fields[openFilesField], 10, 64) if err != nil { return nil, fmt.Errorf( "error parsing files current field '%s': %w", fields[openFilesField], err, ) } fileDescriptorMaximum, err := strconv.ParseUint(fields[maxFilesField], 10, 64) if err != nil { return nil, fmt.Errorf("error parsing files max field '%s': %w", fields[maxFilesField], err) } return &FileDescriptorInformation{fileDescriptorMaximum, fileDescriptorCurrent}, nil } func ParseFileDescriptorInformationFromKV( output string, fileDescriptorMaximumKey string, fileDescriptorCurrentKey string, ) (*FileDescriptorInformation, error) { mapper := func(field string) (uint64, error) { return strconv.ParseUint(field, 10, 64) } pairs := findColonSeparatedPairs(output, []string{fileDescriptorMaximumKey, fileDescriptorCurrentKey}, mapper) fileDescriptorMaximum, exists := pairs[fileDescriptorMaximumKey] if !exists { return nil, fmt.Errorf( "parsing file descriptor information: %w, key=%s", ErrKeyNotFound, fileDescriptorMaximumKey, ) } fileDescriptorCurrent, exists := pairs[fileDescriptorCurrentKey] if !exists { return nil, fmt.Errorf( "parsing file descriptor information: %w, key=%s", ErrKeyNotFound, fileDescriptorCurrentKey, ) } return &FileDescriptorInformation{fileDescriptorMaximum, fileDescriptorCurrent}, nil } type MemoryInformation struct { MemoryMaximum uint64 // size in KB MemoryCurrent uint64 // size in KB } func ParseMemoryInformationFromKV( output string, memoryMaximumKey string, memoryAvailableKey string, mapper func(field string) (uint64, error), ) (*MemoryInformation, error) { pairs := findColonSeparatedPairs(output, []string{memoryMaximumKey, memoryAvailableKey}, mapper) memoryMaximum, exists := pairs[memoryMaximumKey] if !exists { return nil, fmt.Errorf("parsing memory information: %w, key=%s", ErrKeyNotFound, memoryMaximumKey) } memoryAvailable, exists := pairs[memoryAvailableKey] if !exists { return nil, fmt.Errorf("parsing memory information: %w, key=%s", ErrKeyNotFound, memoryAvailableKey) } memoryCurrent := memoryMaximum - memoryAvailable return &MemoryInformation{memoryMaximum, memoryCurrent}, nil } func RawSystemInformation(osInfoRaw string, memoryInfoRaw string, fdInfoRaw string, disksRaw string) string { var builder strings.Builder formatInfo := func(info string, builder *strings.Builder) { if info == "" { builder.WriteString("No information\n") } else { builder.WriteString(info) builder.WriteString("\n") } } builder.WriteString("---BEGIN Operating system information\n") formatInfo(osInfoRaw, &builder) builder.WriteString("---END Operating system information\n") builder.WriteString("---BEGIN Memory information\n") formatInfo(memoryInfoRaw, &builder) builder.WriteString("---END Memory information\n") builder.WriteString("---BEGIN File descriptors information\n") formatInfo(fdInfoRaw, &builder) builder.WriteString("---END File descriptors information\n") builder.WriteString("---BEGIN Disks information\n") formatInfo(disksRaw, &builder) builder.WriteString("---END Disks information\n") rawInformation := builder.String() return rawInformation } func collectDiskVolumeInformationUnix(ctx context.Context) ([]*DiskVolumeInformation, string, error) { command := exec.CommandContext(ctx, "df", "-k") stdout, err := command.Output() if err != nil { return nil, "", fmt.Errorf("error retrieving output from command '%s': %w", command.String(), err) } output := string(stdout) disks, err := ParseDiskVolumeInformationOutput(output, 1, 1) if err != nil { return nil, output, err } // returning raw output in case other collected information // resulted in errors return disks, output, nil } func collectOSInformationUnix(ctx context.Context) (*OsInfo, string, error) { command := exec.CommandContext(ctx, "uname", "-a") stdout, err := command.Output() if err != nil { return nil, "", fmt.Errorf("error retrieving output from command '%s': %w", command.String(), err) } output := string(stdout) osInfo, err := ParseUnameOutput(output, runtime.GOOS) if err != nil { return nil, output, err } // returning raw output in case other collected information // resulted in errors return osInfo, output, nil } ================================================ FILE: diagnostic/system_collector_windows.go ================================================ //go:build windows package diagnostic import ( "context" "fmt" "os/exec" "runtime" "strconv" ) const kiloBytesScale = 1.0 / 1024 type SystemCollectorImpl struct { version string } func NewSystemCollectorImpl( version string, ) *SystemCollectorImpl { return &SystemCollectorImpl{ version, } } func (collector *SystemCollectorImpl) Collect(ctx context.Context) (*SystemInformation, error) { memoryInfo, memoryInfoRaw, memoryInfoErr := collectMemoryInformation(ctx) disks, disksRaw, diskErr := collectDiskVolumeInformation(ctx) osInfo, osInfoRaw, osInfoErr := collectOSInformation(ctx) var memoryMaximum, memoryCurrent, fileDescriptorMaximum, fileDescriptorCurrent uint64 var osSystem, name, osVersion, osRelease, architecture string err := SystemInformationGeneralError{ OperatingSystemInformationError: nil, MemoryInformationError: nil, FileDescriptorsInformationError: nil, DiskVolumeInformationError: nil, } if memoryInfoErr != nil { err.MemoryInformationError = SystemInformationError{ Err: memoryInfoErr, RawInfo: memoryInfoRaw, } } else { memoryMaximum = memoryInfo.MemoryMaximum memoryCurrent = memoryInfo.MemoryCurrent } if diskErr != nil { err.DiskVolumeInformationError = SystemInformationError{ Err: diskErr, RawInfo: disksRaw, } } if osInfoErr != nil { err.OperatingSystemInformationError = SystemInformationError{ Err: osInfoErr, RawInfo: osInfoRaw, } } else { osSystem = osInfo.OsSystem name = osInfo.Name osVersion = osInfo.OsVersion osRelease = osInfo.OsRelease architecture = osInfo.Architecture } cloudflaredVersion := collector.version info := NewSystemInformation( memoryMaximum, memoryCurrent, fileDescriptorMaximum, fileDescriptorCurrent, osSystem, name, osVersion, osRelease, architecture, cloudflaredVersion, runtime.Version(), runtime.GOARCH, disks, ) return info, err } func collectMemoryInformation(ctx context.Context) (*MemoryInformation, string, error) { const ( memoryTotalPrefix = "TotalVirtualMemorySize" memoryAvailablePrefix = "FreeVirtualMemory" ) command := exec.CommandContext( ctx, "powershell", "-Command", "Get-CimInstance -Class Win32_OperatingSystem | Select-Object FreeVirtualMemory, TotalVirtualMemorySize | Format-List", ) stdout, err := command.Output() if err != nil { return nil, "", fmt.Errorf("error retrieving output from command '%s': %w", command.String(), err) } output := string(stdout) // the result of the command above will return values in bytes hence // they need to be converted to kilobytes mapper := func(field string) (uint64, error) { value, err := strconv.ParseUint(field, 10, 64) return uint64(float64(value) * kiloBytesScale), err } memoryInfo, err := ParseMemoryInformationFromKV(output, memoryTotalPrefix, memoryAvailablePrefix, mapper) if err != nil { return nil, output, err } // returning raw output in case other collected information // resulted in errors return memoryInfo, output, nil } func collectDiskVolumeInformation(ctx context.Context) ([]*DiskVolumeInformation, string, error) { command := exec.CommandContext( ctx, "powershell", "-Command", "Get-CimInstance -Class Win32_LogicalDisk | Select-Object DeviceID, Size, FreeSpace") stdout, err := command.Output() if err != nil { return nil, "", fmt.Errorf("error retrieving output from command '%s': %w", command.String(), err) } output := string(stdout) disks, err := ParseDiskVolumeInformationOutput(output, 2, kiloBytesScale) if err != nil { return nil, output, err } // returning raw output in case other collected information // resulted in errors return disks, output, nil } func collectOSInformation(ctx context.Context) (*OsInfo, string, error) { const ( architecturePrefix = "OSArchitecture" osSystemPrefix = "Caption" osVersionPrefix = "Version" osReleasePrefix = "BuildNumber" namePrefix = "CSName" ) command := exec.CommandContext( ctx, "powershell", "-Command", "Get-CimInstance -Class Win32_OperatingSystem | Select-Object OSArchitecture, Caption, Version, BuildNumber, CSName | Format-List", ) stdout, err := command.Output() if err != nil { return nil, "", fmt.Errorf("error retrieving output from command '%s': %w", command.String(), err) } output := string(stdout) osInfo, err := ParseWinOperatingSystemInfo(output, architecturePrefix, osSystemPrefix, osVersionPrefix, osReleasePrefix, namePrefix) if err != nil { return nil, output, err } // returning raw output in case other collected information // resulted in errors return osInfo, output, nil } ================================================ FILE: edgediscovery/allregions/address.go ================================================ package allregions // Region contains cloudflared edge addresses. The edge is partitioned into several regions for // redundancy purposes. type AddrSet map[*EdgeAddr]UsedBy // AddrUsedBy finds the address used by the given connection in this region. // Returns nil if the connection isn't using any IP. func (a AddrSet) AddrUsedBy(connID int) *EdgeAddr { for addr, used := range a { if used.Used && used.ConnID == connID { return addr } } return nil } // AvailableAddrs counts how many unused addresses this region contains. func (a AddrSet) AvailableAddrs() int { n := 0 for _, usedby := range a { if !usedby.Used { n++ } } return n } // GetUnusedIP returns a random unused address in this region. // Returns nil if all addresses are in use. func (a AddrSet) GetUnusedIP(excluding *EdgeAddr) *EdgeAddr { for addr, usedby := range a { if !usedby.Used && addr != excluding { return addr } } return nil } // Use the address, assigning it to a proxy connection. func (a AddrSet) Use(addr *EdgeAddr, connID int) { if addr == nil { return } a[addr] = InUse(connID) } // GetAnyAddress returns an arbitrary address from the region. func (a AddrSet) GetAnyAddress() *EdgeAddr { for addr := range a { return addr } return nil } // GiveBack the address, ensuring it is no longer assigned to an IP. // Returns true if the address is in this region. func (a AddrSet) GiveBack(addr *EdgeAddr) (ok bool) { if _, ok := a[addr]; !ok { return false } a[addr] = Unused() return true } ================================================ FILE: edgediscovery/allregions/address_test.go ================================================ package allregions import ( "reflect" "testing" ) func TestAddrSet_AddrUsedBy(t *testing.T) { type args struct { connID int } tests := []struct { name string addrSet AddrSet args args want *EdgeAddr }{ { name: "happy trivial test", addrSet: AddrSet{ &addr0: InUse(0), }, args: args{connID: 0}, want: &addr0, }, { name: "sad trivial test", addrSet: AddrSet{ &addr0: InUse(0), }, args: args{connID: 1}, want: nil, }, { name: "sad test", addrSet: AddrSet{ &addr0: InUse(0), &addr1: InUse(1), &addr2: InUse(2), }, args: args{connID: 3}, want: nil, }, { name: "happy test", addrSet: AddrSet{ &addr0: InUse(0), &addr1: InUse(1), &addr2: InUse(2), }, args: args{connID: 1}, want: &addr1, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := tt.addrSet.AddrUsedBy(tt.args.connID); !reflect.DeepEqual(got, tt.want) { t.Errorf("Region.AddrUsedBy() = %v, want %v", got, tt.want) } }) } } func TestAddrSet_AvailableAddrs(t *testing.T) { tests := []struct { name string addrSet AddrSet want int }{ { name: "contains addresses", addrSet: AddrSet{ &addr0: InUse(0), &addr1: Unused(), &addr2: InUse(2), }, want: 1, }, { name: "all free", addrSet: AddrSet{ &addr0: Unused(), &addr1: Unused(), &addr2: Unused(), }, want: 3, }, { name: "all used", addrSet: AddrSet{ &addr0: InUse(0), &addr1: InUse(1), &addr2: InUse(2), }, want: 0, }, { name: "empty", addrSet: AddrSet{}, want: 0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := tt.addrSet.AvailableAddrs(); got != tt.want { t.Errorf("Region.AvailableAddrs() = %v, want %v", got, tt.want) } }) } } func TestAddrSet_GetUnusedIP(t *testing.T) { type args struct { excluding *EdgeAddr } tests := []struct { name string addrSet AddrSet args args want *EdgeAddr }{ { name: "happy test with excluding set", addrSet: AddrSet{ &addr0: Unused(), &addr1: Unused(), &addr2: InUse(2), }, args: args{excluding: &addr0}, want: &addr1, }, { name: "happy test with no excluding", addrSet: AddrSet{ &addr0: InUse(0), &addr1: Unused(), &addr2: InUse(2), }, args: args{excluding: nil}, want: &addr1, }, { name: "sad test with no excluding", addrSet: AddrSet{ &addr0: InUse(0), &addr1: InUse(1), &addr2: InUse(2), }, args: args{excluding: nil}, want: nil, }, { name: "sad test with excluding", addrSet: AddrSet{ &addr0: Unused(), &addr1: InUse(1), &addr2: InUse(2), }, args: args{excluding: &addr0}, want: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := tt.addrSet.GetUnusedIP(tt.args.excluding); !reflect.DeepEqual(got, tt.want) { t.Errorf("Region.GetUnusedIP() = %v, want %v", got, tt.want) } }) } } func TestAddrSet_GiveBack(t *testing.T) { type args struct { addr *EdgeAddr } tests := []struct { name string addrSet AddrSet args args wantOk bool availableAfter int }{ { name: "sad test with excluding", addrSet: AddrSet{ &addr1: InUse(1), }, args: args{addr: &addr1}, wantOk: true, availableAfter: 1, }, { name: "sad test with excluding", addrSet: AddrSet{ &addr1: InUse(1), }, args: args{addr: &addr2}, wantOk: false, availableAfter: 0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if gotOk := tt.addrSet.GiveBack(tt.args.addr); gotOk != tt.wantOk { t.Errorf("Region.GiveBack() = %v, want %v", gotOk, tt.wantOk) } if tt.availableAfter != tt.addrSet.AvailableAddrs() { t.Errorf("Region.AvailableAddrs() = %v, want %v", tt.addrSet.AvailableAddrs(), tt.availableAfter) } }) } } func TestAddrSet_GetAnyAddress(t *testing.T) { tests := []struct { name string addrSet AddrSet wantNil bool }{ { name: "Sad test -- GetAnyAddress should only fail if the region is empty", addrSet: AddrSet{}, wantNil: true, }, { name: "Happy test (all addresses unused)", addrSet: AddrSet{ &addr0: Unused(), }, wantNil: false, }, { name: "Happy test (GetAnyAddress can still return addresses used by proxy conns)", addrSet: AddrSet{ &addr0: InUse(2), }, wantNil: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := tt.addrSet.GetAnyAddress(); tt.wantNil != (got == nil) { t.Errorf("Region.GetAnyAddress() = %v, but should it return nil? %v", got, tt.wantNil) } }) } } ================================================ FILE: edgediscovery/allregions/discovery.go ================================================ package allregions import ( "context" "crypto/tls" "fmt" "net" "time" "github.com/pkg/errors" "github.com/rs/zerolog" "github.com/cloudflare/cloudflared/management" ) const ( // Used to discover HA origintunneld servers srvService = "v2-origintunneld" srvProto = "tcp" srvName = "argotunnel.com" // Used to fallback to DoT when we can't use the default resolver to // discover HA origintunneld servers (GitHub issue #75). dotServerName = "cloudflare-dns.com" dotServerAddr = "1.1.1.1:853" dotTimeout = 15 * time.Second logFieldAddress = "address" ) // Redeclare network functions so they can be overridden in tests. var ( netLookupSRV = net.LookupSRV netLookupIP = net.LookupIP ) // ConfigIPVersion is the selection of IP versions from config type ConfigIPVersion int8 const ( Auto ConfigIPVersion = 2 IPv4Only ConfigIPVersion = 4 IPv6Only ConfigIPVersion = 6 ) func (c ConfigIPVersion) String() string { switch c { case Auto: return "auto" case IPv4Only: return "4" case IPv6Only: return "6" default: return "" } } // IPVersion is the IP version of an EdgeAddr type EdgeIPVersion int8 const ( V4 EdgeIPVersion = 4 V6 EdgeIPVersion = 6 ) // String returns the enum's constant name. func (c EdgeIPVersion) String() string { switch c { case V4: return "4" case V6: return "6" default: return "" } } // EdgeAddr is a representation of possible ways to refer an edge location. type EdgeAddr struct { TCP *net.TCPAddr UDP *net.UDPAddr IPVersion EdgeIPVersion } // If the call to net.LookupSRV fails, try to fall back to DoT from Cloudflare directly. // // Note: Instead of DoT, we could also have used DoH. Either of these: // - directly via the JSON API (https://1.1.1.1/dns-query?ct=application/dns-json&name=_origintunneld._tcp.argotunnel.com&type=srv) // - indirectly via `tunneldns.NewUpstreamHTTPS()` // // But both of these cases miss out on a key feature from the stdlib: // // "The returned records are sorted by priority and randomized by weight within a priority." // (https://golang.org/pkg/net/#Resolver.LookupSRV) // // Does this matter? I don't know. It may someday. Let's use DoT so we don't need to worry about it. // See also: Go feature request for stdlib-supported DoH: https://github.com/golang/go/issues/27552 var fallbackLookupSRV = lookupSRVWithDOT var friendlyDNSErrorLines = []string{ `Please try the following things to diagnose this issue:`, ` 1. ensure that argotunnel.com is returning "origintunneld" service records.`, ` Run your system's equivalent of: dig srv _origintunneld._tcp.argotunnel.com`, ` 2. ensure that your DNS resolver is not returning compressed SRV records.`, ` See GitHub issue https://github.com/golang/go/issues/27546`, ` For example, you could use Cloudflare's 1.1.1.1 as your resolver:`, ` https://developers.cloudflare.com/1.1.1.1/setting-up-1.1.1.1/`, } // EdgeDiscovery implements HA service discovery lookup. func edgeDiscovery(log *zerolog.Logger, srvService string) ([][]*EdgeAddr, error) { logger := log.With().Int(management.EventTypeKey, int(management.Cloudflared)).Logger() logger.Debug(). Int(management.EventTypeKey, int(management.Cloudflared)). Str("domain", "_"+srvService+"._"+srvProto+"."+srvName). Msg("edge discovery: looking up edge SRV record") _, addrs, err := netLookupSRV(srvService, srvProto, srvName) if err != nil { _, fallbackAddrs, fallbackErr := fallbackLookupSRV(srvService, srvProto, srvName) if fallbackErr != nil || len(fallbackAddrs) == 0 { // use the original DNS error `err` in messages, not `fallbackErr` logger.Err(err).Msg("edge discovery: error looking up Cloudflare edge IPs: the DNS query failed") for _, s := range friendlyDNSErrorLines { logger.Error().Msg(s) } return nil, errors.Wrapf(err, "Could not lookup srv records on _%v._%v.%v", srvService, srvProto, srvName) } // Accept the fallback results and keep going addrs = fallbackAddrs } var resolvedAddrPerCNAME [][]*EdgeAddr for _, addr := range addrs { edgeAddrs, err := resolveSRV(addr) if err != nil { return nil, err } logAddrs := make([]string, len(edgeAddrs)) for i, e := range edgeAddrs { logAddrs[i] = e.UDP.IP.String() } logger.Debug(). Strs("addresses", logAddrs). Msg("edge discovery: resolved edge addresses") resolvedAddrPerCNAME = append(resolvedAddrPerCNAME, edgeAddrs) } return resolvedAddrPerCNAME, nil } func lookupSRVWithDOT(srvService string, srvProto string, srvName string) (cname string, addrs []*net.SRV, err error) { // Inspiration: https://github.com/artyom/dot/blob/master/dot.go r := &net.Resolver{ PreferGo: true, Dial: func(ctx context.Context, _ string, _ string) (net.Conn, error) { var dialer net.Dialer conn, err := dialer.DialContext(ctx, "tcp", dotServerAddr) if err != nil { return nil, err } tlsConfig := &tls.Config{ServerName: dotServerName} return tls.Client(conn, tlsConfig), nil }, } ctx, cancel := context.WithTimeout(context.Background(), dotTimeout) defer cancel() return r.LookupSRV(ctx, srvService, srvProto, srvName) } func resolveSRV(srv *net.SRV) ([]*EdgeAddr, error) { ips, err := netLookupIP(srv.Target) if err != nil { return nil, errors.Wrapf(err, "Couldn't resolve SRV record %v", srv) } if len(ips) == 0 { return nil, fmt.Errorf("SRV record %v had no IPs", srv) } addrs := make([]*EdgeAddr, len(ips)) for i, ip := range ips { version := V6 if ip.To4() != nil { version = V4 } addrs[i] = &EdgeAddr{ TCP: &net.TCPAddr{IP: ip, Port: int(srv.Port)}, UDP: &net.UDPAddr{IP: ip, Port: int(srv.Port)}, IPVersion: version, } } return addrs, nil } // ResolveAddrs resolves TCP address given a list of addresses. Address can be a hostname, however, it will return at most one // of the hostname's IP addresses. func ResolveAddrs(addrs []string, log *zerolog.Logger) (resolved []*EdgeAddr) { for _, addr := range addrs { tcpAddr, err := net.ResolveTCPAddr("tcp", addr) if err != nil { log.Error().Int(management.EventTypeKey, int(management.Cloudflared)). Str(logFieldAddress, addr).Err(err).Msg("edge discovery: failed to resolve to TCP address") continue } udpAddr, err := net.ResolveUDPAddr("udp", addr) if err != nil { log.Error().Int(management.EventTypeKey, int(management.Cloudflared)). Str(logFieldAddress, addr).Err(err).Msg("edge discovery: failed to resolve to UDP address") continue } version := V6 if udpAddr.IP.To4() != nil { version = V4 } resolved = append(resolved, &EdgeAddr{ TCP: tcpAddr, UDP: udpAddr, IPVersion: version, }) } return } ================================================ FILE: edgediscovery/allregions/discovery_test.go ================================================ package allregions import ( "fmt" "testing" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" ) func (ea *EdgeAddr) String() string { return fmt.Sprintf("%s-%s", ea.TCP, ea.UDP) } func TestEdgeDiscovery(t *testing.T) { mockAddrs := newMockAddrs(19, 2, 5) netLookupSRV = mockNetLookupSRV(mockAddrs) netLookupIP = mockNetLookupIP(mockAddrs) expectedAddrSet := map[string]bool{} for _, addrs := range mockAddrs.addrMap { for _, addr := range addrs { expectedAddrSet[addr.String()] = true } } l := zerolog.Nop() addrLists, err := edgeDiscovery(&l, "") assert.NoError(t, err) actualAddrSet := map[string]bool{} for _, addrs := range addrLists { for _, addr := range addrs { actualAddrSet[addr.String()] = true } } assert.Equal(t, expectedAddrSet, actualAddrSet) } ================================================ FILE: edgediscovery/allregions/mocks_for_test.go ================================================ package allregions import ( "fmt" "math" "math/rand" "net" "reflect" "testing/quick" ) var ( v4Addrs = []*EdgeAddr{&addr0, &addr1, &addr2, &addr3} v6Addrs = []*EdgeAddr{&addr4, &addr5, &addr6, &addr7} addr0 = EdgeAddr{ TCP: &net.TCPAddr{ IP: net.ParseIP("123.4.5.0"), Port: 8000, Zone: "", }, UDP: &net.UDPAddr{ IP: net.ParseIP("123.4.5.0"), Port: 8000, Zone: "", }, IPVersion: V4, } addr1 = EdgeAddr{ TCP: &net.TCPAddr{ IP: net.ParseIP("123.4.5.1"), Port: 8000, Zone: "", }, UDP: &net.UDPAddr{ IP: net.ParseIP("123.4.5.1"), Port: 8000, Zone: "", }, IPVersion: V4, } addr2 = EdgeAddr{ TCP: &net.TCPAddr{ IP: net.ParseIP("123.4.5.2"), Port: 8000, Zone: "", }, UDP: &net.UDPAddr{ IP: net.ParseIP("123.4.5.2"), Port: 8000, Zone: "", }, IPVersion: V4, } addr3 = EdgeAddr{ TCP: &net.TCPAddr{ IP: net.ParseIP("123.4.5.3"), Port: 8000, Zone: "", }, UDP: &net.UDPAddr{ IP: net.ParseIP("123.4.5.3"), Port: 8000, Zone: "", }, IPVersion: V4, } addr4 = EdgeAddr{ TCP: &net.TCPAddr{ IP: net.ParseIP("2606:4700:a0::1"), Port: 8000, Zone: "", }, UDP: &net.UDPAddr{ IP: net.ParseIP("2606:4700:a0::1"), Port: 8000, Zone: "", }, IPVersion: V6, } addr5 = EdgeAddr{ TCP: &net.TCPAddr{ IP: net.ParseIP("2606:4700:a0::2"), Port: 8000, Zone: "", }, UDP: &net.UDPAddr{ IP: net.ParseIP("2606:4700:a0::2"), Port: 8000, Zone: "", }, IPVersion: V6, } addr6 = EdgeAddr{ TCP: &net.TCPAddr{ IP: net.ParseIP("2606:4700:a0::3"), Port: 8000, Zone: "", }, UDP: &net.UDPAddr{ IP: net.ParseIP("2606:4700:a0::3"), Port: 8000, Zone: "", }, IPVersion: V6, } addr7 = EdgeAddr{ TCP: &net.TCPAddr{ IP: net.ParseIP("2606:4700:a0::4"), Port: 8000, Zone: "", }, UDP: &net.UDPAddr{ IP: net.ParseIP("2606:4700:a0::4"), Port: 8000, Zone: "", }, IPVersion: V6, } ) type mockAddrs struct { // a set of synthetic SRV records addrMap map[net.SRV][]*EdgeAddr // the total number of addresses, aggregated across addrMap. // For the convenience of test code that would otherwise have to compute // this by hand every time. numAddrs int } func newMockAddrs(port uint16, numRegions uint8, numAddrsPerRegion uint8) mockAddrs { addrMap := make(map[net.SRV][]*EdgeAddr) numAddrs := 0 for r := uint8(0); r < numRegions; r++ { var ( srv = net.SRV{Target: fmt.Sprintf("test-region-%v.example.com", r), Port: port} addrs []*EdgeAddr ) for a := uint8(0); a < numAddrsPerRegion; a++ { tcpAddr := &net.TCPAddr{ IP: net.ParseIP(fmt.Sprintf("10.0.%v.%v", r, a)), Port: int(port), } udpAddr := &net.UDPAddr{ IP: net.ParseIP(fmt.Sprintf("10.0.%v.%v", r, a)), Port: int(port), } addrs = append(addrs, &EdgeAddr{tcpAddr, udpAddr, V4}) } addrMap[srv] = addrs numAddrs += len(addrs) } return mockAddrs{addrMap: addrMap, numAddrs: numAddrs} } var _ quick.Generator = mockAddrs{} func (mockAddrs) Generate(rand *rand.Rand, size int) reflect.Value { port := uint16(rand.Intn(math.MaxUint16)) numRegions := uint8(1 + rand.Intn(10)) numAddrsPerRegion := uint8(1 + rand.Intn(32)) result := newMockAddrs(port, numRegions, numAddrsPerRegion) return reflect.ValueOf(result) } // Returns a function compatible with net.LookupSRV that will return the SRV // records from mockAddrs. func mockNetLookupSRV( m mockAddrs, ) func(service, proto, name string) (cname string, addrs []*net.SRV, err error) { var addrs []*net.SRV for k := range m.addrMap { addr := k addrs = append(addrs, &addr) // We can't just do // addrs = append(addrs, &k) // `k` will be reused by subsequent loop iterations, // so all the copies of `&k` would point to the same location. } return func(_, _, _ string) (string, []*net.SRV, error) { return "", addrs, nil } } // Returns a function compatible with net.LookupIP that translates the SRV records // from mockAddrs into IP addresses, based on the TCP addresses in mockAddrs. func mockNetLookupIP( m mockAddrs, ) func(host string) ([]net.IP, error) { return func(host string) ([]net.IP, error) { for srv, addrs := range m.addrMap { if srv.Target != host { continue } result := make([]net.IP, len(addrs)) for i, addr := range addrs { result[i] = addr.TCP.IP } return result, nil } return nil, fmt.Errorf("No IPs for %v", host) } } ================================================ FILE: edgediscovery/allregions/region.go ================================================ package allregions import "time" const ( timeoutDuration = 10 * time.Minute ) // Region contains cloudflared edge addresses. The edge is partitioned into several regions for // redundancy purposes. type Region struct { primaryIsActive bool active AddrSet primary AddrSet secondary AddrSet primaryTimeout time.Time timeoutDuration time.Duration } // NewRegion creates a region with the given addresses, which are all unused. func NewRegion(addrs []*EdgeAddr, overrideIPVersion ConfigIPVersion) Region { // The zero value of UsedBy is Unused(), so we can just initialize the map's values with their // zero values. connForv4 := make(AddrSet) connForv6 := make(AddrSet) systemPreference := V6 for i, addr := range addrs { if i == 0 { // First family of IPs returned is system preference of IP systemPreference = addr.IPVersion } switch addr.IPVersion { case V4: connForv4[addr] = Unused() case V6: connForv6[addr] = Unused() } } // Process as system preference var primary AddrSet var secondary AddrSet switch systemPreference { case V4: primary = connForv4 secondary = connForv6 case V6: primary = connForv6 secondary = connForv4 } // Override with provided preference switch overrideIPVersion { case IPv4Only: primary = connForv4 secondary = make(AddrSet) // empty case IPv6Only: primary = connForv6 secondary = make(AddrSet) // empty case Auto: // no change default: // no change } return Region{ primaryIsActive: true, active: primary, primary: primary, secondary: secondary, timeoutDuration: timeoutDuration, } } // AddrUsedBy finds the address used by the given connection in this region. // Returns nil if the connection isn't using any IP. func (r *Region) AddrUsedBy(connID int) *EdgeAddr { edgeAddr := r.primary.AddrUsedBy(connID) if edgeAddr == nil { edgeAddr = r.secondary.AddrUsedBy(connID) } return edgeAddr } // AvailableAddrs counts how many unused addresses this region contains. func (r Region) AvailableAddrs() int { return r.active.AvailableAddrs() } // AssignAnyAddress returns a random unused address in this region now // assigned to the connID excluding the provided EdgeAddr. // Returns nil if all addresses are in use for the region. func (r Region) AssignAnyAddress(connID int, excluding *EdgeAddr) *EdgeAddr { if addr := r.active.GetUnusedIP(excluding); addr != nil { r.active.Use(addr, connID) return addr } return nil } // GetAnyAddress returns an arbitrary address from the region. func (r Region) GetAnyAddress() *EdgeAddr { return r.active.GetAnyAddress() } // GiveBack the address, ensuring it is no longer assigned to an IP. // Returns true if the address is in this region. func (r *Region) GiveBack(addr *EdgeAddr, hasConnectivityError bool) (ok bool) { if ok = r.primary.GiveBack(addr); !ok { // Attempt to give back the address in the secondary set if ok = r.secondary.GiveBack(addr); !ok { // Address is not in this region return } } // No connectivity error: no worry if !hasConnectivityError { return } // If using primary and returned address is IPv6 and secondary is available if r.primaryIsActive && addr.IPVersion == V6 && len(r.secondary) > 0 { r.active = r.secondary r.primaryIsActive = false r.primaryTimeout = time.Now().Add(r.timeoutDuration) return } // Do nothing for IPv4 or if secondary is empty if r.primaryIsActive { return } // Immediately return to primary pool, regardless of current primary timeout if addr.IPVersion == V4 { activatePrimary(r) return } // Timeout exceeded and can be reset to primary pool if r.primaryTimeout.Before(time.Now()) { activatePrimary(r) return } return } // activatePrimary sets the primary set to the active set and resets the timeout. func activatePrimary(r *Region) { r.active = r.primary r.primaryIsActive = true r.primaryTimeout = time.Now() // reset timeout } ================================================ FILE: edgediscovery/allregions/region_test.go ================================================ package allregions import ( "net" "testing" "time" "github.com/stretchr/testify/assert" ) func makeAddrSet(addrs []*EdgeAddr) AddrSet { addrSet := make(AddrSet, len(addrs)) for _, addr := range addrs { addrSet[addr] = Unused() } return addrSet } func TestRegion_New(t *testing.T) { tests := []struct { name string addrs []*EdgeAddr mode ConfigIPVersion expectedAddrs int primary AddrSet secondary AddrSet }{ { name: "IPv4 addresses with IPv4Only", addrs: v4Addrs, mode: IPv4Only, expectedAddrs: len(v4Addrs), primary: makeAddrSet(v4Addrs), secondary: AddrSet{}, }, { name: "IPv6 addresses with IPv4Only", addrs: v6Addrs, mode: IPv4Only, expectedAddrs: 0, primary: AddrSet{}, secondary: AddrSet{}, }, { name: "IPv6 addresses with IPv6Only", addrs: v6Addrs, mode: IPv6Only, expectedAddrs: len(v6Addrs), primary: makeAddrSet(v6Addrs), secondary: AddrSet{}, }, { name: "IPv6 addresses with IPv4Only", addrs: v6Addrs, mode: IPv4Only, expectedAddrs: 0, primary: AddrSet{}, secondary: AddrSet{}, }, { name: "IPv4 (first) and IPv6 addresses with Auto", addrs: append(v4Addrs, v6Addrs...), mode: Auto, expectedAddrs: len(v4Addrs), primary: makeAddrSet(v4Addrs), secondary: makeAddrSet(v6Addrs), }, { name: "IPv6 (first) and IPv4 addresses with Auto", addrs: append(v6Addrs, v4Addrs...), mode: Auto, expectedAddrs: len(v6Addrs), primary: makeAddrSet(v6Addrs), secondary: makeAddrSet(v4Addrs), }, { name: "IPv4 addresses with Auto", addrs: v4Addrs, mode: Auto, expectedAddrs: len(v4Addrs), primary: makeAddrSet(v4Addrs), secondary: AddrSet{}, }, { name: "IPv6 addresses with Auto", addrs: v6Addrs, mode: Auto, expectedAddrs: len(v6Addrs), primary: makeAddrSet(v6Addrs), secondary: AddrSet{}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := NewRegion(tt.addrs, tt.mode) assert.Equal(t, tt.expectedAddrs, r.AvailableAddrs()) assert.Equal(t, tt.primary, r.primary) assert.Equal(t, tt.secondary, r.secondary) }) } } func TestRegion_AnyAddress_EmptyActiveSet(t *testing.T) { tests := []struct { name string addrs []*EdgeAddr mode ConfigIPVersion }{ { name: "IPv6 addresses with IPv4Only", addrs: v6Addrs, mode: IPv4Only, }, { name: "IPv4 addresses with IPv6Only", addrs: v4Addrs, mode: IPv6Only, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := NewRegion(tt.addrs, tt.mode) addr := r.GetAnyAddress() assert.Nil(t, addr) addr = r.AssignAnyAddress(0, nil) assert.Nil(t, addr) }) } } func TestRegion_AssignAnyAddress_FullyUsedActiveSet(t *testing.T) { tests := []struct { name string addrs []*EdgeAddr mode ConfigIPVersion }{ { name: "IPv6 addresses with IPv6Only", addrs: v6Addrs, mode: IPv6Only, }, { name: "IPv4 addresses with IPv4Only", addrs: v4Addrs, mode: IPv4Only, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := NewRegion(tt.addrs, tt.mode) total := r.active.AvailableAddrs() for i := 0; i < total; i++ { addr := r.AssignAnyAddress(i, nil) assert.NotNil(t, addr) } addr := r.AssignAnyAddress(9, nil) assert.Nil(t, addr) }) } } var giveBackTests = []struct { name string addrs []*EdgeAddr mode ConfigIPVersion expectedAddrs int primary AddrSet secondary AddrSet primarySwap bool }{ { name: "IPv4 addresses with IPv4Only", addrs: v4Addrs, mode: IPv4Only, expectedAddrs: len(v4Addrs), primary: makeAddrSet(v4Addrs), secondary: AddrSet{}, primarySwap: false, }, { name: "IPv6 addresses with IPv6Only", addrs: v6Addrs, mode: IPv6Only, expectedAddrs: len(v6Addrs), primary: makeAddrSet(v6Addrs), secondary: AddrSet{}, primarySwap: false, }, { name: "IPv4 (first) and IPv6 addresses with Auto", addrs: append(v4Addrs, v6Addrs...), mode: Auto, expectedAddrs: len(v4Addrs), primary: makeAddrSet(v4Addrs), secondary: makeAddrSet(v6Addrs), primarySwap: false, }, { name: "IPv6 (first) and IPv4 addresses with Auto", addrs: append(v6Addrs, v4Addrs...), mode: Auto, expectedAddrs: len(v6Addrs), primary: makeAddrSet(v6Addrs), secondary: makeAddrSet(v4Addrs), primarySwap: true, }, { name: "IPv4 addresses with Auto", addrs: v4Addrs, mode: Auto, expectedAddrs: len(v4Addrs), primary: makeAddrSet(v4Addrs), secondary: AddrSet{}, primarySwap: false, }, { name: "IPv6 addresses with Auto", addrs: v6Addrs, mode: Auto, expectedAddrs: len(v6Addrs), primary: makeAddrSet(v6Addrs), secondary: AddrSet{}, primarySwap: false, }, } func TestRegion_GiveBack_NoConnectivityError(t *testing.T) { for _, tt := range giveBackTests { t.Run(tt.name, func(t *testing.T) { r := NewRegion(tt.addrs, tt.mode) addr := r.AssignAnyAddress(0, nil) assert.NotNil(t, addr) assert.True(t, r.GiveBack(addr, false)) }) } } func TestRegion_GiveBack_ForeignAddr(t *testing.T) { invalid := EdgeAddr{ TCP: &net.TCPAddr{ IP: net.ParseIP("123.4.5.0"), Port: 8000, Zone: "", }, UDP: &net.UDPAddr{ IP: net.ParseIP("123.4.5.0"), Port: 8000, Zone: "", }, IPVersion: V4, } for _, tt := range giveBackTests { t.Run(tt.name, func(t *testing.T) { r := NewRegion(tt.addrs, tt.mode) assert.False(t, r.GiveBack(&invalid, false)) assert.False(t, r.GiveBack(&invalid, true)) }) } } func TestRegion_GiveBack_SwapPrimary(t *testing.T) { for _, tt := range giveBackTests { t.Run(tt.name, func(t *testing.T) { r := NewRegion(tt.addrs, tt.mode) addr := r.AssignAnyAddress(0, nil) assert.NotNil(t, addr) assert.True(t, r.GiveBack(addr, true)) assert.Equal(t, tt.primarySwap, !r.primaryIsActive) if tt.primarySwap { assert.Equal(t, r.secondary, r.active) assert.False(t, r.primaryTimeout.IsZero()) } else { assert.Equal(t, r.primary, r.active) assert.True(t, r.primaryTimeout.IsZero()) } }) } } func TestRegion_GiveBack_IPv4_ResetPrimary(t *testing.T) { r := NewRegion(append(v6Addrs, v4Addrs...), Auto) // Exhaust all IPv6 addresses a0 := r.AssignAnyAddress(0, nil) a1 := r.AssignAnyAddress(1, nil) a2 := r.AssignAnyAddress(2, nil) a3 := r.AssignAnyAddress(3, nil) assert.NotNil(t, a0) assert.NotNil(t, a1) assert.NotNil(t, a2) assert.NotNil(t, a3) // Give back the first IPv6 address to fallback to secondary IPv4 address set assert.True(t, r.GiveBack(a0, true)) assert.False(t, r.primaryIsActive) // Give back another IPv6 address assert.True(t, r.GiveBack(a1, true)) // Primary shouldn't change assert.False(t, r.primaryIsActive) // Request an address (should be IPv4 from secondary) a4_v4 := r.AssignAnyAddress(4, nil) assert.NotNil(t, a4_v4) assert.Equal(t, V4, a4_v4.IPVersion) a5_v4 := r.AssignAnyAddress(5, nil) assert.NotNil(t, a5_v4) assert.Equal(t, V4, a5_v4.IPVersion) a6_v4 := r.AssignAnyAddress(6, nil) assert.NotNil(t, a6_v4) assert.Equal(t, V4, a6_v4.IPVersion) // Return IPv4 address (without failure) // Primary shouldn't change because it is not a connectivity failure assert.True(t, r.GiveBack(a4_v4, false)) assert.False(t, r.primaryIsActive) // Return IPv4 address (with failure) // Primary should change because it is a connectivity failure assert.True(t, r.GiveBack(a5_v4, true)) assert.True(t, r.primaryIsActive) // Return IPv4 address (with failure) // Primary shouldn't change because the address is returned to the inactive // secondary address set assert.True(t, r.GiveBack(a6_v4, true)) assert.True(t, r.primaryIsActive) // Return IPv6 address (without failure) // Primary shoudn't change because it is not a connectivity failure assert.True(t, r.GiveBack(a2, false)) assert.True(t, r.primaryIsActive) } func TestRegion_GiveBack_Timeout(t *testing.T) { r := NewRegion(append(v6Addrs, v4Addrs...), Auto) a0 := r.AssignAnyAddress(0, nil) a1 := r.AssignAnyAddress(1, nil) a2 := r.AssignAnyAddress(2, nil) assert.NotNil(t, a0) assert.NotNil(t, a1) assert.NotNil(t, a2) // Give back IPv6 address to set timeout assert.True(t, r.GiveBack(a0, true)) assert.False(t, r.primaryIsActive) assert.False(t, r.primaryTimeout.IsZero()) // Request an address (should be IPv4 from secondary) a3_v4 := r.AssignAnyAddress(3, nil) assert.NotNil(t, a3_v4) assert.Equal(t, V4, a3_v4.IPVersion) assert.False(t, r.primaryIsActive) // Give back IPv6 address inside timeout (no change) assert.True(t, r.GiveBack(a2, true)) assert.False(t, r.primaryIsActive) assert.False(t, r.primaryTimeout.IsZero()) // Accelerate timeout r.primaryTimeout = time.Now().Add(-time.Minute) // Return IPv6 address assert.True(t, r.GiveBack(a1, true)) assert.True(t, r.primaryIsActive) // Returning an IPv4 address after primary is active shouldn't change primary // even with a connectivity error assert.True(t, r.GiveBack(a3_v4, true)) assert.True(t, r.primaryIsActive) } ================================================ FILE: edgediscovery/allregions/regions.go ================================================ package allregions import ( "fmt" "math/rand" "github.com/rs/zerolog" ) // Regions stores Cloudflare edge network IPs, partitioned into two regions. // This is NOT thread-safe. Users of this package should use it with a lock. type Regions struct { region1 Region region2 Region } // ------------------------------------ // Constructors // ------------------------------------ // ResolveEdge resolves the Cloudflare edge, returning all regions discovered. func ResolveEdge(log *zerolog.Logger, region string, overrideIPVersion ConfigIPVersion) (*Regions, error) { edgeAddrs, err := edgeDiscovery(log, getRegionalServiceName(region)) if err != nil { return nil, err } if len(edgeAddrs) < 2 { return nil, fmt.Errorf("expected at least 2 Cloudflare Regions regions, but SRV only returned %v", len(edgeAddrs)) } return &Regions{ region1: NewRegion(edgeAddrs[0], overrideIPVersion), region2: NewRegion(edgeAddrs[1], overrideIPVersion), }, nil } // StaticEdge creates a list of edge addresses from the list of hostnames. // Mainly used for testing connectivity. func StaticEdge(hostnames []string, log *zerolog.Logger) (*Regions, error) { resolved := ResolveAddrs(hostnames, log) if len(resolved) == 0 { return nil, fmt.Errorf("failed to resolve any edge address") } return NewNoResolve(resolved), nil } // NewNoResolve doesn't resolve the edge. Instead it just uses the given addresses. // You probably only need this for testing. func NewNoResolve(addrs []*EdgeAddr) *Regions { region1 := make([]*EdgeAddr, 0) region2 := make([]*EdgeAddr, 0) for i, v := range addrs { if i%2 == 0 { region1 = append(region1, v) } else { region2 = append(region2, v) } } return &Regions{ region1: NewRegion(region1, Auto), region2: NewRegion(region2, Auto), } } // ------------------------------------ // Methods // ------------------------------------ // GetAnyAddress returns an arbitrary address from the larger region. func (rs *Regions) GetAnyAddress() *EdgeAddr { if addr := rs.region1.GetAnyAddress(); addr != nil { return addr } return rs.region2.GetAnyAddress() } // AddrUsedBy finds the address used by the given connection. // Returns nil if the connection isn't using an address. func (rs *Regions) AddrUsedBy(connID int) *EdgeAddr { if addr := rs.region1.AddrUsedBy(connID); addr != nil { return addr } return rs.region2.AddrUsedBy(connID) } // GetUnusedAddr gets an unused addr from the edge, excluding the given addr. Prefer to use addresses // evenly across both regions. func (rs *Regions) GetUnusedAddr(excluding *EdgeAddr, connID int) *EdgeAddr { // If both regions have the same number of available addrs, lets randomise which one // we pick. The rest of this algorithm will continue to make sure we always use addresses // evenly across both regions. if rs.region1.AvailableAddrs() == rs.region2.AvailableAddrs() { regions := []Region{rs.region1, rs.region2} firstChoice := rand.Intn(2) return getAddrs(excluding, connID, ®ions[firstChoice], ®ions[1-firstChoice]) } if rs.region1.AvailableAddrs() > rs.region2.AvailableAddrs() { return getAddrs(excluding, connID, &rs.region1, &rs.region2) } return getAddrs(excluding, connID, &rs.region2, &rs.region1) } // getAddrs tries to grab address form `first` region, then `second` region // this is an unrolled loop over 2 element array func getAddrs(excluding *EdgeAddr, connID int, first *Region, second *Region) *EdgeAddr { addr := first.AssignAnyAddress(connID, excluding) if addr != nil { return addr } addr = second.AssignAnyAddress(connID, excluding) if addr != nil { return addr } return nil } // AvailableAddrs returns how many edge addresses aren't used. func (rs *Regions) AvailableAddrs() int { return rs.region1.AvailableAddrs() + rs.region2.AvailableAddrs() } // GiveBack the address so that other connections can use it. // Returns true if the address is in this edge. func (rs *Regions) GiveBack(addr *EdgeAddr, hasConnectivityError bool) bool { if found := rs.region1.GiveBack(addr, hasConnectivityError); found { return found } return rs.region2.GiveBack(addr, hasConnectivityError) } // Return regionalized service name if `region` isn't empty, otherwise return the global service name for origintunneld func getRegionalServiceName(region string) string { if region != "" { return region + "-" + srvService // Example: `us-v2-origintunneld` } return srvService // Global service is just `v2-origintunneld` } ================================================ FILE: edgediscovery/allregions/regions_test.go ================================================ package allregions import ( "testing" "github.com/stretchr/testify/assert" ) func makeRegions(addrs []*EdgeAddr, mode ConfigIPVersion) Regions { r1addrs := make([]*EdgeAddr, 0) r2addrs := make([]*EdgeAddr, 0) for i, addr := range addrs { if i%2 == 0 { r1addrs = append(r1addrs, addr) } else { r2addrs = append(r2addrs, addr) } } r1 := NewRegion(r1addrs, mode) r2 := NewRegion(r2addrs, mode) return Regions{region1: r1, region2: r2} } func TestRegions_AddrUsedBy(t *testing.T) { tests := []struct { name string addrs []*EdgeAddr mode ConfigIPVersion }{ { name: "IPv4 addresses with IPv4Only", addrs: v4Addrs, mode: IPv4Only, }, { name: "IPv6 addresses with IPv6Only", addrs: v6Addrs, mode: IPv6Only, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { rs := makeRegions(tt.addrs, tt.mode) addr1 := rs.GetUnusedAddr(nil, 1) assert.Equal(t, addr1, rs.AddrUsedBy(1)) addr2 := rs.GetUnusedAddr(nil, 2) assert.Equal(t, addr2, rs.AddrUsedBy(2)) addr3 := rs.GetUnusedAddr(nil, 3) assert.Equal(t, addr3, rs.AddrUsedBy(3)) }) } } func TestRegions_Giveback_Region1(t *testing.T) { tests := []struct { name string addrs []*EdgeAddr mode ConfigIPVersion }{ { name: "IPv4 addresses with IPv4Only", addrs: v4Addrs, mode: IPv4Only, }, { name: "IPv6 addresses with IPv6Only", addrs: v6Addrs, mode: IPv6Only, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { rs := makeRegions(tt.addrs, tt.mode) addr := rs.region1.AssignAnyAddress(0, nil) rs.region1.AssignAnyAddress(1, nil) rs.region2.AssignAnyAddress(2, nil) rs.region2.AssignAnyAddress(3, nil) assert.Equal(t, 0, rs.AvailableAddrs()) rs.GiveBack(addr, false) assert.Equal(t, addr, rs.GetUnusedAddr(nil, 0)) }) } } func TestRegions_Giveback_Region2(t *testing.T) { tests := []struct { name string addrs []*EdgeAddr mode ConfigIPVersion }{ { name: "IPv4 addresses with IPv4Only", addrs: v4Addrs, mode: IPv4Only, }, { name: "IPv6 addresses with IPv6Only", addrs: v6Addrs, mode: IPv6Only, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { rs := makeRegions(tt.addrs, tt.mode) rs.region1.AssignAnyAddress(0, nil) rs.region1.AssignAnyAddress(1, nil) addr := rs.region2.AssignAnyAddress(2, nil) rs.region2.AssignAnyAddress(3, nil) assert.Equal(t, 0, rs.AvailableAddrs()) rs.GiveBack(addr, false) assert.Equal(t, addr, rs.GetUnusedAddr(nil, 2)) }) } } func TestRegions_GetUnusedAddr_OneAddrLeft(t *testing.T) { tests := []struct { name string addrs []*EdgeAddr mode ConfigIPVersion }{ { name: "IPv4 addresses with IPv4Only", addrs: v4Addrs, mode: IPv4Only, }, { name: "IPv6 addresses with IPv6Only", addrs: v6Addrs, mode: IPv6Only, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { rs := makeRegions(tt.addrs, tt.mode) rs.region1.AssignAnyAddress(0, nil) rs.region1.AssignAnyAddress(1, nil) rs.region2.AssignAnyAddress(2, nil) addr := rs.region2.active.GetUnusedIP(nil) assert.Equal(t, 1, rs.AvailableAddrs()) assert.Equal(t, addr, rs.GetUnusedAddr(nil, 3)) }) } } func TestRegions_GetUnusedAddr_Excluding_Region1(t *testing.T) { tests := []struct { name string addrs []*EdgeAddr mode ConfigIPVersion }{ { name: "IPv4 addresses with IPv4Only", addrs: v4Addrs, mode: IPv4Only, }, { name: "IPv6 addresses with IPv6Only", addrs: v6Addrs, mode: IPv6Only, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { rs := makeRegions(tt.addrs, tt.mode) rs.region1.AssignAnyAddress(0, nil) rs.region1.AssignAnyAddress(1, nil) addr := rs.region2.active.GetUnusedIP(nil) a2 := rs.region2.active.GetUnusedIP(addr) assert.Equal(t, 2, rs.AvailableAddrs()) assert.Equal(t, addr, rs.GetUnusedAddr(a2, 3)) }) } } func TestRegions_GetUnusedAddr_Excluding_Region2(t *testing.T) { tests := []struct { name string addrs []*EdgeAddr mode ConfigIPVersion }{ { name: "IPv4 addresses with IPv4Only", addrs: v4Addrs, mode: IPv4Only, }, { name: "IPv6 addresses with IPv6Only", addrs: v6Addrs, mode: IPv6Only, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { rs := makeRegions(tt.addrs, tt.mode) rs.region2.AssignAnyAddress(0, nil) rs.region2.AssignAnyAddress(1, nil) addr := rs.region1.active.GetUnusedIP(nil) a2 := rs.region1.active.GetUnusedIP(addr) assert.Equal(t, 2, rs.AvailableAddrs()) assert.Equal(t, addr, rs.GetUnusedAddr(a2, 1)) }) } } func TestNewNoResolveBalancesRegions(t *testing.T) { type args struct { addrs []*EdgeAddr } tests := []struct { name string args args }{ { name: "one address", args: args{addrs: []*EdgeAddr{&addr0}}, }, { name: "two addresses", args: args{addrs: []*EdgeAddr{&addr0, &addr1}}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { regions := NewNoResolve(tt.args.addrs) RegionsIsBalanced(t, regions) }) } } func TestGetRegionalServiceName(t *testing.T) { // Empty region should just go to origintunneld globalServiceName := getRegionalServiceName("") assert.Equal(t, srvService, globalServiceName) // Non-empty region should go to the regional origintunneld variant for _, region := range []string{"us", "pt", "am"} { regionalServiceName := getRegionalServiceName(region) assert.Equal(t, region+"-"+srvService, regionalServiceName) } } func RegionsIsBalanced(t *testing.T, rs *Regions) { delta := rs.region1.AvailableAddrs() - rs.region2.AvailableAddrs() assert.True(t, abs(delta) <= 1) } func abs(x int) int { if x >= 0 { return x } return -x } ================================================ FILE: edgediscovery/allregions/usedby.go ================================================ package allregions type UsedBy struct { ConnID int Used bool } func InUse(connID int) UsedBy { return UsedBy{ConnID: connID, Used: true} } func Unused() UsedBy { return UsedBy{} } ================================================ FILE: edgediscovery/dial.go ================================================ package edgediscovery import ( "context" "crypto/tls" "net" "time" "github.com/pkg/errors" ) // DialEdge makes a TLS connection to a Cloudflare edge node func DialEdge( ctx context.Context, timeout time.Duration, tlsConfig *tls.Config, edgeTCPAddr *net.TCPAddr, localIP net.IP, ) (net.Conn, error) { // Inherit from parent context so we can cancel (Ctrl-C) while dialing dialCtx, dialCancel := context.WithTimeout(ctx, timeout) defer dialCancel() dialer := net.Dialer{} if localIP != nil { dialer.LocalAddr = &net.TCPAddr{IP: localIP, Port: 0} } edgeConn, err := dialer.DialContext(dialCtx, "tcp", edgeTCPAddr.String()) if err != nil { return nil, newDialError(err, "DialContext error") } tlsEdgeConn := tls.Client(edgeConn, tlsConfig) tlsEdgeConn.SetDeadline(time.Now().Add(timeout)) if err = tlsEdgeConn.Handshake(); err != nil { return nil, newDialError(err, "TLS handshake with edge error") } // clear the deadline on the conn; http2 has its own timeouts tlsEdgeConn.SetDeadline(time.Time{}) return tlsEdgeConn, nil } // DialError is an error returned from DialEdge type DialError struct { cause error } func newDialError(err error, message string) error { return DialError{cause: errors.Wrap(err, message)} } func (e DialError) Error() string { return e.cause.Error() } func (e DialError) Cause() error { return e.cause } ================================================ FILE: edgediscovery/edgediscovery.go ================================================ package edgediscovery import ( "sync" "github.com/rs/zerolog" "github.com/cloudflare/cloudflared/edgediscovery/allregions" "github.com/cloudflare/cloudflared/management" ) const ( LogFieldConnIndex = "connIndex" LogFieldIPAddress = "ip" ) var errNoAddressesLeft = ErrNoAddressesLeft{} type ErrNoAddressesLeft struct{} func (e ErrNoAddressesLeft) Error() string { return "there are no free edge addresses left to resolve to" } // Edge finds addresses on the Cloudflare edge and hands them out to connections. type Edge struct { regions *allregions.Regions sync.Mutex log *zerolog.Logger } // ------------------------------------ // Constructors // ------------------------------------ // ResolveEdge runs the initial discovery of the Cloudflare edge, finding Addrs that can be allocated // to connections. func ResolveEdge(log *zerolog.Logger, region string, edgeIpVersion allregions.ConfigIPVersion) (*Edge, error) { regions, err := allregions.ResolveEdge(log, region, edgeIpVersion) if err != nil { return new(Edge), err } return &Edge{ log: log, regions: regions, }, nil } // StaticEdge creates a list of edge addresses from the list of hostnames. Mainly used for testing connectivity. func StaticEdge(log *zerolog.Logger, hostnames []string) (*Edge, error) { regions, err := allregions.StaticEdge(hostnames, log) if err != nil { return new(Edge), err } return &Edge{ log: log, regions: regions, }, nil } // ------------------------------------ // Methods // ------------------------------------ // GetAddrForRPC gives this connection an edge Addr. func (ed *Edge) GetAddrForRPC() (*allregions.EdgeAddr, error) { ed.Lock() defer ed.Unlock() addr := ed.regions.GetAnyAddress() if addr == nil { return nil, errNoAddressesLeft } return addr, nil } // GetAddr gives this proxy connection an edge Addr. Prefer Addrs this connection has already used. func (ed *Edge) GetAddr(connIndex int) (*allregions.EdgeAddr, error) { log := ed.log.With(). Int(LogFieldConnIndex, connIndex). Int(management.EventTypeKey, int(management.Cloudflared)). Logger() ed.Lock() defer ed.Unlock() // If this connection has already used an edge addr, return it. if addr := ed.regions.AddrUsedBy(connIndex); addr != nil { log.Debug().IPAddr(LogFieldIPAddress, addr.UDP.IP).Msg("edge discovery: returning same edge address back to pool") return addr, nil } // Otherwise, give it an unused one addr := ed.regions.GetUnusedAddr(nil, connIndex) if addr == nil { log.Debug().Msg("edge discovery: no addresses left in pool to give proxy connection") return nil, errNoAddressesLeft } log.Debug().IPAddr(LogFieldIPAddress, addr.UDP.IP).Msg("edge discovery: giving new address to connection") return addr, nil } // GetDifferentAddr gives back the proxy connection's edge Addr and uses a new one. func (ed *Edge) GetDifferentAddr(connIndex int, hasConnectivityError bool) (*allregions.EdgeAddr, error) { log := ed.log.With(). Int(LogFieldConnIndex, connIndex). Int(management.EventTypeKey, int(management.Cloudflared)). Logger() ed.Lock() defer ed.Unlock() oldAddr := ed.regions.AddrUsedBy(connIndex) if oldAddr != nil { ed.regions.GiveBack(oldAddr, hasConnectivityError) } addr := ed.regions.GetUnusedAddr(oldAddr, connIndex) if addr == nil { log.Debug().Msg("edge discovery: no addresses left in pool to give proxy connection") // note: if oldAddr were not nil, it will become available on the next iteration return nil, errNoAddressesLeft } log.Debug(). IPAddr(LogFieldIPAddress, addr.UDP.IP). Int("available", ed.regions.AvailableAddrs()). Msg("edge discovery: giving new address to connection") return addr, nil } // AvailableAddrs returns how many unused addresses there are left. func (ed *Edge) AvailableAddrs() int { ed.Lock() defer ed.Unlock() return ed.regions.AvailableAddrs() } // GiveBack the address so that other connections can use it. // Returns true if the address is in this edge. func (ed *Edge) GiveBack(addr *allregions.EdgeAddr, hasConnectivityError bool) bool { ed.Lock() defer ed.Unlock() ed.log.Debug(). Int(management.EventTypeKey, int(management.Cloudflared)). IPAddr(LogFieldIPAddress, addr.UDP.IP). Msg("edge discovery: gave back address to the pool") return ed.regions.GiveBack(addr, hasConnectivityError) } ================================================ FILE: edgediscovery/edgediscovery_test.go ================================================ package edgediscovery import ( "net" "testing" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" "github.com/cloudflare/cloudflared/edgediscovery/allregions" ) var ( testLogger = zerolog.Nop() v4Addrs = []*allregions.EdgeAddr{&addr0, &addr1, &addr2, &addr3} v6Addrs = []*allregions.EdgeAddr{&addr4, &addr5, &addr6, &addr7} addr0 = allregions.EdgeAddr{ TCP: &net.TCPAddr{ IP: net.ParseIP("123.4.5.0"), Port: 8000, Zone: "", }, UDP: &net.UDPAddr{ IP: net.ParseIP("123.4.5.0"), Port: 8000, Zone: "", }, IPVersion: allregions.V4, } addr1 = allregions.EdgeAddr{ TCP: &net.TCPAddr{ IP: net.ParseIP("123.4.5.1"), Port: 8000, Zone: "", }, UDP: &net.UDPAddr{ IP: net.ParseIP("123.4.5.1"), Port: 8000, Zone: "", }, IPVersion: allregions.V4, } addr2 = allregions.EdgeAddr{ TCP: &net.TCPAddr{ IP: net.ParseIP("123.4.5.2"), Port: 8000, Zone: "", }, UDP: &net.UDPAddr{ IP: net.ParseIP("123.4.5.2"), Port: 8000, Zone: "", }, IPVersion: allregions.V4, } addr3 = allregions.EdgeAddr{ TCP: &net.TCPAddr{ IP: net.ParseIP("123.4.5.3"), Port: 8000, Zone: "", }, UDP: &net.UDPAddr{ IP: net.ParseIP("123.4.5.3"), Port: 8000, Zone: "", }, IPVersion: allregions.V4, } addr4 = allregions.EdgeAddr{ TCP: &net.TCPAddr{ IP: net.ParseIP("2606:4700:a0::1"), Port: 8000, Zone: "", }, UDP: &net.UDPAddr{ IP: net.ParseIP("2606:4700:a0::1"), Port: 8000, Zone: "", }, IPVersion: allregions.V6, } addr5 = allregions.EdgeAddr{ TCP: &net.TCPAddr{ IP: net.ParseIP("2606:4700:a0::2"), Port: 8000, Zone: "", }, UDP: &net.UDPAddr{ IP: net.ParseIP("2606:4700:a0::2"), Port: 8000, Zone: "", }, IPVersion: allregions.V6, } addr6 = allregions.EdgeAddr{ TCP: &net.TCPAddr{ IP: net.ParseIP("2606:4700:a0::3"), Port: 8000, Zone: "", }, UDP: &net.UDPAddr{ IP: net.ParseIP("2606:4700:a0::3"), Port: 8000, Zone: "", }, IPVersion: allregions.V6, } addr7 = allregions.EdgeAddr{ TCP: &net.TCPAddr{ IP: net.ParseIP("2606:4700:a0::4"), Port: 8000, Zone: "", }, UDP: &net.UDPAddr{ IP: net.ParseIP("2606:4700:a0::4"), Port: 8000, Zone: "", }, IPVersion: allregions.V6, } ) func TestGiveBack(t *testing.T) { edge := MockEdge(&testLogger, []*allregions.EdgeAddr{&addr0, &addr1, &addr2, &addr3}) // Give this connection an address assert.Equal(t, 4, edge.AvailableAddrs()) const connID = 0 addr, err := edge.GetAddr(connID) assert.NoError(t, err) assert.NotNil(t, addr) assert.Equal(t, 3, edge.AvailableAddrs()) // Get it back edge.GiveBack(addr, false) assert.Equal(t, 4, edge.AvailableAddrs()) } func TestRPCAndProxyShareSingleEdgeIP(t *testing.T) { // Make an edge with a single IP edge := MockEdge(&testLogger, []*allregions.EdgeAddr{&addr0}) tunnelConnID := 0 // Use the IP for a tunnel addrTunnel, err := edge.GetAddr(tunnelConnID) assert.NoError(t, err) // Ensure the IP can be used for RPC too addrRPC, err := edge.GetAddrForRPC() assert.NoError(t, err) assert.Equal(t, addrTunnel, addrRPC) } func TestGetAddrForRPC(t *testing.T) { edge := MockEdge(&testLogger, []*allregions.EdgeAddr{&addr0, &addr1, &addr2, &addr3}) // Get a connection assert.Equal(t, 4, edge.AvailableAddrs()) addr, err := edge.GetAddrForRPC() assert.NoError(t, err) assert.NotNil(t, addr) // Using an address for RPC shouldn't consume it assert.Equal(t, 4, edge.AvailableAddrs()) // Get it back edge.GiveBack(addr, false) assert.Equal(t, 4, edge.AvailableAddrs()) } func TestOnePerRegion(t *testing.T) { // Make an edge with only one address edge := MockEdge(&testLogger, []*allregions.EdgeAddr{&addr0, &addr1}) // Use the only address const connID = 0 a1, err := edge.GetAddr(connID) assert.NoError(t, err) assert.NotNil(t, a1) // if the first address is bad, get the second one a2, err := edge.GetDifferentAddr(connID, false) assert.NoError(t, err) assert.NotNil(t, a2) assert.NotEqual(t, a1, a2) // now that second one is bad, get the first one again a3, err := edge.GetDifferentAddr(connID, false) assert.NoError(t, err) assert.Equal(t, a1, a3) } func TestOnlyOneAddrLeft(t *testing.T) { // Make an edge with only one address edge := MockEdge(&testLogger, []*allregions.EdgeAddr{&addr0}) // Use the only address const connID = 0 addr, err := edge.GetAddr(connID) assert.NoError(t, err) assert.NotNil(t, addr) // If that edge address is "bad", there's no alternative address. _, err = edge.GetDifferentAddr(connID, false) assert.Error(t, err) // previously bad address should become available again on next iteration. addr, err = edge.GetDifferentAddr(connID, false) assert.NoError(t, err) assert.NotNil(t, addr) } func TestNoAddrsLeft(t *testing.T) { // Make an edge with no addresses edge := MockEdge(&testLogger, []*allregions.EdgeAddr{}) _, err := edge.GetAddr(2) assert.Error(t, err) _, err = edge.GetAddrForRPC() assert.Error(t, err) } func TestGetAddr(t *testing.T) { edge := MockEdge(&testLogger, []*allregions.EdgeAddr{&addr0, &addr1, &addr2, &addr3}) // Give this connection an address const connID = 0 addr, err := edge.GetAddr(connID) assert.NoError(t, err) assert.NotNil(t, addr) // If the same connection requests another address, it should get the same one. addr2, err := edge.GetAddr(connID) assert.NoError(t, err) assert.Equal(t, addr, addr2) } func TestGetDifferentAddr(t *testing.T) { edge := MockEdge(&testLogger, []*allregions.EdgeAddr{&addr0, &addr1, &addr2, &addr3}) // Give this connection an address assert.Equal(t, 4, edge.AvailableAddrs()) const connID = 0 addr, err := edge.GetAddr(connID) assert.NoError(t, err) assert.NotNil(t, addr) assert.Equal(t, 3, edge.AvailableAddrs()) // If the same connection requests another address, it should get the same one. addr2, err := edge.GetDifferentAddr(connID, false) assert.NoError(t, err) assert.NotEqual(t, addr, addr2) assert.Equal(t, 3, edge.AvailableAddrs()) } // MockEdge creates a Cloudflare Edge from arbitrary TCP addresses. Used for testing. func MockEdge(log *zerolog.Logger, addrs []*allregions.EdgeAddr) *Edge { regions := allregions.NewNoResolve(addrs) return &Edge{ log: log, regions: regions, } } ================================================ FILE: edgediscovery/mocks_for_test.go ================================================ package edgediscovery import ( "fmt" "math" "math/rand" "net" "reflect" "testing/quick" ) type mockAddrs struct { // a set of synthetic SRV records addrMap map[net.SRV][]*net.TCPAddr // the total number of addresses, aggregated across addrMap. // For the convenience of test code that would otherwise have to compute // this by hand every time. numAddrs int } func newMockAddrs(port uint16, numRegions uint8, numAddrsPerRegion uint8) mockAddrs { addrMap := make(map[net.SRV][]*net.TCPAddr) numAddrs := 0 for r := uint8(0); r < numRegions; r++ { var ( srv = net.SRV{Target: fmt.Sprintf("test-region-%v.example.com", r), Port: port} addrs []*net.TCPAddr ) for a := uint8(0); a < numAddrsPerRegion; a++ { addrs = append(addrs, &net.TCPAddr{ IP: net.ParseIP(fmt.Sprintf("10.0.%v.%v", r, a)), Port: int(port), }) } addrMap[srv] = addrs numAddrs += len(addrs) } return mockAddrs{addrMap: addrMap, numAddrs: numAddrs} } var _ quick.Generator = mockAddrs{} func (mockAddrs) Generate(rand *rand.Rand, size int) reflect.Value { port := uint16(rand.Intn(math.MaxUint16)) numRegions := uint8(1 + rand.Intn(10)) numAddrsPerRegion := uint8(1 + rand.Intn(32)) result := newMockAddrs(port, numRegions, numAddrsPerRegion) return reflect.ValueOf(result) } // Returns a function compatible with net.LookupSRV that will return the SRV // records from mockAddrs. func mockNetLookupSRV( m mockAddrs, ) func(service, proto, name string) (cname string, addrs []*net.SRV, err error) { var addrs []*net.SRV for k := range m.addrMap { addr := k addrs = append(addrs, &addr) // We can't just do // addrs = append(addrs, &k) // `k` will be reused by subsequent loop iterations, // so all the copies of `&k` would point to the same location. } return func(_, _, _ string) (string, []*net.SRV, error) { return "", addrs, nil } } // Returns a function compatible with net.LookupIP that translates the SRV records // from mockAddrs into IP addresses, based on the TCP addresses in mockAddrs. func mockNetLookupIP( m mockAddrs, ) func(host string) ([]net.IP, error) { return func(host string) ([]net.IP, error) { for srv, tcpAddrs := range m.addrMap { if srv.Target != host { continue } result := make([]net.IP, len(tcpAddrs)) for i, tcpAddr := range tcpAddrs { result[i] = tcpAddr.IP } return result, nil } return nil, fmt.Errorf("No IPs for %v", host) } } type mockEdgeServiceDiscoverer struct { } func (mr *mockEdgeServiceDiscoverer) Addr() (*net.TCPAddr, error) { return &net.TCPAddr{ IP: net.ParseIP("127.0.0.1"), Port: 63102, }, nil } func (mr *mockEdgeServiceDiscoverer) AnyAddr() (*net.TCPAddr, error) { return &net.TCPAddr{ IP: net.ParseIP("127.0.0.1"), Port: 63102, }, nil } func (mr *mockEdgeServiceDiscoverer) ReplaceAddr(addr *net.TCPAddr) {} func (mr *mockEdgeServiceDiscoverer) MarkAddrBad(addr *net.TCPAddr) {} func (mr *mockEdgeServiceDiscoverer) AvailableAddrs() int { return 1 } func (mr *mockEdgeServiceDiscoverer) Refresh() error { return nil } ================================================ FILE: edgediscovery/protocol.go ================================================ package edgediscovery import ( "encoding/json" "fmt" "net" "strings" ) const ( protocolRecord = "protocol-v2.argotunnel.com" ) var ( errNoProtocolRecord = fmt.Errorf("No TXT record found for %s to determine connection protocol", protocolRecord) ) type PercentageFetcher func() (ProtocolPercents, error) // ProtocolPercent represents a single Protocol Percentage combination. type ProtocolPercent struct { Protocol string `json:"protocol"` Percentage int32 `json:"percentage"` } // ProtocolPercents represents the preferred distribution ratio of protocols when protocol isn't specified. type ProtocolPercents []ProtocolPercent // GetPercentage returns the threshold percentage of a single protocol. func (p ProtocolPercents) GetPercentage(protocol string) int32 { for _, protocolPercent := range p { if strings.ToLower(protocolPercent.Protocol) == strings.ToLower(protocol) { return protocolPercent.Percentage } } return 0 } // ProtocolPercentage returns the ratio of protocols and a specification ratio for their selection. func ProtocolPercentage() (ProtocolPercents, error) { records, err := net.LookupTXT(protocolRecord) if err != nil { return nil, err } if len(records) == 0 { return nil, errNoProtocolRecord } var protocolsWithPercent ProtocolPercents err = json.Unmarshal([]byte(records[0]), &protocolsWithPercent) return protocolsWithPercent, err } ================================================ FILE: edgediscovery/protocol_test.go ================================================ package edgediscovery import ( "testing" "github.com/stretchr/testify/assert" ) func TestProtocolPercentage(t *testing.T) { _, err := ProtocolPercentage() assert.NoError(t, err) } ================================================ FILE: features/features.go ================================================ package features import "slices" const ( FeatureSerializedHeaders = "serialized_headers" FeatureQuickReconnects = "quick_reconnects" FeatureAllowRemoteConfig = "allow_remote_config" FeatureDatagramV2 = "support_datagram_v2" FeaturePostQuantum = "postquantum" FeatureQUICSupportEOF = "support_quic_eof" FeatureManagementLogs = "management_logs" FeatureDatagramV3_2 = "support_datagram_v3_2" DeprecatedFeatureDatagramV3 = "support_datagram_v3" // Deprecated: TUN-9291 DeprecatedFeatureDatagramV3_1 = "support_datagram_v3_1" // Deprecated: TUN-9883 ) var defaultFeatures = []string{ FeatureAllowRemoteConfig, FeatureSerializedHeaders, FeatureDatagramV2, FeatureQUICSupportEOF, FeatureManagementLogs, } // List of features that are no longer in-use. var deprecatedFeatures = []string{ DeprecatedFeatureDatagramV3, DeprecatedFeatureDatagramV3_1, } // Features set by user provided flags type staticFeatures struct { PostQuantumMode *PostQuantumMode } type FeatureSnapshot struct { PostQuantum PostQuantumMode DatagramVersion DatagramVersion // We provide the list of features since we need it to send in the ConnectionOptions during connection // registrations. FeaturesList []string } type PostQuantumMode uint8 const ( // Prefer post quantum, but fallback if connection cannot be established PostQuantumPrefer PostQuantumMode = iota // If the user passes the --post-quantum flag, we override // CurvePreferences to only support hybrid post-quantum key agreements. PostQuantumStrict ) type DatagramVersion string const ( // DatagramV2 is the currently supported datagram protocol for UDP and ICMP packets DatagramV2 DatagramVersion = FeatureDatagramV2 // DatagramV3 is a new datagram protocol for UDP and ICMP packets. It is not backwards compatible with datagram v2. DatagramV3 DatagramVersion = FeatureDatagramV3_2 ) // Remove any duplicate features from the list and remove deprecated features func dedupAndRemoveFeatures(features []string) []string { // Convert the slice into a set set := map[string]bool{} for _, feature := range features { // Remove deprecated features from the provided list if slices.Contains(deprecatedFeatures, feature) { continue } set[feature] = true } // Convert the set back into a slice keys := make([]string, len(set)) i := 0 for str := range set { keys[i] = str i++ } return keys } ================================================ FILE: features/selector.go ================================================ package features import ( "context" "encoding/json" "fmt" "hash/fnv" "net" "slices" "sync" "time" "github.com/rs/zerolog" ) const ( featureSelectorHostname = "cfd-features.argotunnel.com" lookupTimeout = time.Second * 10 defaultLookupFreq = time.Hour ) // If the TXT record adds other fields, the umarshal logic will ignore those keys // If the TXT record is missing a key, the field will unmarshal to the default Go value type featuresRecord struct { DatagramV3Percentage uint32 `json:"dv3_2"` // DatagramV3Percentage int32 `json:"dv3"` // Removed in TUN-9291 // DatagramV3Percentage uint32 `json:"dv3_1"` // Removed in TUN-9883 // PostQuantumPercentage int32 `json:"pq"` // Removed in TUN-7970 } func NewFeatureSelector(ctx context.Context, accountTag string, cliFeatures []string, pq bool, logger *zerolog.Logger) (FeatureSelector, error) { return newFeatureSelector(ctx, accountTag, logger, newDNSResolver(), cliFeatures, pq, defaultLookupFreq) } type FeatureSelector interface { Snapshot() FeatureSnapshot } // FeatureSelector determines if this account will try new features; loaded once during startup. type featureSelector struct { accountHash uint32 logger *zerolog.Logger resolver resolver staticFeatures staticFeatures cliFeatures []string // lock protects concurrent access to dynamic features lock sync.RWMutex remoteFeatures featuresRecord } func newFeatureSelector(ctx context.Context, accountTag string, logger *zerolog.Logger, resolver resolver, cliFeatures []string, pq bool, refreshFreq time.Duration) (*featureSelector, error) { // Combine default features and user-provided features var pqMode *PostQuantumMode if pq { mode := PostQuantumStrict pqMode = &mode cliFeatures = append(cliFeatures, FeaturePostQuantum) } staticFeatures := staticFeatures{ PostQuantumMode: pqMode, } selector := &featureSelector{ accountHash: switchThreshold(accountTag), logger: logger, resolver: resolver, staticFeatures: staticFeatures, cliFeatures: dedupAndRemoveFeatures(cliFeatures), } // Load the remote features if err := selector.refresh(ctx); err != nil { logger.Err(err).Msg("Failed to fetch features, default to disable") } // Spin off reloading routine go selector.refreshLoop(ctx, refreshFreq) return selector, nil } func (fs *featureSelector) Snapshot() FeatureSnapshot { fs.lock.RLock() defer fs.lock.RUnlock() return FeatureSnapshot{ PostQuantum: fs.postQuantumMode(), DatagramVersion: fs.datagramVersion(), FeaturesList: fs.clientFeatures(), } } func (fs *featureSelector) accountEnabled(percentage uint32) bool { return percentage > fs.accountHash } func (fs *featureSelector) postQuantumMode() PostQuantumMode { if fs.staticFeatures.PostQuantumMode != nil { return *fs.staticFeatures.PostQuantumMode } return PostQuantumPrefer } func (fs *featureSelector) datagramVersion() DatagramVersion { // If user provides the feature via the cli, we take it as priority over remote feature evaluation if slices.Contains(fs.cliFeatures, FeatureDatagramV3_2) { return DatagramV3 } // If the user specifies DatagramV2, we also take that over remote if slices.Contains(fs.cliFeatures, FeatureDatagramV2) { return DatagramV2 } if fs.accountEnabled(fs.remoteFeatures.DatagramV3Percentage) { return DatagramV3 } return DatagramV2 } // clientFeatures will return the list of currently available features that cloudflared should provide to the edge. func (fs *featureSelector) clientFeatures() []string { // Evaluate any remote features along with static feature list to construct the list of features return dedupAndRemoveFeatures(slices.Concat(defaultFeatures, fs.cliFeatures, []string{string(fs.datagramVersion())})) } func (fs *featureSelector) refresh(ctx context.Context) error { record, err := fs.resolver.lookupRecord(ctx) if err != nil { return err } var features featuresRecord if err := json.Unmarshal(record, &features); err != nil { return err } fs.lock.Lock() defer fs.lock.Unlock() fs.remoteFeatures = features return nil } func (fs *featureSelector) refreshLoop(ctx context.Context, refreshFreq time.Duration) { ticker := time.NewTicker(refreshFreq) for { select { case <-ctx.Done(): return case <-ticker.C: err := fs.refresh(ctx) if err != nil { fs.logger.Err(err).Msg("Failed to refresh feature selector") } } } } // resolver represents an object that can look up featuresRecord type resolver interface { lookupRecord(ctx context.Context) ([]byte, error) } type dnsResolver struct { resolver *net.Resolver } func newDNSResolver() *dnsResolver { return &dnsResolver{ resolver: net.DefaultResolver, } } func (dr *dnsResolver) lookupRecord(ctx context.Context) ([]byte, error) { ctx, cancel := context.WithTimeout(ctx, lookupTimeout) defer cancel() records, err := dr.resolver.LookupTXT(ctx, featureSelectorHostname) if err != nil { return nil, err } if len(records) == 0 { return nil, fmt.Errorf("No TXT record found for %s to determine which features to opt-in", featureSelectorHostname) } return []byte(records[0]), nil } func switchThreshold(accountTag string) uint32 { h := fnv.New32a() _, _ = h.Write([]byte(accountTag)) return h.Sum32() % 100 } ================================================ FILE: features/selector_test.go ================================================ package features import ( "context" "encoding/json" "fmt" "testing" "time" "github.com/rs/zerolog" "github.com/stretchr/testify/require" ) const ( testAccountTag = "123456" testAccountHash = 74 // switchThreshold of `accountTag` ) func TestUnmarshalFeaturesRecord(t *testing.T) { tests := []struct { record []byte expectedPercentage uint32 }{ { record: []byte(`{"dv3_2":0}`), expectedPercentage: 0, }, { record: []byte(`{"dv3_2":39}`), expectedPercentage: 39, }, { record: []byte(`{"dv3_2":100}`), expectedPercentage: 100, }, { record: []byte(`{}`), // Unmarshal to default struct if key is not present }, { record: []byte(`{"kyber":768}`), // Unmarshal to default struct if key is not present }, { record: []byte(`{"pq": 101,"dv3":100,"dv3_1":100}`), // Expired keys don't unmarshal to anything }, } for _, test := range tests { var features featuresRecord err := json.Unmarshal(test.record, &features) require.NoError(t, err) require.Equal(t, test.expectedPercentage, features.DatagramV3Percentage, test) } } func TestFeaturePrecedenceEvaluationPostQuantum(t *testing.T) { logger := zerolog.Nop() tests := []struct { name string cli bool expectedFeatures []string expectedVersion PostQuantumMode }{ { name: "default", cli: false, expectedFeatures: defaultFeatures, expectedVersion: PostQuantumPrefer, }, { name: "user_specified", cli: true, expectedFeatures: dedupAndRemoveFeatures(append(defaultFeatures, FeaturePostQuantum)), expectedVersion: PostQuantumStrict, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { resolver := &staticResolver{record: featuresRecord{}} selector, err := newFeatureSelector(t.Context(), test.name, &logger, resolver, []string{}, test.cli, time.Second) require.NoError(t, err) snapshot := selector.Snapshot() require.ElementsMatch(t, test.expectedFeatures, snapshot.FeaturesList) require.Equal(t, test.expectedVersion, snapshot.PostQuantum) }) } } func TestFeaturePrecedenceEvaluationDatagramVersion(t *testing.T) { logger := zerolog.Nop() tests := []struct { name string cli []string remote featuresRecord expectedFeatures []string expectedVersion DatagramVersion }{ { name: "default", cli: []string{}, remote: featuresRecord{}, expectedFeatures: defaultFeatures, expectedVersion: DatagramV2, }, { name: "user_specified_v2", cli: []string{FeatureDatagramV2}, remote: featuresRecord{}, expectedFeatures: defaultFeatures, expectedVersion: DatagramV2, }, { name: "user_specified_v3", cli: []string{FeatureDatagramV3_2}, remote: featuresRecord{}, expectedFeatures: dedupAndRemoveFeatures(append(defaultFeatures, FeatureDatagramV3_2)), expectedVersion: FeatureDatagramV3_2, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { resolver := &staticResolver{record: test.remote} selector, err := newFeatureSelector(t.Context(), test.name, &logger, resolver, test.cli, false, time.Second) require.NoError(t, err) snapshot := selector.Snapshot() require.ElementsMatch(t, test.expectedFeatures, snapshot.FeaturesList) require.Equal(t, test.expectedVersion, snapshot.DatagramVersion) }) } } func TestDeprecatedFeaturesRemoved(t *testing.T) { logger := zerolog.Nop() tests := []struct { name string cli []string remote featuresRecord expectedFeatures []string }{ { name: "no_removals", cli: []string{}, remote: featuresRecord{}, expectedFeatures: defaultFeatures, }, { name: "support_datagram_v3", cli: []string{DeprecatedFeatureDatagramV3}, remote: featuresRecord{}, expectedFeatures: defaultFeatures, }, { name: "support_datagram_v3_1", cli: []string{DeprecatedFeatureDatagramV3_1}, remote: featuresRecord{}, expectedFeatures: defaultFeatures, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { resolver := &staticResolver{record: test.remote} selector, err := newFeatureSelector(t.Context(), test.name, &logger, resolver, test.cli, false, time.Second) require.NoError(t, err) snapshot := selector.Snapshot() require.ElementsMatch(t, test.expectedFeatures, snapshot.FeaturesList) }) } } func TestRefreshFeaturesRecord(t *testing.T) { percentages := []uint32{0, 10, testAccountHash - 1, testAccountHash, testAccountHash + 1, 100, 101, 1000} selector := newTestSelector(t, percentages, false, time.Minute) // Starting out should default to DatagramV2 snapshot := selector.Snapshot() require.Equal(t, DatagramV2, snapshot.DatagramVersion) for _, percentage := range percentages { snapshot = selector.Snapshot() if percentage > testAccountHash { require.Equal(t, DatagramV3, snapshot.DatagramVersion) } else { require.Equal(t, DatagramV2, snapshot.DatagramVersion) } // Manually progress the next refresh _ = selector.refresh(t.Context()) } // Make sure a resolver error doesn't override the last fetched features snapshot = selector.Snapshot() require.Equal(t, DatagramV3, snapshot.DatagramVersion) } func TestSnapshotIsolation(t *testing.T) { percentages := []uint32{testAccountHash, testAccountHash + 1} selector := newTestSelector(t, percentages, false, time.Minute) // Starting out should default to DatagramV2 snapshot := selector.Snapshot() require.Equal(t, DatagramV2, snapshot.DatagramVersion) // Manually progress the next refresh _ = selector.refresh(t.Context()) snapshot2 := selector.Snapshot() require.Equal(t, DatagramV3, snapshot2.DatagramVersion) require.NotEqual(t, snapshot.DatagramVersion, snapshot2.DatagramVersion) } func TestStaticFeatures(t *testing.T) { percentages := []uint32{0} // PostQuantum Enabled from user flag selector := newTestSelector(t, percentages, true, time.Second) snapshot := selector.Snapshot() require.Equal(t, PostQuantumStrict, snapshot.PostQuantum) // PostQuantum Disabled (or not set) selector = newTestSelector(t, percentages, false, time.Second) snapshot = selector.Snapshot() require.Equal(t, PostQuantumPrefer, snapshot.PostQuantum) } func newTestSelector(t *testing.T, percentages []uint32, pq bool, refreshFreq time.Duration) *featureSelector { logger := zerolog.Nop() resolver := &mockResolver{ percentages: percentages, } selector, err := newFeatureSelector(t.Context(), testAccountTag, &logger, resolver, []string{}, pq, refreshFreq) require.NoError(t, err) return selector } type mockResolver struct { nextIndex int percentages []uint32 } func (mr *mockResolver) lookupRecord(ctx context.Context) ([]byte, error) { if mr.nextIndex >= len(mr.percentages) { return nil, fmt.Errorf("no more record to lookup") } record, err := json.Marshal(featuresRecord{ DatagramV3Percentage: mr.percentages[mr.nextIndex], }) mr.nextIndex++ return record, err } type staticResolver struct { record featuresRecord } func (r *staticResolver) lookupRecord(ctx context.Context) ([]byte, error) { return json.Marshal(r.record) } ================================================ FILE: fips/fips.go ================================================ //go:build fips package fips import ( _ "crypto/tls/fipsonly" ) func IsFipsEnabled() bool { return true } ================================================ FILE: fips/nofips.go ================================================ //go:build !fips package fips func IsFipsEnabled() bool { return false } ================================================ FILE: flow/limiter.go ================================================ package flow import ( "errors" "sync" ) const ( unlimitedActiveFlows = 0 ) var ( ErrTooManyActiveFlows = errors.New("too many active flows") ) type Limiter interface { // Acquire tries to acquire a free slot for a flow, if the value of flows is already above // the maximum it returns ErrTooManyActiveFlows. Acquire(flowType string) error // Release releases a slot for a flow. Release() // SetLimit allows to hot swap the limit value of the limiter. SetLimit(uint64) } type flowLimiter struct { limiterLock sync.Mutex activeFlowsCounter uint64 maxActiveFlows uint64 unlimited bool } func NewLimiter(maxActiveFlows uint64) Limiter { flowLimiter := &flowLimiter{ maxActiveFlows: maxActiveFlows, unlimited: isUnlimited(maxActiveFlows), } return flowLimiter } func (s *flowLimiter) Acquire(flowType string) error { s.limiterLock.Lock() defer s.limiterLock.Unlock() if !s.unlimited && s.activeFlowsCounter >= s.maxActiveFlows { flowRegistrationsDropped.WithLabelValues(flowType).Inc() return ErrTooManyActiveFlows } s.activeFlowsCounter++ return nil } func (s *flowLimiter) Release() { s.limiterLock.Lock() defer s.limiterLock.Unlock() if s.activeFlowsCounter <= 0 { return } s.activeFlowsCounter-- } func (s *flowLimiter) SetLimit(newMaxActiveFlows uint64) { s.limiterLock.Lock() defer s.limiterLock.Unlock() s.maxActiveFlows = newMaxActiveFlows s.unlimited = isUnlimited(newMaxActiveFlows) } // isUnlimited checks if the value received matches the configuration for the unlimited flow limiter. func isUnlimited(value uint64) bool { return value == unlimitedActiveFlows } ================================================ FILE: flow/limiter_test.go ================================================ package flow_test import ( "testing" "github.com/stretchr/testify/require" "github.com/cloudflare/cloudflared/flow" ) func TestFlowLimiter_Unlimited(t *testing.T) { unlimitedLimiter := flow.NewLimiter(0) for i := 0; i < 1000; i++ { err := unlimitedLimiter.Acquire("test") require.NoError(t, err) } } func TestFlowLimiter_Limited(t *testing.T) { maxFlows := uint64(5) limiter := flow.NewLimiter(maxFlows) for i := uint64(0); i < maxFlows; i++ { err := limiter.Acquire("test") require.NoError(t, err) } err := limiter.Acquire("should fail") require.ErrorIs(t, err, flow.ErrTooManyActiveFlows) } func TestFlowLimiter_AcquireAndReleaseFlow(t *testing.T) { maxFlows := uint64(5) limiter := flow.NewLimiter(maxFlows) // Acquire the maximum number of flows for i := uint64(0); i < maxFlows; i++ { err := limiter.Acquire("test") require.NoError(t, err) } // Validate acquire 1 more flows fails err := limiter.Acquire("should fail") require.ErrorIs(t, err, flow.ErrTooManyActiveFlows) // Release the maximum number of flows for i := uint64(0); i < maxFlows; i++ { limiter.Release() } // Validate acquire 1 more flows works err = limiter.Acquire("shouldn't fail") require.NoError(t, err) // Release a 10x the number of max flows for i := uint64(0); i < 10*maxFlows; i++ { limiter.Release() } // Validate it still can only acquire a value = number max flows. for i := uint64(0); i < maxFlows; i++ { err := limiter.Acquire("test") require.NoError(t, err) } err = limiter.Acquire("should fail") require.ErrorIs(t, err, flow.ErrTooManyActiveFlows) } func TestFlowLimiter_SetLimit(t *testing.T) { maxFlows := uint64(5) limiter := flow.NewLimiter(maxFlows) // Acquire the maximum number of flows for i := uint64(0); i < maxFlows; i++ { err := limiter.Acquire("test") require.NoError(t, err) } // Validate acquire 1 more flows fails err := limiter.Acquire("should fail") require.ErrorIs(t, err, flow.ErrTooManyActiveFlows) // Set the flow limiter to support one more request limiter.SetLimit(maxFlows + 1) // Validate acquire 1 more flows now works err = limiter.Acquire("shouldn't fail") require.NoError(t, err) // Validate acquire 1 more flows doesn't work because we already reached the limit err = limiter.Acquire("should fail") require.ErrorIs(t, err, flow.ErrTooManyActiveFlows) // Release all flows for i := uint64(0); i < maxFlows+1; i++ { limiter.Release() } // Validate 1 flow works again err = limiter.Acquire("shouldn't fail") require.NoError(t, err) // Set the flow limit to 1 limiter.SetLimit(1) // Validate acquire 1 more flows doesn't work err = limiter.Acquire("should fail") require.ErrorIs(t, err, flow.ErrTooManyActiveFlows) // Set the flow limit to unlimited limiter.SetLimit(0) // Validate it can acquire a lot of flows because it is now unlimited. for i := uint64(0); i < 10*maxFlows; i++ { err := limiter.Acquire("shouldn't fail") require.NoError(t, err) } } ================================================ FILE: flow/metrics.go ================================================ package flow import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" ) const ( namespace = "flow" ) var ( labels = []string{"flow_type"} flowRegistrationsDropped = promauto.NewCounterVec(prometheus.CounterOpts{ Namespace: namespace, Subsystem: "client", Name: "registrations_rate_limited_total", Help: "Count registrations dropped due to high number of concurrent flows being handled", }, labels, ) ) ================================================ FILE: github_message.py ================================================ #!/usr/bin/python3 """ Create Github Releases Notes with binary checksums from Workers KV """ import argparse import logging import os import requests from github import Github, UnknownObjectException FORMAT = "%(levelname)s - %(asctime)s: %(message)s" logging.basicConfig(format=FORMAT, level=logging.INFO) CLOUDFLARED_REPO = os.environ.get("GITHUB_REPO", "cloudflare/cloudflared") GITHUB_CONFLICT_CODE = "already_exists" BASE_KV_URL = 'https://api.cloudflare.com/client/v4/accounts/' def kv_get_keys(prefix, account, namespace, api_token): """ get the KV keys for a given prefix """ response = requests.get( BASE_KV_URL + account + "/storage/kv/namespaces/" + namespace + "/keys" + "?prefix=" + prefix, headers={ "Content-Type": "application/json", "Authorization": "Bearer " + api_token, }, ) if response.status_code != 200: jsonResponse = response.json() errors = jsonResponse["errors"] if len(errors) > 0: raise Exception("failed to get checksums: {0}", errors[0]) return response.json()["result"] def kv_get_value(key, account, namespace, api_token): """ get the KV value for a provided key """ response = requests.get( BASE_KV_URL + account + "/storage/kv/namespaces/" + namespace + "/values/" + key, headers={ "Content-Type": "application/json", "Authorization": "Bearer " + api_token, }, ) if response.status_code != 200: jsonResponse = response.json() errors = jsonResponse["errors"] if len(errors) > 0: raise Exception("failed to get checksums: {0}", errors[0]) return response.text def update_or_add_message(msg, name, sha): """ updates or builds the github version message for each new asset's sha256. Searches the existing message string to update or create. """ new_text = '{0}: {1}\n'.format(name, sha) start = msg.find(name) if (start != -1): end = msg.find("\n", start) if (end != -1): return msg.replace(msg[start:end+1], new_text) back = msg.rfind("```") if (back != -1): return '{0}{1}```'.format(msg[:back], new_text) return '{0} \n### SHA256 Checksums:\n```\n{1}```'.format(msg, new_text) def get_release(repo, version): """ Get a Github Release matching the version tag. """ try: release = repo.get_release(version) logging.info("Release %s found", version) return release except UnknownObjectException: logging.info("Release %s not found", version) def parse_args(): """ Parse and validate args """ parser = argparse.ArgumentParser( description="Updates a Github Release with checksums from KV" ) parser.add_argument( "--api-key", default=os.environ.get("API_KEY"), help="Github API key" ) parser.add_argument( "--kv-namespace-id", default=os.environ.get("KV_NAMESPACE"), help="workers KV namespace id" ) parser.add_argument( "--kv-account-id", default=os.environ.get("KV_ACCOUNT"), help="workers KV account id" ) parser.add_argument( "--kv-api-token", default=os.environ.get("KV_API_TOKEN"), help="workers KV API Token" ) parser.add_argument( "--release-version", metavar="version", default=os.environ.get("VERSION"), help="Release version", ) parser.add_argument( "--dry-run", action="store_true", help="Do not modify the release message" ) args = parser.parse_args() is_valid = True if not args.release_version: logging.error("Missing release version") is_valid = False if not args.api_key: logging.error("Missing API key") is_valid = False if not args.kv_namespace_id: logging.error("Missing KV namespace id") is_valid = False if not args.kv_account_id: logging.error("Missing KV account id") is_valid = False if not args.kv_api_token: logging.error("Missing KV API token") is_valid = False if is_valid: return args parser.print_usage() exit(1) def main(): """ Attempts to update the Github Release message with the github asset's checksums """ try: args = parse_args() client = Github(args.api_key) repo = client.get_repo(CLOUDFLARED_REPO) release = get_release(repo, args.release_version) msg = "" prefix = f"update_{args.release_version}_" keys = kv_get_keys(prefix, args.kv_account_id, args.kv_namespace_id, args.kv_api_token) for key in [k["name"] for k in keys]: checksum = kv_get_value( key, args.kv_account_id, args.kv_namespace_id, args.kv_api_token) binary_name = key[len(prefix):] msg = update_or_add_message(msg, binary_name, checksum) if args.dry_run: logging.info("Skipping release message update because of dry-run") logging.info(f"Github message:\n{msg}") return # update the release body text release.update_release(args.release_version, msg) except Exception as e: logging.exception(e) exit(1) main() ================================================ FILE: github_release.py ================================================ #!/usr/bin/python3 """ Creates Github Releases and uploads assets """ import argparse import logging import os import shutil import hashlib import requests import tarfile from os import listdir from os.path import isfile, join, splitext import re import subprocess from github import Github, GithubException, UnknownObjectException FORMAT = "%(levelname)s - %(asctime)s: %(message)s" logging.basicConfig(format=FORMAT, level=logging.INFO) CLOUDFLARED_REPO = os.environ.get("GITHUB_REPO", "cloudflare/cloudflared") GITHUB_CONFLICT_CODE = "already_exists" BASE_KV_URL = 'https://api.cloudflare.com/client/v4/accounts/' UPDATER_PREFIX = 'update' def get_sha256(filename): """ get the sha256 of a file """ sha256_hash = hashlib.sha256() with open(filename,"rb") as f: for byte_block in iter(lambda: f.read(4096),b""): sha256_hash.update(byte_block) return sha256_hash.hexdigest() def send_hash(pkg_hash, name, version, account, namespace, api_token): """ send the checksum of a file to workers kv """ key = '{0}_{1}_{2}'.format(UPDATER_PREFIX, version, name) headers = { "Content-Type": "application/json", "Authorization": "Bearer " + api_token, } response = requests.put( BASE_KV_URL + account + "/storage/kv/namespaces/" + namespace + "/values/" + key, headers=headers, data=pkg_hash ) if response.status_code != 200: jsonResponse = response.json() errors = jsonResponse["errors"] if len(errors) > 0: raise Exception("failed to upload checksum: {0}", errors[0]) def assert_tag_exists(repo, version): """ Raise exception if repo does not contain a tag matching version """ tags = repo.get_tags() if not tags or tags[0].name != version: raise Exception("Tag {} not found".format(version)) def get_or_create_release(repo, version, dry_run=False, is_draft=False): """ Get a Github Release matching the version tag or create a new one. If a conflict occurs on creation, attempt to fetch the Release on last time """ try: release = repo.get_release(version) logging.info("Release %s found", version) return release except UnknownObjectException: logging.info("Release %s not found", version) # We don't want to create a new release tag if one doesn't already exist assert_tag_exists(repo, version) if dry_run: logging.info("Skipping Release creation because of dry-run") return try: if is_draft: logging.info("Drafting release %s", version) else: logging.info("Creating release %s", version) return repo.create_git_release(version, version, "", is_draft) except GithubException as e: errors = e.data.get("errors", []) if e.status == 422 and any( [err.get("code") == GITHUB_CONFLICT_CODE for err in errors] ): logging.warning( "Conflict: Release was likely just made by a different build: %s", e.data, ) return repo.get_release(version) raise e def parse_args(): """ Parse and validate args """ parser = argparse.ArgumentParser( description="Creates Github Releases and uploads assets." ) parser.add_argument( "--api-key", default=os.environ.get("API_KEY"), help="Github API key" ) parser.add_argument( "--release-version", metavar="version", default=os.environ.get("VERSION"), help="Release version", ) parser.add_argument( "--path", default=os.environ.get("ASSET_PATH"), help="Asset path" ) parser.add_argument( "--name", default=os.environ.get("ASSET_NAME"), help="Asset Name" ) parser.add_argument( "--namespace-id", default=os.environ.get("KV_NAMESPACE"), help="workersKV namespace id" ) parser.add_argument( "--kv-account-id", default=os.environ.get("KV_ACCOUNT"), help="workersKV account id" ) parser.add_argument( "--kv-api-token", default=os.environ.get("KV_API_TOKEN"), help="workersKV API Token" ) parser.add_argument( "--dry-run", action="store_true", help="Do not create release or upload asset" ) parser.add_argument( "--draft", action="store_true", help="Create a draft release" ) args = parser.parse_args() is_valid = True if not args.release_version: logging.error("Missing release version") is_valid = False if not args.path: logging.error("Missing asset path") is_valid = False if not args.name and not os.path.isdir(args.path): logging.error("Missing asset name") is_valid = False if not args.api_key: logging.error("Missing API key") is_valid = False if not args.namespace_id: logging.error("Missing KV namespace id") is_valid = False if not args.kv_account_id: logging.error("Missing KV account id") is_valid = False if not args.kv_api_token: logging.error("Missing KV API token") is_valid = False if is_valid: return args parser.print_usage() exit(1) def upload_asset(release, filepath, filename, release_version, kv_account_id, namespace_id, kv_api_token): logging.info("Uploading asset: %s", filename) assets = release.get_assets() uploaded = False for asset in assets: if asset.name == filename: uploaded = True break if uploaded: logging.info("asset already uploaded, skipping upload") return release.upload_asset(filepath, name=filename) # check and extract if the file is a tar and gzipped file (as is the case with the macos builds) binary_path = filepath if binary_path.endswith("tgz"): try: shutil.rmtree('cfd') except OSError: pass zipfile = tarfile.open(binary_path, "r:gz") zipfile.extractall('cfd') # specify which folder to extract to zipfile.close() binary_path = os.path.join(os.getcwd(), 'cfd', 'cloudflared') # send the sha256 (the checksum) to workers kv logging.info("Uploading sha256 checksum for: %s", filename) pkg_hash = get_sha256(binary_path) send_hash(pkg_hash, filename, release_version, kv_account_id, namespace_id, kv_api_token) def move_asset(filepath, filename): # create the artifacts directory if it doesn't exist artifact_path = os.path.join(os.getcwd(), 'artifacts') if not os.path.isdir(artifact_path): os.mkdir(artifact_path) # copy the binary to the path copy_path = os.path.join(artifact_path, filename) try: shutil.copy(filepath, copy_path) except shutil.SameFileError: pass # the macOS release copy fails with being the same file (already in the artifacts directory) def get_binary_version(binary_path): """ Sample output from go version -m : ... build -compiler=gc build -ldflags="-X \"main.Version=2024.8.3-6-gec072691\" -X \"main.BuildTime=2024-09-10-1027 UTC\" " build CGO_ENABLED=1 ... This function parses the above output to retrieve the following substring 2024.8.3-6-gec072691. To do this a start and end indexes are computed and the a slice is extracted from the output using them. """ needle = "main.Version=" cmd = ['go','version', '-m', binary_path] process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) output, _ = process.communicate() version_info = output.decode() # Find start of needle needle_index = version_info.find(needle) # Find backward slash relative to the beggining of the needle relative_end_index = version_info[needle_index:].find("\\") # Calculate needle position plus needle length to find version beggining start_index = needle_index + len(needle) # Calculate needle position plus relative position of the backward slash end_index = needle_index + relative_end_index return version_info[start_index:end_index] def assert_asset_version(binary_path, release_version): """ Asserts that the artifacts have the correct release_version. The artifacts that are checked must not have an extension expecting .exe and .tgz. In the occurrence of any other extension the function exits early. """ try: shutil.rmtree('tmp') except OSError: pass _, ext = os.path.splitext(binary_path) if ext == '.exe' or ext == '': binary_version = get_binary_version(binary_path) elif ext == '.tgz': tar = tarfile.open(binary_path, "r:gz") tar.extractall("tmp") tar.close() binary_path = os.path.join(os.getcwd(), 'tmp', 'cloudflared') binary_version = get_binary_version(binary_path) else: return if binary_version != release_version: logging.error(f"Version mismatch {binary_path}, binary_version {binary_version} release_version {release_version}") exit(1) def main(): """ Attempts to upload Asset to Github Release. Creates Release if it doesn't exist """ try: args = parse_args() if args.dry_run: if os.path.isdir(args.path): onlyfiles = [f for f in listdir(args.path) if isfile(join(args.path, f))] for filename in onlyfiles: binary_path = os.path.join(args.path, filename) logging.info("binary: " + binary_path) assert_asset_version(binary_path, args.release_version) elif os.path.isfile(args.path): logging.info("binary: " + binary_path) else: logging.error("dryrun failed") return else: client = Github(args.api_key) repo = client.get_repo(CLOUDFLARED_REPO) if os.path.isdir(args.path): onlyfiles = [f for f in listdir(args.path) if isfile(join(args.path, f))] for filename in onlyfiles: binary_path = os.path.join(args.path, filename) assert_asset_version(binary_path, args.release_version) release = get_or_create_release(repo, args.release_version, args.dry_run, args.draft) for filename in onlyfiles: binary_path = os.path.join(args.path, filename) upload_asset(release, binary_path, filename, args.release_version, args.kv_account_id, args.namespace_id, args.kv_api_token) move_asset(binary_path, filename) else: raise Exception("the argument path must be a directory") except Exception as e: logging.exception(e) exit(1) main() ================================================ FILE: go.mod ================================================ module github.com/cloudflare/cloudflared go 1.24.0 require ( github.com/coreos/go-oidc/v3 v3.17.0 github.com/coreos/go-systemd/v22 v22.5.0 github.com/facebookgo/grace v0.0.0-20180706040059-75cf19382434 github.com/fortytw2/leaktest v1.3.0 github.com/fsnotify/fsnotify v1.4.9 github.com/getsentry/sentry-go v0.43.0 github.com/go-chi/chi/v5 v5.2.2 github.com/go-chi/cors v1.2.1 github.com/go-jose/go-jose/v4 v4.1.3 github.com/gobwas/ws v1.2.1 github.com/google/gopacket v1.1.19 github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.0 github.com/json-iterator/go v1.1.12 github.com/mattn/go-colorable v0.1.13 github.com/mitchellh/go-homedir v1.1.0 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.22.0 github.com/prometheus/client_model v0.6.2 github.com/quic-go/quic-go v0.52.0 github.com/rs/zerolog v1.20.0 github.com/stretchr/testify v1.11.1 github.com/urfave/cli/v2 v2.3.0 go.opentelemetry.io/contrib/propagators v0.22.0 go.opentelemetry.io/otel v1.40.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0 go.opentelemetry.io/otel/sdk v1.40.0 go.opentelemetry.io/otel/trace v1.40.0 go.opentelemetry.io/proto/otlp v1.2.0 go.uber.org/automaxprocs v1.6.0 go.uber.org/mock v0.5.1 golang.org/x/crypto v0.38.0 golang.org/x/net v0.40.0 golang.org/x/sync v0.14.0 golang.org/x/sys v0.40.0 golang.org/x/term v0.32.0 google.golang.org/protobuf v1.36.6 gopkg.in/natefinch/lumberjack.v2 v2.0.0 gopkg.in/yaml.v3 v3.0.1 nhooyr.io/websocket v1.8.7 zombiezen.com/go/capnproto2 v2.18.0+incompatible ) require ( github.com/BurntSushi/toml v1.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bytedance/sonic v1.12.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 // indirect github.com/facebookgo/freeport v0.0.0-20150612182905-d4adf43b75b9 // indirect github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 // indirect github.com/gin-gonic/gin v1.9.1 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-playground/validator/v10 v10.15.1 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/google/pprof v0.0.0-20250418163039-24c5476c6587 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/onsi/ginkgo/v2 v2.23.4 // indirect github.com/pelletier/go-toml/v2 v2.0.9 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/common v0.64.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/tinylib/msgp v1.6.3 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel/metric v1.40.0 // indirect golang.org/x/arch v0.4.0 // indirect golang.org/x/mod v0.24.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/text v0.25.0 // indirect golang.org/x/tools v0.32.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9 // indirect google.golang.org/grpc v1.72.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) replace github.com/urfave/cli/v2 => github.com/ipostelnik/cli/v2 v2.3.1-0.20210324024421-b6ea8234fe3d // Avoid 'CVE-2022-21698' replace github.com/prometheus/golang_client => github.com/prometheus/golang_client v1.12.1 replace gopkg.in/yaml.v3 => gopkg.in/yaml.v3 v3.0.1 // This fork is based on quic-go v0.45 replace github.com/quic-go/quic-go => github.com/chungthuang/quic-go v0.45.1-0.20250428085412-43229ad201fd ================================================ FILE: go.sum ================================================ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.2.0 h1:Rt8g24XnyGTyglgET/PRUNlrUeu9F5L+7FilkXfZgs0= github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bytedance/sonic v1.12.0 h1:YGPgxF9xzaCNvd/ZKdQ28yRovhfMFZQjuk6fKBzZ3ls= github.com/bytedance/sonic v1.12.0/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM= github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chungthuang/quic-go v0.45.1-0.20250428085412-43229ad201fd h1:VdYI5zFQ2h1/qzoC6rhyPx479bkF8i177Qpg4Q2n1vk= github.com/chungthuang/quic-go v0.45.1-0.20250428085412-43229ad201fd/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ= github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc= github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 h1:0JZ+dUmQeA8IIVUMzysrX4/AKuQwWhV2dYQuPZdvdSQ= github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64= github.com/facebookgo/freeport v0.0.0-20150612182905-d4adf43b75b9 h1:wWke/RUCl7VRjQhwPlR/v0glZXNYzBHdNUzf/Am2Nmg= github.com/facebookgo/freeport v0.0.0-20150612182905-d4adf43b75b9/go.mod h1:uPmAp6Sws4L7+Q/OokbWDAK1ibXYhB3PXFP1kol5hPg= github.com/facebookgo/grace v0.0.0-20180706040059-75cf19382434 h1:mOp33BLbcbJ8fvTAmZacbBiOASfxN+MLcLxymZCIrGE= github.com/facebookgo/grace v0.0.0-20180706040059-75cf19382434/go.mod h1:KigFdumBXUPSwzLDbeuzyt0elrL7+CP7TKuhrhT4bcU= github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A= github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg= github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 h1:E2s37DuLxFhQDg5gKsWoLBOB0n+ZW8s599zru8FJ2/Y= github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= github.com/getsentry/sentry-go v0.43.0 h1:XbXLpFicpo8HmBDaInk7dum18G9KSLcjZiyUKS+hLW4= github.com/getsentry/sentry-go v0.43.0/go.mod h1:XDotiNZbgf5U8bPDUAfvcFmOnMQQceESxyKaObSssW0= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-playground/validator/v10 v10.15.1 h1:BSe8uhN+xQ4r5guV/ywQI4gO59C2raYcGffYWZEjZzM= github.com/go-playground/validator/v10 v10.15.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/gobwas/ws v1.2.1 h1:F2aeBZrm2NDsc7vbovKrWSogd4wvfAxg0FQ89/iqOTk= github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/pprof v0.0.0-20250418163039-24c5476c6587 h1:b/8HpQhvKLSNzH5oTXN2WkNcMl6YB5K3FRbb+i+Ml34= github.com/google/pprof v0.0.0-20250418163039-24c5476c6587/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= github.com/ipostelnik/cli/v2 v2.3.1-0.20210324024421-b6ea8234fe3d h1:PRDnysJ9dF1vUMmEzBu6aHQeUluSQy4eWH3RsSSy/vI= github.com/ipostelnik/cli/v2 v2.3.1-0.20210324024421-b6ea8234fe3d/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU= github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0= github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4= github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.20.0 h1:38k9hgtUBdxFwE34yS8rTHmHBa4eN16E4DJlv177LNs= github.com/rs/zerolog v1.20.0/go.mod h1:IzD0RJ65iWH0w97OQQebJEvTZYvsCUm9WVLWBQrJRjo= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s= github.com/tinylib/msgp v1.6.3/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/propagators v0.22.0 h1:KGdv58M2//veiYLIhb31mofaI2LgkIPXXAZVeYVyfd8= go.opentelemetry.io/contrib/propagators v0.22.0/go.mod h1:xGOuXr6lLIF9BXipA4pm6UuOSI0M98U6tsI3khbOiwU= go.opentelemetry.io/otel v1.0.0-RC2/go.mod h1:w1thVQ7qbAy8MHb0IFj8a5Q2QU0l2ksf8u/CN8m3NOM= go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0 h1:1u/AyyOqAWzy+SkPxDpahCNZParHV8Vid1RnI2clyDE= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0/go.mod h1:z46paqbJ9l7c9fIPCXTqTGwhQZ5XoTIsfeFYWboizjs= go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= go.opentelemetry.io/otel/trace v1.0.0-RC2/go.mod h1:JPQ+z6nNw9mqEGT8o3eoPTdnNI+Aj5JcxEsVGREIAy4= go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.opentelemetry.io/proto/otlp v1.2.0 h1:pVeZGk7nXDC9O2hncA6nHldxEjm6LByfA2aN8IOkz94= go.opentelemetry.io/proto/otlp v1.2.0/go.mod h1:gGpR8txAl5M03pDhMC79G6SdqNV26naRm/KDsgaHD8A= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.5.1 h1:ASgazW/qBmR+A32MYFDB6E2POoTgOwT509VP0CT/fjs= go.uber.org/mock v0.5.1/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= golang.org/x/arch v0.4.0 h1:A8WCeEWhLwPBKNbFi5Wv5UTCBx5zzubnXDlMOFAzFMc= golang.org/x/arch v0.4.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2 h1:vPV0tzlsK6EzEDHNNH5sa7Hs9bd7iXR7B1tSiPepkV0= google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:pKLAc5OolXC3ViWGI62vvC0n10CpwAtRcTNCFwTKBEw= google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9 h1:IkAfh6J/yllPtpYFU0zZN1hUPYdT0ogkBT/9hMxHjvg= google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8= google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= zombiezen.com/go/capnproto2 v2.18.0+incompatible h1:mwfXZniffG5mXokQGHUJWGnqIBggoPfT/CEwon9Yess= zombiezen.com/go/capnproto2 v2.18.0+incompatible/go.mod h1:XO5Pr2SbXgqZwn0m0Ru54QBqpOf4K5AYBO+8LAOBQEQ= ================================================ FILE: hello/hello.go ================================================ package hello import ( "bytes" "crypto/tls" "encoding/json" "fmt" "html/template" "io" "net" "net/http" "os" "time" "github.com/gorilla/websocket" "github.com/rs/zerolog" "github.com/cloudflare/cloudflared/tlsconfig" ) const ( UptimeRoute = "/uptime" WSRoute = "/ws" SSERoute = "/sse" HealthRoute = "/_health" defaultSSEFreq = time.Second * 10 ) type templateData struct { ServerName string Request *http.Request Body string } type OriginUpTime struct { StartTime time.Time `json:"startTime"` UpTime string `json:"uptime"` } const defaultServerName = "the Cloudflare Tunnel test server" const indexTemplate = ` Cloudflare Tunnel Connection

Congrats! You created a tunnel!

Cloudflare Tunnel exposes locally running applications to the internet by running an encrypted, virtual tunnel from your laptop or server to Cloudflare's edge network.

Ready for the next step?

Get started here

Request

Method: {{.Request.Method}}
Protocol: {{.Request.Proto}}
Request URL: {{.Request.URL}}
Transfer encoding: {{.Request.TransferEncoding}}
Host: {{.Request.Host}}
Remote address: {{.Request.RemoteAddr}}
Request URI: {{.Request.RequestURI}}
{{range $key, $value := .Request.Header}}
Header: {{$key}}, Value: {{$value}}
{{end}}
Body: {{.Body}}
` func StartHelloWorldServer(log *zerolog.Logger, listener net.Listener, shutdownC <-chan struct{}) error { log.Info().Msgf("Starting Hello World server at %s", listener.Addr()) serverName := defaultServerName if hostname, err := os.Hostname(); err == nil { serverName = hostname } upgrader := websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, } muxer := http.NewServeMux() muxer.HandleFunc(UptimeRoute, uptimeHandler(time.Now())) muxer.HandleFunc(WSRoute, websocketHandler(log, upgrader)) muxer.HandleFunc(SSERoute, sseHandler(log)) muxer.HandleFunc(HealthRoute, healthHandler()) muxer.HandleFunc("/", rootHandler(serverName)) httpServer := &http.Server{Addr: listener.Addr().String(), Handler: muxer} go func() { <-shutdownC _ = httpServer.Close() }() err := httpServer.Serve(listener) return err } func CreateTLSListener(address string) (net.Listener, error) { certificate, err := tlsconfig.GetHelloCertificate() if err != nil { return nil, err } // If the port in address is empty, a port number is automatically chosen listener, err := tls.Listen( "tcp", address, &tls.Config{Certificates: []tls.Certificate{certificate}}) return listener, err } func uptimeHandler(startTime time.Time) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Note that if autoupdate is enabled, the uptime is reset when a new client // release is available resp := &OriginUpTime{StartTime: startTime, UpTime: time.Now().Sub(startTime).String()} respJson, err := json.Marshal(resp) if err != nil { w.WriteHeader(http.StatusInternalServerError) } else { w.Header().Set("Content-Type", "application/json") _, _ = w.Write(respJson) } } } // This handler will echo message func websocketHandler(log *zerolog.Logger, upgrader websocket.Upgrader) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // This addresses the issue of r.Host includes port but origin header doesn't host, _, err := net.SplitHostPort(r.Host) if err == nil { r.Host = host } conn, err := upgrader.Upgrade(w, r, nil) if err != nil { log.Err(err).Msg("failed to upgrade to websocket connection") return } defer conn.Close() for { mt, message, err := conn.ReadMessage() if err != nil { log.Err(err).Msg("websocket read message error") break } if err := conn.WriteMessage(mt, message); err != nil { log.Err(err).Msg("websocket write message error") break } } } } func sseHandler(log *zerolog.Logger) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/event-stream; charset=utf-8") flusher, ok := w.(http.Flusher) if !ok { w.WriteHeader(http.StatusInternalServerError) log.Error().Msgf("Can't support SSE. ResponseWriter %T doesn't implement http.Flusher interface", w) return } freq := defaultSSEFreq if requestedFreq := r.URL.Query()["freq"]; len(requestedFreq) > 0 { parsedFreq, err := time.ParseDuration(requestedFreq[0]) if err == nil { freq = parsedFreq } } log.Info().Msgf("Server Sent Events every %s", freq) ticker := time.NewTicker(freq) counter := 0 for { select { case <-r.Context().Done(): return case <-ticker.C: } _, err := fmt.Fprintf(w, "%d\n\n", counter) if err != nil { return } flusher.Flush() counter++ } } } func healthHandler() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("ok")) } } func rootHandler(serverName string) http.HandlerFunc { responseTemplate := template.Must(template.New("index").Parse(indexTemplate)) return func(w http.ResponseWriter, r *http.Request) { var buffer bytes.Buffer var body string rawBody, err := io.ReadAll(r.Body) if err == nil { body = string(rawBody) } else { body = "" } err = responseTemplate.Execute(&buffer, &templateData{ ServerName: serverName, Request: r, Body: body, }) if err != nil { w.WriteHeader(http.StatusInternalServerError) _, _ = fmt.Fprintf(w, "error: %v", err) } else { _, _ = buffer.WriteTo(w) } } } ================================================ FILE: hello/hello_test.go ================================================ package hello import ( "testing" ) func TestCreateTLSListenerHostAndPortSuccess(t *testing.T) { listener, err := CreateTLSListener("localhost:1234") if err != nil { t.Fatal(err) } defer listener.Close() if listener.Addr().String() == "" { t.Fatal("Fail to find available port") } } func TestCreateTLSListenerOnlyHostSuccess(t *testing.T) { listener, err := CreateTLSListener("localhost:") if err != nil { t.Fatal(err) } defer listener.Close() if listener.Addr().String() == "" { t.Fatal("Fail to find available port") } } func TestCreateTLSListenerOnlyPortSuccess(t *testing.T) { listener, err := CreateTLSListener("localhost:8888") if err != nil { t.Fatal(err) } defer listener.Close() if listener.Addr().String() == "" { t.Fatal("Fail to find available port") } } ================================================ FILE: ingress/config.go ================================================ package ingress import ( "encoding/json" "time" "github.com/urfave/cli/v2" "github.com/cloudflare/cloudflared/config" "github.com/cloudflare/cloudflared/ipaccess" "github.com/cloudflare/cloudflared/tlsconfig" ) var ( defaultHTTPConnectTimeout = config.CustomDuration{Duration: 30 * time.Second} defaultWarpRoutingConnectTimeout = config.CustomDuration{Duration: 5 * time.Second} defaultTLSTimeout = config.CustomDuration{Duration: 10 * time.Second} defaultTCPKeepAlive = config.CustomDuration{Duration: 30 * time.Second} defaultKeepAliveTimeout = config.CustomDuration{Duration: 90 * time.Second} ) const ( defaultProxyAddress = "127.0.0.1" defaultKeepAliveConnections = 100 defaultMaxActiveFlows = 0 // unlimited SSHServerFlag = "ssh-server" Socks5Flag = "socks5" ProxyConnectTimeoutFlag = "proxy-connect-timeout" ProxyTLSTimeoutFlag = "proxy-tls-timeout" ProxyTCPKeepAliveFlag = "proxy-tcp-keepalive" ProxyNoHappyEyeballsFlag = "proxy-no-happy-eyeballs" ProxyKeepAliveConnectionsFlag = "proxy-keepalive-connections" ProxyKeepAliveTimeoutFlag = "proxy-keepalive-timeout" HTTPHostHeaderFlag = "http-host-header" OriginServerNameFlag = "origin-server-name" MatchSNIToHostFlag = "match-sni-to-host" NoTLSVerifyFlag = "no-tls-verify" NoChunkedEncodingFlag = "no-chunked-encoding" ProxyAddressFlag = "proxy-address" ProxyPortFlag = "proxy-port" Http2OriginFlag = "http2-origin" ) const ( socksProxy = "socks" ) type WarpRoutingConfig struct { ConnectTimeout config.CustomDuration `yaml:"connectTimeout" json:"connectTimeout,omitempty"` MaxActiveFlows uint64 `yaml:"maxActiveFlows" json:"MaxActiveFlows,omitempty"` TCPKeepAlive config.CustomDuration `yaml:"tcpKeepAlive" json:"tcpKeepAlive,omitempty"` } func NewWarpRoutingConfig(raw *config.WarpRoutingConfig) WarpRoutingConfig { cfg := WarpRoutingConfig{ ConnectTimeout: defaultWarpRoutingConnectTimeout, MaxActiveFlows: defaultMaxActiveFlows, TCPKeepAlive: defaultTCPKeepAlive, } if raw.ConnectTimeout != nil { cfg.ConnectTimeout = *raw.ConnectTimeout } if raw.MaxActiveFlows != nil { cfg.MaxActiveFlows = *raw.MaxActiveFlows } if raw.TCPKeepAlive != nil { cfg.TCPKeepAlive = *raw.TCPKeepAlive } return cfg } func (c *WarpRoutingConfig) RawConfig() config.WarpRoutingConfig { raw := config.WarpRoutingConfig{} if c.ConnectTimeout.Duration != defaultWarpRoutingConnectTimeout.Duration { raw.ConnectTimeout = &c.ConnectTimeout } if c.MaxActiveFlows != defaultMaxActiveFlows { raw.MaxActiveFlows = &c.MaxActiveFlows } if c.TCPKeepAlive.Duration != defaultTCPKeepAlive.Duration { raw.TCPKeepAlive = &c.TCPKeepAlive } return raw } // RemoteConfig models ingress settings that can be managed remotely, for example through the dashboard. type RemoteConfig struct { Ingress Ingress WarpRouting WarpRoutingConfig } type RemoteConfigJSON struct { GlobalOriginRequest *config.OriginRequestConfig `json:"originRequest,omitempty"` IngressRules []config.UnvalidatedIngressRule `json:"ingress"` WarpRouting config.WarpRoutingConfig `json:"warp-routing"` } func (rc *RemoteConfig) UnmarshalJSON(b []byte) error { var rawConfig RemoteConfigJSON if err := json.Unmarshal(b, &rawConfig); err != nil { return err } // if nil, just assume the default values. globalOriginRequestConfig := rawConfig.GlobalOriginRequest if globalOriginRequestConfig == nil { globalOriginRequestConfig = &config.OriginRequestConfig{} } ingress, err := validateIngress(rawConfig.IngressRules, originRequestFromConfig(*globalOriginRequestConfig)) if err != nil { return err } rc.Ingress = ingress rc.WarpRouting = NewWarpRoutingConfig(&rawConfig.WarpRouting) return nil } func originRequestFromSingleRule(c *cli.Context) OriginRequestConfig { var connectTimeout = defaultHTTPConnectTimeout var tlsTimeout = defaultTLSTimeout var tcpKeepAlive = defaultTCPKeepAlive var noHappyEyeballs bool var keepAliveConnections = defaultKeepAliveConnections var keepAliveTimeout = defaultKeepAliveTimeout var httpHostHeader string var originServerName string var matchSNItoHost bool var caPool string var noTLSVerify bool var disableChunkedEncoding bool var bastionMode bool var proxyAddress = defaultProxyAddress var proxyPort uint var proxyType string var http2Origin bool if flag := ProxyConnectTimeoutFlag; c.IsSet(flag) { connectTimeout = config.CustomDuration{Duration: c.Duration(flag)} } if flag := ProxyTLSTimeoutFlag; c.IsSet(flag) { tlsTimeout = config.CustomDuration{Duration: c.Duration(flag)} } if flag := ProxyTCPKeepAliveFlag; c.IsSet(flag) { tcpKeepAlive = config.CustomDuration{Duration: c.Duration(flag)} } if flag := ProxyNoHappyEyeballsFlag; c.IsSet(flag) { noHappyEyeballs = c.Bool(flag) } if flag := ProxyKeepAliveConnectionsFlag; c.IsSet(flag) { keepAliveConnections = c.Int(flag) } if flag := ProxyKeepAliveTimeoutFlag; c.IsSet(flag) { keepAliveTimeout = config.CustomDuration{Duration: c.Duration(flag)} } if flag := HTTPHostHeaderFlag; c.IsSet(flag) { httpHostHeader = c.String(flag) } if flag := OriginServerNameFlag; c.IsSet(flag) { originServerName = c.String(flag) } if flag := MatchSNIToHostFlag; c.IsSet(flag) { matchSNItoHost = c.Bool(flag) } if flag := tlsconfig.OriginCAPoolFlag; c.IsSet(flag) { caPool = c.String(flag) } if flag := NoTLSVerifyFlag; c.IsSet(flag) { noTLSVerify = c.Bool(flag) } if flag := NoChunkedEncodingFlag; c.IsSet(flag) { disableChunkedEncoding = c.Bool(flag) } if flag := config.BastionFlag; c.IsSet(flag) { bastionMode = c.Bool(flag) } if flag := ProxyAddressFlag; c.IsSet(flag) { proxyAddress = c.String(flag) } if flag := ProxyPortFlag; c.IsSet(flag) { // Note TUN-3758 , we use Int because UInt is not supported with altsrc // nolint: gosec proxyPort = uint(c.Int(flag)) } if flag := Http2OriginFlag; c.IsSet(flag) { http2Origin = c.Bool(flag) } if c.IsSet(Socks5Flag) { proxyType = socksProxy } return OriginRequestConfig{ ConnectTimeout: connectTimeout, TLSTimeout: tlsTimeout, TCPKeepAlive: tcpKeepAlive, NoHappyEyeballs: noHappyEyeballs, KeepAliveConnections: keepAliveConnections, KeepAliveTimeout: keepAliveTimeout, HTTPHostHeader: httpHostHeader, OriginServerName: originServerName, MatchSNIToHost: matchSNItoHost, CAPool: caPool, NoTLSVerify: noTLSVerify, DisableChunkedEncoding: disableChunkedEncoding, BastionMode: bastionMode, ProxyAddress: proxyAddress, ProxyPort: proxyPort, ProxyType: proxyType, Http2Origin: http2Origin, } } func originRequestFromConfig(c config.OriginRequestConfig) OriginRequestConfig { out := OriginRequestConfig{ ConnectTimeout: defaultHTTPConnectTimeout, TLSTimeout: defaultTLSTimeout, TCPKeepAlive: defaultTCPKeepAlive, KeepAliveConnections: defaultKeepAliveConnections, KeepAliveTimeout: defaultKeepAliveTimeout, ProxyAddress: defaultProxyAddress, } if c.ConnectTimeout != nil { out.ConnectTimeout = *c.ConnectTimeout } if c.TLSTimeout != nil { out.TLSTimeout = *c.TLSTimeout } if c.TCPKeepAlive != nil { out.TCPKeepAlive = *c.TCPKeepAlive } if c.NoHappyEyeballs != nil { out.NoHappyEyeballs = *c.NoHappyEyeballs } if c.KeepAliveConnections != nil { out.KeepAliveConnections = *c.KeepAliveConnections } if c.KeepAliveTimeout != nil { out.KeepAliveTimeout = *c.KeepAliveTimeout } if c.HTTPHostHeader != nil { out.HTTPHostHeader = *c.HTTPHostHeader } if c.OriginServerName != nil { out.OriginServerName = *c.OriginServerName } if c.MatchSNIToHost != nil { out.MatchSNIToHost = *c.MatchSNIToHost } if c.CAPool != nil { out.CAPool = *c.CAPool } if c.NoTLSVerify != nil { out.NoTLSVerify = *c.NoTLSVerify } if c.DisableChunkedEncoding != nil { out.DisableChunkedEncoding = *c.DisableChunkedEncoding } if c.BastionMode != nil { out.BastionMode = *c.BastionMode } if c.ProxyAddress != nil { out.ProxyAddress = *c.ProxyAddress } if c.ProxyPort != nil { out.ProxyPort = *c.ProxyPort } if c.ProxyType != nil { out.ProxyType = *c.ProxyType } if len(c.IPRules) > 0 { for _, r := range c.IPRules { rule, err := ipaccess.NewRuleByCIDR(r.Prefix, r.Ports, r.Allow) if err == nil { out.IPRules = append(out.IPRules, rule) } } } if c.Http2Origin != nil { out.Http2Origin = *c.Http2Origin } if c.Access != nil { out.Access = *c.Access } return out } // OriginRequestConfig configures how Cloudflared sends requests to origin // services. // Note: To specify a time.Duration in go-yaml, use e.g. "3s" or "24h". type OriginRequestConfig struct { // HTTP proxy timeout for establishing a new connection ConnectTimeout config.CustomDuration `yaml:"connectTimeout" json:"connectTimeout"` // HTTP proxy timeout for completing a TLS handshake TLSTimeout config.CustomDuration `yaml:"tlsTimeout" json:"tlsTimeout"` // HTTP proxy TCP keepalive duration TCPKeepAlive config.CustomDuration `yaml:"tcpKeepAlive" json:"tcpKeepAlive"` // HTTP proxy should disable "happy eyeballs" for IPv4/v6 fallback NoHappyEyeballs bool `yaml:"noHappyEyeballs" json:"noHappyEyeballs"` // HTTP proxy timeout for closing an idle connection KeepAliveTimeout config.CustomDuration `yaml:"keepAliveTimeout" json:"keepAliveTimeout"` // HTTP proxy maximum keepalive connection pool size KeepAliveConnections int `yaml:"keepAliveConnections" json:"keepAliveConnections"` // Sets the HTTP Host header for the local webserver. HTTPHostHeader string `yaml:"httpHostHeader" json:"httpHostHeader"` // Hostname on the origin server certificate. OriginServerName string `yaml:"originServerName" json:"originServerName"` // Auto configure the Hostname on the origin server certificate. MatchSNIToHost bool `yaml:"matchSNItoHost" json:"matchSNItoHost"` // Path to the CA for the certificate of your origin. // This option should be used only if your certificate is not signed by Cloudflare. CAPool string `yaml:"caPool" json:"caPool"` // Disables TLS verification of the certificate presented by your origin. // Will allow any certificate from the origin to be accepted. // Note: The connection from your machine to Cloudflare's Edge is still encrypted. NoTLSVerify bool `yaml:"noTLSVerify" json:"noTLSVerify"` // Disables chunked transfer encoding. // Useful if you are running a WSGI server. DisableChunkedEncoding bool `yaml:"disableChunkedEncoding" json:"disableChunkedEncoding"` // Runs as jump host BastionMode bool `yaml:"bastionMode" json:"bastionMode"` // Listen address for the proxy. ProxyAddress string `yaml:"proxyAddress" json:"proxyAddress"` // Listen port for the proxy. ProxyPort uint `yaml:"proxyPort" json:"proxyPort"` // What sort of proxy should be started ProxyType string `yaml:"proxyType" json:"proxyType"` // IP rules for the proxy service IPRules []ipaccess.Rule `yaml:"ipRules" json:"ipRules"` // Attempt to connect to origin with HTTP/2 Http2Origin bool `yaml:"http2Origin" json:"http2Origin"` // Access holds all access related configs Access config.AccessConfig `yaml:"access" json:"access,omitempty"` } func (defaults *OriginRequestConfig) setConnectTimeout(overrides config.OriginRequestConfig) { if val := overrides.ConnectTimeout; val != nil { defaults.ConnectTimeout = *val } } func (defaults *OriginRequestConfig) setTLSTimeout(overrides config.OriginRequestConfig) { if val := overrides.TLSTimeout; val != nil { defaults.TLSTimeout = *val } } func (defaults *OriginRequestConfig) setNoHappyEyeballs(overrides config.OriginRequestConfig) { if val := overrides.NoHappyEyeballs; val != nil { defaults.NoHappyEyeballs = *val } } func (defaults *OriginRequestConfig) setKeepAliveConnections(overrides config.OriginRequestConfig) { if val := overrides.KeepAliveConnections; val != nil { defaults.KeepAliveConnections = *val } } func (defaults *OriginRequestConfig) setKeepAliveTimeout(overrides config.OriginRequestConfig) { if val := overrides.KeepAliveTimeout; val != nil { defaults.KeepAliveTimeout = *val } } func (defaults *OriginRequestConfig) setTCPKeepAlive(overrides config.OriginRequestConfig) { if val := overrides.TCPKeepAlive; val != nil { defaults.TCPKeepAlive = *val } } func (defaults *OriginRequestConfig) setHTTPHostHeader(overrides config.OriginRequestConfig) { if val := overrides.HTTPHostHeader; val != nil { defaults.HTTPHostHeader = *val } } func (defaults *OriginRequestConfig) setOriginServerName(overrides config.OriginRequestConfig) { if val := overrides.OriginServerName; val != nil { defaults.OriginServerName = *val } } func (defaults *OriginRequestConfig) setMatchSNIToHost(overrides config.OriginRequestConfig) { if val := overrides.MatchSNIToHost; val != nil { defaults.MatchSNIToHost = *val } } func (defaults *OriginRequestConfig) setCAPool(overrides config.OriginRequestConfig) { if val := overrides.CAPool; val != nil { defaults.CAPool = *val } } func (defaults *OriginRequestConfig) setNoTLSVerify(overrides config.OriginRequestConfig) { if val := overrides.NoTLSVerify; val != nil { defaults.NoTLSVerify = *val } } func (defaults *OriginRequestConfig) setDisableChunkedEncoding(overrides config.OriginRequestConfig) { if val := overrides.DisableChunkedEncoding; val != nil { defaults.DisableChunkedEncoding = *val } } func (defaults *OriginRequestConfig) setBastionMode(overrides config.OriginRequestConfig) { if val := overrides.BastionMode; val != nil { defaults.BastionMode = *val } } func (defaults *OriginRequestConfig) setProxyPort(overrides config.OriginRequestConfig) { if val := overrides.ProxyPort; val != nil { defaults.ProxyPort = *val } } func (defaults *OriginRequestConfig) setProxyAddress(overrides config.OriginRequestConfig) { if val := overrides.ProxyAddress; val != nil { defaults.ProxyAddress = *val } } func (defaults *OriginRequestConfig) setProxyType(overrides config.OriginRequestConfig) { if val := overrides.ProxyType; val != nil { defaults.ProxyType = *val } } func (defaults *OriginRequestConfig) setIPRules(overrides config.OriginRequestConfig) { if val := overrides.IPRules; len(val) > 0 { ipAccessRule := make([]ipaccess.Rule, len(overrides.IPRules)) for i, r := range overrides.IPRules { rule, err := ipaccess.NewRuleByCIDR(r.Prefix, r.Ports, r.Allow) if err == nil { ipAccessRule[i] = rule } } defaults.IPRules = ipAccessRule } } func (defaults *OriginRequestConfig) setHttp2Origin(overrides config.OriginRequestConfig) { if val := overrides.Http2Origin; val != nil { defaults.Http2Origin = *val } } func (defaults *OriginRequestConfig) setAccess(overrides config.OriginRequestConfig) { if val := overrides.Access; val != nil { defaults.Access = *val } } // SetConfig gets config for the requests that cloudflared sends to origins. // Each field has a setter method which sets a value for the field by trying to find: // 1. The user config for this rule // 2. The user config for the overall ingress config // 3. Defaults chosen by the cloudflared team // 4. Golang zero values for that type // // If an earlier option isn't set, it will try the next option down. func setConfig(defaults OriginRequestConfig, overrides config.OriginRequestConfig) OriginRequestConfig { cfg := defaults cfg.setConnectTimeout(overrides) cfg.setTLSTimeout(overrides) cfg.setNoHappyEyeballs(overrides) cfg.setKeepAliveConnections(overrides) cfg.setKeepAliveTimeout(overrides) cfg.setTCPKeepAlive(overrides) cfg.setHTTPHostHeader(overrides) cfg.setOriginServerName(overrides) cfg.setMatchSNIToHost(overrides) cfg.setCAPool(overrides) cfg.setNoTLSVerify(overrides) cfg.setDisableChunkedEncoding(overrides) cfg.setBastionMode(overrides) cfg.setProxyPort(overrides) cfg.setProxyAddress(overrides) cfg.setProxyType(overrides) cfg.setIPRules(overrides) cfg.setHttp2Origin(overrides) cfg.setAccess(overrides) return cfg } func ConvertToRawOriginConfig(c OriginRequestConfig) config.OriginRequestConfig { var connectTimeout *config.CustomDuration var tlsTimeout *config.CustomDuration var tcpKeepAlive *config.CustomDuration var keepAliveConnections *int var keepAliveTimeout *config.CustomDuration var proxyAddress *string var access *config.AccessConfig if c.ConnectTimeout != defaultHTTPConnectTimeout { connectTimeout = &c.ConnectTimeout } if c.TLSTimeout != defaultTLSTimeout { tlsTimeout = &c.TLSTimeout } if c.TCPKeepAlive != defaultTCPKeepAlive { tcpKeepAlive = &c.TCPKeepAlive } if c.KeepAliveConnections != defaultKeepAliveConnections { keepAliveConnections = &c.KeepAliveConnections } if c.KeepAliveTimeout != defaultKeepAliveTimeout { keepAliveTimeout = &c.KeepAliveTimeout } if c.ProxyAddress != defaultProxyAddress { proxyAddress = &c.ProxyAddress } if c.Access.Required { access = &c.Access } return config.OriginRequestConfig{ ConnectTimeout: connectTimeout, TLSTimeout: tlsTimeout, TCPKeepAlive: tcpKeepAlive, NoHappyEyeballs: defaultBoolToNil(c.NoHappyEyeballs), KeepAliveConnections: keepAliveConnections, KeepAliveTimeout: keepAliveTimeout, HTTPHostHeader: emptyStringToNil(c.HTTPHostHeader), OriginServerName: emptyStringToNil(c.OriginServerName), MatchSNIToHost: defaultBoolToNil(c.MatchSNIToHost), CAPool: emptyStringToNil(c.CAPool), NoTLSVerify: defaultBoolToNil(c.NoTLSVerify), DisableChunkedEncoding: defaultBoolToNil(c.DisableChunkedEncoding), BastionMode: defaultBoolToNil(c.BastionMode), ProxyAddress: proxyAddress, ProxyPort: zeroUIntToNil(c.ProxyPort), ProxyType: emptyStringToNil(c.ProxyType), IPRules: convertToRawIPRules(c.IPRules), Http2Origin: defaultBoolToNil(c.Http2Origin), Access: access, } } func convertToRawIPRules(ipRules []ipaccess.Rule) []config.IngressIPRule { result := make([]config.IngressIPRule, 0) for _, r := range ipRules { cidr := r.StringCIDR() newRule := config.IngressIPRule{ Prefix: &cidr, Ports: r.Ports(), Allow: r.RulePolicy(), } result = append(result, newRule) } return result } func defaultBoolToNil(b bool) *bool { if !b { return nil } return &b } func emptyStringToNil(s string) *string { if s == "" { return nil } return &s } func zeroUIntToNil(v uint) *uint { if v == 0 { return nil } return &v } ================================================ FILE: ingress/config_test.go ================================================ package ingress import ( "encoding/json" "flag" "testing" "time" "github.com/stretchr/testify/require" "github.com/urfave/cli/v2" yaml "gopkg.in/yaml.v3" "github.com/cloudflare/cloudflared/config" "github.com/cloudflare/cloudflared/ipaccess" ) // Ensure that the nullable config from `config` package and the // non-nullable config from `ingress` package have the same number of // fields. // This test ensures that programmers didn't add a new field to // one struct and forget to add it to the other ;) func TestCorrespondingFields(t *testing.T) { require.Equal( t, CountFields(t, config.OriginRequestConfig{}), CountFields(t, OriginRequestConfig{}), ) } func CountFields(t *testing.T, val interface{}) int { b, err := yaml.Marshal(val) require.NoError(t, err) m := make(map[string]interface{}, 0) err = yaml.Unmarshal(b, &m) require.NoError(t, err) return len(m) } func TestUnmarshalRemoteConfigOverridesGlobal(t *testing.T) { rawConfig := []byte(` { "originRequest": { "connectTimeout": 90, "noHappyEyeballs": true }, "ingress": [ { "hostname": "jira.cfops.com", "service": "http://192.16.19.1:80", "originRequest": { "noTLSVerify": true, "connectTimeout": 10 } }, { "service": "http_status:404" } ], "warp-routing": { "enabled": true } } `) var remoteConfig RemoteConfig err := json.Unmarshal(rawConfig, &remoteConfig) require.NoError(t, err) require.True(t, remoteConfig.Ingress.Rules[0].Config.NoTLSVerify) require.True(t, remoteConfig.Ingress.Defaults.NoHappyEyeballs) } func TestOriginRequestConfigOverrides(t *testing.T) { validate := func(ing Ingress) { // Rule 0 didn't override anything, so it inherits the user-specified // root-level configuration. actual0 := ing.Rules[0].Config expected0 := OriginRequestConfig{ ConnectTimeout: config.CustomDuration{Duration: 1 * time.Minute}, TLSTimeout: config.CustomDuration{Duration: 1 * time.Second}, TCPKeepAlive: config.CustomDuration{Duration: 1 * time.Second}, NoHappyEyeballs: true, KeepAliveTimeout: config.CustomDuration{Duration: 1 * time.Second}, KeepAliveConnections: 1, HTTPHostHeader: "abc", OriginServerName: "a1", CAPool: "/tmp/path0", NoTLSVerify: true, DisableChunkedEncoding: true, BastionMode: true, ProxyAddress: "127.1.2.3", ProxyPort: uint(100), ProxyType: "socks5", IPRules: []ipaccess.Rule{ newIPRule(t, "10.0.0.0/8", []int{80, 8080}, false), newIPRule(t, "fc00::/7", []int{443, 4443}, true), }, } require.Equal(t, expected0, actual0) // Rule 1 overrode all the root-level config. actual1 := ing.Rules[1].Config expected1 := OriginRequestConfig{ ConnectTimeout: config.CustomDuration{Duration: 2 * time.Minute}, TLSTimeout: config.CustomDuration{Duration: 2 * time.Second}, TCPKeepAlive: config.CustomDuration{Duration: 2 * time.Second}, NoHappyEyeballs: false, KeepAliveTimeout: config.CustomDuration{Duration: 2 * time.Second}, KeepAliveConnections: 2, HTTPHostHeader: "def", OriginServerName: "b2", CAPool: "/tmp/path1", NoTLSVerify: false, DisableChunkedEncoding: false, BastionMode: false, ProxyAddress: "interface", ProxyPort: uint(200), ProxyType: "", IPRules: []ipaccess.Rule{ newIPRule(t, "10.0.0.0/16", []int{3000, 3030}, false), newIPRule(t, "192.16.0.0/24", []int{5000, 5050}, true), }, } require.Equal(t, expected1, actual1) } rulesYAML := ` originRequest: connectTimeout: 1m tlsTimeout: 1s noHappyEyeballs: true tcpKeepAlive: 1s keepAliveConnections: 1 keepAliveTimeout: 1s httpHostHeader: abc originServerName: a1 caPool: /tmp/path0 noTLSVerify: true disableChunkedEncoding: true bastionMode: True proxyAddress: 127.1.2.3 proxyPort: 100 proxyType: socks5 ipRules: - prefix: "10.0.0.0/8" ports: - 80 - 8080 allow: false - prefix: "fc00::/7" ports: - 443 - 4443 allow: true ingress: - hostname: tun.example.com service: https://localhost:8000 - hostname: "*" service: https://localhost:8001 originRequest: connectTimeout: 2m tlsTimeout: 2s noHappyEyeballs: false tcpKeepAlive: 2s keepAliveConnections: 2 keepAliveTimeout: 2s httpHostHeader: def originServerName: b2 caPool: /tmp/path1 noTLSVerify: false disableChunkedEncoding: false bastionMode: false proxyAddress: interface proxyPort: 200 proxyType: "" ipRules: - prefix: "10.0.0.0/16" ports: - 3000 - 3030 allow: false - prefix: "192.16.0.0/24" ports: - 5000 - 5050 allow: true ` ing, err := ParseIngress(MustReadIngress(rulesYAML)) require.NoError(t, err) validate(ing) rawConfig := []byte(` { "originRequest": { "connectTimeout": 60, "tlsTimeout": 1, "noHappyEyeballs": true, "tcpKeepAlive": 1, "keepAliveConnections": 1, "keepAliveTimeout": 1, "httpHostHeader": "abc", "originServerName": "a1", "caPool": "/tmp/path0", "noTLSVerify": true, "disableChunkedEncoding": true, "bastionMode": true, "proxyAddress": "127.1.2.3", "proxyPort": 100, "proxyType": "socks5", "ipRules": [ { "prefix": "10.0.0.0/8", "ports": [80, 8080], "allow": false }, { "prefix": "fc00::/7", "ports": [443, 4443], "allow": true } ] }, "ingress": [ { "hostname": "tun.example.com", "service": "https://localhost:8000" }, { "hostname": "*", "service": "https://localhost:8001", "originRequest": { "connectTimeout": 120, "tlsTimeout": 2, "noHappyEyeballs": false, "tcpKeepAlive": 2, "keepAliveConnections": 2, "keepAliveTimeout": 2, "httpHostHeader": "def", "originServerName": "b2", "caPool": "/tmp/path1", "noTLSVerify": false, "disableChunkedEncoding": false, "bastionMode": false, "proxyAddress": "interface", "proxyPort": 200, "proxyType": "", "ipRules": [ { "prefix": "10.0.0.0/16", "ports": [3000, 3030], "allow": false }, { "prefix": "192.16.0.0/24", "ports": [5000, 5050], "allow": true } ] } } ], "warp-routing": { "enabled": true } } `) var remoteConfig RemoteConfig err = json.Unmarshal(rawConfig, &remoteConfig) require.NoError(t, err) validate(remoteConfig.Ingress) } func TestOriginRequestConfigDefaults(t *testing.T) { validate := func(ing Ingress) { // Rule 0 didn't override anything, so it inherits the cloudflared defaults actual0 := ing.Rules[0].Config expected0 := OriginRequestConfig{ ConnectTimeout: defaultHTTPConnectTimeout, TLSTimeout: defaultTLSTimeout, TCPKeepAlive: defaultTCPKeepAlive, KeepAliveConnections: defaultKeepAliveConnections, KeepAliveTimeout: defaultKeepAliveTimeout, ProxyAddress: defaultProxyAddress, } require.Equal(t, expected0, actual0) // Rule 1 overrode all defaults. actual1 := ing.Rules[1].Config expected1 := OriginRequestConfig{ ConnectTimeout: config.CustomDuration{Duration: 2 * time.Minute}, TLSTimeout: config.CustomDuration{Duration: 2 * time.Second}, TCPKeepAlive: config.CustomDuration{Duration: 2 * time.Second}, NoHappyEyeballs: false, KeepAliveTimeout: config.CustomDuration{Duration: 2 * time.Second}, KeepAliveConnections: 2, HTTPHostHeader: "def", OriginServerName: "b2", CAPool: "/tmp/path1", NoTLSVerify: false, DisableChunkedEncoding: false, BastionMode: false, ProxyAddress: "interface", ProxyPort: uint(200), ProxyType: "", IPRules: []ipaccess.Rule{ newIPRule(t, "10.0.0.0/16", []int{3000, 3030}, false), newIPRule(t, "192.16.0.0/24", []int{5000, 5050}, true), }, } require.Equal(t, expected1, actual1) } rulesYAML := ` ingress: - hostname: tun.example.com service: https://localhost:8000 - hostname: "*" service: https://localhost:8001 originRequest: connectTimeout: 2m tlsTimeout: 2s noHappyEyeballs: false tcpKeepAlive: 2s keepAliveConnections: 2 keepAliveTimeout: 2s httpHostHeader: def originServerName: b2 caPool: /tmp/path1 noTLSVerify: false disableChunkedEncoding: false bastionMode: false proxyAddress: interface proxyPort: 200 proxyType: "" ipRules: - prefix: "10.0.0.0/16" ports: - 3000 - 3030 allow: false - prefix: "192.16.0.0/24" ports: - 5000 - 5050 allow: true ` ing, err := ParseIngress(MustReadIngress(rulesYAML)) if err != nil { t.Error(err) } validate(ing) rawConfig := []byte(` { "ingress": [ { "hostname": "tun.example.com", "service": "https://localhost:8000" }, { "hostname": "*", "service": "https://localhost:8001", "originRequest": { "connectTimeout": 120, "tlsTimeout": 2, "noHappyEyeballs": false, "tcpKeepAlive": 2, "keepAliveConnections": 2, "keepAliveTimeout": 2, "httpHostHeader": "def", "originServerName": "b2", "caPool": "/tmp/path1", "noTLSVerify": false, "disableChunkedEncoding": false, "bastionMode": false, "proxyAddress": "interface", "proxyPort": 200, "proxyType": "", "ipRules": [ { "prefix": "10.0.0.0/16", "ports": [3000, 3030], "allow": false }, { "prefix": "192.16.0.0/24", "ports": [5000, 5050], "allow": true } ] } } ] } `) var remoteConfig RemoteConfig err = json.Unmarshal(rawConfig, &remoteConfig) require.NoError(t, err) validate(remoteConfig.Ingress) } func TestDefaultConfigFromCLI(t *testing.T) { set := flag.NewFlagSet("contrive", 0) c := cli.NewContext(nil, set, nil) expected := OriginRequestConfig{ ConnectTimeout: defaultHTTPConnectTimeout, TLSTimeout: defaultTLSTimeout, TCPKeepAlive: defaultTCPKeepAlive, KeepAliveConnections: defaultKeepAliveConnections, KeepAliveTimeout: defaultKeepAliveTimeout, ProxyAddress: defaultProxyAddress, } actual := originRequestFromSingleRule(c) require.Equal(t, expected, actual) } func newIPRule(t *testing.T, prefix string, ports []int, allow bool) ipaccess.Rule { rule, err := ipaccess.NewRuleByCIDR(&prefix, ports, allow) require.NoError(t, err) return rule } ================================================ FILE: ingress/constants_test.go ================================================ package ingress import "github.com/cloudflare/cloudflared/logger" var ( TestLogger = logger.Create(nil) ) ================================================ FILE: ingress/icmp_darwin.go ================================================ //go:build darwin package ingress // This file implements ICMPProxy for Darwin. It uses a non-privileged ICMP socket to send echo requests and listen for // echo replies. The source IP of the requests are rewritten to the bind IP of the socket and the socket reads all // messages, so we use echo ID to distinguish the replies. Each (source IP, destination IP, echo ID) is assigned a // unique echo ID. import ( "context" "fmt" "math" "net/netip" "strconv" "sync" "time" "github.com/rs/zerolog" "go.opentelemetry.io/otel/attribute" "golang.org/x/net/icmp" "github.com/cloudflare/cloudflared/packet" "github.com/cloudflare/cloudflared/tracing" ) type icmpProxy struct { srcFunnelTracker *packet.FunnelTracker echoIDTracker *echoIDTracker conn *icmp.PacketConn logger *zerolog.Logger idleTimeout time.Duration } // echoIDTracker tracks which ID has been assigned. It first loops through assignment from lastAssignment to then end, // then from the beginning to lastAssignment. // ICMP echo are short lived. By the time an ID is revisited, it should have been released. type echoIDTracker struct { lock sync.Mutex // maps the source IP, destination IP and original echo ID to a unique echo ID obtained from assignment mapping map[flow3Tuple]uint16 // assignment tracks if an ID is assigned using index as the ID // The size of the array is math.MaxUint16 because echo ID is 2 bytes assignment [math.MaxUint16]bool // nextAssignment is the next number to check for assigment nextAssignment uint16 } func newEchoIDTracker() *echoIDTracker { return &echoIDTracker{ mapping: make(map[flow3Tuple]uint16), } } // Get assignment or assign a new ID. func (eit *echoIDTracker) getOrAssign(key flow3Tuple) (id uint16, success bool) { eit.lock.Lock() defer eit.lock.Unlock() id, exists := eit.mapping[key] if exists { return id, true } if eit.nextAssignment == math.MaxUint16 { eit.nextAssignment = 0 } for i, assigned := range eit.assignment[eit.nextAssignment:] { if !assigned { echoID := uint16(i) + eit.nextAssignment eit.set(key, echoID) return echoID, true } } for i, assigned := range eit.assignment[0:eit.nextAssignment] { if !assigned { echoID := uint16(i) eit.set(key, echoID) return echoID, true } } return 0, false } // Caller should hold the lock func (eit *echoIDTracker) set(key flow3Tuple, assignedEchoID uint16) { eit.assignment[assignedEchoID] = true eit.mapping[key] = assignedEchoID eit.nextAssignment = assignedEchoID + 1 } func (eit *echoIDTracker) release(key flow3Tuple, assigned uint16) bool { eit.lock.Lock() defer eit.lock.Unlock() currentEchoID, exists := eit.mapping[key] if exists && assigned == currentEchoID { delete(eit.mapping, key) eit.assignment[assigned] = false return true } return false } type echoFunnelID uint16 func (snf echoFunnelID) Type() string { return "echoID" } func (snf echoFunnelID) String() string { return strconv.FormatUint(uint64(snf), 10) } func newICMPProxy(listenIP netip.Addr, logger *zerolog.Logger, idleTimeout time.Duration) (*icmpProxy, error) { conn, err := newICMPConn(listenIP) if err != nil { return nil, err } logger.Info().Msgf("Created ICMP proxy listening on %s", conn.LocalAddr()) return &icmpProxy{ srcFunnelTracker: packet.NewFunnelTracker(), echoIDTracker: newEchoIDTracker(), conn: conn, logger: logger, idleTimeout: idleTimeout, }, nil } func (ip *icmpProxy) Request(ctx context.Context, pk *packet.ICMP, responder ICMPResponder) error { _, span := responder.RequestSpan(ctx, pk) defer responder.ExportSpan() originalEcho, err := getICMPEcho(pk.Message) if err != nil { tracing.EndWithErrorStatus(span, err) return err } observeICMPRequest(ip.logger, span, pk.Src.String(), pk.Dst.String(), originalEcho.ID, originalEcho.Seq) echoIDTrackerKey := flow3Tuple{ srcIP: pk.Src, dstIP: pk.Dst, originalEchoID: originalEcho.ID, } assignedEchoID, success := ip.echoIDTracker.getOrAssign(echoIDTrackerKey) if !success { err := fmt.Errorf("failed to assign unique echo ID") tracing.EndWithErrorStatus(span, err) return err } span.SetAttributes(attribute.Int("assignedEchoID", int(assignedEchoID))) shouldReplaceFunnelFunc := createShouldReplaceFunnelFunc(ip.logger, responder, pk, originalEcho.ID) newFunnelFunc := func() (packet.Funnel, error) { originalEcho, err := getICMPEcho(pk.Message) if err != nil { return nil, err } closeCallback := func() error { ip.echoIDTracker.release(echoIDTrackerKey, assignedEchoID) return nil } icmpFlow := newICMPEchoFlow(pk.Src, closeCallback, ip.conn, responder, int(assignedEchoID), originalEcho.ID) return icmpFlow, nil } funnelID := echoFunnelID(assignedEchoID) funnel, isNew, err := ip.srcFunnelTracker.GetOrRegister(funnelID, shouldReplaceFunnelFunc, newFunnelFunc) if err != nil { tracing.EndWithErrorStatus(span, err) return err } if isNew { span.SetAttributes(attribute.Bool("newFlow", true)) ip.logger.Debug(). Str("src", pk.Src.String()). Str("dst", pk.Dst.String()). Int("originalEchoID", originalEcho.ID). Int("assignedEchoID", int(assignedEchoID)). Msg("New flow") } icmpFlow, err := toICMPEchoFlow(funnel) if err != nil { tracing.EndWithErrorStatus(span, err) return err } err = icmpFlow.sendToDst(pk.Dst, pk.Message) if err != nil { tracing.EndWithErrorStatus(span, err) return err } tracing.End(span) return nil } // Serve listens for responses to the requests until context is done func (ip *icmpProxy) Serve(ctx context.Context) error { go func() { <-ctx.Done() ip.conn.Close() }() go func() { ip.srcFunnelTracker.ScheduleCleanup(ctx, ip.idleTimeout) }() buf := make([]byte, mtu) icmpDecoder := packet.NewICMPDecoder() for { n, from, err := ip.conn.ReadFrom(buf) if err != nil { return err } reply, err := parseReply(from, buf[:n]) if err != nil { ip.logger.Debug().Err(err).Str("dst", from.String()).Msg("Failed to parse ICMP reply, continue to parse as full packet") // In unit test, we found out when the listener listens on 0.0.0.0, the socket reads the full packet after // the second reply if err := ip.handleFullPacket(ctx, icmpDecoder, buf[:n]); err != nil { ip.logger.Debug().Err(err).Str("dst", from.String()).Msg("Failed to parse ICMP reply as full packet") } continue } if !isEchoReply(reply.msg) { ip.logger.Debug().Str("dst", from.String()).Msgf("Drop ICMP %s from reply", reply.msg.Type) continue } if err := ip.sendReply(ctx, reply); err != nil { ip.logger.Debug().Err(err).Str("dst", from.String()).Msg("Failed to send ICMP reply") continue } } } func (ip *icmpProxy) handleFullPacket(ctx context.Context, decoder *packet.ICMPDecoder, rawPacket []byte) error { icmpPacket, err := decoder.Decode(packet.RawPacket{Data: rawPacket}) if err != nil { return err } echo, err := getICMPEcho(icmpPacket.Message) if err != nil { return err } reply := echoReply{ from: icmpPacket.Src, msg: icmpPacket.Message, echo: echo, } if ip.sendReply(ctx, &reply); err != nil { return err } return nil } func (ip *icmpProxy) sendReply(ctx context.Context, reply *echoReply) error { funnelID := echoFunnelID(reply.echo.ID) funnel, ok := ip.srcFunnelTracker.Get(funnelID) if !ok { return packet.ErrFunnelNotFound } icmpFlow, err := toICMPEchoFlow(funnel) if err != nil { return err } _, span := icmpFlow.responder.ReplySpan(ctx, ip.logger) defer icmpFlow.responder.ExportSpan() if err := icmpFlow.returnToSrc(reply); err != nil { tracing.EndWithErrorStatus(span, err) return err } observeICMPReply(ip.logger, span, reply.from.String(), reply.echo.ID, reply.echo.Seq) span.SetAttributes(attribute.Int("originalEchoID", icmpFlow.originalEchoID)) tracing.End(span) return nil } ================================================ FILE: ingress/icmp_darwin_test.go ================================================ //go:build darwin package ingress import ( "math" "net/netip" "testing" "github.com/stretchr/testify/require" "github.com/cloudflare/cloudflared/packet" ) func TestSingleEchoIDTracker(t *testing.T) { tracker := newEchoIDTracker() key := flow3Tuple{ srcIP: netip.MustParseAddr("172.16.0.1"), dstIP: netip.MustParseAddr("172.16.0.2"), originalEchoID: 5182, } // not assigned yet, so nothing to release require.False(t, tracker.release(key, 0)) echoID, ok := tracker.getOrAssign(key) require.True(t, ok) require.Equal(t, uint16(0), echoID) // Second time should return the same echo ID echoID, ok = tracker.getOrAssign(key) require.True(t, ok) require.Equal(t, uint16(0), echoID) // releasing a different ID returns false require.False(t, tracker.release(key, 1999)) require.True(t, tracker.release(key, echoID)) // releasing the second time returns false require.False(t, tracker.release(key, echoID)) // Move to the next IP echoID, ok = tracker.getOrAssign(key) require.True(t, ok) require.Equal(t, uint16(1), echoID) } func TestFullEchoIDTracker(t *testing.T) { var ( dstIP = netip.MustParseAddr("192.168.0.1") originalEchoID = 41820 ) tracker := newEchoIDTracker() firstSrcIP := netip.MustParseAddr("172.16.0.1") srcIP := firstSrcIP for i := uint16(0); i < math.MaxUint16; i++ { key := flow3Tuple{ srcIP: srcIP, dstIP: dstIP, originalEchoID: originalEchoID, } echoID, ok := tracker.getOrAssign(key) require.True(t, ok) require.Equal(t, i, echoID) echoID, ok = tracker.get(key) require.True(t, ok) require.Equal(t, i, echoID) srcIP = srcIP.Next() } key := flow3Tuple{ srcIP: srcIP.Next(), dstIP: dstIP, originalEchoID: originalEchoID, } // All echo IDs are assigned echoID, ok := tracker.getOrAssign(key) require.False(t, ok) require.Equal(t, uint16(0), echoID) srcIP = firstSrcIP for i := uint16(0); i < math.MaxUint16; i++ { key := flow3Tuple{ srcIP: srcIP, dstIP: dstIP, originalEchoID: originalEchoID, } ok := tracker.release(key, i) require.True(t, ok) echoID, ok = tracker.get(key) require.False(t, ok) require.Equal(t, uint16(0), echoID) srcIP = srcIP.Next() } // The IDs are assignable again srcIP = firstSrcIP for i := uint16(0); i < math.MaxUint16; i++ { key := flow3Tuple{ srcIP: srcIP, dstIP: dstIP, originalEchoID: originalEchoID, } echoID, ok := tracker.getOrAssign(key) require.True(t, ok) require.Equal(t, i, echoID) echoID, ok = tracker.get(key) require.True(t, ok) require.Equal(t, i, echoID) srcIP = srcIP.Next() } } func (eit *echoIDTracker) get(key flow3Tuple) (id uint16, exist bool) { eit.lock.Lock() defer eit.lock.Unlock() id, exists := eit.mapping[key] return id, exists } func getFunnel(t *testing.T, proxy *icmpProxy, tuple flow3Tuple) (packet.Funnel, bool) { assignedEchoID, success := proxy.echoIDTracker.getOrAssign(tuple) require.True(t, success) return proxy.srcFunnelTracker.Get(echoFunnelID(assignedEchoID)) } ================================================ FILE: ingress/icmp_generic.go ================================================ //go:build !darwin && !linux && (!windows || !cgo) package ingress import ( "context" "fmt" "net/netip" "runtime" "time" "github.com/rs/zerolog" "github.com/cloudflare/cloudflared/packet" ) var errICMPProxyNotImplemented = fmt.Errorf("ICMP proxy is not implemented on %s %s", runtime.GOOS, runtime.GOARCH) type icmpProxy struct{} func (ip icmpProxy) Request(ctx context.Context, pk *packet.ICMP, responder ICMPResponder) error { return errICMPProxyNotImplemented } func (ip *icmpProxy) Serve(ctx context.Context) error { return errICMPProxyNotImplemented } func newICMPProxy(listenIP netip.Addr, logger *zerolog.Logger, idleTimeout time.Duration) (*icmpProxy, error) { return nil, errICMPProxyNotImplemented } ================================================ FILE: ingress/icmp_linux.go ================================================ //go:build linux package ingress // This file implements ICMPProxy for Linux. Each (source IP, destination IP, echo ID) opens a non-privileged ICMP socket. // The source IP of the requests are rewritten to the bind IP of the socket and echo ID rewritten to the port number of // the socket. The kernel ensures the socket only reads replies whose echo ID matches the port number. // For more information about the socket, see https://man7.org/linux/man-pages/man7/icmp.7.html and https://lwn.net/Articles/422330/ import ( "context" "fmt" "net" "net/netip" "os" "regexp" "strconv" "time" "github.com/pkg/errors" "github.com/rs/zerolog" "go.opentelemetry.io/otel/attribute" "github.com/cloudflare/cloudflared/packet" "github.com/cloudflare/cloudflared/tracing" ) const ( // https://lwn.net/Articles/550551/ IPv4 and IPv6 share the same path pingGroupPath = "/proc/sys/net/ipv4/ping_group_range" ) var ( findGroupIDRegex = regexp.MustCompile(`\d+`) ) type icmpProxy struct { srcFunnelTracker *packet.FunnelTracker listenIP netip.Addr logger *zerolog.Logger idleTimeout time.Duration } func newICMPProxy(listenIP netip.Addr, logger *zerolog.Logger, idleTimeout time.Duration) (*icmpProxy, error) { if err := testPermission(listenIP, logger); err != nil { return nil, err } return &icmpProxy{ srcFunnelTracker: packet.NewFunnelTracker(), listenIP: listenIP, logger: logger, idleTimeout: idleTimeout, }, nil } func testPermission(listenIP netip.Addr, logger *zerolog.Logger) error { // Opens a non-privileged ICMP socket. On Linux the group ID of the process needs to be in ping_group_range // Only check ping_group_range once for IPv4 if listenIP.Is4() { if err := checkInPingGroup(); err != nil { logger.Warn().Err(err).Msgf("The user running cloudflared process has a GID (group ID) that is not within ping_group_range. You might need to add that user to a group within that range, or instead update the range to encompass a group the user is already in by modifying %s. Otherwise cloudflared will not be able to ping this network", pingGroupPath) return err } } conn, err := newICMPConn(listenIP) if err != nil { return err } // This conn is only to test if cloudflared has permission to open this type of socket conn.Close() return nil } func checkInPingGroup() error { file, err := os.ReadFile(pingGroupPath) if err != nil { return err } groupID := uint64(os.Getegid()) // Example content: 999 59999 found := findGroupIDRegex.FindAll(file, 2) if len(found) == 2 { groupMin, err := strconv.ParseUint(string(found[0]), 10, 32) if err != nil { return errors.Wrapf(err, "failed to determine minimum ping group ID") } groupMax, err := strconv.ParseUint(string(found[1]), 10, 32) if err != nil { return errors.Wrapf(err, "failed to determine maximum ping group ID") } if groupID < groupMin || groupID > groupMax { return fmt.Errorf("Group ID %d is not between ping group %d to %d", groupID, groupMin, groupMax) } return nil } return fmt.Errorf("did not find group range in %s", pingGroupPath) } func (ip *icmpProxy) Request(ctx context.Context, pk *packet.ICMP, responder ICMPResponder) error { ctx, span := responder.RequestSpan(ctx, pk) defer responder.ExportSpan() originalEcho, err := getICMPEcho(pk.Message) if err != nil { tracing.EndWithErrorStatus(span, err) return err } observeICMPRequest(ip.logger, span, pk.Src.String(), pk.Dst.String(), originalEcho.ID, originalEcho.Seq) shouldReplaceFunnelFunc := createShouldReplaceFunnelFunc(ip.logger, responder, pk, originalEcho.ID) newFunnelFunc := func() (packet.Funnel, error) { conn, err := newICMPConn(ip.listenIP) if err != nil { tracing.EndWithErrorStatus(span, err) return nil, errors.Wrap(err, "failed to open ICMP socket") } ip.logger.Debug().Msgf("Opened ICMP socket listen on %s", conn.LocalAddr()) closeCallback := func() error { return conn.Close() } localUDPAddr, ok := conn.LocalAddr().(*net.UDPAddr) if !ok { return nil, fmt.Errorf("ICMP listener address %s is not net.UDPAddr", conn.LocalAddr()) } span.SetAttributes(attribute.Int("port", localUDPAddr.Port)) echoID := localUDPAddr.Port icmpFlow := newICMPEchoFlow(pk.Src, closeCallback, conn, responder, echoID, originalEcho.ID) return icmpFlow, nil } funnelID := flow3Tuple{ srcIP: pk.Src, dstIP: pk.Dst, originalEchoID: originalEcho.ID, } funnel, isNew, err := ip.srcFunnelTracker.GetOrRegister(funnelID, shouldReplaceFunnelFunc, newFunnelFunc) if err != nil { tracing.EndWithErrorStatus(span, err) return err } icmpFlow, err := toICMPEchoFlow(funnel) if err != nil { tracing.EndWithErrorStatus(span, err) return err } if isNew { span.SetAttributes(attribute.Bool("newFlow", true)) ip.logger.Debug(). Str("src", pk.Src.String()). Str("dst", pk.Dst.String()). Int("originalEchoID", originalEcho.ID). Msg("New flow") go func() { ip.listenResponse(ctx, icmpFlow) ip.srcFunnelTracker.Unregister(funnelID, icmpFlow) }() } if err := icmpFlow.sendToDst(pk.Dst, pk.Message); err != nil { tracing.EndWithErrorStatus(span, err) return errors.Wrap(err, "failed to send ICMP echo request") } tracing.End(span) return nil } func (ip *icmpProxy) Serve(ctx context.Context) error { ip.srcFunnelTracker.ScheduleCleanup(ctx, ip.idleTimeout) return ctx.Err() } func (ip *icmpProxy) listenResponse(ctx context.Context, flow *icmpEchoFlow) { buf := make([]byte, mtu) for { if done := ip.handleResponse(ctx, flow, buf); done { return } } } // Listens for ICMP response and handles error logging func (ip *icmpProxy) handleResponse(ctx context.Context, flow *icmpEchoFlow, buf []byte) (done bool) { _, span := flow.responder.ReplySpan(ctx, ip.logger) defer flow.responder.ExportSpan() span.SetAttributes( attribute.Int("originalEchoID", flow.originalEchoID), ) n, from, err := flow.originConn.ReadFrom(buf) if err != nil { if flow.IsClosed() { tracing.EndWithErrorStatus(span, fmt.Errorf("flow was closed")) return true } ip.logger.Error().Err(err).Str("socket", flow.originConn.LocalAddr().String()).Msg("Failed to read from ICMP socket") tracing.EndWithErrorStatus(span, err) return true } reply, err := parseReply(from, buf[:n]) if err != nil { ip.logger.Error().Err(err).Str("dst", from.String()).Msg("Failed to parse ICMP reply") tracing.EndWithErrorStatus(span, err) return false } if !isEchoReply(reply.msg) { err := fmt.Errorf("Expect ICMP echo reply, got %s", reply.msg.Type) ip.logger.Debug().Str("dst", from.String()).Msgf("Drop ICMP %s from reply", reply.msg.Type) tracing.EndWithErrorStatus(span, err) return false } if err := flow.returnToSrc(reply); err != nil { ip.logger.Error().Err(err).Str("dst", from.String()).Msg("Failed to send ICMP reply") tracing.EndWithErrorStatus(span, err) return false } observeICMPReply(ip.logger, span, from.String(), reply.echo.ID, reply.echo.Seq) tracing.End(span) return false } // Only linux uses flow3Tuple as FunnelID func (ft flow3Tuple) Type() string { return "srcIP_dstIP_echoID" } func (ft flow3Tuple) String() string { return fmt.Sprintf("%s:%s:%d", ft.srcIP, ft.dstIP, ft.originalEchoID) } ================================================ FILE: ingress/icmp_linux_test.go ================================================ //go:build linux package ingress import ( "testing" "github.com/cloudflare/cloudflared/packet" ) func getFunnel(t *testing.T, proxy *icmpProxy, tuple flow3Tuple) (packet.Funnel, bool) { return proxy.srcFunnelTracker.Get(tuple) } ================================================ FILE: ingress/icmp_metrics.go ================================================ package ingress import ( "github.com/prometheus/client_golang/prometheus" ) const ( namespace = "cloudflared" ) var ( icmpRequests = prometheus.NewCounter(prometheus.CounterOpts{ Namespace: namespace, Subsystem: "icmp", Name: "total_requests", Help: "Total count of ICMP requests that have been proxied to any origin", }) icmpReplies = prometheus.NewCounter(prometheus.CounterOpts{ Namespace: namespace, Subsystem: "icmp", Name: "total_replies", Help: "Total count of ICMP replies that have been proxied from any origin", }) ) func init() { prometheus.MustRegister( icmpRequests, icmpReplies, ) } func incrementICMPRequest() { icmpRequests.Inc() } func incrementICMPReply() { icmpReplies.Inc() } ================================================ FILE: ingress/icmp_posix.go ================================================ //go:build darwin || linux package ingress // This file extracts logic shared by Linux and Darwin implementation if ICMPProxy. import ( "fmt" "net" "net/netip" "sync/atomic" "github.com/google/gopacket/layers" "github.com/rs/zerolog" "golang.org/x/net/icmp" "github.com/cloudflare/cloudflared/packet" ) // Opens a non-privileged ICMP socket on Linux and Darwin func newICMPConn(listenIP netip.Addr) (*icmp.PacketConn, error) { if listenIP.Is4() { return icmp.ListenPacket("udp4", listenIP.String()) } return icmp.ListenPacket("udp6", listenIP.String()) } func netipAddr(addr net.Addr) (netip.Addr, bool) { udpAddr, ok := addr.(*net.UDPAddr) if !ok { return netip.Addr{}, false } return udpAddr.AddrPort().Addr(), true } type flow3Tuple struct { srcIP netip.Addr dstIP netip.Addr originalEchoID int } // icmpEchoFlow implements the packet.Funnel interface. type icmpEchoFlow struct { *packet.ActivityTracker closeCallback func() error closed *atomic.Bool src netip.Addr originConn *icmp.PacketConn responder ICMPResponder assignedEchoID int originalEchoID int } func newICMPEchoFlow(src netip.Addr, closeCallback func() error, originConn *icmp.PacketConn, responder ICMPResponder, assignedEchoID, originalEchoID int) *icmpEchoFlow { return &icmpEchoFlow{ ActivityTracker: packet.NewActivityTracker(), closeCallback: closeCallback, closed: &atomic.Bool{}, src: src, originConn: originConn, responder: responder, assignedEchoID: assignedEchoID, originalEchoID: originalEchoID, } } func (ief *icmpEchoFlow) Equal(other packet.Funnel) bool { otherICMPFlow, ok := other.(*icmpEchoFlow) if !ok { return false } if otherICMPFlow.src != ief.src { return false } if otherICMPFlow.originalEchoID != ief.originalEchoID { return false } if otherICMPFlow.assignedEchoID != ief.assignedEchoID { return false } return true } func (ief *icmpEchoFlow) Close() error { ief.closed.Store(true) return ief.closeCallback() } func (ief *icmpEchoFlow) IsClosed() bool { return ief.closed.Load() } // sendToDst rewrites the echo ID to the one assigned to this flow func (ief *icmpEchoFlow) sendToDst(dst netip.Addr, msg *icmp.Message) error { ief.UpdateLastActive() originalEcho, err := getICMPEcho(msg) if err != nil { return err } sendMsg := icmp.Message{ Type: msg.Type, Code: msg.Code, Body: &icmp.Echo{ ID: ief.assignedEchoID, Seq: originalEcho.Seq, Data: originalEcho.Data, }, } // For IPv4, the pseudoHeader is not used because the checksum is always calculated var pseudoHeader []byte = nil serializedPacket, err := sendMsg.Marshal(pseudoHeader) if err != nil { return err } _, err = ief.originConn.WriteTo(serializedPacket, &net.UDPAddr{ IP: dst.AsSlice(), }) return err } // returnToSrc rewrites the echo ID to the original echo ID from the eyeball func (ief *icmpEchoFlow) returnToSrc(reply *echoReply) error { ief.UpdateLastActive() reply.echo.ID = ief.originalEchoID reply.msg.Body = reply.echo pk := packet.ICMP{ IP: &packet.IP{ Src: reply.from, Dst: ief.src, Protocol: layers.IPProtocol(reply.msg.Type.Protocol()), TTL: packet.DefaultTTL, }, Message: reply.msg, } return ief.responder.ReturnPacket(&pk) } type echoReply struct { from netip.Addr msg *icmp.Message echo *icmp.Echo } func parseReply(from net.Addr, rawMsg []byte) (*echoReply, error) { fromAddr, ok := netipAddr(from) if !ok { return nil, fmt.Errorf("cannot convert %s to netip.Addr", from) } proto := layers.IPProtocolICMPv4 if fromAddr.Is6() { proto = layers.IPProtocolICMPv6 } msg, err := icmp.ParseMessage(int(proto), rawMsg) if err != nil { return nil, err } echo, err := getICMPEcho(msg) if err != nil { return nil, err } return &echoReply{ from: fromAddr, msg: msg, echo: echo, }, nil } func toICMPEchoFlow(funnel packet.Funnel) (*icmpEchoFlow, error) { icmpFlow, ok := funnel.(*icmpEchoFlow) if !ok { return nil, fmt.Errorf("%v is not *ICMPEchoFunnel", funnel) } return icmpFlow, nil } func createShouldReplaceFunnelFunc(logger *zerolog.Logger, responder ICMPResponder, pk *packet.ICMP, originalEchoID int) func(packet.Funnel) bool { return func(existing packet.Funnel) bool { existingFlow, err := toICMPEchoFlow(existing) if err != nil { logger.Err(err). Str("src", pk.Src.String()). Str("dst", pk.Dst.String()). Int("originalEchoID", originalEchoID). Msg("Funnel of wrong type found") return true } // Each quic connection should have a unique muxer. // If the existing flow has a different muxer, there's a new quic connection where return packets should be // routed. Otherwise, return packets will be send to the first observed incoming connection, rather than the // most recently observed connection. if existingFlow.responder.ConnectionIndex() != responder.ConnectionIndex() { logger.Debug(). Str("src", pk.Src.String()). Str("dst", pk.Dst.String()). Int("originalEchoID", originalEchoID). Msg("Replacing funnel with new responder") return true } return false } } ================================================ FILE: ingress/icmp_posix_test.go ================================================ //go:build darwin || linux package ingress import ( "context" "os" "testing" "time" "github.com/fortytw2/leaktest" "github.com/google/gopacket/layers" "github.com/rs/zerolog" "github.com/stretchr/testify/require" "golang.org/x/net/icmp" "golang.org/x/net/ipv4" "github.com/cloudflare/cloudflared/packet" ) func TestFunnelIdleTimeout(t *testing.T) { defer leaktest.Check(t)() const ( idleTimeout = time.Second echoID = 42573 startSeq = 8129 ) logger := zerolog.New(os.Stderr) proxy, err := newICMPProxy(localhostIP, &logger, idleTimeout) require.NoError(t, err) ctx, cancel := context.WithCancel(context.Background()) proxyDone := make(chan struct{}) go func() { proxy.Serve(ctx) close(proxyDone) }() // Send a packet to register the flow pk := packet.ICMP{ IP: &packet.IP{ Src: localhostIP, Dst: localhostIP, Protocol: layers.IPProtocolICMPv4, }, Message: &icmp.Message{ Type: ipv4.ICMPTypeEcho, Code: 0, Body: &icmp.Echo{ ID: echoID, Seq: startSeq, Data: []byte(t.Name()), }, }, } muxer := newMockMuxer(0) responder := newPacketResponder(muxer, 0, packet.NewEncoder()) require.NoError(t, proxy.Request(ctx, &pk, responder)) validateEchoFlow(t, <-muxer.cfdToEdge, &pk) // Send second request, should reuse the funnel require.NoError(t, proxy.Request(ctx, &pk, responder)) validateEchoFlow(t, <-muxer.cfdToEdge, &pk) // New muxer on a different connection should use a new flow time.Sleep(idleTimeout * 2) newMuxer := newMockMuxer(0) newResponder := newPacketResponder(newMuxer, 1, packet.NewEncoder()) require.NoError(t, proxy.Request(ctx, &pk, newResponder)) validateEchoFlow(t, <-newMuxer.cfdToEdge, &pk) time.Sleep(idleTimeout * 2) cancel() <-proxyDone } func TestReuseFunnel(t *testing.T) { defer leaktest.Check(t)() const ( idleTimeout = time.Millisecond * 100 echoID = 42573 startSeq = 8129 ) logger := zerolog.New(os.Stderr) proxy, err := newICMPProxy(localhostIP, &logger, idleTimeout) require.NoError(t, err) ctx, cancel := context.WithCancel(context.Background()) proxyDone := make(chan struct{}) go func() { proxy.Serve(ctx) close(proxyDone) }() // Send a packet to register the flow pk := packet.ICMP{ IP: &packet.IP{ Src: localhostIP, Dst: localhostIP, Protocol: layers.IPProtocolICMPv4, }, Message: &icmp.Message{ Type: ipv4.ICMPTypeEcho, Code: 0, Body: &icmp.Echo{ ID: echoID, Seq: startSeq, Data: []byte(t.Name()), }, }, } tuple := flow3Tuple{ srcIP: pk.Src, dstIP: pk.Dst, originalEchoID: echoID, } muxer := newMockMuxer(0) responder := newPacketResponder(muxer, 0, packet.NewEncoder()) require.NoError(t, proxy.Request(ctx, &pk, responder)) validateEchoFlow(t, <-muxer.cfdToEdge, &pk) funnel1, found := getFunnel(t, proxy, tuple) require.True(t, found) // Send second request, should reuse the funnel require.NoError(t, proxy.Request(ctx, &pk, responder)) validateEchoFlow(t, <-muxer.cfdToEdge, &pk) funnel2, found := getFunnel(t, proxy, tuple) require.True(t, found) require.Equal(t, funnel1, funnel2) time.Sleep(idleTimeout * 2) cancel() <-proxyDone } ================================================ FILE: ingress/icmp_windows.go ================================================ //go:build windows && cgo package ingress /* #include #include */ import "C" import ( "context" "encoding/binary" "fmt" "net/netip" "runtime/debug" "syscall" "time" "unsafe" "github.com/google/gopacket/layers" "github.com/pkg/errors" "github.com/rs/zerolog" "go.opentelemetry.io/otel/attribute" "golang.org/x/net/icmp" "golang.org/x/net/ipv4" "golang.org/x/net/ipv6" "github.com/cloudflare/cloudflared/packet" "github.com/cloudflare/cloudflared/tracing" ) const ( // Value defined in https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsasocketw AF_INET6 = 23 icmpEchoReplyCode = 0 nullParameter = uintptr(0) ) var ( Iphlpapi = syscall.NewLazyDLL("Iphlpapi.dll") IcmpCreateFile_proc = Iphlpapi.NewProc("IcmpCreateFile") Icmp6CreateFile_proc = Iphlpapi.NewProc("Icmp6CreateFile") IcmpSendEcho_proc = Iphlpapi.NewProc("IcmpSendEcho") Icmp6SendEcho_proc = Iphlpapi.NewProc("Icmp6SendEcho2") echoReplySize = unsafe.Sizeof(echoReply{}) echoV6ReplySize = unsafe.Sizeof(echoV6Reply{}) icmpv6ErrMessageSize = 8 ioStatusBlockSize = unsafe.Sizeof(ioStatusBlock{}) endian = binary.LittleEndian ) // IP_STATUS code, see https://docs.microsoft.com/en-us/windows/win32/api/ipexport/ns-ipexport-icmp_echo_reply32#members // for possible values type ipStatus uint32 const ( success ipStatus = 0 bufTooSmall = iota + 11000 destNetUnreachable destHostUnreachable destProtocolUnreachable destPortUnreachable noResources badOption hwError packetTooBig reqTimedOut badReq badRoute ttlExpiredTransit ttlExpiredReassembly paramProblem sourceQuench optionTooBig badDestination // Can be returned for malformed ICMP packets generalFailure = 11050 ) // Additional IP_STATUS codes for ICMPv6 https://docs.microsoft.com/en-us/windows/win32/api/ipexport/ns-ipexport-icmpv6_echo_reply_lh#members const ( ipv6DestUnreachable ipStatus = iota + 11040 ipv6TimeExceeded ipv6BadHeader ipv6UnrecognizedNextHeader ipv6ICMPError ipv6DestScopeMismatch ) func (is ipStatus) String() string { switch is { case success: return "Success" case bufTooSmall: return "The reply buffer too small" case destNetUnreachable: return "The destination network was unreachable" case destHostUnreachable: return "The destination host was unreachable" case destProtocolUnreachable: return "The destination protocol was unreachable" case destPortUnreachable: return "The destination port was unreachable" case noResources: return "Insufficient IP resources were available" case badOption: return "A bad IP option was specified" case hwError: return "A hardware error occurred" case packetTooBig: return "The packet was too big" case reqTimedOut: return "The request timed out" case badReq: return "Bad request" case badRoute: return "Bad route" case ttlExpiredTransit: return "The TTL expired in transit" case ttlExpiredReassembly: return "The TTL expired during fragment reassembly" case paramProblem: return "A parameter problem" case sourceQuench: return "Datagrams are arriving too fast to be processed and datagrams may have been discarded" case optionTooBig: return "The IP option was too big" case badDestination: return "Bad destination" case ipv6DestUnreachable: return "IPv6 destination unreachable" case ipv6TimeExceeded: return "IPv6 time exceeded" case ipv6BadHeader: return "IPv6 bad IP header" case ipv6UnrecognizedNextHeader: return "IPv6 unrecognized next header" case ipv6ICMPError: return "IPv6 ICMP error" case ipv6DestScopeMismatch: return "IPv6 destination scope ID mismatch" case generalFailure: return "The ICMP packet might be malformed" default: return fmt.Sprintf("Unknown ip status %d", is) } } // https://docs.microsoft.com/en-us/windows/win32/api/ipexport/ns-ipexport-ip_option_information type ipOption struct { TTL uint8 Tos uint8 Flags uint8 OptionsSize uint8 OptionsData uintptr } // https://docs.microsoft.com/en-us/windows/win32/api/ipexport/ns-ipexport-icmp_echo_reply type echoReply struct { Address uint32 Status ipStatus RoundTripTime uint32 DataSize uint16 Reserved uint16 // The pointer size defers between 32-bit and 64-bit platforms DataPointer *byte Options ipOption } type echoV6Reply struct { Address ipv6AddrEx Status ipStatus RoundTripTime uint32 } // https://docs.microsoft.com/en-us/windows/win32/api/ipexport/ns-ipexport-ipv6_address_ex // All the fields are in network byte order. The memory alignment is 4 bytes type ipv6AddrEx struct { port uint16 // flowInfo is uint32. Because of field alignment, when we cast reply buffer to ipv6AddrEx, it starts at the 5th byte // But looking at the raw bytes, flowInfo starts at the 3rd byte. We device flowInfo into 2 uint16 so it's aligned flowInfoUpper uint16 flowInfoLower uint16 addr [8]uint16 scopeID uint32 } // https://docs.microsoft.com/en-us/windows/win32/winsock/sockaddr-2 type sockAddrIn6 struct { family int16 // Can't embed ipv6AddrEx, that changes the memory alignment port uint16 flowInfo uint32 addr [16]byte scopeID uint32 } func newSockAddrIn6(addr netip.Addr) (*sockAddrIn6, error) { if !addr.Is6() { return nil, fmt.Errorf("%s is not IPv6", addr) } return &sockAddrIn6{ family: AF_INET6, port: 10, addr: addr.As16(), }, nil } // https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_io_status_block#syntax type ioStatusBlock struct { // The first field is an union of NTSTATUS and PVOID. NTSTATUS is int32 while PVOID depends on the platform. // We model it as uintptr whose size depends on if the platform is 32-bit or 64-bit // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/596a1078-e883-4972-9bbc-49e60bebca55 statusOrPointer uintptr information uintptr } type icmpProxy struct { // An open handle that can send ICMP requests https://docs.microsoft.com/en-us/windows/win32/api/icmpapi/nf-icmpapi-icmpcreatefile handle uintptr // This is a ICMPv6 if srcSocketAddr is not nil srcSocketAddr *sockAddrIn6 logger *zerolog.Logger } func newICMPProxy(listenIP netip.Addr, logger *zerolog.Logger, idleTimeout time.Duration) (*icmpProxy, error) { var ( srcSocketAddr *sockAddrIn6 handle uintptr err error ) if listenIP.Is4() { handle, _, err = IcmpCreateFile_proc.Call() } else { srcSocketAddr, err = newSockAddrIn6(listenIP) if err != nil { return nil, err } handle, _, err = Icmp6CreateFile_proc.Call() } // Windows procedure calls always return non-nil error constructed from the result of GetLastError. // Caller need to inspect the primary returned value if syscall.Handle(handle) == syscall.InvalidHandle { return nil, errors.Wrap(err, "invalid ICMP handle") } return &icmpProxy{ handle: handle, srcSocketAddr: srcSocketAddr, logger: logger, }, nil } func (ip *icmpProxy) Serve(ctx context.Context) error { <-ctx.Done() syscall.CloseHandle(syscall.Handle(ip.handle)) return ctx.Err() } // Request sends an ICMP echo request and wait for a reply or timeout. // The async version of Win32 APIs take a callback whose memory is not garbage collected, so we use the synchronous version. // It's possible that a slow request will block other requests, so we set the timeout to only 1s. func (ip *icmpProxy) Request(ctx context.Context, pk *packet.ICMP, responder ICMPResponder) error { defer func() { if r := recover(); r != nil { ip.logger.Error().Interface("error", r).Msgf("Recover panic from sending icmp request/response, error %s", debug.Stack()) } }() _, requestSpan := responder.RequestSpan(ctx, pk) defer responder.ExportSpan() echo, err := getICMPEcho(pk.Message) if err != nil { return err } observeICMPRequest(ip.logger, requestSpan, pk.Src.String(), pk.Dst.String(), echo.ID, echo.Seq) resp, err := ip.icmpEchoRoundtrip(pk.Dst, echo) if err != nil { ip.logger.Err(err).Msg("ICMP echo roundtrip failed") tracing.EndWithErrorStatus(requestSpan, err) return err } tracing.End(requestSpan) responder.ExportSpan() _, replySpan := responder.ReplySpan(ctx, ip.logger) err = ip.handleEchoReply(pk, echo, resp, responder) if err != nil { ip.logger.Err(err).Msg("Failed to send ICMP reply") tracing.EndWithErrorStatus(replySpan, err) return errors.Wrap(err, "failed to handle ICMP echo reply") } observeICMPReply(ip.logger, replySpan, pk.Dst.String(), echo.ID, echo.Seq) replySpan.SetAttributes( attribute.Int64("rtt", int64(resp.rtt())), attribute.String("status", resp.status().String()), ) tracing.End(replySpan) return nil } func (ip *icmpProxy) handleEchoReply(request *packet.ICMP, echoReq *icmp.Echo, resp echoResp, responder ICMPResponder) error { var replyType icmp.Type if request.Dst.Is4() { replyType = ipv4.ICMPTypeEchoReply } else { replyType = ipv6.ICMPTypeEchoReply } pk := packet.ICMP{ IP: &packet.IP{ Src: request.Dst, Dst: request.Src, Protocol: layers.IPProtocol(request.Type.Protocol()), TTL: packet.DefaultTTL, }, Message: &icmp.Message{ Type: replyType, Code: icmpEchoReplyCode, Body: &icmp.Echo{ ID: echoReq.ID, Seq: echoReq.Seq, Data: resp.payload(), }, }, } return responder.ReturnPacket(&pk) } func (ip *icmpProxy) icmpEchoRoundtrip(dst netip.Addr, echo *icmp.Echo) (echoResp, error) { if dst.Is6() { if ip.srcSocketAddr == nil { return nil, fmt.Errorf("cannot send ICMPv6 using ICMPv4 proxy") } resp, err := ip.icmp6SendEcho(dst, echo) if err != nil { return nil, errors.Wrap(err, "failed to send/receive ICMPv6 echo") } return resp, nil } if ip.srcSocketAddr != nil { return nil, fmt.Errorf("cannot send ICMPv4 using ICMPv6 proxy") } resp, err := ip.icmpSendEcho(dst, echo) if err != nil { return nil, errors.Wrap(err, "failed to send/receive ICMPv4 echo") } return resp, nil } /* Wrapper to call https://docs.microsoft.com/en-us/windows/win32/api/icmpapi/nf-icmpapi-icmpsendecho Parameters: - IcmpHandle: Handle created by IcmpCreateFile - DestinationAddress: IPv4 in the form of https://docs.microsoft.com/en-us/windows/win32/api/inaddr/ns-inaddr-in_addr#syntax - RequestData: A pointer to echo data - RequestSize: Number of bytes in buffer pointed by echo data - RequestOptions: IP header options - ReplyBuffer: A pointer to the buffer for echoReply, options and data - ReplySize: Number of bytes allocated for ReplyBuffer - Timeout: Timeout in milliseconds to wait for a reply Returns: - the number of replies in uint32 https://docs.microsoft.com/en-us/windows/win32/api/icmpapi/nf-icmpapi-icmpsendecho#return-value To retain the reference allocated objects, conversion from pointer to uintptr must happen as arguments to the syscall function */ func (ip *icmpProxy) icmpSendEcho(dst netip.Addr, echo *icmp.Echo) (*echoV4Resp, error) { dataSize := len(echo.Data) replySize := echoReplySize + uintptr(dataSize) replyBuf := make([]byte, replySize) noIPHeaderOption := nullParameter inAddr, err := inAddrV4(dst) if err != nil { return nil, err } replyCount, _, err := IcmpSendEcho_proc.Call( ip.handle, uintptr(inAddr), uintptr(unsafe.Pointer(&echo.Data[0])), uintptr(dataSize), noIPHeaderOption, uintptr(unsafe.Pointer(&replyBuf[0])), replySize, icmpRequestTimeoutMs, ) if replyCount == 0 { // status is returned in 5th to 8th byte of reply buffer if status, parseErr := unmarshalIPStatus(replyBuf[4:8]); parseErr == nil && status != success { return nil, errors.Wrapf(err, "received ip status: %s", status) } return nil, errors.Wrap(err, "did not receive ICMP echo reply") } else if replyCount > 1 { ip.logger.Warn().Msgf("Received %d ICMP echo replies, only sending 1 back", replyCount) } return newEchoV4Resp(replyBuf) } // Third definition of https://docs.microsoft.com/en-us/windows/win32/api/inaddr/ns-inaddr-in_addr#syntax is address in uint32 func inAddrV4(ip netip.Addr) (uint32, error) { if !ip.Is4() { return 0, fmt.Errorf("%s is not IPv4", ip) } v4 := ip.As4() return endian.Uint32(v4[:]), nil } type echoResp interface { status() ipStatus rtt() uint32 payload() []byte } type echoV4Resp struct { reply *echoReply data []byte } func (r *echoV4Resp) status() ipStatus { return r.reply.Status } func (r *echoV4Resp) rtt() uint32 { return r.reply.RoundTripTime } func (r *echoV4Resp) payload() []byte { return r.data } func newEchoV4Resp(replyBuf []byte) (*echoV4Resp, error) { if len(replyBuf) == 0 { return nil, fmt.Errorf("reply buffer is empty") } // This is pattern 1 of https://pkg.go.dev/unsafe@master#Pointer, conversion of *replyBuf to *echoReply // replyBuf size is larger than echoReply reply := *(*echoReply)(unsafe.Pointer(&replyBuf[0])) if reply.Status != success { return nil, fmt.Errorf("status %d", reply.Status) } dataBufStart := len(replyBuf) - int(reply.DataSize) if dataBufStart < int(echoReplySize) { return nil, fmt.Errorf("reply buffer size %d is too small to hold data of size %d", len(replyBuf), int(reply.DataSize)) } return &echoV4Resp{ reply: &reply, data: replyBuf[dataBufStart:], }, nil } /* Wrapper to call https://docs.microsoft.com/en-us/windows/win32/api/icmpapi/nf-icmpapi-icmp6sendecho2 Parameters: - IcmpHandle: Handle created by Icmp6CreateFile - Event (optional): Event object to be signaled when a reply arrives - ApcRoutine (optional): Routine to call when the calling thread is in an alertable thread and a reply arrives - ApcContext (optional): Optional parameter to ApcRoutine - SourceAddress: Source address of the request - DestinationAddress: Destination address of the request - RequestData: A pointer to echo data - RequestSize: Number of bytes in buffer pointed by echo data - RequestOptions (optional): A pointer to the IPv6 header options - ReplyBuffer: A pointer to the buffer for echoReply, options and data - ReplySize: Number of bytes allocated for ReplyBuffer - Timeout: Timeout in milliseconds to wait for a reply Returns: - the number of replies in uint32 To retain the reference allocated objects, conversion from pointer to uintptr must happen as arguments to the syscall function */ func (ip *icmpProxy) icmp6SendEcho(dst netip.Addr, echo *icmp.Echo) (*echoV6Resp, error) { dstAddr, err := newSockAddrIn6(dst) if err != nil { return nil, err } dataSize := len(echo.Data) // Reply buffer needs to be big enough to hold an echoV6Reply, echo data, 8 bytes for ICMP error message // and ioStatusBlock replySize := echoV6ReplySize + uintptr(dataSize) + uintptr(icmpv6ErrMessageSize) + ioStatusBlockSize replyBuf := make([]byte, replySize) noEvent := nullParameter noApcRoutine := nullParameter noAppCtx := nullParameter noIPHeaderOption := nullParameter replyCount, _, err := Icmp6SendEcho_proc.Call( ip.handle, noEvent, noApcRoutine, noAppCtx, uintptr(unsafe.Pointer(ip.srcSocketAddr)), uintptr(unsafe.Pointer(dstAddr)), uintptr(unsafe.Pointer(&echo.Data[0])), uintptr(dataSize), noIPHeaderOption, uintptr(unsafe.Pointer(&replyBuf[0])), replySize, icmpRequestTimeoutMs, ) if replyCount == 0 { // status is in the 4 bytes after ipv6AddrEx. The reply buffer size is at least size of ipv6AddrEx + 4 if status, parseErr := unmarshalIPStatus(replyBuf[unsafe.Sizeof(ipv6AddrEx{}) : unsafe.Sizeof(ipv6AddrEx{})+4]); parseErr == nil && status != success { return nil, fmt.Errorf("received ip status: %s", status) } return nil, errors.Wrap(err, "did not receive ICMP echo reply") } else if replyCount > 1 { ip.logger.Warn().Msgf("Received %d ICMP echo replies, only sending 1 back", replyCount) } return newEchoV6Resp(replyBuf, dataSize) } type echoV6Resp struct { reply *echoV6Reply data []byte } func (r *echoV6Resp) status() ipStatus { return r.reply.Status } func (r *echoV6Resp) rtt() uint32 { return r.reply.RoundTripTime } func (r *echoV6Resp) payload() []byte { return r.data } func newEchoV6Resp(replyBuf []byte, dataSize int) (*echoV6Resp, error) { if len(replyBuf) == 0 { return nil, fmt.Errorf("reply buffer is empty") } reply := *(*echoV6Reply)(unsafe.Pointer(&replyBuf[0])) if reply.Status != success { return nil, fmt.Errorf("status %d", reply.Status) } if uintptr(len(replyBuf)) < unsafe.Sizeof(reply)+uintptr(dataSize) { return nil, fmt.Errorf("reply buffer size %d is too small to hold reply size %d + data size %d", len(replyBuf), echoV6ReplySize, dataSize) } return &echoV6Resp{ reply: &reply, data: replyBuf[echoV6ReplySize : echoV6ReplySize+uintptr(dataSize)], }, nil } func unmarshalIPStatus(replyBuf []byte) (ipStatus, error) { if len(replyBuf) != 4 { return 0, fmt.Errorf("ipStatus needs to be 4 bytes, got %d", len(replyBuf)) } return ipStatus(endian.Uint32(replyBuf)), nil } ================================================ FILE: ingress/icmp_windows_test.go ================================================ //go:build windows && cgo package ingress import ( "bytes" "encoding/binary" "fmt" "io" "net/netip" "testing" "time" "unsafe" "golang.org/x/net/icmp" "github.com/stretchr/testify/require" ) // TestParseEchoReply tests parsing raw bytes from icmpSendEcho into echoResp func TestParseEchoReply(t *testing.T) { dst, err := inAddrV4(netip.MustParseAddr("192.168.10.20")) require.NoError(t, err) validReplyData := []byte(t.Name()) validReply := echoReply{ Address: dst, Status: success, RoundTripTime: uint32(20), DataSize: uint16(len(validReplyData)), DataPointer: &validReplyData[0], Options: ipOption{ TTL: 59, }, } destHostUnreachableReply := validReply destHostUnreachableReply.Status = destHostUnreachable tests := []struct { testCase string replyBuf []byte expectedReply *echoReply expectedData []byte }{ { testCase: "empty buffer", }, { testCase: "status not success", replyBuf: destHostUnreachableReply.marshal(t, []byte{}), }, { testCase: "valid reply", replyBuf: validReply.marshal(t, validReplyData), expectedReply: &validReply, expectedData: validReplyData, }, } for _, test := range tests { resp, err := newEchoV4Resp(test.replyBuf) if test.expectedReply == nil { require.Error(t, err) require.Nil(t, resp) } else { require.NoError(t, err) require.Equal(t, resp.reply, test.expectedReply) require.True(t, bytes.Equal(resp.data, test.expectedData)) } } } // TestParseEchoV6Reply tests parsing raw bytes from icmp6SendEcho into echoV6Resp func TestParseEchoV6Reply(t *testing.T) { dst := netip.MustParseAddr("2606:3600:4500::3333").As16() var addr [8]uint16 for i := 0; i < 8; i++ { addr[i] = binary.BigEndian.Uint16(dst[i*2 : i*2+2]) } validReplyData := []byte(t.Name()) validReply := echoV6Reply{ Address: ipv6AddrEx{ addr: addr, }, Status: success, RoundTripTime: 25, } destHostUnreachableReply := validReply destHostUnreachableReply.Status = ipv6DestUnreachable tests := []struct { testCase string replyBuf []byte expectedReply *echoV6Reply expectedData []byte }{ { testCase: "empty buffer", }, { testCase: "status not success", replyBuf: destHostUnreachableReply.marshal(t, []byte{}), }, { testCase: "valid reply", replyBuf: validReply.marshal(t, validReplyData), expectedReply: &validReply, expectedData: validReplyData, }, } for _, test := range tests { resp, err := newEchoV6Resp(test.replyBuf, len(test.expectedData)) if test.expectedReply == nil { require.Error(t, err) require.Nil(t, resp) } else { require.NoError(t, err) require.Equal(t, resp.reply, test.expectedReply) require.True(t, bytes.Equal(resp.data, test.expectedData)) } } } // TestSendEchoErrors makes sure icmpSendEcho handles error cases func TestSendEchoErrors(t *testing.T) { testSendEchoErrors(t, netip.IPv4Unspecified()) testSendEchoErrors(t, netip.IPv6Unspecified()) } func testSendEchoErrors(t *testing.T, listenIP netip.Addr) { proxy, err := newICMPProxy(listenIP, &noopLogger, time.Second) require.NoError(t, err) echo := icmp.Echo{ ID: 6193, Seq: 25712, Data: []byte(t.Name()), } documentIP := netip.MustParseAddr("192.0.2.200") if listenIP.Is6() { documentIP = netip.MustParseAddr("2001:db8::1") } resp, err := proxy.icmpEchoRoundtrip(documentIP, &echo) require.Error(t, err) require.Nil(t, resp) } func (er *echoReply) marshal(t *testing.T, data []byte) []byte { buf := new(bytes.Buffer) for _, field := range []any{ er.Address, er.Status, er.RoundTripTime, er.DataSize, er.Reserved, } { require.NoError(t, binary.Write(buf, endian, field)) } require.NoError(t, marshalPointer(buf, uintptr(unsafe.Pointer(er.DataPointer)))) for _, field := range []any{ er.Options.TTL, er.Options.Tos, er.Options.Flags, er.Options.OptionsSize, } { require.NoError(t, binary.Write(buf, endian, field)) } require.NoError(t, marshalPointer(buf, er.Options.OptionsData)) padSize := buf.Len() % int(unsafe.Alignof(er)) padding := make([]byte, padSize) n, err := buf.Write(padding) require.NoError(t, err) require.Equal(t, padSize, n) n, err = buf.Write(data) require.NoError(t, err) require.Equal(t, len(data), n) return buf.Bytes() } func marshalPointer(buf io.Writer, ptr uintptr) error { size := unsafe.Sizeof(ptr) switch size { case 4: return binary.Write(buf, endian, uint32(ptr)) case 8: return binary.Write(buf, endian, uint64(ptr)) default: return fmt.Errorf("unexpected pointer size %d", size) } } func (er *echoV6Reply) marshal(t *testing.T, data []byte) []byte { buf := new(bytes.Buffer) for _, field := range []any{ er.Address.port, er.Address.flowInfoUpper, er.Address.flowInfoLower, er.Address.addr, er.Address.scopeID, } { require.NoError(t, binary.Write(buf, endian, field)) } padSize := buf.Len() % int(unsafe.Alignof(er)) padding := make([]byte, padSize) n, err := buf.Write(padding) require.NoError(t, err) require.Equal(t, padSize, n) for _, field := range []any{ er.Status, er.RoundTripTime, } { require.NoError(t, binary.Write(buf, endian, field)) } n, err = buf.Write(data) require.NoError(t, err) require.Equal(t, len(data), n) return buf.Bytes() } ================================================ FILE: ingress/ingress.go ================================================ package ingress import ( "fmt" "net" "net/url" "regexp" "strconv" "strings" "github.com/pkg/errors" "github.com/rs/zerolog" "github.com/urfave/cli/v2" "golang.org/x/net/idna" "github.com/cloudflare/cloudflared/config" "github.com/cloudflare/cloudflared/ingress/middleware" "github.com/cloudflare/cloudflared/ipaccess" ) var ( ErrNoIngressRules = errors.New("The config file doesn't contain any ingress rules") ErrNoIngressRulesCLI = errors.New("No ingress rules were defined in provided config (if any) nor from the cli, cloudflared will return 503 for all incoming HTTP requests") errLastRuleNotCatchAll = errors.New("The last ingress rule must match all URLs (i.e. it should not have a hostname or path filter)") errBadWildcard = errors.New("Hostname patterns can have at most one wildcard character (\"*\") and it can only be used for subdomains, e.g. \"*.example.com\"") errHostnameContainsPort = errors.New("Hostname cannot contain a port") ErrURLIncompatibleWithIngress = errors.New("You can't set the --url flag (or $TUNNEL_URL) when using multiple-origin ingress rules") ) const ( ServiceBastion = "bastion" ServiceSocksProxy = "socks-proxy" ServiceWarpRouting = "warp-routing" ) // FindMatchingRule returns the index of the Ingress Rule which matches the given // hostname and path. This function assumes the last rule matches everything, // which is the case if the rules were instantiated via the ingress#Validate method. // // Negative index rule signifies local cloudflared rules (not-user defined). func (ing Ingress) FindMatchingRule(hostname, path string) (*Rule, int) { // The hostname might contain port. We only want to compare the host part with the rule host, _, err := net.SplitHostPort(hostname) if err == nil { hostname = host } for i, rule := range ing.InternalRules { if rule.Matches(hostname, path) { // Local rule matches return a negative rule index to distiguish local rules from user-defined rules in logs // Full range would be [-1 .. ) return &rule, -1 - i } } for i, rule := range ing.Rules { if rule.Matches(hostname, path) { return &rule, i } } i := len(ing.Rules) - 1 return &ing.Rules[i], i } func matchHost(ruleHost, reqHost string) bool { if ruleHost == reqHost { return true } // Validate hostnames that use wildcards at the start if strings.HasPrefix(ruleHost, "*.") { toMatch := strings.TrimPrefix(ruleHost, "*") return strings.HasSuffix(reqHost, toMatch) } return false } // Ingress maps eyeball requests to origins. type Ingress struct { // Set of ingress rules that are not added to remote config, e.g. management InternalRules []Rule // Rules that are provided by the user from remote or local configuration Rules []Rule `json:"ingress"` Defaults OriginRequestConfig `json:"originRequest"` } // ParseIngress parses ingress rules, but does not send HTTP requests to the origins. func ParseIngress(conf *config.Configuration) (Ingress, error) { if conf == nil || len(conf.Ingress) == 0 { return Ingress{}, ErrNoIngressRules } return validateIngress(conf.Ingress, originRequestFromConfig(conf.OriginRequest)) } // ParseIngressFromConfigAndCLI will parse the configuration rules from config files for ingress // rules and then attempt to parse CLI for ingress rules. // Will always return at least one valid ingress rule. If none are provided by the user, the default // will be to return 503 status code for all incoming requests. func ParseIngressFromConfigAndCLI(conf *config.Configuration, c *cli.Context, log *zerolog.Logger) (Ingress, error) { // Attempt to parse ingress rules from configuration ingressRules, err := ParseIngress(conf) if err == nil && !ingressRules.IsEmpty() { return ingressRules, nil } if err != ErrNoIngressRules { return Ingress{}, err } // Attempt to parse ingress rules from CLI: // --url or --unix-socket flag for a tunnel HTTP ingress // --hello-world for a basic HTTP ingress self-served // --bastion for ssh bastion service ingressRules, err = parseCLIIngress(c, false) if errors.Is(err, ErrNoIngressRulesCLI) { // If no token is provided, the probability of NOT being a remotely managed tunnel is higher. // So, we should warn the user that no ingress rules were found, because remote configuration will most likely not exist. if !c.IsSet("token") { log.Warn().Msg(ErrNoIngressRulesCLI.Error()) } return newDefaultOrigin(c, log), nil } if err != nil { return Ingress{}, err } return ingressRules, nil } // parseCLIIngress constructs an Ingress set with only one rule constructed from // CLI parameters: --url, --hello-world, --bastion, or --unix-socket func parseCLIIngress(c *cli.Context, allowURLFromArgs bool) (Ingress, error) { service, err := parseSingleOriginService(c, allowURLFromArgs) if err != nil { return Ingress{}, err } // Construct an Ingress with the single rule. defaults := originRequestFromSingleRule(c) ing := Ingress{ Rules: []Rule{ { Service: service, Config: setConfig(defaults, config.OriginRequestConfig{}), }, }, Defaults: defaults, } return ing, err } // newDefaultOrigin always returns a 503 response code to help indicate that there are no ingress // rules setup, but the tunnel is reachable. func newDefaultOrigin(c *cli.Context, log *zerolog.Logger) Ingress { defaultRule := GetDefaultIngressRules(log) defaults := originRequestFromSingleRule(c) ingress := Ingress{ Rules: defaultRule, Defaults: defaults, } return ingress } // Get a single origin service from the CLI/config. func parseSingleOriginService(c *cli.Context, allowURLFromArgs bool) (OriginService, error) { if c.IsSet(HelloWorldFlag) { return new(helloWorld), nil } if c.IsSet(config.BastionFlag) { return newBastionService(), nil } if c.IsSet("url") { originURL, err := config.ValidateUrl(c, allowURLFromArgs) if err != nil { return nil, errors.Wrap(err, "Error validating origin URL") } if isHTTPService(originURL) { return &httpService{ url: originURL, }, nil } return newTCPOverWSService(originURL), nil } if c.IsSet("unix-socket") { path, err := config.ValidateUnixSocket(c) if err != nil { return nil, errors.Wrap(err, "Error validating --unix-socket") } return &unixSocketPath{path: path, scheme: "http"}, nil } return nil, ErrNoIngressRulesCLI } // IsEmpty checks if there are any ingress rules. func (ing Ingress) IsEmpty() bool { return len(ing.Rules) == 0 } // IsSingleRule checks if the user only specified a single ingress rule. func (ing Ingress) IsSingleRule() bool { return len(ing.Rules) == 1 } // StartOrigins will start any origin services managed by cloudflared, e.g. proxy servers or Hello World. func (ing Ingress) StartOrigins( log *zerolog.Logger, shutdownC <-chan struct{}, ) error { for _, rule := range ing.Rules { if err := rule.Service.start(log, shutdownC, rule.Config); err != nil { return errors.Wrapf(err, "Error starting local service %s", rule.Service) } } return nil } // CatchAll returns the catch-all rule (i.e. the last rule) func (ing Ingress) CatchAll() *Rule { return &ing.Rules[len(ing.Rules)-1] } // Gets the default ingress rule that will be return 503 status // code for all incoming requests. func GetDefaultIngressRules(log *zerolog.Logger) []Rule { noRulesService := newDefaultStatusCode(log) return []Rule{ { Service: &noRulesService, }, } } func validateAccessConfiguration(cfg *config.AccessConfig) error { if !cfg.Required { return nil } // we allow for an initial setup where user can force Access but not configure the rest of the keys. // however, if the user specified audTags but forgot teamName, we should alert it. if cfg.TeamName == "" && len(cfg.AudTag) > 0 { return errors.New("access.TeamName cannot be blank when access.audTags are present") } return nil } func validateIngress(ingress []config.UnvalidatedIngressRule, defaults OriginRequestConfig) (Ingress, error) { rules := make([]Rule, len(ingress)) for i, r := range ingress { cfg := setConfig(defaults, r.OriginRequest) var service OriginService if prefix := "unix:"; strings.HasPrefix(r.Service, prefix) { // No validation necessary for unix socket filepath services path := strings.TrimPrefix(r.Service, prefix) service = &unixSocketPath{path: path, scheme: "http"} } else if prefix := "unix+tls:"; strings.HasPrefix(r.Service, prefix) { path := strings.TrimPrefix(r.Service, prefix) service = &unixSocketPath{path: path, scheme: "https"} } else if prefix := "http_status:"; strings.HasPrefix(r.Service, prefix) { statusCode, err := strconv.Atoi(strings.TrimPrefix(r.Service, prefix)) if err != nil { return Ingress{}, errors.Wrap(err, "invalid HTTP status code") } if statusCode < 100 || statusCode > 999 { return Ingress{}, fmt.Errorf("invalid HTTP status code: %d", statusCode) } srv := newStatusCode(statusCode) service = &srv } else if r.Service == HelloWorldFlag || r.Service == HelloWorldService { service = new(helloWorld) } else if r.Service == ServiceSocksProxy { rules := make([]ipaccess.Rule, len(r.OriginRequest.IPRules)) for i, ipRule := range r.OriginRequest.IPRules { rule, err := ipaccess.NewRuleByCIDR(ipRule.Prefix, ipRule.Ports, ipRule.Allow) if err != nil { return Ingress{}, fmt.Errorf("unable to create ip rule for %s: %s", r.Service, err) } rules[i] = rule } accessPolicy, err := ipaccess.NewPolicy(false, rules) if err != nil { return Ingress{}, fmt.Errorf("unable to create ip access policy for %s: %s", r.Service, err) } service = newSocksProxyOverWSService(accessPolicy) } else if r.Service == ServiceBastion || cfg.BastionMode { // Bastion mode will always start a Websocket proxy server, which will // overwrite the localService.URL field when `start` is called. So, // leave the URL field empty for now. cfg.BastionMode = true service = newBastionService() } else { // Validate URL services u, err := url.Parse(r.Service) if err != nil { return Ingress{}, err } if u.Scheme == "" || u.Hostname() == "" { return Ingress{}, fmt.Errorf("%s is an invalid address, please make sure it has a scheme and a hostname", r.Service) } if u.Path != "" { return Ingress{}, fmt.Errorf("%s is an invalid address, ingress rules don't support proxying to a different path on the origin service. The path will be the same as the eyeball request's path", r.Service) } if isHTTPService(u) { service = &httpService{url: u} } else { service = newTCPOverWSService(u) } } var handlers []middleware.Handler if access := r.OriginRequest.Access; access != nil { if err := validateAccessConfiguration(access); err != nil { return Ingress{}, err } if access.Required { verifier := middleware.NewJWTValidator(access.TeamName, access.Environment, access.AudTag) handlers = append(handlers, verifier) } } if err := validateHostname(r, i, len(ingress)); err != nil { return Ingress{}, err } isCatchAllRule := (r.Hostname == "" || r.Hostname == "*") && r.Path == "" punycodeHostname := "" if !isCatchAllRule { punycode, err := idna.Lookup.ToASCII(r.Hostname) // Don't provide the punycode hostname if it is the same as the original hostname if err == nil && punycode != r.Hostname { punycodeHostname = punycode } } var pathRegexp *Regexp if r.Path != "" { var err error regex, err := regexp.Compile(r.Path) if err != nil { return Ingress{}, errors.Wrapf(err, "Rule #%d has an invalid regex", i+1) } pathRegexp = &Regexp{Regexp: regex} } rules[i] = Rule{ Hostname: r.Hostname, punycodeHostname: punycodeHostname, Service: service, Path: pathRegexp, Handlers: handlers, Config: cfg, } } return Ingress{Rules: rules, Defaults: defaults}, nil } func validateHostname(r config.UnvalidatedIngressRule, ruleIndex, totalRules int) error { // Ensure that the hostname doesn't contain port _, _, err := net.SplitHostPort(r.Hostname) if err == nil { return errHostnameContainsPort } // Ensure that there are no wildcards anywhere except the first character // of the hostname. if strings.LastIndex(r.Hostname, "*") > 0 { return errBadWildcard } // The last rule should catch all hostnames. isCatchAllRule := (r.Hostname == "" || r.Hostname == "*") && r.Path == "" isLastRule := ruleIndex == totalRules-1 if isLastRule && !isCatchAllRule { return errLastRuleNotCatchAll } // ONLY the last rule should catch all hostnames. if !isLastRule && isCatchAllRule { return ruleShouldNotBeCatchAllError{index: ruleIndex, hostname: r.Hostname} } return nil } type ruleShouldNotBeCatchAllError struct { index int hostname string } func (e ruleShouldNotBeCatchAllError) Error() string { return fmt.Sprintf("Rule #%d is matching the hostname '%s', but "+ "this will match every hostname, meaning the rules which follow it "+ "will never be triggered.", e.index+1, e.hostname) } func isHTTPService(url *url.URL) bool { return url.Scheme == "http" || url.Scheme == "https" || url.Scheme == "ws" || url.Scheme == "wss" } ================================================ FILE: ingress/ingress_test.go ================================================ package ingress import ( "flag" "fmt" "net/http" "net/url" "regexp" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/urfave/cli/v2" yaml "gopkg.in/yaml.v3" "github.com/cloudflare/cloudflared/config" "github.com/cloudflare/cloudflared/ipaccess" "github.com/cloudflare/cloudflared/tlsconfig" ) func TestParseUnixSocket(t *testing.T) { rawYAML := ` ingress: - service: unix:/tmp/echo.sock ` ing, err := ParseIngress(MustReadIngress(rawYAML)) require.NoError(t, err) s, ok := ing.Rules[0].Service.(*unixSocketPath) require.True(t, ok) require.Equal(t, "http", s.scheme) } func TestParseUnixSocketTLS(t *testing.T) { rawYAML := ` ingress: - service: unix+tls:/tmp/echo.sock ` ing, err := ParseIngress(MustReadIngress(rawYAML)) require.NoError(t, err) s, ok := ing.Rules[0].Service.(*unixSocketPath) require.True(t, ok) require.Equal(t, "https", s.scheme) } func TestParseIngressNilConfig(t *testing.T) { _, err := ParseIngress(nil) require.Error(t, err) } func TestParseIngress(t *testing.T) { localhost8000 := MustParseURL(t, "https://localhost:8000") localhost8001 := MustParseURL(t, "https://localhost:8001") fourOhFour := newStatusCode(404) defaultConfig := setConfig(originRequestFromConfig(config.OriginRequestConfig{}), config.OriginRequestConfig{}) require.Equal(t, defaultKeepAliveConnections, defaultConfig.KeepAliveConnections) tr := true type args struct { rawYAML string } tests := []struct { name string args args want []Rule wantErr bool }{ { name: "Empty file", args: args{rawYAML: ""}, wantErr: true, }, { name: "Multiple rules", args: args{rawYAML: ` ingress: - hostname: tunnel1.example.com service: https://localhost:8000 - hostname: "*" service: https://localhost:8001 `}, want: []Rule{ { Hostname: "tunnel1.example.com", Service: &httpService{url: localhost8000}, Config: defaultConfig, }, { Hostname: "*", Service: &httpService{url: localhost8001}, Config: defaultConfig, }, }, }, { name: "Extra keys", args: args{rawYAML: ` ingress: - hostname: "*" service: https://localhost:8000 extraKey: extraValue `}, want: []Rule{ { Hostname: "*", Service: &httpService{url: localhost8000}, Config: defaultConfig, }, }, }, { name: "ws service", args: args{rawYAML: ` ingress: - hostname: "*" service: wss://localhost:8000 `}, want: []Rule{ { Hostname: "*", Service: &httpService{url: MustParseURL(t, "wss://localhost:8000")}, Config: defaultConfig, }, }, }, { name: "Hostname can be omitted", args: args{rawYAML: ` ingress: - service: https://localhost:8000 `}, want: []Rule{ { Service: &httpService{url: localhost8000}, Config: defaultConfig, }, }, }, { name: "Unicode domain", args: args{rawYAML: ` ingress: - hostname: môô.cloudflare.com service: https://localhost:8000 - service: https://localhost:8001 `}, want: []Rule{ { Hostname: "môô.cloudflare.com", punycodeHostname: "xn--m-xgaa.cloudflare.com", Service: &httpService{url: localhost8000}, Config: defaultConfig, }, { Service: &httpService{url: localhost8001}, Config: defaultConfig, }, }, }, { name: "Invalid unicode domain", args: args{rawYAML: fmt.Sprintf(` ingress: - hostname: %s service: https://localhost:8000 `, string(rune(0xd8f3))+".cloudflare.com")}, wantErr: true, }, { name: "Invalid service", args: args{rawYAML: ` ingress: - hostname: "*" service: https://local host:8000 `}, wantErr: true, }, { name: "Last rule isn't catchall", args: args{rawYAML: ` ingress: - hostname: example.com service: https://localhost:8000 `}, wantErr: true, }, { name: "First rule is catchall", args: args{rawYAML: ` ingress: - service: https://localhost:8000 - hostname: example.com service: https://localhost:8000 `}, wantErr: true, }, { name: "Catch-all rule can't have a path", args: args{rawYAML: ` ingress: - service: https://localhost:8001 path: /subpath1/(.*)/subpath2 `}, wantErr: true, }, { name: "Invalid regex", args: args{rawYAML: ` ingress: - hostname: example.com service: https://localhost:8000 path: "*/subpath2" - service: https://localhost:8001 `}, wantErr: true, }, { name: "Service must have a scheme", args: args{rawYAML: ` ingress: - service: localhost:8000 `}, wantErr: true, }, { name: "Wildcard not at start", args: args{rawYAML: ` ingress: - hostname: "test.*.example.com" service: https://localhost:8000 `}, wantErr: true, }, { name: "Service can't have a path", args: args{rawYAML: ` ingress: - service: https://localhost:8000/static/ `}, wantErr: true, }, { name: "Invalid HTTP status", args: args{rawYAML: ` ingress: - service: http_status:asdf `}, wantErr: true, }, { name: "Invalid HTTP status code", args: args{rawYAML: ` ingress: - service: http_status:8080 `}, wantErr: true, }, { name: "Valid HTTP status", args: args{rawYAML: ` ingress: - service: http_status:404 `}, want: []Rule{ { Hostname: "", Service: &fourOhFour, Config: defaultConfig, }, }, }, { name: "Valid hello world service", args: args{rawYAML: ` ingress: - service: hello_world `}, want: []Rule{ { Hostname: "", Service: new(helloWorld), Config: defaultConfig, }, }, }, { name: "TCP services", args: args{rawYAML: ` ingress: - hostname: tcp.foo.com service: tcp://127.0.0.1 - hostname: tcp2.foo.com service: tcp://localhost:8000 - service: http_status:404 `}, want: []Rule{ { Hostname: "tcp.foo.com", Service: newTCPOverWSService(MustParseURL(t, "tcp://127.0.0.1:7864")), Config: defaultConfig, }, { Hostname: "tcp2.foo.com", Service: newTCPOverWSService(MustParseURL(t, "tcp://localhost:8000")), Config: defaultConfig, }, { Service: &fourOhFour, Config: defaultConfig, }, }, }, { name: "SSH services", args: args{rawYAML: ` ingress: - service: ssh://127.0.0.1 `}, want: []Rule{ { Service: newTCPOverWSService(MustParseURL(t, "ssh://127.0.0.1:22")), Config: defaultConfig, }, }, }, { name: "RDP services", args: args{rawYAML: ` ingress: - service: rdp://127.0.0.1 `}, want: []Rule{ { Service: newTCPOverWSService(MustParseURL(t, "rdp://127.0.0.1:3389")), Config: defaultConfig, }, }, }, { name: "SMB services", args: args{rawYAML: ` ingress: - service: smb://127.0.0.1 `}, want: []Rule{ { Service: newTCPOverWSService(MustParseURL(t, "smb://127.0.0.1:445")), Config: defaultConfig, }, }, }, { name: "Other TCP services", args: args{rawYAML: ` ingress: - service: ftp://127.0.0.1 `}, want: []Rule{ { Service: newTCPOverWSService(MustParseURL(t, "ftp://127.0.0.1")), Config: defaultConfig, }, }, }, { name: "SOCKS services", args: args{rawYAML: ` ingress: - hostname: socks.foo.com service: socks-proxy originRequest: ipRules: - prefix: 1.1.1.0/24 ports: [80, 443] allow: true - prefix: 0.0.0.0/0 allow: false - service: http_status:404 `}, want: []Rule{ { Hostname: "socks.foo.com", Service: newSocksProxyOverWSService(accessPolicy()), Config: setConfig(originRequestFromConfig(config.OriginRequestConfig{}), config.OriginRequestConfig{IPRules: []config.IngressIPRule{ { Prefix: ipRulePrefix("1.1.1.0/24"), Ports: []int{80, 443}, Allow: true, }, { Prefix: ipRulePrefix("0.0.0.0/0"), Allow: false, }, }}), }, { Service: &fourOhFour, Config: defaultConfig, }, }, }, { name: "URL isn't necessary if using bastion", args: args{rawYAML: ` ingress: - hostname: bastion.foo.com originRequest: bastionMode: true - service: http_status:404 `}, want: []Rule{ { Hostname: "bastion.foo.com", Service: newBastionService(), Config: setConfig(originRequestFromConfig(config.OriginRequestConfig{}), config.OriginRequestConfig{BastionMode: &tr}), }, { Service: &fourOhFour, Config: defaultConfig, }, }, }, { name: "Bastion service", args: args{rawYAML: ` ingress: - hostname: bastion.foo.com service: bastion - service: http_status:404 `}, want: []Rule{ { Hostname: "bastion.foo.com", Service: newBastionService(), Config: setConfig(originRequestFromConfig(config.OriginRequestConfig{}), config.OriginRequestConfig{BastionMode: &tr}), }, { Service: &fourOhFour, Config: defaultConfig, }, }, }, { name: "Hostname contains port", args: args{rawYAML: ` ingress: - hostname: "test.example.com:443" service: https://localhost:8000 - hostname: "*" service: https://localhost:8001 `}, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := ParseIngress(MustReadIngress(tt.args.rawYAML)) if (err != nil) != tt.wantErr { t.Errorf("ParseIngress() error = %v, wantErr %v", err, tt.wantErr) return } require.Equal(t, tt.want, got.Rules) }) } } func ipRulePrefix(s string) *string { return &s } func TestSingleOriginSetsConfig(t *testing.T) { flagSet := flag.NewFlagSet(t.Name(), flag.PanicOnError) flagSet.Bool("hello-world", true, "") flagSet.Duration(ProxyConnectTimeoutFlag, time.Second, "") flagSet.Duration(ProxyTLSTimeoutFlag, time.Second, "") flagSet.Duration(ProxyTCPKeepAliveFlag, time.Second, "") flagSet.Bool(ProxyNoHappyEyeballsFlag, true, "") flagSet.Int(ProxyKeepAliveConnectionsFlag, 10, "") flagSet.Duration(ProxyKeepAliveTimeoutFlag, time.Second, "") flagSet.String(HTTPHostHeaderFlag, "example.com:8080", "") flagSet.String(OriginServerNameFlag, "example.com", "") flagSet.String(tlsconfig.OriginCAPoolFlag, "/etc/certs/ca.pem", "") flagSet.Bool(NoTLSVerifyFlag, true, "") flagSet.Bool(NoChunkedEncodingFlag, true, "") flagSet.Bool(config.BastionFlag, true, "") flagSet.String(ProxyAddressFlag, "localhost:8080", "") flagSet.Uint(ProxyPortFlag, 8080, "") flagSet.Bool(Socks5Flag, true, "") cliCtx := cli.NewContext(cli.NewApp(), flagSet, nil) err := cliCtx.Set("hello-world", "true") require.NoError(t, err) err = cliCtx.Set(ProxyConnectTimeoutFlag, "1s") require.NoError(t, err) err = cliCtx.Set(ProxyTLSTimeoutFlag, "1s") require.NoError(t, err) err = cliCtx.Set(ProxyTCPKeepAliveFlag, "1s") require.NoError(t, err) err = cliCtx.Set(ProxyNoHappyEyeballsFlag, "true") require.NoError(t, err) err = cliCtx.Set(ProxyKeepAliveConnectionsFlag, "10") require.NoError(t, err) err = cliCtx.Set(ProxyKeepAliveTimeoutFlag, "1s") require.NoError(t, err) err = cliCtx.Set(HTTPHostHeaderFlag, "example.com:8080") require.NoError(t, err) err = cliCtx.Set(OriginServerNameFlag, "example.com") require.NoError(t, err) err = cliCtx.Set(tlsconfig.OriginCAPoolFlag, "/etc/certs/ca.pem") require.NoError(t, err) err = cliCtx.Set(NoTLSVerifyFlag, "true") require.NoError(t, err) err = cliCtx.Set(NoChunkedEncodingFlag, "true") require.NoError(t, err) err = cliCtx.Set(config.BastionFlag, "true") require.NoError(t, err) err = cliCtx.Set(ProxyAddressFlag, "localhost:8080") require.NoError(t, err) err = cliCtx.Set(ProxyPortFlag, "8080") require.NoError(t, err) err = cliCtx.Set(Socks5Flag, "true") require.NoError(t, err) allowURLFromArgs := false require.NoError(t, err) ingress, err := parseCLIIngress(cliCtx, allowURLFromArgs) require.NoError(t, err) assert.Equal(t, config.CustomDuration{Duration: time.Second}, ingress.Rules[0].Config.ConnectTimeout) assert.Equal(t, config.CustomDuration{Duration: time.Second}, ingress.Rules[0].Config.TLSTimeout) assert.Equal(t, config.CustomDuration{Duration: time.Second}, ingress.Rules[0].Config.TCPKeepAlive) assert.True(t, ingress.Rules[0].Config.NoHappyEyeballs) assert.Equal(t, 10, ingress.Rules[0].Config.KeepAliveConnections) assert.Equal(t, config.CustomDuration{Duration: time.Second}, ingress.Rules[0].Config.KeepAliveTimeout) assert.Equal(t, "example.com:8080", ingress.Rules[0].Config.HTTPHostHeader) assert.Equal(t, "example.com", ingress.Rules[0].Config.OriginServerName) assert.Equal(t, "/etc/certs/ca.pem", ingress.Rules[0].Config.CAPool) assert.True(t, ingress.Rules[0].Config.NoTLSVerify) assert.True(t, ingress.Rules[0].Config.DisableChunkedEncoding) assert.True(t, ingress.Rules[0].Config.BastionMode) assert.Equal(t, "localhost:8080", ingress.Rules[0].Config.ProxyAddress) assert.Equal(t, uint(8080), ingress.Rules[0].Config.ProxyPort) assert.Equal(t, socksProxy, ingress.Rules[0].Config.ProxyType) } func TestSingleOriginServices(t *testing.T) { host := "://localhost:8080" httpURL := urlMustParse("http" + host) tcpURL := urlMustParse("tcp" + host) unix := "unix://service" newCli := func(params ...string) *cli.Context { flagSet := flag.NewFlagSet(t.Name(), flag.PanicOnError) flagSet.Bool("hello-world", false, "") flagSet.Bool("bastion", false, "") flagSet.String("url", "", "") flagSet.String("unix-socket", "", "") cliCtx := cli.NewContext(cli.NewApp(), flagSet, nil) for i := 0; i < len(params); i += 2 { cliCtx.Set(params[i], params[i+1]) } return cliCtx } tests := []struct { name string cli *cli.Context expectedService OriginService err error }{ { name: "Valid hello-world", cli: newCli("hello-world", "true"), expectedService: &helloWorld{}, }, { name: "Valid bastion", cli: newCli("bastion", "true"), expectedService: newBastionService(), }, { name: "Valid http url", cli: newCli("url", httpURL.String()), expectedService: &httpService{url: httpURL}, }, { name: "Valid tcp url", cli: newCli("url", tcpURL.String()), expectedService: newTCPOverWSService(tcpURL), }, { name: "Valid unix-socket", cli: newCli("unix-socket", unix), expectedService: &unixSocketPath{path: unix, scheme: "http"}, }, { name: "No origins defined", cli: newCli(), err: ErrNoIngressRulesCLI, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { ingress, err := parseCLIIngress(test.cli, false) require.Equal(t, err, test.err) if test.err != nil { return } require.Equal(t, 1, len(ingress.Rules)) rule := ingress.Rules[0] require.Equal(t, test.expectedService, rule.Service) }) } } func urlMustParse(s string) *url.URL { u, err := url.Parse(s) if err != nil { panic(err) } return u } func TestSingleOriginServices_URL(t *testing.T) { host := "://localhost:8080" newCli := func(param string, value string) *cli.Context { flagSet := flag.NewFlagSet(t.Name(), flag.PanicOnError) flagSet.String("url", "", "") cliCtx := cli.NewContext(cli.NewApp(), flagSet, nil) cliCtx.Set(param, value) return cliCtx } httpTests := []string{"http", "https"} for _, test := range httpTests { t.Run(test, func(t *testing.T) { url := urlMustParse(test + host) ingress, err := parseCLIIngress(newCli("url", url.String()), false) require.NoError(t, err) require.Equal(t, 1, len(ingress.Rules)) rule := ingress.Rules[0] require.Equal(t, &httpService{url: url}, rule.Service) }) } tcpTests := []string{"ssh", "rdp", "smb", "tcp"} for _, test := range tcpTests { t.Run(test, func(t *testing.T) { url := urlMustParse(test + host) ingress, err := parseCLIIngress(newCli("url", url.String()), false) require.NoError(t, err) require.Equal(t, 1, len(ingress.Rules)) rule := ingress.Rules[0] require.Equal(t, newTCPOverWSService(url), rule.Service) }) } } func TestFindMatchingRule(t *testing.T) { ingress := Ingress{ Rules: []Rule{ { Hostname: "tunnel-a.example.com", Path: nil, }, { Hostname: "tunnel-b.example.com", Path: MustParsePath(t, "/health"), }, { Hostname: "*", }, }, } tests := []struct { host string path string req *http.Request wantRuleIndex int }{ { host: "tunnel-a.example.com", path: "/", wantRuleIndex: 0, }, { host: "tunnel-a.example.com", path: "/pages/about", wantRuleIndex: 0, }, { host: "tunnel-a.example.com:443", path: "/pages/about", wantRuleIndex: 0, }, { host: "tunnel-b.example.com", path: "/health", wantRuleIndex: 1, }, { host: "tunnel-b.example.com", path: "/index.html", wantRuleIndex: 2, }, { host: "tunnel-c.example.com", path: "/", wantRuleIndex: 2, }, } for _, test := range tests { _, ruleIndex := ingress.FindMatchingRule(test.host, test.path) assert.Equal(t, test.wantRuleIndex, ruleIndex, fmt.Sprintf("Expect host=%s, path=%s to match rule %d, got %d", test.host, test.path, test.wantRuleIndex, ruleIndex)) } } func TestIsHTTPService(t *testing.T) { tests := []struct { url *url.URL isHTTP bool }{ { url: MustParseURL(t, "http://localhost"), isHTTP: true, }, { url: MustParseURL(t, "https://127.0.0.1:8000"), isHTTP: true, }, { url: MustParseURL(t, "ws://localhost"), isHTTP: true, }, { url: MustParseURL(t, "wss://localhost:8000"), isHTTP: true, }, { url: MustParseURL(t, "tcp://localhost:9000"), isHTTP: false, }, } for _, test := range tests { assert.Equal(t, test.isHTTP, isHTTPService(test.url)) } } func MustParsePath(t *testing.T, path string) *Regexp { regexp, err := regexp.Compile(path) assert.NoError(t, err) return &Regexp{Regexp: regexp} } func MustParseURL(t *testing.T, rawURL string) *url.URL { u, err := url.Parse(rawURL) require.NoError(t, err) return u } func accessPolicy() *ipaccess.Policy { cidr1 := "1.1.1.0/24" cidr2 := "0.0.0.0/0" rule1, _ := ipaccess.NewRuleByCIDR(&cidr1, []int{80, 443}, true) rule2, _ := ipaccess.NewRuleByCIDR(&cidr2, nil, false) rules := []ipaccess.Rule{rule1, rule2} accessPolicy, _ := ipaccess.NewPolicy(false, rules) return accessPolicy } func BenchmarkFindMatch(b *testing.B) { rulesYAML := ` ingress: - hostname: tunnel1.example.com service: https://localhost:8000 - hostname: tunnel2.example.com service: https://localhost:8001 - hostname: "*" service: https://localhost:8002 ` ing, err := ParseIngress(MustReadIngress(rulesYAML)) if err != nil { b.Error(err) } for n := 0; n < b.N; n++ { ing.FindMatchingRule("tunnel1.example.com", "") ing.FindMatchingRule("tunnel2.example.com", "") ing.FindMatchingRule("tunnel3.example.com", "") } } func TestParseAccessConfig(t *testing.T) { tests := []struct { name string cfg config.AccessConfig expectError bool }{ { name: "Config required with teamName only", cfg: config.AccessConfig{Required: true, TeamName: "team"}, expectError: false, }, { name: "required false", cfg: config.AccessConfig{Required: false}, expectError: false, }, { name: "required true but empty config", cfg: config.AccessConfig{Required: true}, expectError: false, }, { name: "complete config", cfg: config.AccessConfig{Required: true, TeamName: "team", AudTag: []string{"a"}}, expectError: false, }, { name: "required true with audTags but no teamName", cfg: config.AccessConfig{Required: true, AudTag: []string{"a"}}, expectError: true, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { err := validateAccessConfiguration(&test.cfg) require.Equal(t, err != nil, test.expectError) }) } } func MustReadIngress(s string) *config.Configuration { var conf config.Configuration err := yaml.Unmarshal([]byte(s), &conf) if err != nil { panic(err) } return &conf } ================================================ FILE: ingress/middleware/jwtvalidator.go ================================================ package middleware import ( "context" "fmt" "net/http" "github.com/coreos/go-oidc/v3/oidc" "github.com/cloudflare/cloudflared/credentials" ) const ( headerKeyAccessJWTAssertion = "Cf-Access-Jwt-Assertion" ) var ( cloudflareAccessCertsURL = "https://%s.cloudflareaccess.com" cloudflareAccessFedCertsURL = "https://%s.fed.cloudflareaccess.com" ) // JWTValidator is an implementation of Verifier that validates access based JWT tokens. type JWTValidator struct { *oidc.IDTokenVerifier audTags []string } func NewJWTValidator(teamName string, environment string, audTags []string) *JWTValidator { var certsURL string if environment == credentials.FedEndpoint { certsURL = fmt.Sprintf(cloudflareAccessFedCertsURL, teamName) } else { certsURL = fmt.Sprintf(cloudflareAccessCertsURL, teamName) } certsEndpoint := fmt.Sprintf("%s/cdn-cgi/access/certs", certsURL) config := &oidc.Config{ SkipClientIDCheck: true, } ctx := context.Background() keySet := oidc.NewRemoteKeySet(ctx, certsEndpoint) verifier := oidc.NewVerifier(certsURL, keySet, config) return &JWTValidator{ IDTokenVerifier: verifier, audTags: audTags, } } func (v *JWTValidator) Name() string { return "AccessJWTValidator" } func (v *JWTValidator) Handle(ctx context.Context, r *http.Request) (*HandleResult, error) { accessJWT := r.Header.Get(headerKeyAccessJWTAssertion) if accessJWT == "" { // log the exact error message here. the message is specific to the handler implementation logic, we don't gain anything // in passing it upstream. and each handler impl know what logging level to use for each. return &HandleResult{ ShouldFilterRequest: true, StatusCode: http.StatusForbidden, Reason: "no access token in request", }, nil } token, err := v.IDTokenVerifier.Verify(ctx, accessJWT) if err != nil { return nil, err } // We want at least one audTag to match for _, jwtAudTag := range token.Audience { for _, acceptedAudTag := range v.audTags { if acceptedAudTag == jwtAudTag { return &HandleResult{ShouldFilterRequest: false}, nil } } } return &HandleResult{ ShouldFilterRequest: true, StatusCode: http.StatusForbidden, Reason: fmt.Sprintf("Invalid token in jwt: %v", token.Audience), }, nil } ================================================ FILE: ingress/middleware/jwtvalidator_test.go ================================================ package middleware import ( "context" "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "encoding/json" "fmt" "net/http/httptest" "testing" "time" "github.com/coreos/go-oidc/v3/oidc" "github.com/go-jose/go-jose/v4" "github.com/go-jose/go-jose/v4/jwt" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var ( issuer = fmt.Sprintf(cloudflareAccessCertsURL, "testteam") ) type accessTokenClaims struct { Email string `json:"email"` Type string `json:"type"` jwt.Claims } func TestJWTValidator(t *testing.T) { req := httptest.NewRequest("GET", "http://example.com", nil) key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err) issued := time.Now() claims := accessTokenClaims{ Email: "test@example.com", Type: "app", Claims: jwt.Claims{ Issuer: issuer, Subject: "ee239b7a-e3e6-4173-972a-8fbe9d99c04f", Audience: []string{""}, Expiry: jwt.NewNumericDate(issued.Add(time.Hour)), IssuedAt: jwt.NewNumericDate(issued), }, } token := signToken(t, claims, key) req.Header.Add(headerKeyAccessJWTAssertion, token) keySet := oidc.StaticKeySet{PublicKeys: []crypto.PublicKey{key.Public()}} config := &oidc.Config{ SkipClientIDCheck: true, SupportedSigningAlgs: []string{string(jose.ES256)}, } verifier := oidc.NewVerifier(issuer, &keySet, config) tests := []struct { name string audTags []string aud jwt.Audience error bool }{ { name: "valid", audTags: []string{ "0bc545634b1732494b3f9472794a549c883fabd48de9dfe0e0413e59c3f96c38", "d7ec5b7fda23ffa8f8c8559fb37c66a2278208a78dbe376a3394b5ffec6911ba", }, aud: jwt.Audience{"d7ec5b7fda23ffa8f8c8559fb37c66a2278208a78dbe376a3394b5ffec6911ba"}, error: false, }, { name: "invalid no match", audTags: []string{ "0bc545634b1732494b3f9472794a549c883fabd48de9dfe0e0413e59c3f96c38", "d7ec5b7fda23ffa8f8c8559fb37c66a2278208a78dbe376a3394b5ffec6911ba", }, aud: jwt.Audience{"09dc377143841843ecca28b196bdb1ec1675af38c8b7b60c7def5876c8877157"}, error: true, }, { name: "invalid empty check", audTags: []string{}, aud: jwt.Audience{"09dc377143841843ecca28b196bdb1ec1675af38c8b7b60c7def5876c8877157"}, error: true, }, { name: "invalid absent aud", audTags: []string{ "0bc545634b1732494b3f9472794a549c883fabd48de9dfe0e0413e59c3f96c38", "d7ec5b7fda23ffa8f8c8559fb37c66a2278208a78dbe376a3394b5ffec6911ba", }, aud: jwt.Audience{""}, error: true, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { validator := JWTValidator{ IDTokenVerifier: verifier, audTags: test.audTags, } claims.Audience = test.aud token := signToken(t, claims, key) req.Header.Set(headerKeyAccessJWTAssertion, token) result, err := validator.Handle(context.Background(), req) assert.NoError(t, err) assert.Equal(t, test.error, result.ShouldFilterRequest) }) } } func signToken(t *testing.T, token accessTokenClaims, key *ecdsa.PrivateKey) string { signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: key}, &jose.SignerOptions{}) require.NoError(t, err) payload, err := json.Marshal(token) require.NoError(t, err) jws, err := signer.Sign(payload) require.NoError(t, err) jwt, err := jws.CompactSerialize() require.NoError(t, err) return jwt } ================================================ FILE: ingress/middleware/middleware.go ================================================ package middleware import ( "context" "net/http" ) type HandleResult struct { // Tells that the request didn't passed the handler and should be filtered ShouldFilterRequest bool // The status code to return in case ShouldFilterRequest is true. StatusCode int Reason string } type Handler interface { Name() string Handle(ctx context.Context, r *http.Request) (result *HandleResult, err error) } ================================================ FILE: ingress/origin_connection.go ================================================ package ingress import ( "context" "io" "net" "time" "github.com/rs/zerolog" "github.com/cloudflare/cloudflared/ipaccess" "github.com/cloudflare/cloudflared/socks" "github.com/cloudflare/cloudflared/stream" "github.com/cloudflare/cloudflared/websocket" ) // OriginConnection is a way to stream to a service running on the user's origin. // Different concrete implementations will stream different protocols as long as they are io.ReadWriters. type OriginConnection interface { // Stream should generally be implemented as a bidirectional io.Copy. Stream(ctx context.Context, tunnelConn io.ReadWriter, log *zerolog.Logger) Close() error } type streamHandlerFunc func(originConn io.ReadWriter, remoteConn net.Conn, log *zerolog.Logger) // DefaultStreamHandler is an implementation of streamHandlerFunc that // performs a two way io.Copy between originConn and remoteConn. func DefaultStreamHandler(originConn io.ReadWriter, remoteConn net.Conn, log *zerolog.Logger) { stream.Pipe(originConn, remoteConn, log) } // tcpConnection is an OriginConnection that directly streams to raw TCP. type tcpConnection struct { net.Conn writeTimeout time.Duration logger *zerolog.Logger } func (tc *tcpConnection) Stream(_ context.Context, tunnelConn io.ReadWriter, _ *zerolog.Logger) { stream.Pipe(tunnelConn, tc, tc.logger) } func (tc *tcpConnection) Write(b []byte) (int, error) { if tc.writeTimeout > 0 { if err := tc.Conn.SetWriteDeadline(time.Now().Add(tc.writeTimeout)); err != nil { tc.logger.Err(err).Msg("Error setting write deadline for TCP connection") } } return tc.Conn.Write(b) } // tcpOverWSConnection is an OriginConnection that streams to TCP over WS. type tcpOverWSConnection struct { conn net.Conn streamHandler streamHandlerFunc } func (wc *tcpOverWSConnection) Stream(ctx context.Context, tunnelConn io.ReadWriter, log *zerolog.Logger) { wsCtx, cancel := context.WithCancel(ctx) wsConn := websocket.NewConn(wsCtx, tunnelConn, log) wc.streamHandler(wsConn, wc.conn, log) cancel() // Makes sure wsConn stops sending ping before terminating the stream wsConn.Close() } func (wc *tcpOverWSConnection) Close() error { return wc.conn.Close() } // socksProxyOverWSConnection is an OriginConnection that streams SOCKS connections over WS. // The connection to the origin happens inside the SOCKS code as the client specifies the origin // details in the packet. type socksProxyOverWSConnection struct { accessPolicy *ipaccess.Policy } func (sp *socksProxyOverWSConnection) Stream(ctx context.Context, tunnelConn io.ReadWriter, log *zerolog.Logger) { wsCtx, cancel := context.WithCancel(ctx) wsConn := websocket.NewConn(wsCtx, tunnelConn, log) socks.StreamNetHandler(wsConn, sp.accessPolicy, log) cancel() // Makes sure wsConn stops sending ping before terminating the stream wsConn.Close() } func (sp *socksProxyOverWSConnection) Close() error { return nil } ================================================ FILE: ingress/origin_connection_test.go ================================================ package ingress import ( "bytes" "context" "fmt" "io" "net" "net/http" "net/http/httptest" "net/url" "testing" "time" "github.com/gobwas/ws/wsutil" gorillaWS "github.com/gorilla/websocket" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/net/proxy" "golang.org/x/sync/errgroup" "github.com/cloudflare/cloudflared/socks" "github.com/cloudflare/cloudflared/stream" "github.com/cloudflare/cloudflared/websocket" ) const ( testStreamTimeout = time.Second * 3 echoHeaderName = "Test-Cloudflared-Echo" ) var ( testMessage = []byte("TestStreamOriginConnection") testResponse = []byte(fmt.Sprintf("echo-%s", testMessage)) ) func TestStreamTCPConnection(t *testing.T) { cfdConn, originConn := net.Pipe() tcpConn := tcpConnection{ Conn: cfdConn, writeTimeout: 30 * time.Second, } eyeballConn, edgeConn := net.Pipe() ctx, cancel := context.WithTimeout(context.Background(), testStreamTimeout) defer cancel() errGroup, ctx := errgroup.WithContext(ctx) errGroup.Go(func() error { _, err := eyeballConn.Write(testMessage) require.NoError(t, err) readBuffer := make([]byte, len(testResponse)) _, err = eyeballConn.Read(readBuffer) require.NoError(t, err) require.Equal(t, testResponse, readBuffer) return nil }) errGroup.Go(func() error { echoTCPOrigin(t, originConn) originConn.Close() return nil }) tcpConn.Stream(ctx, edgeConn, TestLogger) require.NoError(t, errGroup.Wait()) } func TestDefaultStreamWSOverTCPConnection(t *testing.T) { cfdConn, originConn := net.Pipe() tcpOverWSConn := tcpOverWSConnection{ conn: cfdConn, streamHandler: DefaultStreamHandler, } eyeballConn, edgeConn := net.Pipe() ctx, cancel := context.WithTimeout(context.Background(), testStreamTimeout) defer cancel() errGroup, ctx := errgroup.WithContext(ctx) errGroup.Go(func() error { echoWSEyeball(t, eyeballConn) return nil }) errGroup.Go(func() error { echoTCPOrigin(t, originConn) originConn.Close() return nil }) tcpOverWSConn.Stream(ctx, edgeConn, TestLogger) require.NoError(t, errGroup.Wait()) } // TestSocksStreamWSOverTCPConnection simulates proxying in socks mode. // Eyeball side runs cloudflared access tcp with --url flag to start a websocket forwarder which // wraps SOCKS5 traffic in websocket // Origin side runs a tcpOverWSConnection with socks.StreamHandler func TestSocksStreamWSOverTCPConnection(t *testing.T) { var ( sendMessage = t.Name() echoHeaderIncomingValue = fmt.Sprintf("header-%s", sendMessage) echoMessage = fmt.Sprintf("echo-%s", sendMessage) echoHeaderReturnValue = fmt.Sprintf("echo-%s", echoHeaderIncomingValue) ) statusCodes := []int{ http.StatusOK, http.StatusTemporaryRedirect, http.StatusBadRequest, http.StatusInternalServerError, } for _, status := range statusCodes { handler := func(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) require.NoError(t, err) require.Equal(t, []byte(sendMessage), body) require.Equal(t, echoHeaderIncomingValue, r.Header.Get(echoHeaderName)) w.Header().Set(echoHeaderName, echoHeaderReturnValue) w.WriteHeader(status) w.Write([]byte(echoMessage)) } origin := httptest.NewServer(http.HandlerFunc(handler)) defer origin.Close() originURL, err := url.Parse(origin.URL) require.NoError(t, err) originConn, err := net.Dial("tcp", originURL.Host) require.NoError(t, err) tcpOverWSConn := tcpOverWSConnection{ conn: originConn, streamHandler: socks.StreamHandler, } wsForwarderOutConn, edgeConn := net.Pipe() ctx, cancel := context.WithTimeout(context.Background(), testStreamTimeout) defer cancel() errGroup, ctx := errgroup.WithContext(ctx) errGroup.Go(func() error { tcpOverWSConn.Stream(ctx, edgeConn, TestLogger) return nil }) wsForwarderListener, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err) errGroup.Go(func() error { wsForwarderInConn, err := wsForwarderListener.Accept() require.NoError(t, err) defer wsForwarderInConn.Close() stream.Pipe(wsForwarderInConn, &wsEyeball{wsForwarderOutConn}, TestLogger) return nil }) eyeballDialer, err := proxy.SOCKS5("tcp", wsForwarderListener.Addr().String(), nil, proxy.Direct) require.NoError(t, err) transport := &http.Transport{ Dial: eyeballDialer.Dial, } // Request URL doesn't matter because the transport is using eyeballDialer to connectq req, err := http.NewRequestWithContext(ctx, "GET", "http://test-socks-stream.com", bytes.NewBuffer([]byte(sendMessage))) assert.NoError(t, err) req.Header.Set(echoHeaderName, echoHeaderIncomingValue) resp, err := transport.RoundTrip(req) assert.NoError(t, err) assert.Equal(t, status, resp.StatusCode) require.Equal(t, echoHeaderReturnValue, resp.Header.Get(echoHeaderName)) body, err := io.ReadAll(resp.Body) require.NoError(t, err) require.Equal(t, []byte(echoMessage), body) wsForwarderOutConn.Close() edgeConn.Close() tcpOverWSConn.Close() require.NoError(t, errGroup.Wait()) } } func TestWsConnReturnsBeforeStreamReturns(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { eyeballConn := &readWriter{ w: w, r: r.Body, } cfdConn, originConn := net.Pipe() tcpOverWSConn := tcpOverWSConnection{ conn: cfdConn, streamHandler: DefaultStreamHandler, } go func() { time.Sleep(time.Millisecond * 10) // Simulate losing connection to origin originConn.Close() }() ctx := context.WithValue(r.Context(), websocket.PingPeriodContextKey, time.Microsecond) tcpOverWSConn.Stream(ctx, eyeballConn, TestLogger) }) server := httptest.NewServer(handler) defer server.Close() client := server.Client() ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() errGroup, ctx := errgroup.WithContext(ctx) for i := 0; i < 50; i++ { eyeballConn, edgeConn := net.Pipe() req, err := http.NewRequestWithContext(ctx, http.MethodConnect, server.URL, edgeConn) assert.NoError(t, err) resp, err := client.Transport.RoundTrip(req) assert.NoError(t, err) assert.Equal(t, resp.StatusCode, http.StatusOK) errGroup.Go(func() error { for { if err := wsutil.WriteClientBinary(eyeballConn, testMessage); err != nil { return nil } } }) } assert.NoError(t, errGroup.Wait()) } type wsEyeball struct { conn net.Conn } func (wse *wsEyeball) Read(p []byte) (int, error) { data, err := wsutil.ReadServerBinary(wse.conn) if err != nil { return 0, err } return copy(p, data), nil } func (wse *wsEyeball) Write(p []byte) (int, error) { err := wsutil.WriteClientBinary(wse.conn, p) return len(p), err } func echoWSEyeball(t *testing.T, conn net.Conn) { defer func() { assert.NoError(t, conn.Close()) }() if !assert.NoError(t, wsutil.WriteClientBinary(conn, testMessage)) { return } readMsg, err := wsutil.ReadServerBinary(conn) if !assert.NoError(t, err) { return } assert.Equal(t, testResponse, readMsg) } func echoWSOrigin(t *testing.T, expectMessages bool) *httptest.Server { var upgrader = gorillaWS.Upgrader{ ReadBufferSize: 10, WriteBufferSize: 10, } ws := func(w http.ResponseWriter, r *http.Request) { header := make(http.Header) for k, vs := range r.Header { if k == "Test-Cloudflared-Echo" { header[k] = vs } } conn, err := upgrader.Upgrade(w, r, header) require.NoError(t, err) defer conn.Close() sawMessage := false for { messageType, p, err := conn.ReadMessage() if err != nil { if expectMessages && !sawMessage { t.Errorf("unexpected error: %v", err) } return } assert.Equal(t, testMessage, p) sawMessage = true if err := conn.WriteMessage(messageType, testResponse); err != nil { return } } } // NewTLSServer starts the server in another thread return httptest.NewTLSServer(http.HandlerFunc(ws)) } func echoTCPOrigin(t *testing.T, conn net.Conn) { readBuffer := make([]byte, len(testMessage)) _, err := conn.Read(readBuffer) assert.NoError(t, err) assert.Equal(t, testMessage, readBuffer) _, err = conn.Write(testResponse) assert.NoError(t, err) } type readWriter struct { w io.Writer r io.Reader } func (r *readWriter) Read(p []byte) (n int, err error) { return r.r.Read(p) } func (r *readWriter) Write(p []byte) (n int, err error) { return r.w.Write(p) } ================================================ FILE: ingress/origin_dialer.go ================================================ package ingress import ( "context" "fmt" "net" "net/netip" "sync" "time" "github.com/rs/zerolog" ) const writeDeadlineUDP = 200 * time.Millisecond // OriginTCPDialer provides a TCP dial operation to a requested address. type OriginTCPDialer interface { DialTCP(ctx context.Context, addr netip.AddrPort) (net.Conn, error) } // OriginUDPDialer provides a UDP dial operation to a requested address. type OriginUDPDialer interface { DialUDP(addr netip.AddrPort) (net.Conn, error) } // OriginDialer provides both TCP and UDP dial operations to an address. type OriginDialer interface { OriginTCPDialer OriginUDPDialer } type OriginConfig struct { // The default Dialer used if no reserved services are found for an origin request. DefaultDialer OriginDialer // Timeout on write operations for TCP connections to the origin. TCPWriteTimeout time.Duration } // OriginDialerService provides a proxy TCP and UDP dialer to origin services while allowing reserved // services to be provided. These reserved services are assigned to specific [netip.AddrPort]s // and provide their own [OriginDialer]'s to handle origin dialing per protocol. type OriginDialerService struct { // Reserved TCP services for reserved AddrPort values reservedTCPServices map[netip.AddrPort]OriginTCPDialer // Reserved UDP services for reserved AddrPort values reservedUDPServices map[netip.AddrPort]OriginUDPDialer // The default Dialer used if no reserved services are found for an origin request defaultDialer OriginDialer defaultDialerM sync.RWMutex // Write timeout for TCP connections writeTimeout time.Duration logger *zerolog.Logger } func NewOriginDialer(config OriginConfig, logger *zerolog.Logger) *OriginDialerService { return &OriginDialerService{ reservedTCPServices: map[netip.AddrPort]OriginTCPDialer{}, reservedUDPServices: map[netip.AddrPort]OriginUDPDialer{}, defaultDialer: config.DefaultDialer, writeTimeout: config.TCPWriteTimeout, logger: logger, } } // AddReservedService adds a reserved virtual service to dial to. // Not locked and expected to be initialized before calling first dial and not afterwards. func (d *OriginDialerService) AddReservedService(service OriginDialer, addrs []netip.AddrPort) { for _, addr := range addrs { d.reservedTCPServices[addr] = service d.reservedUDPServices[addr] = service } } // UpdateDefaultDialer updates the default dialer. func (d *OriginDialerService) UpdateDefaultDialer(dialer *Dialer) { d.defaultDialerM.Lock() defer d.defaultDialerM.Unlock() d.defaultDialer = dialer } // DialTCP will perform a dial TCP to the requested addr. func (d *OriginDialerService) DialTCP(ctx context.Context, addr netip.AddrPort) (net.Conn, error) { conn, err := d.dialTCP(ctx, addr) if err != nil { return nil, err } // Assign the write timeout for the TCP operations return &tcpConnection{ Conn: conn, writeTimeout: d.writeTimeout, logger: d.logger, }, nil } func (d *OriginDialerService) dialTCP(ctx context.Context, addr netip.AddrPort) (net.Conn, error) { // Check to see if any reserved services are available for this addr and call their dialer instead. if dialer, ok := d.reservedTCPServices[addr]; ok { return dialer.DialTCP(ctx, addr) } d.defaultDialerM.RLock() dialer := d.defaultDialer d.defaultDialerM.RUnlock() return dialer.DialTCP(ctx, addr) } // DialUDP will perform a dial UDP to the requested addr. func (d *OriginDialerService) DialUDP(addr netip.AddrPort) (net.Conn, error) { // Check to see if any reserved services are available for this addr and call their dialer instead. if dialer, ok := d.reservedUDPServices[addr]; ok { return dialer.DialUDP(addr) } d.defaultDialerM.RLock() dialer := d.defaultDialer d.defaultDialerM.RUnlock() return dialer.DialUDP(addr) } type Dialer struct { Dialer net.Dialer } func NewDialer(config WarpRoutingConfig) *Dialer { return &Dialer{ Dialer: net.Dialer{ Timeout: config.ConnectTimeout.Duration, KeepAlive: config.TCPKeepAlive.Duration, }, } } func (d *Dialer) DialTCP(ctx context.Context, dest netip.AddrPort) (net.Conn, error) { conn, err := d.Dialer.DialContext(ctx, "tcp", dest.String()) if err != nil { return nil, fmt.Errorf("unable to dial tcp to origin %s: %w", dest, err) } return conn, nil } func (d *Dialer) DialUDP(dest netip.AddrPort) (net.Conn, error) { conn, err := d.Dialer.Dial("udp", dest.String()) if err != nil { return nil, fmt.Errorf("unable to dial udp to origin %s: %w", dest, err) } return &writeDeadlineConn{ Conn: conn, }, nil } // writeDeadlineConn is a wrapper around a net.Conn that sets a write deadline of 200ms. // This is to prevent the socket from blocking on the write operation if it were to occur. However, // we typically never expect this to occur except under high load or kernel issues. type writeDeadlineConn struct { net.Conn } func (w *writeDeadlineConn) Write(b []byte) (int, error) { if err := w.SetWriteDeadline(time.Now().Add(writeDeadlineUDP)); err != nil { return 0, err } return w.Conn.Write(b) } ================================================ FILE: ingress/origin_icmp_proxy.go ================================================ package ingress import ( "context" "fmt" "net/netip" "time" "github.com/rs/zerolog" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" "golang.org/x/net/icmp" "golang.org/x/net/ipv4" "golang.org/x/net/ipv6" "github.com/cloudflare/cloudflared/packet" "github.com/cloudflare/cloudflared/tracing" ) const ( mtu = 1500 // icmpRequestTimeoutMs controls how long to wait for a reply icmpRequestTimeoutMs = 1000 ) var ( errPacketNil = fmt.Errorf("packet is nil") ) // ICMPRouterServer is a parent interface over-top of ICMPRouter that allows for the operation of the proxy origin listeners. type ICMPRouterServer interface { ICMPRouter // Serve runs the ICMPRouter proxy origin listeners for any of the IPv4 or IPv6 interfaces configured. Serve(ctx context.Context) error } // ICMPRouter manages out-going ICMP requests towards the origin. type ICMPRouter interface { // Request will send an ICMP packet towards the origin with an ICMPResponder to attach to the ICMP flow for the // response to utilize. Request(ctx context.Context, pk *packet.ICMP, responder ICMPResponder) error // ConvertToTTLExceeded will take an ICMP packet and create a ICMP TTL Exceeded response origininating from the // ICMPRouter's IP interface. ConvertToTTLExceeded(pk *packet.ICMP, rawPacket packet.RawPacket) *packet.ICMP } // ICMPResponder manages how to handle incoming ICMP messages coming from the origin to the edge. type ICMPResponder interface { ConnectionIndex() uint8 ReturnPacket(pk *packet.ICMP) error AddTraceContext(tracedCtx *tracing.TracedContext, serializedIdentity []byte) RequestSpan(ctx context.Context, pk *packet.ICMP) (context.Context, trace.Span) ReplySpan(ctx context.Context, logger *zerolog.Logger) (context.Context, trace.Span) ExportSpan() } type icmpRouter struct { ipv4Proxy *icmpProxy ipv4Src netip.Addr ipv6Proxy *icmpProxy ipv6Src netip.Addr } // NewICMPRouter doesn't return an error if either ipv4 proxy or ipv6 proxy can be created. The machine might only // support one of them. // funnelIdleTimeout controls how long to wait to close a funnel without send/return func NewICMPRouter(ipv4Addr, ipv6Addr netip.Addr, logger *zerolog.Logger, funnelIdleTimeout time.Duration) (ICMPRouterServer, error) { ipv4Proxy, ipv4Err := newICMPProxy(ipv4Addr, logger, funnelIdleTimeout) ipv6Proxy, ipv6Err := newICMPProxy(ipv6Addr, logger, funnelIdleTimeout) if ipv4Err != nil && ipv6Err != nil { err := fmt.Errorf("cannot create ICMPv4 proxy: %v nor ICMPv6 proxy: %v", ipv4Err, ipv6Err) logger.Debug().Err(err).Msg("ICMP proxy feature is disabled") return nil, err } if ipv4Err != nil { logger.Debug().Err(ipv4Err).Msg("failed to create ICMPv4 proxy, only ICMPv6 proxy is created") ipv4Proxy = nil } if ipv6Err != nil { logger.Debug().Err(ipv6Err).Msg("failed to create ICMPv6 proxy, only ICMPv4 proxy is created") ipv6Proxy = nil } return &icmpRouter{ ipv4Proxy: ipv4Proxy, ipv4Src: ipv4Addr, ipv6Proxy: ipv6Proxy, ipv6Src: ipv6Addr, }, nil } func (ir *icmpRouter) Serve(ctx context.Context) error { if ir.ipv4Proxy != nil && ir.ipv6Proxy != nil { errC := make(chan error, 2) go func() { errC <- ir.ipv4Proxy.Serve(ctx) }() go func() { errC <- ir.ipv6Proxy.Serve(ctx) }() return <-errC } if ir.ipv4Proxy != nil { return ir.ipv4Proxy.Serve(ctx) } if ir.ipv6Proxy != nil { return ir.ipv6Proxy.Serve(ctx) } return fmt.Errorf("ICMPv4 proxy and ICMPv6 proxy are both nil") } func (ir *icmpRouter) Request(ctx context.Context, pk *packet.ICMP, responder ICMPResponder) error { if pk == nil { return errPacketNil } if pk.Dst.Is4() { if ir.ipv4Proxy != nil { return ir.ipv4Proxy.Request(ctx, pk, responder) } return fmt.Errorf("ICMPv4 proxy was not instantiated") } if ir.ipv6Proxy != nil { return ir.ipv6Proxy.Request(ctx, pk, responder) } return fmt.Errorf("ICMPv6 proxy was not instantiated") } func (ir *icmpRouter) ConvertToTTLExceeded(pk *packet.ICMP, rawPacket packet.RawPacket) *packet.ICMP { var srcIP netip.Addr if pk.Dst.Is4() { srcIP = ir.ipv4Src } else { srcIP = ir.ipv6Src } return packet.NewICMPTTLExceedPacket(pk.IP, rawPacket, srcIP) } func getICMPEcho(msg *icmp.Message) (*icmp.Echo, error) { echo, ok := msg.Body.(*icmp.Echo) if !ok { return nil, fmt.Errorf("expect ICMP echo, got %s", msg.Type) } return echo, nil } func isEchoReply(msg *icmp.Message) bool { return msg.Type == ipv4.ICMPTypeEchoReply || msg.Type == ipv6.ICMPTypeEchoReply } func observeICMPRequest(logger *zerolog.Logger, span trace.Span, src string, dst string, echoID int, seq int) { incrementICMPRequest() logger.Debug(). Str("src", src). Str("dst", dst). Int("originalEchoID", echoID). Int("originalEchoSeq", seq). Msg("Received ICMP request") span.SetAttributes( attribute.Int("originalEchoID", echoID), attribute.Int("seq", seq), ) } func observeICMPReply(logger *zerolog.Logger, span trace.Span, dst string, echoID int, seq int) { incrementICMPReply() logger.Debug().Str("dst", dst).Int("echoID", echoID).Int("seq", seq).Msg("Sent ICMP reply to edge") span.SetAttributes( attribute.String("dst", dst), attribute.Int("echoID", echoID), attribute.Int("seq", seq), ) } ================================================ FILE: ingress/origin_icmp_proxy_test.go ================================================ package ingress import ( "bytes" "context" "fmt" "net" "net/netip" "strings" "sync" "testing" "time" "github.com/fortytw2/leaktest" "github.com/google/gopacket/layers" "github.com/rs/zerolog" "github.com/stretchr/testify/require" "golang.org/x/net/icmp" "golang.org/x/net/ipv4" "golang.org/x/net/ipv6" "github.com/cloudflare/cloudflared/packet" quicpogs "github.com/cloudflare/cloudflared/quic" "github.com/cloudflare/cloudflared/tracing" ) var ( noopLogger = zerolog.Nop() localhostIP = netip.MustParseAddr("127.0.0.1") localhostIPv6 = netip.MustParseAddr("::1") testFunnelIdleTimeout = time.Millisecond * 10 ) // TestICMPProxyEcho makes sure we can send ICMP echo via the Request method and receives response via the // ListenResponse method // // Note: if this test fails on your device under Linux, then most likely you need to make sure that your user // is allowed in ping_group_range. See the following gist for how to do that: // https://github.com/ValentinBELYN/icmplib/blob/main/docs/6-use-icmplib-without-privileges.md func TestICMPRouterEcho(t *testing.T) { testICMPRouterEcho(t, true) testICMPRouterEcho(t, false) } func testICMPRouterEcho(t *testing.T, sendIPv4 bool) { defer leaktest.Check(t)() const ( echoID = 36571 endSeq = 20 ) router, err := NewICMPRouter(localhostIP, localhostIPv6, &noopLogger, testFunnelIdleTimeout) require.NoError(t, err) proxyDone := make(chan struct{}) ctx, cancel := context.WithCancel(context.Background()) go func() { router.Serve(ctx) close(proxyDone) }() muxer := newMockMuxer(1) responder := newPacketResponder(muxer, 0, packet.NewEncoder()) protocol := layers.IPProtocolICMPv6 if sendIPv4 { protocol = layers.IPProtocolICMPv4 } localIPs := getLocalIPs(t, sendIPv4) ips := make([]*packet.IP, len(localIPs)) for i, localIP := range localIPs { ips[i] = &packet.IP{ Src: localIP, Dst: localIP, Protocol: protocol, TTL: packet.DefaultTTL, } } var icmpType icmp.Type = ipv6.ICMPTypeEchoRequest if sendIPv4 { icmpType = ipv4.ICMPTypeEcho } for seq := 0; seq < endSeq; seq++ { for i, ip := range ips { pk := packet.ICMP{ IP: ip, Message: &icmp.Message{ Type: icmpType, Code: 0, Body: &icmp.Echo{ ID: echoID + i, Seq: seq, Data: []byte(fmt.Sprintf("icmp echo seq %d", seq)), }, }, } require.NoError(t, router.Request(ctx, &pk, responder)) validateEchoFlow(t, <-muxer.cfdToEdge, &pk) } } // Make sure funnel cleanup kicks in time.Sleep(testFunnelIdleTimeout * 2) cancel() <-proxyDone } func TestTraceICMPRouterEcho(t *testing.T) { defer leaktest.Check(t)() tracingCtx := "ec31ad8a01fde11fdcabe2efdce36873:52726f6cabc144f5:0:1" router, err := NewICMPRouter(localhostIP, localhostIPv6, &noopLogger, testFunnelIdleTimeout) require.NoError(t, err) proxyDone := make(chan struct{}) ctx, cancel := context.WithCancel(context.Background()) go func() { router.Serve(ctx) close(proxyDone) }() // Buffer 3 packets, request span, reply span and reply muxer := newMockMuxer(3) tracingIdentity, err := tracing.NewIdentity(tracingCtx) require.NoError(t, err) serializedIdentity, err := tracingIdentity.MarshalBinary() require.NoError(t, err) responder := newPacketResponder(muxer, 0, packet.NewEncoder()) responder.AddTraceContext(tracing.NewTracedContext(ctx, tracingIdentity.String(), &noopLogger), serializedIdentity) echo := &icmp.Echo{ ID: 12910, Seq: 182, Data: []byte(t.Name()), } pk := packet.ICMP{ IP: &packet.IP{ Src: localhostIP, Dst: localhostIP, Protocol: layers.IPProtocolICMPv4, TTL: packet.DefaultTTL, }, Message: &icmp.Message{ Type: ipv4.ICMPTypeEcho, Code: 0, Body: echo, }, } require.NoError(t, router.Request(ctx, &pk, responder)) firstPK := <-muxer.cfdToEdge var requestSpan *quicpogs.TracingSpanPacket // The order of receiving reply or request span is not deterministic switch firstPK.Type() { case quicpogs.DatagramTypeIP: // reply packet validateEchoFlow(t, firstPK, &pk) case quicpogs.DatagramTypeTracingSpan: // Request span requestSpan = firstPK.(*quicpogs.TracingSpanPacket) require.NotEmpty(t, requestSpan.Spans) require.True(t, bytes.Equal(serializedIdentity, requestSpan.TracingIdentity)) default: panic(fmt.Sprintf("received unexpected packet type %d", firstPK.Type())) } secondPK := <-muxer.cfdToEdge if requestSpan != nil { // If first packet is request span, second packet should be the reply validateEchoFlow(t, secondPK, &pk) } else { requestSpan = secondPK.(*quicpogs.TracingSpanPacket) require.NotEmpty(t, requestSpan.Spans) require.True(t, bytes.Equal(serializedIdentity, requestSpan.TracingIdentity)) } // Reply span thirdPacket := <-muxer.cfdToEdge replySpan, ok := thirdPacket.(*quicpogs.TracingSpanPacket) require.True(t, ok) require.NotEmpty(t, replySpan.Spans) require.True(t, bytes.Equal(serializedIdentity, replySpan.TracingIdentity)) require.False(t, bytes.Equal(requestSpan.Spans, replySpan.Spans)) echo.Seq++ pk.Body = echo // Only first request for a flow is traced. The edge will not send tracing context for the second request newResponder := newPacketResponder(muxer, 0, packet.NewEncoder()) require.NoError(t, router.Request(ctx, &pk, newResponder)) validateEchoFlow(t, <-muxer.cfdToEdge, &pk) select { case receivedPacket := <-muxer.cfdToEdge: panic(fmt.Sprintf("Receive unexpected packet %+v", receivedPacket)) default: } time.Sleep(testFunnelIdleTimeout * 2) cancel() <-proxyDone } // TestConcurrentRequests makes sure icmpRouter can send concurrent requests to the same destination with different // echo ID. This simulates concurrent ping to the same destination. func TestConcurrentRequestsToSameDst(t *testing.T) { defer leaktest.Check(t)() const ( concurrentPings = 5 endSeq = 5 ) router, err := NewICMPRouter(localhostIP, localhostIPv6, &noopLogger, testFunnelIdleTimeout) require.NoError(t, err) proxyDone := make(chan struct{}) ctx, cancel := context.WithCancel(context.Background()) go func() { router.Serve(ctx) close(proxyDone) }() var wg sync.WaitGroup // icmpv4 and icmpv6 each has concurrentPings wg.Add(concurrentPings * 2) for i := 0; i < concurrentPings; i++ { echoID := 38451 + i go func() { defer wg.Done() muxer := newMockMuxer(1) responder := newPacketResponder(muxer, 0, packet.NewEncoder()) for seq := 0; seq < endSeq; seq++ { pk := &packet.ICMP{ IP: &packet.IP{ Src: localhostIP, Dst: localhostIP, Protocol: layers.IPProtocolICMPv4, TTL: packet.DefaultTTL, }, Message: &icmp.Message{ Type: ipv4.ICMPTypeEcho, Code: 0, Body: &icmp.Echo{ ID: echoID, Seq: seq, Data: []byte(fmt.Sprintf("icmpv4 echo id %d, seq %d", echoID, seq)), }, }, } require.NoError(t, router.Request(ctx, pk, responder)) validateEchoFlow(t, <-muxer.cfdToEdge, pk) } }() go func() { defer wg.Done() muxer := newMockMuxer(1) responder := newPacketResponder(muxer, 0, packet.NewEncoder()) for seq := 0; seq < endSeq; seq++ { pk := &packet.ICMP{ IP: &packet.IP{ Src: localhostIPv6, Dst: localhostIPv6, Protocol: layers.IPProtocolICMPv6, TTL: packet.DefaultTTL, }, Message: &icmp.Message{ Type: ipv6.ICMPTypeEchoRequest, Code: 0, Body: &icmp.Echo{ ID: echoID, Seq: seq, Data: []byte(fmt.Sprintf("icmpv6 echo id %d, seq %d", echoID, seq)), }, }, } require.NoError(t, router.Request(ctx, pk, responder)) validateEchoFlow(t, <-muxer.cfdToEdge, pk) } }() } wg.Wait() time.Sleep(testFunnelIdleTimeout * 2) cancel() <-proxyDone } // TestICMPProxyRejectNotEcho makes sure it rejects messages other than echo func TestICMPRouterRejectNotEcho(t *testing.T) { defer leaktest.Check(t)() msgs := []icmp.Message{ { Type: ipv4.ICMPTypeDestinationUnreachable, Code: 1, Body: &icmp.DstUnreach{ Data: []byte("original packet"), }, }, { Type: ipv4.ICMPTypeTimeExceeded, Code: 1, Body: &icmp.TimeExceeded{ Data: []byte("original packet"), }, }, { Type: ipv4.ICMPType(2), Code: 0, Body: &icmp.PacketTooBig{ MTU: 1280, Data: []byte("original packet"), }, }, } testICMPRouterRejectNotEcho(t, localhostIP, msgs) msgsV6 := []icmp.Message{ { Type: ipv6.ICMPTypeDestinationUnreachable, Code: 3, Body: &icmp.DstUnreach{ Data: []byte("original packet"), }, }, { Type: ipv6.ICMPTypeTimeExceeded, Code: 0, Body: &icmp.TimeExceeded{ Data: []byte("original packet"), }, }, { Type: ipv6.ICMPTypePacketTooBig, Code: 0, Body: &icmp.PacketTooBig{ MTU: 1280, Data: []byte("original packet"), }, }, } testICMPRouterRejectNotEcho(t, localhostIPv6, msgsV6) } func testICMPRouterRejectNotEcho(t *testing.T, srcDstIP netip.Addr, msgs []icmp.Message) { router, err := NewICMPRouter(localhostIP, localhostIPv6, &noopLogger, testFunnelIdleTimeout) require.NoError(t, err) muxer := newMockMuxer(1) responder := newPacketResponder(muxer, 0, packet.NewEncoder()) protocol := layers.IPProtocolICMPv4 if srcDstIP.Is6() { protocol = layers.IPProtocolICMPv6 } for _, m := range msgs { pk := packet.ICMP{ IP: &packet.IP{ Src: srcDstIP, Dst: srcDstIP, Protocol: protocol, TTL: packet.DefaultTTL, }, Message: &m, } require.Error(t, router.Request(context.Background(), &pk, responder)) } } func validateEchoFlow(t *testing.T, pk quicpogs.Packet, echoReq *packet.ICMP) { decoder := packet.NewICMPDecoder() decoded, err := decoder.Decode(packet.RawPacket{Data: pk.Payload()}) require.NoError(t, err, pk) require.Equal(t, decoded.Src, echoReq.Dst) require.Equal(t, decoded.Dst, echoReq.Src) require.Equal(t, echoReq.Protocol, decoded.Protocol) if echoReq.Type == ipv4.ICMPTypeEcho { require.Equal(t, ipv4.ICMPTypeEchoReply, decoded.Type) } else { require.Equal(t, ipv6.ICMPTypeEchoReply, decoded.Type) } require.Equal(t, 0, decoded.Code) require.NotZero(t, decoded.Checksum) require.Equal(t, echoReq.Body, decoded.Body) } func getLocalIPs(t *testing.T, ipv4 bool) []netip.Addr { interfaces, err := net.Interfaces() require.NoError(t, err) localIPs := []netip.Addr{} for _, i := range interfaces { // Skip TUN devices, and Docker Networks if strings.Contains(i.Name, "tun") || strings.Contains(i.Name, "docker") || strings.HasPrefix(i.Name, "br-") { continue } addrs, err := i.Addrs() require.NoError(t, err) for _, addr := range addrs { if ipnet, ok := addr.(*net.IPNet); ok && (ipnet.IP.IsPrivate() || ipnet.IP.IsLoopback()) { // TODO DEVTOOLS-12514: We only run the IPv6 against the loopback interface due to issues on the CI runners. if (ipv4 && ipnet.IP.To4() != nil) || (!ipv4 && ipnet.IP.To4() == nil && ipnet.IP.IsLoopback()) { localIPs = append(localIPs, netip.MustParseAddr(ipnet.IP.String())) } } } } return localIPs } ================================================ FILE: ingress/origin_proxy.go ================================================ package ingress import ( "context" "crypto/tls" "fmt" "net" "net/http" "github.com/rs/zerolog" ) // HTTPOriginProxy can be implemented by origin services that want to proxy http requests. type HTTPOriginProxy interface { // RoundTripper is how cloudflared proxies eyeball requests to the actual origin services http.RoundTripper } // StreamBasedOriginProxy can be implemented by origin services that want to proxy ws/TCP. type StreamBasedOriginProxy interface { EstablishConnection(ctx context.Context, dest string, log *zerolog.Logger) (OriginConnection, error) } // HTTPLocalProxy can be implemented by cloudflared services that want to handle incoming http requests. type HTTPLocalProxy interface { // Handler is how cloudflared proxies eyeball requests to the local cloudflared services http.Handler } func (o *unixSocketPath) RoundTrip(req *http.Request) (*http.Response, error) { req.URL.Scheme = o.scheme return o.transport.RoundTrip(req) } func (o *httpService) RoundTrip(req *http.Request) (*http.Response, error) { // Rewrite the request URL so that it goes to the origin service. req.URL.Host = o.url.Host switch o.url.Scheme { case "ws": req.URL.Scheme = "http" case "wss": req.URL.Scheme = "https" default: req.URL.Scheme = o.url.Scheme } if o.hostHeader != "" { // For incoming requests, the Host header is promoted to the Request.Host field and removed from the Header map. // Pass the original Host header as X-Forwarded-Host. req.Header.Set("X-Forwarded-Host", req.Host) req.Host = o.hostHeader } if o.matchSNIToHost { o.SetOriginServerName(req) } return o.transport.RoundTrip(req) } func (o *httpService) SetOriginServerName(req *http.Request) { o.transport.DialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) { conn, err := o.transport.DialContext(ctx, network, addr) if err != nil { return nil, err } return tls.Client(conn, &tls.Config{ RootCAs: o.transport.TLSClientConfig.RootCAs, InsecureSkipVerify: o.transport.TLSClientConfig.InsecureSkipVerify, // nolint: gosec ServerName: req.Host, }), nil } } func (o *statusCode) RoundTrip(_ *http.Request) (*http.Response, error) { if o.defaultResp { o.log.Warn().Msg(ErrNoIngressRulesCLI.Error()) } resp := &http.Response{ StatusCode: o.code, Status: fmt.Sprintf("%d %s", o.code, http.StatusText(o.code)), Body: new(NopReadCloser), } return resp, nil } func (o *rawTCPService) EstablishConnection(ctx context.Context, dest string, logger *zerolog.Logger) (OriginConnection, error) { conn, err := o.dialer.DialContext(ctx, "tcp", dest) if err != nil { return nil, err } originConn := &tcpConnection{ Conn: conn, writeTimeout: o.writeTimeout, logger: logger, } return originConn, nil } func (o *tcpOverWSService) EstablishConnection(ctx context.Context, dest string, _ *zerolog.Logger) (OriginConnection, error) { var err error if !o.isBastion { dest = o.dest } conn, err := o.dialer.DialContext(ctx, "tcp", dest) if err != nil { return nil, err } originConn := &tcpOverWSConnection{ conn: conn, streamHandler: o.streamHandler, } return originConn, nil } func (o *socksProxyOverWSService) EstablishConnection(_ context.Context, _ string, _ *zerolog.Logger) (OriginConnection, error) { return o.conn, nil } ================================================ FILE: ingress/origin_proxy_test.go ================================================ package ingress import ( "context" "fmt" "io" "net" "net/http" "net/http/httptest" "net/url" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/cloudflare/cloudflared/carrier" "github.com/cloudflare/cloudflared/websocket" ) func TestRawTCPServiceEstablishConnection(t *testing.T) { originListener, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err) listenerClosed := make(chan struct{}) tcpListenRoutine(originListener, listenerClosed) rawTCPService := &rawTCPService{name: ServiceWarpRouting} req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://%s", originListener.Addr()), nil) require.NoError(t, err) originListener.Close() <-listenerClosed req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("http://%s", originListener.Addr()), nil) require.NoError(t, err) // Origin not listening for new connection, should return an error _, err = rawTCPService.EstablishConnection(context.Background(), req.URL.String(), TestLogger) require.Error(t, err) } func TestTCPOverWSServiceEstablishConnection(t *testing.T) { originListener, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err) listenerClosed := make(chan struct{}) tcpListenRoutine(originListener, listenerClosed) originURL := &url.URL{ Scheme: "tcp", Host: originListener.Addr().String(), } baseReq, err := http.NewRequest(http.MethodGet, "https://place-holder", nil) require.NoError(t, err) baseReq.Header.Set("Sec-Websocket-Key", "dGhlIHNhbXBsZSBub25jZQ==") bastionReq := baseReq.Clone(context.Background()) carrier.SetBastionDest(bastionReq.Header, originListener.Addr().String()) tests := []struct { testCase string service *tcpOverWSService req *http.Request expectErr bool }{ { testCase: "specific TCP service", service: newTCPOverWSService(originURL), req: baseReq, }, { testCase: "bastion service", service: newBastionService(), req: bastionReq, }, { testCase: "invalid bastion request", service: newBastionService(), req: baseReq, expectErr: true, }, } for _, test := range tests { t.Run(test.testCase, func(t *testing.T) { if test.expectErr { bastionHost, _ := carrier.ResolveBastionDest(test.req) _, err := test.service.EstablishConnection(context.Background(), bastionHost, TestLogger) assert.Error(t, err) } }) } originListener.Close() <-listenerClosed for _, service := range []*tcpOverWSService{newTCPOverWSService(originURL), newBastionService()} { // Origin not listening for new connection, should return an error bastionHost, _ := carrier.ResolveBastionDest(bastionReq) _, err := service.EstablishConnection(context.Background(), bastionHost, TestLogger) assert.Error(t, err) } } func TestHTTPServiceHostHeaderOverride(t *testing.T) { cfg := OriginRequestConfig{ HTTPHostHeader: t.Name(), } handler := func(w http.ResponseWriter, r *http.Request) { require.Equal(t, r.Host, t.Name()) if websocket.IsWebSocketUpgrade(r) { respHeaders := websocket.NewResponseHeader(r) for k, v := range respHeaders { w.Header().Set(k, v[0]) } w.WriteHeader(http.StatusSwitchingProtocols) return } // return the X-Forwarded-Host header for assertions // as the httptest Server URL isn't available here yet w.Write([]byte(r.Header.Get("X-Forwarded-Host"))) } origin := httptest.NewServer(http.HandlerFunc(handler)) defer origin.Close() originURL, err := url.Parse(origin.URL) require.NoError(t, err) httpService := &httpService{ url: originURL, } shutdownC := make(chan struct{}) require.NoError(t, httpService.start(TestLogger, shutdownC, cfg)) req, err := http.NewRequest(http.MethodGet, originURL.String(), nil) require.NoError(t, err) resp, err := httpService.RoundTrip(req) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.StatusCode) respBody, err := io.ReadAll(resp.Body) require.NoError(t, err) require.Equal(t, respBody, []byte(originURL.Host)) } // TestHTTPServiceUsesIngressRuleScheme makes sure httpService uses scheme defined in ingress rule and not by eyeball request func TestHTTPServiceUsesIngressRuleScheme(t *testing.T) { handler := func(w http.ResponseWriter, r *http.Request) { require.NotNil(t, r.TLS) // Echo the X-Forwarded-Proto header for assertions w.Write([]byte(r.Header.Get("X-Forwarded-Proto"))) } origin := httptest.NewTLSServer(http.HandlerFunc(handler)) defer origin.Close() originURL, err := url.Parse(origin.URL) require.NoError(t, err) require.Equal(t, "https", originURL.Scheme) cfg := OriginRequestConfig{ NoTLSVerify: true, } httpService := &httpService{ url: originURL, } shutdownC := make(chan struct{}) require.NoError(t, httpService.start(TestLogger, shutdownC, cfg)) // Tunnel uses scheme defined in the service field of the ingress rule, independent of the X-Forwarded-Proto header protos := []string{"https", "http", "dne"} for _, p := range protos { req, err := http.NewRequest(http.MethodGet, originURL.String(), nil) require.NoError(t, err) req.Header.Add("X-Forwarded-Proto", p) resp, err := httpService.RoundTrip(req) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.StatusCode) respBody, err := io.ReadAll(resp.Body) require.NoError(t, err) require.Equal(t, respBody, []byte(p)) } } func tcpListenRoutine(listener net.Listener, closeChan chan struct{}) { go func() { for { conn, err := listener.Accept() if err != nil { close(closeChan) return } // Close immediately, this test is not about testing read/write on connection conn.Close() } }() } ================================================ FILE: ingress/origin_service.go ================================================ package ingress import ( "context" "crypto/tls" "encoding/json" "fmt" "io" "net" "net/http" "net/url" "strconv" "time" "github.com/pkg/errors" "github.com/rs/zerolog" "github.com/cloudflare/cloudflared/hello" "github.com/cloudflare/cloudflared/ipaccess" "github.com/cloudflare/cloudflared/management" "github.com/cloudflare/cloudflared/socks" "github.com/cloudflare/cloudflared/tlsconfig" ) const ( HelloWorldService = "hello_world" HelloWorldFlag = "hello-world" HttpStatusService = "http_status" ) // OriginService is something a tunnel can proxy traffic to. type OriginService interface { String() string // Start the origin service if it's managed by cloudflared, e.g. proxy servers or Hello World. // If it's not managed by cloudflared, this is a no-op because the user is responsible for // starting the origin service. // Implementor of services managed by cloudflared should terminate the service if shutdownC is closed start(log *zerolog.Logger, shutdownC <-chan struct{}, cfg OriginRequestConfig) error MarshalJSON() ([]byte, error) } // unixSocketPath is an OriginService representing a unix socket (which accepts HTTP or HTTPS) type unixSocketPath struct { path string scheme string transport *http.Transport } func (o *unixSocketPath) String() string { scheme := "" if o.scheme == "https" { scheme = "+tls" } return fmt.Sprintf("unix%s:%s", scheme, o.path) } func (o *unixSocketPath) start(log *zerolog.Logger, _ <-chan struct{}, cfg OriginRequestConfig) error { transport, err := newHTTPTransport(o, cfg, log) if err != nil { return err } o.transport = transport return nil } func (o unixSocketPath) MarshalJSON() ([]byte, error) { return json.Marshal(o.String()) } type httpService struct { url *url.URL hostHeader string transport *http.Transport matchSNIToHost bool } func (o *httpService) start(log *zerolog.Logger, _ <-chan struct{}, cfg OriginRequestConfig) error { transport, err := newHTTPTransport(o, cfg, log) if err != nil { return err } o.hostHeader = cfg.HTTPHostHeader o.transport = transport o.matchSNIToHost = cfg.MatchSNIToHost return nil } func (o *httpService) String() string { return o.url.String() } func (o httpService) MarshalJSON() ([]byte, error) { return json.Marshal(o.String()) } // rawTCPService dials TCP to the destination specified by the client // It's used by warp routing type rawTCPService struct { name string dialer net.Dialer writeTimeout time.Duration logger *zerolog.Logger } func (o *rawTCPService) String() string { return o.name } func (o *rawTCPService) start(_ *zerolog.Logger, _ <-chan struct{}, _ OriginRequestConfig) error { return nil } func (o rawTCPService) MarshalJSON() ([]byte, error) { return json.Marshal(o.String()) } // tcpOverWSService models TCP origins serving eyeballs connecting over websocket, such as // cloudflared access commands. type tcpOverWSService struct { scheme string dest string isBastion bool streamHandler streamHandlerFunc dialer net.Dialer } type socksProxyOverWSService struct { conn *socksProxyOverWSConnection } func newTCPOverWSService(url *url.URL) *tcpOverWSService { switch url.Scheme { case "ssh": addPortIfMissing(url, 22) case "rdp": addPortIfMissing(url, 3389) case "smb": addPortIfMissing(url, 445) case "tcp": addPortIfMissing(url, 7864) // just a random port since there isn't a default in this case } return &tcpOverWSService{ scheme: url.Scheme, dest: url.Host, } } func newBastionService() *tcpOverWSService { return &tcpOverWSService{ isBastion: true, } } func newSocksProxyOverWSService(accessPolicy *ipaccess.Policy) *socksProxyOverWSService { proxy := socksProxyOverWSService{ conn: &socksProxyOverWSConnection{ accessPolicy: accessPolicy, }, } return &proxy } func addPortIfMissing(uri *url.URL, port int) { hostname := uri.Hostname() if uri.Port() == "" { uri.Host = net.JoinHostPort(hostname, strconv.FormatInt(int64(port), 10)) } } func (o *tcpOverWSService) String() string { if o.isBastion { return ServiceBastion } if o.scheme != "" { return fmt.Sprintf("%s://%s", o.scheme, o.dest) } else { return o.dest } } func (o *tcpOverWSService) start(log *zerolog.Logger, _ <-chan struct{}, cfg OriginRequestConfig) error { if cfg.ProxyType == socksProxy { o.streamHandler = socks.StreamHandler } else { o.streamHandler = DefaultStreamHandler } o.dialer.Timeout = cfg.ConnectTimeout.Duration o.dialer.KeepAlive = cfg.TCPKeepAlive.Duration return nil } func (o tcpOverWSService) MarshalJSON() ([]byte, error) { return json.Marshal(o.String()) } func (o *socksProxyOverWSService) start(log *zerolog.Logger, _ <-chan struct{}, cfg OriginRequestConfig) error { return nil } func (o *socksProxyOverWSService) String() string { return ServiceSocksProxy } func (o socksProxyOverWSService) MarshalJSON() ([]byte, error) { return json.Marshal(o.String()) } // HelloWorld is an OriginService for the built-in Hello World server. // Users only use this for testing and experimenting with cloudflared. type helloWorld struct { httpService server net.Listener } func (o *helloWorld) String() string { return HelloWorldService } // Start starts a HelloWorld server and stores its address in the Service receiver. func (o *helloWorld) start( log *zerolog.Logger, shutdownC <-chan struct{}, cfg OriginRequestConfig, ) error { if err := o.httpService.start(log, shutdownC, cfg); err != nil { return err } helloListener, err := hello.CreateTLSListener("127.0.0.1:") if err != nil { return errors.Wrap(err, "Cannot start Hello World Server") } go hello.StartHelloWorldServer(log, helloListener, shutdownC) o.server = helloListener o.httpService.url = &url.URL{ Scheme: "https", Host: o.server.Addr().String(), } return nil } func (o helloWorld) MarshalJSON() ([]byte, error) { return json.Marshal(o.String()) } // statusCode is an OriginService that just responds with a given HTTP status. // Typical use-case is "user wants the catch-all rule to just respond 404". type statusCode struct { code int // Set only when the user has not defined any ingress rules defaultResp bool log *zerolog.Logger } func newStatusCode(status int) statusCode { return statusCode{code: status} } // default status code (503) that is returned for requests to cloudflared that don't have any ingress rules setup func newDefaultStatusCode(log *zerolog.Logger) statusCode { return statusCode{code: 503, defaultResp: true, log: log} } func (o *statusCode) String() string { return fmt.Sprintf("http_status:%d", o.code) } func (o *statusCode) start( log *zerolog.Logger, _ <-chan struct{}, cfg OriginRequestConfig, ) error { return nil } func (o statusCode) MarshalJSON() ([]byte, error) { return json.Marshal(o.String()) } // WarpRoutingService starts a tcp stream between the origin and requests from // warp clients. type WarpRoutingService struct { Proxy StreamBasedOriginProxy } func NewWarpRoutingService(config WarpRoutingConfig, writeTimeout time.Duration) *WarpRoutingService { svc := &rawTCPService{ name: ServiceWarpRouting, dialer: net.Dialer{ Timeout: config.ConnectTimeout.Duration, KeepAlive: config.TCPKeepAlive.Duration, }, writeTimeout: writeTimeout, } return &WarpRoutingService{Proxy: svc} } // ManagementService starts a local HTTP server to handle incoming management requests. type ManagementService struct { HTTPLocalProxy } func newManagementService(managementProxy HTTPLocalProxy) *ManagementService { return &ManagementService{ HTTPLocalProxy: managementProxy, } } func (o *ManagementService) start(log *zerolog.Logger, _ <-chan struct{}, cfg OriginRequestConfig) error { return nil } func (o *ManagementService) String() string { return "management" } func (o ManagementService) MarshalJSON() ([]byte, error) { return json.Marshal(o.String()) } func NewManagementRule(management *management.ManagementService) Rule { return Rule{ Hostname: management.Hostname, Service: newManagementService(management), } } type NopReadCloser struct{} // Read always returns EOF to signal end of input func (nrc *NopReadCloser) Read(buf []byte) (int, error) { return 0, io.EOF } func (nrc *NopReadCloser) Close() error { return nil } func newHTTPTransport(service OriginService, cfg OriginRequestConfig, log *zerolog.Logger) (*http.Transport, error) { originCertPool, err := tlsconfig.LoadOriginCA(cfg.CAPool, log) if err != nil { return nil, errors.Wrap(err, "Error loading cert pool") } httpTransport := http.Transport{ Proxy: http.ProxyFromEnvironment, MaxIdleConns: cfg.KeepAliveConnections, MaxIdleConnsPerHost: cfg.KeepAliveConnections, IdleConnTimeout: cfg.KeepAliveTimeout.Duration, TLSHandshakeTimeout: cfg.TLSTimeout.Duration, ExpectContinueTimeout: 1 * time.Second, TLSClientConfig: &tls.Config{RootCAs: originCertPool, InsecureSkipVerify: cfg.NoTLSVerify}, ForceAttemptHTTP2: cfg.Http2Origin, } if _, isHelloWorld := service.(*helloWorld); !isHelloWorld && cfg.OriginServerName != "" { httpTransport.TLSClientConfig.ServerName = cfg.OriginServerName } dialer := &net.Dialer{ Timeout: cfg.ConnectTimeout.Duration, KeepAlive: cfg.TCPKeepAlive.Duration, } if cfg.NoHappyEyeballs { dialer.FallbackDelay = -1 // As of Golang 1.12, a negative delay disables "happy eyeballs" } // DialContext depends on which kind of origin is being used. dialContext := dialer.DialContext switch service := service.(type) { // If this origin is a unix socket, enforce network type "unix". case *unixSocketPath: httpTransport.DialContext = func(ctx context.Context, _, _ string) (net.Conn, error) { return dialContext(ctx, "unix", service.path) } // Otherwise, use the regular network config. default: httpTransport.DialContext = dialContext } return &httpTransport, nil } // MockOriginHTTPService should only be used by other packages to mock OriginService. Set Transport to configure desired RoundTripper behavior. type MockOriginHTTPService struct { Transport http.RoundTripper } func (mos MockOriginHTTPService) RoundTrip(req *http.Request) (*http.Response, error) { return mos.Transport.RoundTrip(req) } func (mos MockOriginHTTPService) String() string { return "MockOriginService" } func (mos MockOriginHTTPService) start(log *zerolog.Logger, _ <-chan struct{}, cfg OriginRequestConfig) error { return nil } func (mos MockOriginHTTPService) MarshalJSON() ([]byte, error) { return json.Marshal(mos.String()) } ================================================ FILE: ingress/origin_service_test.go ================================================ package ingress import ( "net/url" "testing" "github.com/stretchr/testify/require" ) func TestAddPortIfMissing(t *testing.T) { testCases := []struct { input string expected string }{ {"ssh://[::1]", "[::1]:22"}, {"ssh://[::1]:38", "[::1]:38"}, {"ssh://abc:38", "abc:38"}, {"ssh://127.0.0.1:38", "127.0.0.1:38"}, {"ssh://127.0.0.1", "127.0.0.1:22"}, } for _, tc := range testCases { t.Run(tc.input, func(t *testing.T) { url1, _ := url.Parse(tc.input) addPortIfMissing(url1, 22) require.Equal(t, tc.expected, url1.Host) }) } } ================================================ FILE: ingress/origins/dns.go ================================================ package origins import ( "context" "crypto/rand" "math/big" "net" "net/netip" "slices" "sync" "time" "github.com/rs/zerolog" "github.com/cloudflare/cloudflared/ingress" ) const ( // We need a DNS record: // 1. That will be around for as long as cloudflared is // 2. That Cloudflare controls: to allow us to make changes if needed // 3. That is an external record to a typical customer's network: enforcing that the DNS request go to the // local DNS resolver over any local /etc/host configurations setup. // 4. That cloudflared would normally query: ensuring that users with a positive security model for DNS queries // don't need to adjust anything. // // This hostname is one that used during the edge discovery process and as such satisfies the above constraints. defaultLookupHost = "region1.v2.argotunnel.com" defaultResolverPort uint16 = 53 // We want the refresh time to be short to accommodate DNS resolver changes locally, but not too frequent as to // shuffle the resolver if multiple are configured. refreshFreq = 5 * time.Minute refreshTimeout = 5 * time.Second ) var ( // Virtual DNS service address VirtualDNSServiceAddr = netip.AddrPortFrom(netip.MustParseAddr("2606:4700:0cf1:2000:0000:0000:0000:0001"), 53) defaultResolverAddr = netip.AddrPortFrom(netip.MustParseAddr("127.0.0.1"), defaultResolverPort) ) type netDial func(network string, address string) (net.Conn, error) // DNSResolverService will make DNS requests to the local DNS resolver via the Dial method. type DNSResolverService struct { addresses []netip.AddrPort addressesM sync.RWMutex static bool dialer ingress.OriginDialer resolver peekResolver logger *zerolog.Logger metrics Metrics } func NewDNSResolverService(dialer ingress.OriginDialer, logger *zerolog.Logger, metrics Metrics) *DNSResolverService { return &DNSResolverService{ addresses: []netip.AddrPort{defaultResolverAddr}, dialer: dialer, resolver: &resolver{dialFunc: net.Dial}, logger: logger, metrics: metrics, } } func NewStaticDNSResolverService(resolverAddrs []netip.AddrPort, dialer ingress.OriginDialer, logger *zerolog.Logger, metrics Metrics) *DNSResolverService { s := NewDNSResolverService(dialer, logger, metrics) s.addresses = resolverAddrs s.static = true return s } func (s *DNSResolverService) DialTCP(ctx context.Context, _ netip.AddrPort) (net.Conn, error) { s.metrics.IncrementDNSTCPRequests() dest := s.getAddress() // The dialer ignores the provided address because the request will instead go to the local DNS resolver. return s.dialer.DialTCP(ctx, dest) } func (s *DNSResolverService) DialUDP(_ netip.AddrPort) (net.Conn, error) { s.metrics.IncrementDNSUDPRequests() dest := s.getAddress() // The dialer ignores the provided address because the request will instead go to the local DNS resolver. return s.dialer.DialUDP(dest) } // StartRefreshLoop is a routine that is expected to run in the background to update the DNS local resolver if // adjusted while the cloudflared process is running. // Does not run when the resolver was provided with external resolver addresses via CLI. func (s *DNSResolverService) StartRefreshLoop(ctx context.Context) { if s.static { s.logger.Debug().Msgf("Canceled DNS local resolver refresh loop because static resolver addresses were provided: %s", s.addresses) return } // Call update first to load an address before handling traffic err := s.update(ctx) if err != nil { s.logger.Err(err).Msg("Failed to initialize DNS local resolver") } for { select { case <-ctx.Done(): return case <-time.Tick(refreshFreq): err := s.update(ctx) if err != nil { s.logger.Err(err).Msg("Failed to refresh DNS local resolver") } } } } func (s *DNSResolverService) update(ctx context.Context) error { ctx, cancel := context.WithTimeout(ctx, refreshTimeout) defer cancel() // Make a standard DNS request to a well-known DNS record that will last a long time _, err := s.resolver.lookupNetIP(ctx, defaultLookupHost) if err != nil { return err } // Validate the address before updating internal reference _, address := s.resolver.addr() peekAddrPort, err := netip.ParseAddrPort(address) if err == nil { s.setAddress(peekAddrPort) return nil } // It's possible that the address didn't have an attached port, attempt to parse just the address and use // the default port 53 peekAddr, err := netip.ParseAddr(address) if err != nil { return err } s.setAddress(netip.AddrPortFrom(peekAddr, defaultResolverPort)) return nil } // returns the address from the peekResolver or from the static addresses if provided. // If multiple addresses are provided in the static addresses pick one randomly. func (s *DNSResolverService) getAddress() netip.AddrPort { s.addressesM.RLock() defer s.addressesM.RUnlock() l := len(s.addresses) if l <= 0 { return defaultResolverAddr } if l == 1 { return s.addresses[0] } // Only initialize the random selection if there is more than one element in the list. var i int64 = 0 r, err := rand.Int(rand.Reader, big.NewInt(int64(l))) // We ignore errors from crypto rand and use index 0; this should be extremely unlikely and the // list index doesn't need to be cryptographically secure, but linters insist. if err == nil { i = r.Int64() } return s.addresses[i] } // lock and update the address used for the local DNS resolver func (s *DNSResolverService) setAddress(addr netip.AddrPort) { s.addressesM.Lock() defer s.addressesM.Unlock() if !slices.Contains(s.addresses, addr) { s.logger.Debug().Msgf("Updating DNS local resolver: %s", addr) } // We only store one address when reading the peekResolver, so we just replace the whole list. s.addresses = []netip.AddrPort{addr} } type peekResolver interface { addr() (network string, address string) lookupNetIP(ctx context.Context, host string) ([]netip.Addr, error) } // resolver is a shim that inspects the go runtime's DNS resolution process to capture the DNS resolver // address used to complete a DNS request. type resolver struct { network string address string dialFunc netDial } func (r *resolver) addr() (network string, address string) { return r.network, r.address } func (r *resolver) lookupNetIP(ctx context.Context, host string) ([]netip.Addr, error) { resolver := &net.Resolver{ PreferGo: true, // Use the peekDial to inspect the results of the DNS resolver used during the LookupIPAddr call. Dial: r.peekDial, } return resolver.LookupNetIP(ctx, "ip", host) } func (r *resolver) peekDial(ctx context.Context, network, address string) (net.Conn, error) { r.network = network r.address = address return r.dialFunc(network, address) } // NewDNSDialer creates a custom dialer for the DNS resolver service to utilize. func NewDNSDialer() *ingress.Dialer { return &ingress.Dialer{ Dialer: net.Dialer{ // We want short timeouts for the DNS requests Timeout: 5 * time.Second, // We do not want keep alive since the edge will not reuse TCP connections per request KeepAlive: -1, KeepAliveConfig: net.KeepAliveConfig{ Enable: false, }, }, } } ================================================ FILE: ingress/origins/dns_test.go ================================================ package origins import ( "context" "errors" "net" "net/netip" "slices" "testing" "time" "github.com/rs/zerolog" ) func TestDNSResolver_DefaultResolver(t *testing.T) { log := zerolog.Nop() service := NewDNSResolverService(NewDNSDialer(), &log, &noopMetrics{}) mockResolver := &mockPeekResolver{ address: "127.0.0.2:53", } service.resolver = mockResolver validateAddrs(t, []netip.AddrPort{defaultResolverAddr}, service.addresses) } func TestStaticDNSResolver_DefaultResolver(t *testing.T) { log := zerolog.Nop() addresses := []netip.AddrPort{netip.MustParseAddrPort("1.1.1.1:53"), netip.MustParseAddrPort("1.0.0.1:53")} service := NewStaticDNSResolverService(addresses, NewDNSDialer(), &log, &noopMetrics{}) mockResolver := &mockPeekResolver{ address: "127.0.0.2:53", } service.resolver = mockResolver validateAddrs(t, addresses, service.addresses) } func TestDNSResolver_UpdateResolverAddress(t *testing.T) { log := zerolog.Nop() service := NewDNSResolverService(NewDNSDialer(), &log, &noopMetrics{}) mockResolver := &mockPeekResolver{} service.resolver = mockResolver tests := []struct { addr string expected netip.AddrPort }{ {"127.0.0.2:53", netip.MustParseAddrPort("127.0.0.2:53")}, // missing port should be added (even though this is unlikely to happen) {"127.0.0.3", netip.MustParseAddrPort("127.0.0.3:53")}, } for _, test := range tests { mockResolver.address = test.addr // Update the resolver address err := service.update(t.Context()) if err != nil { t.Error(err) } // Validate expected validateAddrs(t, []netip.AddrPort{test.expected}, service.addresses) } } func TestStaticDNSResolver_RefreshLoopExits(t *testing.T) { log := zerolog.Nop() addresses := []netip.AddrPort{netip.MustParseAddrPort("1.1.1.1:53"), netip.MustParseAddrPort("1.0.0.1:53")} service := NewStaticDNSResolverService(addresses, NewDNSDialer(), &log, &noopMetrics{}) mockResolver := &mockPeekResolver{ address: "127.0.0.2:53", } service.resolver = mockResolver ctx, cancel := context.WithCancel(t.Context()) defer cancel() go service.StartRefreshLoop(ctx) // Wait for the refresh loop to end _and_ not update the addresses time.Sleep(10 * time.Millisecond) // Validate expected validateAddrs(t, addresses, service.addresses) } func TestDNSResolver_UpdateResolverAddressInvalid(t *testing.T) { log := zerolog.Nop() service := NewDNSResolverService(NewDNSDialer(), &log, &noopMetrics{}) mockResolver := &mockPeekResolver{} service.resolver = mockResolver invalidAddresses := []string{ "999.999.999.999", "localhost", "255.255.255", } for _, addr := range invalidAddresses { mockResolver.address = addr // Update the resolver address should not update for these invalid addresses err := service.update(t.Context()) if err == nil { t.Error("service update should throw an error") } // Validate expected validateAddrs(t, []netip.AddrPort{defaultResolverAddr}, service.addresses) } } func TestDNSResolver_UpdateResolverErrorIgnored(t *testing.T) { log := zerolog.Nop() service := NewDNSResolverService(NewDNSDialer(), &log, &noopMetrics{}) resolverErr := errors.New("test resolver error") mockResolver := &mockPeekResolver{err: resolverErr} service.resolver = mockResolver // Update the resolver address should not update when the resolver cannot complete the lookup err := service.update(t.Context()) if err != resolverErr { t.Error("service update should throw an error") } // Validate expected validateAddrs(t, []netip.AddrPort{defaultResolverAddr}, service.addresses) } func TestDNSResolver_DialUDPUsesResolvedAddress(t *testing.T) { log := zerolog.Nop() mockDialer := &mockDialer{expected: defaultResolverAddr} service := NewDNSResolverService(mockDialer, &log, &noopMetrics{}) mockResolver := &mockPeekResolver{} service.resolver = mockResolver // Attempt a dial to 127.0.0.2:53 which should be ignored and instead resolve to 127.0.0.1:53 _, err := service.DialUDP(netip.MustParseAddrPort("127.0.0.2:53")) if err != nil { t.Error(err) } } func TestDNSResolver_DialTCPUsesResolvedAddress(t *testing.T) { log := zerolog.Nop() mockDialer := &mockDialer{expected: defaultResolverAddr} service := NewDNSResolverService(mockDialer, &log, &noopMetrics{}) mockResolver := &mockPeekResolver{} service.resolver = mockResolver // Attempt a dial to 127.0.0.2:53 which should be ignored and instead resolve to 127.0.0.1:53 _, err := service.DialTCP(t.Context(), netip.MustParseAddrPort("127.0.0.2:53")) if err != nil { t.Error(err) } } type mockPeekResolver struct { err error address string } func (r *mockPeekResolver) addr() (network, address string) { return "udp", r.address } func (r *mockPeekResolver) lookupNetIP(ctx context.Context, host string) ([]netip.Addr, error) { // We can return an empty result as it doesn't matter as long as the lookup doesn't fail return []netip.Addr{}, r.err } type mockDialer struct { expected netip.AddrPort } func (d *mockDialer) DialTCP(ctx context.Context, addr netip.AddrPort) (net.Conn, error) { if d.expected != addr { return nil, errors.New("unexpected address dialed") } return nil, nil } func (d *mockDialer) DialUDP(addr netip.AddrPort) (net.Conn, error) { if d.expected != addr { return nil, errors.New("unexpected address dialed") } return nil, nil } func validateAddrs(t *testing.T, expected []netip.AddrPort, actual []netip.AddrPort) { if len(actual) != len(expected) { t.Errorf("addresses should only contain one element: %s", actual) } for _, e := range expected { if !slices.Contains(actual, e) { t.Errorf("missing address: %s in %s", e, actual) } } } ================================================ FILE: ingress/origins/metrics.go ================================================ package origins import ( "github.com/prometheus/client_golang/prometheus" ) const ( namespace = "cloudflared" subsystem = "virtual_origins" ) type Metrics interface { IncrementDNSUDPRequests() IncrementDNSTCPRequests() } type metrics struct { dnsResolverRequests *prometheus.CounterVec } func (m *metrics) IncrementDNSUDPRequests() { m.dnsResolverRequests.WithLabelValues("udp").Inc() } func (m *metrics) IncrementDNSTCPRequests() { m.dnsResolverRequests.WithLabelValues("tcp").Inc() } func NewMetrics(registerer prometheus.Registerer) Metrics { m := &metrics{ dnsResolverRequests: prometheus.NewCounterVec(prometheus.CounterOpts{ Namespace: namespace, Subsystem: subsystem, Name: "dns_requests_total", Help: "Total count of DNS requests that have been proxied to the virtual DNS resolver origin", }, []string{"protocol"}), } registerer.MustRegister(m.dnsResolverRequests) return m } ================================================ FILE: ingress/origins/metrics_test.go ================================================ package origins type noopMetrics struct{} func (noopMetrics) IncrementDNSUDPRequests() {} func (noopMetrics) IncrementDNSTCPRequests() {} ================================================ FILE: ingress/packet_router.go ================================================ package ingress import ( "context" "fmt" "github.com/rs/zerolog" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" "github.com/cloudflare/cloudflared/packet" quicpogs "github.com/cloudflare/cloudflared/quic" "github.com/cloudflare/cloudflared/tracing" ) // Upstream of raw packets type muxer interface { SendPacket(pk quicpogs.Packet) error // ReceivePacket waits for the next raw packet from upstream ReceivePacket(ctx context.Context) (quicpogs.Packet, error) } // PacketRouter routes packets between Upstream and ICMPRouter. Currently it rejects all other type of ICMP packets type PacketRouter struct { icmpRouter ICMPRouter muxer muxer connIndex uint8 logger *zerolog.Logger encoder *packet.Encoder decoder *packet.ICMPDecoder } // NewPacketRouter creates a PacketRouter that handles ICMP packets. Packets are read from muxer but dropped if globalConfig is nil. func NewPacketRouter(icmpRouter ICMPRouter, muxer muxer, connIndex uint8, logger *zerolog.Logger) *PacketRouter { return &PacketRouter{ icmpRouter: icmpRouter, muxer: muxer, connIndex: connIndex, logger: logger, encoder: packet.NewEncoder(), decoder: packet.NewICMPDecoder(), } } func (r *PacketRouter) Serve(ctx context.Context) error { for { rawPacket, responder, err := r.nextPacket(ctx) if err != nil { return err } r.handlePacket(ctx, rawPacket, responder) } } func (r *PacketRouter) nextPacket(ctx context.Context) (packet.RawPacket, ICMPResponder, error) { pk, err := r.muxer.ReceivePacket(ctx) if err != nil { return packet.RawPacket{}, nil, err } responder := newPacketResponder(r.muxer, r.connIndex, packet.NewEncoder()) switch pk.Type() { case quicpogs.DatagramTypeIP: return packet.RawPacket{Data: pk.Payload()}, responder, nil case quicpogs.DatagramTypeIPWithTrace: var identity tracing.Identity if err := identity.UnmarshalBinary(pk.Metadata()); err != nil { r.logger.Err(err).Bytes("tracingIdentity", pk.Metadata()).Msg("Failed to unmarshal tracing identity") } else { tracedCtx := tracing.NewTracedContext(ctx, identity.String(), r.logger) responder.AddTraceContext(tracedCtx, pk.Metadata()) } return packet.RawPacket{Data: pk.Payload()}, responder, nil default: return packet.RawPacket{}, nil, fmt.Errorf("unexpected datagram type %d", pk.Type()) } } func (r *PacketRouter) handlePacket(ctx context.Context, rawPacket packet.RawPacket, responder ICMPResponder) { // ICMP Proxy feature is disabled, drop packets if r.icmpRouter == nil { return } icmpPacket, err := r.decoder.Decode(rawPacket) if err != nil { r.logger.Err(err).Msg("Failed to decode ICMP packet from quic datagram") return } if icmpPacket.TTL <= 1 { if err := r.sendTTLExceedMsg(icmpPacket, rawPacket); err != nil { r.logger.Err(err).Msg("Failed to return ICMP TTL exceed error") } return } icmpPacket.TTL-- if err := r.icmpRouter.Request(ctx, icmpPacket, responder); err != nil { r.logger.Err(err). Str("src", icmpPacket.Src.String()). Str("dst", icmpPacket.Dst.String()). Interface("type", icmpPacket.Type). Msg("Failed to send ICMP packet") } } func (r *PacketRouter) sendTTLExceedMsg(pk *packet.ICMP, rawPacket packet.RawPacket) error { icmpTTLPacket := r.icmpRouter.ConvertToTTLExceeded(pk, rawPacket) encodedTTLExceed, err := r.encoder.Encode(icmpTTLPacket) if err != nil { return err } return r.muxer.SendPacket(quicpogs.RawPacket(encodedTTLExceed)) } // packetResponder should not be used concurrently. This assumption is upheld because reply packets are ready one-by-one type packetResponder struct { datagramMuxer muxer connIndex uint8 encoder *packet.Encoder tracedCtx *tracing.TracedContext serializedIdentity []byte // hadReply tracks if there has been any reply for this flow hadReply bool } func newPacketResponder(datagramMuxer muxer, connIndex uint8, encoder *packet.Encoder) ICMPResponder { return &packetResponder{ datagramMuxer: datagramMuxer, connIndex: connIndex, encoder: encoder, } } func (pr *packetResponder) tracingEnabled() bool { return pr.tracedCtx != nil } func (pr *packetResponder) ConnectionIndex() uint8 { return pr.connIndex } func (pr *packetResponder) ReturnPacket(pk *packet.ICMP) error { rawPacket, err := pr.encoder.Encode(pk) if err != nil { return err } pr.hadReply = true return pr.datagramMuxer.SendPacket(quicpogs.RawPacket(rawPacket)) } func (pr *packetResponder) AddTraceContext(tracedCtx *tracing.TracedContext, serializedIdentity []byte) { pr.tracedCtx = tracedCtx pr.serializedIdentity = serializedIdentity } func (pr *packetResponder) RequestSpan(ctx context.Context, pk *packet.ICMP) (context.Context, trace.Span) { if !pr.tracingEnabled() { return ctx, tracing.NewNoopSpan() } return pr.tracedCtx.Tracer().Start(pr.tracedCtx, "icmp-echo-request", trace.WithAttributes( attribute.String("src", pk.Src.String()), attribute.String("dst", pk.Dst.String()), )) } func (pr *packetResponder) ReplySpan(ctx context.Context, logger *zerolog.Logger) (context.Context, trace.Span) { if !pr.tracingEnabled() || pr.hadReply { return ctx, tracing.NewNoopSpan() } return pr.tracedCtx.Tracer().Start(pr.tracedCtx, "icmp-echo-reply") } func (pr *packetResponder) ExportSpan() { if !pr.tracingEnabled() { return } spans := pr.tracedCtx.GetProtoSpans() if len(spans) > 0 { pr.datagramMuxer.SendPacket(&quicpogs.TracingSpanPacket{ Spans: spans, TracingIdentity: pr.serializedIdentity, }) } } ================================================ FILE: ingress/packet_router_test.go ================================================ package ingress import ( "bytes" "context" "fmt" "net/netip" "sync/atomic" "testing" "github.com/google/gopacket/layers" "github.com/stretchr/testify/require" "golang.org/x/net/icmp" "golang.org/x/net/ipv4" "golang.org/x/net/ipv6" "github.com/cloudflare/cloudflared/packet" quicpogs "github.com/cloudflare/cloudflared/quic" ) var ( defaultRouter = &icmpRouter{ ipv4Proxy: nil, ipv4Src: netip.MustParseAddr("172.16.0.1"), ipv6Proxy: nil, ipv6Src: netip.MustParseAddr("fd51:2391:523:f4ee::1"), } ) func TestRouterReturnTTLExceed(t *testing.T) { muxer := newMockMuxer(0) router := NewPacketRouter(defaultRouter, muxer, 0, &noopLogger) ctx, cancel := context.WithCancel(context.Background()) routerStopped := make(chan struct{}) go func() { router.Serve(ctx) close(routerStopped) }() pk := packet.ICMP{ IP: &packet.IP{ Src: netip.MustParseAddr("192.168.1.1"), Dst: netip.MustParseAddr("10.0.0.1"), Protocol: layers.IPProtocolICMPv4, TTL: 1, }, Message: &icmp.Message{ Type: ipv4.ICMPTypeEcho, Code: 0, Body: &icmp.Echo{ ID: 12481, Seq: 8036, Data: []byte("TTL exceed"), }, }, } assertTTLExceed(t, &pk, defaultRouter.ipv4Src, muxer) pk = packet.ICMP{ IP: &packet.IP{ Src: netip.MustParseAddr("fd51:2391:523:f4ee::1"), Dst: netip.MustParseAddr("fd51:2391:697:f4ee::2"), Protocol: layers.IPProtocolICMPv6, TTL: 1, }, Message: &icmp.Message{ Type: ipv6.ICMPTypeEchoRequest, Code: 0, Body: &icmp.Echo{ ID: 42583, Seq: 7039, Data: []byte("TTL exceed"), }, }, } assertTTLExceed(t, &pk, defaultRouter.ipv6Src, muxer) cancel() <-routerStopped } func assertTTLExceed(t *testing.T, originalPacket *packet.ICMP, expectedSrc netip.Addr, muxer *mockMuxer) { encoder := packet.NewEncoder() rawPacket, err := encoder.Encode(originalPacket) require.NoError(t, err) muxer.edgeToCfd <- quicpogs.RawPacket(rawPacket) resp := <-muxer.cfdToEdge decoder := packet.NewICMPDecoder() decoded, err := decoder.Decode(packet.RawPacket(resp.(quicpogs.RawPacket))) require.NoError(t, err) require.Equal(t, expectedSrc, decoded.Src) require.Equal(t, originalPacket.Src, decoded.Dst) require.Equal(t, originalPacket.Protocol, decoded.Protocol) require.Equal(t, packet.DefaultTTL, decoded.TTL) if originalPacket.Dst.Is4() { require.Equal(t, ipv4.ICMPTypeTimeExceeded, decoded.Type) } else { require.Equal(t, ipv6.ICMPTypeTimeExceeded, decoded.Type) } require.Equal(t, 0, decoded.Code) timeExceed, ok := decoded.Body.(*icmp.TimeExceeded) require.True(t, ok) require.True(t, bytes.Equal(rawPacket.Data, timeExceed.Data)) } type mockMuxer struct { cfdToEdge chan quicpogs.Packet edgeToCfd chan quicpogs.Packet } func newMockMuxer(capacity int) *mockMuxer { return &mockMuxer{ cfdToEdge: make(chan quicpogs.Packet, capacity), edgeToCfd: make(chan quicpogs.Packet, capacity), } } // Copy packet, because icmpProxy expects the encoder buffer to be reusable after the packet is sent func (mm *mockMuxer) SendPacket(pk quicpogs.Packet) error { payload := pk.Payload() copiedPayload := make([]byte, len(payload)) copy(copiedPayload, payload) metadata := pk.Metadata() copiedMetadata := make([]byte, len(metadata)) copy(copiedMetadata, metadata) var copiedPacket quicpogs.Packet switch pk.Type() { case quicpogs.DatagramTypeIP: copiedPacket = quicpogs.RawPacket(packet.RawPacket{ Data: copiedPayload, }) case quicpogs.DatagramTypeIPWithTrace: copiedPacket = &quicpogs.TracedPacket{ Packet: packet.RawPacket{ Data: copiedPayload, }, TracingIdentity: copiedMetadata, } case quicpogs.DatagramTypeTracingSpan: copiedPacket = &quicpogs.TracingSpanPacket{ Spans: copiedPayload, TracingIdentity: copiedMetadata, } default: return fmt.Errorf("unexpected metadata type %d", pk.Type()) } mm.cfdToEdge <- copiedPacket return nil } func (mm *mockMuxer) ReceivePacket(ctx context.Context) (quicpogs.Packet, error) { select { case <-ctx.Done(): return nil, ctx.Err() case pk := <-mm.edgeToCfd: return pk, nil } } type routerEnabledChecker struct { enabled uint32 } func (rec *routerEnabledChecker) isEnabled() bool { if atomic.LoadUint32(&rec.enabled) == 0 { return false } return true } func (rec *routerEnabledChecker) set(enabled bool) { if enabled { atomic.StoreUint32(&rec.enabled, 1) } else { atomic.StoreUint32(&rec.enabled, 0) } } ================================================ FILE: ingress/rule.go ================================================ package ingress import ( "encoding/json" "regexp" "strings" "github.com/cloudflare/cloudflared/ingress/middleware" ) // Rule routes traffic from a hostname/path on the public internet to the // service running on the given URL. type Rule struct { // Requests for this hostname will be proxied to this rule's service. Hostname string `json:"hostname"` // punycodeHostname is an additional optional hostname converted to punycode. punycodeHostname string // Path is an optional regex that can specify path-driven ingress rules. Path *Regexp `json:"path"` // A (probably local) address. Requests for a hostname which matches this // rule's hostname pattern will be proxied to the service running on this // address. Service OriginService `json:"service"` // Handlers is a list of functions that acts as a middleware during ProxyHTTP Handlers []middleware.Handler // Configure the request cloudflared sends to this specific origin. Config OriginRequestConfig `json:"originRequest"` } // MultiLineString is for outputting rules in a human-friendly way when Cloudflared // is used as a CLI tool (not as a daemon). func (r Rule) MultiLineString() string { var out strings.Builder if r.Hostname != "" { out.WriteString("\thostname: ") out.WriteString(r.Hostname) out.WriteRune('\n') } if r.Path != nil && r.Path.Regexp != nil { out.WriteString("\tpath: ") out.WriteString(r.Path.Regexp.String()) out.WriteRune('\n') } out.WriteString("\tservice: ") out.WriteString(r.Service.String()) return out.String() } // Matches checks if the rule matches a given hostname/path combination. func (r *Rule) Matches(hostname, path string) bool { hostMatch := false if r.Hostname == "" || r.Hostname == "*" { hostMatch = true } else { hostMatch = matchHost(r.Hostname, hostname) } punycodeHostMatch := false if r.punycodeHostname != "" { punycodeHostMatch = matchHost(r.punycodeHostname, hostname) } pathMatch := r.Path == nil || r.Path.Regexp == nil || r.Path.Regexp.MatchString(path) return (hostMatch || punycodeHostMatch) && pathMatch } // Regexp adds unmarshalling from json for regexp.Regexp type Regexp struct { *regexp.Regexp } func (r *Regexp) MarshalJSON() ([]byte, error) { if r.Regexp == nil { return json.Marshal(nil) } return json.Marshal(r.Regexp.String()) } ================================================ FILE: ingress/rule_test.go ================================================ package ingress import ( "encoding/json" "io" "net/http/httptest" "net/url" "regexp" "testing" "github.com/stretchr/testify/require" "github.com/cloudflare/cloudflared/config" ) func Test_rule_matches(t *testing.T) { type args struct { requestURL *url.URL } tests := []struct { name string rule Rule args args want bool }{ { name: "Just hostname, pass", rule: Rule{ Hostname: "example.com", }, args: args{ requestURL: MustParseURL(t, "https://example.com"), }, want: true, }, { name: "Unicode hostname with unicode request, pass", rule: Rule{ Hostname: "môô.cloudflare.com", punycodeHostname: "xn--m-xgaa.cloudflare.com", }, args: args{ requestURL: MustParseURL(t, "https://môô.cloudflare.com"), }, want: true, }, { name: "Unicode hostname with punycode request, pass", rule: Rule{ Hostname: "môô.cloudflare.com", punycodeHostname: "xn--m-xgaa.cloudflare.com", }, args: args{ requestURL: MustParseURL(t, "https://xn--m-xgaa.cloudflare.com"), }, want: true, }, { name: "Entire hostname is wildcard, should match everything", rule: Rule{ Hostname: "*", }, args: args{ requestURL: MustParseURL(t, "https://example.com"), }, want: true, }, { name: "Just hostname, fail", rule: Rule{ Hostname: "example.com", }, args: args{ requestURL: MustParseURL(t, "https://foo.bar"), }, want: false, }, { name: "Just wildcard hostname, pass", rule: Rule{ Hostname: "*.example.com", }, args: args{ requestURL: MustParseURL(t, "https://adam.example.com"), }, want: true, }, { name: "Just wildcard hostname, fail", rule: Rule{ Hostname: "*.example.com", }, args: args{ requestURL: MustParseURL(t, "https://tunnel.com"), }, want: false, }, { name: "Just wildcard outside of subdomain in hostname, fail", rule: Rule{ Hostname: "*example.com", }, args: args{ requestURL: MustParseURL(t, "https://www.example.com"), }, want: false, }, { name: "Wildcard over multiple subdomains", rule: Rule{ Hostname: "*.example.com", }, args: args{ requestURL: MustParseURL(t, "https://adam.chalmers.example.com"), }, want: true, }, { name: "Hostname and path", rule: Rule{ Hostname: "*.example.com", Path: &Regexp{Regexp: regexp.MustCompile("/static/.*\\.html")}, }, args: args{ requestURL: MustParseURL(t, "https://www.example.com/static/index.html"), }, want: true, }, { name: "Hostname and empty Regex", rule: Rule{ Hostname: "example.com", Path: &Regexp{}, }, args: args{ requestURL: MustParseURL(t, "https://example.com/"), }, want: true, }, { name: "Hostname and nil path", rule: Rule{ Hostname: "example.com", Path: nil, }, args: args{ requestURL: MustParseURL(t, "https://example.com/"), }, want: true, }, { name: "Hostname with wildcard should not match if no dot present", rule: Rule{ Hostname: "*.api.abc.cloud", }, args: args{ requestURL: MustParseURL(t, "https://testing-api.abc.cloud"), }, want: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { u := tt.args.requestURL if got := tt.rule.Matches(u.Hostname(), u.Path); got != tt.want { t.Errorf("rule.matches() = %v, want %v", got, tt.want) } }) } } func TestStaticHTTPStatus(t *testing.T) { o := newStatusCode(404) buf := make([]byte, 100) sendReq := func() { resp, err := o.RoundTrip(nil) require.NoError(t, err) _, err = resp.Body.Read(buf) require.Equal(t, io.EOF, err) require.NoError(t, resp.Body.Close()) require.Equal(t, 404, resp.StatusCode) resp, err = o.RoundTrip(nil) require.NoError(t, err) w := httptest.NewRecorder() n, err := io.Copy(w, resp.Body) require.NoError(t, err) require.Equal(t, int64(0), n) } sendReq() sendReq() } func TestMarshalJSON(t *testing.T) { localhost8000 := MustParseURL(t, "https://localhost:8000") defaultConfig := setConfig(originRequestFromConfig(config.OriginRequestConfig{}), config.OriginRequestConfig{}) tests := []struct { name string path *Regexp expected string want bool }{ { name: "Nil", path: nil, expected: `{"hostname":"example.com","path":null,"service":"https://localhost:8000","Handlers":null,"originRequest":{"connectTimeout":30,"tlsTimeout":10,"tcpKeepAlive":30,"noHappyEyeballs":false,"keepAliveTimeout":90,"keepAliveConnections":100,"httpHostHeader":"","originServerName":"","matchSNItoHost":false,"caPool":"","noTLSVerify":false,"disableChunkedEncoding":false,"bastionMode":false,"proxyAddress":"127.0.0.1","proxyPort":0,"proxyType":"","ipRules":null,"http2Origin":false,"access":{"teamName":"","audTag":null}}}`, want: true, }, { name: "Nil regex", path: &Regexp{Regexp: nil}, expected: `{"hostname":"example.com","path":null,"service":"https://localhost:8000","Handlers":null,"originRequest":{"connectTimeout":30,"tlsTimeout":10,"tcpKeepAlive":30,"noHappyEyeballs":false,"keepAliveTimeout":90,"keepAliveConnections":100,"httpHostHeader":"","originServerName":"","matchSNItoHost":false,"caPool":"","noTLSVerify":false,"disableChunkedEncoding":false,"bastionMode":false,"proxyAddress":"127.0.0.1","proxyPort":0,"proxyType":"","ipRules":null,"http2Origin":false,"access":{"teamName":"","audTag":null}}}`, want: true, }, { name: "Empty", path: &Regexp{Regexp: regexp.MustCompile("")}, expected: `{"hostname":"example.com","path":"","service":"https://localhost:8000","Handlers":null,"originRequest":{"connectTimeout":30,"tlsTimeout":10,"tcpKeepAlive":30,"noHappyEyeballs":false,"keepAliveTimeout":90,"keepAliveConnections":100,"httpHostHeader":"","originServerName":"","matchSNItoHost":false,"caPool":"","noTLSVerify":false,"disableChunkedEncoding":false,"bastionMode":false,"proxyAddress":"127.0.0.1","proxyPort":0,"proxyType":"","ipRules":null,"http2Origin":false,"access":{"teamName":"","audTag":null}}}`, want: true, }, { name: "Basic", path: &Regexp{Regexp: regexp.MustCompile("/echo")}, expected: `{"hostname":"example.com","path":"/echo","service":"https://localhost:8000","Handlers":null,"originRequest":{"connectTimeout":30,"tlsTimeout":10,"tcpKeepAlive":30,"noHappyEyeballs":false,"keepAliveTimeout":90,"keepAliveConnections":100,"httpHostHeader":"","originServerName":"","matchSNItoHost":false,"caPool":"","noTLSVerify":false,"disableChunkedEncoding":false,"bastionMode":false,"proxyAddress":"127.0.0.1","proxyPort":0,"proxyType":"","ipRules":null,"http2Origin":false,"access":{"teamName":"","audTag":null}}}`, want: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := Rule{ Hostname: "example.com", Service: &httpService{url: localhost8000}, Path: tt.path, Config: defaultConfig, } bytes, err := json.Marshal(r) require.NoError(t, err) require.Equal(t, tt.expected, string(bytes)) }) } } ================================================ FILE: internal/test/wstest.go ================================================ package test // copied from https://github.com/nhooyr/websocket/blob/master/internal/test/wstest/pipe.go import ( "bufio" "context" "net" "net/http" "net/http/httptest" "nhooyr.io/websocket" ) // Pipe is used to create an in memory connection // between two websockets analogous to net.Pipe. func WSPipe(dialOpts *websocket.DialOptions, acceptOpts *websocket.AcceptOptions) (clientConn, serverConn *websocket.Conn) { tt := fakeTransport{ h: func(w http.ResponseWriter, r *http.Request) { serverConn, _ = websocket.Accept(w, r, acceptOpts) }, } if dialOpts == nil { dialOpts = &websocket.DialOptions{} } dialOpts = &*dialOpts dialOpts.HTTPClient = &http.Client{ Transport: tt, } clientConn, _, _ = websocket.Dial(context.Background(), "ws://example.com", dialOpts) return clientConn, serverConn } type fakeTransport struct { h http.HandlerFunc } func (t fakeTransport) RoundTrip(r *http.Request) (*http.Response, error) { clientConn, serverConn := net.Pipe() hj := testHijacker{ ResponseRecorder: httptest.NewRecorder(), serverConn: serverConn, } t.h.ServeHTTP(hj, r) resp := hj.ResponseRecorder.Result() if resp.StatusCode == http.StatusSwitchingProtocols { resp.Body = clientConn } return resp, nil } type testHijacker struct { *httptest.ResponseRecorder serverConn net.Conn } var _ http.Hijacker = testHijacker{} func (hj testHijacker) Hijack() (net.Conn, *bufio.ReadWriter, error) { return hj.serverConn, bufio.NewReadWriter(bufio.NewReader(hj.serverConn), bufio.NewWriter(hj.serverConn)), nil } ================================================ FILE: ipaccess/access.go ================================================ package ipaccess import ( "fmt" "net" "sort" ) type Policy struct { defaultAllow bool rules []Rule } type Rule struct { ipNet *net.IPNet ports []int allow bool } func NewPolicy(defaultAllow bool, rules []Rule) (*Policy, error) { for _, rule := range rules { if err := rule.Validate(); err != nil { return nil, err } } policy := Policy{ defaultAllow: defaultAllow, rules: rules, } return &policy, nil } func NewRuleByCIDR(prefix *string, ports []int, allow bool) (Rule, error) { if prefix == nil || len(*prefix) == 0 { return Rule{}, fmt.Errorf("no prefix provided") } _, ipnet, err := net.ParseCIDR(*prefix) if err != nil { return Rule{}, fmt.Errorf("unable to parse cidr: %s", *prefix) } return NewRule(ipnet, ports, allow) } func NewRule(ipnet *net.IPNet, ports []int, allow bool) (Rule, error) { rule := Rule{ ipNet: ipnet, ports: ports, allow: allow, } return rule, rule.Validate() } func (r *Rule) Validate() error { if r.ipNet == nil { return fmt.Errorf("no ipnet set on the rule") } if len(r.ports) > 0 { sort.Ints(r.ports) for _, port := range r.ports { if port < 1 || port > 65535 { return fmt.Errorf("invalid port %d, needs to be between 1 and 65535", port) } } } return nil } func (h *Policy) Allowed(ip net.IP, port int) (bool, *Rule) { if len(h.rules) == 0 { return h.defaultAllow, nil } for _, rule := range h.rules { if rule.ipNet.Contains(ip) { if len(rule.ports) == 0 { return rule.allow, &rule } else if pos := sort.SearchInts(rule.ports, port); pos < len(rule.ports) && rule.ports[pos] == port { return rule.allow, &rule } } } return h.defaultAllow, nil } func (ipr *Rule) String() string { return fmt.Sprintf("prefix:%s/port:%s/allow:%t", ipr.ipNet, ipr.PortsString(), ipr.allow) } func (ipr *Rule) PortsString() string { if len(ipr.ports) > 0 { return fmt.Sprint(ipr.ports) } return "all" } func (ipr *Rule) Ports() []int { return ipr.ports } func (ipr *Rule) RulePolicy() bool { return ipr.allow } func (ipr *Rule) StringCIDR() string { return ipr.ipNet.String() } ================================================ FILE: ipaccess/access_test.go ================================================ package ipaccess import ( "bytes" "net" "testing" "github.com/stretchr/testify/assert" ) func TestRuleCreation(t *testing.T) { _, ipnet, _ := net.ParseCIDR("1.1.1.1/24") _, err := NewRule(nil, []int{80}, false) assert.Error(t, err, "expected error as no ipnet provided") _, err = NewRule(ipnet, []int{65536, 80}, false) assert.Error(t, err, "expected error as port higher than 65535") _, err = NewRule(ipnet, []int{80, -1}, false) assert.Error(t, err, "expected error as port less than 0") rule, err := NewRule(ipnet, []int{443, 80}, false) assert.NoError(t, err) assert.True(t, ipnet.IP.Equal(rule.ipNet.IP) && bytes.Compare(ipnet.Mask, rule.ipNet.Mask) == 0, "ipnet expected to be %+v, got: %+v", ipnet, rule.ipNet) assert.True(t, len(rule.ports) == 2 && rule.ports[0] == 80 && rule.ports[1] == 443, "expected ports to be sorted") } func TestRuleCreationByCIDR(t *testing.T) { var cidr *string _, err := NewRuleByCIDR(cidr, []int{80}, false) assert.Error(t, err, "expected error as cidr is nil") badCidr := "1.1.1.1" cidr = &badCidr _, err = NewRuleByCIDR(cidr, []int{80}, false) assert.Error(t, err, "expected error as the cidr is bad") goodCidr := "1.1.1.1/24" _, ipnet, _ := net.ParseCIDR("1.1.1.0/24") cidr = &goodCidr rule, err := NewRuleByCIDR(cidr, []int{80}, false) assert.NoError(t, err) assert.True(t, ipnet.IP.Equal(rule.ipNet.IP) && bytes.Compare(ipnet.Mask, rule.ipNet.Mask) == 0, "ipnet expected to be %+v, got: %+v", ipnet, rule.ipNet) } func TestRulesNoRules(t *testing.T) { ip, _, _ := net.ParseCIDR("1.2.3.4/24") policy, _ := NewPolicy(true, []Rule{}) allowed, rule := policy.Allowed(ip, 80) assert.True(t, allowed, "expected to be allowed as no rules and default allow") assert.Nil(t, rule, "expected to be nil as no rules") policy, _ = NewPolicy(false, []Rule{}) allowed, rule = policy.Allowed(ip, 80) assert.False(t, allowed, "expected to be denied as no rules and default deny") assert.Nil(t, rule, "expected to be nil as no rules") } func TestRulesMatchIPAndPort(t *testing.T) { ip1, ipnet1, _ := net.ParseCIDR("1.2.3.4/24") ip2, _, _ := net.ParseCIDR("2.3.4.5/24") rule1, _ := NewRule(ipnet1, []int{80, 443}, true) rules := []Rule{ rule1, } policy, _ := NewPolicy(false, rules) allowed, rule := policy.Allowed(ip1, 80) assert.True(t, allowed, "expected to be allowed as matching rule") assert.True(t, rule.ipNet == ipnet1, "expected to match ipnet1") allowed, rule = policy.Allowed(ip2, 80) assert.False(t, allowed, "expected to be denied as no matching rule") assert.Nil(t, rule, "expected to be nil") } func TestRulesMatchIPAndPort2(t *testing.T) { ip1, ipnet1, _ := net.ParseCIDR("1.2.3.4/24") ip2, ipnet2, _ := net.ParseCIDR("2.3.4.5/24") rule1, _ := NewRule(ipnet1, []int{53, 80}, false) rule2, _ := NewRule(ipnet2, []int{53, 80}, true) rules := []Rule{ rule1, rule2, } policy, _ := NewPolicy(false, rules) allowed, rule := policy.Allowed(ip1, 80) assert.False(t, allowed, "expected to be denied as matching rule") assert.True(t, rule.ipNet == ipnet1, "expected to match ipnet1") allowed, rule = policy.Allowed(ip2, 80) assert.True(t, allowed, "expected to be allowed as matching rule") assert.True(t, rule.ipNet == ipnet2, "expected to match ipnet1") allowed, rule = policy.Allowed(ip2, 81) assert.False(t, allowed, "expected to be denied as no matching rule") assert.Nil(t, rule, "expected to be nil") } ================================================ FILE: logger/configuration.go ================================================ package logger import ( "path/filepath" ) var defaultConfig = createDefaultConfig() // Logging configuration type Config struct { ConsoleConfig *ConsoleConfig // If nil, the logger will not log into the console FileConfig *FileConfig // If nil, the logger will not use an individual log file RollingConfig *RollingConfig // If nil, the logger will not use a rolling log MinLevel string // debug | info | error | fatal } type ConsoleConfig struct { noColor bool asJSON bool } type FileConfig struct { Dirname string Filename string } func (fc *FileConfig) Fullpath() string { return filepath.Join(fc.Dirname, fc.Filename) } type RollingConfig struct { Dirname string Filename string maxSize int // megabytes maxBackups int // files maxAge int // days } func createDefaultConfig() Config { const minLevel = "info" const RollingMaxSize = 1 // Mb const RollingMaxBackups = 5 // files const RollingMaxAge = 0 // Keep forever const defaultLogFilename = "cloudflared.log" return Config{ ConsoleConfig: &ConsoleConfig{ noColor: false, asJSON: false, }, FileConfig: &FileConfig{ Dirname: "", Filename: defaultLogFilename, }, RollingConfig: &RollingConfig{ Dirname: "", Filename: defaultLogFilename, maxSize: RollingMaxSize, maxBackups: RollingMaxBackups, maxAge: RollingMaxAge, }, MinLevel: minLevel, } } func CreateConfig( minLevel string, disableTerminal bool, formatJSON bool, rollingLogPath, nonRollingLogFilePath string, ) *Config { var console *ConsoleConfig if !disableTerminal { console = createConsoleConfig(formatJSON) } var file *FileConfig var rolling *RollingConfig if nonRollingLogFilePath != "" { file = createFileConfig(nonRollingLogFilePath) } else if rollingLogPath != "" { rolling = createRollingConfig(rollingLogPath) } if minLevel == "" { minLevel = defaultConfig.MinLevel } return &Config{ ConsoleConfig: console, FileConfig: file, RollingConfig: rolling, MinLevel: minLevel, } } func createConsoleConfig(formatJSON bool) *ConsoleConfig { return &ConsoleConfig{ noColor: false, asJSON: formatJSON, } } func createFileConfig(fullpath string) *FileConfig { if fullpath == "" { return defaultConfig.FileConfig } dirname, filename := filepath.Split(fullpath) return &FileConfig{ Dirname: dirname, Filename: filename, } } func createRollingConfig(directory string) *RollingConfig { if directory == "" { directory = defaultConfig.RollingConfig.Dirname } return &RollingConfig{ Dirname: directory, Filename: defaultConfig.RollingConfig.Filename, maxSize: defaultConfig.RollingConfig.maxSize, maxBackups: defaultConfig.RollingConfig.maxBackups, maxAge: defaultConfig.RollingConfig.maxAge, } } ================================================ FILE: logger/console.go ================================================ package logger import ( "bytes" "fmt" "io" jsoniter "github.com/json-iterator/go" ) var json = jsoniter.ConfigCompatibleWithStandardLibrary // consoleWriter allows us the simplicity to prevent duplicate json keys in the logger events reported. // // By default zerolog constructs the json event in parts by appending each additional key after the first. It // doesn't have any internal state or struct of the json message representation so duplicate keys can be // inserted without notice and no pruning will occur before writing the log event out to the io.Writer. // // To help prevent these duplicate keys, we will decode the json log event and then immediately re-encode it // again as writing it to the output io.Writer. Since we encode it to a map[string]any, duplicate keys // are pruned. We pay the cost of decoding and encoding the log event for each time, but helps prevent // us from needing to worry about adding duplicate keys in the log event from different areas of code. type consoleWriter struct { out io.Writer } func (c *consoleWriter) Write(p []byte) (n int, err error) { var evt map[string]any d := json.NewDecoder(bytes.NewReader(p)) d.UseNumber() err = d.Decode(&evt) if err != nil { return n, fmt.Errorf("cannot decode event: %s", err) } e := json.NewEncoder(c.out) return len(p), e.Encode(evt) } ================================================ FILE: logger/console_test.go ================================================ package logger import ( "bytes" "strings" "testing" "github.com/rs/zerolog" ) func TestConsoleLoggerDuplicateKeys(t *testing.T) { r := bytes.NewBuffer(make([]byte, 500)) logger := zerolog.New(&consoleWriter{out: r}).With().Timestamp().Logger() logger.Debug().Str("test", "1234").Int("number", 45).Str("test", "5678").Msg("log message") event, err := r.ReadString('\n') if err != nil { t.Error(err) } if !strings.Contains(event, "\"test\":\"5678\"") { t.Errorf("log event missing key 'test': %s", event) } if !strings.Contains(event, "\"number\":45") { t.Errorf("log event missing key 'number': %s", event) } if !strings.Contains(event, "\"time\":") { t.Errorf("log event missing key 'time': %s", event) } if !strings.Contains(event, "\"level\":\"debug\"") { t.Errorf("log event missing key 'level': %s", event) } } ================================================ FILE: logger/create.go ================================================ package logger import ( "fmt" "io" "os" "path/filepath" "sync" "time" "github.com/mattn/go-colorable" "github.com/rs/zerolog" fallbacklog "github.com/rs/zerolog/log" "github.com/urfave/cli/v2" "golang.org/x/term" "gopkg.in/natefinch/lumberjack.v2" cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags" "github.com/cloudflare/cloudflared/management" ) const ( EnableTerminalLog = false DisableTerminalLog = true dirPermMode = 0744 // rwxr--r-- filePermMode = 0644 // rw-r--r-- consoleTimeFormat = time.RFC3339 ) var ( ManagementLogger *management.Logger ) func init() { zerolog.TimeFieldFormat = time.RFC3339 zerolog.TimestampFunc = utcNow ManagementLogger = management.NewLogger() } func utcNow() time.Time { return time.Now().UTC() } func fallbackLogger(err error) *zerolog.Logger { failLog := fallbacklog.With().Logger() fallbacklog.Error().Msgf("Falling back to a default logger due to logger setup failure: %s", err) return &failLog } // resilientMultiWriter is an alternative to zerolog's so that we can make it resilient to individual // writer's errors. E.g., when running as a Windows service, the console writer fails, but we don't want to // allow that to prevent all logging to fail due to breaking the for loop upon an error. type resilientMultiWriter struct { level zerolog.Level writers []io.Writer managementWriter zerolog.LevelWriter } func (t resilientMultiWriter) Write(p []byte) (n int, err error) { for _, w := range t.writers { _, _ = w.Write(p) } if t.managementWriter != nil { _, _ = t.managementWriter.Write(p) } return len(p), nil } func (t resilientMultiWriter) WriteLevel(level zerolog.Level, p []byte) (n int, err error) { // Only write the event to normal writers if it exceeds the level, but always write to the // management logger and let it decided with the provided level of the log event. if t.level <= level { for _, w := range t.writers { _, _ = w.Write(p) } } if t.managementWriter != nil { _, _ = t.managementWriter.WriteLevel(level, p) } return len(p), nil } var levelErrorLogged = false func newZerolog(loggerConfig *Config) *zerolog.Logger { var writers []io.Writer if loggerConfig.ConsoleConfig != nil { writers = append(writers, createConsoleLogger(*loggerConfig.ConsoleConfig)) } if loggerConfig.FileConfig != nil { fileLogger, err := createFileWriter(*loggerConfig.FileConfig) if err != nil { return fallbackLogger(err) } writers = append(writers, fileLogger) } if loggerConfig.RollingConfig != nil { rollingLogger, err := createRollingLogger(*loggerConfig.RollingConfig) if err != nil { return fallbackLogger(err) } writers = append(writers, rollingLogger) } managementWriter := ManagementLogger level, levelErr := zerolog.ParseLevel(loggerConfig.MinLevel) if levelErr != nil { level = zerolog.InfoLevel } multi := resilientMultiWriter{level, writers, managementWriter} log := zerolog.New(multi).With().Timestamp().Logger() if !levelErrorLogged && levelErr != nil { log.Error().Msgf("Failed to parse log level %q, using %q instead", loggerConfig.MinLevel, level) levelErrorLogged = true } return &log } func CreateTransportLoggerFromContext(c *cli.Context, disableTerminal bool) *zerolog.Logger { return createFromContext(c, cfdflags.TransportLogLevel, cfdflags.LogDirectory, disableTerminal) } func CreateLoggerFromContext(c *cli.Context, disableTerminal bool) *zerolog.Logger { return createFromContext(c, cfdflags.LogLevel, cfdflags.LogDirectory, disableTerminal) } func CreateSSHLoggerFromContext(c *cli.Context, disableTerminal bool) *zerolog.Logger { return createFromContext(c, cfdflags.LogLevelSSH, cfdflags.LogDirectory, disableTerminal) } func createFromContext( c *cli.Context, logLevelFlagName, logDirectoryFlagName string, disableTerminal bool, ) *zerolog.Logger { logLevel := c.String(logLevelFlagName) logFile := c.String(cfdflags.LogFile) logDirectory := c.String(logDirectoryFlagName) var logFormatJSON bool switch c.String(cfdflags.LogFormatOutput) { case cfdflags.LogFormatOutputValueJSON: logFormatJSON = true case cfdflags.LogFormatOutputValueDefault: // "default" and unset use the same logger output format fallthrough default: logFormatJSON = false } loggerConfig := CreateConfig( logLevel, disableTerminal, logFormatJSON, logDirectory, logFile, ) log := newZerolog(loggerConfig) if incompatibleFlagsSet := logFile != "" && logDirectory != ""; incompatibleFlagsSet { log.Error().Msgf("Your config includes values for both %s (%s) and %s (%s), but they are incompatible. %s takes precedence.", cfdflags.LogFile, logFile, logDirectoryFlagName, logDirectory, cfdflags.LogFile) } return log } func Create(loggerConfig *Config) *zerolog.Logger { if loggerConfig == nil { loggerConfig = &Config{ defaultConfig.ConsoleConfig, nil, nil, defaultConfig.MinLevel, } } return newZerolog(loggerConfig) } func createConsoleLogger(config ConsoleConfig) io.Writer { if config.asJSON { return &consoleWriter{out: os.Stderr} } consoleOut := os.Stderr return zerolog.ConsoleWriter{ Out: colorable.NewColorable(consoleOut), NoColor: config.noColor || !term.IsTerminal(int(consoleOut.Fd())), TimeFormat: consoleTimeFormat, } } type fileInitializer struct { once sync.Once writer io.Writer creationError error } var ( singleFileInit fileInitializer rotatingFileInit fileInitializer ) func createFileWriter(config FileConfig) (io.Writer, error) { singleFileInit.once.Do(func() { var logFile io.Writer fullpath := config.Fullpath() // Try to open the existing file logFile, err := os.OpenFile(fullpath, os.O_APPEND|os.O_WRONLY, filePermMode) if err != nil { // If the existing file wasn't found, or couldn't be opened, just ignore // it and recreate a new one. logFile, err = createDirFile(config) // If creating a new logfile fails, then we have no choice but to error out. if err != nil { singleFileInit.creationError = err return } } singleFileInit.writer = logFile }) return singleFileInit.writer, singleFileInit.creationError } func createDirFile(config FileConfig) (io.Writer, error) { if config.Dirname != "" { err := os.MkdirAll(config.Dirname, dirPermMode) if err != nil { return nil, fmt.Errorf("unable to create directories for new logfile: %s", err) } } mode := os.FileMode(filePermMode) fullPath := filepath.Join(config.Dirname, config.Filename) logFile, err := os.OpenFile(fullPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, mode) if err != nil { return nil, fmt.Errorf("unable to create a new logfile: %s", err) } return logFile, nil } func createRollingLogger(config RollingConfig) (io.Writer, error) { rotatingFileInit.once.Do(func() { if err := os.MkdirAll(config.Dirname, dirPermMode); err != nil { rotatingFileInit.creationError = err return } rotatingFileInit.writer = &lumberjack.Logger{ Filename: filepath.Join(config.Dirname, config.Filename), MaxBackups: config.maxBackups, MaxSize: config.maxSize, MaxAge: config.maxAge, } }) return rotatingFileInit.writer, rotatingFileInit.creationError } ================================================ FILE: logger/create_test.go ================================================ package logger import ( "io" "testing" "github.com/pkg/errors" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" ) type mockedWriter struct { wantErr bool writeCalls int } func (c *mockedWriter) Write(p []byte) (int, error) { c.writeCalls++ if c.wantErr { return -1, errors.New("Expected error") } return len(p), nil } // Tests that a new writer is only used if it actually works. func TestResilientMultiWriter_Errors(t *testing.T) { tests := []struct { name string writers []*mockedWriter }{ { name: "All valid writers", writers: []*mockedWriter{ { wantErr: false, }, { wantErr: false, }, }, }, { name: "All invalid writers", writers: []*mockedWriter{ { wantErr: true, }, { wantErr: true, }, }, }, { name: "First invalid writer", writers: []*mockedWriter{ { wantErr: true, }, { wantErr: false, }, }, }, { name: "First valid writer", writers: []*mockedWriter{ { wantErr: false, }, { wantErr: true, }, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { writers := []io.Writer{} for _, w := range test.writers { writers = append(writers, w) } multiWriter := resilientMultiWriter{zerolog.InfoLevel, writers, nil} logger := zerolog.New(multiWriter).With().Timestamp().Logger() logger.Info().Msg("Test msg") for _, w := range test.writers { // Expect each writer to be written to regardless of the previous writers returning an error assert.Equal(t, 1, w.writeCalls) } }) } } type mockedManagementWriter struct { WriteCalls int } func (c *mockedManagementWriter) Write(p []byte) (int, error) { return len(p), nil } func (c *mockedManagementWriter) WriteLevel(level zerolog.Level, p []byte) (int, error) { c.WriteCalls++ return len(p), nil } // Tests that management writer receives write calls of all levels except Disabled func TestResilientMultiWriter_Management(t *testing.T) { for _, level := range []zerolog.Level{ zerolog.DebugLevel, zerolog.InfoLevel, zerolog.WarnLevel, zerolog.ErrorLevel, zerolog.FatalLevel, zerolog.PanicLevel, } { t.Run(level.String(), func(t *testing.T) { managementWriter := mockedManagementWriter{} multiWriter := resilientMultiWriter{level, []io.Writer{&mockedWriter{}}, &managementWriter} logger := zerolog.New(multiWriter).With().Timestamp().Logger() logger.Info().Msg("Test msg") // Always write to management assert.Equal(t, 1, managementWriter.WriteCalls) }) } } ================================================ FILE: management/events.go ================================================ package management import ( "context" "errors" "fmt" "io" jsoniter "github.com/json-iterator/go" "github.com/rs/zerolog" "nhooyr.io/websocket" ) var ( errInvalidMessageType = fmt.Errorf("invalid message type was provided") ) // ServerEventType represents the event types that can come from the server type ServerEventType string // ClientEventType represents the event types that can come from the client type ClientEventType string const ( UnknownClientEventType ClientEventType = "" StartStreaming ClientEventType = "start_streaming" StopStreaming ClientEventType = "stop_streaming" UnknownServerEventType ServerEventType = "" Logs ServerEventType = "logs" ) // ServerEvent is the base struct that informs, based of the Type field, which Event type was provided from the server. type ServerEvent struct { Type ServerEventType `json:"type,omitempty"` // The raw json message is provided to allow better deserialization once the type is known event jsoniter.RawMessage } // ClientEvent is the base struct that informs, based of the Type field, which Event type was provided from the client. type ClientEvent struct { Type ClientEventType `json:"type,omitempty"` // The raw json message is provided to allow better deserialization once the type is known event jsoniter.RawMessage } // EventStartStreaming signifies that the client wishes to start receiving log events. // Additional filters can be provided to augment the log events requested. type EventStartStreaming struct { ClientEvent Filters *StreamingFilters `json:"filters,omitempty"` } type StreamingFilters struct { Events []LogEventType `json:"events,omitempty"` Level *LogLevel `json:"level,omitempty"` Sampling float64 `json:"sampling,omitempty"` } // EventStopStreaming signifies that the client wishes to halt receiving log events. type EventStopStreaming struct { ClientEvent } // EventLog is the event that the server sends to the client with the log events. type EventLog struct { ServerEvent Logs []*Log `json:"logs"` } // LogEventType is the way that logging messages are able to be filtered. // Example: assigning LogEventType.Cloudflared to a zerolog event will allow the client to filter for only // the Cloudflared-related events. type LogEventType int8 const ( // Cloudflared events are significant to cloudflared operations like connection state changes. // Cloudflared is also the default event type for any events that haven't been separated into a proper event type. Cloudflared LogEventType = iota HTTP TCP UDP ) func ParseLogEventType(s string) (LogEventType, bool) { switch s { case "cloudflared": return Cloudflared, true case "http": return HTTP, true case "tcp": return TCP, true case "udp": return UDP, true } return -1, false } func (l LogEventType) String() string { switch l { case Cloudflared: return "cloudflared" case HTTP: return "http" case TCP: return "tcp" case UDP: return "udp" default: return "" } } func (l LogEventType) MarshalJSON() ([]byte, error) { return json.Marshal(l.String()) } func (e *LogEventType) UnmarshalJSON(data []byte) error { var s string if err := json.Unmarshal(data, &s); err != nil { return errors.New("unable to unmarshal LogEventType string") } if event, ok := ParseLogEventType(s); ok { *e = event return nil } return errors.New("unable to unmarshal LogEventType") } // LogLevel corresponds to the zerolog logging levels // "panic", "fatal", and "trace" are exempt from this list as they are rarely used and, at least // the first two are limited to failure conditions that lead to cloudflared shutting down. type LogLevel int8 const ( Debug LogLevel = 0 Info LogLevel = 1 Warn LogLevel = 2 Error LogLevel = 3 ) func ParseLogLevel(l string) (LogLevel, bool) { switch l { case "debug": return Debug, true case "info": return Info, true case "warn": return Warn, true case "error": return Error, true } return -1, false } func (l LogLevel) String() string { switch l { case Debug: return "debug" case Info: return "info" case Warn: return "warn" case Error: return "error" default: return "" } } func (l LogLevel) MarshalJSON() ([]byte, error) { return json.Marshal(l.String()) } func (l *LogLevel) UnmarshalJSON(data []byte) error { var s string if err := json.Unmarshal(data, &s); err != nil { return errors.New("unable to unmarshal LogLevel string") } if level, ok := ParseLogLevel(s); ok { *l = level return nil } return fmt.Errorf("unable to unmarshal LogLevel") } const ( // TimeKey aligns with the zerolog.TimeFieldName TimeKey = "time" // LevelKey aligns with the zerolog.LevelFieldName LevelKey = "level" // LevelKey aligns with the zerolog.MessageFieldName MessageKey = "message" // EventTypeKey is the custom JSON key of the LogEventType in ZeroLogEvent EventTypeKey = "event" // FieldsKey is a custom JSON key to match and store every other key for a zerolog event FieldsKey = "fields" ) // Log is the basic structure of the events that are sent to the client. type Log struct { Time string `json:"time,omitempty"` Level LogLevel `json:"level,omitempty"` Message string `json:"message,omitempty"` Event LogEventType `json:"event,omitempty"` Fields map[string]interface{} `json:"fields,omitempty"` } // IntoClientEvent unmarshals the provided ClientEvent into the proper type. func IntoClientEvent[T EventStartStreaming | EventStopStreaming](e *ClientEvent, eventType ClientEventType) (*T, bool) { if e.Type != eventType { return nil, false } event := new(T) err := json.Unmarshal(e.event, event) if err != nil { return nil, false } return event, true } // IntoServerEvent unmarshals the provided ServerEvent into the proper type. func IntoServerEvent[T EventLog](e *ServerEvent, eventType ServerEventType) (*T, bool) { if e.Type != eventType { return nil, false } event := new(T) err := json.Unmarshal(e.event, event) if err != nil { return nil, false } return event, true } // ReadEvent will read a message from the websocket connection and parse it into a valid ServerEvent. func ReadServerEvent(c *websocket.Conn, ctx context.Context) (*ServerEvent, error) { message, err := readMessage(c, ctx) if err != nil { return nil, err } event := ServerEvent{} if err := json.Unmarshal(message, &event); err != nil { return nil, err } switch event.Type { case Logs: event.event = message return &event, nil case UnknownServerEventType: return nil, errInvalidMessageType default: return nil, fmt.Errorf("invalid server message type was provided: %s", event.Type) } } // ReadEvent will read a message from the websocket connection and parse it into a valid ClientEvent. func ReadClientEvent(c *websocket.Conn, ctx context.Context) (*ClientEvent, error) { message, err := readMessage(c, ctx) if err != nil { return nil, err } event := ClientEvent{} if err := json.Unmarshal(message, &event); err != nil { return nil, err } switch event.Type { case StartStreaming, StopStreaming: event.event = message return &event, nil case UnknownClientEventType: return nil, errInvalidMessageType default: return nil, fmt.Errorf("invalid client message type was provided: %s", event.Type) } } // readMessage will read a message from the websocket connection and return the payload. func readMessage(c *websocket.Conn, ctx context.Context) ([]byte, error) { messageType, reader, err := c.Reader(ctx) if err != nil { return nil, err } if messageType != websocket.MessageText { return nil, errInvalidMessageType } return io.ReadAll(reader) } // WriteEvent will write a Event type message to the websocket connection. func WriteEvent(c *websocket.Conn, ctx context.Context, event any) error { payload, err := json.Marshal(event) if err != nil { return err } return c.Write(ctx, websocket.MessageText, payload) } // IsClosed returns true if the websocket error is a websocket.CloseError; returns false if not a // websocket.CloseError func IsClosed(err error, log *zerolog.Logger) bool { var closeErr websocket.CloseError if errors.As(err, &closeErr) { if closeErr.Code != websocket.StatusNormalClosure { log.Debug().Msgf("connection is already closed: (%d) %s", closeErr.Code, closeErr.Reason) } return true } return false } func AsClosed(err error) *websocket.CloseError { var closeErr websocket.CloseError if errors.As(err, &closeErr) { return &closeErr } return nil } ================================================ FILE: management/events_test.go ================================================ package management import ( "context" "testing" "time" "github.com/stretchr/testify/require" "nhooyr.io/websocket" "github.com/cloudflare/cloudflared/internal/test" ) var ( debugLevel *LogLevel infoLevel *LogLevel warnLevel *LogLevel errorLevel *LogLevel ) func init() { // created here because we can't do a reference to a const enum, i.e. &Info debugLevel := new(LogLevel) *debugLevel = Debug infoLevel := new(LogLevel) *infoLevel = Info warnLevel := new(LogLevel) *warnLevel = Warn errorLevel := new(LogLevel) *errorLevel = Error } func TestIntoClientEvent_StartStreaming(t *testing.T) { for _, test := range []struct { name string expected EventStartStreaming }{ { name: "no filters", expected: EventStartStreaming{ClientEvent: ClientEvent{Type: StartStreaming}}, }, { name: "level filter", expected: EventStartStreaming{ ClientEvent: ClientEvent{Type: StartStreaming}, Filters: &StreamingFilters{ Level: infoLevel, }, }, }, { name: "events filter", expected: EventStartStreaming{ ClientEvent: ClientEvent{Type: StartStreaming}, Filters: &StreamingFilters{ Events: []LogEventType{Cloudflared, HTTP}, }, }, }, { name: "sampling filter", expected: EventStartStreaming{ ClientEvent: ClientEvent{Type: StartStreaming}, Filters: &StreamingFilters{ Sampling: 0.5, }, }, }, { name: "level and events filters", expected: EventStartStreaming{ ClientEvent: ClientEvent{Type: StartStreaming}, Filters: &StreamingFilters{ Level: infoLevel, Events: []LogEventType{Cloudflared}, Sampling: 0.5, }, }, }, } { t.Run(test.name, func(t *testing.T) { data, err := json.Marshal(test.expected) require.NoError(t, err) event := ClientEvent{} err = json.Unmarshal(data, &event) require.NoError(t, err) event.event = data ce, ok := IntoClientEvent[EventStartStreaming](&event, StartStreaming) require.True(t, ok) require.Equal(t, test.expected.ClientEvent, ce.ClientEvent) if test.expected.Filters != nil { f := ce.Filters ef := test.expected.Filters if ef.Level != nil { require.Equal(t, *ef.Level, *f.Level) } require.ElementsMatch(t, ef.Events, f.Events) } }) } } func TestIntoClientEvent_StopStreaming(t *testing.T) { event := ClientEvent{ Type: StopStreaming, event: []byte(`{"type": "stop_streaming"}`), } ce, ok := IntoClientEvent[EventStopStreaming](&event, StopStreaming) require.True(t, ok) require.Equal(t, EventStopStreaming{ClientEvent: ClientEvent{Type: StopStreaming}}, *ce) } func TestIntoClientEvent_Invalid(t *testing.T) { event := ClientEvent{ Type: UnknownClientEventType, event: []byte(`{"type": "invalid"}`), } _, ok := IntoClientEvent[EventStartStreaming](&event, StartStreaming) require.False(t, ok) } func TestIntoServerEvent_Logs(t *testing.T) { event := ServerEvent{ Type: Logs, event: []byte(`{"type": "logs"}`), } ce, ok := IntoServerEvent(&event, Logs) require.True(t, ok) require.Equal(t, EventLog{ServerEvent: ServerEvent{Type: Logs}}, *ce) } func TestIntoServerEvent_Invalid(t *testing.T) { event := ServerEvent{ Type: UnknownServerEventType, event: []byte(`{"type": "invalid"}`), } _, ok := IntoServerEvent(&event, Logs) require.False(t, ok) } func TestReadServerEvent(t *testing.T) { sentEvent := EventLog{ ServerEvent: ServerEvent{Type: Logs}, Logs: []*Log{ { Time: time.Now().UTC().Format(time.RFC3339), Event: HTTP, Level: Info, Message: "test", }, }, } client, server := test.WSPipe(nil, nil) server.CloseRead(context.Background()) defer func() { server.Close(websocket.StatusInternalError, "") }() go func() { err := WriteEvent(server, context.Background(), &sentEvent) require.NoError(t, err) }() event, err := ReadServerEvent(client, context.Background()) require.NoError(t, err) require.Equal(t, sentEvent.Type, event.Type) client.Close(websocket.StatusInternalError, "") } func TestReadServerEvent_InvalidWebSocketMessageType(t *testing.T) { client, server := test.WSPipe(nil, nil) server.CloseRead(context.Background()) defer func() { server.Close(websocket.StatusInternalError, "") }() go func() { err := server.Write(context.Background(), websocket.MessageBinary, []byte("test1234")) require.NoError(t, err) }() _, err := ReadServerEvent(client, context.Background()) require.Error(t, err) client.Close(websocket.StatusInternalError, "") } func TestReadServerEvent_InvalidMessageType(t *testing.T) { sentEvent := ClientEvent{Type: ClientEventType(UnknownServerEventType)} client, server := test.WSPipe(nil, nil) server.CloseRead(context.Background()) defer func() { server.Close(websocket.StatusInternalError, "") }() go func() { err := WriteEvent(server, context.Background(), &sentEvent) require.NoError(t, err) }() _, err := ReadServerEvent(client, context.Background()) require.ErrorIs(t, err, errInvalidMessageType) client.Close(websocket.StatusInternalError, "") } func TestReadClientEvent(t *testing.T) { sentEvent := EventStartStreaming{ ClientEvent: ClientEvent{Type: StartStreaming}, } client, server := test.WSPipe(nil, nil) client.CloseRead(context.Background()) defer func() { client.Close(websocket.StatusInternalError, "") }() go func() { err := WriteEvent(client, context.Background(), &sentEvent) require.NoError(t, err) }() event, err := ReadClientEvent(server, context.Background()) require.NoError(t, err) require.Equal(t, sentEvent.Type, event.Type) server.Close(websocket.StatusInternalError, "") } func TestReadClientEvent_InvalidWebSocketMessageType(t *testing.T) { client, server := test.WSPipe(nil, nil) client.CloseRead(context.Background()) defer func() { client.Close(websocket.StatusInternalError, "") }() go func() { err := client.Write(context.Background(), websocket.MessageBinary, []byte("test1234")) require.NoError(t, err) }() _, err := ReadClientEvent(server, context.Background()) require.Error(t, err) server.Close(websocket.StatusInternalError, "") } func TestReadClientEvent_InvalidMessageType(t *testing.T) { sentEvent := ClientEvent{Type: UnknownClientEventType} client, server := test.WSPipe(nil, nil) client.CloseRead(context.Background()) defer func() { client.Close(websocket.StatusInternalError, "") }() go func() { err := WriteEvent(client, context.Background(), &sentEvent) require.NoError(t, err) }() _, err := ReadClientEvent(server, context.Background()) require.ErrorIs(t, err, errInvalidMessageType) server.Close(websocket.StatusInternalError, "") } ================================================ FILE: management/logger.go ================================================ package management import ( "os" "sync" "time" jsoniter "github.com/json-iterator/go" "github.com/rs/zerolog" ) var json = jsoniter.ConfigFastest // Logger manages the number of management streaming log sessions type Logger struct { sessions []*session mu sync.RWMutex // Unique logger that isn't a io.Writer of the list of zerolog writers. This helps prevent management log // statements from creating infinite recursion to export messages to a session and allows basic debugging and // error statements to be issued in the management code itself. Log *zerolog.Logger } func NewLogger() *Logger { log := zerolog.New(zerolog.ConsoleWriter{ Out: os.Stdout, TimeFormat: time.RFC3339, }).With().Timestamp().Logger().Level(zerolog.InfoLevel) return &Logger{ Log: &log, } } type LoggerListener interface { // ActiveSession returns the first active session for the requested actor. ActiveSession(actor) *session // ActiveSession returns the count of active sessions. ActiveSessions() int // Listen appends the session to the list of sessions that receive log events. Listen(*session) // Remove a session from the available sessions that were receiving log events. Remove(*session) } func (l *Logger) ActiveSession(actor actor) *session { l.mu.RLock() defer l.mu.RUnlock() for _, session := range l.sessions { if session.actor.ID == actor.ID && session.active.Load() { return session } } return nil } func (l *Logger) ActiveSessions() int { l.mu.RLock() defer l.mu.RUnlock() count := 0 for _, session := range l.sessions { if session.active.Load() { count += 1 } } return count } func (l *Logger) Listen(session *session) { l.mu.Lock() defer l.mu.Unlock() session.active.Store(true) l.sessions = append(l.sessions, session) } func (l *Logger) Remove(session *session) { l.mu.Lock() defer l.mu.Unlock() index := -1 for i, v := range l.sessions { if v == session { index = i break } } if index == -1 { // Not found return } copy(l.sessions[index:], l.sessions[index+1:]) l.sessions = l.sessions[:len(l.sessions)-1] } // Write will write the log event to all sessions that have available capacity. For those that are full, the message // will be dropped. // This function is the interface that zerolog expects to call when a log event is to be written out. func (l *Logger) Write(p []byte) (int, error) { l.mu.RLock() defer l.mu.RUnlock() // return early if no active sessions if len(l.sessions) == 0 { return len(p), nil } event, err := parseZerologEvent(p) // drop event if unable to parse properly if err != nil { l.Log.Debug().Msg("unable to parse log event") return len(p), nil } for _, session := range l.sessions { session.Insert(event) } return len(p), nil } func (l *Logger) WriteLevel(level zerolog.Level, p []byte) (n int, err error) { return l.Write(p) } func parseZerologEvent(p []byte) (*Log, error) { var fields map[string]interface{} iter := json.BorrowIterator(p) defer json.ReturnIterator(iter) iter.ReadVal(&fields) if iter.Error != nil { return nil, iter.Error } logTime := time.Now().UTC().Format(zerolog.TimeFieldFormat) if t, ok := fields[TimeKey]; ok { if t, ok := t.(string); ok { logTime = t } } logLevel := Debug // A zerolog Debug event can be created and then an error can be added // via .Err(error), if so, we upgrade the level to error. if _, hasError := fields["error"]; hasError { logLevel = Error } else { if level, ok := fields[LevelKey]; ok { if level, ok := level.(string); ok { if logLevel, ok = ParseLogLevel(level); !ok { logLevel = Debug } } } } // Assume the event type is Cloudflared if unable to parse/find. This could be from log events that haven't // yet been tagged with the appropriate EventType yet. logEvent := Cloudflared e := fields[EventTypeKey] if e != nil { if eventNumber, ok := e.(float64); ok { logEvent = LogEventType(eventNumber) } } logMessage := "" if m, ok := fields[MessageKey]; ok { if m, ok := m.(string); ok { logMessage = m } } event := Log{ Time: logTime, Level: logLevel, Event: logEvent, Message: logMessage, } // Remove the keys that have top level keys on Log delete(fields, TimeKey) delete(fields, LevelKey) delete(fields, EventTypeKey) delete(fields, MessageKey) // The rest of the keys go into the Fields event.Fields = fields return &event, nil } ================================================ FILE: management/logger_test.go ================================================ package management import ( "context" "testing" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // No listening sessions will not write to the channel func TestLoggerWrite_NoSessions(t *testing.T) { logger := NewLogger() zlog := zerolog.New(logger).With().Timestamp().Logger().Level(zerolog.InfoLevel) zlog.Info().Msg("hello") } // Validate that the session receives the event func TestLoggerWrite_OneSession(t *testing.T) { logger := NewLogger() zlog := zerolog.New(logger).With().Timestamp().Logger().Level(zerolog.InfoLevel) _, cancel := context.WithCancel(context.Background()) defer cancel() session := newSession(logWindow, actor{ID: actorID}, cancel) logger.Listen(session) defer logger.Remove(session) assert.Equal(t, 1, logger.ActiveSessions()) assert.Equal(t, session, logger.ActiveSession(actor{ID: actorID})) zlog.Info().Int(EventTypeKey, int(HTTP)).Msg("hello") select { case event := <-session.listener: assert.NotEmpty(t, event.Time) assert.Equal(t, "hello", event.Message) assert.Equal(t, Info, event.Level) assert.Equal(t, HTTP, event.Event) default: assert.Fail(t, "expected an event to be in the listener") } } // Validate all sessions receive the same event func TestLoggerWrite_MultipleSessions(t *testing.T) { logger := NewLogger() zlog := zerolog.New(logger).With().Timestamp().Logger().Level(zerolog.InfoLevel) _, cancel := context.WithCancel(context.Background()) defer cancel() session1 := newSession(logWindow, actor{}, cancel) logger.Listen(session1) defer logger.Remove(session1) assert.Equal(t, 1, logger.ActiveSessions()) session2 := newSession(logWindow, actor{}, cancel) logger.Listen(session2) assert.Equal(t, 2, logger.ActiveSessions()) zlog.Info().Int(EventTypeKey, int(HTTP)).Msg("hello") for _, session := range []*session{session1, session2} { select { case event := <-session.listener: assert.NotEmpty(t, event.Time) assert.Equal(t, "hello", event.Message) assert.Equal(t, Info, event.Level) assert.Equal(t, HTTP, event.Event) default: assert.Fail(t, "expected an event to be in the listener") } } // Close session2 and make sure session1 still receives events logger.Remove(session2) zlog.Info().Int(EventTypeKey, int(HTTP)).Msg("hello2") select { case event := <-session1.listener: assert.NotEmpty(t, event.Time) assert.Equal(t, "hello2", event.Message) assert.Equal(t, Info, event.Level) assert.Equal(t, HTTP, event.Event) default: assert.Fail(t, "expected an event to be in the listener") } // Make sure a held reference to session2 doesn't receive events after being closed select { case <-session2.listener: assert.Fail(t, "An event was not expected to be in the session listener") default: // pass } } type mockWriter struct { event *Log err error } func (m *mockWriter) Write(p []byte) (int, error) { m.event, m.err = parseZerologEvent(p) return len(p), nil } // Validate all event types are set properly func TestParseZerologEvent_EventTypes(t *testing.T) { writer := mockWriter{} zlog := zerolog.New(&writer).With().Timestamp().Logger().Level(zerolog.InfoLevel) for _, test := range []LogEventType{ Cloudflared, HTTP, TCP, UDP, } { t.Run(test.String(), func(t *testing.T) { defer func() { writer.err = nil }() zlog.Info().Int(EventTypeKey, int(test)).Msg("test") require.NoError(t, writer.err) require.Equal(t, test, writer.event.Event) }) } // Invalid defaults to Cloudflared LogEventType t.Run("invalid", func(t *testing.T) { defer func() { writer.err = nil }() zlog.Info().Str(EventTypeKey, "unknown").Msg("test") require.NoError(t, writer.err) require.Equal(t, Cloudflared, writer.event.Event) }) } // Validate top-level keys are removed from Fields func TestParseZerologEvent_Fields(t *testing.T) { writer := mockWriter{} zlog := zerolog.New(&writer).With().Timestamp().Logger().Level(zerolog.InfoLevel) zlog.Info().Int(EventTypeKey, int(Cloudflared)).Str("test", "test").Msg("test message") require.NoError(t, writer.err) event := writer.event require.NotEmpty(t, event.Time) require.Equal(t, Cloudflared, event.Event) require.Equal(t, Info, event.Level) require.Equal(t, "test message", event.Message) // Make sure Fields doesn't have other set keys used in the Log struct require.NotEmpty(t, event.Fields) require.Equal(t, "test", event.Fields["test"]) require.NotContains(t, event.Fields, EventTypeKey) require.NotContains(t, event.Fields, LevelKey) require.NotContains(t, event.Fields, MessageKey) require.NotContains(t, event.Fields, TimeKey) } ================================================ FILE: management/middleware.go ================================================ package management import ( "context" "fmt" "net/http" ) type ctxKey int const ( accessClaimsCtxKey ctxKey = iota ) var errMissingAccessToken = managementError{Code: 1001, Message: "missing access_token query parameter"} // HTTP middleware setting the parsed access_token claims in the request context func ValidateAccessTokenQueryMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Validate access token accessToken := r.URL.Query().Get("access_token") if accessToken == "" { writeHTTPErrorResponse(w, errMissingAccessToken) return } token, err := ParseToken(accessToken) if err != nil { writeHTTPErrorResponse(w, errMissingAccessToken) return } r = r.WithContext(context.WithValue(r.Context(), accessClaimsCtxKey, token)) next.ServeHTTP(w, r) }) } // Middleware validation error struct for returning to the eyeball type managementError struct { Code int `json:"code,omitempty"` Message string `json:"message,omitempty"` } func (m *managementError) Error() string { return m.Message } // Middleware validation error HTTP response JSON for returning to the eyeball type managementErrorResponse struct { Success bool `json:"success,omitempty"` Errors []managementError `json:"errors,omitempty"` } // writeErrorResponse will respond to the eyeball with basic HTTP JSON payloads with validation failure information func writeHTTPErrorResponse(w http.ResponseWriter, errResp managementError) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusBadRequest) err := json.NewEncoder(w).Encode(managementErrorResponse{ Success: false, Errors: []managementError{errResp}, }) // we have already written the header, so write a basic error response if unable to encode the error if err != nil { // fallback to text message http.Error(w, fmt.Sprintf( "%d %s", http.StatusBadRequest, http.StatusText(http.StatusBadRequest), ), http.StatusBadRequest) } } ================================================ FILE: management/middleware_test.go ================================================ package management import ( "io" "net/http" "net/http/httptest" "testing" "github.com/go-chi/chi/v5" "github.com/stretchr/testify/assert" ) func TestValidateAccessTokenQueryMiddleware(t *testing.T) { r := chi.NewRouter() r.Use(ValidateAccessTokenQueryMiddleware) r.Get("/valid", func(w http.ResponseWriter, r *http.Request) { claims, ok := r.Context().Value(accessClaimsCtxKey).(*managementTokenClaims) assert.True(t, ok) assert.True(t, claims.verify()) w.WriteHeader(http.StatusOK) }) r.Get("/invalid", func(w http.ResponseWriter, r *http.Request) { _, ok := r.Context().Value(accessClaimsCtxKey).(*managementTokenClaims) assert.False(t, ok) w.WriteHeader(http.StatusOK) }) ts := httptest.NewServer(r) defer ts.Close() // valid: with access_token query param path := "/valid?access_token=" + validToken resp, _ := testRequest(t, ts, "GET", path, nil) assert.Equal(t, http.StatusOK, resp.StatusCode) // invalid: unset token path = "/invalid" resp, err := testRequest(t, ts, "GET", path, nil) assert.Equal(t, http.StatusBadRequest, resp.StatusCode) assert.NotNil(t, err) assert.Equal(t, errMissingAccessToken, err.Errors[0]) // invalid: invalid token path = "/invalid?access_token=eyJ" resp, err = testRequest(t, ts, "GET", path, nil) assert.Equal(t, http.StatusBadRequest, resp.StatusCode) assert.NotNil(t, err) assert.Equal(t, errMissingAccessToken, err.Errors[0]) } func testRequest(t *testing.T, ts *httptest.Server, method, path string, body io.Reader) (*http.Response, *managementErrorResponse) { req, err := http.NewRequest(method, ts.URL+path, body) if err != nil { t.Fatal(err) } resp, err := ts.Client().Do(req) if err != nil { t.Fatal(err) } var claims managementErrorResponse err = json.NewDecoder(resp.Body).Decode(&claims) if err != nil { return resp, nil } defer resp.Body.Close() return resp, &claims } ================================================ FILE: management/service.go ================================================ package management import ( "context" "fmt" "net" "net/http" "net/http/pprof" "os" "sync" "time" "github.com/go-chi/chi/v5" "github.com/go-chi/cors" "github.com/google/uuid" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/rs/zerolog" "nhooyr.io/websocket" ) const ( // In the current state, an invalid command was provided by the client StatusInvalidCommand websocket.StatusCode = 4001 reasonInvalidCommand = "expected start streaming as first event" // There are a limited number of available streaming log sessions that cloudflared will service, exceeding this // value will return this error to incoming requests. StatusSessionLimitExceeded websocket.StatusCode = 4002 reasonSessionLimitExceeded = "limit exceeded for streaming sessions" // There is a limited idle time while not actively serving a session for a request before dropping the connection. StatusIdleLimitExceeded websocket.StatusCode = 4003 reasonIdleLimitExceeded = "session was idle for too long" ) var ( // CORS middleware required to allow dash to access management.argotunnel.com requests corsHandler = cors.Handler(cors.Options{ // Allows for any subdomain of cloudflare.com AllowedOrigins: []string{"https://*.cloudflare.com"}, // Required to present cookies or other authentication across origin boundries AllowCredentials: true, MaxAge: 300, // Maximum value not ignored by any of major browsers }) ) type ManagementService struct { // The management tunnel hostname Hostname string // Host details related configurations serviceIP string clientID uuid.UUID label string // Additional Handlers metricsHandler http.Handler log *zerolog.Logger router chi.Router // streamingMut is a lock to prevent concurrent requests to start streaming. Utilizing the atomic.Bool is not // sufficient to complete this operation since many other checks during an incoming new request are needed // to validate this before setting streaming to true. streamingMut sync.Mutex logger LoggerListener } func New(managementHostname string, enableDiagServices bool, serviceIP string, clientID uuid.UUID, label string, log *zerolog.Logger, logger LoggerListener, ) *ManagementService { s := &ManagementService{ Hostname: managementHostname, log: log, logger: logger, serviceIP: serviceIP, clientID: clientID, label: label, metricsHandler: promhttp.Handler(), } r := chi.NewRouter() r.Use(ValidateAccessTokenQueryMiddleware) // Default management services r.With(corsHandler).Get("/ping", ping) r.With(corsHandler).Head("/ping", ping) r.Get("/logs", s.logs) r.With(corsHandler).Get("/host_details", s.getHostDetails) // Diagnostic management services if enableDiagServices { // Prometheus endpoint r.With(corsHandler).Get("/metrics", s.metricsHandler.ServeHTTP) // Supports only heap and goroutine r.With(corsHandler).Get("/debug/pprof/{profile:heap|goroutine}", pprof.Index) } s.router = r return s } func (m *ManagementService) ServeHTTP(w http.ResponseWriter, r *http.Request) { m.router.ServeHTTP(w, r) } // Management Ping handler func ping(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) } // The response provided by the /host_details endpoint type getHostDetailsResponse struct { ClientID string `json:"connector_id"` IP string `json:"ip,omitempty"` HostName string `json:"hostname,omitempty"` } func (m *ManagementService) getHostDetails(w http.ResponseWriter, r *http.Request) { var getHostDetailsResponse = getHostDetailsResponse{ ClientID: m.clientID.String(), } if ip, err := getPrivateIP(m.serviceIP); err == nil { getHostDetailsResponse.IP = ip } getHostDetailsResponse.HostName = m.getLabel() w.Header().Set("Content-Type", "application/json") w.WriteHeader(200) json.NewEncoder(w).Encode(getHostDetailsResponse) } func (m *ManagementService) getLabel() string { if m.label != "" { return fmt.Sprintf("custom:%s", m.label) } // If no label is provided we return the system hostname. This is not // a fqdn hostname. hostname, err := os.Hostname() if err != nil { return "unknown" } return hostname } // Get preferred private ip of this machine func getPrivateIP(addr string) (string, error) { conn, err := net.DialTimeout("tcp", addr, 1*time.Second) if err != nil { return "", err } defer conn.Close() localAddr := conn.LocalAddr().String() host, _, err := net.SplitHostPort(localAddr) return host, err } // readEvents will loop through all incoming websocket messages from a client and marshal them into the // proper Event structure and pass through to the events channel. Any invalid messages sent will automatically // terminate the connection. func (m *ManagementService) readEvents(c *websocket.Conn, ctx context.Context, events chan<- *ClientEvent) { for { event, err := ReadClientEvent(c, ctx) select { case <-ctx.Done(): return default: if err != nil { // If the client (or the server) already closed the connection, don't attempt to close it again if !IsClosed(err, m.log) { m.log.Err(err).Send() m.log.Err(c.Close(websocket.StatusUnsupportedData, err.Error())).Send() } // Any errors when reading the messages from the client will close the connection return } events <- event } } } // streamLogs will begin the process of reading from the Session listener and write the log events to the client. func (m *ManagementService) streamLogs(c *websocket.Conn, ctx context.Context, session *session) { for session.Active() { select { case <-ctx.Done(): session.Stop() return case event := <-session.listener: err := WriteEvent(c, ctx, &EventLog{ ServerEvent: ServerEvent{Type: Logs}, Logs: []*Log{event}, }) if err != nil { // If the client (or the server) already closed the connection, don't attempt to close it again if !IsClosed(err, m.log) { m.log.Err(err).Send() m.log.Err(c.Close(websocket.StatusInternalError, err.Error())).Send() } // Any errors when writing the messages to the client will stop streaming and close the connection session.Stop() return } default: // No messages to send } } } // canStartStream will check the conditions of the request and return if the session can begin streaming. func (m *ManagementService) canStartStream(session *session) bool { m.streamingMut.Lock() defer m.streamingMut.Unlock() // Limits to one actor for streaming logs if m.logger.ActiveSessions() > 0 { // Allow the same user to preempt their existing session to disconnect their old session and start streaming // with this new session instead. if existingSession := m.logger.ActiveSession(session.actor); existingSession != nil { m.log.Info(). Msgf("Another management session request for the same actor was requested; the other session will be disconnected to handle the new request.") existingSession.Stop() m.logger.Remove(existingSession) existingSession.cancel() } else { m.log.Warn(). Msgf("Another management session request was attempted but one session already being served; there is a limit of streaming log sessions to reduce overall performance impact.") return false } } return true } // parseFilters will check the ClientEvent for start_streaming and assign filters if provided to the session func (m *ManagementService) parseFilters(c *websocket.Conn, event *ClientEvent, session *session) bool { // Expect the first incoming request startEvent, ok := IntoClientEvent[EventStartStreaming](event, StartStreaming) if !ok { return false } session.Filters(startEvent.Filters) return true } // Management Streaming Logs accept handler func (m *ManagementService) logs(w http.ResponseWriter, r *http.Request) { c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ OriginPatterns: []string{ "*.cloudflare.com", }, }) if err != nil { m.log.Debug().Msgf("management handshake: %s", err.Error()) return } // Make sure the connection is closed if other go routines fail to close the connection after completing. defer c.Close(websocket.StatusInternalError, "") ctx, cancel := context.WithCancel(r.Context()) defer cancel() events := make(chan *ClientEvent) go m.readEvents(c, ctx, events) // Send a heartbeat ping to hold the connection open even if not streaming. ping := time.NewTicker(15 * time.Second) defer ping.Stop() // Close the connection if no operation has occurred after the idle timeout. The timeout is halted // when streaming logs is active. idleTimeout := 5 * time.Minute idle := time.NewTimer(idleTimeout) defer idle.Stop() // Fetch the claims from the request context to acquire the actor claims, ok := ctx.Value(accessClaimsCtxKey).(*managementTokenClaims) if !ok || claims == nil { // Typically should never happen as it is provided in the context from the middleware m.log.Err(c.Close(websocket.StatusInternalError, "missing access_token")).Send() return } session := newSession(logWindow, claims.Actor, cancel) defer m.logger.Remove(session) for { select { case <-ctx.Done(): m.log.Debug().Msgf("management logs: context cancelled") c.Close(websocket.StatusNormalClosure, "context closed") return case event := <-events: switch event.Type { case StartStreaming: idle.Stop() // Expect the first incoming request startEvent, ok := IntoClientEvent[EventStartStreaming](event, StartStreaming) if !ok { m.log.Warn().Msgf("expected start_streaming as first recieved event") m.log.Err(c.Close(StatusInvalidCommand, reasonInvalidCommand)).Send() return } // Make sure the session can start if !m.canStartStream(session) { m.log.Err(c.Close(StatusSessionLimitExceeded, reasonSessionLimitExceeded)).Send() return } session.Filters(startEvent.Filters) m.logger.Listen(session) m.log.Debug().Msgf("Streaming logs") go m.streamLogs(c, ctx, session) continue case StopStreaming: idle.Reset(idleTimeout) // Stop the current session for the current actor who requested it session.Stop() m.logger.Remove(session) case UnknownClientEventType: fallthrough default: // Drop unknown events and close connection m.log.Debug().Msgf("unexpected management message received: %s", event.Type) // If the client (or the server) already closed the connection, don't attempt to close it again if !IsClosed(err, m.log) { m.log.Err(err).Err(c.Close(websocket.StatusUnsupportedData, err.Error())).Send() } return } case <-ping.C: go c.Ping(ctx) case <-idle.C: c.Close(StatusIdleLimitExceeded, reasonIdleLimitExceeded) return } } } ================================================ FILE: management/service_test.go ================================================ package management import ( "context" "io" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/google/uuid" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "nhooyr.io/websocket" "github.com/cloudflare/cloudflared/internal/test" ) var ( noopLogger = zerolog.New(io.Discard) managementHostname = "https://management.argotunnel.com" ) func TestDisableDiagnosticRoutes(t *testing.T) { mgmt := New("management.argotunnel.com", false, "1.1.1.1:80", uuid.Nil, "", &noopLogger, nil) for _, path := range []string{"/metrics", "/debug/pprof/goroutine", "/debug/pprof/heap"} { t.Run(strings.Replace(path, "/", "_", -1), func(t *testing.T) { req := httptest.NewRequest("GET", managementHostname+path+"?access_token="+validToken, nil) recorder := httptest.NewRecorder() mgmt.ServeHTTP(recorder, req) resp := recorder.Result() require.Equal(t, http.StatusNotFound, resp.StatusCode) }) } } func TestReadEventsLoop(t *testing.T) { sentEvent := EventStartStreaming{ ClientEvent: ClientEvent{Type: StartStreaming}, } client, server := test.WSPipe(nil, nil) client.CloseRead(context.Background()) defer func() { client.Close(websocket.StatusInternalError, "") }() go func() { err := WriteEvent(client, context.Background(), &sentEvent) require.NoError(t, err) }() m := ManagementService{ log: &noopLogger, } events := make(chan *ClientEvent) go m.readEvents(server, context.Background(), events) event := <-events require.Equal(t, sentEvent.Type, event.Type) server.Close(websocket.StatusInternalError, "") } func TestReadEventsLoop_ContextCancelled(t *testing.T) { client, server := test.WSPipe(nil, nil) ctx, cancel := context.WithCancel(context.Background()) client.CloseRead(ctx) defer func() { client.Close(websocket.StatusInternalError, "") }() m := ManagementService{ log: &noopLogger, } events := make(chan *ClientEvent) go func() { time.Sleep(time.Second) cancel() }() // Want to make sure this function returns when context is cancelled m.readEvents(server, ctx, events) server.Close(websocket.StatusInternalError, "") } func TestCanStartStream_NoSessions(t *testing.T) { m := ManagementService{ log: &noopLogger, logger: &Logger{ Log: &noopLogger, }, } _, cancel := context.WithCancel(context.Background()) session := newSession(0, actor{}, cancel) assert.True(t, m.canStartStream(session)) } func TestCanStartStream_ExistingSessionDifferentActor(t *testing.T) { m := ManagementService{ log: &noopLogger, logger: &Logger{ Log: &noopLogger, }, } _, cancel := context.WithCancel(context.Background()) session1 := newSession(0, actor{ID: "test"}, cancel) assert.True(t, m.canStartStream(session1)) m.logger.Listen(session1) assert.True(t, session1.Active()) // Try another session session2 := newSession(0, actor{ID: "test2"}, cancel) assert.Equal(t, 1, m.logger.ActiveSessions()) assert.False(t, m.canStartStream(session2)) // Close session1 m.logger.Remove(session1) assert.True(t, session1.Active()) // Remove doesn't stop a session session1.Stop() assert.False(t, session1.Active()) assert.Equal(t, 0, m.logger.ActiveSessions()) // Try session2 again assert.True(t, m.canStartStream(session2)) } func TestCanStartStream_ExistingSessionSameActor(t *testing.T) { m := ManagementService{ log: &noopLogger, logger: &Logger{ Log: &noopLogger, }, } actor := actor{ID: "test"} _, cancel := context.WithCancel(context.Background()) session1 := newSession(0, actor, cancel) assert.True(t, m.canStartStream(session1)) m.logger.Listen(session1) assert.True(t, session1.Active()) // Try another session session2 := newSession(0, actor, cancel) assert.Equal(t, 1, m.logger.ActiveSessions()) assert.True(t, m.canStartStream(session2)) // session1 is removed and stopped assert.Equal(t, 0, m.logger.ActiveSessions()) assert.False(t, session1.Active()) } ================================================ FILE: management/session.go ================================================ package management import ( "context" "math/rand" "sync/atomic" ) const ( // Indicates how many log messages the listener will hold before dropping. // Provides a throttling mechanism to drop latest messages if the sender // can't keep up with the influx of log messages. logWindow = 30 ) // session captures a streaming logs session for a connection of an actor. type session struct { // Indicates if the session is streaming or not. Modifying this will affect the active session. active atomic.Bool // Allows the session to control the context of the underlying connection to close it out when done. Mostly // used by the LoggerListener to close out and cleanup a session. cancel context.CancelFunc // Actor who started the session actor actor // Buffered channel that holds the recent log events listener chan *Log // Types of log events that this session will provide through the listener filters *StreamingFilters // Sampling of the log events this session will send (runs after all other filters if available) sampler *sampler } // NewSession creates a new session. func newSession(size int, actor actor, cancel context.CancelFunc) *session { s := &session{ active: atomic.Bool{}, cancel: cancel, actor: actor, listener: make(chan *Log, size), filters: &StreamingFilters{}, } return s } // Filters assigns the StreamingFilters to the session func (s *session) Filters(filters *StreamingFilters) { if filters != nil { s.filters = filters sampling := filters.Sampling // clamp the sampling values between 0 and 1 if sampling < 0 { sampling = 0 } if sampling > 1 { sampling = 1 } s.filters.Sampling = sampling if sampling > 0 && sampling < 1 { s.sampler = &sampler{ p: int(sampling * 100), } } } else { s.filters = &StreamingFilters{} } } // Insert attempts to insert the log to the session. If the log event matches the provided session filters, it // will be applied to the listener. func (s *session) Insert(log *Log) { // Level filters are optional if s.filters.Level != nil { if *s.filters.Level > log.Level { return } } // Event filters are optional if len(s.filters.Events) != 0 && !contains(s.filters.Events, log.Event) { return } // Sampling is also optional if s.sampler != nil && !s.sampler.Sample() { return } select { case s.listener <- log: default: // buffer is full, discard } } // Active returns if the session is active func (s *session) Active() bool { return s.active.Load() } // Stop will halt the session func (s *session) Stop() { s.active.Store(false) } func contains(array []LogEventType, t LogEventType) bool { for _, v := range array { if v == t { return true } } return false } // sampler will send approximately every p percentage log events out of 100. type sampler struct { p int } // Sample returns true if the event should be part of the sample, false if the event should be dropped. func (s *sampler) Sample() bool { return rand.Intn(100) <= s.p } ================================================ FILE: management/session_test.go ================================================ package management import ( "context" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // Validate the active states of the session func TestSession_ActiveControl(t *testing.T) { _, cancel := context.WithCancel(context.Background()) defer cancel() session := newSession(4, actor{}, cancel) // session starts out not active assert.False(t, session.Active()) session.active.Store(true) assert.True(t, session.Active()) session.Stop() assert.False(t, session.Active()) } // Validate that the session filters events func TestSession_Insert(t *testing.T) { _, cancel := context.WithCancel(context.Background()) defer cancel() infoLevel := new(LogLevel) *infoLevel = Info warnLevel := new(LogLevel) *warnLevel = Warn for _, test := range []struct { name string filters StreamingFilters expectLog bool }{ { name: "none", expectLog: true, }, { name: "level", filters: StreamingFilters{ Level: infoLevel, }, expectLog: true, }, { name: "filtered out level", filters: StreamingFilters{ Level: warnLevel, }, expectLog: false, }, { name: "events", filters: StreamingFilters{ Events: []LogEventType{HTTP}, }, expectLog: true, }, { name: "filtered out event", filters: StreamingFilters{ Events: []LogEventType{Cloudflared}, }, expectLog: false, }, { name: "sampling", filters: StreamingFilters{ Sampling: 0.9999999, }, expectLog: true, }, { name: "sampling (invalid negative)", filters: StreamingFilters{ Sampling: -1.0, }, expectLog: true, }, { name: "sampling (invalid too large)", filters: StreamingFilters{ Sampling: 2.0, }, expectLog: true, }, { name: "filter and event", filters: StreamingFilters{ Level: infoLevel, Events: []LogEventType{HTTP}, }, expectLog: true, }, } { t.Run(test.name, func(t *testing.T) { session := newSession(4, actor{}, cancel) session.Filters(&test.filters) log := Log{ Time: time.Now().UTC().Format(time.RFC3339), Event: HTTP, Level: Info, Message: "test", } session.Insert(&log) select { case <-session.listener: require.True(t, test.expectLog) default: require.False(t, test.expectLog) } }) } } // Validate that the session has a max amount of events to hold func TestSession_InsertOverflow(t *testing.T) { _, cancel := context.WithCancel(context.Background()) defer cancel() session := newSession(1, actor{}, cancel) log := Log{ Time: time.Now().UTC().Format(time.RFC3339), Event: HTTP, Level: Info, Message: "test", } // Insert 2 but only max channel size for 1 session.Insert(&log) session.Insert(&log) select { case <-session.listener: // pass default: require.Fail(t, "expected one log event") } // Second dequeue should fail select { case <-session.listener: require.Fail(t, "expected no more remaining log events") default: // pass } } ================================================ FILE: management/token.go ================================================ package management import ( "fmt" "github.com/go-jose/go-jose/v4" "github.com/go-jose/go-jose/v4/jwt" ) const tunnelstoreFEDIssuer = "fed-tunnelstore" type managementTokenClaims struct { Tunnel tunnel `json:"tun"` Actor actor `json:"actor"` jwt.Claims } // VerifyTunnel compares the tun claim isn't empty func (c *managementTokenClaims) verify() bool { return c.Tunnel.verify() && c.Actor.verify() } type tunnel struct { ID string `json:"id"` AccountTag string `json:"account_tag"` } // verify compares the tun claim isn't empty func (t *tunnel) verify() bool { return t.AccountTag != "" && t.ID != "" } type actor struct { ID string `json:"id"` Support bool `json:"support"` } // verify checks the ID claim isn't empty func (t *actor) verify() bool { return t.ID != "" } func ParseToken(token string) (*managementTokenClaims, error) { jwt, err := jwt.ParseSigned(token, []jose.SignatureAlgorithm{jose.ES256}) if err != nil { return nil, fmt.Errorf("malformed jwt: %v", err) } var claims managementTokenClaims // This is actually safe because we verify the token in the edge before it reaches cloudflared err = jwt.UnsafeClaimsWithoutVerification(&claims) if err != nil { return nil, fmt.Errorf("malformed jwt: %v", err) } if !claims.verify() { return nil, fmt.Errorf("invalid management token format provided") } return &claims, nil } func (m *managementTokenClaims) IsFed() bool { return m.Issuer == tunnelstoreFEDIssuer } ================================================ FILE: management/token_test.go ================================================ package management import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "errors" "testing" "github.com/go-jose/go-jose/v4" "github.com/stretchr/testify/require" ) const ( validToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IjEifQ.eyJ0dW4iOnsiaWQiOiI3YjA5ODE0OS01MWZlLTRlZTUtYTY4Ny0zZTM3NDQ2NmVmYzciLCJhY2NvdW50X3RhZyI6ImNkMzkxZTljMDYyNmE4Zjc2Y2IxZjY3MGY2NTkxYjA1In0sImFjdG9yIjp7ImlkIjoiZGNhcnJAY2xvdWRmbGFyZS5jb20iLCJzdXBwb3J0IjpmYWxzZX0sInJlcyI6WyJsb2dzIl0sImV4cCI6MTY3NzExNzY5NiwiaWF0IjoxNjc3MTE0MDk2LCJpc3MiOiJ0dW5uZWxzdG9yZSJ9.mKenOdOy3Xi4O-grldFnAAemdlE9WajEpTDC_FwezXQTstWiRTLwU65P5jt4vNsIiZA4OJRq7bH-QYID9wf9NA" // nolint: gosec accountTag = "cd391e9c0626a8f76cb1f670f6591b05" tunnelID = "7b098149-51fe-4ee5-a687-3e374466efc7" actorID = "45d2751e-6b59-45a9-814d-f630786bd0cd" ) type invalidManagementTokenClaims struct { Invalid string `json:"invalid"` } func TestParseToken(t *testing.T) { key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err) for _, test := range []struct { name string claims any err error }{ { name: "Valid", claims: managementTokenClaims{ Tunnel: tunnel{ ID: tunnelID, AccountTag: accountTag, }, Actor: actor{ ID: actorID, }, }, }, { name: "Invalid claims", claims: invalidManagementTokenClaims{Invalid: "invalid"}, err: errors.New("invalid management token format provided"), }, { name: "Missing Tunnel", claims: managementTokenClaims{ Actor: actor{ ID: actorID, }, }, err: errors.New("invalid management token format provided"), }, { name: "Missing Tunnel ID", claims: managementTokenClaims{ Tunnel: tunnel{ AccountTag: accountTag, }, Actor: actor{ ID: actorID, }, }, err: errors.New("invalid management token format provided"), }, { name: "Missing Account Tag", claims: managementTokenClaims{ Tunnel: tunnel{ ID: tunnelID, }, Actor: actor{ ID: actorID, }, }, err: errors.New("invalid management token format provided"), }, { name: "Missing Actor", claims: managementTokenClaims{ Tunnel: tunnel{ ID: tunnelID, AccountTag: accountTag, }, }, err: errors.New("invalid management token format provided"), }, { name: "Missing Actor ID", claims: managementTokenClaims{ Tunnel: tunnel{ ID: tunnelID, }, Actor: actor{}, }, err: errors.New("invalid management token format provided"), }, } { t.Run(test.name, func(t *testing.T) { jwt := signToken(t, test.claims, key) claims, err := ParseToken(jwt) if test.err != nil { require.EqualError(t, err, test.err.Error()) return } require.NoError(t, err) require.Equal(t, test.claims, *claims) }) } } func signToken(t *testing.T, token any, key *ecdsa.PrivateKey) string { opts := (&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", "1") signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: key}, opts) require.NoError(t, err) payload, err := json.Marshal(token) require.NoError(t, err) jws, err := signer.Sign(payload) require.NoError(t, err) jwt, err := jws.CompactSerialize() require.NoError(t, err) return jwt } ================================================ FILE: metrics/config.go ================================================ package metrics type HistogramConfig struct { BucketsStart float64 BucketsWidth float64 BucketsCount int } ================================================ FILE: metrics/metrics.go ================================================ package metrics import ( "context" "fmt" "net" "net/http" _ "net/http/pprof" "runtime" "sync" "time" "github.com/facebookgo/grace/gracenet" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/rs/zerolog" "golang.org/x/net/trace" "github.com/cloudflare/cloudflared/diagnostic" ) const ( startupTime = time.Millisecond * 500 defaultShutdownTimeout = time.Second * 15 ) // This variable is set at compile time to allow the default local address to change. var Runtime = "host" func GetMetricsDefaultAddress(runtimeType string) string { // When issuing the diagnostic command we may have to reach a server that is // running in a virtual environment and in that case we must bind to 0.0.0.0 // otherwise the server won't be reachable. switch runtimeType { case "virtual": return "0.0.0.0:0" default: return "localhost:0" } } // GetMetricsKnownAddresses returns the addresses used by the metrics server to bind at // startup time to allow a semi-deterministic approach to know where the server is listening at. // The ports were selected because at the time we are in 2024 and they do not collide with any // know/registered port according https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers. func GetMetricsKnownAddresses(runtimeType string) []string { switch runtimeType { case "virtual": return []string{"0.0.0.0:20241", "0.0.0.0:20242", "0.0.0.0:20243", "0.0.0.0:20244", "0.0.0.0:20245"} default: return []string{"localhost:20241", "localhost:20242", "localhost:20243", "localhost:20244", "localhost:20245"} } } type Config struct { ReadyServer *ReadyServer DiagnosticHandler *diagnostic.Handler QuickTunnelHostname string Orchestrator orchestrator ShutdownTimeout time.Duration } type orchestrator interface { GetVersionedConfigJSON() ([]byte, error) } func newMetricsHandler( config Config, log *zerolog.Logger, ) *http.ServeMux { router := http.NewServeMux() router.Handle("/debug/", http.DefaultServeMux) router.Handle("/metrics", promhttp.Handler()) router.HandleFunc("/healthcheck", func(w http.ResponseWriter, r *http.Request) { _, _ = fmt.Fprintf(w, "OK\n") }) if config.ReadyServer != nil { router.Handle("/ready", config.ReadyServer) } router.HandleFunc("/quicktunnel", func(w http.ResponseWriter, r *http.Request) { _, _ = fmt.Fprintf(w, `{"hostname":"%s"}`, config.QuickTunnelHostname) }) if config.Orchestrator != nil { router.HandleFunc("/config", func(w http.ResponseWriter, r *http.Request) { json, err := config.Orchestrator.GetVersionedConfigJSON() if err != nil { w.WriteHeader(500) _, _ = fmt.Fprintf(w, "ERR: %v", err) log.Err(err).Msg("Failed to serve config") return } _, _ = w.Write(json) }) } config.DiagnosticHandler.InstallEndpoints(router) return router } // CreateMetricsListener will create a new [net.Listener] by using an // known set of ports when the default address is passed with the fallback // of choosing a random port when none is available. // // In case the provided address is not the default one then it will be used // as is. func CreateMetricsListener(listeners *gracenet.Net, laddr string) (net.Listener, error) { if laddr == GetMetricsDefaultAddress(Runtime) { // On the presence of the default address select // a port from the known set of addresses iteratively. addresses := GetMetricsKnownAddresses(Runtime) for _, address := range addresses { listener, err := listeners.Listen("tcp", address) if err == nil { return listener, nil } } // When no port is available then bind to a random one listener, err := listeners.Listen("tcp", laddr) if err != nil { return nil, fmt.Errorf("failed to listen to default metrics address: %w", err) } return listener, nil } // Explicitly got a local address then bind to it listener, err := listeners.Listen("tcp", laddr) if err != nil { return nil, fmt.Errorf("failed to bind to address (%s): %w", laddr, err) } return listener, nil } func ServeMetrics( l net.Listener, ctx context.Context, config Config, log *zerolog.Logger, ) (err error) { var wg sync.WaitGroup // Metrics port is privileged, so no need for further access control trace.AuthRequest = func(*http.Request) (bool, bool) { return true, true } // TODO: parameterize ReadTimeout and WriteTimeout. The maximum time we can // profile CPU usage depends on WriteTimeout h := newMetricsHandler(config, log) server := &http.Server{ ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, Handler: h, } wg.Add(1) go func() { defer wg.Done() err = server.Serve(l) }() log.Info().Msgf("Starting metrics server on %s", fmt.Sprintf("%v/metrics", l.Addr())) // server.Serve will hang if server.Shutdown is called before the server is // fully started up. So add artificial delay. time.Sleep(startupTime) <-ctx.Done() shutdownTimeout := config.ShutdownTimeout if shutdownTimeout == 0 { shutdownTimeout = defaultShutdownTimeout } ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout) _ = server.Shutdown(ctx) cancel() wg.Wait() if err == http.ErrServerClosed { log.Info().Msg("Metrics server stopped") return nil } log.Err(err).Msg("Metrics server failed") return err } func RegisterBuildInfo(buildType, buildTime, version string) { buildInfo := prometheus.NewGaugeVec( prometheus.GaugeOpts{ // Don't namespace build_info, since we want it to be consistent across all Cloudflare services Name: "build_info", Help: "Build and version information", }, []string{"goversion", "type", "revision", "version"}, ) prometheus.MustRegister(buildInfo) buildInfo.WithLabelValues(runtime.Version(), buildType, buildTime, version).Set(1) } ================================================ FILE: metrics/metrics_test.go ================================================ package metrics_test import ( "testing" "github.com/facebookgo/grace/gracenet" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/cloudflare/cloudflared/metrics" ) func TestMetricsListenerCreation(t *testing.T) { t.Parallel() listeners := gracenet.Net{} listener1, err := metrics.CreateMetricsListener(&listeners, metrics.GetMetricsDefaultAddress("host")) assert.Equal(t, "127.0.0.1:20241", listener1.Addr().String()) require.NoError(t, err) listener2, err := metrics.CreateMetricsListener(&listeners, metrics.GetMetricsDefaultAddress("host")) assert.Equal(t, "127.0.0.1:20242", listener2.Addr().String()) require.NoError(t, err) listener3, err := metrics.CreateMetricsListener(&listeners, metrics.GetMetricsDefaultAddress("host")) assert.Equal(t, "127.0.0.1:20243", listener3.Addr().String()) require.NoError(t, err) listener4, err := metrics.CreateMetricsListener(&listeners, metrics.GetMetricsDefaultAddress("host")) assert.Equal(t, "127.0.0.1:20244", listener4.Addr().String()) require.NoError(t, err) listener5, err := metrics.CreateMetricsListener(&listeners, metrics.GetMetricsDefaultAddress("host")) assert.Equal(t, "127.0.0.1:20245", listener5.Addr().String()) require.NoError(t, err) listener6, err := metrics.CreateMetricsListener(&listeners, metrics.GetMetricsDefaultAddress("host")) addresses := [5]string{"127.0.0.1:20241", "127.0.0.1:20242", "127.0.0.1:20243", "127.0.0.1:20244", "127.0.0.1:20245"} assert.NotContains(t, addresses, listener6.Addr().String()) require.NoError(t, err) listener7, err := metrics.CreateMetricsListener(&listeners, "localhost:12345") assert.Equal(t, "127.0.0.1:12345", listener7.Addr().String()) require.NoError(t, err) err = listener1.Close() require.NoError(t, err) err = listener2.Close() require.NoError(t, err) err = listener3.Close() require.NoError(t, err) err = listener4.Close() require.NoError(t, err) err = listener5.Close() require.NoError(t, err) err = listener6.Close() require.NoError(t, err) err = listener7.Close() require.NoError(t, err) } ================================================ FILE: metrics/readiness.go ================================================ package metrics import ( "encoding/json" "fmt" "net/http" "github.com/google/uuid" "github.com/cloudflare/cloudflared/tunnelstate" ) // ReadyServer serves HTTP 200 if the tunnel can serve traffic. Intended for k8s readiness checks. type ReadyServer struct { clientID uuid.UUID tracker *tunnelstate.ConnTracker } // NewReadyServer initializes a ReadyServer and starts listening for dis/connection events. func NewReadyServer( clientID uuid.UUID, tracker *tunnelstate.ConnTracker, ) *ReadyServer { return &ReadyServer{ clientID, tracker, } } type body struct { Status int `json:"status"` ReadyConnections uint `json:"readyConnections"` ConnectorID uuid.UUID `json:"connectorId"` } // ServeHTTP responds with HTTP 200 if the tunnel is connected to the edge. func (rs *ReadyServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { statusCode, readyConnections := rs.makeResponse() w.WriteHeader(statusCode) body := body{ Status: statusCode, ReadyConnections: readyConnections, ConnectorID: rs.clientID, } msg, err := json.Marshal(body) if err != nil { _, _ = fmt.Fprintf(w, `{"error": "%s"}`, err) } _, _ = w.Write(msg) } // This is the bulk of the logic for ServeHTTP, broken into its own pure function // to make unit testing easy. func (rs *ReadyServer) makeResponse() (statusCode int, readyConnections uint) { readyConnections = rs.tracker.CountActiveConns() if readyConnections > 0 { return http.StatusOK, readyConnections } else { return http.StatusServiceUnavailable, readyConnections } } ================================================ FILE: metrics/readiness_test.go ================================================ package metrics_test import ( "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/google/uuid" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/cloudflare/cloudflared/connection" "github.com/cloudflare/cloudflared/metrics" "github.com/cloudflare/cloudflared/tunnelstate" ) func mockRequest(t *testing.T, readyServer *metrics.ReadyServer) (int, uint) { t.Helper() var readyreadyConnections struct { Status int `json:"status"` ReadyConnections uint `json:"readyConnections"` ConnectorID uuid.UUID `json:"connectorId"` } rec := httptest.NewRecorder() readyServer.ServeHTTP(rec, nil) decoder := json.NewDecoder(rec.Body) err := decoder.Decode(&readyreadyConnections) require.NoError(t, err) return rec.Code, readyreadyConnections.ReadyConnections } func TestReadinessEventHandling(t *testing.T) { nopLogger := zerolog.Nop() tracker := tunnelstate.NewConnTracker(&nopLogger) rs := metrics.NewReadyServer(uuid.Nil, tracker) // start not ok code, readyConnections := mockRequest(t, rs) assert.NotEqualValues(t, http.StatusOK, code) assert.Zero(t, readyConnections) // one connected => ok tracker.OnTunnelEvent(connection.Event{ Index: 1, EventType: connection.Connected, }) code, readyConnections = mockRequest(t, rs) assert.EqualValues(t, http.StatusOK, code) assert.EqualValues(t, 1, readyConnections) // another connected => still ok tracker.OnTunnelEvent(connection.Event{ Index: 2, EventType: connection.Connected, }) code, readyConnections = mockRequest(t, rs) assert.EqualValues(t, http.StatusOK, code) assert.EqualValues(t, 2, readyConnections) // one reconnecting => still ok tracker.OnTunnelEvent(connection.Event{ Index: 2, EventType: connection.Reconnecting, }) code, readyConnections = mockRequest(t, rs) assert.EqualValues(t, http.StatusOK, code) assert.EqualValues(t, 1, readyConnections) // Regression test for TUN-3777 tracker.OnTunnelEvent(connection.Event{ Index: 1, EventType: connection.RegisteringTunnel, }) code, readyConnections = mockRequest(t, rs) assert.NotEqualValues(t, http.StatusOK, code) assert.Zero(t, readyConnections) // other connected then unregistered => not ok tracker.OnTunnelEvent(connection.Event{ Index: 1, EventType: connection.Connected, }) code, readyConnections = mockRequest(t, rs) assert.EqualValues(t, http.StatusOK, code) assert.EqualValues(t, 1, readyConnections) tracker.OnTunnelEvent(connection.Event{ Index: 1, EventType: connection.Unregistering, }) code, readyConnections = mockRequest(t, rs) assert.NotEqualValues(t, http.StatusOK, code) assert.Zero(t, readyConnections) // other disconnected => not ok tracker.OnTunnelEvent(connection.Event{ Index: 1, EventType: connection.Disconnected, }) code, readyConnections = mockRequest(t, rs) assert.NotEqualValues(t, http.StatusOK, code) assert.Zero(t, readyConnections) } ================================================ FILE: mocks/mock_limiter.go ================================================ // Code generated by MockGen. DO NOT EDIT. // Source: ../flow/limiter.go // // Generated by this command: // // mockgen -typed -build_flags=-tags=gomock -package mocks -destination mock_limiter.go -source=../flow/limiter.go Limiter // // Package mocks is a generated GoMock package. package mocks import ( reflect "reflect" gomock "go.uber.org/mock/gomock" ) // MockLimiter is a mock of Limiter interface. type MockLimiter struct { ctrl *gomock.Controller recorder *MockLimiterMockRecorder isgomock struct{} } // MockLimiterMockRecorder is the mock recorder for MockLimiter. type MockLimiterMockRecorder struct { mock *MockLimiter } // NewMockLimiter creates a new mock instance. func NewMockLimiter(ctrl *gomock.Controller) *MockLimiter { mock := &MockLimiter{ctrl: ctrl} mock.recorder = &MockLimiterMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. func (m *MockLimiter) EXPECT() *MockLimiterMockRecorder { return m.recorder } // Acquire mocks base method. func (m *MockLimiter) Acquire(flowType string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Acquire", flowType) ret0, _ := ret[0].(error) return ret0 } // Acquire indicates an expected call of Acquire. func (mr *MockLimiterMockRecorder) Acquire(flowType any) *MockLimiterAcquireCall { mr.mock.ctrl.T.Helper() call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Acquire", reflect.TypeOf((*MockLimiter)(nil).Acquire), flowType) return &MockLimiterAcquireCall{Call: call} } // MockLimiterAcquireCall wrap *gomock.Call type MockLimiterAcquireCall struct { *gomock.Call } // Return rewrite *gomock.Call.Return func (c *MockLimiterAcquireCall) Return(arg0 error) *MockLimiterAcquireCall { c.Call = c.Call.Return(arg0) return c } // Do rewrite *gomock.Call.Do func (c *MockLimiterAcquireCall) Do(f func(string) error) *MockLimiterAcquireCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn func (c *MockLimiterAcquireCall) DoAndReturn(f func(string) error) *MockLimiterAcquireCall { c.Call = c.Call.DoAndReturn(f) return c } // Release mocks base method. func (m *MockLimiter) Release() { m.ctrl.T.Helper() m.ctrl.Call(m, "Release") } // Release indicates an expected call of Release. func (mr *MockLimiterMockRecorder) Release() *MockLimiterReleaseCall { mr.mock.ctrl.T.Helper() call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Release", reflect.TypeOf((*MockLimiter)(nil).Release)) return &MockLimiterReleaseCall{Call: call} } // MockLimiterReleaseCall wrap *gomock.Call type MockLimiterReleaseCall struct { *gomock.Call } // Return rewrite *gomock.Call.Return func (c *MockLimiterReleaseCall) Return() *MockLimiterReleaseCall { c.Call = c.Call.Return() return c } // Do rewrite *gomock.Call.Do func (c *MockLimiterReleaseCall) Do(f func()) *MockLimiterReleaseCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn func (c *MockLimiterReleaseCall) DoAndReturn(f func()) *MockLimiterReleaseCall { c.Call = c.Call.DoAndReturn(f) return c } // SetLimit mocks base method. func (m *MockLimiter) SetLimit(arg0 uint64) { m.ctrl.T.Helper() m.ctrl.Call(m, "SetLimit", arg0) } // SetLimit indicates an expected call of SetLimit. func (mr *MockLimiterMockRecorder) SetLimit(arg0 any) *MockLimiterSetLimitCall { mr.mock.ctrl.T.Helper() call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLimit", reflect.TypeOf((*MockLimiter)(nil).SetLimit), arg0) return &MockLimiterSetLimitCall{Call: call} } // MockLimiterSetLimitCall wrap *gomock.Call type MockLimiterSetLimitCall struct { *gomock.Call } // Return rewrite *gomock.Call.Return func (c *MockLimiterSetLimitCall) Return() *MockLimiterSetLimitCall { c.Call = c.Call.Return() return c } // Do rewrite *gomock.Call.Do func (c *MockLimiterSetLimitCall) Do(f func(uint64)) *MockLimiterSetLimitCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn func (c *MockLimiterSetLimitCall) DoAndReturn(f func(uint64)) *MockLimiterSetLimitCall { c.Call = c.Call.DoAndReturn(f) return c } ================================================ FILE: mocks/mockgen.go ================================================ //go:build gomock || generate package mocks //go:generate sh -c "go run go.uber.org/mock/mockgen -typed -build_flags=\"-tags=gomock\" -package mocks -destination mock_limiter.go -source=../flow/limiter.go Limiter" ================================================ FILE: orchestration/config.go ================================================ package orchestration import ( "encoding/json" "github.com/cloudflare/cloudflared/config" "github.com/cloudflare/cloudflared/ingress" ) type newRemoteConfig struct { ingress.RemoteConfig // Add more fields when we support other settings in tunnel orchestration } type newLocalConfig struct { RemoteConfig ingress.RemoteConfig ConfigurationFlags map[string]string `json:"__configuration_flags,omitempty"` } // Config is the original config as read and parsed by cloudflared. type Config struct { Ingress *ingress.Ingress WarpRouting ingress.WarpRoutingConfig OriginDialerService *ingress.OriginDialerService // Extra settings used to configure this instance but that are not eligible for remotely management // ie. (--protocol, --loglevel, ...) ConfigurationFlags map[string]string } func (rc *newLocalConfig) MarshalJSON() ([]byte, error) { var r = struct { ConfigurationFlags map[string]string `json:"__configuration_flags,omitempty"` ingress.RemoteConfigJSON }{ ConfigurationFlags: rc.ConfigurationFlags, RemoteConfigJSON: ingress.RemoteConfigJSON{ // UI doesn't support top level configs, so we reconcile to individual ingress configs. GlobalOriginRequest: nil, IngressRules: convertToUnvalidatedIngressRules(rc.RemoteConfig.Ingress), WarpRouting: rc.RemoteConfig.WarpRouting.RawConfig(), }, } return json.Marshal(r) } func convertToUnvalidatedIngressRules(i ingress.Ingress) []config.UnvalidatedIngressRule { result := make([]config.UnvalidatedIngressRule, 0) for _, rule := range i.Rules { var path string if rule.Path != nil { path = rule.Path.String() } newRule := config.UnvalidatedIngressRule{ Hostname: rule.Hostname, Path: path, Service: rule.Service.String(), OriginRequest: ingress.ConvertToRawOriginConfig(rule.Config), } result = append(result, newRule) } return result } ================================================ FILE: orchestration/config_test.go ================================================ package orchestration import ( "encoding/json" "testing" "time" "github.com/stretchr/testify/require" "github.com/cloudflare/cloudflared/config" "github.com/cloudflare/cloudflared/ingress" ) // TestNewLocalConfig_MarshalJSON tests that we are able to converte a compiled and validated config back // into an "unvalidated" format which is compatible with Remote Managed configurations. func TestNewLocalConfig_MarshalJSON(t *testing.T) { rawConfig := []byte(` { "originRequest": { "connectTimeout": 160, "httpHostHeader": "default" }, "ingress": [ { "hostname": "tun.example.com", "service": "https://localhost:8000" }, { "hostname": "*", "service": "https://localhost:8001", "originRequest": { "connectTimeout": 121, "tlsTimeout": 2, "noHappyEyeballs": false, "tcpKeepAlive": 2, "keepAliveConnections": 2, "keepAliveTimeout": 2, "httpHostHeader": "def", "originServerName": "b2", "caPool": "/tmp/path1", "noTLSVerify": false, "disableChunkedEncoding": false, "bastionMode": false, "proxyAddress": "interface", "proxyPort": 200, "proxyType": "", "ipRules": [ { "prefix": "10.0.0.0/16", "ports": [3000, 3030], "allow": false }, { "prefix": "192.16.0.0/24", "ports": [5000, 5050], "allow": true } ] } } ], "warp-routing": { "connectTimeout": 1 } } `) var expectedConfig ingress.RemoteConfig err := json.Unmarshal(rawConfig, &expectedConfig) require.NoError(t, err) c := &newLocalConfig{ RemoteConfig: expectedConfig, ConfigurationFlags: nil, } jsonSerde, err := json.Marshal(c) require.NoError(t, err) var remoteConfig ingress.RemoteConfig err = json.Unmarshal(jsonSerde, &remoteConfig) require.NoError(t, err) require.Equal(t, remoteConfig.WarpRouting, ingress.WarpRoutingConfig{ ConnectTimeout: config.CustomDuration{ Duration: time.Second, }, TCPKeepAlive: config.CustomDuration{ Duration: 30 * time.Second, // default value is 30 seconds }, }) require.Equal(t, remoteConfig.Ingress.Rules, expectedConfig.Ingress.Rules) } ================================================ FILE: orchestration/metrics.go ================================================ package orchestration import ( "github.com/prometheus/client_golang/prometheus" ) const ( MetricsNamespace = "cloudflared" MetricsSubsystem = "orchestration" ) var ( configVersion = prometheus.NewGauge( prometheus.GaugeOpts{ Namespace: MetricsNamespace, Subsystem: MetricsSubsystem, Name: "config_version", Help: "Configuration Version", }, ) ) func init() { prometheus.MustRegister(configVersion) } ================================================ FILE: orchestration/orchestrator.go ================================================ package orchestration import ( "context" "encoding/json" "fmt" "strconv" "sync" "sync/atomic" pkgerrors "github.com/pkg/errors" "github.com/rs/zerolog" "github.com/cloudflare/cloudflared/cmd/cloudflared/flags" "github.com/cloudflare/cloudflared/config" "github.com/cloudflare/cloudflared/connection" cfdflow "github.com/cloudflare/cloudflared/flow" "github.com/cloudflare/cloudflared/ingress" "github.com/cloudflare/cloudflared/proxy" "github.com/cloudflare/cloudflared/tunnelrpc/pogs" ) // Orchestrator manages configurations, so they can be updatable during runtime // properties are static, so it can be read without lock // currentVersion and config are read/write infrequently, so their access are synchronized with RWMutex // access to proxy is synchronized with atomic.Value, because it uses copy-on-write to provide scalable frequently // read when update is infrequent type Orchestrator struct { currentVersion int32 // Used by UpdateConfig to make sure one update at a time lock sync.RWMutex // Underlying value is proxy.Proxy, can be read without the lock, but still needs the lock to update proxy atomic.Value // Set of internal ingress rules defined at cloudflared startup (separate from user-defined ingress rules) internalRules []ingress.Rule // cloudflared Configuration config *Config tags []pogs.Tag // flowLimiter tracks active sessions across the tunnel and limits new sessions if they are above the limit. flowLimiter cfdflow.Limiter // Origin dialer service to manage egress socket dialing. originDialerService *ingress.OriginDialerService log *zerolog.Logger // orchestrator must not handle any more updates after shutdownC is closed shutdownC <-chan struct{} // Closing proxyShutdownC will close the previous proxy proxyShutdownC chan<- struct{} } func NewOrchestrator(ctx context.Context, config *Config, tags []pogs.Tag, internalRules []ingress.Rule, log *zerolog.Logger, ) (*Orchestrator, error) { o := &Orchestrator{ // Lowest possible version, any remote configuration will have version higher than this // Starting at -1 allows a configuration migration (local to remote) to override the current configuration as it // will start at version 0. currentVersion: -1, internalRules: internalRules, config: config, tags: tags, flowLimiter: cfdflow.NewLimiter(config.WarpRouting.MaxActiveFlows), originDialerService: config.OriginDialerService, log: log, shutdownC: ctx.Done(), } if err := o.updateIngress(*config.Ingress, config.WarpRouting); err != nil { return nil, err } go o.waitToCloseLastProxy() return o, nil } // UpdateConfig creates a new proxy with the new ingress rules func (o *Orchestrator) UpdateConfig(version int32, config []byte) *pogs.UpdateConfigurationResponse { o.lock.Lock() defer o.lock.Unlock() if o.currentVersion >= version { o.log.Debug(). Int32("current_version", o.currentVersion). Int32("received_version", version). Msg("Current version is equal or newer than received version") return &pogs.UpdateConfigurationResponse{ LastAppliedVersion: o.currentVersion, } } var newConf newRemoteConfig if err := json.Unmarshal(config, &newConf); err != nil { o.log.Err(err). Int32("version", version). Str("config", string(config)). Msgf("Failed to deserialize new configuration") return &pogs.UpdateConfigurationResponse{ LastAppliedVersion: o.currentVersion, Err: err, } } if err := o.updateIngress(newConf.Ingress, newConf.WarpRouting); err != nil { o.log.Err(err). Int32("version", version). Str("config", string(config)). Msgf("Failed to update ingress") return &pogs.UpdateConfigurationResponse{ LastAppliedVersion: o.currentVersion, Err: err, } } o.currentVersion = version o.log.Info(). Int32("version", version). Str("config", string(config)). Msg("Updated to new configuration") configVersion.Set(float64(version)) return &pogs.UpdateConfigurationResponse{ LastAppliedVersion: o.currentVersion, } } // overrideRemoteWarpRoutingWithLocalValues overrides the ingress.WarpRoutingConfig that comes from the remote with // the local values if there is any. func (o *Orchestrator) overrideRemoteWarpRoutingWithLocalValues(remoteWarpRouting *ingress.WarpRoutingConfig) error { return o.overrideMaxActiveFlows(o.config.ConfigurationFlags[flags.MaxActiveFlows], remoteWarpRouting) } // overrideMaxActiveFlows checks the local configuration flags, and if a value is found for the flags.MaxActiveFlows // overrides the value that comes on the remote ingress.WarpRoutingConfig with the local value. func (o *Orchestrator) overrideMaxActiveFlows(maxActiveFlowsLocalConfig string, remoteWarpRouting *ingress.WarpRoutingConfig) error { // If max active flows isn't defined locally just use the remote value if maxActiveFlowsLocalConfig == "" { return nil } maxActiveFlowsLocalOverride, err := strconv.ParseUint(maxActiveFlowsLocalConfig, 10, 64) if err != nil { return pkgerrors.Wrapf(err, "failed to parse %s", flags.MaxActiveFlows) } // Override the value that comes from the remote with the local value remoteWarpRouting.MaxActiveFlows = maxActiveFlowsLocalOverride return nil } // The caller is responsible to make sure there is no concurrent access func (o *Orchestrator) updateIngress(ingressRules ingress.Ingress, warpRouting ingress.WarpRoutingConfig) error { select { case <-o.shutdownC: return fmt.Errorf("cloudflared already shutdown") default: } // Overrides the local values, onto the remote values of the warp routing configuration if err := o.overrideRemoteWarpRoutingWithLocalValues(&warpRouting); err != nil { return pkgerrors.Wrap(err, "failed to merge local overrides into warp routing configuration") } // Assign the internal ingress rules to the parsed ingress ingressRules.InternalRules = o.internalRules // Check if ingress rules are empty, and add the default route if so. if ingressRules.IsEmpty() { ingressRules.Rules = ingress.GetDefaultIngressRules(o.log) } // Start new proxy before closing the ones from last version. // The upside is we don't need to restart proxy from last version, which can fail // The downside is new version might have ingress rule that require previous version to be shutdown first // The downside is minimized because none of the ingress.OriginService implementation have that requirement proxyShutdownC := make(chan struct{}) if err := ingressRules.StartOrigins(o.log, proxyShutdownC); err != nil { return pkgerrors.Wrap(err, "failed to start origin") } // Update the flow limit since the configuration might have changed o.flowLimiter.SetLimit(warpRouting.MaxActiveFlows) // Update the origin dialer service with the new dialer settings // We need to update the dialer here instead of creating a new instance of OriginDialerService because it has // its own references and go routines. Specifically, the UDP dialer is a reference to this same service all the // way into the datagram manager. Reconstructing the datagram manager is not something we currently provide during // runtime in response to a configuration push except when starting a tunnel connection. o.originDialerService.UpdateDefaultDialer(ingress.NewDialer(warpRouting)) // Create and replace the origin proxy with a new instance proxy := proxy.NewOriginProxy(ingressRules, o.originDialerService, o.tags, o.flowLimiter, o.log) o.proxy.Store(proxy) o.config.Ingress = &ingressRules o.config.WarpRouting = warpRouting // If proxyShutdownC is nil, there is no previous running proxy if o.proxyShutdownC != nil { close(o.proxyShutdownC) } o.proxyShutdownC = proxyShutdownC return nil } // GetConfigJSON returns the current json serialization of the config as the edge understands it func (o *Orchestrator) GetConfigJSON() ([]byte, error) { o.lock.RLock() defer o.lock.RUnlock() c := &newLocalConfig{ RemoteConfig: ingress.RemoteConfig{ Ingress: *o.config.Ingress, WarpRouting: o.config.WarpRouting, }, ConfigurationFlags: o.config.ConfigurationFlags, } return json.Marshal(c) } // GetVersionedConfigJSON returns the current version and configuration as JSON func (o *Orchestrator) GetVersionedConfigJSON() ([]byte, error) { o.lock.RLock() defer o.lock.RUnlock() var currentConfiguration = struct { Version int32 `json:"version"` Config struct { Ingress []ingress.Rule `json:"ingress"` WarpRouting config.WarpRoutingConfig `json:"warp-routing"` OriginRequest ingress.OriginRequestConfig `json:"originRequest"` } `json:"config"` }{ Version: o.currentVersion, Config: struct { Ingress []ingress.Rule `json:"ingress"` WarpRouting config.WarpRoutingConfig `json:"warp-routing"` OriginRequest ingress.OriginRequestConfig `json:"originRequest"` }{ Ingress: o.config.Ingress.Rules, WarpRouting: o.config.WarpRouting.RawConfig(), OriginRequest: o.config.Ingress.Defaults, }, } return json.Marshal(currentConfiguration) } // GetOriginProxy returns an interface to proxy to origin. It satisfies connection.ConfigManager interface func (o *Orchestrator) GetOriginProxy() (connection.OriginProxy, error) { val := o.proxy.Load() if val == nil { err := fmt.Errorf("origin proxy not configured") o.log.Error().Msg(err.Error()) return nil, err } proxy, ok := val.(connection.OriginProxy) if !ok { err := fmt.Errorf("origin proxy has unexpected value %+v", val) o.log.Error().Msg(err.Error()) return nil, err } return proxy, nil } // GetFlowLimiter returns the flow limiter used across cloudflared, that can be hot reload when // the configuration changes. func (o *Orchestrator) GetFlowLimiter() cfdflow.Limiter { return o.flowLimiter } func (o *Orchestrator) waitToCloseLastProxy() { <-o.shutdownC o.lock.Lock() defer o.lock.Unlock() if o.proxyShutdownC != nil { close(o.proxyShutdownC) o.proxyShutdownC = nil } } ================================================ FILE: orchestration/orchestrator_test.go ================================================ package orchestration import ( "context" "encoding/json" "fmt" "io" "net" "net/http" "net/http/httptest" "sync" "testing" "time" "github.com/gobwas/ws/wsutil" "github.com/google/uuid" gows "github.com/gorilla/websocket" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/cloudflare/cloudflared/cmd/cloudflared/flags" "github.com/cloudflare/cloudflared/config" "github.com/cloudflare/cloudflared/connection" "github.com/cloudflare/cloudflared/ingress" "github.com/cloudflare/cloudflared/management" "github.com/cloudflare/cloudflared/tracing" "github.com/cloudflare/cloudflared/tunnelrpc/pogs" ) var ( testLogger = zerolog.Nop() testTags = []pogs.Tag{ { Name: "package", Value: "orchestration", }, { Name: "purpose", Value: "test", }, } testDefaultDialer = ingress.NewDialer(ingress.WarpRoutingConfig{ ConnectTimeout: config.CustomDuration{Duration: 1 * time.Second}, TCPKeepAlive: config.CustomDuration{Duration: 15 * time.Second}, MaxActiveFlows: 0, }) ) // TestUpdateConfiguration tests that // - configurations can be deserialized // - proxy can be updated // - last applied version and error are returned // - configurations can be deserialized // - receiving an old version is noop func TestUpdateConfiguration(t *testing.T) { originDialer := ingress.NewOriginDialer(ingress.OriginConfig{ DefaultDialer: testDefaultDialer, TCPWriteTimeout: 1 * time.Second, }, &testLogger) initConfig := &Config{ Ingress: &ingress.Ingress{}, OriginDialerService: originDialer, } orchestrator, err := NewOrchestrator(t.Context(), initConfig, testTags, []ingress.Rule{ingress.NewManagementRule(management.New("management.argotunnel.com", false, "1.1.1.1:80", uuid.Nil, "", &testLogger, nil))}, &testLogger) require.NoError(t, err) initOriginProxy, err := orchestrator.GetOriginProxy() require.NoError(t, err) require.Implements(t, (*connection.OriginProxy)(nil), initOriginProxy) configJSONV2 := []byte(` { "unknown_field": "not_deserialized", "originRequest": { "connectTimeout": 90, "noHappyEyeballs": true }, "ingress": [ { "hostname": "jira.tunnel.org", "path": "^\/login", "service": "http://192.16.19.1:443", "originRequest": { "noTLSVerify": true, "connectTimeout": 10 } }, { "hostname": "jira.tunnel.org", "service": "http://172.32.20.6:80", "originRequest": { "noTLSVerify": true, "connectTimeout": 30 } }, { "service": "http_status:404" } ], "warp-routing": { "connectTimeout": 10 } } `) updateWithValidation(t, orchestrator, 2, configJSONV2) configV2 := orchestrator.config // Validate internal ingress rules require.Equal(t, "management.argotunnel.com", configV2.Ingress.InternalRules[0].Hostname) require.True(t, configV2.Ingress.InternalRules[0].Matches("management.argotunnel.com", "/ping")) require.Equal(t, "management", configV2.Ingress.InternalRules[0].Service.String()) // Validate ingress rule 0 require.Equal(t, "jira.tunnel.org", configV2.Ingress.Rules[0].Hostname) require.True(t, configV2.Ingress.Rules[0].Matches("jira.tunnel.org", "/login")) require.True(t, configV2.Ingress.Rules[0].Matches("jira.tunnel.org", "/login/2fa")) require.False(t, configV2.Ingress.Rules[0].Matches("jira.tunnel.org", "/users")) require.Equal(t, "http://192.16.19.1:443", configV2.Ingress.Rules[0].Service.String()) require.Len(t, configV2.Ingress.Rules, 3) // originRequest of this ingress rule overrides global default require.Equal(t, config.CustomDuration{Duration: time.Second * 10}, configV2.Ingress.Rules[0].Config.ConnectTimeout) require.True(t, configV2.Ingress.Rules[0].Config.NoTLSVerify) // Inherited from global default require.True(t, configV2.Ingress.Rules[0].Config.NoHappyEyeballs) // Validate ingress rule 1 require.Equal(t, "jira.tunnel.org", configV2.Ingress.Rules[1].Hostname) require.True(t, configV2.Ingress.Rules[1].Matches("jira.tunnel.org", "/users")) require.Equal(t, "http://172.32.20.6:80", configV2.Ingress.Rules[1].Service.String()) // originRequest of this ingress rule overrides global default require.Equal(t, config.CustomDuration{Duration: time.Second * 30}, configV2.Ingress.Rules[1].Config.ConnectTimeout) require.True(t, configV2.Ingress.Rules[1].Config.NoTLSVerify) // Inherited from global default require.True(t, configV2.Ingress.Rules[1].Config.NoHappyEyeballs) // Validate ingress rule 2, it's the catch-all rule require.True(t, configV2.Ingress.Rules[2].Matches("blogs.tunnel.io", "/2022/02/10")) // Inherited from global default require.Equal(t, config.CustomDuration{Duration: time.Second * 90}, configV2.Ingress.Rules[2].Config.ConnectTimeout) require.False(t, configV2.Ingress.Rules[2].Config.NoTLSVerify) require.True(t, configV2.Ingress.Rules[2].Config.NoHappyEyeballs) require.Equal(t, 10*time.Second, configV2.WarpRouting.ConnectTimeout.Duration) originProxyV2, err := orchestrator.GetOriginProxy() require.NoError(t, err) require.Implements(t, (*connection.OriginProxy)(nil), originProxyV2) require.NotEqual(t, originProxyV2, initOriginProxy) // Should not downgrade to an older version resp := orchestrator.UpdateConfig(1, nil) require.NoError(t, resp.Err) require.Equal(t, int32(2), resp.LastAppliedVersion) invalidJSON := []byte(` { "originRequest": } `) resp = orchestrator.UpdateConfig(3, invalidJSON) require.Error(t, resp.Err) require.Equal(t, int32(2), resp.LastAppliedVersion) originProxyV3, err := orchestrator.GetOriginProxy() require.NoError(t, err) require.Equal(t, originProxyV2, originProxyV3) configJSONV10 := []byte(` { "ingress": [ { "service": "hello-world" } ], "warp-routing": { } } `) updateWithValidation(t, orchestrator, 10, configJSONV10) configV10 := orchestrator.config require.Len(t, configV10.Ingress.Rules, 1) require.True(t, configV10.Ingress.Rules[0].Matches("blogs.tunnel.io", "/2022/02/10")) require.Equal(t, ingress.HelloWorldService, configV10.Ingress.Rules[0].Service.String()) originProxyV10, err := orchestrator.GetOriginProxy() require.NoError(t, err) require.Implements(t, (*connection.OriginProxy)(nil), originProxyV10) require.NotEqual(t, originProxyV10, originProxyV2) } // Validates that a new version 0 will be applied if the configuration is loaded locally. // This will happen when a locally managed tunnel is migrated to remote configuration and receives its first configuration. func TestUpdateConfiguration_FromMigration(t *testing.T) { originDialer := ingress.NewOriginDialer(ingress.OriginConfig{ DefaultDialer: testDefaultDialer, TCPWriteTimeout: 1 * time.Second, }, &testLogger) initConfig := &Config{ Ingress: &ingress.Ingress{}, OriginDialerService: originDialer, } orchestrator, err := NewOrchestrator(t.Context(), initConfig, testTags, []ingress.Rule{}, &testLogger) require.NoError(t, err) initOriginProxy, err := orchestrator.GetOriginProxy() require.NoError(t, err) require.Implements(t, (*connection.OriginProxy)(nil), initOriginProxy) configJSONV2 := []byte(` { "ingress": [ { "service": "http_status:404" } ], "warp-routing": { } } `) updateWithValidation(t, orchestrator, 0, configJSONV2) require.Len(t, orchestrator.config.Ingress.Rules, 1) } // Validates that the default ingress rule will be set if there is no rule provided from the remote. func TestUpdateConfiguration_WithoutIngressRule(t *testing.T) { originDialer := ingress.NewOriginDialer(ingress.OriginConfig{ DefaultDialer: testDefaultDialer, TCPWriteTimeout: 1 * time.Second, }, &testLogger) initConfig := &Config{ Ingress: &ingress.Ingress{}, OriginDialerService: originDialer, } orchestrator, err := NewOrchestrator(t.Context(), initConfig, testTags, []ingress.Rule{}, &testLogger) require.NoError(t, err) initOriginProxy, err := orchestrator.GetOriginProxy() require.NoError(t, err) require.Implements(t, (*connection.OriginProxy)(nil), initOriginProxy) // We need to create an empty RemoteConfigJSON because that will get unmarshalled to a RemoteConfig emptyConfig := &ingress.RemoteConfigJSON{} configBytes, err := json.Marshal(emptyConfig) if err != nil { require.FailNow(t, "The RemoteConfigJSON shouldn't fail while being marshalled") } updateWithValidation(t, orchestrator, 0, configBytes) require.Len(t, orchestrator.config.Ingress.Rules, 1) } // TestConcurrentUpdateAndRead makes sure orchestrator can receive updates and return origin proxy concurrently func TestConcurrentUpdateAndRead(t *testing.T) { const ( concurrentRequests = 200 hostname = "public.tunnels.org" expectedHost = "internal.tunnels.svc.cluster.local" tcpBody = "testProxyTCP" ) httpOrigin := httptest.NewServer(&validateHostHandler{ expectedHost: expectedHost, body: t.Name(), }) defer httpOrigin.Close() tcpOrigin, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err) defer tcpOrigin.Close() originDialer := ingress.NewOriginDialer(ingress.OriginConfig{ DefaultDialer: testDefaultDialer, TCPWriteTimeout: 1 * time.Second, }, &testLogger) var ( configJSONV1 = []byte(fmt.Sprintf(` { "originRequest": { "connectTimeout": 90, "noHappyEyeballs": true }, "ingress": [ { "hostname": "%s", "service": "%s", "originRequest": { "httpHostHeader": "%s", "connectTimeout": 10 } }, { "service": "http_status:404" } ], "warp-routing": { } } `, hostname, httpOrigin.URL, expectedHost)) configJSONV2 = []byte(` { "ingress": [ { "service": "http_status:204" } ], "warp-routing": { } } `) configJSONV3 = []byte(` { "ingress": [ { "service": "http_status:418" } ], "warp-routing": { } } `) // appliedV2 makes sure v3 is applied after v2 appliedV2 = make(chan struct{}) initConfig = &Config{ Ingress: &ingress.Ingress{}, OriginDialerService: originDialer, } ) ctx, cancel := context.WithCancel(t.Context()) defer cancel() orchestrator, err := NewOrchestrator(ctx, initConfig, testTags, []ingress.Rule{}, &testLogger) require.NoError(t, err) updateWithValidation(t, orchestrator, 1, configJSONV1) var wg sync.WaitGroup // tcpOrigin will be closed when the test exits. Only the handler routines are included in the wait group go func() { serveTCPOrigin(t, tcpOrigin, &wg) }() for i := range concurrentRequests { originProxy, err := orchestrator.GetOriginProxy() require.NoError(t, err) wg.Add(1) go func(i int, originProxy connection.OriginProxy) { defer wg.Done() resp, err := proxyHTTP(originProxy, hostname) assert.NoError(t, err, "proxyHTTP %d failed %v", i, err) defer resp.Body.Close() // The response can be from initOrigin, http_status:204 or http_status:418 switch resp.StatusCode { // v1 proxy case 200: body, err := io.ReadAll(resp.Body) assert.NoError(t, err) assert.Equal(t, t.Name(), string(body)) // v2 proxy case 204: assert.Greater(t, i, concurrentRequests/4) // v3 proxy case 418: assert.Greater(t, i, concurrentRequests/2) } // Once we have originProxy, it won't be changed by configuration updates. // We can infer the version by the ProxyHTTP response code pr, pw := io.Pipe() w := newRespReadWriteFlusher() // Write TCP message and make sure it's echo back. This has to be done in a go routune since ProxyTCP doesn't // return until the stream is closed. wg.Add(1) go func() { defer wg.Done() defer pw.Close() tcpEyeball(t, pw, tcpBody, w) }() err = proxyTCP(ctx, originProxy, tcpOrigin.Addr().String(), w, pr) assert.NoError(t, err, "proxyTCP %d failed %v", i, err) }(i, originProxy) if i == concurrentRequests/4 { wg.Add(1) go func() { defer wg.Done() updateWithValidation(t, orchestrator, 2, configJSONV2) close(appliedV2) }() } if i == concurrentRequests/2 { wg.Add(1) go func() { defer wg.Done() // Makes sure v2 is applied before v3 <-appliedV2 updateWithValidation(t, orchestrator, 3, configJSONV3) }() } } wg.Wait() } // TestOverrideWarpRoutingConfigWithLocalValues tests that if a value is defined in the Config.ConfigurationFlags, // it will override the value that comes from the remote result. func TestOverrideWarpRoutingConfigWithLocalValues(t *testing.T) { ctx, cancel := context.WithCancel(t.Context()) defer cancel() assertMaxActiveFlows := func(orchestrator *Orchestrator, expectedValue uint64) { configJson, err := orchestrator.GetConfigJSON() require.NoError(t, err) var result map[string]interface{} err = json.Unmarshal(configJson, &result) require.NoError(t, err) warpRouting := result["warp-routing"].(map[string]interface{}) require.EqualValues(t, expectedValue, warpRouting["maxActiveFlows"]) } originDialer := ingress.NewOriginDialer(ingress.OriginConfig{ DefaultDialer: testDefaultDialer, TCPWriteTimeout: 1 * time.Second, }, &testLogger) // All the possible values set for MaxActiveFlows from the various points that can provide it: // 1. Initialized value // 2. Local CLI flag config // 3. Remote configuration value initValue := uint64(0) localValue := uint64(100) remoteValue := uint64(500) initConfig := &Config{ Ingress: &ingress.Ingress{}, WarpRouting: ingress.WarpRoutingConfig{ MaxActiveFlows: initValue, }, OriginDialerService: originDialer, ConfigurationFlags: map[string]string{ flags.MaxActiveFlows: fmt.Sprintf("%d", localValue), }, } // We expect the local configuration flag to be the starting value orchestrator, err := NewOrchestrator(ctx, initConfig, testTags, []ingress.Rule{}, &testLogger) require.NoError(t, err) assertMaxActiveFlows(orchestrator, localValue) // Assigning the MaxActiveFlows in the remote config should be ignored over the local config remoteWarpConfig := ingress.WarpRoutingConfig{ MaxActiveFlows: remoteValue, } // Force a configuration refresh err = orchestrator.updateIngress(ingress.Ingress{}, remoteWarpConfig) require.NoError(t, err) // Check the value being used is the local one assertMaxActiveFlows(orchestrator, localValue) } func proxyHTTP(originProxy connection.OriginProxy, hostname string) (*http.Response, error) { req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://%s", hostname), nil) if err != nil { return nil, err } w := httptest.NewRecorder() log := zerolog.Nop() respWriter, err := connection.NewHTTP2RespWriter(req, w, connection.TypeHTTP, &log) if err != nil { return nil, err } err = originProxy.ProxyHTTP(respWriter, tracing.NewTracedHTTPRequest(req, 0, &log), false) if err != nil { return nil, err } return w.Result(), nil } // nolint: testifylint // this is used inside go routines so it can't use `require.` func tcpEyeball(t *testing.T, reqWriter io.WriteCloser, body string, respReadWriter *respReadWriteFlusher) { writeN, err := reqWriter.Write([]byte(body)) assert.NoError(t, err) readBuffer := make([]byte, writeN) n, err := respReadWriter.Read(readBuffer) assert.NoError(t, err) assert.Equal(t, body, string(readBuffer[:n])) assert.Equal(t, writeN, n) } func proxyTCP(ctx context.Context, originProxy connection.OriginProxy, originAddr string, w http.ResponseWriter, reqBody io.ReadCloser) error { req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://%s", originAddr), reqBody) if err != nil { return err } log := zerolog.Nop() respWriter, err := connection.NewHTTP2RespWriter(req, w, connection.TypeTCP, &log) if err != nil { return err } tcpReq := &connection.TCPRequest{ Dest: originAddr, CFRay: "123", LBProbe: false, } rws := connection.NewHTTPResponseReadWriterAcker(respWriter, w.(http.Flusher), req) return originProxy.ProxyTCP(ctx, rws, tcpReq) } func serveTCPOrigin(t *testing.T, tcpOrigin net.Listener, wg *sync.WaitGroup) { for { conn, err := tcpOrigin.Accept() if err != nil { return } wg.Add(1) go func() { defer wg.Done() defer conn.Close() echoTCP(t, conn) }() } } // nolint: testifylint // this is used inside go routines so it can't use `require.` func echoTCP(t *testing.T, conn net.Conn) { readBuf := make([]byte, 1000) readN, err := conn.Read(readBuf) assert.NoError(t, err) writeN, err := conn.Write(readBuf[:readN]) assert.NoError(t, err) assert.Equal(t, readN, writeN) } type validateHostHandler struct { expectedHost string body string } func (vhh *validateHostHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Host != vhh.expectedHost { w.WriteHeader(http.StatusBadRequest) return } w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(vhh.body)) } // nolint: testifylint // this is used inside go routines so it can't use `require.` func updateWithValidation(t *testing.T, orchestrator *Orchestrator, version int32, config []byte) { resp := orchestrator.UpdateConfig(version, config) assert.NoError(t, resp.Err) assert.Equal(t, version, resp.LastAppliedVersion) } // TestClosePreviousProxies makes sure proxies started in the previous configuration version are shutdown func TestClosePreviousProxies(t *testing.T) { originDialer := ingress.NewOriginDialer(ingress.OriginConfig{ DefaultDialer: testDefaultDialer, TCPWriteTimeout: 1 * time.Second, }, &testLogger) var ( hostname = "hello.tunnel1.org" configWithHelloWorld = []byte(fmt.Sprintf(` { "ingress": [ { "hostname": "%s", "service": "hello-world" }, { "service": "http_status:404" } ], "warp-routing": { } } `, hostname)) configTeapot = []byte(` { "ingress": [ { "service": "http_status:418" } ], "warp-routing": { } } `) initConfig = &Config{ Ingress: &ingress.Ingress{}, OriginDialerService: originDialer, } ) ctx, cancel := context.WithCancel(t.Context()) orchestrator, err := NewOrchestrator(ctx, initConfig, testTags, []ingress.Rule{}, &testLogger) require.NoError(t, err) updateWithValidation(t, orchestrator, 1, configWithHelloWorld) originProxyV1, err := orchestrator.GetOriginProxy() require.NoError(t, err) // nolint: bodyclose resp, err := proxyHTTP(originProxyV1, hostname) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.StatusCode) updateWithValidation(t, orchestrator, 2, configTeapot) originProxyV2, err := orchestrator.GetOriginProxy() require.NoError(t, err) // nolint: bodyclose resp, err = proxyHTTP(originProxyV2, hostname) require.NoError(t, err) require.Equal(t, http.StatusTeapot, resp.StatusCode) // The hello-world server in config v1 should have been stopped. We wait a bit since it's closed asynchronously. time.Sleep(time.Millisecond * 10) // nolint: bodyclose resp, err = proxyHTTP(originProxyV1, hostname) require.Error(t, err) require.Nil(t, resp) // Apply the config with hello world server again, orchestrator should spin up another hello world server updateWithValidation(t, orchestrator, 3, configWithHelloWorld) originProxyV3, err := orchestrator.GetOriginProxy() require.NoError(t, err) require.NotEqual(t, originProxyV1, originProxyV3) // nolint: bodyclose resp, err = proxyHTTP(originProxyV3, hostname) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.StatusCode) // cancel the context should terminate the last proxy cancel() // Wait for proxies to shutdown time.Sleep(time.Millisecond * 10) // nolint: bodyclose resp, err = proxyHTTP(originProxyV3, hostname) require.Error(t, err) require.Nil(t, resp) } // TestPersistentConnection makes sure updating the ingress doesn't intefere with existing connections func TestPersistentConnection(t *testing.T) { const ( hostname = "http://ws.tunnel.org" ) msg := t.Name() originDialer := ingress.NewOriginDialer(ingress.OriginConfig{ DefaultDialer: testDefaultDialer, TCPWriteTimeout: 1 * time.Second, }, &testLogger) initConfig := &Config{ Ingress: &ingress.Ingress{}, OriginDialerService: originDialer, } orchestrator, err := NewOrchestrator(t.Context(), initConfig, testTags, []ingress.Rule{}, &testLogger) require.NoError(t, err) wsOrigin := httptest.NewServer(http.HandlerFunc(wsEcho)) defer wsOrigin.Close() tcpOrigin, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err) defer tcpOrigin.Close() configWithWSAndWarp := []byte(fmt.Sprintf(` { "ingress": [ { "service": "%s" } ], "warp-routing": { } } `, wsOrigin.URL)) updateWithValidation(t, orchestrator, 1, configWithWSAndWarp) originProxy, err := orchestrator.GetOriginProxy() require.NoError(t, err) wsReqReader, wsReqWriter := io.Pipe() wsRespReadWriter := newRespReadWriteFlusher() tcpReqReader, tcpReqWriter := io.Pipe() tcpRespReadWriter := newRespReadWriteFlusher() ctx, cancel := context.WithCancel(t.Context()) defer cancel() var wg sync.WaitGroup wg.Add(3) // Start TCP origin go func() { defer wg.Done() conn, err := tcpOrigin.Accept() assert.NoError(t, err) defer conn.Close() // Expect 3 TCP messages for i := 0; i < 3; i++ { echoTCP(t, conn) } }() // Simulate cloudflared receiving a TCP connection go func() { defer wg.Done() assert.NoError(t, proxyTCP(ctx, originProxy, tcpOrigin.Addr().String(), tcpRespReadWriter, tcpReqReader)) }() // Simulate cloudflared receiving a WS connection go func() { defer wg.Done() req, err := http.NewRequest(http.MethodGet, hostname, wsReqReader) assert.NoError(t, err) // ProxyHTTP will add Connection, Upgrade and Sec-Websocket-Version headers req.Header.Add("Sec-WebSocket-Key", "dGhlIHNhbXBsZSBub25jZQ==") log := zerolog.Nop() respWriter, err := connection.NewHTTP2RespWriter(req, wsRespReadWriter, connection.TypeWebsocket, &log) assert.NoError(t, err) err = originProxy.ProxyHTTP(respWriter, tracing.NewTracedHTTPRequest(req, 0, &log), true) assert.NoError(t, err) }() // Simulate eyeball WS and TCP connections validateWsEcho(t, msg, wsReqWriter, wsRespReadWriter) tcpEyeball(t, tcpReqWriter, msg, tcpRespReadWriter) configNoWSAndWarp := []byte(` { "ingress": [ { "service": "http_status:404" } ], "warp-routing": { } } `) updateWithValidation(t, orchestrator, 2, configNoWSAndWarp) // Make sure connection is still up validateWsEcho(t, msg, wsReqWriter, wsRespReadWriter) tcpEyeball(t, tcpReqWriter, msg, tcpRespReadWriter) updateWithValidation(t, orchestrator, 3, configWithWSAndWarp) // Make sure connection is still up validateWsEcho(t, msg, wsReqWriter, wsRespReadWriter) tcpEyeball(t, tcpReqWriter, msg, tcpRespReadWriter) wsReqWriter.Close() tcpReqWriter.Close() wg.Wait() } func TestSerializeLocalConfig(t *testing.T) { c := &newLocalConfig{ RemoteConfig: ingress.RemoteConfig{ Ingress: ingress.Ingress{}, }, ConfigurationFlags: map[string]string{"a": "b"}, } result, err := json.Marshal(c) require.NoError(t, err) require.JSONEq(t, `{"__configuration_flags":{"a":"b"},"ingress":[],"warp-routing":{"connectTimeout":0,"tcpKeepAlive":0}}`, string(result)) } func wsEcho(w http.ResponseWriter, r *http.Request) { upgrader := gows.Upgrader{} conn, err := upgrader.Upgrade(w, r, nil) if err != nil { return } defer conn.Close() for { mt, message, err := conn.ReadMessage() if err != nil { fmt.Println("read message err", err) break } err = conn.WriteMessage(mt, message) if err != nil { fmt.Println("write message err", err) break } } } func validateWsEcho(t *testing.T, msg string, reqWriter io.Writer, respReadWriter io.ReadWriter) { err := wsutil.WriteClientText(reqWriter, []byte(msg)) require.NoError(t, err) receivedMsg, err := wsutil.ReadServerText(respReadWriter) require.NoError(t, err) require.Equal(t, msg, string(receivedMsg)) } type respReadWriteFlusher struct { io.Reader w io.Writer headers http.Header statusCode int setStatusOnce sync.Once hasStatus chan struct{} } func newRespReadWriteFlusher() *respReadWriteFlusher { pr, pw := io.Pipe() return &respReadWriteFlusher{ Reader: pr, w: pw, headers: make(http.Header), hasStatus: make(chan struct{}), } } func (rrw *respReadWriteFlusher) Write(buf []byte) (int, error) { rrw.WriteHeader(http.StatusOK) return rrw.w.Write(buf) } func (rrw *respReadWriteFlusher) Flush() {} func (rrw *respReadWriteFlusher) Header() http.Header { return rrw.headers } func (rrw *respReadWriteFlusher) WriteHeader(statusCode int) { rrw.setStatusOnce.Do(func() { rrw.statusCode = statusCode close(rrw.hasStatus) }) } ================================================ FILE: overwatch/app_manager.go ================================================ package overwatch // ServiceCallback is a service notify it's runloop finished. // the first parameter is the service type // the second parameter is the service name // the third parameter is an optional error if the service failed type ServiceCallback func(string, string, error) // AppManager is the default implementation of overwatch service management type AppManager struct { services map[string]Service callback ServiceCallback } // NewAppManager creates a new overwatch manager func NewAppManager(callback ServiceCallback) Manager { return &AppManager{services: make(map[string]Service), callback: callback} } // Add takes in a new service to manage. // It stops the service if it already exist in the manager and is running // It then starts the newly added service func (m *AppManager) Add(service Service) { // check for existing service if currentService, ok := m.services[service.Name()]; ok { if currentService.Hash() == service.Hash() { return // the exact same service, no changes, so move along } currentService.Shutdown() //shutdown the listener since a new one is starting } m.services[service.Name()] = service //start the service! go m.serviceRun(service) } // Remove shutdowns the service by name and removes it from its current management list func (m *AppManager) Remove(name string) { if currentService, ok := m.services[name]; ok { currentService.Shutdown() } delete(m.services, name) } // Services returns all the current Services being managed func (m *AppManager) Services() []Service { values := []Service{} for _, value := range m.services { values = append(values, value) } return values } func (m *AppManager) serviceRun(service Service) { err := service.Run() if m.callback != nil { m.callback(service.Type(), service.Name(), err) } } ================================================ FILE: overwatch/manager.go ================================================ package overwatch // Service is the required functions for an object to be managed by the overwatch Manager type Service interface { Name() string Type() string Hash() string Shutdown() Run() error } // Manager is based type to manage running services type Manager interface { Add(Service) Remove(string) Services() []Service } ================================================ FILE: overwatch/manager_test.go ================================================ package overwatch import ( "crypto/md5" "errors" "fmt" "io" "testing" "github.com/stretchr/testify/assert" ) type mockService struct { serviceName string serviceType string runError error } func (s *mockService) Name() string { return s.serviceName } func (s *mockService) Type() string { return s.serviceType } func (s *mockService) Hash() string { h := md5.New() io.WriteString(h, s.serviceName) io.WriteString(h, s.serviceType) return fmt.Sprintf("%x", h.Sum(nil)) } func (s *mockService) Shutdown() { } func (s *mockService) Run() error { return s.runError } func TestManagerAddAndRemove(t *testing.T) { m := NewAppManager(nil) first := &mockService{serviceName: "first", serviceType: "mock"} second := &mockService{serviceName: "second", serviceType: "mock"} m.Add(first) m.Add(second) assert.Len(t, m.Services(), 2, "expected 2 services in the list") m.Remove(first.Name()) services := m.Services() assert.Len(t, services, 1, "expected 1 service in the list") assert.Equal(t, second.Hash(), services[0].Hash(), "hashes should match. Wrong service was removed") } func TestManagerDuplicate(t *testing.T) { m := NewAppManager(nil) first := &mockService{serviceName: "first", serviceType: "mock"} m.Add(first) m.Add(first) assert.Len(t, m.Services(), 1, "expected 1 service in the list") } func TestManagerErrorChannel(t *testing.T) { errChan := make(chan error) serviceCallback := func(t string, name string, err error) { errChan <- err } m := NewAppManager(serviceCallback) err := errors.New("test error") first := &mockService{serviceName: "first", serviceType: "mock", runError: err} m.Add(first) respErr := <-errChan assert.Equal(t, err, respErr, "errors don't match") } ================================================ FILE: packet/decoder.go ================================================ package packet import ( "fmt" "github.com/google/gopacket" "github.com/google/gopacket/layers" "github.com/pkg/errors" "golang.org/x/net/icmp" ) func FindProtocol(p []byte) (layers.IPProtocol, error) { version, err := FindIPVersion(p) if err != nil { return 0, err } switch version { case 4: if len(p) < ipv4MinHeaderLen { return 0, fmt.Errorf("IPv4 packet should have at least %d bytes, got %d bytes", ipv4MinHeaderLen, len(p)) } // Protocol is in the 10th byte of IPv4 header return layers.IPProtocol(p[9]), nil case 6: if len(p) < ipv6HeaderLen { return 0, fmt.Errorf("IPv6 packet should have at least %d bytes, got %d bytes", ipv6HeaderLen, len(p)) } // Next header is in the 7th byte of IPv6 header return layers.IPProtocol(p[6]), nil default: return 0, fmt.Errorf("unknown ip version %d", version) } } func FindIPVersion(p []byte) (uint8, error) { if len(p) == 0 { return 0, fmt.Errorf("packet length is 0") } return p[0] >> 4, nil } // IPDecoder decodes raw packets into IP. It can process packets sequentially without allocating // memory for the layers, so it cannot be called concurrently. type IPDecoder struct { ipv4 *layers.IPv4 ipv6 *layers.IPv6 layers uint8 v4parser *gopacket.DecodingLayerParser v6parser *gopacket.DecodingLayerParser } func NewIPDecoder() *IPDecoder { var ( ipv4 layers.IPv4 ipv6 layers.IPv6 ) dlpv4 := gopacket.NewDecodingLayerParser(layers.LayerTypeIPv4) dlpv4.SetDecodingLayerContainer(gopacket.DecodingLayerSparse(nil)) dlpv4.AddDecodingLayer(&ipv4) // Stop parsing when it encounter a layer that it doesn't have a parser dlpv4.IgnoreUnsupported = true dlpv6 := gopacket.NewDecodingLayerParser(layers.LayerTypeIPv6) dlpv6.SetDecodingLayerContainer(gopacket.DecodingLayerSparse(nil)) dlpv6.AddDecodingLayer(&ipv6) dlpv6.IgnoreUnsupported = true return &IPDecoder{ ipv4: &ipv4, ipv6: &ipv6, layers: 1, v4parser: dlpv4, v6parser: dlpv6, } } func (pd *IPDecoder) Decode(packet RawPacket) (*IP, error) { // Should decode to IP layer decoded, err := pd.decodeByVersion(packet.Data) if err != nil { return nil, err } for _, layerType := range decoded { switch layerType { case layers.LayerTypeIPv4: return newIPv4(pd.ipv4) case layers.LayerTypeIPv6: return newIPv6(pd.ipv6) } } return nil, fmt.Errorf("no ip layer is decoded") } func (pd *IPDecoder) decodeByVersion(packet []byte) ([]gopacket.LayerType, error) { version, err := FindIPVersion(packet) if err != nil { return nil, err } decoded := make([]gopacket.LayerType, 0, pd.layers) switch version { case 4: err = pd.v4parser.DecodeLayers(packet, &decoded) case 6: err = pd.v6parser.DecodeLayers(packet, &decoded) default: err = fmt.Errorf("unknown ip version %d", version) } if err != nil { return nil, err } return decoded, nil } // ICMPDecoder decodes raw packets into IP and ICMP. It can process packets sequentially without allocating // memory for the layers, so it cannot be called concurrently. type ICMPDecoder struct { *IPDecoder icmpv4 *layers.ICMPv4 icmpv6 *layers.ICMPv6 } func NewICMPDecoder() *ICMPDecoder { ipDecoder := NewIPDecoder() var ( icmpv4 layers.ICMPv4 icmpv6 layers.ICMPv6 ) ipDecoder.layers++ ipDecoder.v4parser.AddDecodingLayer(&icmpv4) ipDecoder.v6parser.AddDecodingLayer(&icmpv6) return &ICMPDecoder{ IPDecoder: ipDecoder, icmpv4: &icmpv4, icmpv6: &icmpv6, } } func (pd *ICMPDecoder) Decode(packet RawPacket) (*ICMP, error) { // Should decode to IP and optionally ICMP layer decoded, err := pd.decodeByVersion(packet.Data) if err != nil { return nil, err } for _, layerType := range decoded { switch layerType { case layers.LayerTypeICMPv4: ipv4, err := newIPv4(pd.ipv4) if err != nil { return nil, err } msg, err := icmp.ParseMessage(int(layers.IPProtocolICMPv4), append(pd.icmpv4.Contents, pd.icmpv4.Payload...)) if err != nil { return nil, errors.Wrap(err, "failed to parse ICMPv4 message") } return &ICMP{ IP: ipv4, Message: msg, }, nil case layers.LayerTypeICMPv6: ipv6, err := newIPv6(pd.ipv6) if err != nil { return nil, err } msg, err := icmp.ParseMessage(int(layers.IPProtocolICMPv6), append(pd.icmpv6.Contents, pd.icmpv6.Payload...)) if err != nil { return nil, errors.Wrap(err, "failed to parse ICMPv6") } return &ICMP{ IP: ipv6, Message: msg, }, nil } } layers := make([]string, len(decoded)) for i, l := range decoded { layers[i] = l.String() } return nil, fmt.Errorf("Expect to decode IP and ICMP layers, got %s", layers) } ================================================ FILE: packet/decoder_test.go ================================================ package packet import ( "net" "net/netip" "testing" "github.com/google/gopacket" "github.com/google/gopacket/layers" "github.com/stretchr/testify/require" "golang.org/x/net/icmp" "golang.org/x/net/ipv4" "golang.org/x/net/ipv6" ) func TestDecodeIP(t *testing.T) { ipDecoder := NewIPDecoder() icmpDecoder := NewICMPDecoder() udps := []UDP{ { IP: IP{ Src: netip.MustParseAddr("172.16.0.1"), Dst: netip.MustParseAddr("10.0.0.1"), Protocol: layers.IPProtocolUDP, }, SrcPort: 31678, DstPort: 53, }, { IP: IP{ Src: netip.MustParseAddr("fd51:2391:523:f4ee::1"), Dst: netip.MustParseAddr("fd51:2391:697:f4ee::2"), Protocol: layers.IPProtocolUDP, }, SrcPort: 52139, DstPort: 1053, }, } encoder := NewEncoder() for _, udp := range udps { p, err := encoder.Encode(&udp) require.NoError(t, err) ipPacket, err := ipDecoder.Decode(p) require.NoError(t, err) assertIPLayer(t, &udp.IP, ipPacket) icmpPacket, err := icmpDecoder.Decode(p) require.Error(t, err) require.Nil(t, icmpPacket) } } func TestDecodeICMP(t *testing.T) { ipDecoder := NewIPDecoder() icmpDecoder := NewICMPDecoder() var ( ipv4Packet = IP{ Src: netip.MustParseAddr("172.16.0.1"), Dst: netip.MustParseAddr("10.0.0.1"), Protocol: layers.IPProtocolICMPv4, TTL: DefaultTTL, } ipv6Packet = IP{ Src: netip.MustParseAddr("fd51:2391:523:f4ee::1"), Dst: netip.MustParseAddr("fd51:2391:697:f4ee::2"), Protocol: layers.IPProtocolICMPv6, TTL: DefaultTTL, } icmpID = 100 icmpSeq = 52819 ) tests := []struct { testCase string packet *ICMP }{ { testCase: "icmpv4 time exceed", packet: &ICMP{ IP: &ipv4Packet, Message: &icmp.Message{ Type: ipv4.ICMPTypeTimeExceeded, Code: 0, Body: &icmp.TimeExceeded{ Data: []byte("original packet"), }, }, }, }, { testCase: "icmpv4 echo", packet: &ICMP{ IP: &ipv4Packet, Message: &icmp.Message{ Type: ipv4.ICMPTypeEcho, Code: 0, Body: &icmp.Echo{ ID: icmpID, Seq: icmpSeq, Data: []byte("icmpv4 echo"), }, }, }, }, { testCase: "icmpv6 destination unreachable", packet: &ICMP{ IP: &ipv6Packet, Message: &icmp.Message{ Type: ipv6.ICMPTypeDestinationUnreachable, Code: 4, Body: &icmp.DstUnreach{ Data: []byte("original packet"), }, }, }, }, { testCase: "icmpv6 echo", packet: &ICMP{ IP: &ipv6Packet, Message: &icmp.Message{ Type: ipv6.ICMPTypeEchoRequest, Code: 0, Body: &icmp.Echo{ ID: icmpID, Seq: icmpSeq, Data: []byte("icmpv6 echo"), }, }, }, }, } encoder := NewEncoder() for _, test := range tests { p, err := encoder.Encode(test.packet) require.NoError(t, err) ipPacket, err := ipDecoder.Decode(p) require.NoError(t, err) if ipPacket.Src.Is4() { assertIPLayer(t, &ipv4Packet, ipPacket) } else { assertIPLayer(t, &ipv6Packet, ipPacket) } icmpPacket, err := icmpDecoder.Decode(p) require.NoError(t, err) require.Equal(t, ipPacket, icmpPacket.IP) require.Equal(t, test.packet.Type, icmpPacket.Type) require.Equal(t, test.packet.Code, icmpPacket.Code) assertICMPChecksum(t, icmpPacket) require.Equal(t, test.packet.Body, icmpPacket.Body) expectedBody, err := test.packet.Body.Marshal(test.packet.Type.Protocol()) require.NoError(t, err) decodedBody, err := icmpPacket.Body.Marshal(test.packet.Type.Protocol()) require.NoError(t, err) require.Equal(t, expectedBody, decodedBody) } } // TestDecodeBadPackets makes sure decoders don't decode invalid packets func TestDecodeBadPackets(t *testing.T) { var ( srcIPv4 = net.ParseIP("172.16.0.1") dstIPv4 = net.ParseIP("10.0.0.1") ) ipLayer := layers.IPv4{ Version: 10, SrcIP: srcIPv4, DstIP: dstIPv4, Protocol: layers.IPProtocolICMPv4, TTL: DefaultTTL, } icmpLayer := layers.ICMPv4{ TypeCode: layers.CreateICMPv4TypeCode(uint8(ipv4.ICMPTypeEcho), 0), Id: 100, Seq: 52819, } wrongIPVersion, err := createPacket(&ipLayer, &icmpLayer, nil, nil) require.NoError(t, err) tests := []struct { testCase string packet []byte }{ { testCase: "unknown IP version", packet: wrongIPVersion, }, { testCase: "invalid packet", packet: []byte("not a packet"), }, { testCase: "zero length packet", packet: []byte{}, }, } ipDecoder := NewIPDecoder() icmpDecoder := NewICMPDecoder() for _, test := range tests { ipPacket, err := ipDecoder.Decode(RawPacket{Data: test.packet}) require.Error(t, err) require.Nil(t, ipPacket) icmpPacket, err := icmpDecoder.Decode(RawPacket{Data: test.packet}) require.Error(t, err) require.Nil(t, icmpPacket) } } func createPacket(ipLayer, secondLayer, thirdLayer gopacket.SerializableLayer, body []byte) ([]byte, error) { payload := gopacket.Payload(body) packet := gopacket.NewSerializeBuffer() var err error if thirdLayer != nil { err = gopacket.SerializeLayers(packet, serializeOpts, ipLayer, secondLayer, thirdLayer, payload) } else { err = gopacket.SerializeLayers(packet, serializeOpts, ipLayer, secondLayer, payload) } if err != nil { return nil, err } return packet.Bytes(), nil } func assertIPLayer(t *testing.T, expected, actual *IP) { require.Equal(t, expected.Src, actual.Src) require.Equal(t, expected.Dst, actual.Dst) require.Equal(t, expected.Protocol, actual.Protocol) require.Equal(t, expected.TTL, actual.TTL) } type UDP struct { IP SrcPort, DstPort layers.UDPPort } func (u *UDP) EncodeLayers() ([]gopacket.SerializableLayer, error) { ipLayers, err := u.IP.EncodeLayers() if err != nil { return nil, err } udpLayer := layers.UDP{ SrcPort: u.SrcPort, DstPort: u.DstPort, } udpLayer.SetNetworkLayerForChecksum(ipLayers[0].(gopacket.NetworkLayer)) return append(ipLayers, &udpLayer), nil } func FuzzIPDecoder(f *testing.F) { f.Fuzz(func(t *testing.T, data []byte) { ipDecoder := NewIPDecoder() ipDecoder.Decode(RawPacket{Data: data}) }) } func FuzzICMPDecoder(f *testing.F) { f.Fuzz(func(t *testing.T, data []byte) { icmpDecoder := NewICMPDecoder() icmpDecoder.Decode(RawPacket{Data: data}) }) } ================================================ FILE: packet/encoder.go ================================================ package packet import ( "github.com/google/gopacket" ) var ( serializeOpts = gopacket.SerializeOptions{ FixLengths: true, ComputeChecksums: true, } ) // RawPacket represents a raw packet or one encoded by Encoder type RawPacket struct { Data []byte } type Encoder struct { // buf is reusable because SerializeLayers calls the Clear method before each encoding buf gopacket.SerializeBuffer } func NewEncoder() *Encoder { return &Encoder{ buf: gopacket.NewSerializeBuffer(), } } func (e *Encoder) Encode(packet Packet) (RawPacket, error) { encodedLayers, err := packet.EncodeLayers() if err != nil { return RawPacket{}, err } if err := gopacket.SerializeLayers(e.buf, serializeOpts, encodedLayers...); err != nil { return RawPacket{}, err } return RawPacket{ Data: e.buf.Bytes(), }, nil } ================================================ FILE: packet/funnel.go ================================================ package packet import ( "context" "errors" "fmt" "net/netip" "sync" "sync/atomic" "time" ) var ( ErrFunnelNotFound = errors.New("funnel not found") ) // Funnel is an abstraction to pipe from 1 src to 1 or more destinations type Funnel interface { // Updates the last time traffic went through this funnel UpdateLastActive() // LastActive returns the last time there is traffic through this funnel LastActive() time.Time // Close closes the funnel. Further call to SendToDst or ReturnToSrc should return an error Close() error // Equal compares if 2 funnels are equivalent Equal(other Funnel) bool } // FunnelUniPipe is a unidirectional pipe for sending raw packets type FunnelUniPipe interface { // SendPacket sends a packet to/from the funnel. It must not modify the packet, // and after return it must not read the packet SendPacket(dst netip.Addr, pk RawPacket) error Close() error } type ActivityTracker struct { // last active unix time. Unit is seconds lastActive int64 } func NewActivityTracker() *ActivityTracker { return &ActivityTracker{ lastActive: time.Now().Unix(), } } func (at *ActivityTracker) UpdateLastActive() { atomic.StoreInt64(&at.lastActive, time.Now().Unix()) } func (at *ActivityTracker) LastActive() time.Time { lastActive := atomic.LoadInt64(&at.lastActive) return time.Unix(lastActive, 0) } // FunnelID represents a key type that can be used by FunnelTracker type FunnelID interface { // Type returns the name of the type that implements the FunnelID Type() string fmt.Stringer } // FunnelTracker tracks funnel from the perspective of eyeball to origin type FunnelTracker struct { lock sync.RWMutex funnels map[FunnelID]Funnel } func NewFunnelTracker() *FunnelTracker { return &FunnelTracker{ funnels: make(map[FunnelID]Funnel), } } func (ft *FunnelTracker) ScheduleCleanup(ctx context.Context, idleTimeout time.Duration) { checkIdleTicker := time.NewTicker(idleTimeout) defer checkIdleTicker.Stop() for { select { case <-ctx.Done(): return case <-checkIdleTicker.C: ft.cleanup(idleTimeout) } } } func (ft *FunnelTracker) cleanup(idleTimeout time.Duration) { ft.lock.Lock() defer ft.lock.Unlock() now := time.Now() for id, funnel := range ft.funnels { lastActive := funnel.LastActive() if now.After(lastActive.Add(idleTimeout)) { funnel.Close() delete(ft.funnels, id) } } } func (ft *FunnelTracker) Get(id FunnelID) (Funnel, bool) { ft.lock.RLock() defer ft.lock.RUnlock() funnel, ok := ft.funnels[id] return funnel, ok } // Registers a funnel. If the `id` is already registered and `shouldReplaceFunc` returns true, it closes and replaces // the current funnel. If `newFunnelFunc` returns an error, the `id` will remain unregistered, even if it was registered // when calling this function. func (ft *FunnelTracker) GetOrRegister( id FunnelID, shouldReplaceFunc func(Funnel) bool, newFunnelFunc func() (Funnel, error), ) (funnel Funnel, new bool, err error) { ft.lock.Lock() defer ft.lock.Unlock() currentFunnel, exists := ft.funnels[id] if exists { if !shouldReplaceFunc(currentFunnel) { return currentFunnel, false, nil } currentFunnel.Close() delete(ft.funnels, id) } newFunnel, err := newFunnelFunc() if err != nil { return nil, false, err } ft.funnels[id] = newFunnel return newFunnel, true, nil } // Unregisters and closes a funnel if the funnel equals to the current funnel func (ft *FunnelTracker) Unregister(id FunnelID, funnel Funnel) (deleted bool) { ft.lock.Lock() defer ft.lock.Unlock() currentFunnel, exists := ft.funnels[id] if !exists { return true } if currentFunnel.Equal(funnel) { currentFunnel.Close() delete(ft.funnels, id) return true } return false } ================================================ FILE: packet/funnel_test.go ================================================ package packet import ( "fmt" "net/netip" "testing" "time" "github.com/stretchr/testify/require" ) type mockFunnelUniPipe struct { uniPipe chan RawPacket } func (mfui *mockFunnelUniPipe) SendPacket(dst netip.Addr, pk RawPacket) error { mfui.uniPipe <- pk return nil } func (mfui *mockFunnelUniPipe) Close() error { return nil } func TestFunnelRegistration(t *testing.T) { id := testFunnelID{"id1"} funnelErr := fmt.Errorf("expected error") newFunnelFuncErr := func() (Funnel, error) { return nil, funnelErr } newFunnelFuncUncalled := func() (Funnel, error) { require.FailNow(t, "a new funnel should not be created") panic("unreached") } funnel1, newFunnelFunc1 := newFunnelAndFunc("funnel1") funnel2, newFunnelFunc2 := newFunnelAndFunc("funnel2") ft := NewFunnelTracker() // Register funnel1 funnel, new, err := ft.GetOrRegister(id, shouldReplaceFalse, newFunnelFunc1) require.NoError(t, err) require.True(t, new) require.Equal(t, funnel1, funnel) // Register funnel, no replace funnel, new, err = ft.GetOrRegister(id, shouldReplaceFalse, newFunnelFuncUncalled) require.NoError(t, err) require.False(t, new) require.Equal(t, funnel1, funnel) // Register funnel2, replace funnel, new, err = ft.GetOrRegister(id, shouldReplaceTrue, newFunnelFunc2) require.NoError(t, err) require.True(t, new) require.Equal(t, funnel2, funnel) require.True(t, funnel1.closed) // Register funnel error, replace funnel, new, err = ft.GetOrRegister(id, shouldReplaceTrue, newFunnelFuncErr) require.ErrorIs(t, err, funnelErr) require.False(t, new) require.Nil(t, funnel) require.True(t, funnel2.closed) } func TestFunnelUnregister(t *testing.T) { id := testFunnelID{"id1"} funnel1, newFunnelFunc1 := newFunnelAndFunc("funnel1") funnel2, newFunnelFunc2 := newFunnelAndFunc("funnel2") funnel3, newFunnelFunc3 := newFunnelAndFunc("funnel3") ft := NewFunnelTracker() // Register & unregister _, _, err := ft.GetOrRegister(id, shouldReplaceFalse, newFunnelFunc1) require.NoError(t, err) require.True(t, ft.Unregister(id, funnel1)) require.True(t, funnel1.closed) require.True(t, ft.Unregister(id, funnel1)) // Register, replace, and unregister _, _, err = ft.GetOrRegister(id, shouldReplaceFalse, newFunnelFunc2) require.NoError(t, err) _, _, err = ft.GetOrRegister(id, shouldReplaceTrue, newFunnelFunc3) require.NoError(t, err) require.True(t, funnel2.closed) require.False(t, ft.Unregister(id, funnel2)) require.True(t, ft.Unregister(id, funnel3)) require.True(t, funnel3.closed) } func shouldReplaceFalse(_ Funnel) bool { return false } func shouldReplaceTrue(_ Funnel) bool { return true } func newFunnelAndFunc(id string) (*testFunnel, func() (Funnel, error)) { funnel := newTestFunnel(id) funnelFunc := func() (Funnel, error) { return funnel, nil } return funnel, funnelFunc } type testFunnelID struct { id string } func (t testFunnelID) Type() string { return "testFunnelID" } func (t testFunnelID) String() string { return t.id } type testFunnel struct { id string closed bool } func newTestFunnel(id string) *testFunnel { return &testFunnel{ id, false, } } func (tf *testFunnel) Close() error { tf.closed = true return nil } func (tf *testFunnel) Equal(other Funnel) bool { return tf.id == other.(*testFunnel).id } func (tf *testFunnel) LastActive() time.Time { return time.Now() } func (tf *testFunnel) UpdateLastActive() {} ================================================ FILE: packet/packet.go ================================================ package packet import ( "encoding/binary" "fmt" "net/netip" "github.com/google/gopacket" "github.com/google/gopacket/layers" "golang.org/x/net/icmp" "golang.org/x/net/ipv4" "golang.org/x/net/ipv6" ) const ( ipv4MinHeaderLen = 20 ipv6HeaderLen = 40 ipv4MinMTU = 576 ipv6MinMTU = 1280 icmpHeaderLen = 8 // https://www.rfc-editor.org/rfc/rfc792 and https://datatracker.ietf.org/doc/html/rfc4443#section-3.3 define 2 codes. // 0 = ttl exceed in transit, 1 = fragment reassembly time exceeded icmpTTLExceedInTransitCode = 0 DefaultTTL uint8 = 255 pseudoHeaderLen = 40 ) // Packet represents an IP packet or a packet that is encapsulated by IP type Packet interface { // IPLayer returns the IP of the packet IPLayer() *IP // EncodeLayers returns the layers that make up this packet. They can be passed to an Encoder to serialize into RawPacket EncodeLayers() ([]gopacket.SerializableLayer, error) } // IP represents a generic IP packet. It can be embedded in more specific IP protocols type IP struct { Src netip.Addr Dst netip.Addr Protocol layers.IPProtocol TTL uint8 } func newIPv4(ipLayer *layers.IPv4) (*IP, error) { src, ok := netip.AddrFromSlice(ipLayer.SrcIP) if !ok { return nil, fmt.Errorf("cannot convert source IP %s to netip.Addr", ipLayer.SrcIP) } dst, ok := netip.AddrFromSlice(ipLayer.DstIP) if !ok { return nil, fmt.Errorf("cannot convert source IP %s to netip.Addr", ipLayer.DstIP) } return &IP{ Src: src, Dst: dst, Protocol: ipLayer.Protocol, TTL: ipLayer.TTL, }, nil } func newIPv6(ipLayer *layers.IPv6) (*IP, error) { src, ok := netip.AddrFromSlice(ipLayer.SrcIP) if !ok { return nil, fmt.Errorf("cannot convert source IP %s to netip.Addr", ipLayer.SrcIP) } dst, ok := netip.AddrFromSlice(ipLayer.DstIP) if !ok { return nil, fmt.Errorf("cannot convert source IP %s to netip.Addr", ipLayer.DstIP) } return &IP{ Src: src, Dst: dst, Protocol: ipLayer.NextHeader, TTL: ipLayer.HopLimit, }, nil } func (ip *IP) IPLayer() *IP { return ip } func (ip *IP) isIPv4() bool { return ip.Src.Is4() } func (ip *IP) EncodeLayers() ([]gopacket.SerializableLayer, error) { if ip.isIPv4() { return []gopacket.SerializableLayer{ &layers.IPv4{ Version: 4, SrcIP: ip.Src.AsSlice(), DstIP: ip.Dst.AsSlice(), Protocol: layers.IPProtocol(ip.Protocol), TTL: ip.TTL, }, }, nil } else { return []gopacket.SerializableLayer{ &layers.IPv6{ Version: 6, SrcIP: ip.Src.AsSlice(), DstIP: ip.Dst.AsSlice(), NextHeader: layers.IPProtocol(ip.Protocol), HopLimit: ip.TTL, }, }, nil } } // ICMP represents is an IP packet + ICMP message type ICMP struct { *IP *icmp.Message } func (i *ICMP) EncodeLayers() ([]gopacket.SerializableLayer, error) { ipLayers, err := i.IP.EncodeLayers() if err != nil { return nil, err } var serializedPsh []byte = nil if i.Protocol == layers.IPProtocolICMPv6 { psh := &PseudoHeader{ SrcIP: i.Src.As16(), DstIP: i.Dst.As16(), // i.Marshal re-calculates the UpperLayerPacketLength UpperLayerPacketLength: 0, NextHeader: uint8(i.Protocol), } serializedPsh = psh.Marshal() } msg, err := i.Marshal(serializedPsh) if err != nil { return nil, err } icmpLayer := gopacket.Payload(msg) return append(ipLayers, icmpLayer), nil } // https://www.rfc-editor.org/rfc/rfc2460#section-8.1 type PseudoHeader struct { SrcIP [16]byte DstIP [16]byte UpperLayerPacketLength uint32 zero [3]byte NextHeader uint8 } func (ph *PseudoHeader) Marshal() []byte { buf := make([]byte, pseudoHeaderLen) index := 0 copy(buf, ph.SrcIP[:]) index += 16 copy(buf[index:], ph.DstIP[:]) index += 16 binary.BigEndian.PutUint32(buf[index:], ph.UpperLayerPacketLength) index += 4 copy(buf[index:], ph.zero[:]) buf[pseudoHeaderLen-1] = ph.NextHeader return buf } func NewICMPTTLExceedPacket(originalIP *IP, originalPacket RawPacket, routerIP netip.Addr) *ICMP { var ( protocol layers.IPProtocol icmpType icmp.Type ) if originalIP.Dst.Is4() { protocol = layers.IPProtocolICMPv4 icmpType = ipv4.ICMPTypeTimeExceeded } else { protocol = layers.IPProtocolICMPv6 icmpType = ipv6.ICMPTypeTimeExceeded } return &ICMP{ IP: &IP{ Src: routerIP, Dst: originalIP.Src, Protocol: protocol, TTL: DefaultTTL, }, Message: &icmp.Message{ Type: icmpType, Code: icmpTTLExceedInTransitCode, Body: &icmp.TimeExceeded{ Data: originalDatagram(originalPacket, originalIP.Dst.Is4()), }, }, } } // originalDatagram returns a slice of the original datagram for ICMP error messages // https://www.rfc-editor.org/rfc/rfc1812#section-4.3.2.3 suggests to copy as much without exceeding 576 bytes. // https://datatracker.ietf.org/doc/html/rfc4443#section-3.3 suggests to copy as much without exceeding 1280 bytes func originalDatagram(originalPacket RawPacket, isIPv4 bool) []byte { var upperBound int if isIPv4 { upperBound = ipv4MinMTU - ipv4MinHeaderLen - icmpHeaderLen if upperBound > len(originalPacket.Data) { upperBound = len(originalPacket.Data) } } else { upperBound = ipv6MinMTU - ipv6HeaderLen - icmpHeaderLen if upperBound > len(originalPacket.Data) { upperBound = len(originalPacket.Data) } } return originalPacket.Data[:upperBound] } ================================================ FILE: packet/packet_test.go ================================================ package packet import ( "bytes" "net/netip" "testing" "github.com/google/gopacket" "github.com/google/gopacket/layers" "github.com/stretchr/testify/require" "golang.org/x/net/icmp" "golang.org/x/net/ipv4" "golang.org/x/net/ipv6" ) func TestNewICMPTTLExceedPacket(t *testing.T) { ipv4Packet := IP{ Src: netip.MustParseAddr("192.168.1.1"), Dst: netip.MustParseAddr("10.0.0.1"), Protocol: layers.IPProtocolICMPv4, TTL: 0, } icmpV4Packet := ICMP{ IP: &ipv4Packet, Message: &icmp.Message{ Type: ipv4.ICMPTypeEcho, Code: 0, Body: &icmp.Echo{ ID: 25821, Seq: 58129, Data: []byte("test ttl=0"), }, }, } assertTTLExceedPacket(t, &icmpV4Packet) icmpV4Packet.Body = &icmp.Echo{ ID: 3487, Seq: 19183, Data: make([]byte, ipv4MinMTU), } assertTTLExceedPacket(t, &icmpV4Packet) ipv6Packet := IP{ Src: netip.MustParseAddr("fd51:2391:523:f4ee::1"), Dst: netip.MustParseAddr("fd51:2391:697:f4ee::2"), Protocol: layers.IPProtocolICMPv6, TTL: 0, } icmpV6Packet := ICMP{ IP: &ipv6Packet, Message: &icmp.Message{ Type: ipv6.ICMPTypeEchoRequest, Code: 0, Body: &icmp.Echo{ ID: 25821, Seq: 58129, Data: []byte("test ttl=0"), }, }, } assertTTLExceedPacket(t, &icmpV6Packet) icmpV6Packet.Body = &icmp.Echo{ ID: 1497, Seq: 39284, Data: make([]byte, ipv6MinMTU), } assertTTLExceedPacket(t, &icmpV6Packet) } func assertTTLExceedPacket(t *testing.T, pk *ICMP) { encoder := NewEncoder() rawPacket, err := encoder.Encode(pk) require.NoError(t, err) minMTU := ipv4MinMTU headerLen := ipv4MinHeaderLen routerIP := netip.MustParseAddr("172.16.0.3") if pk.Dst.Is6() { minMTU = ipv6MinMTU headerLen = ipv6HeaderLen routerIP = netip.MustParseAddr("fd51:2391:697:f4ee::3") } ttlExceedPacket := NewICMPTTLExceedPacket(pk.IP, rawPacket, routerIP) require.Equal(t, routerIP, ttlExceedPacket.Src) require.Equal(t, pk.Src, ttlExceedPacket.Dst) require.Equal(t, pk.Protocol, ttlExceedPacket.Protocol) require.Equal(t, DefaultTTL, ttlExceedPacket.TTL) timeExceed, ok := ttlExceedPacket.Body.(*icmp.TimeExceeded) require.True(t, ok) if len(rawPacket.Data) > minMTU { require.True(t, bytes.Equal(timeExceed.Data, rawPacket.Data[:minMTU-headerLen-icmpHeaderLen])) } else { require.True(t, bytes.Equal(timeExceed.Data, rawPacket.Data)) } rawTTLExceedPacket, err := encoder.Encode(ttlExceedPacket) require.NoError(t, err) if len(rawPacket.Data) > minMTU { require.Len(t, rawTTLExceedPacket.Data, minMTU) } else { require.Len(t, rawTTLExceedPacket.Data, headerLen+icmpHeaderLen+len(rawPacket.Data)) require.True(t, bytes.Equal(rawPacket.Data, rawTTLExceedPacket.Data[headerLen+icmpHeaderLen:])) } decoder := NewICMPDecoder() decodedPacket, err := decoder.Decode(rawTTLExceedPacket) require.NoError(t, err) assertICMPChecksum(t, decodedPacket) } func assertICMPChecksum(t *testing.T, icmpPacket *ICMP) { buf := gopacket.NewSerializeBuffer() if icmpPacket.Protocol == layers.IPProtocolICMPv4 { icmpv4 := layers.ICMPv4{ TypeCode: layers.CreateICMPv4TypeCode(uint8(icmpPacket.Type.(ipv4.ICMPType)), uint8(icmpPacket.Code)), } switch body := icmpPacket.Body.(type) { case *icmp.Echo: icmpv4.Id = uint16(body.ID) icmpv4.Seq = uint16(body.Seq) payload := gopacket.Payload(body.Data) require.NoError(t, payload.SerializeTo(buf, serializeOpts)) default: require.NoError(t, serializeICMPAsPayload(icmpPacket.Message, buf)) } // SerializeTo sets the checksum in icmpv4 require.NoError(t, icmpv4.SerializeTo(buf, serializeOpts)) require.Equal(t, icmpv4.Checksum, uint16(icmpPacket.Checksum)) } else { switch body := icmpPacket.Body.(type) { case *icmp.Echo: payload := gopacket.Payload(body.Data) require.NoError(t, payload.SerializeTo(buf, serializeOpts)) echo := layers.ICMPv6Echo{ Identifier: uint16(body.ID), SeqNumber: uint16(body.Seq), } require.NoError(t, echo.SerializeTo(buf, serializeOpts)) default: require.NoError(t, serializeICMPAsPayload(icmpPacket.Message, buf)) } icmpv6 := layers.ICMPv6{ TypeCode: layers.CreateICMPv6TypeCode(uint8(icmpPacket.Type.(ipv6.ICMPType)), uint8(icmpPacket.Code)), } ipLayer := layers.IPv6{ Version: 6, SrcIP: icmpPacket.Src.AsSlice(), DstIP: icmpPacket.Dst.AsSlice(), NextHeader: icmpPacket.Protocol, HopLimit: icmpPacket.TTL, } require.NoError(t, icmpv6.SetNetworkLayerForChecksum(&ipLayer)) // SerializeTo sets the checksum in icmpv4 require.NoError(t, icmpv6.SerializeTo(buf, serializeOpts)) require.Equal(t, icmpv6.Checksum, uint16(icmpPacket.Checksum)) } } func serializeICMPAsPayload(message *icmp.Message, buf gopacket.SerializeBuffer) error { serializedBody, err := message.Body.Marshal(message.Type.Protocol()) if err != nil { return err } payload := gopacket.Payload(serializedBody) return payload.SerializeTo(buf, serializeOpts) } func TestChecksum(t *testing.T) { data := []byte{0x63, 0x2c, 0x49, 0xd6, 0x00, 0x0d, 0xc1, 0xda} pk := ICMP{ IP: &IP{ Src: netip.MustParseAddr("2606:4700:110:89c1:c63a:861:e08c:b049"), Dst: netip.MustParseAddr("fde8:b693:d420:109b::2"), Protocol: layers.IPProtocolICMPv6, TTL: 3, }, Message: &icmp.Message{ Type: ipv6.ICMPTypeEchoRequest, Code: 0, Body: &icmp.Echo{ ID: 0x20a7, Seq: 8, Data: data, }, }, } encoder := NewEncoder() encoded, err := encoder.Encode(&pk) require.NoError(t, err) decoder := NewICMPDecoder() decoded, err := decoder.Decode(encoded) require.Equal(t, 0xff96, decoded.Checksum) } ================================================ FILE: packet/session.go ================================================ package packet import "github.com/google/uuid" type Session struct { ID uuid.UUID Payload []byte } ================================================ FILE: postinst.sh ================================================ #!/bin/bash set -eu ln -sf /usr/bin/cloudflared /usr/local/bin/cloudflared mkdir -p /usr/local/etc/cloudflared/ touch /usr/local/etc/cloudflared/.installedFromPackageManager || true ================================================ FILE: postrm.sh ================================================ #!/bin/bash set -eu rm -f /usr/local/bin/cloudflared rm -f /usr/local/etc/cloudflared/.installedFromPackageManager ================================================ FILE: proxy/logger.go ================================================ package proxy import ( "net/http" "strconv" "github.com/rs/zerolog" "github.com/cloudflare/cloudflared/connection" "github.com/cloudflare/cloudflared/ingress" "github.com/cloudflare/cloudflared/management" ) const ( logFieldCFRay = "cfRay" logFieldLBProbe = "lbProbe" logFieldRule = "ingressRule" logFieldOriginService = "originService" logFieldConnIndex = "connIndex" logFieldDestAddr = "destAddr" ) var ( LogFieldFlowID = "flowID" ) // newHTTPLogger creates a child zerolog.Logger from the provided with added context from the HTTP request, ingress // services, and connection index. func newHTTPLogger(logger *zerolog.Logger, connIndex uint8, req *http.Request, rule int, serviceName string) zerolog.Logger { ctx := logger.With(). Int(management.EventTypeKey, int(management.HTTP)). Uint8(logFieldConnIndex, connIndex) cfRay := connection.FindCfRayHeader(req) lbProbe := connection.IsLBProbeRequest(req) if cfRay != "" { ctx.Str(logFieldCFRay, cfRay) } if lbProbe { ctx.Bool(logFieldLBProbe, lbProbe) } return ctx. Str(logFieldOriginService, serviceName). Interface(logFieldRule, rule). Logger() } // newTCPLogger creates a child zerolog.Logger from the provided with added context from the TCPRequest. func newTCPLogger(logger *zerolog.Logger, req *connection.TCPRequest) zerolog.Logger { return logger.With(). Int(management.EventTypeKey, int(management.TCP)). Uint8(logFieldConnIndex, req.ConnIndex). Str(logFieldOriginService, ingress.ServiceWarpRouting). Str(LogFieldFlowID, req.FlowID). Str(logFieldDestAddr, req.Dest). Uint8(logFieldConnIndex, req.ConnIndex). Logger() } // logHTTPRequest logs a Debug message with the corresponding HTTP request details from the eyeball. func logHTTPRequest(logger *zerolog.Logger, r *http.Request) { logger.Debug(). Str("host", r.Host). Str("path", r.URL.Path). Interface("headers", r.Header). Int64("content-length", r.ContentLength). Msgf("%s %s %s", r.Method, r.URL, r.Proto) } // logOriginHTTPResponse logs a Debug message of the origin response. func logOriginHTTPResponse(logger *zerolog.Logger, resp *http.Response) { responseByCode.WithLabelValues(strconv.Itoa(resp.StatusCode)).Inc() logger.Debug(). Int64("content-length", resp.ContentLength). Msgf("%s", resp.Status) } // logRequestError logs an error for the proxied request. func logRequestError(logger *zerolog.Logger, err error) { requestErrors.Inc() logger.Error().Err(err).Send() } ================================================ FILE: proxy/metrics.go ================================================ package proxy import ( "github.com/prometheus/client_golang/prometheus" "github.com/cloudflare/cloudflared/connection" ) // Metrics uses connection.MetricsNamespace(aka cloudflared) as namespace and connection.TunnelSubsystem // (tunnel) as subsystem to keep them consistent with the previous qualifier. var ( totalRequests = prometheus.NewCounter( prometheus.CounterOpts{ Namespace: connection.MetricsNamespace, Subsystem: connection.TunnelSubsystem, Name: "total_requests", Help: "Amount of requests proxied through all the tunnels", }, ) concurrentRequests = prometheus.NewGauge( prometheus.GaugeOpts{ Namespace: connection.MetricsNamespace, Subsystem: connection.TunnelSubsystem, Name: "concurrent_requests_per_tunnel", Help: "Concurrent requests proxied through each tunnel", }, ) responseByCode = prometheus.NewCounterVec( prometheus.CounterOpts{ Namespace: connection.MetricsNamespace, Subsystem: connection.TunnelSubsystem, Name: "response_by_code", Help: "Count of responses by HTTP status code", }, []string{"status_code"}, ) requestErrors = prometheus.NewCounter( prometheus.CounterOpts{ Namespace: connection.MetricsNamespace, Subsystem: connection.TunnelSubsystem, Name: "request_errors", Help: "Count of error proxying to origin", }, ) activeTCPSessions = prometheus.NewGauge( prometheus.GaugeOpts{ Namespace: connection.MetricsNamespace, Subsystem: "tcp", Name: "active_sessions", Help: "Concurrent count of TCP sessions that are being proxied to any origin", }, ) totalTCPSessions = prometheus.NewCounter( prometheus.CounterOpts{ Namespace: connection.MetricsNamespace, Subsystem: "tcp", Name: "total_sessions", Help: "Total count of TCP sessions that have been proxied to any origin", }, ) connectLatency = prometheus.NewHistogram( prometheus.HistogramOpts{ Namespace: connection.MetricsNamespace, Subsystem: "proxy", Name: "connect_latency", Help: "Time it takes to establish and acknowledge connections in milliseconds", Buckets: []float64{1, 10, 25, 50, 100, 500, 1000, 5000}, }, ) connectStreamErrors = prometheus.NewCounter( prometheus.CounterOpts{ Namespace: connection.MetricsNamespace, Subsystem: "proxy", Name: "connect_streams_errors", Help: "Total count of failure to establish and acknowledge connections", }, ) ) func init() { prometheus.MustRegister( totalRequests, concurrentRequests, responseByCode, requestErrors, activeTCPSessions, totalTCPSessions, connectLatency, connectStreamErrors, ) } func incrementRequests() { totalRequests.Inc() concurrentRequests.Inc() } func decrementConcurrentRequests() { concurrentRequests.Dec() } func incrementTCPRequests() { incrementRequests() totalTCPSessions.Inc() activeTCPSessions.Inc() } func decrementTCPConcurrentRequests() { decrementConcurrentRequests() activeTCPSessions.Dec() } ================================================ FILE: proxy/proxy.go ================================================ package proxy import ( "context" "fmt" "io" "net/http" "net/netip" "strconv" "time" "github.com/pkg/errors" "github.com/rs/zerolog" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" cfdflow "github.com/cloudflare/cloudflared/flow" "github.com/cloudflare/cloudflared/management" "github.com/cloudflare/cloudflared/carrier" "github.com/cloudflare/cloudflared/cfio" "github.com/cloudflare/cloudflared/connection" "github.com/cloudflare/cloudflared/ingress" "github.com/cloudflare/cloudflared/stream" "github.com/cloudflare/cloudflared/tracing" "github.com/cloudflare/cloudflared/tunnelrpc/pogs" ) const ( // TagHeaderNamePrefix indicates a Cloudflared Warp Tag prefix that gets appended for warp traffic stream headers. TagHeaderNamePrefix = "Cf-Warp-Tag-" trailerHeaderName = "Trailer" ) // Proxy represents a means to Proxy between cloudflared and the origin services. type Proxy struct { ingressRules ingress.Ingress originDialer ingress.OriginTCPDialer tags []pogs.Tag flowLimiter cfdflow.Limiter log *zerolog.Logger } // NewOriginProxy returns a new instance of the Proxy struct. func NewOriginProxy( ingressRules ingress.Ingress, originDialer ingress.OriginDialer, tags []pogs.Tag, flowLimiter cfdflow.Limiter, log *zerolog.Logger, ) *Proxy { proxy := &Proxy{ ingressRules: ingressRules, originDialer: originDialer, tags: tags, flowLimiter: flowLimiter, log: log, } return proxy } func (p *Proxy) applyIngressMiddleware(rule *ingress.Rule, r *http.Request, w connection.ResponseWriter) (error, bool) { for _, handler := range rule.Handlers { result, err := handler.Handle(r.Context(), r) if err != nil { return errors.Wrap(err, fmt.Sprintf("error while processing middleware handler %s", handler.Name())), false } if result.ShouldFilterRequest { _ = w.WriteRespHeaders(result.StatusCode, nil) return fmt.Errorf("request filtered by middleware handler (%s) due to: %s", handler.Name(), result.Reason), true } } return nil, true } // ProxyHTTP further depends on ingress rules to establish a connection with the origin service. This may be // a simple roundtrip or a tcp/websocket dial depending on ingres rule setup. func (p *Proxy) ProxyHTTP( w connection.ResponseWriter, tr *tracing.TracedHTTPRequest, isWebsocket bool, ) error { incrementRequests() defer decrementConcurrentRequests() req := tr.Request p.appendTagHeaders(req) _, ruleSpan := tr.Tracer().Start(req.Context(), "ingress_match", trace.WithAttributes(attribute.String("req-host", req.Host))) rule, ruleNum := p.ingressRules.FindMatchingRule(req.Host, req.URL.Path) ruleSpan.SetAttributes(attribute.Int("rule-num", ruleNum)) ruleSpan.End() logger := newHTTPLogger(p.log, tr.ConnIndex, req, ruleNum, rule.Service.String()) logHTTPRequest(&logger, req) if err, applied := p.applyIngressMiddleware(rule, req, w); err != nil { if applied { logRequestError(&logger, err) return nil } return err } switch originProxy := rule.Service.(type) { case ingress.HTTPOriginProxy: if err := p.proxyHTTPRequest( w, tr, originProxy, isWebsocket, rule.Config.DisableChunkedEncoding, &logger, ); err != nil { logRequestError(&logger, err) return err } return nil case ingress.StreamBasedOriginProxy: dest, err := getDestFromRule(rule, req) if err != nil { return err } flusher, ok := w.(http.Flusher) if !ok { return fmt.Errorf("response writer is not a flusher") } rws := connection.NewHTTPResponseReadWriterAcker(w, flusher, req) logger := logger.With().Str(logFieldDestAddr, dest).Logger() if err := p.proxyStream(tr.ToTracedContext(), rws, dest, originProxy, &logger); err != nil { logRequestError(&logger, err) return err } return nil case ingress.HTTPLocalProxy: p.proxyLocalRequest(originProxy, w, req, isWebsocket) return nil default: return fmt.Errorf("Unrecognized service: %s, %t", rule.Service, originProxy) } } // ProxyTCP proxies to a TCP connection between the origin service and cloudflared. func (p *Proxy) ProxyTCP( ctx context.Context, conn connection.ReadWriteAcker, req *connection.TCPRequest, ) error { incrementTCPRequests() defer decrementTCPConcurrentRequests() logger := newTCPLogger(p.log, req) // Try to start a new flow if err := p.flowLimiter.Acquire(management.TCP.String()); err != nil { logger.Warn().Msg("Too many concurrent flows being handled, rejecting tcp proxy") return errors.Wrap(err, "failed to start tcp flow due to rate limiting") } defer p.flowLimiter.Release() serveCtx, cancel := context.WithCancel(ctx) defer cancel() tracedCtx := tracing.NewTracedContext(serveCtx, req.CfTraceID, &logger) logger.Debug().Msg("tcp proxy stream started") // Parse the destination into a netip.AddrPort dest, err := netip.ParseAddrPort(req.Dest) if err != nil { logRequestError(&logger, err) return err } if err := p.proxyTCPStream(tracedCtx, conn, dest, p.originDialer, &logger); err != nil { logRequestError(&logger, err) return err } logger.Debug().Msg("tcp proxy stream finished successfully") return nil } // ProxyHTTPRequest proxies requests of underlying type http and websocket to the origin service. func (p *Proxy) proxyHTTPRequest( w connection.ResponseWriter, tr *tracing.TracedHTTPRequest, httpService ingress.HTTPOriginProxy, isWebsocket bool, disableChunkedEncoding bool, logger *zerolog.Logger, ) error { roundTripReq := tr.Request if isWebsocket { roundTripReq = tr.Clone(tr.Request.Context()) roundTripReq.Header.Set("Connection", "Upgrade") roundTripReq.Header.Set("Upgrade", "websocket") roundTripReq.Header.Set("Sec-Websocket-Version", "13") roundTripReq.ContentLength = 0 roundTripReq.Body = nil } else { // Support for WSGI Servers by switching transfer encoding from chunked to gzip/deflate if disableChunkedEncoding { roundTripReq.TransferEncoding = []string{"gzip", "deflate"} cLength, err := strconv.Atoi(tr.Request.Header.Get("Content-Length")) if err == nil { roundTripReq.ContentLength = int64(cLength) } } // Request origin to keep connection alive to improve performance roundTripReq.Header.Set("Connection", "keep-alive") } // Set the User-Agent as an empty string if not provided to avoid inserting golang default UA if roundTripReq.Header.Get("User-Agent") == "" { roundTripReq.Header.Set("User-Agent", "") } _, ttfbSpan := tr.Tracer().Start(tr.Context(), "ttfb_origin") resp, err := httpService.RoundTrip(roundTripReq) if err != nil { tracing.EndWithErrorStatus(ttfbSpan, err) if err := roundTripReq.Context().Err(); err != nil { return errors.Wrap(err, "Incoming request ended abruptly") } return errors.Wrap(err, "Unable to reach the origin service. The service may be down or it may not be responding to traffic from cloudflared") } tracing.EndWithStatusCode(ttfbSpan, resp.StatusCode) defer resp.Body.Close() headers := make(http.Header, len(resp.Header)) // copy headers for k, v := range resp.Header { headers[k] = v } // Add spans to response header (if available) tr.AddSpans(headers) err = w.WriteRespHeaders(resp.StatusCode, headers) if err != nil { return errors.Wrap(err, "Error writing response header") } if resp.StatusCode == http.StatusSwitchingProtocols { rwc, ok := resp.Body.(io.ReadWriteCloser) if !ok { return errors.New("internal error: unsupported connection type") } defer rwc.Close() eyeballStream := &bidirectionalStream{ writer: w, reader: tr.Request.Body, } stream.Pipe(eyeballStream, rwc, logger) return nil } if _, err = cfio.Copy(w, resp.Body); err != nil { return err } // copy trailers copyTrailers(w, resp) logOriginHTTPResponse(logger, resp) return nil } // proxyStream proxies type TCP and other underlying types if the connection is defined as a stream oriented // ingress rule. // connectedLogger is used to log when the connection is acknowledged func (p *Proxy) proxyStream( tr *tracing.TracedContext, rwa connection.ReadWriteAcker, dest string, originDialer ingress.StreamBasedOriginProxy, logger *zerolog.Logger, ) error { ctx := tr.Context _, connectSpan := tr.Tracer().Start(ctx, "stream-connect") start := time.Now() originConn, err := originDialer.EstablishConnection(ctx, dest, logger) if err != nil { connectStreamErrors.Inc() tracing.EndWithErrorStatus(connectSpan, err) return err } connectSpan.End() defer originConn.Close() logger.Debug().Msg("origin connection established") encodedSpans := tr.GetSpans() if err := rwa.AckConnection(encodedSpans); err != nil { connectStreamErrors.Inc() return err } connectLatency.Observe(float64(time.Since(start).Milliseconds())) logger.Debug().Msg("proxy stream acknowledged") originConn.Stream(ctx, rwa, logger) return nil } // proxyTCPStream proxies private network type TCP connections as a stream towards an available origin. // // This is different than proxyStream because it's not leveraged ingress rule services and uses the // originDialer from OriginDialerService. func (p *Proxy) proxyTCPStream( tr *tracing.TracedContext, tunnelConn connection.ReadWriteAcker, dest netip.AddrPort, originDialer ingress.OriginTCPDialer, logger *zerolog.Logger, ) error { ctx := tr.Context _, connectSpan := tr.Tracer().Start(ctx, "stream-connect") start := time.Now() originConn, err := originDialer.DialTCP(ctx, dest) if err != nil { connectStreamErrors.Inc() tracing.EndWithErrorStatus(connectSpan, err) return err } connectSpan.End() defer originConn.Close() logger.Debug().Msg("origin connection established") encodedSpans := tr.GetSpans() if err := tunnelConn.AckConnection(encodedSpans); err != nil { connectStreamErrors.Inc() return err } connectLatency.Observe(float64(time.Since(start).Milliseconds())) logger.Debug().Msg("proxy stream acknowledged") stream.Pipe(tunnelConn, originConn, logger) return nil } func (p *Proxy) proxyLocalRequest(proxy ingress.HTTPLocalProxy, w connection.ResponseWriter, req *http.Request, isWebsocket bool) { if isWebsocket { // These headers are added since they are stripped off during an eyeball request to origintunneld, but they // are required during the Handshake process of a WebSocket request. req.Header.Set("Connection", "Upgrade") req.Header.Set("Upgrade", "websocket") req.Header.Set("Sec-Websocket-Version", "13") } proxy.ServeHTTP(w, req) } type bidirectionalStream struct { reader io.Reader writer io.Writer } func (wr *bidirectionalStream) Read(p []byte) (n int, err error) { return wr.reader.Read(p) } func (wr *bidirectionalStream) Write(p []byte) (n int, err error) { return wr.writer.Write(p) } func (p *Proxy) appendTagHeaders(r *http.Request) { for _, tag := range p.tags { r.Header.Add(TagHeaderNamePrefix+tag.Name, tag.Value) } } func copyTrailers(w connection.ResponseWriter, response *http.Response) { for trailerHeader, trailerValues := range response.Trailer { for _, trailerValue := range trailerValues { w.AddTrailer(trailerHeader, trailerValue) } } } func getDestFromRule(rule *ingress.Rule, req *http.Request) (string, error) { switch rule.Service.String() { case ingress.ServiceBastion: return carrier.ResolveBastionDest(req) default: return rule.Service.String(), nil } } ================================================ FILE: proxy/proxy_posix_test.go ================================================ //go:build !windows package proxy import ( "net" "net/http" "net/http/httptest" "os" "testing" "github.com/stretchr/testify/require" "github.com/cloudflare/cloudflared/config" ) func TestUnixSocketOrigin(t *testing.T) { file, err := os.CreateTemp("", "unix.sock") require.NoError(t, err) os.Remove(file.Name()) // remove the file since binding the socket expects to create it l, err := net.Listen("unix", file.Name()) require.NoError(t, err) defer l.Close() defer os.Remove(file.Name()) api := &httptest.Server{ Listener: l, Config: &http.Server{Handler: mockAPI{}}, } api.Start() defer api.Close() unvalidatedIngress := []config.UnvalidatedIngressRule{ { Hostname: "unix.example.com", Service: "unix:" + file.Name(), }, { Hostname: "*", Service: "http_status:404", }, } tests := []MultipleIngressTest{ { url: "http://unix.example.com", expectedStatus: http.StatusCreated, expectedBody: []byte("Created"), }, } runIngressTestScenarios(t, unvalidatedIngress, tests) } ================================================ FILE: proxy/proxy_test.go ================================================ package proxy import ( "bufio" "bytes" "context" "flag" "fmt" "io" "net" "net/http" "net/http/httptest" "strings" "sync" "testing" "time" "github.com/gobwas/ws/wsutil" gorillaWS "github.com/gorilla/websocket" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/urfave/cli/v2" "go.uber.org/mock/gomock" "golang.org/x/sync/errgroup" "github.com/cloudflare/cloudflared/mocks" cfdflow "github.com/cloudflare/cloudflared/flow" "github.com/cloudflare/cloudflared/cfio" "github.com/cloudflare/cloudflared/config" "github.com/cloudflare/cloudflared/connection" "github.com/cloudflare/cloudflared/hello" "github.com/cloudflare/cloudflared/ingress" "github.com/cloudflare/cloudflared/tracing" "github.com/cloudflare/cloudflared/tunnelrpc/pogs" ) var ( testTags = []pogs.Tag{{Name: "Name", Value: "value"}} testDefaultDialer = ingress.NewDialer(ingress.WarpRoutingConfig{ ConnectTimeout: config.CustomDuration{Duration: 1 * time.Second}, TCPKeepAlive: config.CustomDuration{Duration: 15 * time.Second}, MaxActiveFlows: 0, }) ) type mockHTTPRespWriter struct { *httptest.ResponseRecorder } func newMockHTTPRespWriter() *mockHTTPRespWriter { return &mockHTTPRespWriter{ httptest.NewRecorder(), } } func (w *mockHTTPRespWriter) WriteResponse() error { return nil } func (w *mockHTTPRespWriter) WriteRespHeaders(status int, header http.Header) error { w.WriteHeader(status) for header, val := range header { w.Header()[header] = val } return nil } func (w *mockHTTPRespWriter) AddTrailer(trailerName, trailerValue string) { // do nothing } func (w *mockHTTPRespWriter) Read(data []byte) (int, error) { return 0, fmt.Errorf("mockHTTPRespWriter doesn't implement io.Reader") } func (m *mockHTTPRespWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { panic("Hijack not implemented") } type mockWSRespWriter struct { *mockHTTPRespWriter writeNotification chan []byte reader io.Reader } func newMockWSRespWriter(reader io.Reader) *mockWSRespWriter { return &mockWSRespWriter{ newMockHTTPRespWriter(), make(chan []byte), reader, } } func (w *mockWSRespWriter) Write(data []byte) (int, error) { w.writeNotification <- data return len(data), nil } func (w *mockWSRespWriter) respBody() io.ReadWriter { data := <-w.writeNotification return bytes.NewBuffer(data) } func (w *mockWSRespWriter) Close() error { close(w.writeNotification) return nil } func (w *mockWSRespWriter) Read(data []byte) (int, error) { return w.reader.Read(data) } func (w *mockWSRespWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { panic("Hijack not implemented") } type mockSSERespWriter struct { *mockHTTPRespWriter writeNotification chan []byte } func newMockSSERespWriter() *mockSSERespWriter { return &mockSSERespWriter{ newMockHTTPRespWriter(), make(chan []byte), } } func (w *mockSSERespWriter) Write(data []byte) (int, error) { newData := make([]byte, len(data)) copy(newData, data) w.writeNotification <- newData return len(data), nil } func (w *mockSSERespWriter) WriteString(str string) (int, error) { return w.Write([]byte(str)) } func (w *mockSSERespWriter) ReadBytes() []byte { return <-w.writeNotification } func TestProxySingleOrigin(t *testing.T) { log := zerolog.Nop() ctx, cancel := context.WithCancel(t.Context()) flagSet := flag.NewFlagSet(t.Name(), flag.PanicOnError) flagSet.Bool("hello-world", true, "") cliCtx := cli.NewContext(cli.NewApp(), flagSet, nil) err := cliCtx.Set("hello-world", "true") require.NoError(t, err) ingressRule, err := ingress.ParseIngressFromConfigAndCLI(&config.Configuration{}, cliCtx, &log) require.NoError(t, err) require.NoError(t, ingressRule.StartOrigins(&log, ctx.Done())) originDialer := ingress.NewOriginDialer(ingress.OriginConfig{ DefaultDialer: testDefaultDialer, TCPWriteTimeout: 1 * time.Second, }, &log) proxy := NewOriginProxy(ingressRule, originDialer, testTags, cfdflow.NewLimiter(0), &log) t.Run("testProxyHTTP", testProxyHTTP(proxy)) t.Run("testProxyWebsocket", testProxyWebsocket(proxy)) t.Run("testProxySSE", testProxySSE(proxy)) cancel() } func testProxyHTTP(proxy connection.OriginProxy) func(t *testing.T) { return func(t *testing.T) { responseWriter := newMockHTTPRespWriter() req, err := http.NewRequest(http.MethodGet, "http://localhost:8080", nil) require.NoError(t, err) log := zerolog.Nop() err = proxy.ProxyHTTP(responseWriter, tracing.NewTracedHTTPRequest(req, 0, &log), false) require.NoError(t, err) for _, tag := range testTags { assert.Equal(t, tag.Value, req.Header.Get(TagHeaderNamePrefix+tag.Name)) } assert.Equal(t, http.StatusOK, responseWriter.Code) } } func testProxyWebsocket(proxy connection.OriginProxy) func(t *testing.T) { return func(t *testing.T) { // WSRoute is a websocket echo handler const testTimeout = 5 * time.Second * 1000 ctx, cancel := context.WithTimeout(t.Context(), testTimeout) defer cancel() readPipe, writePipe := io.Pipe() req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("http://localhost:8080%s", hello.WSRoute), readPipe) req.Header.Set("Sec-Websocket-Key", "dGhlIHNhbXBsZSBub25jZQ==") req.Header.Set("Connection", "Upgrade") req.Header.Set("Upgrade", "websocket") responseWriter := newMockWSRespWriter(nil) finished := make(chan struct{}) errGroup, ctx := errgroup.WithContext(ctx) errGroup.Go(func() error { log := zerolog.Nop() err = proxy.ProxyHTTP(responseWriter, tracing.NewTracedHTTPRequest(req, 0, &log), true) require.NoError(t, err) require.Equal(t, http.StatusSwitchingProtocols, responseWriter.Code) return nil }) errGroup.Go(func() error { select { case <-finished: case <-ctx.Done(): } if ctx.Err() == context.DeadlineExceeded { t.Errorf("Test timed out") readPipe.Close() writePipe.Close() responseWriter.Close() } return nil }) msg := []byte("test websocket") err = wsutil.WriteClientText(writePipe, msg) require.NoError(t, err) // ReadServerText reads next data message from rw, considering that caller represents proxy side. returnedMsg, err := wsutil.ReadServerText(responseWriter.respBody()) require.NoError(t, err) require.Equal(t, msg, returnedMsg) err = wsutil.WriteClientBinary(writePipe, msg) require.NoError(t, err) returnedMsg, err = wsutil.ReadServerBinary(responseWriter.respBody()) require.NoError(t, err) require.Equal(t, msg, returnedMsg) _ = readPipe.Close() _ = writePipe.Close() _ = responseWriter.Close() close(finished) _ = errGroup.Wait() } } func testProxySSE(proxy connection.OriginProxy) func(t *testing.T) { return func(t *testing.T) { var ( pushCount = 50 pushFreq = time.Millisecond * 10 ) responseWriter := newMockSSERespWriter() ctx, cancel := context.WithCancel(t.Context()) req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("http://localhost:8080%s?freq=%s", hello.SSERoute, pushFreq), nil) require.NoError(t, err) var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() log := zerolog.Nop() err = proxy.ProxyHTTP(responseWriter, tracing.NewTracedHTTPRequest(req, 0, &log), false) require.Equal(t, "context canceled", err.Error()) require.Equal(t, http.StatusOK, responseWriter.Code) }() for i := 0; i < pushCount; i++ { line := responseWriter.ReadBytes() expect := fmt.Sprintf("%d\n\n", i) require.Equal(t, []byte(expect), line, "Expect to read %v, got %v", expect, line) } cancel() wg.Wait() } } // Regression test to guarantee that we always write the contents downstream even if EOF is reached without // hitting the delimiter func TestProxySSEAllData(t *testing.T) { eyeballReader := io.NopCloser(strings.NewReader("data\r\r")) responseWriter := newMockSSERespWriter() // responseWriter uses an unbuffered channel, so we call in a different go-routine go func() { _, _ = cfio.Copy(responseWriter, eyeballReader) }() result := string(<-responseWriter.writeNotification) require.Equal(t, "data\r\r", result) } func TestProxyMultipleOrigins(t *testing.T) { api := httptest.NewServer(mockAPI{}) defer api.Close() unvalidatedIngress := []config.UnvalidatedIngressRule{ { Hostname: "api.example.com", Service: api.URL, }, { Hostname: "hello.example.com", Service: "hello-world", }, { Hostname: "health.example.com", Path: "/health", Service: "http_status:200", }, { Hostname: "*", Service: "http_status:404", }, } tests := []MultipleIngressTest{ { url: "http://api.example.com", expectedStatus: http.StatusCreated, expectedBody: []byte("Created"), }, { url: fmt.Sprintf("http://hello.example.com%s", hello.HealthRoute), expectedStatus: http.StatusOK, expectedBody: []byte("ok"), }, { url: "http://health.example.com/health", expectedStatus: http.StatusOK, }, { url: "http://health.example.com/", expectedStatus: http.StatusNotFound, }, { url: "http://not-found.example.com", expectedStatus: http.StatusNotFound, }, } runIngressTestScenarios(t, unvalidatedIngress, tests) } type MultipleIngressTest struct { url string expectedStatus int expectedBody []byte } func runIngressTestScenarios(t *testing.T, unvalidatedIngress []config.UnvalidatedIngressRule, tests []MultipleIngressTest) { ingressRule, err := ingress.ParseIngress(&config.Configuration{ TunnelID: t.Name(), Ingress: unvalidatedIngress, }) require.NoError(t, err) log := zerolog.Nop() ctx, cancel := context.WithCancel(t.Context()) require.NoError(t, ingressRule.StartOrigins(&log, ctx.Done())) originDialer := ingress.NewOriginDialer(ingress.OriginConfig{ DefaultDialer: testDefaultDialer, TCPWriteTimeout: 1 * time.Second, }, &log) proxy := NewOriginProxy(ingressRule, originDialer, testTags, cfdflow.NewLimiter(0), &log) for _, test := range tests { responseWriter := newMockHTTPRespWriter() req, err := http.NewRequest(http.MethodGet, test.url, nil) require.NoError(t, err) err = proxy.ProxyHTTP(responseWriter, tracing.NewTracedHTTPRequest(req, 0, &log), false) require.NoError(t, err) assert.Equal(t, test.expectedStatus, responseWriter.Code) if test.expectedBody != nil { assert.Equal(t, test.expectedBody, responseWriter.Body.Bytes()) } else { assert.Equal(t, 0, responseWriter.Body.Len()) } } cancel() } type mockAPI struct{} func (ma mockAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusCreated) _, _ = w.Write([]byte("Created")) } type errorOriginTransport struct{} func (errorOriginTransport) RoundTrip(*http.Request) (*http.Response, error) { return nil, fmt.Errorf("Proxy error") } func TestProxyError(t *testing.T) { ing := ingress.Ingress{ Rules: []ingress.Rule{ { Hostname: "*", Path: nil, Service: ingress.MockOriginHTTPService{ Transport: errorOriginTransport{}, }, }, }, } log := zerolog.Nop() originDialer := ingress.NewOriginDialer(ingress.OriginConfig{ DefaultDialer: testDefaultDialer, TCPWriteTimeout: 1 * time.Second, }, &log) proxy := NewOriginProxy(ing, originDialer, testTags, cfdflow.NewLimiter(0), &log) responseWriter := newMockHTTPRespWriter() req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1", nil) require.NoError(t, err) require.Error(t, proxy.ProxyHTTP(responseWriter, tracing.NewTracedHTTPRequest(req, 0, &log), false)) } type replayer struct { sync.RWMutex rw *bytes.Buffer } func (r *replayer) Read(p []byte) (int, error) { r.RLock() defer r.RUnlock() return r.rw.Read(p) } func (r *replayer) Write(p []byte) (int, error) { r.Lock() defer r.Unlock() n, err := r.rw.Write(p) return n, err } func (r *replayer) String() string { r.Lock() defer r.Unlock() return r.rw.String() } func (r *replayer) Bytes() []byte { r.Lock() defer r.Unlock() return r.rw.Bytes() } // TestConnections tests every possible permutation of connection protocols // proxied by cloudflared. // // WS - WS : When a websocket based ingress is configured on the origin and // the eyeball is also a websocket client streaming data. // TCP - TCP : When teamnet is enabled and an http or tcp service is running // on the origin. // TCP - WS: When teamnet is enabled and a websocket based service is running // on the origin. // WS - TCP: When a tcp based ingress is configured on the origin and the // eyeball sends tcp packets wrapped in websockets. (E.g: cloudflared access). func TestConnections(t *testing.T) { log := zerolog.Nop() replayer := &replayer{rw: bytes.NewBuffer([]byte{})} type args struct { ingressServiceScheme string originService func(*testing.T, net.Listener) eyeballResponseWriter connection.ResponseWriter eyeballRequestBody io.ReadCloser // eyeball connection type. connectionType connection.Type // requestheaders to be sent in the call to proxy.Proxy requestHeaders http.Header // flowLimiterResponse is the response of the cfdflow.Limiter#Acquire method call flowLimiterResponse error } originDialer := ingress.NewOriginDialer(ingress.OriginConfig{ DefaultDialer: testDefaultDialer, TCPWriteTimeout: 0, }, &log) type want struct { message []byte headers http.Header err bool } tests := []struct { name string args args want want }{ { name: "ws-ws proxy", args: args{ ingressServiceScheme: "ws://", originService: runEchoWSService, eyeballResponseWriter: newWSRespWriter(replayer), eyeballRequestBody: newWSRequestBody([]byte("test1")), connectionType: connection.TypeWebsocket, requestHeaders: map[string][]string{ // Example key from https://tools.ietf.org/html/rfc6455#section-1.2 "Sec-Websocket-Key": {"dGhlIHNhbXBsZSBub25jZQ=="}, "Test-Cloudflared-Echo": {"Echo"}, }, }, want: want{ message: []byte("echo-test1"), headers: map[string][]string{ "Connection": {"Upgrade"}, "Sec-Websocket-Accept": {"s3pPLMBiTxaQ9kYGzzhZRbK+xOo="}, "Upgrade": {"websocket"}, "Test-Cloudflared-Echo": {"Echo"}, }, }, }, { name: "tcp-tcp proxy", args: args{ ingressServiceScheme: "tcp://", originService: runEchoTCPService, eyeballResponseWriter: newTCPRespWriter(replayer), eyeballRequestBody: newTCPRequestBody([]byte("test2")), connectionType: connection.TypeTCP, requestHeaders: map[string][]string{ "Cf-Cloudflared-Proxy-Src": {"non-blank-value"}, }, }, want: want{ message: []byte("echo-test2"), headers: http.Header{}, }, }, { name: "tcp-ws proxy", args: args{ ingressServiceScheme: "ws://", originService: runEchoWSService, // eyeballResponseWriter gets set after roundtrip dial. eyeballRequestBody: newPipedWSRequestBody([]byte("test3")), requestHeaders: map[string][]string{ "Cf-Cloudflared-Proxy-Src": {"non-blank-value"}, }, connectionType: connection.TypeTCP, }, want: want{ message: []byte("echo-test3"), // We expect no headers here because they are sent back via // the stream. headers: http.Header{}, }, }, { name: "ws-tcp proxy", args: args{ ingressServiceScheme: "tcp://", originService: runEchoTCPService, eyeballResponseWriter: newWSRespWriter(replayer), eyeballRequestBody: newWSRequestBody([]byte("test4")), connectionType: connection.TypeWebsocket, requestHeaders: map[string][]string{ // Example key from https://tools.ietf.org/html/rfc6455#section-1.2 "Sec-Websocket-Key": {"dGhlIHNhbXBsZSBub25jZQ=="}, }, }, want: want{ message: []byte("echo-test4"), headers: map[string][]string{ "Connection": {"Upgrade"}, "Sec-Websocket-Accept": {"s3pPLMBiTxaQ9kYGzzhZRbK+xOo="}, "Upgrade": {"websocket"}, }, }, }, { // Send (unexpected) HTTP when origin expects WS (to unwrap for raw TCP) name: "http-(ws)tcp proxy", args: args{ ingressServiceScheme: "tcp://", originService: runEchoTCPService, eyeballResponseWriter: newMockHTTPRespWriter(), eyeballRequestBody: http.NoBody, connectionType: connection.TypeHTTP, requestHeaders: map[string][]string{ "Cf-Cloudflared-Proxy-Src": {"non-blank-value"}, }, }, want: want{ message: []byte{}, headers: map[string][]string{}, }, }, { name: "ws-ws proxy when origin is different", args: args{ ingressServiceScheme: "ws://", originService: runEchoWSService, eyeballResponseWriter: newWSRespWriter(replayer), eyeballRequestBody: newWSRequestBody([]byte("test1")), connectionType: connection.TypeWebsocket, requestHeaders: map[string][]string{ // Example key from https://tools.ietf.org/html/rfc6455#section-1.2 "Sec-Websocket-Key": {"dGhlIHNhbXBsZSBub25jZQ=="}, "Origin": {"Different origin"}, }, }, want: want{ message: []byte("Forbidden\n"), err: false, headers: map[string][]string{ "Content-Length": {"10"}, "Content-Type": {"text/plain; charset=utf-8"}, "Sec-Websocket-Version": {"13"}, "X-Content-Type-Options": {"nosniff"}, }, }, }, { name: "tcp-* proxy when origin service has already closed the connection/ is no longer running", args: args{ ingressServiceScheme: "tcp://", originService: func(t *testing.T, ln net.Listener) { // closing the listener created by the test. ln.Close() }, eyeballResponseWriter: newTCPRespWriter(replayer), eyeballRequestBody: newTCPRequestBody([]byte("test2")), connectionType: connection.TypeTCP, requestHeaders: map[string][]string{ "Cf-Cloudflared-Proxy-Src": {"non-blank-value"}, }, }, want: want{ message: []byte{}, err: true, }, }, { name: "tcp-* proxy rate limited flow", args: args{ ingressServiceScheme: "tcp://", originService: runEchoTCPService, eyeballResponseWriter: newTCPRespWriter(replayer), eyeballRequestBody: newTCPRequestBody([]byte("rate-limited")), connectionType: connection.TypeTCP, requestHeaders: map[string][]string{ "Cf-Cloudflared-Proxy-Src": {"non-blank-value"}, }, flowLimiterResponse: cfdflow.ErrTooManyActiveFlows, }, want: want{ message: []byte{}, err: true, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { ctx, cancel := context.WithCancel(t.Context()) ln, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err) // Starts origin service test.args.originService(t, ln) ingressRule := createSingleIngressConfig(t, test.args.ingressServiceScheme+ln.Addr().String()) _ = ingressRule.StartOrigins(&log, ctx.Done()) // Mock flow limiter ctrl := gomock.NewController(t) defer ctrl.Finish() flowLimiter := mocks.NewMockLimiter(ctrl) flowLimiter.EXPECT().Acquire("tcp").AnyTimes().Return(test.args.flowLimiterResponse) flowLimiter.EXPECT().Release().AnyTimes() proxy := NewOriginProxy(ingressRule, originDialer, testTags, flowLimiter, &log) dest := ln.Addr().String() req, err := http.NewRequest( http.MethodGet, test.args.ingressServiceScheme+ln.Addr().String(), test.args.eyeballRequestBody, ) require.NoError(t, err) req.Header = test.args.requestHeaders respWriter := test.args.eyeballResponseWriter if pipedReqBody, ok := test.args.eyeballRequestBody.(*pipedRequestBody); ok { respWriter = newTCPRespWriter(pipedReqBody.pipedConn) go func() { resp := pipedReqBody.roundtrip(test.args.ingressServiceScheme + ln.Addr().String()) _, _ = replayer.Write(resp) }() } if test.args.connectionType == connection.TypeTCP { rwa := connection.NewHTTPResponseReadWriterAcker(respWriter, respWriter.(http.Flusher), req) err = proxy.ProxyTCP(ctx, rwa, &connection.TCPRequest{Dest: dest}) } else { log := zerolog.Nop() err = proxy.ProxyHTTP(respWriter, tracing.NewTracedHTTPRequest(req, 0, &log), test.args.connectionType == connection.TypeWebsocket) } cancel() require.Equal(t, test.want.err, err != nil) require.Equal(t, test.want.message, replayer.Bytes()) require.Equal(t, test.want.headers, respWriter.Header()) replayer.rw.Reset() }) } } type requestBody struct { pw *io.PipeWriter pr *io.PipeReader } func newWSRequestBody(data []byte) *requestBody { pr, pw := io.Pipe() go func() { _ = wsutil.WriteClientBinary(pw, data) }() return &requestBody{ pr: pr, pw: pw, } } func newTCPRequestBody(data []byte) *requestBody { pr, pw := io.Pipe() go func() { _, _ = pw.Write(data) }() return &requestBody{ pr: pr, pw: pw, } } func (r *requestBody) Read(p []byte) (n int, err error) { return r.pr.Read(p) } func (r *requestBody) Close() error { _ = r.pw.Close() _ = r.pr.Close() return nil } type pipedRequestBody struct { dialer gorillaWS.Dialer pipedConn net.Conn wsConn net.Conn messageToWrite []byte } func newPipedWSRequestBody(data []byte) *pipedRequestBody { conn1, conn2 := net.Pipe() dialer := gorillaWS.Dialer{ NetDial: func(network, addr string) (net.Conn, error) { return conn2, nil }, } return &pipedRequestBody{ dialer: dialer, pipedConn: conn1, wsConn: conn2, messageToWrite: data, } } func (p *pipedRequestBody) roundtrip(addr string) []byte { header := http.Header{} conn, resp, err := p.dialer.Dial(addr, header) if err != nil { panic(err) } defer conn.Close() defer resp.Body.Close() if resp.StatusCode != http.StatusSwitchingProtocols { panic(fmt.Errorf("resp returned status code: %d", resp.StatusCode)) } err = conn.WriteMessage(gorillaWS.TextMessage, p.messageToWrite) if err != nil { panic(err) } _, data, err := conn.ReadMessage() if err != nil { panic(err) } return data } func (p *pipedRequestBody) Read(data []byte) (n int, err error) { return p.pipedConn.Read(data) } func (p *pipedRequestBody) Close() error { return nil } type wsRespWriter struct { w io.Writer responseHeaders http.Header code int } // newWSRespWriter uses wsutil.WriteClientText to generate websocket frames. // and wsutil.ReadClientText to translate frames from server to byte data. // In essence, this acts as a wsClient. func newWSRespWriter(w io.Writer) *wsRespWriter { return &wsRespWriter{ w: w, } } // Write is written to by ingress.Stream and serves as the output to the client. func (w *wsRespWriter) Write(p []byte) (int, error) { returnedMsg, err := wsutil.ReadServerBinary(bytes.NewBuffer(p)) if err != nil { // The data was not returned by a websocket connection. if err != io.ErrUnexpectedEOF { return w.w.Write(p) } } return w.w.Write(returnedMsg) } func (w *wsRespWriter) WriteRespHeaders(status int, header http.Header) error { w.responseHeaders = header w.code = status return nil } func (w *wsRespWriter) Flush() {} func (w *wsRespWriter) AddTrailer(trailerName, trailerValue string) { // do nothing } // respHeaders is a test function to read respHeaders func (w *wsRespWriter) Header() http.Header { // Removing indeterminstic header because it cannot be asserted. w.responseHeaders.Del("Date") return w.responseHeaders } func (w *wsRespWriter) WriteHeader(status int) { // unused } func (m *wsRespWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { panic("Hijack not implemented") } type mockTCPRespWriter struct { w io.Writer responseHeaders http.Header code int } func newTCPRespWriter(w io.Writer) *mockTCPRespWriter { return &mockTCPRespWriter{ w: w, } } func (m *mockTCPRespWriter) Read(p []byte) (n int, err error) { return len(p), nil } func (m *mockTCPRespWriter) Write(p []byte) (n int, err error) { return m.w.Write(p) } func (m *mockTCPRespWriter) Flush() {} func (m *mockTCPRespWriter) AddTrailer(trailerName, trailerValue string) { // do nothing } func (m *mockTCPRespWriter) WriteRespHeaders(status int, header http.Header) error { m.responseHeaders = header m.code = status return nil } // respHeaders is a test function to read respHeaders func (m *mockTCPRespWriter) Header() http.Header { return m.responseHeaders } func (m *mockTCPRespWriter) WriteHeader(status int) { // do nothing } func (m *mockTCPRespWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { panic("Hijack not implemented") } func createSingleIngressConfig(t *testing.T, service string) ingress.Ingress { ingressConfig := &config.Configuration{ Ingress: []config.UnvalidatedIngressRule{ { Hostname: "*", Service: service, }, }, } ingressRule, err := ingress.ParseIngress(ingressConfig) require.NoError(t, err) return ingressRule } func runEchoTCPService(t *testing.T, l net.Listener) { go func() { for { conn, err := l.Accept() if err != nil { panic(err) } defer conn.Close() for { buf := make([]byte, 1024) size, err := conn.Read(buf) if err == io.EOF { return } data := []byte("echo-") data = append(data, buf[:size]...) _, err = conn.Write(data) if err != nil { t.Log(err) } return } } }() } func runEchoWSService(t *testing.T, l net.Listener) { upgrader := gorillaWS.Upgrader{ ReadBufferSize: 10, WriteBufferSize: 10, } ws := func(w http.ResponseWriter, r *http.Request) { header := make(http.Header) for k, vs := range r.Header { if k == "Test-Cloudflared-Echo" { header[k] = vs } } conn, err := upgrader.Upgrade(w, r, header) if err != nil { t.Log(err) return } defer conn.Close() for { messageType, p, err := conn.ReadMessage() if err != nil { return } data := []byte("echo-") data = append(data, p...) if err := conn.WriteMessage(messageType, data); err != nil { return } } } // nolint: gosec server := http.Server{ Handler: http.HandlerFunc(ws), } go func() { err := server.Serve(l) if err != nil { panic(err) } }() } ================================================ FILE: quic/constants.go ================================================ package quic import "time" const ( HandshakeIdleTimeout = 5 * time.Second MaxIdleTimeout = 5 * time.Second MaxIdlePingPeriod = 1 * time.Second // MaxIncomingStreams is 2^60, which is the maximum supported value by Quic-Go MaxIncomingStreams = 1 << 60 ) ================================================ FILE: quic/conversion.go ================================================ package quic import ( "strconv" "time" "github.com/quic-go/quic-go/logging" ) // Helper to convert logging.ByteCount(alias for int64) to float64 used in prometheus func byteCountToPromCount(count logging.ByteCount) float64 { return float64(count) } // Helper to convert Duration to float64 used in prometheus func durationToPromGauge(duration time.Duration) float64 { return float64(duration.Milliseconds()) } // Helper to convert https://pkg.go.dev/github.com/quic-go/quic-go@v0.23.0/logging#PacketType into string func packetTypeString(pt logging.PacketType) string { switch pt { case logging.PacketTypeInitial: return "initial" case logging.PacketTypeHandshake: return "handshake" case logging.PacketTypeRetry: return "retry" case logging.PacketType0RTT: return "0_rtt" case logging.PacketTypeVersionNegotiation: return "version_negotiation" case logging.PacketType1RTT: return "1_rtt" case logging.PacketTypeStatelessReset: return "stateless_reset" case logging.PacketTypeNotDetermined: return "undetermined" default: return "unknown_packet_type" } } // Helper to convert https://pkg.go.dev/github.com/quic-go/quic-go@v0.23.0/logging#PacketDropReason into string func packetDropReasonString(reason logging.PacketDropReason) string { switch reason { case logging.PacketDropKeyUnavailable: return "key_unavailable" case logging.PacketDropUnknownConnectionID: return "unknown_conn_id" case logging.PacketDropHeaderParseError: return "header_parse_err" case logging.PacketDropPayloadDecryptError: return "payload_decrypt_err" case logging.PacketDropProtocolViolation: return "protocol_violation" case logging.PacketDropDOSPrevention: return "dos_prevention" case logging.PacketDropUnsupportedVersion: return "unsupported_version" case logging.PacketDropUnexpectedPacket: return "unexpected_packet" case logging.PacketDropUnexpectedSourceConnectionID: return "unexpected_src_conn_id" case logging.PacketDropUnexpectedVersion: return "unexpected_version" case logging.PacketDropDuplicate: return "duplicate" default: return "unknown_reason" } } // Helper to convert https://pkg.go.dev/github.com/quic-go/quic-go@v0.23.0/logging#PacketLossReason into string func packetLossReasonString(reason logging.PacketLossReason) string { switch reason { case logging.PacketLossReorderingThreshold: return "reordering" case logging.PacketLossTimeThreshold: return "timeout" default: return "unknown_loss_reason" } } func uint8ToString(input uint8) string { return strconv.FormatUint(uint64(input), 10) } ================================================ FILE: quic/datagram.go ================================================ package quic import ( "context" "fmt" "github.com/google/uuid" "github.com/pkg/errors" "github.com/quic-go/quic-go" "github.com/rs/zerolog" "github.com/cloudflare/cloudflared/packet" ) const ( sessionIDLen = len(uuid.UUID{}) ) type BaseDatagramMuxer interface { // SendToSession suffix the session ID to the payload so the other end of the QUIC connection can demultiplex the // payload from multiple datagram sessions. SendToSession(session *packet.Session) error // ServeReceive starts a loop to receive datagrams from the QUIC connection ServeReceive(ctx context.Context) error } type DatagramMuxer struct { session quic.Connection logger *zerolog.Logger demuxChan chan<- *packet.Session } func NewDatagramMuxer(quicSession quic.Connection, log *zerolog.Logger, demuxChan chan<- *packet.Session) *DatagramMuxer { logger := log.With().Uint8("datagramVersion", 1).Logger() return &DatagramMuxer{ session: quicSession, logger: &logger, demuxChan: demuxChan, } } // Maximum application payload to send to / receive from QUIC datagram frame func (dm *DatagramMuxer) mtu() int { return maxDatagramPayloadSize } func (dm *DatagramMuxer) SendToSession(session *packet.Session) error { if len(session.Payload) > dm.mtu() { packetTooBigDropped.Inc() return fmt.Errorf("origin UDP payload has %d bytes, which exceeds transport MTU %d", len(session.Payload), dm.mtu()) } payloadWithMetadata, err := SuffixSessionID(session.ID, session.Payload) if err != nil { return errors.Wrap(err, "Failed to suffix session ID to datagram, it will be dropped") } if err := dm.session.SendDatagram(payloadWithMetadata); err != nil { return errors.Wrap(err, "Failed to send datagram back to edge") } return nil } func (dm *DatagramMuxer) ServeReceive(ctx context.Context) error { for { // Extracts datagram session ID, then sends the session ID and payload to receiver // which determines how to proxy to the origin. It assumes the datagram session has already been // registered with receiver through other side channel msg, err := dm.session.ReceiveDatagram(ctx) if err != nil { return err } if err := dm.demux(ctx, msg); err != nil { dm.logger.Error().Err(err).Msg("Failed to demux datagram") if err == context.Canceled { return err } } } } func (dm *DatagramMuxer) demux(ctx context.Context, msg []byte) error { sessionID, payload, err := extractSessionID(msg) if err != nil { return err } sessionDatagram := packet.Session{ ID: sessionID, Payload: payload, } select { case dm.demuxChan <- &sessionDatagram: return nil case <-ctx.Done(): return ctx.Err() } } // Each QUIC datagram should be suffixed with session ID. // extractSessionID extracts the session ID and a slice with only the payload func extractSessionID(b []byte) (uuid.UUID, []byte, error) { msgLen := len(b) if msgLen < sessionIDLen { return uuid.Nil, nil, fmt.Errorf("session ID has %d bytes, but data only has %d", sessionIDLen, len(b)) } // Parse last 16 bytess as UUID and remove it from slice sessionID, err := uuid.FromBytes(b[len(b)-sessionIDLen:]) if err != nil { return uuid.Nil, nil, err } b = b[:len(b)-sessionIDLen] return sessionID, b, nil } // SuffixSessionID appends the session ID at the end of the payload. Suffix is more performant than prefix because // the payload slice might already have enough capacity to append the session ID at the end func SuffixSessionID(sessionID uuid.UUID, b []byte) ([]byte, error) { return suffixMetadata(b, sessionID[:]) } func suffixMetadata(payload, metadata []byte) ([]byte, error) { if len(payload)+len(metadata) > MaxDatagramFrameSize { return nil, fmt.Errorf("datagram size exceed %d", MaxDatagramFrameSize) } return append(payload, metadata...), nil } ================================================ FILE: quic/datagram_test.go ================================================ package quic import ( "bytes" "context" "crypto/rand" "crypto/rsa" "crypto/tls" "crypto/x509" "encoding/pem" "fmt" "math/big" "net" "net/netip" "testing" "time" "github.com/google/gopacket/layers" "github.com/google/uuid" "github.com/quic-go/quic-go" "github.com/rs/zerolog" "github.com/stretchr/testify/require" "golang.org/x/net/icmp" "golang.org/x/net/ipv4" "golang.org/x/sync/errgroup" "github.com/cloudflare/cloudflared/packet" "github.com/cloudflare/cloudflared/tracing" ) var ( testSessionID = uuid.New() ) func TestSuffixThenRemoveSessionID(t *testing.T) { msg := []byte(t.Name()) msgWithID, err := SuffixSessionID(testSessionID, msg) require.NoError(t, err) require.Len(t, msgWithID, len(msg)+sessionIDLen) sessionID, msgWithoutID, err := extractSessionID(msgWithID) require.NoError(t, err) require.Equal(t, msg, msgWithoutID) require.Equal(t, testSessionID, sessionID) } func TestRemoveSessionIDError(t *testing.T) { // message is too short to contain session ID msg := []byte("test") _, _, err := extractSessionID(msg) require.Error(t, err) } func TestSuffixSessionIDError(t *testing.T) { msg := make([]byte, MaxDatagramFrameSize-sessionIDLen) _, err := SuffixSessionID(testSessionID, msg) require.NoError(t, err) msg = make([]byte, MaxDatagramFrameSize-sessionIDLen+1) _, err = SuffixSessionID(testSessionID, msg) require.Error(t, err) } func TestDatagram(t *testing.T) { maxPayload := make([]byte, maxDatagramPayloadSize) noPayloadSession := uuid.New() maxPayloadSession := uuid.New() sessionToPayload := []*packet.Session{ { ID: noPayloadSession, Payload: make([]byte, 0), }, { ID: maxPayloadSession, Payload: maxPayload, }, } packets := []packet.ICMP{ { IP: &packet.IP{ Src: netip.MustParseAddr("172.16.0.1"), Dst: netip.MustParseAddr("192.168.0.1"), Protocol: layers.IPProtocolICMPv4, }, Message: &icmp.Message{ Type: ipv4.ICMPTypeTimeExceeded, Code: 0, Body: &icmp.TimeExceeded{ Data: []byte("original packet"), }, }, }, { IP: &packet.IP{ Src: netip.MustParseAddr("172.16.0.2"), Dst: netip.MustParseAddr("192.168.0.2"), Protocol: layers.IPProtocolICMPv4, }, Message: &icmp.Message{ Type: ipv4.ICMPTypeEcho, Code: 0, Body: &icmp.Echo{ ID: 6182, Seq: 9151, Data: []byte("Test ICMP echo"), }, }, }, } testDatagram(t, 1, sessionToPayload, nil) testDatagram(t, 2, sessionToPayload, packets) } func testDatagram(t *testing.T, version uint8, sessionToPayloads []*packet.Session, packets []packet.ICMP) { quicConfig := &quic.Config{ KeepAlivePeriod: 5 * time.Millisecond, EnableDatagrams: true, } quicListener := newQUICListener(t, quicConfig) defer quicListener.Close() logger := zerolog.Nop() tracingIdentity, err := tracing.NewIdentity("ec31ad8a01fde11fdcabe2efdce36873:52726f6cabc144f5:0:1") require.NoError(t, err) serializedTracingID, err := tracingIdentity.MarshalBinary() require.NoError(t, err) tracingSpan := &TracingSpanPacket{ Spans: []byte("tracing"), TracingIdentity: serializedTracingID, } errGroup, ctx := errgroup.WithContext(context.Background()) // Run edge side of datagram muxer errGroup.Go(func() error { // Accept quic connection quicSession, err := quicListener.Accept(ctx) if err != nil { return err } sessionDemuxChan := make(chan *packet.Session, 16) switch version { case 1: muxer := NewDatagramMuxer(quicSession, &logger, sessionDemuxChan) muxer.ServeReceive(ctx) case 2: muxer := NewDatagramMuxerV2(quicSession, &logger, sessionDemuxChan) muxer.ServeReceive(ctx) for _, pk := range packets { received, err := muxer.ReceivePacket(ctx) require.NoError(t, err) validateIPPacket(t, received, &pk) received, err = muxer.ReceivePacket(ctx) require.NoError(t, err) validateIPPacketWithTracing(t, received, &pk, serializedTracingID) } received, err := muxer.ReceivePacket(ctx) require.NoError(t, err) validateTracingSpans(t, received, tracingSpan) default: return fmt.Errorf("unknown datagram version %d", version) } for _, expectedPayload := range sessionToPayloads { actualPayload := <-sessionDemuxChan require.Equal(t, expectedPayload, actualPayload) } return nil }) largePayload := make([]byte, MaxDatagramFrameSize) // Run cloudflared side of datagram muxer errGroup.Go(func() error { tlsClientConfig := &tls.Config{ InsecureSkipVerify: true, NextProtos: []string{"argotunnel"}, } ctx, cancel := context.WithCancel(context.Background()) defer cancel() // https://github.com/quic-go/quic-go/issues/3793 MTU discovery is disabled on OSX for dual stack listeners udpConn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4zero, Port: 0}) require.NoError(t, err) // Establish quic connection quicSession, err := quic.DialEarly(ctx, udpConn, quicListener.Addr(), tlsClientConfig, quicConfig) require.NoError(t, err) defer quicSession.CloseWithError(0, "") // Wait a few milliseconds for MTU discovery to take place time.Sleep(time.Millisecond * 100) var muxer BaseDatagramMuxer switch version { case 1: muxer = NewDatagramMuxer(quicSession, &logger, nil) case 2: muxerV2 := NewDatagramMuxerV2(quicSession, &logger, nil) encoder := packet.NewEncoder() for _, pk := range packets { encodedPacket, err := encoder.Encode(&pk) require.NoError(t, err) require.NoError(t, muxerV2.SendPacket(RawPacket(encodedPacket))) require.NoError(t, muxerV2.SendPacket(&TracedPacket{ Packet: encodedPacket, TracingIdentity: serializedTracingID, })) } require.NoError(t, muxerV2.SendPacket(tracingSpan)) // Payload larger than transport MTU, should not be sent require.Error(t, muxerV2.SendPacket(RawPacket{ Data: largePayload, })) muxer = muxerV2 default: return fmt.Errorf("unknown datagram version %d", version) } for _, session := range sessionToPayloads { require.NoError(t, muxer.SendToSession(session)) } // Payload larger than transport MTU, should not be sent require.Error(t, muxer.SendToSession(&packet.Session{ ID: testSessionID, Payload: largePayload, })) // Wait for edge to finish receiving the messages time.Sleep(time.Millisecond * 100) return nil }) require.NoError(t, errGroup.Wait()) } func validateIPPacket(t *testing.T, receivedPacket Packet, expectedICMP *packet.ICMP) { require.Equal(t, DatagramTypeIP, receivedPacket.Type()) rawPacket := receivedPacket.(RawPacket) decoder := packet.NewICMPDecoder() receivedICMP, err := decoder.Decode(packet.RawPacket(rawPacket)) require.NoError(t, err) validateICMP(t, expectedICMP, receivedICMP) } func validateIPPacketWithTracing(t *testing.T, receivedPacket Packet, expectedICMP *packet.ICMP, serializedTracingID []byte) { require.Equal(t, DatagramTypeIPWithTrace, receivedPacket.Type()) tracedPacket := receivedPacket.(*TracedPacket) decoder := packet.NewICMPDecoder() receivedICMP, err := decoder.Decode(tracedPacket.Packet) require.NoError(t, err) validateICMP(t, expectedICMP, receivedICMP) require.True(t, bytes.Equal(tracedPacket.TracingIdentity, serializedTracingID)) } func validateICMP(t *testing.T, expected, actual *packet.ICMP) { require.Equal(t, expected.IP, actual.IP) require.Equal(t, expected.Type, actual.Type) require.Equal(t, expected.Code, actual.Code) require.Equal(t, expected.Body, actual.Body) } func validateTracingSpans(t *testing.T, receivedPacket Packet, expectedSpan *TracingSpanPacket) { require.Equal(t, DatagramTypeTracingSpan, receivedPacket.Type()) tracingSpans := receivedPacket.(*TracingSpanPacket) require.Equal(t, tracingSpans, expectedSpan) } func newQUICListener(t *testing.T, config *quic.Config) *quic.Listener { // Create a simple tls config. tlsConfig := generateTLSConfig() listener, err := quic.ListenAddr("127.0.0.1:0", tlsConfig, config) require.NoError(t, err) return listener } func generateTLSConfig() *tls.Config { key, err := rsa.GenerateKey(rand.Reader, 1024) if err != nil { panic(err) } template := x509.Certificate{SerialNumber: big.NewInt(1)} certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key) if err != nil { panic(err) } keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}) certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) tlsCert, err := tls.X509KeyPair(certPEM, keyPEM) if err != nil { panic(err) } return &tls.Config{ Certificates: []tls.Certificate{tlsCert}, NextProtos: []string{"argotunnel"}, } } ================================================ FILE: quic/datagramv2.go ================================================ package quic import ( "context" "fmt" "github.com/pkg/errors" "github.com/quic-go/quic-go" "github.com/rs/zerolog" "github.com/cloudflare/cloudflared/packet" "github.com/cloudflare/cloudflared/tracing" ) type DatagramV2Type byte const ( // UDP payload DatagramTypeUDP DatagramV2Type = iota // Full IP packet DatagramTypeIP // DatagramTypeIP + tracing ID DatagramTypeIPWithTrace // Tracing spans in protobuf format DatagramTypeTracingSpan ) type Packet interface { Type() DatagramV2Type Payload() []byte Metadata() []byte } const ( typeIDLen = 1 // Same as sessionDemuxChan capacity packetChanCapacity = 128 ) func SuffixType(b []byte, datagramType DatagramV2Type) ([]byte, error) { if len(b)+typeIDLen > MaxDatagramFrameSize { return nil, fmt.Errorf("datagram size %d exceeds max frame size %d", len(b), MaxDatagramFrameSize) } b = append(b, byte(datagramType)) return b, nil } // Maximum application payload to send to / receive from QUIC datagram frame func (dm *DatagramMuxerV2) mtu() int { return maxDatagramPayloadSize } type DatagramMuxerV2 struct { session quic.Connection logger *zerolog.Logger sessionDemuxChan chan<- *packet.Session packetDemuxChan chan Packet } func NewDatagramMuxerV2( quicSession quic.Connection, log *zerolog.Logger, sessionDemuxChan chan<- *packet.Session, ) *DatagramMuxerV2 { logger := log.With().Uint8("datagramVersion", 2).Logger() return &DatagramMuxerV2{ session: quicSession, logger: &logger, sessionDemuxChan: sessionDemuxChan, packetDemuxChan: make(chan Packet, packetChanCapacity), } } // SendToSession suffix the session ID and datagram version to the payload so the other end of the QUIC connection can // demultiplex the payload from multiple datagram sessions func (dm *DatagramMuxerV2) SendToSession(session *packet.Session) error { if len(session.Payload) > dm.mtu() { packetTooBigDropped.Inc() return fmt.Errorf("origin UDP payload has %d bytes, which exceeds transport MTU %d", len(session.Payload), dm.mtu()) } msgWithID, err := SuffixSessionID(session.ID, session.Payload) if err != nil { return errors.Wrap(err, "Failed to suffix session ID to datagram, it will be dropped") } msgWithIDAndType, err := SuffixType(msgWithID, DatagramTypeUDP) if err != nil { return errors.Wrap(err, "Failed to suffix datagram type, it will be dropped") } if err := dm.session.SendDatagram(msgWithIDAndType); err != nil { return errors.Wrap(err, "Failed to send datagram back to edge") } return nil } // SendPacket sends a packet with datagram version in the suffix. If ctx is a TracedContext, it adds the tracing // context between payload and datagram version. // The other end of the QUIC connection can demultiplex by parsing the payload as IP and look at the source and destination. func (dm *DatagramMuxerV2) SendPacket(pk Packet) error { payloadWithMetadata, err := suffixMetadata(pk.Payload(), pk.Metadata()) if err != nil { return err } payloadWithMetadataAndType, err := SuffixType(payloadWithMetadata, pk.Type()) if err != nil { return errors.Wrap(err, "Failed to suffix datagram type, it will be dropped") } if err := dm.session.SendDatagram(payloadWithMetadataAndType); err != nil { return errors.Wrap(err, "Failed to send datagram back to edge") } return nil } // Demux reads datagrams from the QUIC connection and demuxes depending on whether it's a session or packet func (dm *DatagramMuxerV2) ServeReceive(ctx context.Context) error { for { msg, err := dm.session.ReceiveDatagram(ctx) if err != nil { return err } if err := dm.demux(ctx, msg); err != nil { dm.logger.Error().Err(err).Msg("Failed to demux datagram") if err == context.Canceled { return err } } } } func (dm *DatagramMuxerV2) ReceivePacket(ctx context.Context) (pk Packet, err error) { select { case <-ctx.Done(): return nil, ctx.Err() case pk := <-dm.packetDemuxChan: return pk, nil } } func (dm *DatagramMuxerV2) demux(ctx context.Context, msgWithType []byte) error { if len(msgWithType) < typeIDLen { return fmt.Errorf("QUIC datagram should have at least %d byte", typeIDLen) } msgType := DatagramV2Type(msgWithType[len(msgWithType)-typeIDLen]) msg := msgWithType[0 : len(msgWithType)-typeIDLen] switch msgType { case DatagramTypeUDP: return dm.handleSession(ctx, msg) default: return dm.handlePacket(ctx, msg, msgType) } } func (dm *DatagramMuxerV2) handleSession(ctx context.Context, session []byte) error { sessionID, payload, err := extractSessionID(session) if err != nil { return err } sessionDatagram := packet.Session{ ID: sessionID, Payload: payload, } select { case dm.sessionDemuxChan <- &sessionDatagram: return nil case <-ctx.Done(): return ctx.Err() } } func (dm *DatagramMuxerV2) handlePacket(ctx context.Context, pk []byte, msgType DatagramV2Type) error { var demuxedPacket Packet switch msgType { case DatagramTypeIP: demuxedPacket = RawPacket(packet.RawPacket{Data: pk}) case DatagramTypeIPWithTrace: tracingIdentity, payload, err := extractTracingIdentity(pk) if err != nil { return err } demuxedPacket = &TracedPacket{ Packet: packet.RawPacket{Data: payload}, TracingIdentity: tracingIdentity, } case DatagramTypeTracingSpan: tracingIdentity, spans, err := extractTracingIdentity(pk) if err != nil { return err } demuxedPacket = &TracingSpanPacket{ Spans: spans, TracingIdentity: tracingIdentity, } default: return fmt.Errorf("Unexpected datagram type %d", msgType) } select { case <-ctx.Done(): return ctx.Err() case dm.packetDemuxChan <- demuxedPacket: return nil } } func extractTracingIdentity(pk []byte) (tracingIdentity []byte, payload []byte, err error) { if len(pk) < tracing.IdentityLength { return nil, nil, fmt.Errorf("packet with tracing context should have at least %d bytes, got %v", tracing.IdentityLength, pk) } tracingIdentity = pk[len(pk)-tracing.IdentityLength:] payload = pk[:len(pk)-tracing.IdentityLength] return tracingIdentity, payload, nil } type RawPacket packet.RawPacket func (rw RawPacket) Type() DatagramV2Type { return DatagramTypeIP } func (rw RawPacket) Payload() []byte { return rw.Data } func (rw RawPacket) Metadata() []byte { return []byte{} } type TracedPacket struct { Packet packet.RawPacket TracingIdentity []byte } func (tp *TracedPacket) Type() DatagramV2Type { return DatagramTypeIPWithTrace } func (tp *TracedPacket) Payload() []byte { return tp.Packet.Data } func (tp *TracedPacket) Metadata() []byte { return tp.TracingIdentity } type TracingSpanPacket struct { Spans []byte TracingIdentity []byte } func (tsp *TracingSpanPacket) Type() DatagramV2Type { return DatagramTypeTracingSpan } func (tsp *TracingSpanPacket) Payload() []byte { return tsp.Spans } func (tsp *TracingSpanPacket) Metadata() []byte { return tsp.TracingIdentity } ================================================ FILE: quic/metrics.go ================================================ package quic import ( "reflect" "strings" "sync" "github.com/prometheus/client_golang/prometheus" "github.com/quic-go/quic-go/logging" "github.com/rs/zerolog" ) const ( namespace = "quic" ConnectionIndexMetricLabel = "conn_index" frameTypeMetricLabel = "frame_type" packetTypeMetricLabel = "packet_type" reasonMetricLabel = "reason" ) var ( clientMetrics = struct { totalConnections prometheus.Counter closedConnections prometheus.Counter maxUDPPayloadSize *prometheus.GaugeVec sentFrames *prometheus.CounterVec sentBytes *prometheus.CounterVec receivedFrames *prometheus.CounterVec receivedBytes *prometheus.CounterVec bufferedPackets *prometheus.CounterVec droppedPackets *prometheus.CounterVec lostPackets *prometheus.CounterVec minRTT *prometheus.GaugeVec latestRTT *prometheus.GaugeVec smoothedRTT *prometheus.GaugeVec mtu *prometheus.GaugeVec congestionWindow *prometheus.GaugeVec congestionState *prometheus.GaugeVec }{ totalConnections: prometheus.NewCounter( prometheus.CounterOpts{ //nolint:promlinter Namespace: namespace, Subsystem: "client", Name: "total_connections", Help: "Number of connections initiated", }, ), closedConnections: prometheus.NewCounter( prometheus.CounterOpts{ //nolint:promlinter Namespace: namespace, Subsystem: "client", Name: "closed_connections", Help: "Number of connections that has been closed", }, ), maxUDPPayloadSize: prometheus.NewGaugeVec( prometheus.GaugeOpts{ Namespace: namespace, Subsystem: "client", Name: "max_udp_payload", Help: "Maximum UDP payload size in bytes for a QUIC packet", }, []string{ConnectionIndexMetricLabel}, ), sentFrames: prometheus.NewCounterVec( prometheus.CounterOpts{ //nolint:promlinter Namespace: namespace, Subsystem: "client", Name: "sent_frames", Help: "Number of frames that have been sent through a connection", }, []string{ConnectionIndexMetricLabel, frameTypeMetricLabel}, ), sentBytes: prometheus.NewCounterVec( prometheus.CounterOpts{ //nolint:promlinter Namespace: namespace, Subsystem: "client", Name: "sent_bytes", Help: "Number of bytes that have been sent through a connection", }, []string{ConnectionIndexMetricLabel}, ), receivedFrames: prometheus.NewCounterVec( prometheus.CounterOpts{ //nolint:promlinter Namespace: namespace, Subsystem: "client", Name: "received_frames", Help: "Number of frames that have been received through a connection", }, []string{ConnectionIndexMetricLabel, frameTypeMetricLabel}, ), receivedBytes: prometheus.NewCounterVec( prometheus.CounterOpts{ //nolint:promlinter Namespace: namespace, Subsystem: "client", Name: "receive_bytes", Help: "Number of bytes that have been received through a connection", }, []string{ConnectionIndexMetricLabel}, ), bufferedPackets: prometheus.NewCounterVec( prometheus.CounterOpts{ //nolint:promlinter Namespace: namespace, Subsystem: "client", Name: "buffered_packets", Help: "Number of bytes that have been buffered on a connection", }, []string{ConnectionIndexMetricLabel, packetTypeMetricLabel}, ), droppedPackets: prometheus.NewCounterVec( prometheus.CounterOpts{ //nolint:promlinter Namespace: namespace, Subsystem: "client", Name: "dropped_packets", Help: "Number of bytes that have been dropped on a connection", }, []string{ConnectionIndexMetricLabel, packetTypeMetricLabel, reasonMetricLabel}, ), lostPackets: prometheus.NewCounterVec( prometheus.CounterOpts{ //nolint:promlinter Namespace: namespace, Subsystem: "client", Name: "lost_packets", Help: "Number of packets that have been lost from a connection", }, []string{ConnectionIndexMetricLabel, reasonMetricLabel}, ), minRTT: prometheus.NewGaugeVec( prometheus.GaugeOpts{ Namespace: namespace, Subsystem: "client", Name: "min_rtt", Help: "Lowest RTT measured on a connection in millisec", }, []string{ConnectionIndexMetricLabel}, ), latestRTT: prometheus.NewGaugeVec( prometheus.GaugeOpts{ Namespace: namespace, Subsystem: "client", Name: "latest_rtt", Help: "Latest RTT measured on a connection", }, []string{ConnectionIndexMetricLabel}, ), smoothedRTT: prometheus.NewGaugeVec( prometheus.GaugeOpts{ Namespace: namespace, Subsystem: "client", Name: "smoothed_rtt", Help: "Calculated smoothed RTT measured on a connection in millisec", }, []string{ConnectionIndexMetricLabel}, ), mtu: prometheus.NewGaugeVec( prometheus.GaugeOpts{ Namespace: namespace, Subsystem: "client", Name: "mtu", Help: "Current maximum transmission unit (MTU) of a connection", }, []string{ConnectionIndexMetricLabel}, ), congestionWindow: prometheus.NewGaugeVec( prometheus.GaugeOpts{ Namespace: namespace, Subsystem: "client", Name: "congestion_window", Help: "Current congestion window size", }, []string{ConnectionIndexMetricLabel}, ), congestionState: prometheus.NewGaugeVec( prometheus.GaugeOpts{ Namespace: namespace, Subsystem: "client", Name: "congestion_state", Help: "Current congestion control state. See https://pkg.go.dev/github.com/quic-go/quic-go@v0.45.0/logging#CongestionState for what each value maps to", }, []string{ConnectionIndexMetricLabel}, ), } registerClient = sync.Once{} packetTooBigDropped = prometheus.NewCounter(prometheus.CounterOpts{ //nolint:promlinter Namespace: namespace, Subsystem: "client", Name: "packet_too_big_dropped", Help: "Count of packets received from origin that are too big to send to the edge and are dropped as a result", }) ) type clientCollector struct { index string logger *zerolog.Logger } func newClientCollector(index string, logger *zerolog.Logger) *clientCollector { registerClient.Do(func() { prometheus.MustRegister( clientMetrics.totalConnections, clientMetrics.closedConnections, clientMetrics.maxUDPPayloadSize, clientMetrics.sentFrames, clientMetrics.sentBytes, clientMetrics.receivedFrames, clientMetrics.receivedBytes, clientMetrics.bufferedPackets, clientMetrics.droppedPackets, clientMetrics.lostPackets, clientMetrics.minRTT, clientMetrics.latestRTT, clientMetrics.smoothedRTT, clientMetrics.mtu, clientMetrics.congestionWindow, clientMetrics.congestionState, packetTooBigDropped, ) }) return &clientCollector{ index: index, logger: logger, } } func (cc *clientCollector) startedConnection() { clientMetrics.totalConnections.Inc() } func (cc *clientCollector) closedConnection(error) { clientMetrics.closedConnections.Inc() } func (cc *clientCollector) receivedTransportParameters(params *logging.TransportParameters) { clientMetrics.maxUDPPayloadSize.WithLabelValues(cc.index).Set(float64(params.MaxUDPPayloadSize)) cc.logger.Debug().Msgf("Received transport parameters: MaxUDPPayloadSize=%d, MaxIdleTimeout=%v, MaxDatagramFrameSize=%d", params.MaxUDPPayloadSize, params.MaxIdleTimeout, params.MaxDatagramFrameSize) } func (cc *clientCollector) sentPackets(size logging.ByteCount, frames []logging.Frame) { cc.collectPackets(size, frames, clientMetrics.sentFrames, clientMetrics.sentBytes, sent) } func (cc *clientCollector) receivedPackets(size logging.ByteCount, frames []logging.Frame) { cc.collectPackets(size, frames, clientMetrics.receivedFrames, clientMetrics.receivedBytes, received) } func (cc *clientCollector) bufferedPackets(packetType logging.PacketType) { clientMetrics.bufferedPackets.WithLabelValues(cc.index, packetTypeString(packetType)).Inc() } func (cc *clientCollector) droppedPackets(packetType logging.PacketType, size logging.ByteCount, reason logging.PacketDropReason) { clientMetrics.droppedPackets.WithLabelValues( cc.index, packetTypeString(packetType), packetDropReasonString(reason), ).Add(byteCountToPromCount(size)) } func (cc *clientCollector) lostPackets(reason logging.PacketLossReason) { clientMetrics.lostPackets.WithLabelValues(cc.index, packetLossReasonString(reason)).Inc() } func (cc *clientCollector) updatedRTT(rtt *logging.RTTStats) { clientMetrics.minRTT.WithLabelValues(cc.index).Set(durationToPromGauge(rtt.MinRTT())) clientMetrics.latestRTT.WithLabelValues(cc.index).Set(durationToPromGauge(rtt.LatestRTT())) clientMetrics.smoothedRTT.WithLabelValues(cc.index).Set(durationToPromGauge(rtt.SmoothedRTT())) } func (cc *clientCollector) updateCongestionWindow(size logging.ByteCount) { clientMetrics.congestionWindow.WithLabelValues(cc.index).Set(float64(size)) } func (cc *clientCollector) updatedCongestionState(state logging.CongestionState) { clientMetrics.congestionState.WithLabelValues(cc.index).Set(float64(state)) } func (cc *clientCollector) updateMTU(mtu logging.ByteCount) { clientMetrics.mtu.WithLabelValues(cc.index).Set(float64(mtu)) cc.logger.Debug().Msgf("QUIC MTU updated to %d", mtu) } func (cc *clientCollector) collectPackets(size logging.ByteCount, frames []logging.Frame, counter, bandwidth *prometheus.CounterVec, direction direction) { for _, frame := range frames { switch f := frame.(type) { case logging.DataBlockedFrame: cc.logger.Debug().Msgf("%s data_blocked frame", direction) case logging.StreamDataBlockedFrame: cc.logger.Debug().Int64("streamID", int64(f.StreamID)).Msgf("%s stream_data_blocked frame", direction) } counter.WithLabelValues(cc.index, frameName(frame)).Inc() } bandwidth.WithLabelValues(cc.index).Add(byteCountToPromCount(size)) } func frameName(frame logging.Frame) string { if frame == nil { return "nil" } else { name := reflect.TypeOf(frame).Elem().Name() return strings.TrimSuffix(name, "Frame") } } type direction uint8 const ( sent direction = iota received ) func (d direction) String() string { if d == sent { return "sent" } return "received" } ================================================ FILE: quic/param_unix.go ================================================ //go:build !windows package quic const ( MaxDatagramFrameSize = 1350 // maxDatagramPayloadSize is the maximum packet size allowed by warp client maxDatagramPayloadSize = 1280 ) ================================================ FILE: quic/param_windows.go ================================================ //go:build windows package quic const ( // Due to https://github.com/quic-go/quic-go/issues/3273, MTU discovery is disabled on Windows // 1220 is the default value https://github.com/quic-go/quic-go/blob/84e03e59760ceee37359688871bb0688fcc4e98f/internal/protocol/params.go#L138 MaxDatagramFrameSize = 1220 // 3 more bytes are reserved at https://github.com/quic-go/quic-go/blob/v0.24.0/internal/wire/datagram_frame.go#L61 maxDatagramPayloadSize = MaxDatagramFrameSize - 3 - sessionIDLen - typeIDLen ) ================================================ FILE: quic/safe_stream.go ================================================ package quic import ( "errors" "net" "sync" "sync/atomic" "time" "github.com/quic-go/quic-go" "github.com/rs/zerolog" "github.com/rs/zerolog/log" ) // The error that is throw by the writer when there is `no network activity`. var idleTimeoutError = quic.IdleTimeoutError{} type SafeStreamCloser struct { lock sync.Mutex stream quic.Stream writeTimeout time.Duration log *zerolog.Logger closing atomic.Bool } func NewSafeStreamCloser(stream quic.Stream, writeTimeout time.Duration, log *zerolog.Logger) *SafeStreamCloser { return &SafeStreamCloser{ stream: stream, writeTimeout: writeTimeout, log: log, } } func (s *SafeStreamCloser) Read(p []byte) (n int, err error) { return s.stream.Read(p) } func (s *SafeStreamCloser) Write(p []byte) (n int, err error) { s.lock.Lock() defer s.lock.Unlock() if s.writeTimeout > 0 { err = s.stream.SetWriteDeadline(time.Now().Add(s.writeTimeout)) if err != nil { log.Err(err).Msg("Error setting write deadline for QUIC stream") } } nBytes, err := s.stream.Write(p) if err != nil { s.handleWriteError(err) } return nBytes, err } // Handles the timeout error in case it happened, by canceling the stream write. func (s *SafeStreamCloser) handleWriteError(err error) { // If we are closing the stream we just ignore any write error. if s.closing.Load() { return } var netErr net.Error if errors.As(err, &netErr) { if netErr.Timeout() { // We don't need to log if what cause the timeout was no network activity. if !errors.Is(netErr, &idleTimeoutError) { s.log.Error().Err(netErr).Msg("Closing quic stream due to timeout while writing") } // We need to explicitly cancel the write so that it frees all buffers. s.stream.CancelWrite(0) } } } func (s *SafeStreamCloser) Close() error { // Set this stream to a closing state. s.closing.Store(true) // Make sure a possible writer does not block the lock forever. We need it, so we can close the writer // side of the stream safely. _ = s.stream.SetWriteDeadline(time.Now()) // This lock is eventually acquired despite Write also acquiring it, because we set a deadline to writes. s.lock.Lock() defer s.lock.Unlock() // We have to clean up the receiving stream ourselves since the Close in the bottom does not handle that. s.stream.CancelRead(0) return s.stream.Close() } func (s *SafeStreamCloser) CloseWrite() error { s.lock.Lock() defer s.lock.Unlock() // As documented by the quic-go library, this doesn't actually close the entire stream. // It prevents further writes, which in turn will result in an EOF signal being sent the other side of stream when // reading. // We can still read from this stream. return s.stream.Close() } func (s *SafeStreamCloser) SetDeadline(deadline time.Time) error { return s.stream.SetDeadline(deadline) } ================================================ FILE: quic/safe_stream_test.go ================================================ package quic import ( "context" "crypto/rand" "crypto/rsa" "crypto/tls" "crypto/x509" "encoding/pem" "io" "math/big" "net" "sync" "testing" "time" "github.com/rs/zerolog" "github.com/quic-go/quic-go" "github.com/stretchr/testify/require" ) var ( testTLSServerConfig = GenerateTLSConfig() testQUICConfig = &quic.Config{ KeepAlivePeriod: 5 * time.Second, EnableDatagrams: true, } exchanges = 1000 msgsPerExchange = 10 testMsg = "Ok message" ) func TestSafeStreamClose(t *testing.T) { udpAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:0") require.NoError(t, err) udpListener, err := net.ListenUDP(udpAddr.Network(), udpAddr) require.NoError(t, err) defer udpListener.Close() var serverReady sync.WaitGroup serverReady.Add(1) var done sync.WaitGroup done.Add(1) go func() { defer done.Done() quicServer(t, &serverReady, udpListener) }() done.Add(1) go func() { serverReady.Wait() defer done.Done() quicClient(t, udpListener.LocalAddr()) }() done.Wait() } func quicClient(t *testing.T, addr net.Addr) { tlsConf := &tls.Config{ InsecureSkipVerify: true, NextProtos: []string{"argotunnel"}, } ctx, cancel := context.WithCancel(context.Background()) defer cancel() session, err := quic.DialAddr(ctx, addr.String(), tlsConf, testQUICConfig) require.NoError(t, err) var wg sync.WaitGroup for exchange := 0; exchange < exchanges; exchange++ { quicStream, err := session.AcceptStream(context.Background()) require.NoError(t, err) wg.Add(1) go func(iter int) { defer wg.Done() log := zerolog.Nop() stream := NewSafeStreamCloser(quicStream, 30*time.Second, &log) defer stream.Close() // Do a bunch of round trips over this stream that should work. for msg := 0; msg < msgsPerExchange; msg++ { clientRoundTrip(t, stream, true) } // And one that won't work necessarily, but shouldn't break other streams in the session. if iter%2 == 0 { clientRoundTrip(t, stream, false) } }(exchange) } wg.Wait() } func quicServer(t *testing.T, serverReady *sync.WaitGroup, conn net.PacketConn) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() earlyListener, err := quic.Listen(conn, testTLSServerConfig, testQUICConfig) require.NoError(t, err) serverReady.Done() session, err := earlyListener.Accept(ctx) require.NoError(t, err) var wg sync.WaitGroup for exchange := 0; exchange < exchanges; exchange++ { quicStream, err := session.OpenStreamSync(context.Background()) require.NoError(t, err) wg.Add(1) go func(iter int) { defer wg.Done() log := zerolog.Nop() stream := NewSafeStreamCloser(quicStream, 30*time.Second, &log) defer stream.Close() // Do a bunch of round trips over this stream that should work. for msg := 0; msg < msgsPerExchange; msg++ { serverRoundTrip(t, stream, true) } // And one that won't work necessarily, but shouldn't break other streams in the session. if iter%2 == 1 { serverRoundTrip(t, stream, false) } }(exchange) } wg.Wait() } func clientRoundTrip(t *testing.T, stream io.ReadWriteCloser, mustWork bool) { response := make([]byte, len(testMsg)) _, err := stream.Read(response) if !mustWork { return } if err != io.EOF { require.NoError(t, err) } require.Equal(t, testMsg, string(response)) } func serverRoundTrip(t *testing.T, stream io.ReadWriteCloser, mustWork bool) { _, err := stream.Write([]byte(testMsg)) if !mustWork { return } require.NoError(t, err) } // GenerateTLSConfig sets up a bare-bones TLS config for a QUIC server func GenerateTLSConfig() *tls.Config { key, err := rsa.GenerateKey(rand.Reader, 1024) if err != nil { panic(err) } template := x509.Certificate{SerialNumber: big.NewInt(1)} certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key) if err != nil { panic(err) } keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}) certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) tlsCert, err := tls.X509KeyPair(certPEM, keyPEM) if err != nil { panic(err) } return &tls.Config{ Certificates: []tls.Certificate{tlsCert}, NextProtos: []string{"argotunnel"}, } } ================================================ FILE: quic/tracing.go ================================================ package quic import ( "context" "net" "github.com/quic-go/quic-go/logging" "github.com/rs/zerolog" ) // QUICTracer is a wrapper to create new quicConnTracer type tracer struct { index string logger *zerolog.Logger } func NewClientTracer(logger *zerolog.Logger, index uint8) func(context.Context, logging.Perspective, logging.ConnectionID) *logging.ConnectionTracer { t := &tracer{ index: uint8ToString(index), logger: logger, } return t.TracerForConnection } func (t *tracer) TracerForConnection(_ctx context.Context, _p logging.Perspective, _odcid logging.ConnectionID) *logging.ConnectionTracer { return newConnTracer(newClientCollector(t.index, t.logger)) } // connTracer collects connection level metrics type connTracer struct { metricsCollector *clientCollector } func newConnTracer(metricsCollector *clientCollector) *logging.ConnectionTracer { tracer := connTracer{ metricsCollector: metricsCollector, } return &logging.ConnectionTracer{ StartedConnection: tracer.StartedConnection, ClosedConnection: tracer.ClosedConnection, ReceivedTransportParameters: tracer.ReceivedTransportParameters, SentLongHeaderPacket: tracer.SentLongHeaderPacket, SentShortHeaderPacket: tracer.SentShortHeaderPacket, ReceivedLongHeaderPacket: tracer.ReceivedLongHeaderPacket, ReceivedShortHeaderPacket: tracer.ReceivedShortHeaderPacket, BufferedPacket: tracer.BufferedPacket, DroppedPacket: tracer.DroppedPacket, UpdatedMetrics: tracer.UpdatedMetrics, LostPacket: tracer.LostPacket, UpdatedMTU: tracer.UpdatedMTU, UpdatedCongestionState: tracer.UpdatedCongestionState, } } func (ct *connTracer) StartedConnection(local, remote net.Addr, srcConnID, destConnID logging.ConnectionID) { ct.metricsCollector.startedConnection() } func (ct *connTracer) ClosedConnection(err error) { ct.metricsCollector.closedConnection(err) } func (ct *connTracer) ReceivedTransportParameters(params *logging.TransportParameters) { ct.metricsCollector.receivedTransportParameters(params) } func (ct *connTracer) BufferedPacket(pt logging.PacketType, size logging.ByteCount) { ct.metricsCollector.bufferedPackets(pt) } func (ct *connTracer) DroppedPacket(pt logging.PacketType, number logging.PacketNumber, size logging.ByteCount, reason logging.PacketDropReason) { ct.metricsCollector.droppedPackets(pt, size, reason) } func (ct *connTracer) LostPacket(level logging.EncryptionLevel, number logging.PacketNumber, reason logging.PacketLossReason) { ct.metricsCollector.lostPackets(reason) } func (ct *connTracer) UpdatedMetrics(rttStats *logging.RTTStats, cwnd, bytesInFlight logging.ByteCount, packetsInFlight int) { ct.metricsCollector.updatedRTT(rttStats) ct.metricsCollector.updateCongestionWindow(cwnd) } func (ct *connTracer) SentLongHeaderPacket(hdr *logging.ExtendedHeader, size logging.ByteCount, ecn logging.ECN, ack *logging.AckFrame, frames []logging.Frame) { ct.metricsCollector.sentPackets(size, frames) } func (ct *connTracer) SentShortHeaderPacket(hdr *logging.ShortHeader, size logging.ByteCount, ecn logging.ECN, ack *logging.AckFrame, frames []logging.Frame) { ct.metricsCollector.sentPackets(size, frames) } func (ct *connTracer) ReceivedLongHeaderPacket(hdr *logging.ExtendedHeader, size logging.ByteCount, ecn logging.ECN, frames []logging.Frame) { ct.metricsCollector.receivedPackets(size, frames) } func (ct *connTracer) ReceivedShortHeaderPacket(hdr *logging.ShortHeader, size logging.ByteCount, ecn logging.ECN, frames []logging.Frame) { ct.metricsCollector.receivedPackets(size, frames) } func (ct *connTracer) UpdatedMTU(mtu logging.ByteCount, done bool) { ct.metricsCollector.updateMTU(mtu) } func (ct *connTracer) UpdatedCongestionState(state logging.CongestionState) { ct.metricsCollector.updatedCongestionState(state) } ================================================ FILE: quic/v3/datagram.go ================================================ package v3 import ( "encoding/binary" "net/netip" "time" ) type DatagramType byte const ( // UDP Registration UDPSessionRegistrationType DatagramType = 0x0 // UDP Session Payload UDPSessionPayloadType DatagramType = 0x1 // DatagramTypeICMP (supporting both ICMPv4 and ICMPv6) ICMPType DatagramType = 0x2 // UDP Session Registration Response UDPSessionRegistrationResponseType DatagramType = 0x3 ) const ( // Total number of bytes representing the [DatagramType] datagramTypeLen = 1 // 1280 is the default datagram packet length used before MTU discovery: https://github.com/quic-go/quic-go/blob/v0.45.0/internal/protocol/params.go#L12 maxDatagramPayloadLen = 1280 ) func ParseDatagramType(data []byte) (DatagramType, error) { if len(data) < datagramTypeLen { return 0, ErrDatagramHeaderTooSmall } return DatagramType(data[0]), nil } // UDPSessionRegistrationDatagram handles a request to initialize a UDP session on the remote client. type UDPSessionRegistrationDatagram struct { RequestID RequestID Dest netip.AddrPort Traced bool IdleDurationHint time.Duration Payload []byte } const ( sessionRegistrationFlagsIPMask byte = 0b0000_0001 sessionRegistrationFlagsTracedMask byte = 0b0000_0010 sessionRegistrationFlagsBundledMask byte = 0b0000_0100 sessionRegistrationIPv4DatagramHeaderLen = datagramTypeLen + 1 + // Flag length 2 + // Destination port length 2 + // Idle duration seconds length datagramRequestIdLen + // Request ID length 4 // IPv4 address length // The IPv4 and IPv6 address share space, so adding 12 to the header length gets the space taken by the IPv6 field. sessionRegistrationIPv6DatagramHeaderLen = sessionRegistrationIPv4DatagramHeaderLen + 12 ) // The datagram structure for UDPSessionRegistrationDatagram is: // // 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // 0| Type | Flags | Destination Port | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // 4| Idle Duration Seconds | | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // 8| | // + Session Identifier + // 12| (16 Bytes) | // + + // 16| | // + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // 20| | Destination IPv4 Address | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- - - - - - - - - - - - - - - -+ // 24| Destination IPv4 Address cont | | // +- - - - - - - - - - - - - - - - + // 28| Destination IPv6 Address | // + (extension of IPv4 region) + // 32| | // + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // 36| | | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // . . // . Bundle Payload . // . . // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ func (s *UDPSessionRegistrationDatagram) MarshalBinary() (data []byte, err error) { ipv6 := s.Dest.Addr().Is6() var flags byte if s.Traced { flags |= sessionRegistrationFlagsTracedMask } hasPayload := len(s.Payload) > 0 if hasPayload { flags |= sessionRegistrationFlagsBundledMask } var maxPayloadLen int if ipv6 { maxPayloadLen = maxDatagramPayloadLen + sessionRegistrationIPv6DatagramHeaderLen flags |= sessionRegistrationFlagsIPMask } else { maxPayloadLen = maxDatagramPayloadLen + sessionRegistrationIPv4DatagramHeaderLen } // Make sure that the payload being bundled can actually fit in the payload destination if len(s.Payload) > maxPayloadLen { return nil, wrapMarshalErr(ErrDatagramPayloadTooLarge) } // Allocate the buffer with the right size for the destination IP family if ipv6 { data = make([]byte, sessionRegistrationIPv6DatagramHeaderLen+len(s.Payload)) } else { data = make([]byte, sessionRegistrationIPv4DatagramHeaderLen+len(s.Payload)) } data[0] = byte(UDPSessionRegistrationType) data[1] = flags binary.BigEndian.PutUint16(data[2:4], s.Dest.Port()) binary.BigEndian.PutUint16(data[4:6], uint16(s.IdleDurationHint.Seconds())) err = s.RequestID.MarshalBinaryTo(data[6:22]) if err != nil { return nil, wrapMarshalErr(err) } var end int if ipv6 { copy(data[22:38], s.Dest.Addr().AsSlice()) end = 38 } else { copy(data[22:26], s.Dest.Addr().AsSlice()) end = 26 } if hasPayload { copy(data[end:], s.Payload) } return data, nil } func (s *UDPSessionRegistrationDatagram) UnmarshalBinary(data []byte) error { datagramType, err := ParseDatagramType(data) if err != nil { return err } if datagramType != UDPSessionRegistrationType { return wrapUnmarshalErr(ErrInvalidDatagramType) } requestID, err := RequestIDFromSlice(data[6:22]) if err != nil { return wrapUnmarshalErr(err) } traced := (data[1] & sessionRegistrationFlagsTracedMask) == sessionRegistrationFlagsTracedMask bundled := (data[1] & sessionRegistrationFlagsBundledMask) == sessionRegistrationFlagsBundledMask ipv6 := (data[1] & sessionRegistrationFlagsIPMask) == sessionRegistrationFlagsIPMask port := binary.BigEndian.Uint16(data[2:4]) var datagramHeaderSize int var dest netip.AddrPort if ipv6 { datagramHeaderSize = sessionRegistrationIPv6DatagramHeaderLen dest = netip.AddrPortFrom(netip.AddrFrom16([16]byte(data[22:38])), port) } else { datagramHeaderSize = sessionRegistrationIPv4DatagramHeaderLen dest = netip.AddrPortFrom(netip.AddrFrom4([4]byte(data[22:26])), port) } idle := time.Duration(binary.BigEndian.Uint16(data[4:6])) * time.Second var payload []byte if bundled && len(data) >= datagramHeaderSize && len(data[datagramHeaderSize:]) > 0 { payload = data[datagramHeaderSize:] } *s = UDPSessionRegistrationDatagram{ RequestID: requestID, Dest: dest, Traced: traced, IdleDurationHint: idle, Payload: payload, } return nil } // UDPSessionPayloadDatagram provides the payload for a session to be send to either the origin or the client. type UDPSessionPayloadDatagram struct { RequestID RequestID Payload []byte } const ( DatagramPayloadHeaderLen = datagramTypeLen + datagramRequestIdLen // The maximum size that a proxied UDP payload can be in a [UDPSessionPayloadDatagram] maxPayloadPlusHeaderLen = maxDatagramPayloadLen + DatagramPayloadHeaderLen ) // The datagram structure for UDPSessionPayloadDatagram is: // // 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // 0| Type | | // +-+-+-+-+-+-+-+-+ + // 4| | // + + // 8| Session Identifier | // + (16 Bytes) + // 12| | // + +-+-+-+-+-+-+-+-+ // 16| | | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // . . // . Payload . // . . // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // MarshalPayloadHeaderTo provides a way to insert the Session Payload header into an already existing byte slice // without having to allocate and copy the payload into the destination. // // This method should be used in-place of MarshalBinary which will allocate in-place the required byte array to return. func MarshalPayloadHeaderTo(requestID RequestID, payload []byte) error { if len(payload) < DatagramPayloadHeaderLen { return wrapMarshalErr(ErrDatagramPayloadHeaderTooSmall) } payload[0] = byte(UDPSessionPayloadType) return requestID.MarshalBinaryTo(payload[1:DatagramPayloadHeaderLen]) } func (s *UDPSessionPayloadDatagram) UnmarshalBinary(data []byte) error { datagramType, err := ParseDatagramType(data) if err != nil { return err } if datagramType != UDPSessionPayloadType { return wrapUnmarshalErr(ErrInvalidDatagramType) } // Make sure that the slice provided is the right size to be parsed. if len(data) < DatagramPayloadHeaderLen || len(data) > maxPayloadPlusHeaderLen { return wrapUnmarshalErr(ErrDatagramPayloadInvalidSize) } requestID, err := RequestIDFromSlice(data[1:DatagramPayloadHeaderLen]) if err != nil { return wrapUnmarshalErr(err) } *s = UDPSessionPayloadDatagram{ RequestID: requestID, Payload: data[DatagramPayloadHeaderLen:], } return nil } // UDPSessionRegistrationResponseDatagram is used to either return a successful registration or error to the client // that requested the registration of a UDP session. type UDPSessionRegistrationResponseDatagram struct { RequestID RequestID ResponseType SessionRegistrationResp ErrorMsg string } const ( datagramRespTypeLen = 1 datagramRespErrMsgLen = 2 datagramSessionRegistrationResponseLen = datagramTypeLen + datagramRespTypeLen + datagramRequestIdLen + datagramRespErrMsgLen // The maximum size that an error message can be in a [UDPSessionRegistrationResponseDatagram]. maxResponseErrorMessageLen = maxDatagramPayloadLen - datagramSessionRegistrationResponseLen ) // SessionRegistrationResp represents all of the responses that a UDP session registration response // can return back to the client. type SessionRegistrationResp byte const ( // Session was received and is ready to proxy. ResponseOk SessionRegistrationResp = 0x00 // Session registration was unable to reach the requested origin destination. ResponseDestinationUnreachable SessionRegistrationResp = 0x01 // Session registration was unable to bind to a local UDP socket. ResponseUnableToBindSocket SessionRegistrationResp = 0x02 // Session registration failed due to the number of flows being higher than the limit. ResponseTooManyActiveFlows SessionRegistrationResp = 0x03 // Session registration failed with an unexpected error but provided a message. ResponseErrorWithMsg SessionRegistrationResp = 0xff ) // The datagram structure for UDPSessionRegistrationResponseDatagram is: // // 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // 0| Type | Resp Type | | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // 4| | // + Session Identifier + // 8| (16 Bytes) | // + + // 12| | // + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // 16| | Error Length | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // . . // . . // . . // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ func (s *UDPSessionRegistrationResponseDatagram) MarshalBinary() (data []byte, err error) { if len(s.ErrorMsg) > maxResponseErrorMessageLen { return nil, wrapMarshalErr(ErrDatagramResponseMsgInvalidSize) } // nolint: gosec errMsgLen := uint16(len(s.ErrorMsg)) data = make([]byte, datagramSessionRegistrationResponseLen+errMsgLen) data[0] = byte(UDPSessionRegistrationResponseType) data[1] = byte(s.ResponseType) err = s.RequestID.MarshalBinaryTo(data[2:18]) if err != nil { return nil, wrapMarshalErr(err) } if errMsgLen > 0 { binary.BigEndian.PutUint16(data[18:20], errMsgLen) copy(data[20:], []byte(s.ErrorMsg)) } return data, nil } func (s *UDPSessionRegistrationResponseDatagram) UnmarshalBinary(data []byte) error { datagramType, err := ParseDatagramType(data) if err != nil { return wrapUnmarshalErr(err) } if datagramType != UDPSessionRegistrationResponseType { return wrapUnmarshalErr(ErrInvalidDatagramType) } if len(data) < datagramSessionRegistrationResponseLen { return wrapUnmarshalErr(ErrDatagramResponseInvalidSize) } respType := SessionRegistrationResp(data[1]) requestID, err := RequestIDFromSlice(data[2:18]) if err != nil { return wrapUnmarshalErr(err) } errMsgLen := binary.BigEndian.Uint16(data[18:20]) if errMsgLen > maxResponseErrorMessageLen { return wrapUnmarshalErr(ErrDatagramResponseMsgTooLargeMaximum) } if len(data[20:]) < int(errMsgLen) { return wrapUnmarshalErr(ErrDatagramResponseMsgTooLargeDatagram) } var errMsg string if errMsgLen > 0 { errMsg = string(data[20:]) } *s = UDPSessionRegistrationResponseDatagram{ RequestID: requestID, ResponseType: respType, ErrorMsg: errMsg, } return nil } // ICMPDatagram is used to propagate ICMPv4 and ICMPv6 payloads. type ICMPDatagram struct { Payload []byte } // The maximum size that an ICMP packet can be. const maxICMPPayloadLen = maxDatagramPayloadLen // The datagram structure for ICMPDatagram is: // // 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // 0| Type | | // +-+-+-+-+-+-+-+-+ + // . Payload . // . . // . . // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ func (d *ICMPDatagram) MarshalBinary() (data []byte, err error) { if len(d.Payload) > maxICMPPayloadLen { return nil, wrapMarshalErr(ErrDatagramICMPPayloadTooLarge) } // We shouldn't attempt to marshal an ICMP datagram with no ICMP payload provided if len(d.Payload) == 0 { return nil, wrapMarshalErr(ErrDatagramICMPPayloadMissing) } // Make room for the 1 byte ICMPType header datagram := make([]byte, len(d.Payload)+datagramTypeLen) datagram[0] = byte(ICMPType) copy(datagram[1:], d.Payload) return datagram, nil } func (d *ICMPDatagram) UnmarshalBinary(data []byte) error { datagramType, err := ParseDatagramType(data) if err != nil { return wrapUnmarshalErr(err) } if datagramType != ICMPType { return wrapUnmarshalErr(ErrInvalidDatagramType) } if len(data[1:]) > maxDatagramPayloadLen { return wrapUnmarshalErr(ErrDatagramICMPPayloadTooLarge) } // We shouldn't attempt to unmarshal an ICMP datagram with no ICMP payload provided if len(data[1:]) == 0 { return wrapUnmarshalErr(ErrDatagramICMPPayloadMissing) } payload := make([]byte, len(data[1:])) copy(payload, data[1:]) d.Payload = payload return nil } ================================================ FILE: quic/v3/datagram_errors.go ================================================ package v3 import ( "errors" "fmt" ) var ( ErrInvalidDatagramType error = errors.New("invalid datagram type expected") ErrDatagramHeaderTooSmall error = fmt.Errorf("datagram should have at least %d byte", datagramTypeLen) ErrDatagramPayloadTooLarge error = errors.New("payload length is too large to be bundled in datagram") ErrDatagramPayloadHeaderTooSmall error = errors.New("payload length is too small to fit the datagram header") ErrDatagramPayloadInvalidSize error = errors.New("datagram provided is an invalid size") ErrDatagramResponseMsgInvalidSize error = errors.New("datagram response message is an invalid size") ErrDatagramResponseInvalidSize error = errors.New("datagram response is an invalid size") ErrDatagramResponseMsgTooLargeMaximum error = fmt.Errorf("datagram response error message length exceeds the length of the datagram maximum: %d", maxResponseErrorMessageLen) ErrDatagramResponseMsgTooLargeDatagram error = fmt.Errorf("datagram response error message length exceeds the length of the provided datagram") ErrDatagramICMPPayloadTooLarge error = fmt.Errorf("datagram icmp payload exceeds %d bytes", maxICMPPayloadLen) ErrDatagramICMPPayloadMissing error = errors.New("datagram icmp payload is missing") ) func wrapMarshalErr(err error) error { return fmt.Errorf("datagram marshal error: %w", err) } func wrapUnmarshalErr(err error) error { return fmt.Errorf("datagram unmarshal error: %w", err) } ================================================ FILE: quic/v3/datagram_test.go ================================================ package v3_test import ( "crypto/rand" "encoding/binary" "errors" "net/netip" "testing" "time" "github.com/stretchr/testify/require" v3 "github.com/cloudflare/cloudflared/quic/v3" ) func makePayload(size int) []byte { payload := make([]byte, size) _, _ = rand.Read(payload) return payload } func makePayloads(size int, count int) [][]byte { payloads := make([][]byte, count) for i := range payloads { payloads[i] = makePayload(size) } return payloads } func TestSessionRegistration_MarshalUnmarshal(t *testing.T) { payload := makePayload(1280) tests := []*v3.UDPSessionRegistrationDatagram{ // Default (IPv4) { RequestID: testRequestID, Dest: netip.MustParseAddrPort("1.1.1.1:8080"), Traced: false, IdleDurationHint: 5 * time.Second, Payload: nil, }, // Request ID (max) { RequestID: mustRequestID([16]byte{ ^uint8(0), ^uint8(0), ^uint8(0), ^uint8(0), ^uint8(0), ^uint8(0), ^uint8(0), ^uint8(0), ^uint8(0), ^uint8(0), ^uint8(0), ^uint8(0), ^uint8(0), ^uint8(0), ^uint8(0), ^uint8(0), }), Dest: netip.MustParseAddrPort("1.1.1.1:8080"), Traced: false, IdleDurationHint: 5 * time.Second, Payload: nil, }, // IPv6 { RequestID: testRequestID, Dest: netip.MustParseAddrPort("[fc00::0]:8080"), Traced: false, IdleDurationHint: 5 * time.Second, Payload: nil, }, // Traced { RequestID: testRequestID, Dest: netip.MustParseAddrPort("1.1.1.1:8080"), Traced: true, IdleDurationHint: 5 * time.Second, Payload: nil, }, // IdleDurationHint (max) { RequestID: testRequestID, Dest: netip.MustParseAddrPort("1.1.1.1:8080"), Traced: false, IdleDurationHint: 65535 * time.Second, Payload: nil, }, // Payload { RequestID: testRequestID, Dest: netip.MustParseAddrPort("1.1.1.1:8080"), Traced: false, IdleDurationHint: 5 * time.Second, Payload: []byte{0xff, 0xaa, 0xcc, 0x44}, }, // Payload (max: 1254) for IPv4 { RequestID: testRequestID, Dest: netip.MustParseAddrPort("1.1.1.1:8080"), Traced: false, IdleDurationHint: 5 * time.Second, Payload: payload, }, // Payload (max: 1242) for IPv4 { RequestID: testRequestID, Dest: netip.MustParseAddrPort("1.1.1.1:8080"), Traced: false, IdleDurationHint: 5 * time.Second, Payload: payload[:1242], }, } for _, tt := range tests { marshaled, err := tt.MarshalBinary() if err != nil { t.Error(err) } unmarshaled := v3.UDPSessionRegistrationDatagram{} err = unmarshaled.UnmarshalBinary(marshaled) if err != nil { t.Error(err) } if !compareRegistrationDatagrams(t, tt, &unmarshaled) { t.Errorf("not equal:\n%+v\n%+v", tt, &unmarshaled) } } } func TestSessionRegistration_MarshalBinary(t *testing.T) { t.Run("idle hint too large", func(t *testing.T) { // idle hint duration overflows back to 1 datagram := &v3.UDPSessionRegistrationDatagram{ RequestID: testRequestID, Dest: netip.MustParseAddrPort("1.1.1.1:8080"), Traced: false, IdleDurationHint: 65537 * time.Second, Payload: nil, } expected := &v3.UDPSessionRegistrationDatagram{ RequestID: testRequestID, Dest: netip.MustParseAddrPort("1.1.1.1:8080"), Traced: false, IdleDurationHint: 1 * time.Second, Payload: nil, } marshaled, err := datagram.MarshalBinary() if err != nil { t.Error(err) } unmarshaled := v3.UDPSessionRegistrationDatagram{} err = unmarshaled.UnmarshalBinary(marshaled) if err != nil { t.Error(err) } if !compareRegistrationDatagrams(t, expected, &unmarshaled) { t.Errorf("not equal:\n%+v\n%+v", expected, &unmarshaled) } }) } func TestTypeUnmarshalErrors(t *testing.T) { t.Run("invalid length", func(t *testing.T) { d1 := v3.UDPSessionRegistrationDatagram{} err := d1.UnmarshalBinary([]byte{}) if !errors.Is(err, v3.ErrDatagramHeaderTooSmall) { t.Errorf("expected invalid length to throw error") } d2 := v3.UDPSessionPayloadDatagram{} err = d2.UnmarshalBinary([]byte{}) if !errors.Is(err, v3.ErrDatagramHeaderTooSmall) { t.Errorf("expected invalid length to throw error") } d3 := v3.UDPSessionRegistrationResponseDatagram{} err = d3.UnmarshalBinary([]byte{}) if !errors.Is(err, v3.ErrDatagramHeaderTooSmall) { t.Errorf("expected invalid length to throw error") } d4 := v3.ICMPDatagram{} err = d4.UnmarshalBinary([]byte{}) if !errors.Is(err, v3.ErrDatagramHeaderTooSmall) { t.Errorf("expected invalid length to throw error") } }) t.Run("invalid types", func(t *testing.T) { d1 := v3.UDPSessionRegistrationDatagram{} err := d1.UnmarshalBinary([]byte{byte(v3.UDPSessionRegistrationResponseType)}) if !errors.Is(err, v3.ErrInvalidDatagramType) { t.Errorf("expected invalid type to throw error") } d2 := v3.UDPSessionPayloadDatagram{} err = d2.UnmarshalBinary([]byte{byte(v3.UDPSessionRegistrationType)}) if !errors.Is(err, v3.ErrInvalidDatagramType) { t.Errorf("expected invalid type to throw error") } d3 := v3.UDPSessionRegistrationResponseDatagram{} err = d3.UnmarshalBinary([]byte{byte(v3.UDPSessionPayloadType)}) if !errors.Is(err, v3.ErrInvalidDatagramType) { t.Errorf("expected invalid type to throw error") } d4 := v3.ICMPDatagram{} err = d4.UnmarshalBinary([]byte{byte(v3.UDPSessionPayloadType)}) if !errors.Is(err, v3.ErrInvalidDatagramType) { t.Errorf("expected invalid type to throw error") } }) } func TestSessionPayload(t *testing.T) { t.Run("basic", func(t *testing.T) { payload := makePayload(128) err := v3.MarshalPayloadHeaderTo(testRequestID, payload[0:17]) if err != nil { t.Error(err) } unmarshaled := v3.UDPSessionPayloadDatagram{} err = unmarshaled.UnmarshalBinary(payload) if err != nil { t.Error(err) } require.Equal(t, testRequestID, unmarshaled.RequestID) require.Equal(t, payload[17:], unmarshaled.Payload) }) t.Run("empty", func(t *testing.T) { payload := makePayload(17) err := v3.MarshalPayloadHeaderTo(testRequestID, payload) if err != nil { t.Error(err) } unmarshaled := v3.UDPSessionPayloadDatagram{} err = unmarshaled.UnmarshalBinary(payload) if err != nil { t.Error(err) } require.Equal(t, testRequestID, unmarshaled.RequestID) require.Equal(t, payload[17:], unmarshaled.Payload) }) t.Run("header size too small", func(t *testing.T) { payload := makePayload(16) err := v3.MarshalPayloadHeaderTo(testRequestID, payload) if !errors.Is(err, v3.ErrDatagramPayloadHeaderTooSmall) { t.Errorf("expected an error") } }) t.Run("payload size too small", func(t *testing.T) { payload := makePayload(17) err := v3.MarshalPayloadHeaderTo(testRequestID, payload) if err != nil { t.Error(err) } unmarshaled := v3.UDPSessionPayloadDatagram{} err = unmarshaled.UnmarshalBinary(payload[:16]) if !errors.Is(err, v3.ErrDatagramPayloadInvalidSize) { t.Errorf("expected an error: %s", err) } }) t.Run("payload size too large", func(t *testing.T) { datagram := makePayload(17 + 1281) // 1280 is the largest payload size allowed err := v3.MarshalPayloadHeaderTo(testRequestID, datagram) if err != nil { t.Error(err) } unmarshaled := v3.UDPSessionPayloadDatagram{} err = unmarshaled.UnmarshalBinary(datagram[:]) if !errors.Is(err, v3.ErrDatagramPayloadInvalidSize) { t.Errorf("expected an error: %s", err) } }) } func TestSessionRegistrationResponse(t *testing.T) { validRespTypes := []v3.SessionRegistrationResp{ v3.ResponseOk, v3.ResponseDestinationUnreachable, v3.ResponseUnableToBindSocket, v3.ResponseErrorWithMsg, } t.Run("basic", func(t *testing.T) { for _, responseType := range validRespTypes { datagram := &v3.UDPSessionRegistrationResponseDatagram{ RequestID: testRequestID, ResponseType: responseType, ErrorMsg: "test", } marshaled, err := datagram.MarshalBinary() if err != nil { t.Error(err) } unmarshaled := &v3.UDPSessionRegistrationResponseDatagram{} err = unmarshaled.UnmarshalBinary(marshaled) if err != nil { t.Error(err) } require.Equal(t, datagram, unmarshaled) } }) t.Run("unsupported resp type is valid", func(t *testing.T) { datagram := &v3.UDPSessionRegistrationResponseDatagram{ RequestID: testRequestID, ResponseType: v3.SessionRegistrationResp(0xfc), ErrorMsg: "", } marshaled, err := datagram.MarshalBinary() if err != nil { t.Error(err) } unmarshaled := &v3.UDPSessionRegistrationResponseDatagram{} err = unmarshaled.UnmarshalBinary(marshaled) if err != nil { t.Error(err) } require.Equal(t, datagram, unmarshaled) }) t.Run("too small to unmarshal", func(t *testing.T) { payload := makePayload(17) payload[0] = byte(v3.UDPSessionRegistrationResponseType) unmarshaled := &v3.UDPSessionRegistrationResponseDatagram{} err := unmarshaled.UnmarshalBinary(payload) if !errors.Is(err, v3.ErrDatagramResponseInvalidSize) { t.Errorf("expected an error") } }) t.Run("error message too long", func(t *testing.T) { message := "" for i := 0; i < 1280; i++ { message += "a" } datagram := &v3.UDPSessionRegistrationResponseDatagram{ RequestID: testRequestID, ResponseType: v3.SessionRegistrationResp(0xfc), ErrorMsg: message, } _, err := datagram.MarshalBinary() if !errors.Is(err, v3.ErrDatagramResponseMsgInvalidSize) { t.Errorf("expected an error") } }) t.Run("error message too large to unmarshal", func(t *testing.T) { payload := makePayload(1280) payload[0] = byte(v3.UDPSessionRegistrationResponseType) binary.BigEndian.PutUint16(payload[18:20], 1280) // larger than the datagram size could be unmarshaled := &v3.UDPSessionRegistrationResponseDatagram{} err := unmarshaled.UnmarshalBinary(payload) if !errors.Is(err, v3.ErrDatagramResponseMsgTooLargeMaximum) { t.Errorf("expected an error: %v", err) } }) t.Run("error message larger than provided buffer", func(t *testing.T) { payload := makePayload(1000) payload[0] = byte(v3.UDPSessionRegistrationResponseType) binary.BigEndian.PutUint16(payload[18:20], 1001) // larger than the datagram size provided unmarshaled := &v3.UDPSessionRegistrationResponseDatagram{} err := unmarshaled.UnmarshalBinary(payload) if !errors.Is(err, v3.ErrDatagramResponseMsgTooLargeDatagram) { t.Errorf("expected an error: %v", err) } }) } func TestICMPDatagram(t *testing.T) { t.Run("basic", func(t *testing.T) { payload := makePayload(128) datagram := v3.ICMPDatagram{Payload: payload} marshaled, err := datagram.MarshalBinary() if err != nil { t.Error(err) } unmarshaled := &v3.ICMPDatagram{} err = unmarshaled.UnmarshalBinary(marshaled) if err != nil { t.Error(err) } require.Equal(t, payload, unmarshaled.Payload) }) t.Run("payload size empty", func(t *testing.T) { payload := []byte{} datagram := v3.ICMPDatagram{Payload: payload} _, err := datagram.MarshalBinary() if !errors.Is(err, v3.ErrDatagramICMPPayloadMissing) { t.Errorf("expected an error: %s", err) } payload = []byte{byte(v3.ICMPType)} unmarshaled := &v3.ICMPDatagram{} err = unmarshaled.UnmarshalBinary(payload) if !errors.Is(err, v3.ErrDatagramICMPPayloadMissing) { t.Errorf("expected an error: %s", err) } }) t.Run("payload size too large", func(t *testing.T) { payload := makePayload(1280 + 1) // larger than the datagram size could be datagram := v3.ICMPDatagram{Payload: payload} _, err := datagram.MarshalBinary() if !errors.Is(err, v3.ErrDatagramICMPPayloadTooLarge) { t.Errorf("expected an error: %s", err) } payload = makePayload(1280 + 2) // larger than the datagram size could be + header payload[0] = byte(v3.ICMPType) unmarshaled := &v3.ICMPDatagram{} err = unmarshaled.UnmarshalBinary(payload) if !errors.Is(err, v3.ErrDatagramICMPPayloadTooLarge) { t.Errorf("expected an error: %s", err) } }) } func compareRegistrationDatagrams(t *testing.T, l *v3.UDPSessionRegistrationDatagram, r *v3.UDPSessionRegistrationDatagram) bool { require.Equal(t, l.Payload, r.Payload) return l.RequestID == r.RequestID && l.Dest == r.Dest && l.IdleDurationHint == r.IdleDurationHint && l.Traced == r.Traced } func FuzzRegistrationDatagram(f *testing.F) { f.Fuzz(func(t *testing.T, data []byte) { unmarshaled := v3.UDPSessionRegistrationDatagram{} err := unmarshaled.UnmarshalBinary(data) if err == nil { _, _ = unmarshaled.MarshalBinary() } }) } func FuzzPayloadDatagram(f *testing.F) { f.Fuzz(func(t *testing.T, data []byte) { unmarshaled := v3.UDPSessionPayloadDatagram{} _ = unmarshaled.UnmarshalBinary(data) }) } func FuzzRegistrationResponseDatagram(f *testing.F) { f.Fuzz(func(t *testing.T, data []byte) { unmarshaled := v3.UDPSessionRegistrationResponseDatagram{} err := unmarshaled.UnmarshalBinary(data) if err == nil { _, _ = unmarshaled.MarshalBinary() } }) } func FuzzICMPDatagram(f *testing.F) { f.Fuzz(func(t *testing.T, data []byte) { unmarshaled := v3.ICMPDatagram{} err := unmarshaled.UnmarshalBinary(data) if err == nil { _, _ = unmarshaled.MarshalBinary() } }) } ================================================ FILE: quic/v3/icmp.go ================================================ package v3 import ( "context" "github.com/rs/zerolog" "go.opentelemetry.io/otel/trace" "github.com/cloudflare/cloudflared/ingress" "github.com/cloudflare/cloudflared/packet" "github.com/cloudflare/cloudflared/tracing" ) // packetResponder is an implementation of the [ingress.ICMPResponder] which provides the ICMP Flow manager the // return path to return and ICMP Echo response back to the QUIC muxer. type packetResponder struct { datagramMuxer DatagramICMPWriter connID uint8 } func newPacketResponder(datagramMuxer DatagramICMPWriter, connID uint8) ingress.ICMPResponder { return &packetResponder{ datagramMuxer, connID, } } func (pr *packetResponder) ConnectionIndex() uint8 { return pr.connID } func (pr *packetResponder) ReturnPacket(pk *packet.ICMP) error { return pr.datagramMuxer.SendICMPPacket(pk) } func (pr *packetResponder) AddTraceContext(tracedCtx *tracing.TracedContext, serializedIdentity []byte) { // datagram v3 does not support tracing ICMP packets } func (pr *packetResponder) RequestSpan(ctx context.Context, pk *packet.ICMP) (context.Context, trace.Span) { // datagram v3 does not support tracing ICMP packets return ctx, tracing.NewNoopSpan() } func (pr *packetResponder) ReplySpan(ctx context.Context, logger *zerolog.Logger) (context.Context, trace.Span) { // datagram v3 does not support tracing ICMP packets return ctx, tracing.NewNoopSpan() } func (pr *packetResponder) ExportSpan() { // datagram v3 does not support tracing ICMP packets } ================================================ FILE: quic/v3/icmp_test.go ================================================ package v3_test import ( "context" "testing" "github.com/cloudflare/cloudflared/ingress" "github.com/cloudflare/cloudflared/packet" ) type noopICMPRouter struct{} func (noopICMPRouter) Request(ctx context.Context, pk *packet.ICMP, responder ingress.ICMPResponder) error { return nil } func (noopICMPRouter) ConvertToTTLExceeded(pk *packet.ICMP, rawPacket packet.RawPacket) *packet.ICMP { return nil } type mockICMPRouter struct { recv chan *packet.ICMP } func newMockICMPRouter() *mockICMPRouter { return &mockICMPRouter{ recv: make(chan *packet.ICMP, 1), } } func (m *mockICMPRouter) Request(ctx context.Context, pk *packet.ICMP, responder ingress.ICMPResponder) error { m.recv <- pk return nil } func (mockICMPRouter) ConvertToTTLExceeded(pk *packet.ICMP, rawPacket packet.RawPacket) *packet.ICMP { return packet.NewICMPTTLExceedPacket(pk.IP, rawPacket, testLocalAddr.AddrPort().Addr()) } func assertICMPEqual(t *testing.T, expected *packet.ICMP, actual *packet.ICMP) { if expected.Src != actual.Src { t.Fatalf("Src address not equal: %+v\t%+v", expected, actual) } if expected.Dst != actual.Dst { t.Fatalf("Dst address not equal: %+v\t%+v", expected, actual) } } ================================================ FILE: quic/v3/manager.go ================================================ package v3 import ( "errors" "sync" "github.com/rs/zerolog" "github.com/cloudflare/cloudflared/ingress" "github.com/cloudflare/cloudflared/management" cfdflow "github.com/cloudflare/cloudflared/flow" ) var ( // ErrSessionNotFound indicates that a session has not been registered yet for the request id. ErrSessionNotFound = errors.New("flow not found") // ErrSessionBoundToOtherConn is returned when a registration already exists for a different connection. ErrSessionBoundToOtherConn = errors.New("flow is in use by another connection") // ErrSessionAlreadyRegistered is returned when a registration already exists for this connection. ErrSessionAlreadyRegistered = errors.New("flow is already registered for this connection") // ErrSessionRegistrationRateLimited is returned when a registration fails due to rate limiting on the number of active flows. ErrSessionRegistrationRateLimited = errors.New("flow registration rate limited") ) type SessionManager interface { // RegisterSession will register a new session if it does not already exist for the request ID. // During new session creation, the session will also bind the UDP socket for the origin. // If the session exists for a different connection, it will return [ErrSessionBoundToOtherConn]. RegisterSession(request *UDPSessionRegistrationDatagram, conn DatagramConn) (Session, error) // GetSession returns an active session if available for the provided connection. // If the session does not exist, it will return [ErrSessionNotFound]. If the session exists for a different // connection, it will return [ErrSessionBoundToOtherConn]. GetSession(requestID RequestID) (Session, error) // UnregisterSession will remove a session from the current session manager. It will attempt to close the session // before removal. UnregisterSession(requestID RequestID) } type sessionManager struct { sessions map[RequestID]Session mutex sync.RWMutex originDialer ingress.OriginUDPDialer limiter cfdflow.Limiter metrics Metrics log *zerolog.Logger } func NewSessionManager(metrics Metrics, log *zerolog.Logger, originDialer ingress.OriginUDPDialer, limiter cfdflow.Limiter) SessionManager { return &sessionManager{ sessions: make(map[RequestID]Session), originDialer: originDialer, limiter: limiter, metrics: metrics, log: log, } } func (s *sessionManager) RegisterSession(request *UDPSessionRegistrationDatagram, conn DatagramConn) (Session, error) { s.mutex.Lock() defer s.mutex.Unlock() // Check to make sure session doesn't already exist for requestID if session, exists := s.sessions[request.RequestID]; exists { if conn.ID() == session.ConnectionID() { return nil, ErrSessionAlreadyRegistered } return nil, ErrSessionBoundToOtherConn } // Try to start a new session if err := s.limiter.Acquire(management.UDP.String()); err != nil { return nil, ErrSessionRegistrationRateLimited } // Attempt to bind the UDP socket for the new session origin, err := s.originDialer.DialUDP(request.Dest) if err != nil { return nil, err } // Create and insert the new session in the map session := NewSession( request.RequestID, request.IdleDurationHint, origin, origin.RemoteAddr(), origin.LocalAddr(), conn, s.metrics, s.log) s.sessions[request.RequestID] = session return session, nil } func (s *sessionManager) GetSession(requestID RequestID) (Session, error) { s.mutex.RLock() defer s.mutex.RUnlock() session, exists := s.sessions[requestID] if exists { return session, nil } return nil, ErrSessionNotFound } func (s *sessionManager) UnregisterSession(requestID RequestID) { s.mutex.Lock() defer s.mutex.Unlock() // Get the session and make sure to close it if it isn't already closed session, exists := s.sessions[requestID] if exists { // We ignore any errors when attempting to close the session _ = session.Close() } delete(s.sessions, requestID) s.limiter.Release() } ================================================ FILE: quic/v3/manager_test.go ================================================ package v3_test import ( "errors" "net/netip" "strings" "testing" "time" "github.com/rs/zerolog" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" "github.com/cloudflare/cloudflared/config" "github.com/cloudflare/cloudflared/mocks" cfdflow "github.com/cloudflare/cloudflared/flow" "github.com/cloudflare/cloudflared/ingress" v3 "github.com/cloudflare/cloudflared/quic/v3" ) var ( testDefaultDialer = ingress.NewDialer(ingress.WarpRoutingConfig{ ConnectTimeout: config.CustomDuration{Duration: 1 * time.Second}, TCPKeepAlive: config.CustomDuration{Duration: 15 * time.Second}, MaxActiveFlows: 0, }) ) func TestRegisterSession(t *testing.T) { log := zerolog.Nop() originDialerService := ingress.NewOriginDialer(ingress.OriginConfig{ DefaultDialer: testDefaultDialer, TCPWriteTimeout: 0, }, &log) manager := v3.NewSessionManager(&noopMetrics{}, &log, originDialerService, cfdflow.NewLimiter(0)) request := v3.UDPSessionRegistrationDatagram{ RequestID: testRequestID, Dest: netip.MustParseAddrPort("127.0.0.1:5000"), Traced: false, IdleDurationHint: 5 * time.Second, Payload: nil, } session, err := manager.RegisterSession(&request, &noopEyeball{}) if err != nil { t.Fatalf("register session should've succeeded: %v", err) } if request.RequestID != session.ID() { t.Fatalf("session id doesn't match: %v != %v", request.RequestID, session.ID()) } // We shouldn't be able to register another session with the same request id _, err = manager.RegisterSession(&request, &noopEyeball{}) if !errors.Is(err, v3.ErrSessionAlreadyRegistered) { t.Fatalf("session is already registered for this connection: %v", err) } // We shouldn't be able to register another session with the same request id for a different connection _, err = manager.RegisterSession(&request, &noopEyeball{connID: 1}) if !errors.Is(err, v3.ErrSessionBoundToOtherConn) { t.Fatalf("session is already registered for a separate connection: %v", err) } // Get session sessionGet, err := manager.GetSession(request.RequestID) if err != nil { t.Fatalf("get session failed: %v", err) } if session.ID() != sessionGet.ID() { t.Fatalf("session's do not match: %v != %v", session.ID(), sessionGet.ID()) } // Remove the session manager.UnregisterSession(request.RequestID) // Get session should fail _, err = manager.GetSession(request.RequestID) if !errors.Is(err, v3.ErrSessionNotFound) { t.Fatalf("get session failed: %v", err) } // Closing the original session should return that the socket is already closed (by the session unregistration) err = session.Close() if err != nil && !strings.Contains(err.Error(), "use of closed network connection") { t.Fatalf("session should've closed without issue: %v", err) } } func TestGetSession_Empty(t *testing.T) { log := zerolog.Nop() originDialerService := ingress.NewOriginDialer(ingress.OriginConfig{ DefaultDialer: testDefaultDialer, TCPWriteTimeout: 0, }, &log) manager := v3.NewSessionManager(&noopMetrics{}, &log, originDialerService, cfdflow.NewLimiter(0)) _, err := manager.GetSession(testRequestID) if !errors.Is(err, v3.ErrSessionNotFound) { t.Fatalf("get session find no session: %v", err) } } func TestRegisterSessionRateLimit(t *testing.T) { log := zerolog.Nop() originDialerService := ingress.NewOriginDialer(ingress.OriginConfig{ DefaultDialer: testDefaultDialer, TCPWriteTimeout: 0, }, &log) ctrl := gomock.NewController(t) flowLimiterMock := mocks.NewMockLimiter(ctrl) flowLimiterMock.EXPECT().Acquire("udp").Return(cfdflow.ErrTooManyActiveFlows) flowLimiterMock.EXPECT().Release().Times(0) manager := v3.NewSessionManager(&noopMetrics{}, &log, originDialerService, flowLimiterMock) request := v3.UDPSessionRegistrationDatagram{ RequestID: testRequestID, Dest: netip.MustParseAddrPort("127.0.0.1:5000"), Traced: false, IdleDurationHint: 5 * time.Second, Payload: nil, } _, err := manager.RegisterSession(&request, &noopEyeball{}) require.ErrorIs(t, err, v3.ErrSessionRegistrationRateLimited) } ================================================ FILE: quic/v3/metrics.go ================================================ package v3 import ( "fmt" "github.com/prometheus/client_golang/prometheus" "github.com/cloudflare/cloudflared/quic" ) const ( namespace = "cloudflared" subsystem_udp = "udp" subsystem_icmp = "icmp" commandMetricLabel = "command" reasonMetricLabel = "reason" ) type DroppedReason int const ( DroppedWriteFailed DroppedReason = iota DroppedWriteDeadlineExceeded DroppedWriteFull DroppedWriteFlowUnknown DroppedReadFailed // Origin payloads that are too large to proxy. DroppedReadTooLarge ) var droppedReason = map[DroppedReason]string{ DroppedWriteFailed: "write_failed", DroppedWriteDeadlineExceeded: "write_deadline_exceeded", DroppedWriteFull: "write_full", DroppedWriteFlowUnknown: "write_flow_unknown", DroppedReadFailed: "read_failed", DroppedReadTooLarge: "read_too_large", } func (dr DroppedReason) String() string { return droppedReason[dr] } type Metrics interface { IncrementFlows(connIndex uint8) DecrementFlows(connIndex uint8) FailedFlow(connIndex uint8) RetryFlowResponse(connIndex uint8) MigrateFlow(connIndex uint8) UnsupportedRemoteCommand(connIndex uint8, command string) DroppedUDPDatagram(connIndex uint8, reason DroppedReason) DroppedICMPPackets(connIndex uint8, reason DroppedReason) } type metrics struct { activeUDPFlows *prometheus.GaugeVec totalUDPFlows *prometheus.CounterVec retryFlowResponses *prometheus.CounterVec migratedFlows *prometheus.CounterVec unsupportedRemoteCommands *prometheus.CounterVec droppedUDPDatagrams *prometheus.CounterVec droppedICMPPackets *prometheus.CounterVec failedFlows *prometheus.CounterVec } func (m *metrics) IncrementFlows(connIndex uint8) { m.totalUDPFlows.WithLabelValues(fmt.Sprintf("%d", connIndex)).Inc() m.activeUDPFlows.WithLabelValues(fmt.Sprintf("%d", connIndex)).Inc() } func (m *metrics) DecrementFlows(connIndex uint8) { m.activeUDPFlows.WithLabelValues(fmt.Sprintf("%d", connIndex)).Dec() } func (m *metrics) FailedFlow(connIndex uint8) { m.failedFlows.WithLabelValues(fmt.Sprintf("%d", connIndex)).Inc() } func (m *metrics) RetryFlowResponse(connIndex uint8) { m.retryFlowResponses.WithLabelValues(fmt.Sprintf("%d", connIndex)).Inc() } func (m *metrics) MigrateFlow(connIndex uint8) { m.migratedFlows.WithLabelValues(fmt.Sprintf("%d", connIndex)).Inc() } func (m *metrics) UnsupportedRemoteCommand(connIndex uint8, command string) { m.unsupportedRemoteCommands.WithLabelValues(fmt.Sprintf("%d", connIndex), command).Inc() } func (m *metrics) DroppedUDPDatagram(connIndex uint8, reason DroppedReason) { m.droppedUDPDatagrams.WithLabelValues(fmt.Sprintf("%d", connIndex), reason.String()).Inc() } func (m *metrics) DroppedICMPPackets(connIndex uint8, reason DroppedReason) { m.droppedICMPPackets.WithLabelValues(fmt.Sprintf("%d", connIndex), reason.String()).Inc() } func NewMetrics(registerer prometheus.Registerer) Metrics { m := &metrics{ activeUDPFlows: prometheus.NewGaugeVec(prometheus.GaugeOpts{ Namespace: namespace, Subsystem: subsystem_udp, Name: "active_flows", Help: "Concurrent count of UDP flows that are being proxied to any origin", }, []string{quic.ConnectionIndexMetricLabel}), totalUDPFlows: prometheus.NewCounterVec(prometheus.CounterOpts{ //nolint:promlinter Namespace: namespace, Subsystem: subsystem_udp, Name: "total_flows", Help: "Total count of UDP flows that have been proxied to any origin", }, []string{quic.ConnectionIndexMetricLabel}), failedFlows: prometheus.NewCounterVec(prometheus.CounterOpts{ //nolint:promlinter Namespace: namespace, Subsystem: subsystem_udp, Name: "failed_flows", Help: "Total count of flows that errored and closed", }, []string{quic.ConnectionIndexMetricLabel}), retryFlowResponses: prometheus.NewCounterVec(prometheus.CounterOpts{ //nolint:promlinter Namespace: namespace, Subsystem: subsystem_udp, Name: "retry_flow_responses", Help: "Total count of UDP flows that have had to send their registration response more than once", }, []string{quic.ConnectionIndexMetricLabel}), migratedFlows: prometheus.NewCounterVec(prometheus.CounterOpts{ //nolint:promlinter Namespace: namespace, Subsystem: subsystem_udp, Name: "migrated_flows", Help: "Total count of UDP flows have been migrated across local connections", }, []string{quic.ConnectionIndexMetricLabel}), unsupportedRemoteCommands: prometheus.NewCounterVec(prometheus.CounterOpts{ //nolint:promlinter Namespace: namespace, Subsystem: subsystem_udp, Name: "unsupported_remote_command_total", Help: "Total count of unsupported remote RPC commands called", }, []string{quic.ConnectionIndexMetricLabel, commandMetricLabel}), droppedUDPDatagrams: prometheus.NewCounterVec(prometheus.CounterOpts{ //nolint:promlinter Namespace: namespace, Subsystem: subsystem_udp, Name: "dropped_datagrams", Help: "Total count of UDP dropped datagrams", }, []string{quic.ConnectionIndexMetricLabel, reasonMetricLabel}), droppedICMPPackets: prometheus.NewCounterVec(prometheus.CounterOpts{ //nolint:promlinter Namespace: namespace, Subsystem: subsystem_icmp, Name: "dropped_packets", Help: "Total count of ICMP dropped datagrams", }, []string{quic.ConnectionIndexMetricLabel, reasonMetricLabel}), } registerer.MustRegister( m.activeUDPFlows, m.totalUDPFlows, m.failedFlows, m.retryFlowResponses, m.migratedFlows, m.unsupportedRemoteCommands, m.droppedUDPDatagrams, m.droppedICMPPackets, ) return m } ================================================ FILE: quic/v3/metrics_test.go ================================================ package v3_test import v3 "github.com/cloudflare/cloudflared/quic/v3" type noopMetrics struct{} func (noopMetrics) IncrementFlows(connIndex uint8) {} func (noopMetrics) DecrementFlows(connIndex uint8) {} func (noopMetrics) FailedFlow(connIndex uint8) {} func (noopMetrics) PayloadTooLarge(connIndex uint8) {} func (noopMetrics) RetryFlowResponse(connIndex uint8) {} func (noopMetrics) MigrateFlow(connIndex uint8) {} func (noopMetrics) UnsupportedRemoteCommand(connIndex uint8, command string) {} func (noopMetrics) DroppedUDPDatagram(connIndex uint8, reason v3.DroppedReason) {} func (noopMetrics) DroppedICMPPackets(connIndex uint8, reason v3.DroppedReason) {} ================================================ FILE: quic/v3/muxer.go ================================================ package v3 import ( "context" "errors" "fmt" "sync" "time" "github.com/rs/zerolog" "github.com/cloudflare/cloudflared/ingress" "github.com/cloudflare/cloudflared/packet" ) const ( // Allocating a 16 channel buffer here allows for the writer to be slightly faster than the reader. // This has worked previously well for datagramv2, so we will start with this as well demuxChanCapacity = 16 // This provides a small buffer for the PacketRouter to poll ICMP packets from the QUIC connection // before writing them to the origin. icmpDatagramChanCapacity = 128 logSrcKey = "src" logDstKey = "dst" logICMPTypeKey = "type" logDurationKey = "durationMS" ) // DatagramConn is the bridge that multiplexes writes and reads of datagrams for UDP sessions and ICMP packets to // a connection. type DatagramConn interface { DatagramUDPWriter DatagramICMPWriter // Serve provides a server interface to process and handle incoming QUIC datagrams and demux their datagram v3 payloads. Serve(context.Context) error // ID indicates connection index identifier ID() uint8 } // DatagramUDPWriter provides the Muxer interface to create proper UDP Datagrams when sending over a connection. type DatagramUDPWriter interface { SendUDPSessionDatagram(datagram []byte) error SendUDPSessionResponse(id RequestID, resp SessionRegistrationResp) error } // DatagramICMPWriter provides the Muxer interface to create ICMP Datagrams when sending over a connection. type DatagramICMPWriter interface { SendICMPPacket(icmp *packet.ICMP) error SendICMPTTLExceed(icmp *packet.ICMP, rawPacket packet.RawPacket) error } // QuicConnection provides an interface that matches [quic.Connection] for only the datagram operations. // // We currently rely on the mutex for the [quic.Connection.SendDatagram] and [quic.Connection.ReceiveDatagram] and // do not have any locking for them. If the implementation in quic-go were to ever change, we would need to make // sure that we lock properly on these operations. type QuicConnection interface { Context() context.Context SendDatagram(payload []byte) error ReceiveDatagram(context.Context) ([]byte, error) } type datagramConn struct { conn QuicConnection index uint8 sessionManager SessionManager icmpRouter ingress.ICMPRouter metrics Metrics logger *zerolog.Logger datagrams chan []byte icmpDatagramChan chan *ICMPDatagram readErrors chan error icmpEncoderPool sync.Pool // a pool of *packet.Encoder icmpDecoderPool sync.Pool } func NewDatagramConn(conn QuicConnection, sessionManager SessionManager, icmpRouter ingress.ICMPRouter, index uint8, metrics Metrics, logger *zerolog.Logger) DatagramConn { log := logger.With().Uint8("datagramVersion", 3).Logger() return &datagramConn{ conn: conn, index: index, sessionManager: sessionManager, icmpRouter: icmpRouter, metrics: metrics, logger: &log, datagrams: make(chan []byte, demuxChanCapacity), icmpDatagramChan: make(chan *ICMPDatagram, icmpDatagramChanCapacity), readErrors: make(chan error, 2), icmpEncoderPool: sync.Pool{ New: func() any { return packet.NewEncoder() }, }, icmpDecoderPool: sync.Pool{ New: func() any { return packet.NewICMPDecoder() }, }, } } func (c *datagramConn) ID() uint8 { return c.index } func (c *datagramConn) SendUDPSessionDatagram(datagram []byte) error { return c.conn.SendDatagram(datagram) } func (c *datagramConn) SendUDPSessionResponse(id RequestID, resp SessionRegistrationResp) error { datagram := UDPSessionRegistrationResponseDatagram{ RequestID: id, ResponseType: resp, } data, err := datagram.MarshalBinary() if err != nil { return err } return c.conn.SendDatagram(data) } func (c *datagramConn) SendICMPPacket(icmp *packet.ICMP) error { cachedEncoder := c.icmpEncoderPool.Get() // The encoded packet is a slice to a buffer owned by the encoder, so we shouldn't return the encoder back to the // pool until the encoded packet is sent. defer c.icmpEncoderPool.Put(cachedEncoder) encoder, ok := cachedEncoder.(*packet.Encoder) if !ok { return fmt.Errorf("encoderPool returned %T, expect *packet.Encoder", cachedEncoder) } payload, err := encoder.Encode(icmp) if err != nil { return err } icmpDatagram := ICMPDatagram{ Payload: payload.Data, } datagram, err := icmpDatagram.MarshalBinary() if err != nil { return err } return c.conn.SendDatagram(datagram) } func (c *datagramConn) SendICMPTTLExceed(icmp *packet.ICMP, rawPacket packet.RawPacket) error { return c.SendICMPPacket(c.icmpRouter.ConvertToTTLExceeded(icmp, rawPacket)) } // pollDatagrams will read datagrams from the underlying connection until the provided context is done. func (c *datagramConn) pollDatagrams(ctx context.Context) { for ctx.Err() == nil { datagram, err := c.conn.ReceiveDatagram(ctx) // If the read returns an error, we want to return the failure to the channel. if err != nil { c.readErrors <- err return } c.datagrams <- datagram } if ctx.Err() != nil { c.readErrors <- ctx.Err() } } // Serve will begin the process of receiving datagrams from the [quic.Connection] and demuxing them to their destination. // The [DatagramConn] when serving, will be responsible for the sessions it accepts. func (c *datagramConn) Serve(ctx context.Context) error { connCtx := c.conn.Context() // We want to make sure that we cancel the reader context if the Serve method returns. This could also mean that the // underlying connection is also closing, but that is handled outside of the context of the datagram muxer. readCtx, cancel := context.WithCancel(connCtx) defer cancel() go c.pollDatagrams(readCtx) // Processing ICMP datagrams also monitors the reader context since the ICMP datagrams from the reader are the input // for the routine. go c.processICMPDatagrams(readCtx) for { // We make sure to monitor the context of cloudflared and the underlying connection to return if any errors occur. var datagram []byte select { // Monitor the context of cloudflared case <-ctx.Done(): return ctx.Err() // Monitor the context of the underlying quic connection case <-connCtx.Done(): return connCtx.Err() // Monitor for any hard errors from reading the connection case err := <-c.readErrors: return err // Wait and dequeue datagrams as they come in case d := <-c.datagrams: datagram = d } // Each incoming datagram will be processed in a new go routine to handle the demuxing and action associated. typ, err := ParseDatagramType(datagram) if err != nil { c.logger.Err(err).Msgf("unable to parse datagram type: %d", typ) continue } switch typ { case UDPSessionRegistrationType: reg := &UDPSessionRegistrationDatagram{} err := reg.UnmarshalBinary(datagram) if err != nil { c.logger.Err(err).Msgf("unable to unmarshal session registration datagram") continue } logger := c.logger.With().Str(logFlowID, reg.RequestID.String()).Logger() // We bind the new session to the quic connection context instead of cloudflared context to allow for the // quic connection to close and close only the sessions bound to it. Closing of cloudflared will also // initiate the close of the quic connection, so we don't have to worry about the application context // in the scope of a session. // // Additionally, we spin out the registration into a separate go routine to handle the Serve'ing of the // session in a separate routine from the demuxer. go c.handleSessionRegistrationDatagram(connCtx, reg, &logger) case UDPSessionPayloadType: payload := &UDPSessionPayloadDatagram{} err := payload.UnmarshalBinary(datagram) if err != nil { c.logger.Err(err).Msgf("unable to unmarshal session payload datagram") continue } logger := c.logger.With().Str(logFlowID, payload.RequestID.String()).Logger() c.handleSessionPayloadDatagram(payload, &logger) case ICMPType: packet := &ICMPDatagram{} err := packet.UnmarshalBinary(datagram) if err != nil { c.logger.Err(err).Msgf("unable to unmarshal icmp datagram") continue } c.handleICMPPacket(packet) case UDPSessionRegistrationResponseType: // cloudflared should never expect to receive UDP session responses as it will not initiate new // sessions towards the edge. c.logger.Error().Msgf("unexpected datagram type received: %d", UDPSessionRegistrationResponseType) continue default: c.logger.Error().Msgf("unknown datagram type received: %d", typ) } } } // This method handles new registrations of a session and the serve loop for the session. func (c *datagramConn) handleSessionRegistrationDatagram(ctx context.Context, datagram *UDPSessionRegistrationDatagram, logger *zerolog.Logger) { log := logger.With(). Str(logFlowID, datagram.RequestID.String()). Str(logDstKey, datagram.Dest.String()). Logger() session, err := c.sessionManager.RegisterSession(datagram, c) if err != nil { switch err { case ErrSessionAlreadyRegistered: // Session is already registered and likely the response got lost c.handleSessionAlreadyRegistered(datagram.RequestID, &log) case ErrSessionBoundToOtherConn: // Session is already registered but to a different connection c.handleSessionMigration(datagram.RequestID, &log) case ErrSessionRegistrationRateLimited: // There are too many concurrent sessions so we return an error to force a retry later c.handleSessionRegistrationRateLimited(datagram, &log) default: log.Err(err).Msg("flow registration failure") c.handleSessionRegistrationFailure(datagram.RequestID, &log) } return } log = log.With().Str(logSrcKey, session.LocalAddr().String()).Logger() c.metrics.IncrementFlows(c.index) // Make sure to eventually remove the session from the session manager when the session is closed defer c.sessionManager.UnregisterSession(session.ID()) defer c.metrics.DecrementFlows(c.index) // Respond that we are able to process the new session err = c.SendUDPSessionResponse(datagram.RequestID, ResponseOk) if err != nil { log.Err(err).Msgf("flow registration failure: unable to send session registration response") return } // We bind the context of the session to the [quic.Connection] that initiated the session. // [Session.Serve] is blocking and will continue this go routine till the end of the session lifetime. start := time.Now() err = session.Serve(ctx) elapsedMS := time.Since(start).Milliseconds() log = log.With().Int64(logDurationKey, elapsedMS).Logger() if err == nil { // We typically don't expect a session to close without some error response. [SessionIdleErr] is the typical // expected error response. log.Warn().Msg("flow closed: no explicit close or timeout elapsed") return } // SessionIdleErr and SessionCloseErr are valid and successful error responses to end a session. if errors.Is(err, SessionIdleErr{}) || errors.Is(err, SessionCloseErr) { log.Debug().Msgf("flow closed: %s", err.Error()) return } // All other errors should be reported as errors log.Err(err).Msgf("flow closed with an error") } func (c *datagramConn) handleSessionAlreadyRegistered(requestID RequestID, logger *zerolog.Logger) { // Send another registration response since the session is already active err := c.SendUDPSessionResponse(requestID, ResponseOk) if err != nil { logger.Err(err).Msgf("flow registration failure: unable to send an additional flow registration response") return } session, err := c.sessionManager.GetSession(requestID) if err != nil { // If for some reason we can not find the session after attempting to register it, we can just return // instead of trying to reset the idle timer for it. return } // The session is already running in another routine so we want to restart the idle timeout since no proxied // packets have come down yet. session.ResetIdleTimer() c.metrics.RetryFlowResponse(c.index) logger.Debug().Msgf("flow registration response retry") } func (c *datagramConn) handleSessionMigration(requestID RequestID, logger *zerolog.Logger) { // We need to migrate the currently running session to this edge connection. session, err := c.sessionManager.GetSession(requestID) if err != nil { // If for some reason we can not find the session after attempting to register it, we can just return // instead of trying to reset the idle timer for it. return } // Migrate the session to use this edge connection instead of the currently running one. // We also pass in this connection's logger to override the existing logger for the session. session.Migrate(c, c.conn.Context(), c.logger) // Send another registration response since the session is already active err = c.SendUDPSessionResponse(requestID, ResponseOk) if err != nil { logger.Err(err).Msgf("flow registration failure: unable to send an additional flow registration response") return } logger.Debug().Msgf("flow registration migration") } func (c *datagramConn) handleSessionRegistrationFailure(requestID RequestID, logger *zerolog.Logger) { err := c.SendUDPSessionResponse(requestID, ResponseUnableToBindSocket) if err != nil { logger.Err(err).Msgf("unable to send flow registration error response (%d)", ResponseUnableToBindSocket) } } func (c *datagramConn) handleSessionRegistrationRateLimited(datagram *UDPSessionRegistrationDatagram, logger *zerolog.Logger) { c.logger.Warn().Msg("Too many concurrent sessions being handled, rejecting udp proxy") rateLimitResponse := ResponseTooManyActiveFlows err := c.SendUDPSessionResponse(datagram.RequestID, rateLimitResponse) if err != nil { logger.Err(err).Msgf("unable to send flow registration error response (%d)", rateLimitResponse) } } // Handles incoming datagrams that need to be sent to a registered session. func (c *datagramConn) handleSessionPayloadDatagram(datagram *UDPSessionPayloadDatagram, logger *zerolog.Logger) { s, err := c.sessionManager.GetSession(datagram.RequestID) if err != nil { c.metrics.DroppedUDPDatagram(c.index, DroppedWriteFlowUnknown) logger.Err(err).Msgf("unable to find flow") return } s.Write(datagram.Payload) } // Handles incoming ICMP datagrams into a serialized channel to be handled by a single consumer. func (c *datagramConn) handleICMPPacket(datagram *ICMPDatagram) { if c.icmpRouter == nil { // ICMPRouter is disabled so we drop the current packet and ignore all incoming ICMP packets return } select { case c.icmpDatagramChan <- datagram: default: // If the ICMP datagram channel is full, drop any additional incoming. c.metrics.DroppedICMPPackets(c.index, DroppedWriteFull) c.logger.Warn().Msg("failed to write icmp packet to origin: dropped") } } // Consumes from the ICMP datagram channel to write out the ICMP requests to an origin. func (c *datagramConn) processICMPDatagrams(ctx context.Context) { if c.icmpRouter == nil { // ICMPRouter is disabled so we ignore all incoming ICMP packets return } for { select { // If the provided context is closed we want to exit the write loop case <-ctx.Done(): return case datagram := <-c.icmpDatagramChan: c.writeICMPPacket(datagram) } } } func (c *datagramConn) writeICMPPacket(datagram *ICMPDatagram) { // Decode the provided ICMPDatagram as an ICMP packet rawPacket := packet.RawPacket{Data: datagram.Payload} cachedDecoder := c.icmpDecoderPool.Get() defer c.icmpDecoderPool.Put(cachedDecoder) decoder, ok := cachedDecoder.(*packet.ICMPDecoder) if !ok { c.metrics.DroppedICMPPackets(c.index, DroppedWriteFailed) c.logger.Error().Msg("Could not get ICMPDecoder from the pool. Dropping packet") return } icmp, err := decoder.Decode(rawPacket) if err != nil { c.metrics.DroppedICMPPackets(c.index, DroppedWriteFailed) c.logger.Err(err).Msgf("unable to marshal icmp packet") return } // If the ICMP packet's TTL is expired, we won't send it to the origin and immediately return a TTL Exceeded Message if icmp.TTL <= 1 { if err := c.SendICMPTTLExceed(icmp, rawPacket); err != nil { c.metrics.DroppedICMPPackets(c.index, DroppedWriteFailed) c.logger.Err(err).Msg("failed to return ICMP TTL exceed error") } return } icmp.TTL-- // The context isn't really needed here since it's only really used throughout the ICMP router as a way to store // the tracing context, however datagram V3 does not support tracing ICMP packets, so we just pass the current // connection context which will have no tracing information available. err = c.icmpRouter.Request(c.conn.Context(), icmp, newPacketResponder(c, c.index)) if err != nil { c.metrics.DroppedICMPPackets(c.index, DroppedWriteFailed) c.logger.Err(err). Str(logSrcKey, icmp.Src.String()). Str(logDstKey, icmp.Dst.String()). Interface(logICMPTypeKey, icmp.Type). Msgf("unable to write icmp datagram to origin") return } } ================================================ FILE: quic/v3/muxer_test.go ================================================ package v3_test import ( "bytes" "context" "errors" "fmt" "net" "net/netip" "slices" "sort" "sync" "testing" "time" "github.com/fortytw2/leaktest" "github.com/google/gopacket/layers" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/net/icmp" "golang.org/x/net/ipv4" cfdflow "github.com/cloudflare/cloudflared/flow" "github.com/cloudflare/cloudflared/ingress" "github.com/cloudflare/cloudflared/packet" v3 "github.com/cloudflare/cloudflared/quic/v3" ) type noopEyeball struct { connID uint8 } func (noopEyeball) Serve(ctx context.Context) error { return nil } func (n noopEyeball) ID() uint8 { return n.connID } func (noopEyeball) SendUDPSessionDatagram(datagram []byte) error { return nil } func (noopEyeball) SendUDPSessionResponse(id v3.RequestID, resp v3.SessionRegistrationResp) error { return nil } func (noopEyeball) SendICMPPacket(icmp *packet.ICMP) error { return nil } func (noopEyeball) SendICMPTTLExceed(icmp *packet.ICMP, rawPacket packet.RawPacket) error { return nil } type mockEyeball struct { connID uint8 // datagram sent via SendUDPSessionDatagram recvData chan []byte // responses sent via SendUDPSessionResponse recvResp chan struct { id v3.RequestID resp v3.SessionRegistrationResp } } func newMockEyeball() mockEyeball { return mockEyeball{ connID: 0, recvData: make(chan []byte, 1), recvResp: make(chan struct { id v3.RequestID resp v3.SessionRegistrationResp }, 1), } } func (mockEyeball) Serve(ctx context.Context) error { return nil } func (m *mockEyeball) ID() uint8 { return m.connID } func (m *mockEyeball) SendUDPSessionDatagram(datagram []byte) error { b := make([]byte, len(datagram)) copy(b, datagram) m.recvData <- b return nil } func (m *mockEyeball) SendUDPSessionResponse(id v3.RequestID, resp v3.SessionRegistrationResp) error { m.recvResp <- struct { id v3.RequestID resp v3.SessionRegistrationResp }{ id, resp, } return nil } func (m *mockEyeball) SendICMPPacket(icmp *packet.ICMP) error { return nil } func (m *mockEyeball) SendICMPTTLExceed(icmp *packet.ICMP, rawPacket packet.RawPacket) error { return nil } func TestDatagramConn_New(t *testing.T) { log := zerolog.Nop() originDialerService := ingress.NewOriginDialer(ingress.OriginConfig{ DefaultDialer: testDefaultDialer, TCPWriteTimeout: 0, }, &log) conn := v3.NewDatagramConn(newMockQuicConn(t.Context()), v3.NewSessionManager(&noopMetrics{}, &log, originDialerService, cfdflow.NewLimiter(0)), &noopICMPRouter{}, 0, &noopMetrics{}, &log) if conn == nil { t.Fatal("expected valid connection") } } func TestDatagramConn_SendUDPSessionDatagram(t *testing.T) { log := zerolog.Nop() originDialerService := ingress.NewOriginDialer(ingress.OriginConfig{ DefaultDialer: testDefaultDialer, TCPWriteTimeout: 0, }, &log) connCtx, connCancel := context.WithCancelCause(t.Context()) defer connCancel(context.Canceled) quic := newMockQuicConn(connCtx) conn := v3.NewDatagramConn(quic, v3.NewSessionManager(&noopMetrics{}, &log, originDialerService, cfdflow.NewLimiter(0)), &noopICMPRouter{}, 0, &noopMetrics{}, &log) payload := []byte{0xef, 0xef} err := conn.SendUDPSessionDatagram(payload) require.NoError(t, err) p := <-quic.recv if !slices.Equal(p, payload) { t.Fatal("datagram sent does not match datagram received on quic side") } } func TestDatagramConn_SendUDPSessionResponse(t *testing.T) { log := zerolog.Nop() originDialerService := ingress.NewOriginDialer(ingress.OriginConfig{ DefaultDialer: testDefaultDialer, TCPWriteTimeout: 0, }, &log) connCtx, connCancel := context.WithCancelCause(t.Context()) defer connCancel(context.Canceled) quic := newMockQuicConn(connCtx) conn := v3.NewDatagramConn(quic, v3.NewSessionManager(&noopMetrics{}, &log, originDialerService, cfdflow.NewLimiter(0)), &noopICMPRouter{}, 0, &noopMetrics{}, &log) err := conn.SendUDPSessionResponse(testRequestID, v3.ResponseDestinationUnreachable) require.NoError(t, err) resp := <-quic.recv var response v3.UDPSessionRegistrationResponseDatagram err = response.UnmarshalBinary(resp) require.NoError(t, err) expected := v3.UDPSessionRegistrationResponseDatagram{ RequestID: testRequestID, ResponseType: v3.ResponseDestinationUnreachable, } if response != expected { t.Fatal("datagram response sent does not match expected datagram response received") } } func TestDatagramConnServe_ApplicationClosed(t *testing.T) { log := zerolog.Nop() originDialerService := ingress.NewOriginDialer(ingress.OriginConfig{ DefaultDialer: testDefaultDialer, TCPWriteTimeout: 0, }, &log) connCtx, connCancel := context.WithCancelCause(t.Context()) defer connCancel(context.Canceled) quic := newMockQuicConn(connCtx) conn := v3.NewDatagramConn(quic, v3.NewSessionManager(&noopMetrics{}, &log, originDialerService, cfdflow.NewLimiter(0)), &noopICMPRouter{}, 0, &noopMetrics{}, &log) ctx, cancel := context.WithTimeout(t.Context(), 1*time.Second) defer cancel() err := conn.Serve(ctx) if !errors.Is(err, context.DeadlineExceeded) { t.Fatal(err) } } func TestDatagramConnServe_ConnectionClosed(t *testing.T) { log := zerolog.Nop() originDialerService := ingress.NewOriginDialer(ingress.OriginConfig{ DefaultDialer: testDefaultDialer, TCPWriteTimeout: 0, }, &log) connCtx, connCancel := context.WithCancelCause(t.Context()) defer connCancel(context.Canceled) quic := newMockQuicConn(connCtx) ctx, cancel := context.WithTimeout(t.Context(), 1*time.Second) defer cancel() quic.ctx = ctx conn := v3.NewDatagramConn(quic, v3.NewSessionManager(&noopMetrics{}, &log, originDialerService, cfdflow.NewLimiter(0)), &noopICMPRouter{}, 0, &noopMetrics{}, &log) err := conn.Serve(t.Context()) if !errors.Is(err, context.DeadlineExceeded) { t.Fatal(err) } } func TestDatagramConnServe_ReceiveDatagramError(t *testing.T) { log := zerolog.Nop() originDialerService := ingress.NewOriginDialer(ingress.OriginConfig{ DefaultDialer: testDefaultDialer, TCPWriteTimeout: 0, }, &log) quic := &mockQuicConnReadError{err: net.ErrClosed} conn := v3.NewDatagramConn(quic, v3.NewSessionManager(&noopMetrics{}, &log, originDialerService, cfdflow.NewLimiter(0)), &noopICMPRouter{}, 0, &noopMetrics{}, &log) err := conn.Serve(t.Context()) if !errors.Is(err, net.ErrClosed) { t.Fatal(err) } } func TestDatagramConnServe_SessionRegistrationRateLimit(t *testing.T) { log := zerolog.Nop() connCtx, connCancel := context.WithCancelCause(t.Context()) defer connCancel(context.Canceled) quic := newMockQuicConn(connCtx) sessionManager := &mockSessionManager{ expectedRegErr: v3.ErrSessionRegistrationRateLimited, } conn := v3.NewDatagramConn(quic, sessionManager, &noopICMPRouter{}, 0, &noopMetrics{}, &log) // Setup the muxer ctx, cancel := context.WithCancelCause(t.Context()) defer cancel(context.Canceled) done := make(chan error, 1) go func() { done <- conn.Serve(ctx) }() // Send new session registration datagram := newRegisterSessionDatagram(testRequestID) quic.send <- datagram // Wait for session registration response with failure datagram = <-quic.recv var resp v3.UDPSessionRegistrationResponseDatagram err := resp.UnmarshalBinary(datagram) if err != nil { t.Fatal(err) } require.EqualValues(t, testRequestID, resp.RequestID) require.EqualValues(t, v3.ResponseTooManyActiveFlows, resp.ResponseType) assertContextClosed(t, ctx, done, cancel) } func TestDatagramConnServe_ErrorDatagramTypes(t *testing.T) { defer leaktest.Check(t)() for _, test := range []struct { name string input []byte expected string }{ { "empty", []byte{}, "{\"level\":\"error\",\"datagramVersion\":3,\"error\":\"datagram should have at least 1 byte\",\"message\":\"unable to parse datagram type: 0\"}\n", }, { "unexpected", []byte{byte(v3.UDPSessionRegistrationResponseType)}, "{\"level\":\"error\",\"datagramVersion\":3,\"message\":\"unexpected datagram type received: 3\"}\n", }, { "unknown", []byte{99}, "{\"level\":\"error\",\"datagramVersion\":3,\"message\":\"unknown datagram type received: 99\"}\n", }, } { t.Run(test.name, func(t *testing.T) { logOutput := new(LockedBuffer) log := zerolog.New(logOutput) connCtx, connCancel := context.WithCancelCause(t.Context()) defer connCancel(context.Canceled) quic := newMockQuicConn(connCtx) quic.send <- test.input conn := v3.NewDatagramConn(quic, &mockSessionManager{}, &noopICMPRouter{}, 0, &noopMetrics{}, &log) ctx, cancel := context.WithTimeout(t.Context(), 1*time.Second) defer cancel() err := conn.Serve(ctx) // we cancel the Serve method to check to see if the log output was written since the unsupported datagram // is dropped with only a log message as a side-effect. if !errors.Is(err, context.DeadlineExceeded) { t.Fatal(err) } out := logOutput.String() if out != test.expected { t.Fatalf("incorrect log output expected: %s", out) } }) } } type LockedBuffer struct { bytes.Buffer l sync.Mutex } func (b *LockedBuffer) Write(p []byte) (n int, err error) { b.l.Lock() defer b.l.Unlock() return b.Buffer.Write(p) } func (b *LockedBuffer) String() string { b.l.Lock() defer b.l.Unlock() return b.Buffer.String() } func TestDatagramConnServe_RegisterSession_SessionManagerError(t *testing.T) { defer leaktest.Check(t)() log := zerolog.Nop() connCtx, connCancel := context.WithCancelCause(t.Context()) defer connCancel(context.Canceled) quic := newMockQuicConn(connCtx) expectedErr := errors.New("unable to register session") sessionManager := mockSessionManager{expectedRegErr: expectedErr} conn := v3.NewDatagramConn(quic, &sessionManager, &noopICMPRouter{}, 0, &noopMetrics{}, &log) // Setup the muxer ctx, cancel := context.WithCancelCause(t.Context()) defer cancel(errors.New("other error")) done := make(chan error, 1) go func() { done <- conn.Serve(ctx) }() // Send new session registration datagram := newRegisterSessionDatagram(testRequestID) quic.send <- datagram // Wait for session registration response with failure datagram = <-quic.recv var resp v3.UDPSessionRegistrationResponseDatagram err := resp.UnmarshalBinary(datagram) if err != nil { t.Fatal(err) } if resp.RequestID != testRequestID || resp.ResponseType != v3.ResponseUnableToBindSocket { t.Fatalf("expected registration response failure") } // Cancel the muxer Serve context and make sure it closes with the expected error assertContextClosed(t, ctx, done, cancel) } func TestDatagramConnServe(t *testing.T) { defer leaktest.Check(t)() log := zerolog.Nop() connCtx, connCancel := context.WithCancelCause(t.Context()) defer connCancel(context.Canceled) quic := newMockQuicConn(connCtx) session := newMockSession() sessionManager := mockSessionManager{session: &session} conn := v3.NewDatagramConn(quic, &sessionManager, &noopICMPRouter{}, 0, &noopMetrics{}, &log) // Setup the muxer ctx, cancel := context.WithCancelCause(t.Context()) defer cancel(errors.New("other error")) done := make(chan error, 1) go func() { done <- conn.Serve(ctx) }() // Send new session registration datagram := newRegisterSessionDatagram(testRequestID) quic.send <- datagram // Wait for session registration response with success datagram = <-quic.recv var resp v3.UDPSessionRegistrationResponseDatagram err := resp.UnmarshalBinary(datagram) if err != nil { t.Fatal(err) } if resp.RequestID != testRequestID || resp.ResponseType != v3.ResponseOk { t.Fatalf("expected registration response ok") } // We expect the session to be served timer := time.NewTimer(15 * time.Second) defer timer.Stop() select { case <-session.served: break case <-timer.C: t.Fatalf("expected session serve to be called") } // Cancel the muxer Serve context and make sure it closes with the expected error assertContextClosed(t, ctx, done, cancel) } // This test exists because decoding multiple packets in parallel with the same decoder // instances causes inteference resulting in multiple different raw packets being decoded // as the same decoded packet. func TestDatagramConnServeDecodeMultipleICMPInParallel(t *testing.T) { defer leaktest.Check(t)() log := zerolog.Nop() connCtx, connCancel := context.WithCancelCause(t.Context()) defer connCancel(context.Canceled) quic := newMockQuicConn(connCtx) session := newMockSession() sessionManager := mockSessionManager{session: &session} router := newMockICMPRouter() conn := v3.NewDatagramConn(quic, &sessionManager, router, 0, &noopMetrics{}, &log) // Setup the muxer ctx, cancel := context.WithCancelCause(t.Context()) defer cancel(errors.New("other error")) done := make(chan error, 1) go func() { done <- conn.Serve(ctx) }() packetCount := 100 packets := make([]*packet.ICMP, 100) ipTemplate := "10.0.0.%d" for i := 1; i <= packetCount; i++ { packets[i-1] = &packet.ICMP{ IP: &packet.IP{ Src: netip.MustParseAddr("192.168.1.1"), Dst: netip.MustParseAddr(fmt.Sprintf(ipTemplate, i)), Protocol: layers.IPProtocolICMPv4, TTL: 20, }, Message: &icmp.Message{ Type: ipv4.ICMPTypeEcho, Code: 0, Body: &icmp.Echo{ ID: 25821, Seq: 58129, Data: []byte("test"), }, }, } } wg := sync.WaitGroup{} var receivedPackets []*packet.ICMP go func() { for { select { case <-ctx.Done(): return case icmpPacket := <-router.recv: receivedPackets = append(receivedPackets, icmpPacket) wg.Done() } } }() for _, p := range packets { // We increment here but only decrement when receiving the packet wg.Add(1) go func() { datagram := newICMPDatagram(p) quic.send <- datagram }() } wg.Wait() // If there were duplicates then we won't have the same number of IPs packetSet := make(map[netip.Addr]*packet.ICMP, 0) for _, p := range receivedPackets { packetSet[p.Dst] = p } assert.Equal(t, len(packetSet), len(packets)) // Sort the slice by last byte of IP address (the one we increment for each destination) // and then check that we have one match for each packet sent sort.Slice(receivedPackets, func(i, j int) bool { return receivedPackets[i].Dst.As4()[3] < receivedPackets[j].Dst.As4()[3] }) for i, p := range receivedPackets { assert.Equal(t, p.Dst, packets[i].Dst) } // Cancel the muxer Serve context and make sure it closes with the expected error assertContextClosed(t, ctx, done, cancel) } func TestDatagramConnServe_RegisterTwice(t *testing.T) { defer leaktest.Check(t)() log := zerolog.Nop() connCtx, connCancel := context.WithCancelCause(t.Context()) defer connCancel(context.Canceled) quic := newMockQuicConn(connCtx) session := newMockSession() sessionManager := mockSessionManager{session: &session} conn := v3.NewDatagramConn(quic, &sessionManager, &noopICMPRouter{}, 0, &noopMetrics{}, &log) // Setup the muxer ctx, cancel := context.WithCancelCause(t.Context()) defer cancel(errors.New("other error")) done := make(chan error, 1) go func() { done <- conn.Serve(ctx) }() // Send new session registration datagram := newRegisterSessionDatagram(testRequestID) quic.send <- datagram // Wait for session registration response with success datagram = <-quic.recv var resp v3.UDPSessionRegistrationResponseDatagram err := resp.UnmarshalBinary(datagram) if err != nil { t.Fatal(err) } if resp.RequestID != testRequestID || resp.ResponseType != v3.ResponseOk { t.Fatalf("expected registration response ok") } // Set the session manager to return already registered sessionManager.expectedRegErr = v3.ErrSessionAlreadyRegistered // Send the registration again as if we didn't receive it at the edge datagram = newRegisterSessionDatagram(testRequestID) quic.send <- datagram // Wait for session registration response with success datagram = <-quic.recv err = resp.UnmarshalBinary(datagram) if err != nil { t.Fatal(err) } if resp.RequestID != testRequestID || resp.ResponseType != v3.ResponseOk { t.Fatalf("expected registration response ok") } // We expect the session to be served timer := time.NewTimer(15 * time.Second) defer timer.Stop() select { case <-session.served: break case <-timer.C: t.Fatalf("expected session serve to be called") } // Cancel the muxer Serve context and make sure it closes with the expected error assertContextClosed(t, ctx, done, cancel) } func TestDatagramConnServe_MigrateConnection(t *testing.T) { defer leaktest.Check(t)() log := zerolog.Nop() connCtx, connCancel := context.WithCancelCause(t.Context()) defer connCancel(context.Canceled) quic := newMockQuicConn(connCtx) session := newMockSession() sessionManager := mockSessionManager{session: &session} conn := v3.NewDatagramConn(quic, &sessionManager, &noopICMPRouter{}, 0, &noopMetrics{}, &log) conn2Ctx, conn2Cancel := context.WithCancelCause(t.Context()) defer conn2Cancel(context.Canceled) quic2 := newMockQuicConn(conn2Ctx) conn2 := v3.NewDatagramConn(quic2, &sessionManager, &noopICMPRouter{}, 1, &noopMetrics{}, &log) // Setup the muxer ctx, cancel := context.WithCancelCause(t.Context()) defer cancel(errors.New("other error")) done := make(chan error, 1) go func() { done <- conn.Serve(ctx) }() ctx2, cancel2 := context.WithCancelCause(t.Context()) defer cancel2(errors.New("other error")) done2 := make(chan error, 1) go func() { done2 <- conn2.Serve(ctx2) }() // Send new session registration datagram := newRegisterSessionDatagram(testRequestID) quic.send <- datagram // Wait for session registration response with success datagram = <-quic.recv var resp v3.UDPSessionRegistrationResponseDatagram err := resp.UnmarshalBinary(datagram) if err != nil { t.Fatal(err) } if resp.RequestID != testRequestID || resp.ResponseType != v3.ResponseOk { t.Fatalf("expected registration response ok") } // Set the session manager to return already registered to another connection sessionManager.expectedRegErr = v3.ErrSessionBoundToOtherConn // Send the registration again as if we didn't receive it at the edge for a new connection datagram = newRegisterSessionDatagram(testRequestID) quic2.send <- datagram // Wait for session registration response with success datagram = <-quic2.recv err = resp.UnmarshalBinary(datagram) if err != nil { t.Fatal(err) } if resp.RequestID != testRequestID || resp.ResponseType != v3.ResponseOk { t.Fatalf("expected registration response ok") } // We expect the session to be served timer := time.NewTimer(15 * time.Second) defer timer.Stop() select { case <-session.served: break case <-timer.C: t.Fatalf("expected session serve to be called") } // Expect session to be migrated select { case id := <-session.migrated: if id != conn2.ID() { t.Fatalf("expected session to be migrated to connection 2") } case <-timer.C: t.Fatalf("expected session migration to be called") } // Cancel the muxer Serve context and make sure it closes with the expected error assertContextClosed(t, ctx, done, cancel) // Cancel the second muxer Serve context and make sure it closes with the expected error assertContextClosed(t, ctx2, done2, cancel2) } func TestDatagramConnServe_Payload_GetSessionError(t *testing.T) { defer leaktest.Check(t)() log := zerolog.Nop() connCtx, connCancel := context.WithCancelCause(t.Context()) defer connCancel(context.Canceled) quic := newMockQuicConn(connCtx) // mockSessionManager will return the ErrSessionNotFound for any session attempting to be queried by the muxer sessionManager := mockSessionManager{session: nil, expectedGetErr: v3.ErrSessionNotFound} conn := v3.NewDatagramConn(quic, &sessionManager, &noopICMPRouter{}, 0, &noopMetrics{}, &log) // Setup the muxer ctx, cancel := context.WithCancelCause(t.Context()) defer cancel(errors.New("other error")) done := make(chan error, 1) go func() { done <- conn.Serve(ctx) }() // Send new session registration datagram := newSessionPayloadDatagram(testRequestID, []byte{0xef, 0xef}) quic.send <- datagram // Since the muxer should eventually discard a failed registration request, there is no side-effect // that the registration was failed beyond the muxer accepting the registration request. As such, the // test can only ensure that the quic.send channel was consumed and that the muxer closes normally // afterwards with the expected context cancelled trigger. // Cancel the muxer Serve context and make sure it closes with the expected error assertContextClosed(t, ctx, done, cancel) } func TestDatagramConnServe_Payloads(t *testing.T) { defer leaktest.Check(t)() log := zerolog.Nop() connCtx, connCancel := context.WithCancelCause(t.Context()) defer connCancel(context.Canceled) quic := newMockQuicConn(connCtx) session := newMockSession() sessionManager := mockSessionManager{session: &session} conn := v3.NewDatagramConn(quic, &sessionManager, &noopICMPRouter{}, 0, &noopMetrics{}, &log) // Setup the muxer ctx, cancel := context.WithCancelCause(t.Context()) defer cancel(errors.New("other error")) done := make(chan error, 1) go func() { done <- conn.Serve(ctx) }() // Send session payloads expectedPayloads := makePayloads(256, 16) go func() { for _, payload := range expectedPayloads { datagram := newSessionPayloadDatagram(testRequestID, payload) quic.send <- datagram } }() // Session should receive the payloads (in-order) for i, payload := range expectedPayloads { select { case recv := <-session.recv: if !slices.Equal(recv, payload) { t.Fatalf("expected session receieve the payload[%d] sent via the muxer: (%x) (%x)", i, recv[:16], payload[:16]) } case err := <-ctx.Done(): // we expect the payload to return before the context to cancel on the session t.Fatal(err) } } // Cancel the muxer Serve context and make sure it closes with the expected error assertContextClosed(t, ctx, done, cancel) } func TestDatagramConnServe_ICMPDatagram_TTLDecremented(t *testing.T) { defer leaktest.Check(t)() log := zerolog.Nop() connCtx, connCancel := context.WithCancelCause(t.Context()) defer connCancel(context.Canceled) quic := newMockQuicConn(connCtx) router := newMockICMPRouter() conn := v3.NewDatagramConn(quic, &mockSessionManager{}, router, 0, &noopMetrics{}, &log) // Setup the muxer ctx, cancel := context.WithCancelCause(t.Context()) defer cancel(errors.New("other error")) done := make(chan error, 1) go func() { done <- conn.Serve(ctx) }() // Send new ICMP Echo request expectedICMP := &packet.ICMP{ IP: &packet.IP{ Src: netip.MustParseAddr("192.168.1.1"), Dst: netip.MustParseAddr("10.0.0.1"), Protocol: layers.IPProtocolICMPv4, TTL: 20, }, Message: &icmp.Message{ Type: ipv4.ICMPTypeEcho, Code: 0, Body: &icmp.Echo{ ID: 25821, Seq: 58129, Data: []byte("test ttl=0"), }, }, } datagram := newICMPDatagram(expectedICMP) quic.send <- datagram // Router should receive the packet actualICMP := <-router.recv assertICMPEqual(t, expectedICMP, actualICMP) if expectedICMP.TTL-1 != actualICMP.TTL { t.Fatalf("TTL should be decremented by one before sending to origin: %d, %d", expectedICMP.TTL, actualICMP.TTL) } // Cancel the muxer Serve context and make sure it closes with the expected error assertContextClosed(t, ctx, done, cancel) } func TestDatagramConnServe_ICMPDatagram_TTLExceeded(t *testing.T) { defer leaktest.Check(t)() log := zerolog.Nop() connCtx, connCancel := context.WithCancelCause(t.Context()) defer connCancel(context.Canceled) quic := newMockQuicConn(connCtx) router := newMockICMPRouter() conn := v3.NewDatagramConn(quic, &mockSessionManager{}, router, 0, &noopMetrics{}, &log) // Setup the muxer ctx, cancel := context.WithCancelCause(t.Context()) defer cancel(errors.New("other error")) done := make(chan error, 1) go func() { done <- conn.Serve(ctx) }() // Send new ICMP Echo request expectedICMP := &packet.ICMP{ IP: &packet.IP{ Src: netip.MustParseAddr("192.168.1.1"), Dst: netip.MustParseAddr("10.0.0.1"), Protocol: layers.IPProtocolICMPv4, TTL: 0, }, Message: &icmp.Message{ Type: ipv4.ICMPTypeEcho, Code: 0, Body: &icmp.Echo{ ID: 25821, Seq: 58129, Data: []byte("test ttl=0"), }, }, } datagram := newICMPDatagram(expectedICMP) quic.send <- datagram // Origin should not receive a packet select { case <-router.recv: t.Fatalf("TTL should be expired and no origin ICMP sent") default: } // Eyeball should receive the packet datagram = <-quic.recv icmpDatagram := v3.ICMPDatagram{} err := icmpDatagram.UnmarshalBinary(datagram) if err != nil { t.Fatal(err) } decoder := packet.NewICMPDecoder() ttlExpiredICMP, err := decoder.Decode(packet.RawPacket{Data: icmpDatagram.Payload}) if err != nil { t.Fatal(err) } // Packet should be a TTL Exceeded ICMP if ttlExpiredICMP.TTL != packet.DefaultTTL || ttlExpiredICMP.Message.Type != ipv4.ICMPTypeTimeExceeded { t.Fatalf("ICMP packet should be a ICMP Exceeded: %+v", ttlExpiredICMP) } // Cancel the muxer Serve context and make sure it closes with the expected error assertContextClosed(t, ctx, done, cancel) } func newRegisterSessionDatagram(id v3.RequestID) []byte { datagram := v3.UDPSessionRegistrationDatagram{ RequestID: id, Dest: netip.MustParseAddrPort("127.0.0.1:8080"), IdleDurationHint: 5 * time.Second, } payload, err := datagram.MarshalBinary() if err != nil { panic(err) } return payload } func newSessionPayloadDatagram(id v3.RequestID, payload []byte) []byte { datagram := make([]byte, len(payload)+17) err := v3.MarshalPayloadHeaderTo(id, datagram[:]) if err != nil { panic(err) } copy(datagram[17:], payload) return datagram } func newICMPDatagram(pk *packet.ICMP) []byte { encoder := packet.NewEncoder() rawPacket, err := encoder.Encode(pk) if err != nil { panic(err) } datagram := v3.ICMPDatagram{ Payload: rawPacket.Data, } payload, err := datagram.MarshalBinary() if err != nil { panic(err) } return payload } // Cancel the provided context and make sure it closes with the expected cancellation error func assertContextClosed(t *testing.T, ctx context.Context, done <-chan error, cancel context.CancelCauseFunc) { cancel(errExpectedContextCanceled) err := <-done if !errors.Is(err, context.Canceled) { t.Fatal(err) } if !errors.Is(context.Cause(ctx), errExpectedContextCanceled) { t.Fatal(err) } } type mockQuicConn struct { ctx context.Context send chan []byte recv chan []byte } func newMockQuicConn(ctx context.Context) *mockQuicConn { return &mockQuicConn{ ctx: ctx, send: make(chan []byte, 1), recv: make(chan []byte, 1), } } func (m *mockQuicConn) Context() context.Context { return m.ctx } func (m *mockQuicConn) SendDatagram(payload []byte) error { b := make([]byte, len(payload)) copy(b, payload) m.recv <- b return nil } func (m *mockQuicConn) ReceiveDatagram(_ context.Context) ([]byte, error) { select { case <-m.ctx.Done(): return nil, m.ctx.Err() case b := <-m.send: return b, nil } } type mockQuicConnReadError struct { err error } func (m *mockQuicConnReadError) Context() context.Context { return context.Background() } func (m *mockQuicConnReadError) SendDatagram(payload []byte) error { return nil } func (m *mockQuicConnReadError) ReceiveDatagram(_ context.Context) ([]byte, error) { return nil, m.err } type mockSessionManager struct { session v3.Session expectedRegErr error expectedGetErr error } func (m *mockSessionManager) RegisterSession(request *v3.UDPSessionRegistrationDatagram, conn v3.DatagramConn) (v3.Session, error) { return m.session, m.expectedRegErr } func (m *mockSessionManager) GetSession(requestID v3.RequestID) (v3.Session, error) { return m.session, m.expectedGetErr } func (m *mockSessionManager) UnregisterSession(requestID v3.RequestID) {} type mockSession struct { served chan struct{} migrated chan uint8 recv chan []byte } func newMockSession() mockSession { return mockSession{ served: make(chan struct{}), migrated: make(chan uint8, 2), recv: make(chan []byte, 1), } } func (m *mockSession) ID() v3.RequestID { return testRequestID } func (m *mockSession) RemoteAddr() net.Addr { return testOriginAddr } func (m *mockSession) LocalAddr() net.Addr { return testLocalAddr } func (m *mockSession) ConnectionID() uint8 { return 0 } func (m *mockSession) Migrate(conn v3.DatagramConn, ctx context.Context, log *zerolog.Logger) { m.migrated <- conn.ID() } func (m *mockSession) ResetIdleTimer() {} func (m *mockSession) Serve(ctx context.Context) error { close(m.served) return v3.SessionCloseErr } func (m *mockSession) Write(payload []byte) { b := make([]byte, len(payload)) copy(b, payload) m.recv <- b } func (m *mockSession) Close() error { return nil } ================================================ FILE: quic/v3/request.go ================================================ package v3 import ( "encoding/binary" "errors" "fmt" ) const ( datagramRequestIdLen = 16 ) var ( // ErrInvalidRequestIDLen is returned when the provided request id can not be parsed from the provided byte slice. ErrInvalidRequestIDLen error = errors.New("invalid request id length provided") // ErrInvalidPayloadDestLen is returned when the provided destination byte slice cannot fit the whole request id. ErrInvalidPayloadDestLen error = errors.New("invalid payload size provided") ) // RequestID is the request-id-v2 identifier, it is used to distinguish between specific flows or sessions proxied // from the edge to cloudflared. type RequestID uint128 type uint128 struct { hi uint64 lo uint64 } // RequestIDFromSlice reads a request ID from a byte slice. func RequestIDFromSlice(data []byte) (RequestID, error) { if len(data) != datagramRequestIdLen { return RequestID{}, ErrInvalidRequestIDLen } return RequestID{ hi: binary.BigEndian.Uint64(data[:8]), lo: binary.BigEndian.Uint64(data[8:]), }, nil } func (id RequestID) String() string { return fmt.Sprintf("%016x%016x", id.hi, id.lo) } // Compare returns an integer comparing two IPs. // The result will be 0 if id == id2, -1 if id < id2, and +1 if id > id2. // The definition of "less than" is the same as the [RequestID.Less] method. func (id RequestID) Compare(id2 RequestID) int { hi1, hi2 := id.hi, id2.hi if hi1 < hi2 { return -1 } if hi1 > hi2 { return 1 } lo1, lo2 := id.lo, id2.lo if lo1 < lo2 { return -1 } if lo1 > lo2 { return 1 } return 0 } // Less reports whether id sorts before id2. func (id RequestID) Less(id2 RequestID) bool { return id.Compare(id2) == -1 } // MarshalBinaryTo writes the id to the provided destination byte slice; the byte slice must be of at least size 16. func (id RequestID) MarshalBinaryTo(data []byte) error { if len(data) < datagramRequestIdLen { return ErrInvalidPayloadDestLen } binary.BigEndian.PutUint64(data[:8], id.hi) binary.BigEndian.PutUint64(data[8:], id.lo) return nil } func (id *RequestID) UnmarshalBinary(data []byte) error { if len(data) != 16 { return fmt.Errorf("invalid length slice provided to unmarshal: %d (expected 16)", len(data)) } *id = RequestID{ binary.BigEndian.Uint64(data[:8]), binary.BigEndian.Uint64(data[8:]), } return nil } ================================================ FILE: quic/v3/request_test.go ================================================ package v3_test import ( "crypto/rand" "slices" "testing" "github.com/stretchr/testify/require" v3 "github.com/cloudflare/cloudflared/quic/v3" ) var ( testRequestIDBytes = [16]byte{ 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, } testRequestID = mustRequestID(testRequestIDBytes) ) func mustRequestID(data [16]byte) v3.RequestID { id, err := v3.RequestIDFromSlice(data[:]) if err != nil { panic(err) } return id } func TestRequestIDParsing(t *testing.T) { buf1 := make([]byte, 16) n, err := rand.Read(buf1) if err != nil { t.Fatal(err) } if n != 16 { t.Fatalf("did not read 16 bytes: %d", n) } id, err := v3.RequestIDFromSlice(buf1) if err != nil { t.Fatal(err) } buf2 := make([]byte, 16) err = id.MarshalBinaryTo(buf2) if err != nil { t.Fatal(err) } if !slices.Equal(buf1, buf2) { t.Fatalf("buf1 != buf2: %+v %+v", buf1, buf2) } } func TestRequestID_MarshalBinary(t *testing.T) { buf := make([]byte, 16) err := testRequestID.MarshalBinaryTo(buf) require.NoError(t, err) require.Len(t, buf, 16) parsed := v3.RequestID{} err = parsed.UnmarshalBinary(buf) require.NoError(t, err) require.Equal(t, testRequestID, parsed) } ================================================ FILE: quic/v3/session.go ================================================ package v3 import ( "context" "errors" "fmt" "io" "net" "os" "sync" "sync/atomic" "time" "github.com/rs/zerolog" ) const ( // A default is provided in the case that the client does not provide a close idle timeout. defaultCloseIdleAfter = 210 * time.Second // The maximum payload from the origin that we will be able to read. However, even though we will // read 1500 bytes from the origin, we limit the amount of bytes to be proxied to less than // this value (maxDatagramPayloadLen). maxOriginUDPPacketSize = 1500 // The maximum amount of datagrams a session will queue up before it begins dropping datagrams. // This channel buffer is small because we assume that the dedicated writer to the origin is typically // fast enought to keep the channel empty. writeChanCapacity = 512 logFlowID = "flowID" logPacketSizeKey = "packetSize" ) // SessionCloseErr indicates that the session's Close method was called. var SessionCloseErr error = errors.New("flow was closed directly") //nolint:errname // SessionIdleErr is returned when the session was closed because there was no communication // in either direction over the session for the timeout period. type SessionIdleErr struct { //nolint:errname timeout time.Duration } func (e SessionIdleErr) Error() string { return fmt.Sprintf("flow was idle for %v", e.timeout) } func (e SessionIdleErr) Is(target error) bool { _, ok := target.(SessionIdleErr) return ok } func newSessionIdleErr(timeout time.Duration) error { return SessionIdleErr{timeout} } type Session interface { io.Closer ID() RequestID ConnectionID() uint8 RemoteAddr() net.Addr LocalAddr() net.Addr ResetIdleTimer() Migrate(eyeball DatagramConn, ctx context.Context, logger *zerolog.Logger) // Serve starts the event loop for processing UDP packets Serve(ctx context.Context) error Write(payload []byte) } type session struct { id RequestID closeAfterIdle time.Duration origin io.ReadWriteCloser originAddr net.Addr localAddr net.Addr eyeball atomic.Pointer[DatagramConn] writeChan chan []byte // activeAtChan is used to communicate the last read/write time activeAtChan chan time.Time errChan chan error // The close channel signal only exists for the write loop because the read loop is always waiting on a read // from the UDP socket to the origin. To close the read loop we close the socket. // Additionally, we can't close the writeChan to indicate that writes are complete because the producer (edge) // side may still be trying to write to this session. closeWrite chan struct{} contextChan chan context.Context metrics Metrics log *zerolog.Logger // A special close function that we wrap with sync.Once to make sure it is only called once closeFn func() error } func NewSession( id RequestID, closeAfterIdle time.Duration, origin io.ReadWriteCloser, originAddr net.Addr, localAddr net.Addr, eyeball DatagramConn, metrics Metrics, log *zerolog.Logger, ) Session { logger := log.With().Str(logFlowID, id.String()).Logger() writeChan := make(chan []byte, writeChanCapacity) // errChan has three slots to allow for all writers (the closeFn, the read loop and the write loop) to // write to the channel without blocking since there is only ever one value read from the errChan by the // waitForCloseCondition. errChan := make(chan error, 3) closeWrite := make(chan struct{}) session := &session{ id: id, closeAfterIdle: closeAfterIdle, origin: origin, originAddr: originAddr, localAddr: localAddr, eyeball: atomic.Pointer[DatagramConn]{}, writeChan: writeChan, // activeAtChan has low capacity. It can be full when there are many concurrent read/write. markActive() will // drop instead of blocking because last active time only needs to be an approximation activeAtChan: make(chan time.Time, 1), errChan: errChan, closeWrite: closeWrite, // contextChan is an unbounded channel to help enforce one active migration of a session at a time. contextChan: make(chan context.Context), metrics: metrics, log: &logger, closeFn: sync.OnceValue(func() error { // We don't want to block on sending to the close channel if it is already full select { case errChan <- SessionCloseErr: default: } // Indicate to the write loop that the session is now closed close(closeWrite) // Close the socket directly to unblock the read loop and cause it to also end return origin.Close() }), } session.eyeball.Store(&eyeball) return session } func (s *session) ID() RequestID { return s.id } func (s *session) RemoteAddr() net.Addr { return s.originAddr } func (s *session) LocalAddr() net.Addr { return s.localAddr } func (s *session) ConnectionID() uint8 { eyeball := *(s.eyeball.Load()) return eyeball.ID() } func (s *session) Migrate(eyeball DatagramConn, ctx context.Context, logger *zerolog.Logger) { current := *(s.eyeball.Load()) // Only migrate if the connection ids are different. if current.ID() != eyeball.ID() { s.eyeball.Store(&eyeball) s.contextChan <- ctx log := logger.With().Str(logFlowID, s.id.String()).Logger() s.log = &log } // The session is already running so we want to restart the idle timeout since no proxied packets have come down yet. s.markActive() connectionIndex := eyeball.ID() s.metrics.MigrateFlow(connectionIndex) } func (s *session) Serve(ctx context.Context) error { go s.writeLoop() go s.readLoop() return s.waitForCloseCondition(ctx, s.closeAfterIdle) } // Read datagrams from the origin and write them to the connection. func (s *session) readLoop() { // QUIC implementation copies data to another buffer before returning https://github.com/quic-go/quic-go/blob/v0.24.0/session.go#L1967-L1975 // This makes it safe to share readBuffer between iterations readBuffer := [maxOriginUDPPacketSize + DatagramPayloadHeaderLen]byte{} // To perform a zero copy write when passing the datagram to the connection, we prepare the buffer with // the required datagram header information. We can reuse this buffer for this session since the header is the // same for the each read. _ = MarshalPayloadHeaderTo(s.id, readBuffer[:DatagramPayloadHeaderLen]) for { // Read from the origin UDP socket n, err := s.origin.Read(readBuffer[DatagramPayloadHeaderLen:]) if err != nil { if isConnectionClosed(err) { s.log.Debug().Msgf("flow (read) connection closed: %v", err) } s.closeSession(err) return } if n < 0 { s.metrics.DroppedUDPDatagram(s.ConnectionID(), DroppedReadFailed) s.log.Warn().Int(logPacketSizeKey, n).Msg("flow (origin) packet read was negative and was dropped") continue } if n > maxDatagramPayloadLen { s.metrics.DroppedUDPDatagram(s.ConnectionID(), DroppedReadTooLarge) s.log.Error().Int(logPacketSizeKey, n).Msg("flow (origin) packet read was too large and was dropped") continue } // We need to synchronize on the eyeball in-case that the connection was migrated. This should be rarely a point // of lock contention, as a migration can only happen during startup of a session before traffic flow. eyeball := *(s.eyeball.Load()) // Sending a packet to the session does block on the [quic.Connection], however, this is okay because it // will cause back-pressure to the kernel buffer if the writes are not fast enough to the edge. err = eyeball.SendUDPSessionDatagram(readBuffer[:DatagramPayloadHeaderLen+n]) if err != nil { s.closeSession(err) return } // Mark the session as active since we proxied a valid packet from the origin. s.markActive() } } func (s *session) Write(payload []byte) { select { case s.writeChan <- payload: default: s.metrics.DroppedUDPDatagram(s.ConnectionID(), DroppedWriteFull) s.log.Error().Msg("failed to write flow payload to origin: dropped") } } // Read datagrams from the write channel to the origin. func (s *session) writeLoop() { for { select { case <-s.closeWrite: // When the closeWrite channel is closed, we will no longer write to the origin and end this // goroutine since the session is now closed. return case payload := <-s.writeChan: n, err := s.origin.Write(payload) if err != nil { // Check if this is a write deadline exceeded to the connection if errors.Is(err, os.ErrDeadlineExceeded) { s.metrics.DroppedUDPDatagram(s.ConnectionID(), DroppedWriteDeadlineExceeded) s.log.Warn().Err(err).Msg("flow (write) deadline exceeded: dropping packet") continue } if isConnectionClosed(err) { s.log.Debug().Msgf("flow (write) connection closed: %v", err) } s.log.Err(err).Msg("failed to write flow payload to origin") s.closeSession(err) // If we fail to write to the origin socket, we need to end the writer and close the session return } // Write must return a non-nil error if it returns n < len(p). https://pkg.go.dev/io#Writer if n < len(payload) { s.metrics.DroppedUDPDatagram(s.ConnectionID(), DroppedWriteFailed) s.log.Err(io.ErrShortWrite).Msg("failed to write the full flow payload to origin") continue } // Mark the session as active since we successfully proxied a packet to the origin. s.markActive() } } } func isConnectionClosed(err error) bool { return errors.Is(err, net.ErrClosed) || errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) } // Send an error to the error channel to report that an error has either happened on the tunnel or origin side of the // proxied connection. func (s *session) closeSession(err error) { select { case s.errChan <- err: default: // In the case that the errChan is already full, we will skip over it and return as to not block // the caller because we should start cleaning up the session. s.log.Warn().Msg("error channel was full") } } // ResetIdleTimer will restart the current idle timer. // // This public method is used to allow operators of sessions the ability to extend the session using information that is // known external to the session itself. func (s *session) ResetIdleTimer() { s.markActive() } // Sends the last active time to the idle checker loop without blocking. activeAtChan will only be full when there // are many concurrent read/write. It is fine to lose some precision func (s *session) markActive() { select { case s.activeAtChan <- time.Now(): default: } } func (s *session) Close() error { // Make sure that we only close the origin connection once return s.closeFn() } func (s *session) waitForCloseCondition(ctx context.Context, closeAfterIdle time.Duration) error { connCtx := ctx // Closing the session at the end cancels read so Serve() can return, additionally, it closes the // closeWrite channel which indicates to the write loop to return. defer s.Close() if closeAfterIdle == 0 { // Provided that the default caller doesn't specify one closeAfterIdle = defaultCloseIdleAfter } checkIdleTimer := time.NewTimer(closeAfterIdle) defer checkIdleTimer.Stop() for { select { case <-connCtx.Done(): return connCtx.Err() case newContext := <-s.contextChan: // During migration of a session, we need to make sure that the context of the new connection is used instead // of the old connection context. This will ensure that when the old connection goes away, this session will // still be active on the existing connection. connCtx = newContext continue case reason := <-s.errChan: // Any error returned here is from the read or write loops indicating that it can no longer process datagrams // and as such the session needs to close. s.metrics.FailedFlow(s.ConnectionID()) return reason case <-checkIdleTimer.C: // The check idle timer will only return after an idle period since the last active // operation (read or write). return newSessionIdleErr(closeAfterIdle) case <-s.activeAtChan: // The session is still active, we want to reset the timer. First we have to stop the timer, drain the // current value and then reset. It's okay if we lose some time on this operation as we don't need to // close an idle session directly on-time. if !checkIdleTimer.Stop() { <-checkIdleTimer.C } checkIdleTimer.Reset(closeAfterIdle) } } } ================================================ FILE: quic/v3/session_fuzz_test.go ================================================ package v3_test import ( "testing" ) // FuzzSessionWrite verifies that we don't run into any panics when writing a single variable sized payload to the origin. func FuzzSessionWrite(f *testing.F) { f.Fuzz(func(t *testing.T, b []byte) { // The origin transport read is bound to 1280 bytes if len(b) > 1280 { b = b[:1280] } testSessionWrite(t, [][]byte{b}) }) } // FuzzSessionRead verifies that we don't run into any panics when reading a single variable sized payload from the origin. func FuzzSessionRead(f *testing.F) { f.Fuzz(func(t *testing.T, b []byte) { // The origin transport read is bound to 1280 bytes if len(b) > 1280 { b = b[:1280] } testSessionRead(t, [][]byte{b}) }) } ================================================ FILE: quic/v3/session_test.go ================================================ package v3_test import ( "context" "errors" "io" "net" "net/netip" "slices" "testing" "time" "github.com/fortytw2/leaktest" "github.com/rs/zerolog" v3 "github.com/cloudflare/cloudflared/quic/v3" ) var ( errExpectedContextCanceled = errors.New("expected context canceled") testOriginAddr = net.UDPAddrFromAddrPort(netip.MustParseAddrPort("127.0.0.1:0")) testLocalAddr = net.UDPAddrFromAddrPort(netip.MustParseAddrPort("127.0.0.1:0")) ) func TestSessionNew(t *testing.T) { log := zerolog.Nop() session := v3.NewSession(testRequestID, 5*time.Second, nil, testOriginAddr, testLocalAddr, &noopEyeball{}, &noopMetrics{}, &log) if testRequestID != session.ID() { t.Fatalf("session id doesn't match: %s != %s", testRequestID, session.ID()) } } func testSessionWrite(t *testing.T, payloads [][]byte) { log := zerolog.Nop() origin, server := net.Pipe() defer origin.Close() defer server.Close() // Start origin server reads serverRead := make(chan []byte, len(payloads)) go func() { for range len(payloads) { buf := make([]byte, 1500) _, _ = server.Read(buf[:]) serverRead <- buf } close(serverRead) }() // Create a session session := v3.NewSession(testRequestID, 5*time.Second, origin, testOriginAddr, testLocalAddr, &noopEyeball{}, &noopMetrics{}, &log) defer session.Close() // Start the Serve to begin the writeLoop ctx, cancel := context.WithCancelCause(t.Context()) defer cancel(context.Canceled) done := make(chan error) go func() { done <- session.Serve(ctx) }() // Write the payloads to the session for _, payload := range payloads { session.Write(payload) } // Read from the origin to ensure the payloads were received (in-order) for i, payload := range payloads { read := <-serverRead if !slices.Equal(payload, read[:len(payload)]) { t.Fatalf("payload[%d] provided from origin and read value are not the same (%x) and (%x)", i, payload[:16], read[:16]) } } _, more := <-serverRead if more { t.Fatalf("expected the session to have all of the origin payloads received: %d", len(serverRead)) } assertContextClosed(t, ctx, done, cancel) } func TestSessionWrite(t *testing.T) { defer leaktest.Check(t)() for i := range 1280 { payloads := makePayloads(i, 16) testSessionWrite(t, payloads) } } func testSessionRead(t *testing.T, payloads [][]byte) { log := zerolog.Nop() origin, server := net.Pipe() defer origin.Close() defer server.Close() eyeball := newMockEyeball() session := v3.NewSession(testRequestID, 3*time.Second, origin, testOriginAddr, testLocalAddr, &eyeball, &noopMetrics{}, &log) defer session.Close() ctx, cancel := context.WithCancelCause(t.Context()) defer cancel(context.Canceled) done := make(chan error) go func() { done <- session.Serve(ctx) }() // Write from the origin server to the eyeball go func() { for _, payload := range payloads { _, _ = server.Write(payload) } }() // Read from the eyeball to ensure the payloads were received (in-order) for i, payload := range payloads { select { case data := <-eyeball.recvData: // check received data matches provided from origin expectedData := makePayload(1500) _ = v3.MarshalPayloadHeaderTo(testRequestID, expectedData[:]) copy(expectedData[17:], payload) if !slices.Equal(expectedData[:v3.DatagramPayloadHeaderLen+len(payload)], data) { t.Fatalf("expected datagram[%d] did not equal expected", i) } case err := <-ctx.Done(): // we expect the payload to return before the context to cancel on the session t.Fatal(err) } } assertContextClosed(t, ctx, done, cancel) } func TestSessionRead(t *testing.T) { defer leaktest.Check(t)() for i := range 1280 { payloads := makePayloads(i, 16) testSessionRead(t, payloads) } } func TestSessionRead_OriginTooLarge(t *testing.T) { defer leaktest.Check(t)() log := zerolog.Nop() eyeball := newMockEyeball() payload := makePayload(1281) origin, server := net.Pipe() defer origin.Close() defer server.Close() session := v3.NewSession(testRequestID, 2*time.Second, origin, testOriginAddr, testLocalAddr, &eyeball, &noopMetrics{}, &log) defer session.Close() done := make(chan error) go func() { done <- session.Serve(t.Context()) }() // Attempt to write a payload too large from the origin _, err := server.Write(payload) if err != nil { t.Fatal(err) } select { case data := <-eyeball.recvData: // we never expect a read to make it here because the origin provided a payload that is too large // for cloudflared to proxy and it will drop it. t.Fatalf("we should never proxy a payload of this size: %d", len(data)) case err := <-done: if !errors.Is(err, v3.SessionIdleErr{}) { t.Error(err) } } } func TestSessionServe_Migrate(t *testing.T) { defer leaktest.Check(t)() log := zerolog.Nop() eyeball := newMockEyeball() pipe1, pipe2 := net.Pipe() session := v3.NewSession(testRequestID, 2*time.Second, pipe2, testOriginAddr, testLocalAddr, &eyeball, &noopMetrics{}, &log) defer session.Close() done := make(chan error) eyeball1Ctx, cancel := context.WithCancelCause(t.Context()) go func() { done <- session.Serve(eyeball1Ctx) }() // Migrate the session to a new connection before origin sends data eyeball2 := newMockEyeball() eyeball2.connID = 1 eyeball2Ctx := t.Context() session.Migrate(&eyeball2, eyeball2Ctx, &log) // Cancel the origin eyeball context; this should not cancel the session contextCancelErr := errors.New("context canceled for first eyeball connection") cancel(contextCancelErr) select { case <-done: t.Fatalf("expected session to still be running") default: } if context.Cause(eyeball1Ctx) != contextCancelErr { t.Fatalf("first eyeball context should be cancelled manually: %+v", context.Cause(eyeball1Ctx)) } // Origin sends data payload2 := []byte{0xde} _, _ = pipe1.Write(payload2) // Expect write to eyeball2 data := <-eyeball2.recvData if len(data) <= 17 || !slices.Equal(payload2, data[17:]) { t.Fatalf("expected data to write to eyeball2 after migration: %+v", data) } select { case data := <-eyeball.recvData: t.Fatalf("expected no data to write to eyeball1 after migration: %+v", data) default: } err := <-done if !errors.Is(err, v3.SessionIdleErr{}) { t.Error(err) } if eyeball2Ctx.Err() != nil { t.Fatalf("second eyeball context should be not be cancelled") } } func TestSessionServe_Migrate_CloseContext2(t *testing.T) { defer leaktest.Check(t)() log := zerolog.Nop() eyeball := newMockEyeball() pipe1, pipe2 := net.Pipe() session := v3.NewSession(testRequestID, 2*time.Second, pipe2, testOriginAddr, testLocalAddr, &eyeball, &noopMetrics{}, &log) defer session.Close() done := make(chan error) eyeball1Ctx, cancel := context.WithCancelCause(t.Context()) go func() { done <- session.Serve(eyeball1Ctx) }() // Migrate the session to a new connection before origin sends data eyeball2 := newMockEyeball() eyeball2.connID = 1 eyeball2Ctx, cancel2 := context.WithCancelCause(t.Context()) session.Migrate(&eyeball2, eyeball2Ctx, &log) // Cancel the origin eyeball context; this should not cancel the session contextCancelErr := errors.New("context canceled for first eyeball connection") cancel(contextCancelErr) select { case <-done: t.Fatalf("expected session to still be running") default: } if !errors.Is(context.Cause(eyeball1Ctx), contextCancelErr) { t.Fatalf("first eyeball context should be cancelled manually: %+v", context.Cause(eyeball1Ctx)) } // Origin sends data payload2 := []byte{0xde} _, _ = pipe1.Write(payload2) // Expect write to eyeball2 data := <-eyeball2.recvData if len(data) <= 17 || !slices.Equal(payload2, data[17:]) { t.Fatalf("expected data to write to eyeball2 after migration: %+v", data) } select { case data := <-eyeball.recvData: t.Fatalf("expected no data to write to eyeball1 after migration: %+v", data) default: } // Close the connection2 context manually contextCancel2Err := errors.New("context canceled for second eyeball connection") cancel2(contextCancel2Err) err := <-done if err != context.Canceled { t.Fatalf("session Serve should be done: %+v", err) } if context.Cause(eyeball2Ctx) != contextCancel2Err { t.Fatalf("second eyeball context should have been cancelled manually: %+v", context.Cause(eyeball2Ctx)) } } func TestSessionClose_Multiple(t *testing.T) { defer leaktest.Check(t)() log := zerolog.Nop() origin, server := net.Pipe() defer origin.Close() defer server.Close() session := v3.NewSession(testRequestID, 5*time.Second, origin, testOriginAddr, testLocalAddr, &noopEyeball{}, &noopMetrics{}, &log) err := session.Close() if err != nil { t.Fatal(err) } b := [1500]byte{} _, err = server.Read(b[:]) if !errors.Is(err, io.EOF) { t.Fatalf("origin server connection should be closed: %s", err) } // subsequent closes shouldn't call close again or cause any errors err = session.Close() if err != nil { t.Fatal(err) } _, err = server.Read(b[:]) if !errors.Is(err, io.EOF) { t.Fatalf("origin server connection should still be closed: %s", err) } } func TestSessionServe_IdleTimeout(t *testing.T) { defer leaktest.Check(t)() log := zerolog.Nop() origin, server := net.Pipe() defer origin.Close() defer server.Close() closeAfterIdle := 2 * time.Second session := v3.NewSession(testRequestID, closeAfterIdle, origin, testOriginAddr, testLocalAddr, &noopEyeball{}, &noopMetrics{}, &log) err := session.Serve(t.Context()) // Session should idle timeout if no reads or writes occur if !errors.Is(err, v3.SessionIdleErr{}) { t.Fatal(err) } // session should be closed b := [1500]byte{} _, err = server.Read(b[:]) if !errors.Is(err, io.EOF) { t.Fatalf("session should be closed after Serve returns") } // closing a session again should not return an error err = session.Close() if err != nil { t.Fatal(err) } } func TestSessionServe_ParentContextCanceled(t *testing.T) { defer leaktest.Check(t)() log := zerolog.Nop() origin, server := net.Pipe() defer origin.Close() defer server.Close() closeAfterIdle := 10 * time.Second session := v3.NewSession(testRequestID, closeAfterIdle, origin, testOriginAddr, testLocalAddr, &noopEyeball{}, &noopMetrics{}, &log) ctx, cancel := context.WithTimeout(t.Context(), 2*time.Second) defer cancel() err := session.Serve(ctx) if !errors.Is(err, context.DeadlineExceeded) { t.Fatal(err) } // session should be closed b := [1500]byte{} _, err = server.Read(b[:]) if !errors.Is(err, io.EOF) { t.Fatalf("session should be closed after Serve returns") } // closing a session again should not return an error err = session.Close() if err != nil { t.Fatal(err) } } func TestSessionServe_ReadErrors(t *testing.T) { defer leaktest.Check(t)() log := zerolog.Nop() origin := newTestErrOrigin(net.ErrClosed, nil) session := v3.NewSession(testRequestID, 30*time.Second, &origin, testOriginAddr, testLocalAddr, &noopEyeball{}, &noopMetrics{}, &log) err := session.Serve(t.Context()) if !errors.Is(err, net.ErrClosed) { t.Fatal(err) } } type testErrOrigin struct { readErr error writeErr error } func newTestErrOrigin(readErr error, writeErr error) testErrOrigin { return testErrOrigin{readErr, writeErr} } func (o *testErrOrigin) Read(p []byte) (n int, err error) { return 0, o.readErr } func (o *testErrOrigin) Write(p []byte) (n int, err error) { return len(p), o.writeErr } func (o *testErrOrigin) Close() error { return nil } ================================================ FILE: release/index.html ================================================

Cloudflare packages


Cloudflared

Warning: Public Key Rollover (30 October 2025)

We have rolled our public key for package signing. If you are using RPM-based distributions (RHEL, CentOS, Amazon Linux, etc.) or Debian Trixie and have the old key installed, RPM/Deb packages will no longer work with the old key. Please update your repository configuration using the instructions below to ensure you can continue receiving package updates. The previous keys will still work for other distributions for the time being, but it is now DEPRECATED and will be removed on 30 April 2026

Any Debian Based Distribution (Recommended)

# Add cloudflare gpg key
sudo mkdir -p --mode=0755 /usr/share/keyrings
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null

# Add this repo to your apt repositories
# Stable
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared any main' | sudo tee /etc/apt/sources.list.d/cloudflared.list
# Nightly
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://next.pkg.cloudflare.com/cloudflared any main' | sudo tee /etc/apt/sources.list.d/cloudflared.list

# install cloudflared
sudo apt-get update && sudo apt-get install cloudflared

Debian Bookworm

# Add cloudflare gpg key
sudo mkdir -p --mode=0755 /usr/share/keyrings
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null

# Add this repo to your apt repositories
# Stable
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared bookworm main' | sudo tee /etc/apt/sources.list.d/cloudflared.list
# Nightly
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://next.pkg.cloudflare.com/cloudflared bookworm main' | sudo tee /etc/apt/sources.list.d/cloudflared.list

# install cloudflared
sudo apt-get update && sudo apt-get install cloudflared

Ubuntu 20.04 (Focal Fossa)

# Add cloudflare gpg key
sudo mkdir -p --mode=0755 /usr/share/keyrings
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null

# Add this repo to your apt repositories
# Stable
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared focal main' | sudo tee /etc/apt/sources.list.d/cloudflared.list
# Nightly
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://next.pkg.cloudflare.com/cloudflared focal main' | sudo tee /etc/apt/sources.list.d/cloudflared.list

# install cloudflared
sudo apt-get update && sudo apt-get install cloudflared

Ubuntu 22.04 (Jammy Jellyfish)

# Add cloudflare gpg key
sudo mkdir -p --mode=0755 /usr/share/keyrings
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null

# Add this repo to your apt repositories
# Stable
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared jammy main' | sudo tee /etc/apt/sources.list.d/cloudflared.list
# Nightly
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://next.pkg.cloudflare.com/cloudflared jammy main' | sudo tee /etc/apt/sources.list.d/cloudflared.list

# install cloudflared
sudo apt-get update && sudo apt-get install cloudflared

Ubuntu 24.04 (Noble Numbat)

# Add cloudflare gpg key
sudo mkdir -p --mode=0755 /usr/share/keyrings
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null

# Add this repo to your apt repositories
# Stable
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared noble main' | sudo tee /etc/apt/sources.list.d/cloudflared.list
# Nightly
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://next.pkg.cloudflare.com/cloudflared noble main' | sudo tee /etc/apt/sources.list.d/cloudflared.list

# install cloudflared
sudo apt-get update && sudo apt-get install cloudflared

Amazon Linux

# Add cloudflared.repo to /etc/yum.repos.d/
# Stable
curl -fsSl https://pkg.cloudflare.com/cloudflared.repo | sudo tee /etc/yum.repos.d/cloudflared.repo
# Nightly
curl -fsSl https://next.pkg.cloudflare.com/cloudflared.repo | sudo tee /etc/yum.repos.d/cloudflared.repo

#update repo
sudo yum update

# install cloudflared
sudo yum install cloudflared

RHEL Generic

# Add cloudflared.repo to /etc/yum.repos.d/
# Stable
curl -fsSl https://pkg.cloudflare.com/cloudflared.repo | sudo tee /etc/yum.repos.d/cloudflared.repo
# Nightly
curl -fsSl https://next.pkg.cloudflare.com/cloudflared.repo | sudo tee /etc/yum.repos.d/cloudflared.repo

#update repo
sudo yum update

# install cloudflared
sudo yum install cloudflared

Centos 7

# This requires yum config-manager
sudo yum install yum-utils

# Add cloudflared.repo to config-manager
# Stable
sudo yum-config-manager --add-repo https://pkg.cloudflare.com/cloudflared.repo
# Nightly
sudo yum-config-manager --add-repo https://next.pkg.cloudflare.com/cloudflared.repo

# install cloudflared
yum install cloudflared

Centos 8

# This requires dnf config-manager
# Add cloudflared.repo to config-manager
# Stable
sudo dnf config-manager --add-repo https://pkg.cloudflare.com/cloudflared.repo
# Nightly
sudo dnf config-manager --add-repo https://next.pkg.cloudflare.com/cloudflared.repo

# install cloudflared
sudo dnf install cloudflared

Centos Stream

# This requires dnf config-manager
# Add cloudflared.repo to config-manager
# Stable
sudo dnf config-manager --add-repo https://pkg.cloudflare.com/cloudflared.repo
# Nightly
sudo dnf config-manager --add-repo https://next.pkg.cloudflare.com/cloudflared.repo

# install cloudflared
sudo dnf install cloudflared

Gokeyless

Debian

sudo mkdir -p --mode=0755 /usr/share/keyrings
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null

# Add this repo to your apt repositories
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/gokeyless buster main' | sudo tee /etc/apt/sources.list.d/cloudflare.list

# install gokeyless
sudo apt-get update && sudo apt-get install gokeyless

Centos 8

# This requires dnf config-manager
# Add gokeyless.repo to config-manager
sudo dnf config-manager --add-repo https://pkg.cloudflare.com/gokeyless.repo

# install gokeyless
sudo dnf install gokeyless
================================================ FILE: release_pkgs.py ================================================ """ This is a utility for creating deb and rpm packages, signing them and uploading them to a storage and adding metadata to workers KV. It has two over-arching responsiblities: 1. Create deb and yum repositories from .deb and .rpm files. This is also responsible for signing the packages and generally preparing them to be in an uploadable state. 2. Upload these packages to a storage in a format that apt and yum expect. """ import argparse import base64 import logging import os import shutil from pathlib import Path from subprocess import Popen, PIPE import boto3 import gnupg from botocore.client import Config from botocore.exceptions import ClientError # The front facing R2 URL to access assets from. R2_ASSET_URL = 'https://demo-r2-worker.cloudflare-tunnel.workers.dev/' class PkgUploader: def __init__(self, account_id, bucket_name, client_id, client_secret): self.account_id = account_id self.bucket_name = bucket_name self.client_id = client_id self.client_secret = client_secret def upload_pkg_to_r2(self, filename, upload_file_path): endpoint_url = f"https://{self.account_id}.r2.cloudflarestorage.com" config = Config( region_name='auto', s3={ "addressing_style": "path", } ) r2 = boto3.client( "s3", endpoint_url=endpoint_url, aws_access_key_id=self.client_id, aws_secret_access_key=self.client_secret, config=config, ) print(f"uploading asset: {filename} to {upload_file_path} in bucket {self.bucket_name}...") try: r2.upload_file(filename, self.bucket_name, upload_file_path) except ClientError as e: raise e class PkgCreator: """ The distribution conf is what dictates to reprepro, the debian packaging building and signing tool we use, what distros to support, what GPG key to use for signing and what to call the debian binary etc. This function creates it "./conf/distributions". origin - name of your package (String) label - label of your package (could be same as the name) (String) release - release you want this to be distributed for (List of Strings) components - could be a channel like main/stable/beta archs - Architecture (List of Strings) description - (String) gpg_key_id - gpg key id of what you want to use to sign the packages.(String) """ def create_distribution_conf(self, file_path, origin, label, releases, archs, components, description, gpg_key_id): with open(file_path, "w+") as distributions_file: for release in releases: distributions_file.write(f"Origin: {origin}\n") distributions_file.write(f"Label: {label}\n") distributions_file.write(f"Codename: {release}\n") archs_list = " ".join(archs) distributions_file.write(f"Architectures: {archs_list}\n") distributions_file.write(f"Components: {components}\n") distributions_file.write(f"Description: {description} - {release}\n") distributions_file.write(f"SignWith: {gpg_key_id}\n") distributions_file.write("\n") return distributions_file """ Uses the reprepro tool to generate packages, sign them and create the InRelease as specified by the distribution_conf file. This function creates three folders db, pool and dist. db and pool contain information and metadata about builds. We can ignore these. dist: contains all the pkgs and signed releases that are necessary for an apt download. """ def create_deb_pkgs(self, release, deb_file): print(f"creating deb pkgs: {release} : {deb_file}") p = Popen(["reprepro", "includedeb", release, deb_file], stdout=PIPE, stderr=PIPE) out, err = p.communicate() if p.returncode != 0: print(f"create deb_pkgs result => {out}, {err}") raise def create_rpm_pkgs(self, artifacts_path, gpg_key_name): self._setup_rpm_pkg_directories(artifacts_path, gpg_key_name) p = Popen(["createrepo_c", "./rpm"], stdout=PIPE, stderr=PIPE) out, err = p.communicate() if p.returncode != 0: print(f"create rpm_pkgs result => {out}, {err}") raise self._sign_repomd() """ creates a .repo file with details like so [cloudflared-stable] name=cloudflared-stable baseurl=https://pkg.cloudflare.com/cloudflared/rpm enabled=1 type=rpm gpgcheck=1 gpgkey=https://pkg.cloudflare.com/cloudflare-main.gpg """ def create_repo_file(self, file_path, binary_name, baseurl, gpgkey_url): repo_file_path = os.path.join(file_path, binary_name + '.repo') with open(repo_file_path, "w+") as repo_file: repo_file.write(f"[{binary_name}-stable]\n") repo_file.write(f"name={binary_name}-stable\n") repo_file.write(f"baseurl={baseurl}/rpm\n") repo_file.write("enabled=1\n") repo_file.write("type=rpm\n") repo_file.write("gpgcheck=1\n") repo_file.write(f"gpgkey={gpgkey_url}\n") return repo_file_path def _sign_rpms(self, file_path, gpg_key_name): p = Popen(["rpm", "--define", f"_gpg_name {gpg_key_name}", "--addsign", file_path], stdout=PIPE, stderr=PIPE) out, err = p.communicate() if p.returncode != 0: print(f"rpm sign result result => {out}, {err}") raise def _sign_repomd(self): p = Popen(["gpg", "--batch", "--yes", "--detach-sign", "--armor", "./rpm/repodata/repomd.xml"], stdout=PIPE, stderr=PIPE) out, err = p.communicate() if p.returncode != 0: print(f"sign repomd result => {out}, {err}") raise """ sets up and signs the RPM directories in the following format: - rpm - aarch64 - x86_64 - 386 this assumes the assets are in the format -.rpm """ def _setup_rpm_pkg_directories(self, artifacts_path, gpg_key_name, archs=["aarch64", "x86_64", "386"]): for arch in archs: for root, _, files in os.walk(artifacts_path): for file in files: if file.endswith(f"{arch}.rpm"): new_dir = f"./rpm/{arch}" os.makedirs(new_dir, exist_ok=True) old_path = os.path.join(root, file) new_path = os.path.join(new_dir, file) shutil.copyfile(old_path, new_path) self._sign_rpms(new_path, gpg_key_name) """ imports gpg keys into the system so reprepro and createrepo can use it to sign packages. it returns the GPG ID after a successful import """ def import_gpg_keys(self, private_key, public_key): gpg = gnupg.GPG() private_key = base64.b64decode(private_key) import_result = gpg.import_keys(private_key) if not import_result.fingerprints: raise Exception("Failed to import private key") public_key = base64.b64decode(public_key) gpg.import_keys(public_key) imported_fingerprint = import_result.fingerprints[0] data = gpg.list_keys(secret=True) # Find the specific key we just imported by comparing fingerprints for key in data: if key["fingerprint"] == imported_fingerprint: return (key["fingerprint"], key["uids"][0]) raise Exception(f"Could not find imported key with fingerprint {imported_fingerprint}") def import_multiple_gpg_keys(self, primary_private_key, primary_public_key, secondary_private_key=None, secondary_public_key=None): """ Import one or two GPG keypairs. Returns a list of (fingerprint, uid) with the primary first. """ results = [] if primary_private_key and primary_public_key: results.append(self.import_gpg_keys(primary_private_key, primary_public_key)) if secondary_private_key and secondary_public_key: # Ensure secondary is imported and appended results.append(self.import_gpg_keys(secondary_private_key, secondary_public_key)) return results """ basically rpm --import This enables us to sign rpms. """ def import_rpm_key(self, public_key): file_name = "pb.key" with open(file_name, "wb") as f: public_key = base64.b64decode(public_key) f.write(public_key) p = Popen(["rpm", "--import", file_name], stdout=PIPE, stderr=PIPE) out, err = p.communicate() if p.returncode != 0: print(f"create rpm import result => {out}, {err}") raise """ Walks through a directory and uploads it's assets to R2. directory : root directory to walk through (String). release: release string. If this value is none, a specific release path will not be created and the release will be uploaded to the default path. binary: name of the binary to upload """ def upload_from_directories(pkg_uploader, directory, release, binary): for root, _, files in os.walk(directory): for file in files: upload_file_name = os.path.join(binary, root, file) if release: upload_file_name = os.path.join(release, upload_file_name) filename = os.path.join(root, file) try: pkg_uploader.upload_pkg_to_r2(filename, upload_file_name) except ClientError as e: logging.error(e) return """ 1. looks into a artifacts folder for cloudflared debs 2. creates Packages.gz, InRelease (signed) files 3. uploads them to Cloudflare R2 pkg_creator, pkg_uploader: are instantiations of the two classes above. gpg_key_id: is an id indicating the key the package should be signed with. The public key of this id will be uploaded to R2 so it can be presented to apt downloaders. release_version: is the cloudflared release version. Only provide this if you want a permanent backup. """ def create_deb_packaging(pkg_creator, pkg_uploader, releases, primary_gpg_key_id, secondary_gpg_key_id, binary_name, archs, package_component, release_version): # set configuration for package creation. print(f"initialising configuration for {binary_name} , {archs}") Path("./conf").mkdir(parents=True, exist_ok=True) # If in rollover mode (secondary provided), tell reprepro to sign with both keys. sign_with_ids = primary_gpg_key_id if not secondary_gpg_key_id else f"{primary_gpg_key_id} {secondary_gpg_key_id}" pkg_creator.create_distribution_conf( "./conf/distributions", binary_name, binary_name, releases, archs, package_component, f"apt repository for {binary_name}", sign_with_ids) # create deb pkgs for release in releases: for arch in archs: print(f"creating deb pkgs for {release} and {arch}...") pkg_creator.create_deb_pkgs(release, f"./artifacts/cloudflared-linux-{arch}.deb") print("uploading latest to r2...") upload_from_directories(pkg_uploader, "dists", None, binary_name) upload_from_directories(pkg_uploader, "pool", None, binary_name) if release_version: print(f"uploading versioned release {release_version} to r2...") upload_from_directories(pkg_uploader, "dists", release_version, binary_name) upload_from_directories(pkg_uploader, "pool", release_version, binary_name) def create_rpm_packaging( pkg_creator, pkg_uploader, artifacts_path, release_version, binary_name, gpg_key_name, base_url, gpg_key_url, upload_repo_file=False, ): print(f"creating rpm pkgs...") pkg_creator.create_rpm_pkgs(artifacts_path, gpg_key_name) repo_file = pkg_creator.create_repo_file(artifacts_path, binary_name, base_url, gpg_key_url) print("Uploading repo file") pkg_uploader.upload_pkg_to_r2(repo_file, binary_name + ".repo") print("uploading latest to r2...") upload_from_directories(pkg_uploader, "rpm", None, binary_name) if upload_repo_file: print(f"uploading versioned release {release_version} to r2...") upload_from_directories(pkg_uploader, "rpm", release_version, binary_name) def parse_args(): parser = argparse.ArgumentParser( description="Creates linux releases and uploads them in a packaged format" ) parser.add_argument( "--bucket", default=os.environ.get("R2_BUCKET"), help="R2 Bucket name" ) parser.add_argument( "--id", default=os.environ.get("R2_CLIENT_ID"), help="R2 Client ID" ) parser.add_argument( "--secret", default=os.environ.get("R2_CLIENT_SECRET"), help="R2 Client Secret" ) parser.add_argument( "--account", default=os.environ.get("R2_ACCOUNT_ID"), help="R2 Account Tag" ) parser.add_argument( "--release-tag", default=os.environ.get("RELEASE_VERSION"), help="Release version you want your pkgs to be\ prefixed with. Leave empty if you don't want tagged release versions backed up to R2." ) parser.add_argument( "--binary", default=os.environ.get("BINARY_NAME"), help="The name of the binary the packages are for" ) parser.add_argument( "--gpg-private-key", default=os.environ.get("LINUX_SIGNING_PRIVATE_KEY"), help="GPG private key to sign the\ packages" ) parser.add_argument( "--gpg-public-key", default=os.environ.get("LINUX_SIGNING_PUBLIC_KEY"), help="GPG public key used for\ signing packages" ) # Optional secondary keypair for key rollover parser.add_argument( "--gpg-private-key-2", default=os.environ.get("LINUX_SIGNING_PRIVATE_KEY_2"), help="Secondary GPG private key for rollover" ) parser.add_argument( "--gpg-public-key-2", default=os.environ.get("LINUX_SIGNING_PUBLIC_KEY_2"), help="Secondary GPG public key for rollover" ) parser.add_argument( "--gpg-public-key-url", default=os.environ.get("GPG_PUBLIC_KEY_URL"), help="GPG public key url that\ downloaders can use to verify signing" ) parser.add_argument( "--pkg-upload-url", default=os.environ.get("PKG_URL"), help="URL to be used by downloaders" ) parser.add_argument( "--deb-based-releases", default=["any", "bookworm", "noble", "jammy", "focal", "bionic", "xenial"], help="list of debian based releases that need to be packaged for" ) parser.add_argument( "--archs", default=["amd64", "386", "arm64", "arm", "armhf"], help="list of architectures we want to package for. Note that\ it is the caller's responsiblity to ensure that these debs are already present in a directory. This script\ will not build binaries or create their debs." ) parser.add_argument( "--upload-repo-file", action='store_true', help="Upload RPM repo file to R2" ) args = parser.parse_args() return args if __name__ == "__main__": try: args = parse_args() except Exception as e: logging.exception(e) exit(1) pkg_creator = PkgCreator() # Import one or two keypairs; primary first key_results = pkg_creator.import_multiple_gpg_keys( args.gpg_private_key, args.gpg_public_key, args.gpg_private_key_2, args.gpg_public_key_2, ) if not key_results or len(key_results) == 0: raise SystemExit("No GPG keys were provided for signing") primary_gpg_key_id, primary_gpg_key_name = key_results[0] secondary_gpg_key_id = None secondary_gpg_key_name = None if len(key_results) > 1: secondary_gpg_key_id, secondary_gpg_key_name = key_results[1] if args.gpg_private_key_2: print(f"signing RPM with secondary gpg_key: {secondary_gpg_key_id}") pkg_creator.import_rpm_key(args.gpg_public_key_2) else: print(f"signing RPM with primary gpg_key: {primary_gpg_key_name}") pkg_creator.import_rpm_key(args.gpg_public_key) pkg_uploader = PkgUploader(args.account, args.bucket, args.id, args.secret) print(f"signing deb with primary gpg_key: {primary_gpg_key_id} and secondary gpg_key: {secondary_gpg_key_id}") create_deb_packaging( pkg_creator, pkg_uploader, args.deb_based_releases, primary_gpg_key_id, secondary_gpg_key_id, args.binary, args.archs, "main", args.release_tag, ) create_rpm_packaging( pkg_creator, pkg_uploader, "./artifacts", args.release_tag, args.binary, secondary_gpg_key_name, args.pkg_upload_url, args.gpg_public_key_url, args.upload_repo_file, ) ================================================ FILE: retry/backoffhandler.go ================================================ package retry import ( "context" "math/rand" "time" ) const ( DefaultBaseTime time.Duration = time.Second ) // Redeclare time functions so they can be overridden in tests. type Clock struct { Now func() time.Time After func(d time.Duration) <-chan time.Time } // BackoffHandler manages exponential backoff and limits the maximum number of retries. // The base time period is 1 second, doubling with each retry. // After initial success, a grace period can be set to reset the backoff timer if // a connection is maintained successfully for a long enough period. The base grace period // is 2 seconds, doubling with each retry. type BackoffHandler struct { // MaxRetries sets the maximum number of retries to perform. The default value // of 0 disables retry completely. maxRetries uint // RetryForever caps the exponential backoff period according to MaxRetries // but allows you to retry indefinitely. retryForever bool // BaseTime sets the initial backoff period. baseTime time.Duration retries uint resetDeadline time.Time Clock Clock } func NewBackoff(maxRetries uint, baseTime time.Duration, retryForever bool) BackoffHandler { return BackoffHandler{ maxRetries: maxRetries, baseTime: baseTime, retryForever: retryForever, Clock: Clock{Now: time.Now, After: time.After}, } } func (b BackoffHandler) GetMaxBackoffDuration(ctx context.Context) (time.Duration, bool) { // Follows the same logic as Backoff, but without mutating the receiver. // This select has to happen first to reflect the actual behaviour of the Backoff function. select { case <-ctx.Done(): return time.Duration(0), false default: } if !b.resetDeadline.IsZero() && b.Clock.Now().After(b.resetDeadline) { // b.retries would be set to 0 at this point return time.Second, true } if b.retries >= b.maxRetries && !b.retryForever { return time.Duration(0), false } maxTimeToWait := b.GetBaseTime() * 1 << (b.retries + 1) return maxTimeToWait, true } // BackoffTimer returns a channel that sends the current time when the exponential backoff timeout expires. // Returns nil if the maximum number of retries have been used. func (b *BackoffHandler) BackoffTimer() <-chan time.Time { if !b.resetDeadline.IsZero() && b.Clock.Now().After(b.resetDeadline) { b.retries = 0 b.resetDeadline = time.Time{} } if b.retries >= b.maxRetries { if !b.retryForever { return nil } } else { b.retries++ } maxTimeToWait := b.GetBaseTime() * (1 << b.retries) timeToWait := time.Duration(rand.Int63n(maxTimeToWait.Nanoseconds())) // #nosec G404 return b.Clock.After(timeToWait) } // Backoff is used to wait according to exponential backoff. Returns false if the // maximum number of retries have been used or if the underlying context has been cancelled. func (b *BackoffHandler) Backoff(ctx context.Context) bool { c := b.BackoffTimer() if c == nil { return false } select { case <-c: return true case <-ctx.Done(): return false } } // Sets a grace period within which the backoff timer is maintained. After the grace // period expires, the number of retries & backoff duration is reset. func (b *BackoffHandler) SetGracePeriod() time.Duration { maxTimeToWait := b.GetBaseTime() * 2 << (b.retries + 1) timeToWait := time.Duration(rand.Int63n(maxTimeToWait.Nanoseconds())) // #nosec G404 b.resetDeadline = b.Clock.Now().Add(timeToWait) return timeToWait } func (b BackoffHandler) GetBaseTime() time.Duration { if b.baseTime == 0 { return DefaultBaseTime } return b.baseTime } // Retries returns the number of retries consumed so far. func (b *BackoffHandler) Retries() int { return int(b.retries) // #nosec G115 } func (b *BackoffHandler) ReachedMaxRetries() bool { return b.retries == b.maxRetries } func (b *BackoffHandler) ResetNow() { b.resetDeadline = b.Clock.Now() b.retries = 0 } ================================================ FILE: retry/backoffhandler_test.go ================================================ package retry import ( "context" "testing" "time" ) func immediateTimeAfter(time.Duration) <-chan time.Time { c := make(chan time.Time, 1) c <- time.Now() return c } func TestBackoffRetries(t *testing.T) { ctx := context.Background() // make backoff return immediately backoff := BackoffHandler{maxRetries: 3, Clock: Clock{time.Now, immediateTimeAfter}} if !backoff.Backoff(ctx) { t.Fatalf("backoff failed immediately") } if !backoff.Backoff(ctx) { t.Fatalf("backoff failed after 1 retry") } if !backoff.Backoff(ctx) { t.Fatalf("backoff failed after 2 retry") } if backoff.Backoff(ctx) { t.Fatalf("backoff allowed after 3 (max) retries") } } func TestBackoffCancel(t *testing.T) { ctx, cancelFunc := context.WithCancel(context.Background()) // prevent backoff from returning normally after := func(time.Duration) <-chan time.Time { return make(chan time.Time) } backoff := BackoffHandler{maxRetries: 3, Clock: Clock{time.Now, after}} cancelFunc() if backoff.Backoff(ctx) { t.Fatalf("backoff allowed after cancel") } if _, ok := backoff.GetMaxBackoffDuration(ctx); ok { t.Fatalf("backoff allowed after cancel") } } func TestBackoffGracePeriod(t *testing.T) { ctx := context.Background() currentTime := time.Now() // make Clock.Now return whatever we like now := func() time.Time { return currentTime } // make backoff return immediately backoff := BackoffHandler{maxRetries: 1, Clock: Clock{now, immediateTimeAfter}} if !backoff.Backoff(ctx) { t.Fatalf("backoff failed immediately") } // the next call to Backoff would fail unless it's after the grace period gracePeriod := backoff.SetGracePeriod() // advance time to after the grace period, which at most will be 8 seconds, but we will advance +1 second. currentTime = currentTime.Add(gracePeriod + time.Second) if !backoff.Backoff(ctx) { t.Fatalf("backoff failed after the grace period expired") } // confirm we ignore grace period after backoff if backoff.Backoff(ctx) { t.Fatalf("backoff allowed after 1 (max) retry") } } func TestGetMaxBackoffDurationRetries(t *testing.T) { ctx := context.Background() // make backoff return immediately backoff := BackoffHandler{maxRetries: 3, Clock: Clock{time.Now, immediateTimeAfter}} if _, ok := backoff.GetMaxBackoffDuration(ctx); !ok { t.Fatalf("backoff failed immediately") } backoff.Backoff(ctx) // noop if _, ok := backoff.GetMaxBackoffDuration(ctx); !ok { t.Fatalf("backoff failed after 1 retry") } backoff.Backoff(ctx) // noop if _, ok := backoff.GetMaxBackoffDuration(ctx); !ok { t.Fatalf("backoff failed after 2 retry") } backoff.Backoff(ctx) // noop if _, ok := backoff.GetMaxBackoffDuration(ctx); ok { t.Fatalf("backoff allowed after 3 (max) retries") } if backoff.Backoff(ctx) { t.Fatalf("backoff allowed after 3 (max) retries") } } func TestGetMaxBackoffDuration(t *testing.T) { ctx := context.Background() // make backoff return immediately backoff := BackoffHandler{maxRetries: 3, Clock: Clock{time.Now, immediateTimeAfter}} if duration, ok := backoff.GetMaxBackoffDuration(ctx); !ok || duration > time.Second*2 { t.Fatalf("backoff (%s) didn't return < 2 seconds on first retry", duration) } backoff.Backoff(ctx) // noop if duration, ok := backoff.GetMaxBackoffDuration(ctx); !ok || duration > time.Second*4 { t.Fatalf("backoff (%s) didn't return < 4 seconds on second retry", duration) } backoff.Backoff(ctx) // noop if duration, ok := backoff.GetMaxBackoffDuration(ctx); !ok || duration > time.Second*8 { t.Fatalf("backoff (%s) didn't return < 8 seconds on third retry", duration) } backoff.Backoff(ctx) // noop if duration, ok := backoff.GetMaxBackoffDuration(ctx); ok || duration != 0 { t.Fatalf("backoff (%s) didn't return 0 seconds on fourth retry (exceeding limit)", duration) } } func TestBackoffRetryForever(t *testing.T) { ctx := context.Background() // make backoff return immediately backoff := BackoffHandler{maxRetries: 3, retryForever: true, Clock: Clock{time.Now, immediateTimeAfter}} if duration, ok := backoff.GetMaxBackoffDuration(ctx); !ok || duration > time.Second*2 { t.Fatalf("backoff (%s) didn't return < 2 seconds on first retry", duration) } backoff.Backoff(ctx) // noop if duration, ok := backoff.GetMaxBackoffDuration(ctx); !ok || duration > time.Second*4 { t.Fatalf("backoff (%s) didn't return < 4 seconds on second retry", duration) } backoff.Backoff(ctx) // noop if duration, ok := backoff.GetMaxBackoffDuration(ctx); !ok || duration > time.Second*8 { t.Fatalf("backoff (%s) didn't return < 8 seconds on third retry", duration) } if !backoff.Backoff(ctx) { t.Fatalf("backoff refused on fourth retry despire RetryForever") } if duration, ok := backoff.GetMaxBackoffDuration(ctx); !ok || duration > time.Second*16 { t.Fatalf("backoff returned %v instead of 8 seconds on fourth retry", duration) } if !backoff.Backoff(ctx) { t.Fatalf("backoff refused on fifth retry despire RetryForever") } if duration, ok := backoff.GetMaxBackoffDuration(ctx); !ok || duration > time.Second*16 { t.Fatalf("backoff returned %v instead of 8 seconds on fifth retry", duration) } } ================================================ FILE: signal/safe_signal.go ================================================ package signal import ( "sync" ) // Signal lets goroutines signal that some event has occurred. Other goroutines can wait for the signal. type Signal struct { ch chan struct{} once sync.Once } // New wraps a channel and turns it into a signal for a one-time event. func New(ch chan struct{}) *Signal { return &Signal{ ch: ch, once: sync.Once{}, } } // Notify alerts any goroutines waiting on this signal that the event has occurred. // After the first call to Notify(), future calls are no-op. func (s *Signal) Notify() { s.once.Do(func() { close(s.ch) }) } // Wait returns a channel which will be written to when Notify() is called for the first time. // This channel will never be written to a second time. func (s *Signal) Wait() <-chan struct{} { return s.ch } ================================================ FILE: signal/safe_signal_test.go ================================================ package signal import ( "testing" ) func TestMultiNotifyDoesntCrash(t *testing.T) { sig := New(make(chan struct{})) sig.Notify() sig.Notify() // If code has reached here without crashing, the test has passed. } func TestWait(t *testing.T) { sig := New(make(chan struct{})) sig.Notify() select { case <-sig.Wait(): // Test succeeds return default: // sig.Wait() should have been read from, because sig.Notify() wrote to it. t.Fail() } } ================================================ FILE: socks/auth_handler.go ================================================ package socks import ( "fmt" "io" ) const ( // NoAuth means no authentication is used when connecting NoAuth = uint8(0) // UserPassAuth means a user/password is used when connecting UserPassAuth = uint8(2) noAcceptable = uint8(255) userAuthVersion = uint8(1) authSuccess = uint8(0) authFailure = uint8(1) ) // AuthHandler handles socks authentication requests type AuthHandler interface { Handle(io.Reader, io.Writer) error Register(uint8, Authenticator) } // StandardAuthHandler loads the default authenticators type StandardAuthHandler struct { authenticators map[uint8]Authenticator } // NewAuthHandler creates a default auth handler func NewAuthHandler() AuthHandler { defaults := make(map[uint8]Authenticator) defaults[NoAuth] = NewNoAuthAuthenticator() return &StandardAuthHandler{ authenticators: defaults, } } // Register adds/replaces an Authenticator to use when handling Authentication requests func (h *StandardAuthHandler) Register(method uint8, a Authenticator) { h.authenticators[method] = a } // Handle gets the methods from the SOCKS5 client and authenticates with the first supported method func (h *StandardAuthHandler) Handle(bufConn io.Reader, conn io.Writer) error { methods, err := readMethods(bufConn) if err != nil { return fmt.Errorf("Failed to read auth methods: %v", err) } // first supported method is used for _, method := range methods { authenticator := h.authenticators[method] if authenticator != nil { return authenticator.Handle(bufConn, conn) } } // failed to authenticate. No supported authentication type found conn.Write([]byte{socks5Version, noAcceptable}) return fmt.Errorf("unknown authentication type") } // readMethods is used to read the number and type of methods func readMethods(r io.Reader) ([]byte, error) { header := []byte{0} if _, err := r.Read(header); err != nil { return nil, err } numMethods := int(header[0]) methods := make([]byte, numMethods) _, err := io.ReadAtLeast(r, methods, numMethods) return methods, err } ================================================ FILE: socks/authenticator.go ================================================ package socks import ( "fmt" "io" ) // Authenticator is the connection passed in as a reader/writer to support different authentication types type Authenticator interface { Handle(io.Reader, io.Writer) error } // NoAuthAuthenticator is used to handle the No Authentication mode type NoAuthAuthenticator struct{} // NewNoAuthAuthenticator creates a authless Authenticator func NewNoAuthAuthenticator() Authenticator { return &NoAuthAuthenticator{} } // Handle writes back the version and NoAuth func (a *NoAuthAuthenticator) Handle(reader io.Reader, writer io.Writer) error { _, err := writer.Write([]byte{socks5Version, NoAuth}) return err } // UserPassAuthAuthenticator is used to handle the user/password mode type UserPassAuthAuthenticator struct { IsValid func(string, string) bool } // NewUserPassAuthAuthenticator creates a new username/password validator Authenticator func NewUserPassAuthAuthenticator(isValid func(string, string) bool) Authenticator { return &UserPassAuthAuthenticator{ IsValid: isValid, } } // Handle writes back the version and NoAuth func (a *UserPassAuthAuthenticator) Handle(reader io.Reader, writer io.Writer) error { if _, err := writer.Write([]byte{socks5Version, UserPassAuth}); err != nil { return err } // Get the version and username length header := []byte{0, 0} if _, err := io.ReadAtLeast(reader, header, 2); err != nil { return err } // Ensure compatibility. Someone call E-harmony if header[0] != userAuthVersion { return fmt.Errorf("Unsupported auth version: %v", header[0]) } // Get the user name userLen := int(header[1]) user := make([]byte, userLen) if _, err := io.ReadAtLeast(reader, user, userLen); err != nil { return err } // Get the password length if _, err := reader.Read(header[:1]); err != nil { return err } // Get the password passLen := int(header[0]) pass := make([]byte, passLen) if _, err := io.ReadAtLeast(reader, pass, passLen); err != nil { return err } // Verify the password if a.IsValid(string(user), string(pass)) { _, err := writer.Write([]byte{userAuthVersion, authSuccess}) return err } // password failed. Write back failure if _, err := writer.Write([]byte{userAuthVersion, authFailure}); err != nil { return err } return fmt.Errorf("User authentication failed") } ================================================ FILE: socks/connection_handler.go ================================================ package socks import ( "bufio" "fmt" "io" ) // ConnectionHandler is the Serve method to handle connections // from a local TCP listener of the standard library (net.Listener) type ConnectionHandler interface { Serve(io.ReadWriter) error } // StandardConnectionHandler is the base implementation of handling SOCKS5 requests type StandardConnectionHandler struct { requestHandler RequestHandler authHandler AuthHandler } // NewConnectionHandler creates a standard SOCKS5 connection handler // This process connections from a generic TCP listener from the standard library func NewConnectionHandler(requestHandler RequestHandler) ConnectionHandler { return &StandardConnectionHandler{ requestHandler: requestHandler, authHandler: NewAuthHandler(), } } // Serve process new connection created after calling `Accept()` in the standard library func (h *StandardConnectionHandler) Serve(c io.ReadWriter) error { bufConn := bufio.NewReader(c) // read the version byte version := []byte{0} if _, err := bufConn.Read(version); err != nil { return err } // ensure compatibility if version[0] != socks5Version { return fmt.Errorf("Unsupported SOCKS version: %v", version) } // handle auth if err := h.authHandler.Handle(bufConn, c); err != nil { return err } // process command/request req, err := NewRequest(bufConn) if err != nil { return err } return h.requestHandler.Handle(req, c) } ================================================ FILE: socks/connection_handler_test.go ================================================ package socks import ( "encoding/json" "io" "net" "net/http" "testing" "time" "github.com/stretchr/testify/assert" "golang.org/x/net/proxy" ) type successResponse struct { Status string `json:"status"` } func sendSocksRequest(t *testing.T) []byte { dialer, err := proxy.SOCKS5("tcp", "127.0.0.1:8086", nil, proxy.Direct) assert.NoError(t, err) httpTransport := &http.Transport{} httpClient := &http.Client{Transport: httpTransport} // set our socks5 as the dialer httpTransport.Dial = dialer.Dial req, err := http.NewRequest("GET", "http://127.0.0.1:8085", nil) assert.NoError(t, err) resp, err := httpClient.Do(req) assert.NoError(t, err) defer resp.Body.Close() b, err := io.ReadAll(resp.Body) assert.NoError(t, err) return b } func startTestServer(t *testing.T, httpHandler func(w http.ResponseWriter, r *http.Request)) { // create a socks server requestHandler := NewRequestHandler(NewNetDialer(), nil) socksServer := NewConnectionHandler(requestHandler) listener, err := net.Listen("tcp", "localhost:8086") assert.NoError(t, err) go func() { defer listener.Close() for { conn, _ := listener.Accept() go socksServer.Serve(conn) } }() // create an http server mux := http.NewServeMux() mux.HandleFunc("/", httpHandler) // start the servers go http.ListenAndServe("localhost:8085", mux) } func respondWithJSON(w http.ResponseWriter, v interface{}, status int) { data, _ := json.Marshal(v) w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) w.Write(data) } func OkJSONResponseHandler(w http.ResponseWriter, r *http.Request) { resp := successResponse{ Status: "ok", } respondWithJSON(w, resp, http.StatusOK) } func TestSocksConnection(t *testing.T) { startTestServer(t, OkJSONResponseHandler) time.Sleep(100 * time.Millisecond) b := sendSocksRequest(t) assert.True(t, len(b) > 0, "no data returned!") var resp successResponse json.Unmarshal(b, &resp) assert.True(t, resp.Status == "ok", "response didn't return ok") } ================================================ FILE: socks/dialer.go ================================================ package socks import ( "fmt" "io" "net" ) // Dialer is used to provided the transport of the proxy type Dialer interface { Dial(string) (io.ReadWriteCloser, *AddrSpec, error) } // NetDialer is a standard TCP dialer type NetDialer struct { } // NewNetDialer creates a new dialer func NewNetDialer() Dialer { return &NetDialer{} } // Dial is a base TCP dialer func (d *NetDialer) Dial(address string) (io.ReadWriteCloser, *AddrSpec, error) { c, err := net.Dial("tcp", address) if err != nil { return nil, nil, err } local := c.LocalAddr().(*net.TCPAddr) addr := AddrSpec{IP: local.IP, Port: local.Port} return c, &addr, nil } // ConnDialer is like NetDialer but with an existing TCP dialer already created type ConnDialer struct { conn net.Conn } // NewConnDialer creates a new dialer with a already created net.conn (TCP expected) func NewConnDialer(conn net.Conn) Dialer { return &ConnDialer{ conn: conn, } } // Dial is a TCP dialer but already created func (d *ConnDialer) Dial(address string) (io.ReadWriteCloser, *AddrSpec, error) { local, ok := d.conn.LocalAddr().(*net.TCPAddr) if !ok { return nil, nil, fmt.Errorf("not a tcp connection") } addr := AddrSpec{IP: local.IP, Port: local.Port} return d.conn, &addr, nil } ================================================ FILE: socks/request.go ================================================ package socks import ( "fmt" "io" "net" "strconv" ) const ( // version socks5Version = uint8(5) // commands https://tools.ietf.org/html/rfc1928#section-4 connectCommand = uint8(1) bindCommand = uint8(2) associateCommand = uint8(3) // address types ipv4Address = uint8(1) fqdnAddress = uint8(3) ipv6Address = uint8(4) ) // https://tools.ietf.org/html/rfc1928#section-6 const ( successReply uint8 = iota serverFailure ruleFailure networkUnreachable hostUnreachable connectionRefused ttlExpired commandNotSupported addrTypeNotSupported ) // AddrSpec is used to return the target IPv4, IPv6, or a FQDN type AddrSpec struct { FQDN string IP net.IP Port int } // String gives a host version of the Address func (a *AddrSpec) String() string { if a.FQDN != "" { return fmt.Sprintf("%s (%s):%d", a.FQDN, a.IP, a.Port) } return fmt.Sprintf("%s:%d", a.IP, a.Port) } // Address returns a string suitable to dial; prefer returning IP-based // address, fallback to FQDN func (a AddrSpec) Address() string { if len(a.IP) != 0 { return net.JoinHostPort(a.IP.String(), strconv.Itoa(a.Port)) } return net.JoinHostPort(a.FQDN, strconv.Itoa(a.Port)) } // Request is a SOCKS5 command with supporting field of the connection type Request struct { // Protocol version Version uint8 // Requested command Command uint8 // AddrSpec of the destination DestAddr *AddrSpec // reading from the connection bufConn io.Reader } // NewRequest creates a new request from the connection data stream func NewRequest(bufConn io.Reader) (*Request, error) { // Read the version byte header := []byte{0, 0, 0} if _, err := io.ReadAtLeast(bufConn, header, 3); err != nil { return nil, fmt.Errorf("Failed to get command version: %v", err) } // ensure compatibility if header[0] != socks5Version { return nil, fmt.Errorf("Unsupported command version: %v", header[0]) } // Read in the destination address dest, err := readAddrSpec(bufConn) if err != nil { return nil, err } return &Request{ Version: socks5Version, Command: header[1], DestAddr: dest, bufConn: bufConn, }, nil } func sendReply(w io.Writer, resp uint8, addr *AddrSpec) error { var addrType uint8 var addrBody []byte var addrPort uint16 switch { case addr == nil: addrType = ipv4Address addrBody = []byte{0, 0, 0, 0} addrPort = 0 case addr.FQDN != "": addrType = fqdnAddress addrBody = append([]byte{byte(len(addr.FQDN))}, addr.FQDN...) addrPort = uint16(addr.Port) case addr.IP.To4() != nil: addrType = ipv4Address addrBody = []byte(addr.IP.To4()) addrPort = uint16(addr.Port) case addr.IP.To16() != nil: addrType = ipv6Address addrBody = []byte(addr.IP.To16()) addrPort = uint16(addr.Port) default: return fmt.Errorf("Failed to format address: %v", addr) } // Format the message msg := make([]byte, 6+len(addrBody)) msg[0] = socks5Version msg[1] = resp msg[2] = 0 // Reserved msg[3] = addrType copy(msg[4:], addrBody) msg[4+len(addrBody)] = byte(addrPort >> 8) msg[4+len(addrBody)+1] = byte(addrPort & 0xff) // Send the message _, err := w.Write(msg) return err } // readAddrSpec is used to read AddrSpec. // Expects an address type byte, followed by the address and port func readAddrSpec(r io.Reader) (*AddrSpec, error) { d := &AddrSpec{} // Get the address type addrType := []byte{0} if _, err := r.Read(addrType); err != nil { return nil, err } // Handle on a per type basis switch addrType[0] { case ipv4Address: addr := make([]byte, 4) if _, err := io.ReadAtLeast(r, addr, len(addr)); err != nil { return nil, err } d.IP = net.IP(addr) case ipv6Address: addr := make([]byte, 16) if _, err := io.ReadAtLeast(r, addr, len(addr)); err != nil { return nil, err } d.IP = net.IP(addr) case fqdnAddress: if _, err := r.Read(addrType); err != nil { return nil, err } addrLen := int(addrType[0]) fqdn := make([]byte, addrLen) if _, err := io.ReadAtLeast(r, fqdn, addrLen); err != nil { return nil, err } d.FQDN = string(fqdn) default: return nil, fmt.Errorf("Unrecognized address type") } // Read the port port := []byte{0, 0} if _, err := io.ReadAtLeast(r, port, 2); err != nil { return nil, err } d.Port = (int(port[0]) << 8) | int(port[1]) return d, nil } ================================================ FILE: socks/request_handler.go ================================================ package socks import ( "fmt" "io" "net" "strings" "github.com/rs/zerolog" "github.com/cloudflare/cloudflared/ipaccess" ) // RequestHandler is the functions needed to handle a SOCKS5 command type RequestHandler interface { Handle(*Request, io.ReadWriter) error } // StandardRequestHandler implements the base socks5 command processing type StandardRequestHandler struct { dialer Dialer accessPolicy *ipaccess.Policy } // NewRequestHandler creates a standard SOCKS5 request handler // This handles the SOCKS5 commands and proxies them to their destination func NewRequestHandler(dialer Dialer, accessPolicy *ipaccess.Policy) RequestHandler { return &StandardRequestHandler{ dialer: dialer, accessPolicy: accessPolicy, } } // Handle processes and responds to socks5 commands func (h *StandardRequestHandler) Handle(req *Request, conn io.ReadWriter) error { switch req.Command { case connectCommand: return h.handleConnect(conn, req) case bindCommand: return h.handleBind(conn, req) case associateCommand: return h.handleAssociate(conn, req) default: if err := sendReply(conn, commandNotSupported, nil); err != nil { return fmt.Errorf("Failed to send reply: %v", err) } return fmt.Errorf("Unsupported command: %v", req.Command) } } // handleConnect is used to handle a connect command func (h *StandardRequestHandler) handleConnect(conn io.ReadWriter, req *Request) error { if h.accessPolicy != nil { if req.DestAddr.IP == nil { addr, err := net.ResolveIPAddr("ip", req.DestAddr.FQDN) if err != nil { _ = sendReply(conn, ruleFailure, req.DestAddr) return fmt.Errorf("unable to resolve host to confirm access") } req.DestAddr.IP = addr.IP } if allowed, rule := h.accessPolicy.Allowed(req.DestAddr.IP, req.DestAddr.Port); !allowed { _ = sendReply(conn, ruleFailure, req.DestAddr) if rule != nil { return fmt.Errorf("Connect to %v denied due to iprule: %s", req.DestAddr, rule.String()) } return fmt.Errorf("Connect to %v denied", req.DestAddr) } } target, localAddr, err := h.dialer.Dial(req.DestAddr.Address()) if err != nil { msg := err.Error() resp := hostUnreachable if strings.Contains(msg, "refused") { resp = connectionRefused } else if strings.Contains(msg, "network is unreachable") { resp = networkUnreachable } if err := sendReply(conn, resp, nil); err != nil { return fmt.Errorf("Failed to send reply: %v", err) } return fmt.Errorf("Connect to %v failed: %v", req.DestAddr, err) } defer target.Close() // Send success if err := sendReply(conn, successReply, localAddr); err != nil { return fmt.Errorf("Failed to send reply: %v", err) } // Start proxying proxyDone := make(chan error, 2) go func() { _, e := io.Copy(target, req.bufConn) proxyDone <- e }() go func() { _, e := io.Copy(conn, target) proxyDone <- e }() // Wait for both for i := 0; i < 2; i++ { e := <-proxyDone if e != nil { return e } } return nil } // handleBind is used to handle a bind command // TODO: Support bind command func (h *StandardRequestHandler) handleBind(conn io.ReadWriter, req *Request) error { if err := sendReply(conn, commandNotSupported, nil); err != nil { return fmt.Errorf("Failed to send reply: %v", err) } return nil } // handleAssociate is used to handle a connect command // TODO: Support associate command func (h *StandardRequestHandler) handleAssociate(conn io.ReadWriter, req *Request) error { if err := sendReply(conn, commandNotSupported, nil); err != nil { return fmt.Errorf("Failed to send reply: %v", err) } return nil } func StreamHandler(tunnelConn io.ReadWriter, originConn net.Conn, log *zerolog.Logger) { dialer := NewConnDialer(originConn) requestHandler := NewRequestHandler(dialer, nil) socksServer := NewConnectionHandler(requestHandler) if err := socksServer.Serve(tunnelConn); err != nil { log.Debug().Err(err).Msg("Socks stream handler error") } } func StreamNetHandler(tunnelConn io.ReadWriter, accessPolicy *ipaccess.Policy, log *zerolog.Logger) { dialer := NewNetDialer() requestHandler := NewRequestHandler(dialer, accessPolicy) socksServer := NewConnectionHandler(requestHandler) if err := socksServer.Serve(tunnelConn); err != nil { log.Debug().Err(err).Msg("Socks stream handler error") } } ================================================ FILE: socks/request_handler_test.go ================================================ package socks import ( "bytes" "testing" "github.com/stretchr/testify/assert" "github.com/cloudflare/cloudflared/ipaccess" ) func TestUnsupportedBind(t *testing.T) { req := createRequest(t, socks5Version, bindCommand, "2001:db8::68", 1337, false) var b bytes.Buffer requestHandler := NewRequestHandler(NewNetDialer(), nil) err := requestHandler.Handle(req, &b) assert.NoError(t, err) assert.True(t, b.Bytes()[1] == commandNotSupported, "expected a response") } func TestUnsupportedAssociate(t *testing.T) { req := createRequest(t, socks5Version, associateCommand, "127.0.0.1", 1337, false) var b bytes.Buffer requestHandler := NewRequestHandler(NewNetDialer(), nil) err := requestHandler.Handle(req, &b) assert.NoError(t, err) assert.True(t, b.Bytes()[1] == commandNotSupported, "expected a response") } func TestHandleConnect(t *testing.T) { req := createRequest(t, socks5Version, connectCommand, "127.0.0.1", 1337, false) var b bytes.Buffer requestHandler := NewRequestHandler(NewNetDialer(), nil) err := requestHandler.Handle(req, &b) assert.Error(t, err) assert.True(t, b.Bytes()[1] == connectionRefused, "expected a response") } func TestHandleConnectIPAccess(t *testing.T) { prefix := "127.0.0.0/24" rule1, _ := ipaccess.NewRuleByCIDR(&prefix, []int{1337}, true) rule2, _ := ipaccess.NewRuleByCIDR(&prefix, []int{1338}, false) rules := []ipaccess.Rule{rule1, rule2} var b bytes.Buffer accessPolicy, _ := ipaccess.NewPolicy(false, nil) requestHandler := NewRequestHandler(NewNetDialer(), accessPolicy) req := createRequest(t, socks5Version, connectCommand, "127.0.0.1", 1337, false) err := requestHandler.Handle(req, &b) assert.Error(t, err) assert.True(t, b.Bytes()[1] == ruleFailure, "expected to be denied as no rules and defaultAllow=false") b.Reset() accessPolicy, _ = ipaccess.NewPolicy(true, nil) requestHandler = NewRequestHandler(NewNetDialer(), accessPolicy) req = createRequest(t, socks5Version, connectCommand, "127.0.0.1", 1337, false) err = requestHandler.Handle(req, &b) assert.Error(t, err) assert.True(t, b.Bytes()[1] == connectionRefused, "expected to be allowed as no rules and defaultAllow=true") b.Reset() accessPolicy, _ = ipaccess.NewPolicy(false, rules) requestHandler = NewRequestHandler(NewNetDialer(), accessPolicy) req = createRequest(t, socks5Version, connectCommand, "127.0.0.1", 1337, false) err = requestHandler.Handle(req, &b) assert.Error(t, err) assert.True(t, b.Bytes()[1] == connectionRefused, "expected to be allowed as matching rule") b.Reset() req = createRequest(t, socks5Version, connectCommand, "127.0.0.1", 1338, false) err = requestHandler.Handle(req, &b) assert.Error(t, err) assert.True(t, b.Bytes()[1] == ruleFailure, "expected to be denied as matching rule") b.Reset() req = createRequest(t, socks5Version, connectCommand, "127.0.0.1", 1339, false) err = requestHandler.Handle(req, &b) assert.Error(t, err) assert.True(t, b.Bytes()[1] == ruleFailure, "expect to be denied as no matching rule and defaultAllow=false") } ================================================ FILE: socks/request_test.go ================================================ package socks import ( "bytes" "encoding/binary" "net" "testing" "github.com/stretchr/testify/assert" ) func createRequestData(version, command uint8, ip net.IP, port uint16) []byte { // set the command b := []byte{version, command, 0} // append the ip if len(ip) == net.IPv4len { b = append(b, 1) b = append(b, ip.To4()...) } else { b = append(b, 4) b = append(b, ip.To16()...) } // append the port p := []byte{0, 0} binary.BigEndian.PutUint16(p, port) b = append(b, p...) return b } func createRequest(t *testing.T, version, command uint8, ipStr string, port uint16, shouldFail bool) *Request { ip := net.ParseIP(ipStr) data := createRequestData(version, command, ip, port) reader := bytes.NewReader(data) req, err := NewRequest(reader) if shouldFail { assert.Error(t, err) return nil } assert.NoError(t, err) assert.True(t, req.Version == socks5Version, "version doesn't match expectation: %v", req.Version) assert.True(t, req.Command == command, "command doesn't match expectation: %v", req.Command) assert.True(t, req.DestAddr.Port == int(port), "port doesn't match expectation: %v", req.DestAddr.Port) assert.True(t, req.DestAddr.IP.String() == ipStr, "ip doesn't match expectation: %v", req.DestAddr.IP.String()) return req } func TestValidConnectRequest(t *testing.T) { createRequest(t, socks5Version, connectCommand, "127.0.0.1", 1337, false) } func TestValidBindRequest(t *testing.T) { createRequest(t, socks5Version, bindCommand, "2001:db8::68", 1337, false) } func TestValidAssociateRequest(t *testing.T) { createRequest(t, socks5Version, associateCommand, "127.0.0.1", 1234, false) } func TestInValidVersionRequest(t *testing.T) { createRequest(t, 4, connectCommand, "127.0.0.1", 1337, true) } func TestInValidIPRequest(t *testing.T) { createRequest(t, 4, connectCommand, "127.0.01", 1337, true) } ================================================ FILE: sshgen/sshgen.go ================================================ package sshgen import ( "bytes" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/x509" "encoding/json" "encoding/pem" "fmt" "io" "net/http" "net/url" "os" "time" "github.com/go-jose/go-jose/v4" "github.com/go-jose/go-jose/v4/jwt" homedir "github.com/mitchellh/go-homedir" "github.com/pkg/errors" gossh "golang.org/x/crypto/ssh" "github.com/cloudflare/cloudflared/config" cfpath "github.com/cloudflare/cloudflared/token" ) const ( signEndpoint = "/cdn-cgi/access/cert_sign" keyName = "cf_key" ) // signPayload represents the request body sent to the sign handler API type signPayload struct { PublicKey string `json:"public_key"` JWT string `json:"jwt"` Issuer string `json:"issuer"` } // signResponse represents the response body from the sign handler API type signResponse struct { KeyID string `json:"id"` Certificate string `json:"certificate"` ExpiresAt time.Time `json:"expires_at"` } // ErrorResponse struct stores error information after any error-prone function type errorResponse struct { Status int `json:"status"` Message string `json:"message"` } var mockRequest func(url, contentType string, body io.Reader) (*http.Response, error) = nil var signatureAlgs = []jose.SignatureAlgorithm{jose.RS256} // GenerateShortLivedCertificate generates and stores a keypair for short lived certs func GenerateShortLivedCertificate(appURL *url.URL, token string) error { fullName, err := cfpath.GenerateSSHCertFilePathFromURL(appURL, keyName) if err != nil { return err } cert, err := handleCertificateGeneration(token, fullName) if err != nil { return err } name := fullName + "-cert.pub" if err := writeKey(name, []byte(cert)); err != nil { return err } return nil } // handleCertificateGeneration takes a JWT and uses it build a signPayload // to send to the Sign endpoint with the public key from the keypair it generated func handleCertificateGeneration(token, fullName string) (string, error) { pub, err := generateKeyPair(fullName) if err != nil { return "", err } return SignCert(token, string(pub)) } func SignCert(token, pubKey string) (string, error) { if token == "" { return "", errors.New("invalid token") } parsedToken, err := jwt.ParseSigned(token, signatureAlgs) if err != nil { return "", errors.Wrap(err, "failed to parse JWT") } claims := jwt.Claims{} err = parsedToken.UnsafeClaimsWithoutVerification(&claims) if err != nil { return "", errors.Wrap(err, "failed to retrieve JWT claims") } buf, err := json.Marshal(&signPayload{ PublicKey: pubKey, JWT: token, Issuer: claims.Issuer, }) if err != nil { return "", errors.Wrap(err, "failed to marshal signPayload") } var res *http.Response if mockRequest != nil { res, err = mockRequest(claims.Issuer+signEndpoint, "application/json", bytes.NewBuffer(buf)) } else { client := http.Client{ Timeout: 10 * time.Second, } res, err = client.Post(claims.Issuer+signEndpoint, "application/json", bytes.NewBuffer(buf)) } if err != nil { return "", errors.Wrap(err, "failed to send request") } defer res.Body.Close() decoder := json.NewDecoder(res.Body) if res.StatusCode != 200 { var errResponse errorResponse if err := decoder.Decode(&errResponse); err != nil { return "", err } return "", fmt.Errorf("%d: %s", errResponse.Status, errResponse.Message) } var signRes signResponse if err := decoder.Decode(&signRes); err != nil { return "", errors.Wrap(err, "failed to decode HTTP response") } return signRes.Certificate, nil } // generateKeyPair creates a EC keypair (P256) and stores them in the homedir. // returns the generated public key from the successful keypair generation func generateKeyPair(fullName string) ([]byte, error) { pubKeyName := fullName + ".pub" exist, err := config.FileExists(pubKeyName) if err != nil { return nil, err } if exist { return os.ReadFile(pubKeyName) } key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { return nil, err } parsed, err := x509.MarshalECPrivateKey(key) if err != nil { return nil, err } if err := writeKey(fullName, pem.EncodeToMemory(&pem.Block{ Type: "EC PRIVATE KEY", Bytes: parsed, })); err != nil { return nil, err } pub, err := gossh.NewPublicKey(&key.PublicKey) if err != nil { return nil, err } data := gossh.MarshalAuthorizedKey(pub) if err := writeKey(pubKeyName, data); err != nil { return nil, err } return data, nil } // writeKey will write a key to disk in DER format (it's a standard pem key) func writeKey(filename string, data []byte) error { filepath, err := homedir.Expand(filename) if err != nil { return err } return os.WriteFile(filepath, data, 0600) } ================================================ FILE: sshgen/sshgen_test.go ================================================ //go:build !windows package sshgen import ( "crypto/rand" "crypto/rsa" "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "net/url" "os" "strings" "testing" "time" "github.com/go-jose/go-jose/v4" "github.com/go-jose/go-jose/v4/jwt" "github.com/stretchr/testify/assert" "github.com/cloudflare/cloudflared/config" cfpath "github.com/cloudflare/cloudflared/token" ) const ( audTest = "cf-test-aud" nonceTest = "asfd" ) type signingArguments struct { Principals []string `json:"principals"` ClientPubKey string `json:"public_key"` Duration string `json:"duration"` } func TestCertGenSuccess(t *testing.T) { url, _ := url.Parse("https://cf-test-access.com/testpath") token := tokenGenerator() fullName, err := cfpath.GenerateSSHCertFilePathFromURL(url, keyName) assert.NoError(t, err) assert.True(t, strings.HasSuffix(fullName, "/cf-test-access.com-testpath-cf_key")) pubKeyName := fullName + ".pub" certKeyName := fullName + "-cert.pub" defer func() { os.Remove(fullName) os.Remove(pubKeyName) os.Remove(certKeyName) }() resp := signingArguments{ Principals: []string{"dalton"}, ClientPubKey: "ecdsa-sha2-nistp256-cert-v01@openssh.com AAAAKGVjZHNhLXNoYTItbmlzdHAyNTYtY2VydC12MDFAb3BlbnNzaC5jb20AAAAg+0rYq4mNGIAHiH1xPOJXfmOpTEwFIcyXzGJieTOhRs8AAAAIbmlzdHAyNTYAAABBBJIcsq02e8ZaofJXOZKp7yQdKW/JIouJ90lybr76hHIRrZBL1t4JEimfLvNDphPrTW9VDQaIcBSKNaxRqHOS8jezoJbhFGWhqQAAAAEAAAAgZWU5OTliNGRkZmFmNjgxNDEwMTVhMDJiY2ZhMTdiN2UAAAAKAAAABmF1c3RpbgAAAABc1KFoAAAAAFzUohwAAAAAAAAARwAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAAGgAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAAhuaXN0cDI1NgAAAEEEeAuYR56XaxvH5Z1p0hDCTQ7wC4dbj0Gc+LOKu1f94og2ilZTv9tutg8cZrqAT97REmGH6j9KIOVLGsPVjajSKAAAAGQAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAEkAAAAhAORY9ZO3TQsrUm6ajnVW+arbnVfTkxYBYFlVoeOEXKZuAAAAIG96A8nQnTuprWXLSemWL68RXC1NVKnBOIPD2Z7UIOB1", Duration: "3m", } w := httptest.NewRecorder() respJson, err := json.Marshal(resp) assert.NoError(t, err) w.Write(respJson) mockRequest = func(url, contentType string, body io.Reader) (*http.Response, error) { assert.Contains(t, "/cdn-cgi/access/cert_sign", url) assert.Equal(t, "application/json", contentType) buf, err := io.ReadAll(body) assert.NoError(t, err) assert.NotEmpty(t, buf) return w.Result(), nil } err = GenerateShortLivedCertificate(url, token) assert.NoError(t, err) exist, err := config.FileExists(fullName) assert.NoError(t, err) if !exist { assert.FailNow(t, fmt.Sprintf("key should exist at: %s", fullName), fullName) return } exist, err = config.FileExists(pubKeyName) assert.NoError(t, err) if !exist { assert.FailNow(t, fmt.Sprintf("key should exist at: %s", pubKeyName), pubKeyName) return } exist, err = config.FileExists(certKeyName) assert.NoError(t, err) if !exist { assert.FailNow(t, fmt.Sprintf("key should exist at: %s", certKeyName), certKeyName) return } } func tokenGenerator() string { iat := time.Now() exp := time.Now().Add(time.Minute * 5) claims := jwt.Claims{ Audience: jwt.Audience{audTest}, IssuedAt: jwt.NewNumericDate(iat), Expiry: jwt.NewNumericDate(exp), } key, err := rsa.GenerateKey(rand.Reader, 4096) if err != nil { panic(err) } signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.RS256, Key: key}, (&jose.SignerOptions{}).WithType("JWT")) if err != nil { panic(err) } signedToken, err := jwt.Signed(signer).Claims(claims).Serialize() if err != nil { panic(err) } return signedToken } ================================================ FILE: stream/debug.go ================================================ package stream import ( "io" "sync/atomic" "github.com/rs/zerolog" ) // DebugStream will tee each read and write to the output logger as a debug message type DebugStream struct { reader io.Reader writer io.Writer log *zerolog.Logger max uint64 count atomic.Uint64 } func NewDebugStream(stream io.ReadWriter, logger *zerolog.Logger, max uint64) *DebugStream { return &DebugStream{ reader: stream, writer: stream, log: logger, max: max, } } func (d *DebugStream) Read(p []byte) (n int, err error) { n, err = d.reader.Read(p) if n > 0 && d.max > d.count.Load() { d.count.Add(1) if err != nil { d.log.Err(err). Str("dir", "r"). Int("count", n). Msgf("%+q", p[:n]) } else { d.log.Debug(). Str("dir", "r"). Int("count", n). Msgf("%+q", p[:n]) } } return } func (d *DebugStream) Write(p []byte) (n int, err error) { n, err = d.writer.Write(p) if n > 0 && d.max > d.count.Load() { d.count.Add(1) if err != nil { d.log.Err(err). Str("dir", "w"). Int("count", n). Msgf("%+q", p[:n]) } else { d.log.Debug(). Str("dir", "w"). Int("count", n). Msgf("%+q", p[:n]) } } return } ================================================ FILE: stream/stream.go ================================================ package stream import ( "encoding/hex" "fmt" "io" "runtime/debug" "sync/atomic" "time" "github.com/getsentry/sentry-go" "github.com/pkg/errors" "github.com/rs/zerolog" "github.com/cloudflare/cloudflared/cfio" ) type Stream interface { Reader WriterCloser } type Reader interface { io.Reader } type WriterCloser interface { io.Writer WriteCloser } type WriteCloser interface { CloseWrite() error } type nopCloseWriterAdapter struct { io.ReadWriter } func NopCloseWriterAdapter(stream io.ReadWriter) *nopCloseWriterAdapter { return &nopCloseWriterAdapter{stream} } func (n *nopCloseWriterAdapter) CloseWrite() error { return nil } type bidirectionalStreamStatus struct { doneChan chan struct{} anyDone uint32 } func newBiStreamStatus() *bidirectionalStreamStatus { return &bidirectionalStreamStatus{ doneChan: make(chan struct{}, 2), anyDone: 0, } } func (s *bidirectionalStreamStatus) markUniStreamDone() { atomic.StoreUint32(&s.anyDone, 1) s.doneChan <- struct{}{} } func (s *bidirectionalStreamStatus) wait(maxWaitForSecondStream time.Duration) error { <-s.doneChan // Only wait for second stream to finish if maxWait is greater than zero if maxWaitForSecondStream > 0 { timer := time.NewTimer(maxWaitForSecondStream) defer timer.Stop() select { case <-timer.C: return fmt.Errorf("timeout waiting for second stream to finish") case <-s.doneChan: return nil } } return nil } func (s *bidirectionalStreamStatus) isAnyDone() bool { return atomic.LoadUint32(&s.anyDone) > 0 } // Pipe copies copy data to & from provided io.ReadWriters. func Pipe(tunnelConn, originConn io.ReadWriter, log *zerolog.Logger) { PipeBidirectional(NopCloseWriterAdapter(tunnelConn), NopCloseWriterAdapter(originConn), 0, log) } // PipeBidirectional copies data two BidirectionStreams. It is a special case of Pipe where it receives a concept that allows for Read and Write side to be closed independently. // The main difference is that when piping data from a reader to a writer, if EOF is read, then this implementation propagates the EOF signal to the destination/writer by closing the write side of the // Bidirectional Stream. // Finally, depending on once EOF is ready from one of the provided streams, the other direction of streaming data will have a configured time period to also finish, otherwise, // the method will return immediately with a timeout error. It is however, the responsability of the caller to close the associated streams in both ends in order to free all the resources/go-routines. func PipeBidirectional(downstream, upstream Stream, maxWaitForSecondStream time.Duration, log *zerolog.Logger) error { status := newBiStreamStatus() go unidirectionalStream(downstream, upstream, "upstream->downstream", status, log) go unidirectionalStream(upstream, downstream, "downstream->upstream", status, log) if err := status.wait(maxWaitForSecondStream); err != nil { return errors.Wrap(err, "unable to wait for both streams while proxying") } return nil } func unidirectionalStream(dst WriterCloser, src Reader, dir string, status *bidirectionalStreamStatus, log *zerolog.Logger) { defer func() { // The bidirectional streaming spawns 2 goroutines to stream each direction. // If any ends, the callstack returns, meaning the Tunnel request/stream (depending on http2 vs quic) will // close. In such case, if the other direction did not stop (due to application level stopping, e.g., if a // server/origin listens forever until closure), it may read/write from the underlying ReadWriter (backed by // the Edge<->cloudflared transport) in an unexpected state. // Because of this, we set this recover() logic. if err := recover(); err != nil { if status.isAnyDone() { // We handle such unexpected errors only when we detect that one side of the streaming is done. log.Debug().Msgf("recovered from panic in stream.Pipe for %s, error %s, %s", dir, err, debug.Stack()) } else { // Otherwise, this is unexpected, but we prevent the program from crashing anyway. log.Warn().Msgf("recovered from panic in stream.Pipe for %s, error %s, %s", dir, err, debug.Stack()) sentry.CurrentHub().Recover(err) sentry.Flush(time.Second * 5) } } }() defer dst.CloseWrite() _, err := copyData(dst, src, dir) if err != nil { log.Debug().Msgf("%s copy: %v", dir, err) } status.markUniStreamDone() } // when set to true, enables logging of content copied to/from origin and tunnel const debugCopy = false func copyData(dst io.Writer, src io.Reader, dir string) (written int64, err error) { if debugCopy { // copyBuffer is based on stdio Copy implementation but shows copied data copyBuffer := func(dst io.Writer, src io.Reader, dir string) (written int64, err error) { var buf []byte size := 32 * 1024 buf = make([]byte, size) for { t := time.Now() nr, er := src.Read(buf) if nr > 0 { fmt.Println(dir, t.UnixNano(), "\n"+hex.Dump(buf[0:nr])) nw, ew := dst.Write(buf[0:nr]) if nw < 0 || nr < nw { nw = 0 if ew == nil { ew = errors.New("invalid write") } } written += int64(nw) if ew != nil { err = ew break } if nr != nw { err = io.ErrShortWrite break } } if er != nil { if er != io.EOF { err = er } break } } return written, err } return copyBuffer(dst, src, dir) } else { return cfio.Copy(dst, src) } } ================================================ FILE: stream/stream_test.go ================================================ package stream import ( "fmt" "io" "sync" "testing" "time" "github.com/rs/zerolog" "github.com/stretchr/testify/require" ) func TestPipeBidirectionalFinishBothSides(t *testing.T) { fun := func(upstream, downstream *mockedStream) { downstream.closeReader() upstream.closeReader() } testPipeBidirectionalUnblocking(t, fun, time.Millisecond*200, false) } func TestPipeBidirectionalFinishOneSideTimeout(t *testing.T) { fun := func(upstream, downstream *mockedStream) { downstream.closeReader() } testPipeBidirectionalUnblocking(t, fun, time.Millisecond*200, true) } func TestPipeBidirectionalClosingWriteBothSidesAlsoExists(t *testing.T) { fun := func(upstream, downstream *mockedStream) { downstream.CloseWrite() upstream.CloseWrite() downstream.writeToReader("abc") upstream.writeToReader("abc") } testPipeBidirectionalUnblocking(t, fun, time.Millisecond*200, false) } func TestPipeBidirectionalClosingWriteSingleSideAlsoExists(t *testing.T) { fun := func(upstream, downstream *mockedStream) { downstream.CloseWrite() downstream.writeToReader("abc") upstream.writeToReader("abc") } testPipeBidirectionalUnblocking(t, fun, time.Millisecond*200, true) } func testPipeBidirectionalUnblocking(t *testing.T, afterFun func(*mockedStream, *mockedStream), timeout time.Duration, expectTimeout bool) { logger := zerolog.Nop() downstream := newMockedStream() upstream := newMockedStream() resultCh := make(chan error) go func() { resultCh <- PipeBidirectional(downstream, upstream, timeout, &logger) }() afterFun(upstream, downstream) select { case err := <-resultCh: if expectTimeout { require.NotNil(t, err) } else { require.Nil(t, err) } case <-time.After(timeout * 2): require.Fail(t, "test timeout") } } func newMockedStream() *mockedStream { return &mockedStream{ readCh: make(chan *string), writeCh: make(chan struct{}), } } type mockedStream struct { readCh chan *string writeCh chan struct{} writeCloseOnce sync.Once } func (m *mockedStream) Read(p []byte) (n int, err error) { result := <-m.readCh if result == nil { return 0, io.EOF } return len(*result), nil } func (m *mockedStream) Write(p []byte) (n int, err error) { <-m.writeCh return 0, fmt.Errorf("closed") } func (m *mockedStream) CloseWrite() error { m.writeCloseOnce.Do(func() { close(m.writeCh) }) return nil } func (m *mockedStream) closeReader() { close(m.readCh) } func (m *mockedStream) writeToReader(content string) { m.readCh <- &content } ================================================ FILE: supervisor/conn_aware_logger.go ================================================ package supervisor import ( "github.com/rs/zerolog" "github.com/cloudflare/cloudflared/connection" "github.com/cloudflare/cloudflared/tunnelstate" ) type ConnAwareLogger struct { tracker *tunnelstate.ConnTracker logger *zerolog.Logger } func NewConnAwareLogger(logger *zerolog.Logger, tracker *tunnelstate.ConnTracker, observer *connection.Observer) *ConnAwareLogger { connAwareLogger := &ConnAwareLogger{ tracker: tracker, logger: logger, } observer.RegisterSink(connAwareLogger.tracker) return connAwareLogger } func (c *ConnAwareLogger) ReplaceLogger(logger *zerolog.Logger) *ConnAwareLogger { return &ConnAwareLogger{ tracker: c.tracker, logger: logger, } } func (c *ConnAwareLogger) ConnAwareLogger() *zerolog.Event { if c.tracker.CountActiveConns() == 0 { return c.logger.Error() } return c.logger.Warn() } func (c *ConnAwareLogger) Logger() *zerolog.Logger { return c.logger } ================================================ FILE: supervisor/external_control.go ================================================ package supervisor import ( "time" ) type ReconnectSignal struct { // wait this many seconds before re-establish the connection Delay time.Duration } // Error allows us to use ReconnectSignal as a special error to force connection abort func (r ReconnectSignal) Error() string { return "reconnect signal" } func (r ReconnectSignal) DelayBeforeReconnect() { if r.Delay > 0 { time.Sleep(r.Delay) } } ================================================ FILE: supervisor/fuse.go ================================================ package supervisor import "sync" // booleanFuse is a data structure that can be set once to a particular value using Fuse(value). // Subsequent calls to Fuse() will have no effect. type booleanFuse struct { value int32 mu sync.Mutex cond *sync.Cond } func newBooleanFuse() *booleanFuse { f := &booleanFuse{} f.cond = sync.NewCond(&f.mu) return f } // Value gets the value func (f *booleanFuse) Value() bool { // 0: unset // 1: set true // 2: set false f.mu.Lock() defer f.mu.Unlock() return f.value == 1 } func (f *booleanFuse) Fuse(result bool) { f.mu.Lock() defer f.mu.Unlock() newValue := int32(2) if result { newValue = 1 } if f.value == 0 { f.value = newValue f.cond.Broadcast() } } // Await blocks until Fuse has been called at least once. func (f *booleanFuse) Await() bool { f.mu.Lock() defer f.mu.Unlock() for f.value == 0 { f.cond.Wait() } return f.value == 1 } ================================================ FILE: supervisor/metrics.go ================================================ package supervisor import ( "github.com/prometheus/client_golang/prometheus" "github.com/cloudflare/cloudflared/connection" ) // Metrics uses connection.MetricsNamespace(aka cloudflared) as namespace and connection.TunnelSubsystem // (tunnel) as subsystem to keep them consistent with the previous qualifier. var ( haConnections = prometheus.NewGauge( prometheus.GaugeOpts{ Namespace: connection.MetricsNamespace, Subsystem: connection.TunnelSubsystem, Name: "ha_connections", Help: "Number of active ha connections", }, ) ) func init() { prometheus.MustRegister( haConnections, ) } ================================================ FILE: supervisor/pqtunnels.go ================================================ package supervisor import ( "crypto/tls" "fmt" "github.com/cloudflare/cloudflared/features" ) const ( X25519Kyber768Draft00PQKex = tls.CurveID(0x6399) // X25519Kyber768Draft00 X25519Kyber768Draft00PQKexName = "X25519Kyber768Draft00" P256Kyber768Draft00PQKex = tls.CurveID(0xfe32) // P256Kyber768Draft00 P256Kyber768Draft00PQKexName = "P256Kyber768Draft00" X25519MLKEM768PQKex = tls.CurveID(0x11ec) // X25519MLKEM768 X25519MLKEM768PQKexName = "X25519MLKEM768" ) var ( nonFipsPostQuantumStrictPKex []tls.CurveID = []tls.CurveID{X25519MLKEM768PQKex} nonFipsPostQuantumPreferPKex []tls.CurveID = []tls.CurveID{X25519MLKEM768PQKex} fipsPostQuantumStrictPKex []tls.CurveID = []tls.CurveID{P256Kyber768Draft00PQKex} fipsPostQuantumPreferPKex []tls.CurveID = []tls.CurveID{P256Kyber768Draft00PQKex, tls.CurveP256} ) func removeDuplicates(curves []tls.CurveID) []tls.CurveID { bucket := make(map[tls.CurveID]bool) var result []tls.CurveID for _, curve := range curves { if _, ok := bucket[curve]; !ok { bucket[curve] = true result = append(result, curve) } } return result } func curvePreference(pqMode features.PostQuantumMode, fipsEnabled bool, currentCurve []tls.CurveID) ([]tls.CurveID, error) { switch pqMode { case features.PostQuantumStrict: // If the user passes the -post-quantum flag, we override // CurvePreferences to only support hybrid post-quantum key agreements. if fipsEnabled { return fipsPostQuantumStrictPKex, nil } return nonFipsPostQuantumStrictPKex, nil case features.PostQuantumPrefer: if fipsEnabled { // Ensure that all curves returned are FIPS compliant. // Moreover the first curves are post-quantum and then the // non post-quantum. return fipsPostQuantumPreferPKex, nil } curves := append(nonFipsPostQuantumPreferPKex, currentCurve...) curves = removeDuplicates(curves) return curves, nil default: return nil, fmt.Errorf("Unexpected post quantum mode") } } ================================================ FILE: supervisor/pqtunnels_test.go ================================================ package supervisor import ( "crypto/tls" "net/http" "net/http/httptest" "slices" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/cloudflare/cloudflared/features" "github.com/cloudflare/cloudflared/fips" ) func TestCurvePreferences(t *testing.T) { // This tests if the correct curves are returned // given a PostQuantumMode and a FIPS enabled bool t.Parallel() tests := []struct { name string currentCurves []tls.CurveID expectedCurves []tls.CurveID pqMode features.PostQuantumMode fipsEnabled bool }{ { name: "FIPS with Prefer PQ", pqMode: features.PostQuantumPrefer, fipsEnabled: true, currentCurves: []tls.CurveID{tls.CurveP384}, expectedCurves: []tls.CurveID{P256Kyber768Draft00PQKex, tls.CurveP256}, }, { name: "FIPS with Strict PQ", pqMode: features.PostQuantumStrict, fipsEnabled: true, currentCurves: []tls.CurveID{tls.CurveP256, tls.CurveP384}, expectedCurves: []tls.CurveID{P256Kyber768Draft00PQKex}, }, { name: "FIPS with Prefer PQ - no duplicates", pqMode: features.PostQuantumPrefer, fipsEnabled: true, currentCurves: []tls.CurveID{tls.CurveP256}, expectedCurves: []tls.CurveID{P256Kyber768Draft00PQKex, tls.CurveP256}, }, { name: "Non FIPS with Prefer PQ", pqMode: features.PostQuantumPrefer, fipsEnabled: false, currentCurves: []tls.CurveID{tls.CurveP256}, expectedCurves: []tls.CurveID{X25519MLKEM768PQKex, tls.CurveP256}, }, { name: "Non FIPS with Prefer PQ - no duplicates", pqMode: features.PostQuantumPrefer, fipsEnabled: false, currentCurves: []tls.CurveID{X25519Kyber768Draft00PQKex, tls.CurveP256}, expectedCurves: []tls.CurveID{X25519MLKEM768PQKex, X25519Kyber768Draft00PQKex, tls.CurveP256}, }, { name: "Non FIPS with Prefer PQ - correct preference order", pqMode: features.PostQuantumPrefer, fipsEnabled: false, currentCurves: []tls.CurveID{tls.CurveP256, X25519Kyber768Draft00PQKex}, expectedCurves: []tls.CurveID{X25519MLKEM768PQKex, tls.CurveP256, X25519Kyber768Draft00PQKex}, }, { name: "Non FIPS with Strict PQ", pqMode: features.PostQuantumStrict, fipsEnabled: false, currentCurves: []tls.CurveID{tls.CurveP256}, expectedCurves: []tls.CurveID{X25519MLKEM768PQKex}, }, } for _, tcase := range tests { t.Run(tcase.name, func(t *testing.T) { t.Parallel() curves, err := curvePreference(tcase.pqMode, tcase.fipsEnabled, tcase.currentCurves) require.NoError(t, err) assert.Equal(t, tcase.expectedCurves, curves) }) } } func runClientServerHandshake(t *testing.T, curves []tls.CurveID) []tls.CurveID { var advertisedCurves []tls.CurveID ts := httptest.NewUnstartedServer(nil) ts.TLS = &tls.Config{ // nolint: gosec GetConfigForClient: func(chi *tls.ClientHelloInfo) (*tls.Config, error) { advertisedCurves = slices.Clone(chi.SupportedCurves) return nil, nil }, } ts.StartTLS() defer ts.Close() clientTlsConfig := ts.Client().Transport.(*http.Transport).TLSClientConfig clientTlsConfig.CurvePreferences = curves resp, err := ts.Client().Head(ts.URL) if err != nil { t.Error(err) return nil } defer resp.Body.Close() return advertisedCurves } func TestSupportedCurvesNegotiation(t *testing.T) { for _, tcase := range []features.PostQuantumMode{features.PostQuantumPrefer} { curves, err := curvePreference(tcase, fips.IsFipsEnabled(), make([]tls.CurveID, 0)) require.NoError(t, err) advertisedCurves := runClientServerHandshake(t, curves) assert.Equal(t, curves, advertisedCurves) } } ================================================ FILE: supervisor/supervisor.go ================================================ package supervisor import ( "context" "errors" "net" "strings" "time" "github.com/prometheus/client_golang/prometheus" "github.com/quic-go/quic-go" "github.com/rs/zerolog" "github.com/cloudflare/cloudflared/connection" "github.com/cloudflare/cloudflared/edgediscovery" "github.com/cloudflare/cloudflared/orchestration" v3 "github.com/cloudflare/cloudflared/quic/v3" "github.com/cloudflare/cloudflared/retry" "github.com/cloudflare/cloudflared/signal" "github.com/cloudflare/cloudflared/tunnelstate" ) const ( // Waiting time before retrying a failed tunnel connection tunnelRetryDuration = time.Second * 10 // Interval between registering new tunnels registrationInterval = time.Second ) // Supervisor manages non-declarative tunnels. Establishes TCP connections with the edge, and // reconnects them if they disconnect. type Supervisor struct { config *TunnelConfig orchestrator *orchestration.Orchestrator edgeIPs *edgediscovery.Edge edgeTunnelServer TunnelServer tunnelErrors chan tunnelError tunnelsConnecting map[int]chan struct{} tunnelsProtocolFallback map[int]*protocolFallback // nextConnectedIndex and nextConnectedSignal are used to wait for all // currently-connecting tunnels to finish connecting so we can reset backoff timer nextConnectedIndex int nextConnectedSignal chan struct{} log *ConnAwareLogger logTransport *zerolog.Logger reconnectCh chan ReconnectSignal gracefulShutdownC <-chan struct{} } var errEarlyShutdown = errors.New("shutdown started") type tunnelError struct { index int err error } func NewSupervisor(config *TunnelConfig, orchestrator *orchestration.Orchestrator, reconnectCh chan ReconnectSignal, gracefulShutdownC <-chan struct{}) (*Supervisor, error) { isStaticEdge := len(config.EdgeAddrs) > 0 var err error var edgeIPs *edgediscovery.Edge if isStaticEdge { // static edge addresses edgeIPs, err = edgediscovery.StaticEdge(config.Log, config.EdgeAddrs) } else { edgeIPs, err = edgediscovery.ResolveEdge(config.Log, config.Region, config.EdgeIPVersion) } if err != nil { return nil, err } tracker := tunnelstate.NewConnTracker(config.Log) log := NewConnAwareLogger(config.Log, tracker, config.Observer) edgeAddrHandler := NewIPAddrFallback(config.MaxEdgeAddrRetries) edgeBindAddr := config.EdgeBindAddr datagramMetrics := v3.NewMetrics(prometheus.DefaultRegisterer) sessionManager := v3.NewSessionManager(datagramMetrics, config.Log, config.OriginDialerService, orchestrator.GetFlowLimiter()) edgeTunnelServer := EdgeTunnelServer{ config: config, orchestrator: orchestrator, sessionManager: sessionManager, datagramMetrics: datagramMetrics, edgeAddrs: edgeIPs, edgeAddrHandler: edgeAddrHandler, edgeBindAddr: edgeBindAddr, tracker: tracker, reconnectCh: reconnectCh, gracefulShutdownC: gracefulShutdownC, connAwareLogger: log, } return &Supervisor{ config: config, orchestrator: orchestrator, edgeIPs: edgeIPs, edgeTunnelServer: &edgeTunnelServer, tunnelErrors: make(chan tunnelError), tunnelsConnecting: map[int]chan struct{}{}, tunnelsProtocolFallback: map[int]*protocolFallback{}, log: log, logTransport: config.LogTransport, reconnectCh: reconnectCh, gracefulShutdownC: gracefulShutdownC, }, nil } func (s *Supervisor) Run( ctx context.Context, connectedSignal *signal.Signal, ) error { if s.config.ICMPRouterServer != nil { go func() { if err := s.config.ICMPRouterServer.Serve(ctx); err != nil { if errors.Is(err, net.ErrClosed) { s.log.Logger().Info().Err(err).Msg("icmp router terminated") } else { s.log.Logger().Err(err).Msg("icmp router terminated") } } }() } // Setup DNS Resolver refresh go s.config.OriginDNSService.StartRefreshLoop(ctx) if err := s.initialize(ctx, connectedSignal); err != nil { if err == errEarlyShutdown { return nil } s.log.Logger().Error().Err(err).Msg("initial tunnel connection failed") return err } var tunnelsWaiting []int tunnelsActive := s.config.HAConnections backoff := retry.NewBackoff(s.config.Retries, tunnelRetryDuration, true) var backoffTimer <-chan time.Time shuttingDown := false for { select { // Context cancelled case <-ctx.Done(): for tunnelsActive > 0 { <-s.tunnelErrors tunnelsActive-- } return nil // startTunnel completed with a response // (note that this may also be caused by context cancellation) case tunnelError := <-s.tunnelErrors: tunnelsActive-- s.log.ConnAwareLogger().Err(tunnelError.err).Int(connection.LogFieldConnIndex, tunnelError.index).Msg("Connection terminated") if tunnelError.err != nil && !shuttingDown { switch tunnelError.err.(type) { case ReconnectSignal: // For tunnels that closed with reconnect signal, we reconnect immediately go s.startTunnel(ctx, tunnelError.index, s.newConnectedTunnelSignal(tunnelError.index)) tunnelsActive++ continue } // Make sure we don't continue if there is no more fallback allowed if _, retry := s.tunnelsProtocolFallback[tunnelError.index].GetMaxBackoffDuration(ctx); !retry { continue } tunnelsWaiting = append(tunnelsWaiting, tunnelError.index) s.waitForNextTunnel(tunnelError.index) if backoffTimer == nil { backoffTimer = backoff.BackoffTimer() } } else if tunnelsActive == 0 { s.log.ConnAwareLogger().Msg("no more connections active and exiting") // All connected tunnels exited gracefully, no more work to do return nil } // Backoff was set and its timer expired case <-backoffTimer: backoffTimer = nil for _, index := range tunnelsWaiting { go s.startTunnel(ctx, index, s.newConnectedTunnelSignal(index)) } tunnelsActive += len(tunnelsWaiting) tunnelsWaiting = nil // Tunnel successfully connected case <-s.nextConnectedSignal: if !s.waitForNextTunnel(s.nextConnectedIndex) && len(tunnelsWaiting) == 0 { // No more tunnels outstanding, clear backoff timer backoff.SetGracePeriod() } case <-s.gracefulShutdownC: shuttingDown = true } } } // Returns nil if initialization succeeded, else the initialization error. // Attempts here will be made to connect one tunnel, if successful, it will // connect the available tunnels up to config.HAConnections. func (s *Supervisor) initialize( ctx context.Context, connectedSignal *signal.Signal, ) error { availableAddrs := s.edgeIPs.AvailableAddrs() if s.config.HAConnections > availableAddrs { s.log.Logger().Info().Msgf("You requested %d HA connections but I can give you at most %d.", s.config.HAConnections, availableAddrs) s.config.HAConnections = availableAddrs } s.tunnelsProtocolFallback[0] = &protocolFallback{ retry.NewBackoff(s.config.Retries, retry.DefaultBaseTime, true), s.config.ProtocolSelector.Current(), false, } go s.startFirstTunnel(ctx, connectedSignal) // Wait for response from first tunnel before proceeding to attempt other HA edge tunnels select { case <-ctx.Done(): <-s.tunnelErrors return ctx.Err() case tunnelError := <-s.tunnelErrors: return tunnelError.err case <-s.gracefulShutdownC: return errEarlyShutdown case <-connectedSignal.Wait(): } // At least one successful connection, so start the rest for i := 1; i < s.config.HAConnections; i++ { s.tunnelsProtocolFallback[i] = &protocolFallback{ retry.NewBackoff(s.config.Retries, retry.DefaultBaseTime, true), // Set the protocol we know the first tunnel connected with. s.tunnelsProtocolFallback[0].protocol, false, } go s.startTunnel(ctx, i, s.newConnectedTunnelSignal(i)) time.Sleep(registrationInterval) } return nil } // startTunnel starts the first tunnel connection. The resulting error will be sent on // s.tunnelErrors. It will send a signal via connectedSignal if registration succeed func (s *Supervisor) startFirstTunnel( ctx context.Context, connectedSignal *signal.Signal, ) { var err error const firstConnIndex = 0 isStaticEdge := len(s.config.EdgeAddrs) > 0 defer func() { s.tunnelErrors <- tunnelError{index: firstConnIndex, err: err} }() // If the first tunnel disconnects, keep restarting it. for { err = s.edgeTunnelServer.Serve(ctx, firstConnIndex, s.tunnelsProtocolFallback[firstConnIndex], connectedSignal) if ctx.Err() != nil { return } if err == nil { return } // Make sure we don't continue if there is no more fallback allowed if _, retry := s.tunnelsProtocolFallback[firstConnIndex].GetMaxBackoffDuration(ctx); !retry { return } // Try again for Unauthorized errors because we hope them to be // transient due to edge propagation lag on new Tunnels. if strings.Contains(err.Error(), "Unauthorized") { continue } switch err.(type) { case edgediscovery.ErrNoAddressesLeft: // If your provided addresses are not available, we will keep trying regardless. if !isStaticEdge { return } case connection.DupConnRegisterTunnelError, *quic.IdleTimeoutError, *quic.ApplicationError, edgediscovery.DialError, *connection.EdgeQuicDialError, *connection.ControlStreamError, *connection.StreamListenerError, *connection.DatagramManagerError: // Try again for these types of errors default: // Uncaught errors should bail startup return } } } // startTunnel starts a new tunnel connection. The resulting error will be sent on // s.tunnelError as this is expected to run in a goroutine. func (s *Supervisor) startTunnel( ctx context.Context, index int, connectedSignal *signal.Signal, ) { // nolint: gosec err := s.edgeTunnelServer.Serve(ctx, uint8(index), s.tunnelsProtocolFallback[index], connectedSignal) s.tunnelErrors <- tunnelError{index: index, err: err} } func (s *Supervisor) newConnectedTunnelSignal(index int) *signal.Signal { sig := make(chan struct{}) s.tunnelsConnecting[index] = sig s.nextConnectedSignal = sig s.nextConnectedIndex = index return signal.New(sig) } func (s *Supervisor) waitForNextTunnel(index int) bool { delete(s.tunnelsConnecting, index) s.nextConnectedSignal = nil for k, v := range s.tunnelsConnecting { s.nextConnectedIndex = k s.nextConnectedSignal = v return true } return false } ================================================ FILE: supervisor/tunnel.go ================================================ package supervisor import ( "context" "crypto/tls" "fmt" "net" "net/netip" "runtime/debug" "strings" "sync" "time" "github.com/getsentry/sentry-go" "github.com/pkg/errors" "github.com/quic-go/quic-go" "github.com/rs/zerolog" "golang.org/x/sync/errgroup" "github.com/cloudflare/cloudflared/client" "github.com/cloudflare/cloudflared/connection" "github.com/cloudflare/cloudflared/edgediscovery" "github.com/cloudflare/cloudflared/edgediscovery/allregions" "github.com/cloudflare/cloudflared/features" "github.com/cloudflare/cloudflared/fips" "github.com/cloudflare/cloudflared/ingress" "github.com/cloudflare/cloudflared/ingress/origins" "github.com/cloudflare/cloudflared/management" "github.com/cloudflare/cloudflared/orchestration" quicpogs "github.com/cloudflare/cloudflared/quic" v3 "github.com/cloudflare/cloudflared/quic/v3" "github.com/cloudflare/cloudflared/retry" "github.com/cloudflare/cloudflared/signal" "github.com/cloudflare/cloudflared/tunnelrpc/pogs" "github.com/cloudflare/cloudflared/tunnelstate" ) const ( dialTimeout = 15 * time.Second ) type TunnelConfig struct { ClientConfig *client.Config GracePeriod time.Duration CloseConnOnce *sync.Once // Used to close connectedSignal no more than once EdgeAddrs []string Region string EdgeIPVersion allregions.ConfigIPVersion EdgeBindAddr net.IP HAConnections int IsAutoupdated bool LBPool string Tags []pogs.Tag Log *zerolog.Logger LogTransport *zerolog.Logger Observer *connection.Observer ReportedVersion string Retries uint MaxEdgeAddrRetries uint8 RunFromTerminal bool NeedPQ bool NamedTunnel *connection.TunnelProperties ProtocolSelector connection.ProtocolSelector EdgeTLSConfigs map[connection.Protocol]*tls.Config ICMPRouterServer ingress.ICMPRouterServer OriginDNSService *origins.DNSResolverService OriginDialerService *ingress.OriginDialerService RPCTimeout time.Duration WriteStreamTimeout time.Duration DisableQUICPathMTUDiscovery bool QUICConnectionLevelFlowControlLimit uint64 QUICStreamLevelFlowControlLimit uint64 } func (c *TunnelConfig) connectionOptions(originLocalAddr string, previousAttempts uint8) *client.ConnectionOptionsSnapshot { // attempt to parse out origin IP, but don't fail since it's informational field host, _, _ := net.SplitHostPort(originLocalAddr) originIP := net.ParseIP(host) return c.ClientConfig.ConnectionOptionsSnapshot(originIP, previousAttempts) } func StartTunnelDaemon( ctx context.Context, config *TunnelConfig, orchestrator *orchestration.Orchestrator, connectedSignal *signal.Signal, reconnectCh chan ReconnectSignal, graceShutdownC <-chan struct{}, ) error { s, err := NewSupervisor(config, orchestrator, reconnectCh, graceShutdownC) if err != nil { return err } return s.Run(ctx, connectedSignal) } type ConnectivityError struct { reachedMaxRetries bool } func NewConnectivityError(hasReachedMaxRetries bool) *ConnectivityError { return &ConnectivityError{ reachedMaxRetries: hasReachedMaxRetries, } } func (e *ConnectivityError) Error() string { return fmt.Sprintf("connectivity error - reached max retries: %t", e.HasReachedMaxRetries()) } func (e *ConnectivityError) HasReachedMaxRetries() bool { return e.reachedMaxRetries } // EdgeAddrHandler provides a mechanism switch between behaviors in ServeTunnel // for handling the errors when attempting to make edge connections. type EdgeAddrHandler interface { // ShouldGetNewAddress will check the edge connection error and determine if // the edge address should be replaced with a new one. Also, will return if the // error should be recognized as a connectivity error, or otherwise, a general // application error. ShouldGetNewAddress(connIndex uint8, err error) (needsNewAddress bool, connectivityError error) } func NewIPAddrFallback(maxRetries uint8) *ipAddrFallback { return &ipAddrFallback{ retriesByConnIndex: make(map[uint8]uint8), maxRetries: maxRetries, } } // ipAddrFallback will have more conditions to fall back to a new address for certain // edge connection errors. This means that this handler will return true for isConnectivityError // for more cases like duplicate connection register and edge quic dial errors. type ipAddrFallback struct { m sync.Mutex retriesByConnIndex map[uint8]uint8 maxRetries uint8 } func (f *ipAddrFallback) ShouldGetNewAddress(connIndex uint8, err error) (needsNewAddress bool, connectivityError error) { f.m.Lock() defer f.m.Unlock() switch err.(type) { case nil: // maintain current IP address // Try the next address if it was a quic.IdleTimeoutError // DupConnRegisterTunnelError needs to also receive a new ip address case connection.DupConnRegisterTunnelError, *quic.IdleTimeoutError: return true, nil // Network problems should be retried with new address immediately and report // as connectivity error case edgediscovery.DialError, *connection.EdgeQuicDialError: if f.retriesByConnIndex[connIndex] >= f.maxRetries { f.retriesByConnIndex[connIndex] = 0 return true, NewConnectivityError(true) } f.retriesByConnIndex[connIndex]++ return true, NewConnectivityError(false) default: // maintain current IP address } return false, nil } type EdgeTunnelServer struct { config *TunnelConfig orchestrator *orchestration.Orchestrator sessionManager v3.SessionManager datagramMetrics v3.Metrics edgeAddrHandler EdgeAddrHandler edgeAddrs *edgediscovery.Edge edgeBindAddr net.IP reconnectCh chan ReconnectSignal gracefulShutdownC <-chan struct{} tracker *tunnelstate.ConnTracker connAwareLogger *ConnAwareLogger } type TunnelServer interface { Serve(ctx context.Context, connIndex uint8, protocolFallback *protocolFallback, connectedSignal *signal.Signal) error } func (e *EdgeTunnelServer) Serve(ctx context.Context, connIndex uint8, protocolFallback *protocolFallback, connectedSignal *signal.Signal) error { haConnections.Inc() defer haConnections.Dec() connectedFuse := newBooleanFuse() go func() { if connectedFuse.Await() { connectedSignal.Notify() } }() // Ensure the above goroutine will terminate if we return without connecting defer connectedFuse.Fuse(false) // Fetch IP address to associated connection index addr, err := e.edgeAddrs.GetAddr(int(connIndex)) switch err.(type) { case nil: // no error case edgediscovery.ErrNoAddressesLeft: return err default: return err } logger := e.config.Log.With(). Int(management.EventTypeKey, int(management.Cloudflared)). IPAddr(connection.LogFieldIPAddress, addr.UDP.IP). Uint8(connection.LogFieldConnIndex, connIndex). Logger() connLog := e.connAwareLogger.ReplaceLogger(&logger) // Each connection to keep its own copy of protocol, because individual connections might fallback // to another protocol when a particular metal doesn't support new protocol // Each connection can also have it's own IP version because individual connections might fallback // to another IP version. err, shouldFallbackProtocol := e.serveTunnel( ctx, connLog, addr, connIndex, connectedFuse, protocolFallback, protocolFallback.protocol, ) // Check if the connection error was from an IP issue with the host or // establishing a connection to the edge and if so, rotate the IP address. shouldRotateEdgeIP, cErr := e.edgeAddrHandler.ShouldGetNewAddress(connIndex, err) if shouldRotateEdgeIP { // rotate IP, but forcing internal state to assign a new IP to connection index. if _, err := e.edgeAddrs.GetDifferentAddr(int(connIndex), true); err != nil { return err } // In addition, if it is a connectivity error, and we have exhausted the configurable maximum edge IPs to rotate, // then just fallback protocol on next iteration run. connectivityErr, ok := cErr.(*ConnectivityError) if ok { shouldFallbackProtocol = connectivityErr.HasReachedMaxRetries() } } // set connection has re-connecting and log the next retrying backoff duration, ok := protocolFallback.GetMaxBackoffDuration(ctx) if !ok { return err } e.config.Observer.SendReconnect(connIndex) connLog.Logger().Info().Msgf("Retrying connection in up to %s", duration) select { case <-ctx.Done(): return ctx.Err() case <-e.gracefulShutdownC: return nil case <-protocolFallback.BackoffTimer(): // should we fallback protocol? If not, just return. Otherwise, set new protocol for next method call. if !shouldFallbackProtocol { return err } // If a single connection has connected with the current protocol, we know we know we don't have to fallback // to a different protocol. if e.tracker.HasConnectedWith(e.config.ProtocolSelector.Current()) { return err } if !selectNextProtocol( connLog.Logger(), protocolFallback, e.config.ProtocolSelector, err, ) { return err } } return err } // protocolFallback is a wrapper around backoffHandler that will try fallback option when backoff reaches // max retries type protocolFallback struct { retry.BackoffHandler protocol connection.Protocol inFallback bool } func (pf *protocolFallback) reset() { pf.ResetNow() pf.inFallback = false } func (pf *protocolFallback) fallback(fallback connection.Protocol) { pf.ResetNow() pf.protocol = fallback pf.inFallback = true } // selectNextProtocol picks connection protocol for the next retry iteration, // returns true if it was able to pick the protocol, false if we are out of options and should stop retrying func selectNextProtocol( connLog *zerolog.Logger, protocolBackoff *protocolFallback, selector connection.ProtocolSelector, cause error, ) bool { isQuicBroken := isQuicBroken(cause) _, hasFallback := selector.Fallback() if protocolBackoff.ReachedMaxRetries() || (hasFallback && isQuicBroken) { if isQuicBroken { connLog.Warn().Msg("If this log occurs persistently, and cloudflared is unable to connect to " + "Cloudflare Network with `quic` protocol, then most likely your machine/network is getting its egress " + "UDP to port 7844 (or others) blocked or dropped. Make sure to allow egress connectivity as per " + "https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/configuration/ports-and-ips/\n" + "If you are using private routing to this Tunnel, then ICMP, UDP (and Private DNS Resolution) will not work " + "unless your cloudflared can connect with Cloudflare Network with `quic`.") } fallback, hasFallback := selector.Fallback() if !hasFallback { return false } // Already using fallback protocol, no point to retry if protocolBackoff.protocol == fallback { return false } connLog.Info().Msgf("Switching to fallback protocol %s", fallback) protocolBackoff.fallback(fallback) } else if !protocolBackoff.inFallback { current := selector.Current() if protocolBackoff.protocol != current { protocolBackoff.protocol = current connLog.Info().Msgf("Changing protocol to %s", current) } } return true } func isQuicBroken(cause error) bool { var idleTimeoutError *quic.IdleTimeoutError if errors.As(cause, &idleTimeoutError) { return true } var transportError *quic.TransportError if errors.As(cause, &transportError) && strings.Contains(cause.Error(), "operation not permitted") { return true } return false } // ServeTunnel runs a single tunnel connection, returns nil on graceful shutdown, // on error returns a flag indicating if error can be retried func (e *EdgeTunnelServer) serveTunnel( ctx context.Context, connLog *ConnAwareLogger, addr *allregions.EdgeAddr, connIndex uint8, fuse *booleanFuse, backoff *protocolFallback, protocol connection.Protocol, ) (err error, recoverable bool) { // Treat panics as recoverable errors defer func() { if r := recover(); r != nil { var ok bool err, ok = r.(error) if !ok { err = fmt.Errorf("ServeTunnel: %v", r) } err = errors.Wrapf(err, "stack trace: %s", string(debug.Stack())) recoverable = true } }() defer e.config.Observer.SendDisconnect(connIndex) err, recoverable = e.serveConnection( ctx, connLog, addr, connIndex, fuse, backoff, protocol, ) if err != nil { switch err := err.(type) { case connection.DupConnRegisterTunnelError: connLog.ConnAwareLogger().Err(err).Msg("Unable to establish connection.") // don't retry this connection anymore, let supervisor pick a new address return err, false case connection.ServerRegisterTunnelError: connLog.ConnAwareLogger().Err(err).Msg("Register tunnel error from server side") // Don't send registration error return from server to Sentry. They are // logged on server side return err.Cause, !err.Permanent case *connection.EdgeQuicDialError: return err, false case ReconnectSignal: connLog.Logger().Info(). IPAddr(connection.LogFieldIPAddress, addr.UDP.IP). Uint8(connection.LogFieldConnIndex, connIndex). Msgf("Restarting connection due to reconnect signal in %s", err.Delay) err.DelayBeforeReconnect() return err, true default: if err == context.Canceled { connLog.Logger().Debug().Err(err).Msgf("Serve tunnel error") return err, false } connLog.ConnAwareLogger().Err(err).Msgf("Serve tunnel error") _, permanent := err.(unrecoverableError) return err, !permanent } } return nil, false } func (e *EdgeTunnelServer) serveConnection( ctx context.Context, connLog *ConnAwareLogger, addr *allregions.EdgeAddr, connIndex uint8, fuse *booleanFuse, backoff *protocolFallback, protocol connection.Protocol, ) (err error, recoverable bool) { connectedFuse := &connectedFuse{ fuse: fuse, backoff: backoff, } controlStream := connection.NewControlStream( e.config.Observer, connectedFuse, e.config.NamedTunnel, connIndex, addr.UDP.IP, nil, e.config.RPCTimeout, e.gracefulShutdownC, e.config.GracePeriod, protocol, ) switch protocol { case connection.QUIC: // nolint: gosec connOptions := e.config.connectionOptions(addr.UDP.String(), uint8(backoff.Retries())) // nolint: zerologlint connOptions.LogFields(connLog.Logger().Debug().Uint8(connection.LogFieldConnIndex, connIndex)).Msgf("Tunnel connection options") return e.serveQUIC(ctx, addr.UDP.AddrPort(), connLog, connOptions, controlStream, connIndex) case connection.HTTP2: edgeConn, err := edgediscovery.DialEdge(ctx, dialTimeout, e.config.EdgeTLSConfigs[protocol], addr.TCP, e.edgeBindAddr) if err != nil { connLog.ConnAwareLogger().Err(err).Msg("Unable to establish connection with Cloudflare edge") return err, true } // nolint: gosec connOptions := e.config.connectionOptions(edgeConn.LocalAddr().String(), uint8(backoff.Retries())) // nolint: zerologlint connOptions.LogFields(connLog.Logger().Debug().Uint8(connection.LogFieldConnIndex, connIndex)).Msgf("Tunnel connection options") if err := e.serveHTTP2( ctx, connLog, edgeConn, connOptions, controlStream, connIndex, ); err != nil { return err, false } default: return fmt.Errorf("invalid protocol selected: %s", protocol), false } return } type unrecoverableError struct { err error } func (r unrecoverableError) Error() string { return r.err.Error() } func (e *EdgeTunnelServer) serveHTTP2( ctx context.Context, connLog *ConnAwareLogger, tlsServerConn net.Conn, connOptions *client.ConnectionOptionsSnapshot, controlStreamHandler connection.ControlStreamHandler, connIndex uint8, ) error { pqMode := connOptions.FeatureSnapshot.PostQuantum if pqMode == features.PostQuantumStrict { return unrecoverableError{errors.New("HTTP/2 transport does not support post-quantum")} } connLog.Logger().Debug().Msgf("Connecting via http2") h2conn := connection.NewHTTP2Connection( tlsServerConn, e.orchestrator, connOptions, e.config.Observer, connIndex, controlStreamHandler, e.config.Log, ) errGroup, serveCtx := errgroup.WithContext(ctx) errGroup.Go(func() error { return h2conn.Serve(serveCtx) }) errGroup.Go(func() error { err := listenReconnect(serveCtx, e.reconnectCh, e.gracefulShutdownC) if err != nil { // forcefully break the connection (this is only used for testing) // errgroup will return context canceled for the h2conn.Serve connLog.Logger().Debug().Msg("Forcefully breaking http2 connection") } return err }) return errGroup.Wait() } func (e *EdgeTunnelServer) serveQUIC( ctx context.Context, edgeAddr netip.AddrPort, connLogger *ConnAwareLogger, connOptions *client.ConnectionOptionsSnapshot, controlStreamHandler connection.ControlStreamHandler, connIndex uint8, ) (err error, recoverable bool) { tlsConfig := e.config.EdgeTLSConfigs[connection.QUIC] pqMode := connOptions.FeatureSnapshot.PostQuantum curvePref, err := curvePreference(pqMode, fips.IsFipsEnabled(), tlsConfig.CurvePreferences) if err != nil { connLogger.ConnAwareLogger().Err(err).Msgf("failed to get curve preferences") return err, true } connLogger.Logger().Info().Msgf("Tunnel connection curve preferences: %v", curvePref) tlsConfig.CurvePreferences = curvePref // quic-go 0.44 increases the initial packet size to 1280 by default. That breaks anyone running tunnel through WARP // because WARP MTU is 1280. var initialPacketSize uint16 = 1252 if edgeAddr.Addr().Is4() { initialPacketSize = 1232 } quicConfig := &quic.Config{ HandshakeIdleTimeout: quicpogs.HandshakeIdleTimeout, MaxIdleTimeout: quicpogs.MaxIdleTimeout, KeepAlivePeriod: quicpogs.MaxIdlePingPeriod, MaxIncomingStreams: quicpogs.MaxIncomingStreams, MaxIncomingUniStreams: quicpogs.MaxIncomingStreams, EnableDatagrams: true, Tracer: quicpogs.NewClientTracer(connLogger.Logger(), connIndex), DisablePathMTUDiscovery: e.config.DisableQUICPathMTUDiscovery, MaxConnectionReceiveWindow: e.config.QUICConnectionLevelFlowControlLimit, MaxStreamReceiveWindow: e.config.QUICStreamLevelFlowControlLimit, InitialPacketSize: initialPacketSize, } // Dial the QUIC connection to the edge conn, err := connection.DialQuic( ctx, quicConfig, tlsConfig, edgeAddr, e.edgeBindAddr, connIndex, connLogger.Logger(), ) if err != nil { connLogger.ConnAwareLogger().Err(err).Msgf("Failed to dial a quic connection") e.reportErrorToSentry(err, connOptions.FeatureSnapshot.PostQuantum) return err, true } var datagramSessionManager connection.DatagramSessionHandler if connOptions.FeatureSnapshot.DatagramVersion == features.DatagramV3 { datagramSessionManager = connection.NewDatagramV3Connection( ctx, conn, e.sessionManager, e.config.ICMPRouterServer, connIndex, e.datagramMetrics, connLogger.Logger(), ) } else { datagramSessionManager = connection.NewDatagramV2Connection( ctx, conn, e.config.OriginDialerService, e.config.ICMPRouterServer, connIndex, e.config.RPCTimeout, e.config.WriteStreamTimeout, e.orchestrator.GetFlowLimiter(), connLogger.Logger(), ) } // Wrap the [quic.Connection] as a TunnelConnection tunnelConn := connection.NewTunnelConnection( ctx, conn, connIndex, e.orchestrator, datagramSessionManager, controlStreamHandler, connOptions, e.config.RPCTimeout, e.config.WriteStreamTimeout, e.config.GracePeriod, connLogger.Logger(), ) // Serve the TunnelConnection errGroup, serveCtx := errgroup.WithContext(ctx) errGroup.Go(func() error { err := tunnelConn.Serve(serveCtx) if err != nil { connLogger.ConnAwareLogger().Err(err).Msg("failed to serve tunnel connection") } return err }) errGroup.Go(func() error { err := listenReconnect(serveCtx, e.reconnectCh, e.gracefulShutdownC) if err != nil { // forcefully break the connection (this is only used for testing) // errgroup will return context canceled for the tunnelConn.Serve connLogger.Logger().Debug().Msg("Forcefully breaking tunnel connection") } return err }) return errGroup.Wait(), false } // The reportErrorToSentry is an helper function that handles // verifies if an error should be reported to Sentry. func (e *EdgeTunnelServer) reportErrorToSentry(err error, pqMode features.PostQuantumMode) { dialErr, ok := err.(*connection.EdgeQuicDialError) if ok { // The TransportError provides an Unwrap function however // the err MAY not always be set transportErr, ok := dialErr.Cause.(*quic.TransportError) if ok && transportErr.ErrorCode.IsCryptoError() && fips.IsFipsEnabled() && pqMode == features.PostQuantumStrict { // Only report to Sentry when using FIPS, PQ, // and the error is a Crypto error reported by // an EdgeQuicDialError sentry.CaptureException(err) } } } func listenReconnect(ctx context.Context, reconnectCh <-chan ReconnectSignal, gracefulShutdownCh <-chan struct{}) error { select { case reconnect := <-reconnectCh: return reconnect case <-gracefulShutdownCh: return nil case <-ctx.Done(): return nil } } type connectedFuse struct { fuse *booleanFuse backoff *protocolFallback } func (cf *connectedFuse) Connected() { cf.fuse.Fuse(true) cf.backoff.reset() } func (cf *connectedFuse) IsConnected() bool { return cf.fuse.Value() } ================================================ FILE: supervisor/tunnel_test.go ================================================ package supervisor import ( "testing" "time" "github.com/quic-go/quic-go" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" "github.com/cloudflare/cloudflared/connection" "github.com/cloudflare/cloudflared/edgediscovery" "github.com/cloudflare/cloudflared/retry" ) type dynamicMockFetcher struct { protocolPercents edgediscovery.ProtocolPercents err error } func (dmf *dynamicMockFetcher) fetch() edgediscovery.PercentageFetcher { return func() (edgediscovery.ProtocolPercents, error) { return dmf.protocolPercents, dmf.err } } func immediateTimeAfter(time.Duration) <-chan time.Time { c := make(chan time.Time, 1) c <- time.Now() return c } func TestWaitForBackoffFallback(t *testing.T) { maxRetries := uint(3) backoff := retry.NewBackoff(maxRetries, 40*time.Millisecond, false) backoff.Clock.After = immediateTimeAfter log := zerolog.Nop() resolveTTL := 10 * time.Second mockFetcher := dynamicMockFetcher{ protocolPercents: edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "quic", Percentage: 100}}, } protocolSelector, err := connection.NewProtocolSelector( "auto", "", false, false, mockFetcher.fetch(), resolveTTL, &log, ) assert.NoError(t, err) initProtocol := protocolSelector.Current() assert.Equal(t, connection.QUIC, initProtocol) protoFallback := &protocolFallback{ backoff, initProtocol, false, } // Retry #0 and #1. At retry #2, we switch protocol, so the fallback loop has one more retry than this for i := 0; i < int(maxRetries-1); i++ { protoFallback.BackoffTimer() // simulate retry ok := selectNextProtocol(&log, protoFallback, protocolSelector, nil) assert.True(t, ok) assert.Equal(t, initProtocol, protoFallback.protocol) } // Retry fallback protocol protoFallback.BackoffTimer() // simulate retry ok := selectNextProtocol(&log, protoFallback, protocolSelector, nil) assert.True(t, ok) fallback, ok := protocolSelector.Fallback() assert.True(t, ok) assert.Equal(t, fallback, protoFallback.protocol) assert.Equal(t, connection.HTTP2, protoFallback.protocol) currentGlobalProtocol := protocolSelector.Current() assert.Equal(t, initProtocol, currentGlobalProtocol) // Simulate max retries again (retries reset after protocol switch) for i := 0; i < int(maxRetries); i++ { protoFallback.BackoffTimer() } // No protocol to fallback, return error ok = selectNextProtocol(&log, protoFallback, protocolSelector, nil) assert.False(t, ok) protoFallback.reset() protoFallback.BackoffTimer() // simulate retry ok = selectNextProtocol(&log, protoFallback, protocolSelector, nil) assert.True(t, ok) assert.Equal(t, initProtocol, protoFallback.protocol) protoFallback.reset() protoFallback.BackoffTimer() // simulate retry ok = selectNextProtocol(&log, protoFallback, protocolSelector, &quic.IdleTimeoutError{}) // Check that we get a true after the first try itself when this flag is true. This allows us to immediately // switch protocols when there is a fallback. assert.True(t, ok) // But if there is no fallback available, then we exhaust the retries despite the type of error. // The reason why there's no fallback available is because we pick a specific protocol instead of letting it be auto. protocolSelector, err = connection.NewProtocolSelector( "quic", "", false, false, mockFetcher.fetch(), resolveTTL, &log, ) assert.NoError(t, err) protoFallback = &protocolFallback{backoff, protocolSelector.Current(), false} for i := 0; i < int(maxRetries-1); i++ { protoFallback.BackoffTimer() // simulate retry ok := selectNextProtocol(&log, protoFallback, protocolSelector, &quic.IdleTimeoutError{}) assert.True(t, ok) assert.Equal(t, connection.QUIC, protoFallback.protocol) } // And finally it fails as it should, with no fallback. protoFallback.BackoffTimer() ok = selectNextProtocol(&log, protoFallback, protocolSelector, &quic.IdleTimeoutError{}) assert.False(t, ok) } ================================================ FILE: supervisor/tunnelsforha.go ================================================ package supervisor import ( "fmt" "sync" "github.com/prometheus/client_golang/prometheus" ) // tunnelsForHA maps this cloudflared instance's HA connections to the tunnel IDs they serve. type tunnelsForHA struct { sync.Mutex metrics *prometheus.GaugeVec entries map[uint8]string } // NewTunnelsForHA initializes the Prometheus metrics etc for a tunnelsForHA. func NewTunnelsForHA() tunnelsForHA { metrics := prometheus.NewGaugeVec( prometheus.GaugeOpts{ Name: "tunnel_ids", Help: "The ID of all tunnels (and their corresponding HA connection ID) running in this instance of cloudflared.", }, []string{"tunnel_id", "ha_conn_id"}, ) prometheus.MustRegister(metrics) return tunnelsForHA{ metrics: metrics, entries: make(map[uint8]string), } } // Track a new tunnel ID, removing the disconnected tunnel (if any) and update metrics. func (t *tunnelsForHA) AddTunnelID(haConn uint8, tunnelID string) { t.Lock() defer t.Unlock() haStr := fmt.Sprintf("%v", haConn) if oldTunnelID, ok := t.entries[haConn]; ok { t.metrics.WithLabelValues(oldTunnelID, haStr).Dec() } t.entries[haConn] = tunnelID t.metrics.WithLabelValues(tunnelID, haStr).Inc() } func (t *tunnelsForHA) String() string { t.Lock() defer t.Unlock() return fmt.Sprintf("%v", t.entries) } ================================================ FILE: tlsconfig/certreloader.go ================================================ package tlsconfig import ( "crypto/tls" "crypto/x509" "fmt" "os" "runtime" "sync" "github.com/getsentry/sentry-go" "github.com/pkg/errors" "github.com/rs/zerolog" "github.com/urfave/cli/v2" ) const ( OriginCAPoolFlag = "origin-ca-pool" CaCertFlag = "cacert" ) // CertReloader can load and reload a TLS certificate from a particular filepath. // Hooks into tls.Config's GetCertificate to allow a TLS server to update its certificate without restarting. type CertReloader struct { sync.Mutex certificate *tls.Certificate certPath string keyPath string } // NewCertReloader makes a CertReloader. It loads the cert during initialization to make sure certPath and keyPath are valid func NewCertReloader(certPath, keyPath string) (*CertReloader, error) { cr := new(CertReloader) cr.certPath = certPath cr.keyPath = keyPath if err := cr.LoadCert(); err != nil { return nil, err } return cr, nil } // Cert returns the TLS certificate most recently read by the CertReloader. // This method works as a direct utility method for tls.Config#Cert. func (cr *CertReloader) Cert(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { cr.Lock() defer cr.Unlock() return cr.certificate, nil } // ClientCert returns the TLS certificate most recently read by the CertReloader. // This method works as a direct utility method for tls.Config#ClientCert. func (cr *CertReloader) ClientCert(certRequestInfo *tls.CertificateRequestInfo) (*tls.Certificate, error) { cr.Lock() defer cr.Unlock() return cr.certificate, nil } // LoadCert loads a TLS certificate from the CertReloader's specified filepath. // Call this after writing a new certificate to the disk (e.g. after renewing a certificate) func (cr *CertReloader) LoadCert() error { cr.Lock() defer cr.Unlock() cert, err := tls.LoadX509KeyPair(cr.certPath, cr.keyPath) // Keep the old certificate if there's a problem reading the new one. if err != nil { sentry.CaptureException(fmt.Errorf("Error parsing X509 key pair: %v", err)) return err } cr.certificate = &cert return nil } func LoadOriginCA(originCAPoolFilename string, log *zerolog.Logger) (*x509.CertPool, error) { var originCustomCAPool []byte if originCAPoolFilename != "" { var err error originCustomCAPool, err = os.ReadFile(originCAPoolFilename) if err != nil { return nil, errors.Wrap(err, fmt.Sprintf("unable to read the file %s for --%s", originCAPoolFilename, OriginCAPoolFlag)) } } originCertPool, err := loadOriginCertPool(originCustomCAPool, log) if err != nil { return nil, errors.Wrap(err, "error loading the certificate pool") } // Windows users should be notified that they can use the flag if runtime.GOOS == "windows" && originCAPoolFilename == "" { log.Info().Msgf("cloudflared does not support loading the system root certificate pool on Windows. Please use --%s to specify the path to the certificate pool", OriginCAPoolFlag) } return originCertPool, nil } func LoadCustomOriginCA(originCAFilename string) (*x509.CertPool, error) { // First, obtain the system certificate pool certPool, err := x509.SystemCertPool() if err != nil { certPool = x509.NewCertPool() } // Next, append the Cloudflare CAs into the system pool cfRootCA, err := GetCloudflareRootCA() if err != nil { return nil, errors.Wrap(err, "could not append Cloudflare Root CAs to cloudflared certificate pool") } for _, cert := range cfRootCA { certPool.AddCert(cert) } if originCAFilename == "" { return certPool, nil } customOriginCA, err := os.ReadFile(originCAFilename) if err != nil { return nil, errors.Wrap(err, fmt.Sprintf("unable to read the file %s", originCAFilename)) } if !certPool.AppendCertsFromPEM(customOriginCA) { return nil, fmt.Errorf("error appending custom CA to cert pool") } return certPool, nil } func CreateTunnelConfig(c *cli.Context, serverName string) (*tls.Config, error) { var rootCAs []string if c.String(CaCertFlag) != "" { rootCAs = append(rootCAs, c.String(CaCertFlag)) } userConfig := &TLSParameters{RootCAs: rootCAs, ServerName: serverName} tlsConfig, err := GetConfig(userConfig) if err != nil { return nil, err } if tlsConfig.RootCAs == nil { rootCAPool, err := x509.SystemCertPool() if err != nil { return nil, errors.Wrap(err, "unable to get x509 system cert pool") } cfRootCA, err := GetCloudflareRootCA() if err != nil { return nil, errors.Wrap(err, "could not append Cloudflare Root CAs to cloudflared certificate pool") } for _, cert := range cfRootCA { rootCAPool.AddCert(cert) } tlsConfig.RootCAs = rootCAPool } if tlsConfig.ServerName == "" && !tlsConfig.InsecureSkipVerify { return nil, fmt.Errorf("either ServerName or InsecureSkipVerify must be specified in the tls.Config") } return tlsConfig, nil } func loadOriginCertPool(originCAPoolPEM []byte, log *zerolog.Logger) (*x509.CertPool, error) { // Get the global pool certPool, err := loadGlobalCertPool(log) if err != nil { return nil, err } // Then, add any custom origin CA pool the user may have passed if originCAPoolPEM != nil { if !certPool.AppendCertsFromPEM(originCAPoolPEM) { log.Info().Msg("could not append the provided origin CA to the cloudflared certificate pool") } } return certPool, nil } func loadGlobalCertPool(log *zerolog.Logger) (*x509.CertPool, error) { // First, obtain the system certificate pool certPool, err := x509.SystemCertPool() if err != nil { if runtime.GOOS != "windows" { // See https://github.com/golang/go/issues/16736 log.Err(err).Msg("error obtaining the system certificates") } certPool = x509.NewCertPool() } // Next, append the Cloudflare CAs into the system pool cfRootCA, err := GetCloudflareRootCA() if err != nil { return nil, errors.Wrap(err, "could not append Cloudflare Root CAs to cloudflared certificate pool") } for _, cert := range cfRootCA { certPool.AddCert(cert) } // Finally, add the Hello certificate into the pool (since it's self-signed) helloCert, err := GetHelloCertificateX509() if err != nil { return nil, errors.Wrap(err, "could not append Hello server certificate to cloudflared certificate pool") } certPool.AddCert(helloCert) return certPool, nil } ================================================ FILE: tlsconfig/cloudflare_ca.go ================================================ package tlsconfig import ( "crypto/x509" "encoding/pem" ) // TODO: remove the Origin CA root certs when migrated to Authenticated Origin Pull certs var cloudflareRootCA = []byte(` Issuer: C=US, ST=California, L=San Francisco, O=CloudFlare, Inc., OU=CloudFlare Origin SSL ECC Certificate Authority -----BEGIN CERTIFICATE----- MIICiTCCAi6gAwIBAgIUXZP3MWb8MKwBE1Qbawsp1sfA/Y4wCgYIKoZIzj0EAwIw gY8xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1T YW4gRnJhbmNpc2NvMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMTgwNgYDVQQL Ey9DbG91ZEZsYXJlIE9yaWdpbiBTU0wgRUNDIENlcnRpZmljYXRlIEF1dGhvcml0 eTAeFw0xOTA4MjMyMTA4MDBaFw0yOTA4MTUxNzAwMDBaMIGPMQswCQYDVQQGEwJV UzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEZ MBcGA1UEChMQQ2xvdWRGbGFyZSwgSW5jLjE4MDYGA1UECxMvQ2xvdWRGbGFyZSBP cmlnaW4gU1NMIEVDQyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwWTATBgcqhkjOPQIB BggqhkjOPQMBBwNCAASR+sGALuaGshnUbcxKry+0LEXZ4NY6JUAtSeA6g87K3jaA xpIg9G50PokpfWkhbarLfpcZu0UAoYy2su0EhN7wo2YwZDAOBgNVHQ8BAf8EBAMC AQYwEgYDVR0TAQH/BAgwBgEB/wIBAjAdBgNVHQ4EFgQUhTBdOypw1O3VkmcH/es5 tBoOOKcwHwYDVR0jBBgwFoAUhTBdOypw1O3VkmcH/es5tBoOOKcwCgYIKoZIzj0E AwIDSQAwRgIhAKilfntP2ILGZjwajktkBtXE1pB4Y/fjAfLkIRUzrI15AiEA5UCL XYZZ9m2c3fKwIenMMojL1eqydsgqj/wK4p5kagQ= -----END CERTIFICATE----- Issuer: C=US, O=CloudFlare, Inc., OU=CloudFlare Origin SSL Certificate Authority, L=San Francisco, ST=California -----BEGIN CERTIFICATE----- MIIEADCCAuigAwIBAgIID+rOSdTGfGcwDQYJKoZIhvcNAQELBQAwgYsxCzAJBgNV BAYTAlVTMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMTQwMgYDVQQLEytDbG91 ZEZsYXJlIE9yaWdpbiBTU0wgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MRYwFAYDVQQH Ew1TYW4gRnJhbmNpc2NvMRMwEQYDVQQIEwpDYWxpZm9ybmlhMB4XDTE5MDgyMzIx MDgwMFoXDTI5MDgxNTE3MDAwMFowgYsxCzAJBgNVBAYTAlVTMRkwFwYDVQQKExBD bG91ZEZsYXJlLCBJbmMuMTQwMgYDVQQLEytDbG91ZEZsYXJlIE9yaWdpbiBTU0wg Q2VydGlmaWNhdGUgQXV0aG9yaXR5MRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMRMw EQYDVQQIEwpDYWxpZm9ybmlhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC AQEAwEiVZ/UoQpHmFsHvk5isBxRehukP8DG9JhFev3WZtG76WoTthvLJFRKFCHXm V6Z5/66Z4S09mgsUuFwvJzMnE6Ej6yIsYNCb9r9QORa8BdhrkNn6kdTly3mdnykb OomnwbUfLlExVgNdlP0XoRoeMwbQ4598foiHblO2B/LKuNfJzAMfS7oZe34b+vLB yrP/1bgCSLdc1AxQc1AC0EsQQhgcyTJNgnG4va1c7ogPlwKyhbDyZ4e59N5lbYPJ SmXI/cAe3jXj1FBLJZkwnoDKe0v13xeF+nF32smSH0qB7aJX2tBMW4TWtFPmzs5I lwrFSySWAdwYdgxw180yKU0dvwIDAQABo2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYD VR0TAQH/BAgwBgEB/wIBAjAdBgNVHQ4EFgQUJOhTV118NECHqeuU27rhFnj8KaQw HwYDVR0jBBgwFoAUJOhTV118NECHqeuU27rhFnj8KaQwDQYJKoZIhvcNAQELBQAD ggEBAHwOf9Ur1l0Ar5vFE6PNrZWrDfQIMyEfdgSKofCdTckbqXNTiXdgbHs+TWoQ wAB0pfJDAHJDXOTCWRyTeXOseeOi5Btj5CnEuw3P0oXqdqevM1/+uWp0CM35zgZ8 VD4aITxity0djzE6Qnx3Syzz+ZkoBgTnNum7d9A66/V636x4vTeqbZFBr9erJzgz hhurjcoacvRNhnjtDRM0dPeiCJ50CP3wEYuvUzDHUaowOsnLCjQIkWbR7Ni6KEIk MOz2U0OBSif3FTkhCgZWQKOOLo1P42jHC3ssUZAtVNXrCk3fw9/E15k8NPkBazZ6 0iykLhH1trywrKRMVw67F44IE8Y= -----END CERTIFICATE----- Issuer: C=US, O=CloudFlare, Inc., OU=Origin Pull, L=San Francisco, ST=California, CN=origin-pull.cloudflare.net -----BEGIN CERTIFICATE----- MIIGCjCCA/KgAwIBAgIIV5G6lVbCLmEwDQYJKoZIhvcNAQENBQAwgZAxCzAJBgNV BAYTAlVTMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMRQwEgYDVQQLEwtPcmln aW4gUHVsbDEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzETMBEGA1UECBMKQ2FsaWZv cm5pYTEjMCEGA1UEAxMab3JpZ2luLXB1bGwuY2xvdWRmbGFyZS5uZXQwHhcNMTkx MDEwMTg0NTAwWhcNMjkxMTAxMTcwMDAwWjCBkDELMAkGA1UEBhMCVVMxGTAXBgNV BAoTEENsb3VkRmxhcmUsIEluYy4xFDASBgNVBAsTC09yaWdpbiBQdWxsMRYwFAYD VQQHEw1TYW4gRnJhbmNpc2NvMRMwEQYDVQQIEwpDYWxpZm9ybmlhMSMwIQYDVQQD ExpvcmlnaW4tcHVsbC5jbG91ZGZsYXJlLm5ldDCCAiIwDQYJKoZIhvcNAQEBBQAD ggIPADCCAgoCggIBAN2y2zojYfl0bKfhp0AJBFeV+jQqbCw3sHmvEPwLmqDLqynI 42tZXR5y914ZB9ZrwbL/K5O46exd/LujJnV2b3dzcx5rtiQzso0xzljqbnbQT20e ihx/WrF4OkZKydZzsdaJsWAPuplDH5P7J82q3re88jQdgE5hqjqFZ3clCG7lxoBw hLaazm3NJJlUfzdk97ouRvnFGAuXd5cQVx8jYOOeU60sWqmMe4QHdOvpqB91bJoY QSKVFjUgHeTpN8tNpKJfb9LIn3pun3bC9NKNHtRKMNX3Kl/sAPq7q/AlndvA2Kw3 Dkum2mHQUGdzVHqcOgea9BGjLK2h7SuX93zTWL02u799dr6Xkrad/WShHchfjjRn aL35niJUDr02YJtPgxWObsrfOU63B8juLUphW/4BOjjJyAG5l9j1//aUGEi/sEe5 lqVv0P78QrxoxR+MMXiJwQab5FB8TG/ac6mRHgF9CmkX90uaRh+OC07XjTdfSKGR PpM9hB2ZhLol/nf8qmoLdoD5HvODZuKu2+muKeVHXgw2/A6wM7OwrinxZiyBk5Hh CvaADH7PZpU6z/zv5NU5HSvXiKtCzFuDu4/Zfi34RfHXeCUfHAb4KfNRXJwMsxUa +4ZpSAX2G6RnGU5meuXpU5/V+DQJp/e69XyyY6RXDoMywaEFlIlXBqjRRA2pAgMB AAGjZjBkMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgECMB0GA1Ud DgQWBBRDWUsraYuA4REzalfNVzjann3F6zAfBgNVHSMEGDAWgBRDWUsraYuA4REz alfNVzjann3F6zANBgkqhkiG9w0BAQ0FAAOCAgEAkQ+T9nqcSlAuW/90DeYmQOW1 QhqOor5psBEGvxbNGV2hdLJY8h6QUq48BCevcMChg/L1CkznBNI40i3/6heDn3IS zVEwXKf34pPFCACWVMZxbQjkNRTiH8iRur9EsaNQ5oXCPJkhwg2+IFyoPAAYURoX VcI9SCDUa45clmYHJ/XYwV1icGVI8/9b2JUqklnOTa5tugwIUi5sTfipNcJXHhgz 6BKYDl0/UP0lLKbsUETXeTGDiDpxZYIgbcFrRDDkHC6BSvdWVEiH5b9mH2BON60z 0O0j8EEKTwi9jnafVtZQXP/D8yoVowdFDjXcKkOPF/1gIh9qrFR6GdoPVgB3SkLc 5ulBqZaCHm563jsvWb/kXJnlFxW+1bsO9BDD6DweBcGdNurgmH625wBXksSdD7y/ fakk8DagjbjKShYlPEFOAqEcliwjF45eabL0t27MJV61O/jHzHL3dknXeE4BDa2j bA+JbyJeUMtU7KMsxvx82RmhqBEJJDBCJ3scVptvhDMRrtqDBW5JShxoAOcpFQGm iYWicn46nPDjgTU0bX1ZPpTpryXbvciVL5RkVBuyX2ntcOLDPlZWgxZCBp96x07F AnOzKgZk4RzZPNAxCXERVxajn/FLcOhglVAKo5H0ac+AitlQ0ip55D2/mf8o72tM fVQ6VpyjEXdiIXWUq/o= -----END CERTIFICATE-----`) func GetCloudflareRootCA() ([]*x509.Certificate, error) { var certs []*x509.Certificate pemBlocks := cloudflareRootCA for len(pemBlocks) > 0 { var block *pem.Block block, pemBlocks = pem.Decode(pemBlocks) if block == nil { break } if block.Type != "CERTIFICATE" { continue } cert, err := x509.ParseCertificate(block.Bytes) if err != nil { return nil, err } certs = append(certs, cert) } return certs, nil } ================================================ FILE: tlsconfig/hello_ca.go ================================================ package tlsconfig import ( "crypto/tls" "crypto/x509" ) const ( helloKey = ` -----BEGIN EC PARAMETERS----- BgUrgQQAIg== -----END EC PARAMETERS----- -----BEGIN EC PRIVATE KEY----- MIGkAgEBBDBGGfwhIJdiUiJUVIItqJjEIMmlXxsMa8TQeer47+g+cIZ466rgg8EK +Mdn6BY48GCgBwYFK4EEACKhZANiAASW//A9iDbPKg3OLkn7yJqLer32g9I5lBKR tPc/zBubQLLz9lAaYI6AOQiJXhGr5JkKmQfi1sYHK5rJITPFy4W8Et4hHLdazDZH WnEd+TStQABFUjrhtqXPWmGKcly0pOE= -----END EC PRIVATE KEY-----` helloCRT = ` -----BEGIN CERTIFICATE----- MIICiDCCAg6gAwIBAgIJAJ/FfkBTtbuIMAkGByqGSM49BAEwfzELMAkGA1UEBhMC VVMxDjAMBgNVBAgMBVRleGFzMQ8wDQYDVQQHDAZBdXN0aW4xGTAXBgNVBAoMEENs b3VkZmxhcmUsIEluYy4xNDAyBgNVBAMMK0FyZ28gVHVubmVsIFNhbXBsZSBIZWxs byBTZXJ2ZXIgQ2VydGlmaWNhdGUwHhcNMTgwMzE5MjMwNTMyWhcNMjgwMzE2MjMw NTMyWjB/MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxDzANBgNVBAcMBkF1 c3RpbjEZMBcGA1UECgwQQ2xvdWRmbGFyZSwgSW5jLjE0MDIGA1UEAwwrQXJnbyBU dW5uZWwgU2FtcGxlIEhlbGxvIFNlcnZlciBDZXJ0aWZpY2F0ZTB2MBAGByqGSM49 AgEGBSuBBAAiA2IABJb/8D2INs8qDc4uSfvImot6vfaD0jmUEpG09z/MG5tAsvP2 UBpgjoA5CIleEavkmQqZB+LWxgcrmskhM8XLhbwS3iEct1rMNkdacR35NK1AAEVS OuG2pc9aYYpyXLSk4aNXMFUwUwYDVR0RBEwwSoIJbG9jYWxob3N0ghFjbG91ZGZs YXJlZC1oZWxsb4ISY2xvdWRmbGFyZWQyLWhlbGxvhwR/AAABhxAAAAAAAAAAAAAA AAAAAAABMAkGByqGSM49BAEDaQAwZgIxAPxkdghH6y8xLMnY9Bom3Llf4NYM6yB9 PD1YsaNUJTsxjTk3YY1Jsp+yzK0yUKtTZwIxAPcdvqCF2/iR9H288pCT1TgtO0a9 cJL9RY1lq7DIGN37v1ZXReWaD+3hNokY8NriVg== -----END CERTIFICATE-----` ) func GetHelloCertificate() (tls.Certificate, error) { return tls.X509KeyPair([]byte(helloCRT), []byte(helloKey)) } func GetHelloCertificateX509() (*x509.Certificate, error) { helloCertificate, err := GetHelloCertificate() if err != nil { return nil, err } return x509.ParseCertificate(helloCertificate.Certificate[0]) } ================================================ FILE: tlsconfig/testcert.pem ================================================ -----BEGIN CERTIFICATE----- MIICBjCCAbCgAwIBAgIJAPKk4bYMrSFMMA0GCSqGSIb3DQEBCwUAMF0xCzAJBgNV BAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEPMA0GA1UEBwwGQXVzdGluMRkwFwYDVQQK DBBDbG91ZGZsYXJlLCBJbmMuMRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcNMTgxMTE1 MjA1NzU3WhcNMjgxMTEyMjA1NzU3WjBdMQswCQYDVQQGEwJVUzEOMAwGA1UECAwF VGV4YXMxDzANBgNVBAcMBkF1c3RpbjEZMBcGA1UECgwQQ2xvdWRmbGFyZSwgSW5j LjESMBAGA1UEAwwJbG9jYWxob3N0MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAOQN pTRn5wLf8SSI5x2kpDvbdDy7lfhamJ2En4Q+wy1cSKp8bn8/oyhVF7QTsimDGTI4 45pV9nDfNJPYB3IW0x0CAwEAAaNTMFEwHQYDVR0OBBYEFE4jIa97mIEiYFa02X++ uu5mCEn+MB8GA1UdIwQYMBaAFE4jIa97mIEiYFa02X++uu5mCEn+MA8GA1UdEwEB /wQFMAMBAf8wDQYJKoZIhvcNAQELBQADQQAkE+pDee0o5cNcZRUszy8sTQzB1Wlp J6ucfmo16crqRaK7uGvhkMyibIc4D8z2Cxw3aI3IMMFoIIlYoYKiUcbd -----END CERTIFICATE----- ================================================ FILE: tlsconfig/testcert2.pem ================================================ -----BEGIN CERTIFICATE----- MIICBjCCAbCgAwIBAgIJAN6cXRTbJtFnMA0GCSqGSIb3DQEBCwUAMF0xCzAJBgNV BAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEPMA0GA1UEBwwGQXVzdGluMRgwFgYDVQQK DA9DbG91ZGZsYXJlLCBJbmMxEzARBgNVBAMMCmxvY2FsaG9zdDIwHhcNMTgxMTE1 MjExMTU4WhcNMjgxMTEyMjExMTU4WjBdMQswCQYDVQQGEwJVUzEOMAwGA1UECAwF VGV4YXMxDzANBgNVBAcMBkF1c3RpbjEYMBYGA1UECgwPQ2xvdWRmbGFyZSwgSW5j MRMwEQYDVQQDDApsb2NhbGhvc3QyMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKQx IMZ6QgXoul2ITF/7sly4fW2Ol+a/AYw42zCWhVqOXv8AhY21I0Q8lkRR6wOroQwZ O7jKKOcE5TnR/NRcZr8CAwEAAaNTMFEwHQYDVR0OBBYEFONKxLZc2RUD0KTHkAz4 8nrb5688MB8GA1UdIwQYMBaAFONKxLZc2RUD0KTHkAz48nrb5688MA8GA1UdEwEB /wQFMAMBAf8wDQYJKoZIhvcNAQELBQADQQA56pwhvGpNPjyLcWfJHu/vI3ZjdoLB LnrkRaMjJmv0H0Beh4upJhoz8u6lhMACerKQrrdQhEPB2u+maFrEBtmN -----END CERTIFICATE----- ================================================ FILE: tlsconfig/testkey.pem ================================================ -----BEGIN PRIVATE KEY----- MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEA5A2lNGfnAt/xJIjn HaSkO9t0PLuV+FqYnYSfhD7DLVxIqnxufz+jKFUXtBOyKYMZMjjjmlX2cN80k9gH chbTHQIDAQABAkAoeDtu91lJa1AxuZG58vOqI6GW/Xr5naojmdts7m5YaAhDa7DE zJUp4d8SP5cGBf1/PB3x6Cu9UviFNQ16wmzJAiEA8gUm4UYpWZD4Ze2l/xb+BK8D IglSUIy1VxW+X1G55wMCIQDxOfXiFzPqnv/e5avKGv6CU11Dhmbi1OpiyybZTjGz XwIhAM3bE/cJdqJ4bNBGE6umIupY8pFA3IMnLBempwbsvPOBAiEAgzJ+5OSxu92W VGidsmJUIhWtF9i1hJFAmVLcYjwBFAkCICtjP/vv0qOZWk4mAAn2zz9UVWp45DSR p/FA8V77ohXD -----END PRIVATE KEY----- ================================================ FILE: tlsconfig/tlsconfig.go ================================================ // Package tlsconfig provides convenience functions for configuring TLS connections from the // command line. package tlsconfig import ( "crypto/tls" "crypto/x509" "os" "github.com/pkg/errors" ) // Config is the user provided parameters to create a tls.Config type TLSParameters struct { Cert string Key string GetCertificate *CertReloader GetClientCertificate *CertReloader ClientCAs []string RootCAs []string ServerName string CurvePreferences []tls.CurveID MinVersion uint16 // min tls version. If zero, TLS1.0 is defined as minimum. MaxVersion uint16 // max tls version. If zero, last TLS version is used defined as limit (currently TLS1.3) } // GetConfig returns a TLS configuration according to the Config set by the user. func GetConfig(p *TLSParameters) (*tls.Config, error) { tlsconfig := &tls.Config{} if p.Cert != "" && p.Key != "" { cert, err := tls.LoadX509KeyPair(p.Cert, p.Key) if err != nil { return nil, errors.Wrap(err, "Error parsing X509 key pair") } tlsconfig.Certificates = []tls.Certificate{cert} // BuildNameToCertificate parses Certificates and builds NameToCertificate from common name // and SAN fields of leaf certificates tlsconfig.BuildNameToCertificate() } if p.GetCertificate != nil { // GetCertificate is called when client supplies SNI info or Certificates is empty. // Order of retrieving certificate is GetCertificate, NameToCertificate and lastly first element of Certificates tlsconfig.GetCertificate = p.GetCertificate.Cert } if p.GetClientCertificate != nil { // GetClientCertificate is called when using an HTTP client library and mTLS is required. tlsconfig.GetClientCertificate = p.GetClientCertificate.ClientCert } if len(p.ClientCAs) > 0 { // set of root certificate authorities that servers use if required to verify a client certificate // by the policy in ClientAuth clientCAs, err := LoadCert(p.ClientCAs) if err != nil { return nil, errors.Wrap(err, "Error loading client CAs") } tlsconfig.ClientCAs = clientCAs // server's policy for TLS Client Authentication. Default is no client cert tlsconfig.ClientAuth = tls.RequireAndVerifyClientCert } if len(p.RootCAs) > 0 { rootCAs, err := LoadCert(p.RootCAs) if err != nil { return nil, errors.Wrap(err, "Error loading root CAs") } tlsconfig.RootCAs = rootCAs } if p.ServerName != "" { tlsconfig.ServerName = p.ServerName } if len(p.CurvePreferences) > 0 { tlsconfig.CurvePreferences = p.CurvePreferences } else { // Cloudflare optimize CurveP256 tlsconfig.CurvePreferences = []tls.CurveID{tls.CurveP256} } tlsconfig.MinVersion = p.MinVersion tlsconfig.MaxVersion = p.MaxVersion return tlsconfig, nil } // LoadCert creates a CertPool containing all certificates in a PEM-format file. func LoadCert(certPaths []string) (*x509.CertPool, error) { ca := x509.NewCertPool() for _, certPath := range certPaths { caCert, err := os.ReadFile(certPath) if err != nil { return nil, errors.Wrapf(err, "Error reading certificate %s", certPath) } if !ca.AppendCertsFromPEM(caCert) { return nil, errors.Wrapf(err, "Error parsing certificate %s", certPath) } } return ca, nil } ================================================ FILE: tlsconfig/tlsconfig_test.go ================================================ package tlsconfig import ( "crypto/tls" "testing" "github.com/stretchr/testify/assert" ) // testcert.pem and testcert2.pem are Generated using `openssl req -newkey rsa:512 -nodes -x509 -days 3650` const ( testcertCommonName = "localhost" ) func TestGetFromEmptyConfig(t *testing.T) { c := &TLSParameters{} tlsConfig, err := GetConfig(c) assert.NoError(t, err) assert.Empty(t, tlsConfig.Certificates) assert.Empty(t, tlsConfig.NameToCertificate) assert.Nil(t, tlsConfig.ClientCAs) assert.Equal(t, tls.NoClientCert, tlsConfig.ClientAuth) assert.Nil(t, tlsConfig.RootCAs) assert.Len(t, tlsConfig.CurvePreferences, 1) assert.Equal(t, tls.CurveP256, tlsConfig.CurvePreferences[0]) } func TestGetConfig(t *testing.T) { cert, err := tls.LoadX509KeyPair("testcert.pem", "testkey.pem") assert.NoError(t, err) c := &TLSParameters{ Cert: "testcert.pem", Key: "testkey.pem", ClientCAs: []string{"testcert.pem", "testcert2.pem"}, RootCAs: []string{"testcert.pem", "testcert2.pem"}, ServerName: "test", CurvePreferences: []tls.CurveID{tls.CurveP384}, } tlsConfig, err := GetConfig(c) assert.NoError(t, err) assert.Len(t, tlsConfig.Certificates, 1) assert.Equal(t, cert, tlsConfig.Certificates[0]) assert.Equal(t, cert, *tlsConfig.NameToCertificate[testcertCommonName]) assert.NotNil(t, tlsConfig.ClientCAs) assert.Equal(t, tls.RequireAndVerifyClientCert, tlsConfig.ClientAuth) assert.NotNil(t, tlsConfig.RootCAs) assert.Len(t, tlsConfig.CurvePreferences, 1) assert.Equal(t, tls.CurveP384, tlsConfig.CurvePreferences[0]) } func TestCertReloader(t *testing.T) { expectedCert, err := tls.LoadX509KeyPair("testcert.pem", "testkey.pem") assert.NoError(t, err) certReloader, err := NewCertReloader("testcert.pem", "testkey.pem") assert.NoError(t, err) chi := &tls.ClientHelloInfo{ServerName: testcertCommonName} cert, err := certReloader.Cert(chi) assert.NoError(t, err) assert.Equal(t, expectedCert, *cert) c := &TLSParameters{ GetCertificate: certReloader, } tlsConfig, err := GetConfig(c) assert.NoError(t, err) cert, err = tlsConfig.GetCertificate(chi) assert.NoError(t, err) assert.Equal(t, expectedCert, *cert) } ================================================ FILE: token/encrypt.go ================================================ // Package encrypter is suitable for encrypting messages you would like to securely share between two points. // Useful for providing end to end encryption (E2EE). It uses Box (NaCl) for encrypting the messages. // tldr is it uses Elliptic Curves (Curve25519) for the keys, XSalsa20 and Poly1305 for encryption. // You can read more here https://godoc.org/golang.org/x/crypto/nacl/box. // // msg := []byte("super safe message.") // alice, err := NewEncrypter("alice_priv_key.pem", "alice_pub_key.pem") // if err != nil { // log.Fatal(err) // } // // bob, err := NewEncrypter("bob_priv_key.pem", "bob_pub_key.pem") // if err != nil { // log.Fatal(err) // } // encrypted, err := alice.Encrypt(msg, bob.PublicKey()) // if err != nil { // log.Fatal(err) // } // // data, err := bob.Decrypt(encrypted, alice.PublicKey()) // if err != nil { // log.Fatal(err) // } // fmt.Println(string(data)) package token import ( "bytes" "crypto/rand" "encoding/base64" "encoding/pem" "errors" "io" "os" "golang.org/x/crypto/nacl/box" ) // Encrypter represents a keypair value with auxiliary functions to make // doing encryption and decryption easier type Encrypter struct { privateKey *[32]byte publicKey *[32]byte } // NewEncrypter returns a new encrypter with initialized keypair func NewEncrypter(privateKey, publicKey string) (*Encrypter, error) { e := &Encrypter{} pubKey, key, err := e.fetchOrGenerateKeys(privateKey, publicKey) if err != nil { return nil, err } e.privateKey, e.publicKey = key, pubKey return e, nil } // PublicKey returns a base64 encoded public key. Useful for transport (like in HTTP requests) func (e *Encrypter) PublicKey() string { return base64.URLEncoding.EncodeToString(e.publicKey[:]) } // Decrypt data that was encrypted using our publicKey. It will use our privateKey and the sender's publicKey to decrypt // data is an encrypted buffer of data, mostly like from the Encrypt function. Messages contain the nonce data on the front // of the message. // senderPublicKey is a base64 encoded version of the sender's public key (most likely from the PublicKey function). // The return value is the decrypted buffer or an error. func (e *Encrypter) Decrypt(data []byte, senderPublicKey string) ([]byte, error) { var decryptNonce [24]byte copy(decryptNonce[:], data[:24]) // we pull the nonce from the front of the actual message. pubKey, err := e.decodePublicKey(senderPublicKey) if err != nil { return nil, err } decrypted, ok := box.Open(nil, data[24:], &decryptNonce, pubKey, e.privateKey) if !ok { return nil, errors.New("failed to decrypt message") } return decrypted, nil } // Encrypt data using our privateKey and the recipient publicKey // data is a buffer of data that we would like to encrypt. Messages will have the nonce added to front // as they have to unique for each message shared. // recipientPublicKey is a base64 encoded version of the sender's public key (most likely from the PublicKey function). // The return value is the encrypted buffer or an error. func (e *Encrypter) Encrypt(data []byte, recipientPublicKey string) ([]byte, error) { var nonce [24]byte if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil { return nil, err } pubKey, err := e.decodePublicKey(recipientPublicKey) if err != nil { return nil, err } // This encrypts msg and adds the nonce to the front of the message, since the nonce has to be // the same for encrypting and decrypting return box.Seal(nonce[:], data, &nonce, pubKey, e.privateKey), nil } // WriteKeys keys will take the currently initialized keypair and write them to provided filenames func (e *Encrypter) WriteKeys(privateKey, publicKey string) error { if err := e.writeKey(e.privateKey[:], "BOX PRIVATE KEY", privateKey); err != nil { return err } return e.writeKey(e.publicKey[:], "PUBLIC KEY", publicKey) } // fetchOrGenerateKeys will either load or create a keypair if it doesn't exist func (e *Encrypter) fetchOrGenerateKeys(privateKey, publicKey string) (*[32]byte, *[32]byte, error) { key, err := e.fetchKey(privateKey) if os.IsNotExist(err) { return box.GenerateKey(rand.Reader) } else if err != nil { return nil, nil, err } pub, err := e.fetchKey(publicKey) if os.IsNotExist(err) { return box.GenerateKey(rand.Reader) } else if err != nil { return nil, nil, err } return pub, key, nil } // writeKey will write a key to disk in DER format (it's a standard pem key) func (e *Encrypter) writeKey(key []byte, pemType, filename string) error { data := pem.EncodeToMemory(&pem.Block{ Type: pemType, Bytes: key, }) f, err := os.Create(filename) if err != nil { return err } _, err = f.Write(data) if err != nil { return err } return nil } // fetchKey will load a a DER formatted key from disk func (e *Encrypter) fetchKey(filename string) (*[32]byte, error) { f, err := os.Open(filename) if err != nil { return nil, err } buf := new(bytes.Buffer) io.Copy(buf, f) p, _ := pem.Decode(buf.Bytes()) if p == nil { return nil, errors.New("Failed to decode key") } var newKey [32]byte copy(newKey[:], p.Bytes) return &newKey, nil } // decodePublicKey will base64 decode the provided key to the box representation func (e *Encrypter) decodePublicKey(key string) (*[32]byte, error) { pub, err := base64.URLEncoding.DecodeString(key) if err != nil { return nil, err } var newKey [32]byte copy(newKey[:], pub) return &newKey, nil } ================================================ FILE: token/launch_browser_darwin.go ================================================ //go:build darwin package token import ( "os/exec" ) func getBrowserCmd(url string) *exec.Cmd { return exec.Command("open", url) } ================================================ FILE: token/launch_browser_other.go ================================================ //go:build !windows && !darwin && !linux && !netbsd && !freebsd && !openbsd package token import ( "os/exec" ) func getBrowserCmd(url string) *exec.Cmd { return nil } ================================================ FILE: token/launch_browser_unix.go ================================================ //go:build linux || freebsd || openbsd || netbsd package token import ( "os/exec" ) func getBrowserCmd(url string) *exec.Cmd { return exec.Command("xdg-open", url) } ================================================ FILE: token/launch_browser_windows.go ================================================ //go:build windows package token import ( "fmt" "os/exec" "syscall" ) func getBrowserCmd(url string) *exec.Cmd { cmd := exec.Command("cmd") // CmdLine is only defined when compiling for windows. // Empty string is the cmd proc "Title". Needs to be included because the start command will interpret the first // quoted string as that field and we want to quote the URL. cmd.SysProcAttr = &syscall.SysProcAttr{CmdLine: fmt.Sprintf(`/c start "" "%s"`, url)} return cmd } ================================================ FILE: token/path.go ================================================ package token import ( "fmt" "net/url" "os" "path/filepath" "strings" homedir "github.com/mitchellh/go-homedir" "github.com/cloudflare/cloudflared/config" ) // GenerateSSHCertFilePathFromURL will return a file path for creating short lived certificates func GenerateSSHCertFilePathFromURL(url *url.URL, suffix string) (string, error) { configPath, err := getConfigPath() if err != nil { return "", err } name := strings.Replace(fmt.Sprintf("%s%s-%s", url.Hostname(), url.EscapedPath(), suffix), "/", "-", -1) return filepath.Join(configPath, name), nil } // GenerateAppTokenFilePathFromURL will return a filepath for given Access org token func GenerateAppTokenFilePathFromURL(appDomain, aud string, suffix string) (string, error) { configPath, err := getConfigPath() if err != nil { return "", err } name := fmt.Sprintf("%s-%s-%s", appDomain, aud, suffix) name = strings.Replace(strings.Replace(name, "/", "-", -1), "*", "-", -1) return filepath.Join(configPath, name), nil } // generateOrgTokenFilePathFromURL will return a filepath for given Access application token func generateOrgTokenFilePathFromURL(authDomain string) (string, error) { configPath, err := getConfigPath() if err != nil { return "", err } name := strings.Replace(fmt.Sprintf("%s-org-token", authDomain), "/", "-", -1) return filepath.Join(configPath, name), nil } func getConfigPath() (string, error) { configPath, err := homedir.Expand(config.DefaultConfigSearchDirectories()[0]) if err != nil { return "", err } ok, err := config.FileExists(configPath) if !ok && err == nil { // create config directory if doesn't already exist err = os.Mkdir(configPath, 0700) } return configPath, err } ================================================ FILE: token/shell.go ================================================ package token // OpenBrowser opens the specified URL in the default browser of the user func OpenBrowser(url string) error { return getBrowserCmd(url).Start() } ================================================ FILE: token/signal_test.go ================================================ //go:build linux || darwin package token import ( "os" "syscall" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestSignalHandler(t *testing.T) { sigHandler := signalHandler{signals: []os.Signal{syscall.SIGUSR1}} handlerRan := false done := make(chan struct{}) timer := time.NewTimer(time.Second) sigHandler.register(func() { handlerRan = true done <- struct{}{} }) p, err := os.FindProcess(os.Getpid()) require.Nil(t, err) p.Signal(syscall.SIGUSR1) // Blocks for up to one second to make sure the handler callback runs before the assert. select { case <-done: assert.True(t, handlerRan) case <-timer.C: t.Fail() } sigHandler.deregister() } func TestSignalHandlerClose(t *testing.T) { sigHandler := signalHandler{signals: []os.Signal{syscall.SIGUSR1}} done := make(chan struct{}) timer := time.NewTimer(time.Second) sigHandler.register(func() { done <- struct{}{} }) sigHandler.deregister() p, err := os.FindProcess(os.Getpid()) require.Nil(t, err) p.Signal(syscall.SIGUSR1) select { case <-done: t.Fail() case <-timer.C: } } ================================================ FILE: token/token.go ================================================ package token import ( "context" "encoding/json" "fmt" "net/http" "net/url" "os" "os/signal" "strings" "syscall" "time" "github.com/go-jose/go-jose/v4" "github.com/pkg/errors" "github.com/rs/zerolog" "github.com/cloudflare/cloudflared/config" "github.com/cloudflare/cloudflared/retry" ) const ( keyName = "token" tokenCookie = "CF_Authorization" appSessionCookie = "CF_AppSession" appDomainHeader = "CF-Access-Domain" appAUDHeader = "CF-Access-Aud" AccessLoginWorkerPath = "/cdn-cgi/access/login" AccessAuthorizedWorkerPath = "/cdn-cgi/access/authorized" ) var ( userAgent = "DEV" signatureAlgs = []jose.SignatureAlgorithm{jose.RS256} ) type AppInfo struct { AuthDomain string AppAUD string AppDomain string } type lock struct { lockFilePath string backoff *retry.BackoffHandler sigHandler *signalHandler } type signalHandler struct { sigChannel chan os.Signal signals []os.Signal } type jwtPayload struct { Aud []string `json:"-"` Email string `json:"email"` Exp int `json:"exp"` Iat int `json:"iat"` Nbf int `json:"nbf"` Iss string `json:"iss"` Type string `json:"type"` Subt string `json:"sub"` } type transferServiceResponse struct { AppToken string `json:"app_token"` OrgToken string `json:"org_token"` } func (p *jwtPayload) UnmarshalJSON(data []byte) error { type Alias jwtPayload if err := json.Unmarshal(data, (*Alias)(p)); err != nil { return err } var audParser struct { Aud any `json:"aud"` } if err := json.Unmarshal(data, &audParser); err != nil { return err } switch aud := audParser.Aud.(type) { case string: p.Aud = []string{aud} case []any: for _, a := range aud { s, ok := a.(string) if !ok { return errors.New("aud array contains non-string elements") } p.Aud = append(p.Aud, s) } default: return errors.New("aud field is not a string or an array of strings") } return nil } func (p jwtPayload) isExpired() bool { return int(time.Now().Unix()) > p.Exp } func (s *signalHandler) register(handler func()) { s.sigChannel = make(chan os.Signal, 1) signal.Notify(s.sigChannel, s.signals...) go func(s *signalHandler) { for range s.sigChannel { handler() } }(s) } func (s *signalHandler) deregister() { signal.Stop(s.sigChannel) close(s.sigChannel) } func errDeleteTokenFailed(lockFilePath string) error { return fmt.Errorf("failed to acquire a new Access token. Please try to delete %s", lockFilePath) } // newLock will get a new file lock func newLock(path string) *lock { lockPath := path + ".lock" backoff := retry.NewBackoff(uint(7), retry.DefaultBaseTime, false) return &lock{ lockFilePath: lockPath, backoff: &backoff, sigHandler: &signalHandler{ signals: []os.Signal{syscall.SIGINT, syscall.SIGTERM}, }, } } func (l *lock) Acquire() error { // Intercept SIGINT and SIGTERM to release lock before exiting l.sigHandler.register(func() { _ = l.deleteLockFile() os.Exit(0) }) // Check for a lock file // if the lock file exists; start polling // if not, create the lock file and go through the normal flow. // See AUTH-1736 for the reason why we do all this for isTokenLocked(l.lockFilePath) { if l.backoff.Backoff(context.Background()) { continue } if err := l.deleteLockFile(); err != nil { return err } } // Create a lock file so other processes won't also try to get the token at // the same time if err := os.WriteFile(l.lockFilePath, []byte{}, 0600); err != nil { return err } return nil } func (l *lock) deleteLockFile() error { if err := os.Remove(l.lockFilePath); err != nil && !os.IsNotExist(err) { return errDeleteTokenFailed(l.lockFilePath) } return nil } func (l *lock) Release() error { defer l.sigHandler.deregister() return l.deleteLockFile() } // isTokenLocked checks to see if there is another process attempting to get the token already func isTokenLocked(lockFilePath string) bool { exists, err := config.FileExists(lockFilePath) return exists && err == nil } func Init(version string) { userAgent = fmt.Sprintf("cloudflared/%s", version) } // FetchTokenWithRedirect will either load a stored token or generate a new one // it appends the full url as the redirect URL to the access cli request if opening the browser func FetchTokenWithRedirect(appURL *url.URL, appInfo *AppInfo, autoClose bool, isFedramp bool, log *zerolog.Logger) (string, error) { return getToken(appURL, appInfo, false, autoClose, isFedramp, log) } // FetchToken will either load a stored token or generate a new one // it appends the host of the appURL as the redirect URL to the access cli request if opening the browser func FetchToken(appURL *url.URL, appInfo *AppInfo, autoClose bool, isFedramp bool, log *zerolog.Logger) (string, error) { return getToken(appURL, appInfo, true, autoClose, isFedramp, log) } // getToken will either load a stored token or generate a new one func getToken(appURL *url.URL, appInfo *AppInfo, useHostOnly bool, autoClose bool, isFedramp bool, log *zerolog.Logger) (string, error) { if token, err := GetAppTokenIfExists(appInfo); token != "" && err == nil { return token, nil } appTokenPath, err := GenerateAppTokenFilePathFromURL(appInfo.AppDomain, appInfo.AppAUD, keyName) if err != nil { return "", errors.Wrap(err, "failed to generate app token file path") } fileLockAppToken := newLock(appTokenPath) if err = fileLockAppToken.Acquire(); err != nil { return "", errors.Wrap(err, "failed to acquire app token lock") } defer func() { _ = fileLockAppToken.Release() }() // check to see if another process has gotten a token while we waited for the lock if token, err := GetAppTokenIfExists(appInfo); token != "" && err == nil { return token, nil } // If an app token couldn't be found on disk, check for an org token and attempt to exchange it for an app token. var orgTokenPath string orgToken, err := GetOrgTokenIfExists(appInfo.AuthDomain) if err != nil { orgTokenPath, err = generateOrgTokenFilePathFromURL(appInfo.AuthDomain) if err != nil { return "", errors.Wrap(err, "failed to generate org token file path") } fileLockOrgToken := newLock(orgTokenPath) if err = fileLockOrgToken.Acquire(); err != nil { return "", errors.Wrap(err, "failed to acquire org token lock") } defer func() { _ = fileLockOrgToken.Release() }() // check if an org token has been created since the lock was acquired orgToken, err = GetOrgTokenIfExists(appInfo.AuthDomain) } if err == nil { if appToken, err := exchangeOrgToken(appURL, orgToken); err != nil { log.Debug().Msgf("failed to exchange org token for app token: %s", err) } else { // generate app path if err := os.WriteFile(appTokenPath, []byte(appToken), 0600); err != nil { return "", errors.Wrap(err, "failed to write app token to disk") } return appToken, nil } } return getTokensFromEdge(appURL, appInfo.AppAUD, appTokenPath, orgTokenPath, useHostOnly, autoClose, isFedramp, log) } // getTokensFromEdge will attempt to use the transfer service to retrieve an app and org token, save them to disk, // and return the app token. func getTokensFromEdge(appURL *url.URL, appAUD, appTokenPath, orgTokenPath string, useHostOnly bool, autoClose bool, isFedramp bool, log *zerolog.Logger) (string, error) { // If no org token exists or if it couldn't be exchanged for an app token, then run the transfer service flow. // this weird parameter is the resource name (token) and the key/value // we want to send to the transfer service. the key is token and the value // is blank (basically just the id generated in the transfer service) resourceData, err := RunTransfer(appURL, appAUD, keyName, keyName, "", true, useHostOnly, autoClose, isFedramp, log) if err != nil { return "", errors.Wrap(err, "failed to run transfer service") } var resp transferServiceResponse if err = json.Unmarshal(resourceData, &resp); err != nil { return "", errors.Wrap(err, "failed to marshal transfer service response") } // If we were able to get the auth domain and generate an org token path, lets write it to disk. if orgTokenPath != "" { if err := os.WriteFile(orgTokenPath, []byte(resp.OrgToken), 0600); err != nil { return "", errors.Wrap(err, "failed to write org token to disk") } } if err := os.WriteFile(appTokenPath, []byte(resp.AppToken), 0600); err != nil { return "", errors.Wrap(err, "failed to write app token to disk") } return resp.AppToken, nil } // GetAppInfo makes a request to the appURL and stops at the first redirect. The 302 location header will contain the // auth domain func GetAppInfo(reqURL *url.URL) (*AppInfo, error) { client := &http.Client{ // do not follow redirects CheckRedirect: func(req *http.Request, via []*http.Request) error { // stop after hitting login endpoint since it will contain app path if strings.Contains(via[len(via)-1].URL.Path, AccessLoginWorkerPath) { return http.ErrUseLastResponse } return nil }, Timeout: time.Second * 7, } appInfoReq, err := http.NewRequest("HEAD", reqURL.String(), nil) if err != nil { return nil, errors.Wrap(err, "failed to create app info request") } appInfoReq.Header.Add("User-Agent", userAgent) resp, err := client.Do(appInfoReq) if err != nil { return nil, errors.Wrap(err, "failed to get app info") } resp.Body.Close() var aud string location := resp.Request.URL if strings.Contains(location.Path, AccessLoginWorkerPath) { aud = resp.Request.URL.Query().Get("kid") if aud == "" { return nil, errors.New("Empty app aud") } } else if audHeader := resp.Header.Get(appAUDHeader); audHeader != "" { // 403/401 from the edge will have aud in a header aud = audHeader } else { return nil, fmt.Errorf("failed to find Access application at %s", reqURL.String()) } domain := resp.Header.Get(appDomainHeader) if domain == "" { return nil, errors.New("Empty app domain") } return &AppInfo{location.Hostname(), aud, domain}, nil } func handleRedirects(req *http.Request, via []*http.Request, orgToken string) error { // attach org token to login request if strings.Contains(req.URL.Path, AccessLoginWorkerPath) { req.AddCookie(&http.Cookie{Name: tokenCookie, Value: orgToken}) } // attach app session cookie to authorized request if strings.Contains(req.URL.Path, AccessAuthorizedWorkerPath) { // We need to check and see if the CF_APP_SESSION cookie was set for _, prevReq := range via { if prevReq != nil && prevReq.Response != nil { for _, c := range prevReq.Response.Cookies() { if c.Name == appSessionCookie { req.AddCookie(&http.Cookie{Name: appSessionCookie, Value: c.Value}) return nil } } } } } // stop after hitting authorized endpoint since it will contain the app token if len(via) > 0 && strings.Contains(via[len(via)-1].URL.Path, AccessAuthorizedWorkerPath) { return http.ErrUseLastResponse } return nil } // exchangeOrgToken attaches an org token to a request to the appURL and returns an app token. This uses the Access SSO // flow to automatically generate and return an app token without the login page. func exchangeOrgToken(appURL *url.URL, orgToken string) (string, error) { client := &http.Client{ CheckRedirect: func(req *http.Request, via []*http.Request) error { return handleRedirects(req, via, orgToken) }, Timeout: time.Second * 7, } appTokenRequest, err := http.NewRequest("HEAD", appURL.String(), nil) if err != nil { return "", errors.Wrap(err, "failed to create app token request") } appTokenRequest.Header.Add("User-Agent", userAgent) resp, err := client.Do(appTokenRequest) if err != nil { return "", errors.Wrap(err, "failed to get app token") } resp.Body.Close() var appToken string for _, c := range resp.Cookies() { //if Org token revoked on exchange, getTokensFromEdge instead validAppToken := c.Name == tokenCookie && time.Now().Before(c.Expires) if validAppToken { appToken = c.Value break } } if len(appToken) > 0 { return appToken, nil } return "", fmt.Errorf("response from %s did not contain app token", resp.Request.URL.String()) } func GetOrgTokenIfExists(authDomain string) (string, error) { path, err := generateOrgTokenFilePathFromURL(authDomain) if err != nil { return "", err } token, err := getTokenIfExists(path) if err != nil { return "", err } var payload jwtPayload err = json.Unmarshal(token.UnsafePayloadWithoutVerification(), &payload) if err != nil { return "", err } if payload.isExpired() { err := os.Remove(path) return "", err } return token.CompactSerialize() } func GetAppTokenIfExists(appInfo *AppInfo) (string, error) { path, err := GenerateAppTokenFilePathFromURL(appInfo.AppDomain, appInfo.AppAUD, keyName) if err != nil { return "", err } token, err := getTokenIfExists(path) if err != nil { return "", err } var payload jwtPayload err = json.Unmarshal(token.UnsafePayloadWithoutVerification(), &payload) if err != nil { return "", err } if payload.isExpired() { err := os.Remove(path) return "", err } return token.CompactSerialize() } // GetTokenIfExists will return the token from local storage if it exists and not expired func getTokenIfExists(path string) (*jose.JSONWebSignature, error) { content, err := os.ReadFile(path) if err != nil { return nil, err } token, err := jose.ParseSigned(string(content), signatureAlgs) if err != nil { return nil, err } return token, nil } // RemoveTokenIfExists removes the a token from local storage if it exists func RemoveTokenIfExists(appInfo *AppInfo) error { path, err := GenerateAppTokenFilePathFromURL(appInfo.AppDomain, appInfo.AppAUD, keyName) if err != nil { return err } if err := os.Remove(path); err != nil && !os.IsNotExist(err) { return err } return nil } ================================================ FILE: token/token_test.go ================================================ package token import ( "encoding/json" "net/http" "net/url" "testing" ) func TestHandleRedirects_AttachOrgToken(t *testing.T) { req, _ := http.NewRequest("GET", "http://example.com/cdn-cgi/access/login", nil) via := []*http.Request{} orgToken := "orgTokenValue" _ = handleRedirects(req, via, orgToken) // Check if the orgToken cookie is attached cookies := req.Cookies() found := false for _, cookie := range cookies { if cookie.Name == tokenCookie && cookie.Value == orgToken { found = true break } } if !found { t.Errorf("OrgToken cookie not attached to the request.") } } func TestHandleRedirects_AttachAppSessionCookie(t *testing.T) { req, _ := http.NewRequest("GET", "http://example.com/cdn-cgi/access/authorized", nil) via := []*http.Request{ { URL: &url.URL{Path: "/cdn-cgi/access/login"}, Response: &http.Response{ Header: http.Header{"Set-Cookie": {"CF_AppSession=appSessionValue"}}, }, }, } orgToken := "orgTokenValue" err := handleRedirects(req, via, orgToken) // Check if the appSessionCookie is attached to the request cookies := req.Cookies() found := false for _, cookie := range cookies { if cookie.Name == appSessionCookie && cookie.Value == "appSessionValue" { found = true break } } if !found { t.Errorf("AppSessionCookie not attached to the request.") } if err != nil { t.Errorf("Expected no error, got %v", err) } } func TestHandleRedirects_StopAtAuthorizedEndpoint(t *testing.T) { req, _ := http.NewRequest("GET", "http://example.com/cdn-cgi/access/authorized", nil) via := []*http.Request{ { URL: &url.URL{Path: "other"}, }, { URL: &url.URL{Path: AccessAuthorizedWorkerPath}, }, } orgToken := "orgTokenValue" err := handleRedirects(req, via, orgToken) // Check if ErrUseLastResponse is returned if err != http.ErrUseLastResponse { t.Errorf("Expected ErrUseLastResponse, got %v", err) } } func TestJwtPayloadUnmarshal_AudAsString(t *testing.T) { jwt := `{"aud":"7afbdaf987054f889b3bdd0d29ebfcd2"}` var payload jwtPayload if err := json.Unmarshal([]byte(jwt), &payload); err != nil { t.Errorf("Expected no error, got %v", err) } if len(payload.Aud) != 1 || payload.Aud[0] != "7afbdaf987054f889b3bdd0d29ebfcd2" { t.Errorf("Expected aud to be 7afbdaf987054f889b3bdd0d29ebfcd2, got %v", payload.Aud) } } func TestJwtPayloadUnmarshal_AudAsSlice(t *testing.T) { jwt := `{"aud":["7afbdaf987054f889b3bdd0d29ebfcd2", "f835c0016f894768976c01e076844efe"]}` var payload jwtPayload if err := json.Unmarshal([]byte(jwt), &payload); err != nil { t.Errorf("Expected no error, got %v", err) } if len(payload.Aud) != 2 || payload.Aud[0] != "7afbdaf987054f889b3bdd0d29ebfcd2" || payload.Aud[1] != "f835c0016f894768976c01e076844efe" { t.Errorf("Expected aud to be [7afbdaf987054f889b3bdd0d29ebfcd2, f835c0016f894768976c01e076844efe], got %v", payload.Aud) } } func TestJwtPayloadUnmarshal_FailsWhenAudIsInt(t *testing.T) { jwt := `{"aud":123}` var payload jwtPayload err := json.Unmarshal([]byte(jwt), &payload) wantErr := "aud field is not a string or an array of strings" if err.Error() != wantErr { t.Errorf("Expected %v, got %v", wantErr, err) } } func TestJwtPayloadUnmarshal_FailsWhenAudIsArrayOfInts(t *testing.T) { jwt := `{"aud": [999, 123] }` var payload jwtPayload err := json.Unmarshal([]byte(jwt), &payload) wantErr := "aud array contains non-string elements" if err.Error() != wantErr { t.Errorf("Expected %v, got %v", wantErr, err) } } func TestJwtPayloadUnmarshal_FailsWhenAudIsOmitted(t *testing.T) { jwt := `{}` var payload jwtPayload err := json.Unmarshal([]byte(jwt), &payload) wantErr := "aud field is not a string or an array of strings" if err.Error() != wantErr { t.Errorf("Expected %v, got %v", wantErr, err) } } ================================================ FILE: token/transfer.go ================================================ package token import ( "bytes" "encoding/base64" "fmt" "io" "net/http" "net/url" "os" "time" "github.com/pkg/errors" "github.com/rs/zerolog" ) const ( baseStoreURL = "https://login.cloudflareaccess.org/" fedStoreURL = "https://login.fed.cloudflareaccess.org/" clientTimeout = time.Second * 60 ) // RunTransfer does the transfer "dance" with the end result downloading the supported resource. // The expanded description is run is encapsulation of shared business logic needed // to request a resource (token/cert/etc) from the transfer service (loginhelper). // The "dance" we refer to is building a HTTP request, opening that in a browser waiting for // the user to complete an action, while it long polls in the background waiting for an // action to be completed to download the resource. func RunTransfer(transferURL *url.URL, appAUD, resourceName, key, value string, shouldEncrypt bool, useHostOnly bool, autoClose bool, fedramp bool, log *zerolog.Logger) ([]byte, error) { encrypterClient, err := NewEncrypter("cloudflared_priv.pem", "cloudflared_pub.pem") if err != nil { return nil, err } requestURL, err := buildRequestURL(transferURL, appAUD, key, value+encrypterClient.PublicKey(), shouldEncrypt, useHostOnly, autoClose) if err != nil { return nil, err } // See AUTH-1423 for why we use stderr (the way git wraps ssh) err = OpenBrowser(requestURL) if err != nil { fmt.Fprintf(os.Stderr, "Please open the following URL and log in with your Cloudflare account:\n\n%s\n\nLeave cloudflared running to download the %s automatically.\n", requestURL, resourceName) } else { fmt.Fprintf(os.Stderr, "A browser window should have opened at the following URL:\n\n%s\n\nIf the browser failed to open, please visit the URL above directly in your browser.\n", requestURL) } var resourceData []byte storeURL := baseStoreURL if fedramp { storeURL = fedStoreURL } if shouldEncrypt { buf, key, err := transferRequest(storeURL+"transfer/"+encrypterClient.PublicKey(), log) if err != nil { return nil, err } decodedBuf, err := base64.StdEncoding.DecodeString(string(buf)) if err != nil { return nil, err } decrypted, err := encrypterClient.Decrypt(decodedBuf, key) if err != nil { return nil, err } resourceData = decrypted } else { buf, _, err := transferRequest(storeURL+encrypterClient.PublicKey(), log) if err != nil { return nil, err } resourceData = buf } return resourceData, nil } // BuildRequestURL creates a request suitable for a resource transfer. // it will return a constructed url based off the base url and query key/value provided. // cli will build a url for cli transfer request. func buildRequestURL(baseURL *url.URL, appAUD string, key, value string, cli, useHostOnly bool, autoClose bool) (string, error) { q := baseURL.Query() q.Set(key, value) q.Set("aud", appAUD) baseURL.RawQuery = q.Encode() if useHostOnly { baseURL.Path = "" } // TODO: pass arg for tunnel login if !cli { return baseURL.String(), nil } q.Set("redirect_url", baseURL.String()) // we add the token as a query param on both the redirect_url and the main url q.Set("send_org_token", "true") // indicates that the cli endpoint should return both the org and app token q.Set("edge_token_transfer", "true") // use new LoginHelper service built on workers if autoClose { q.Set("close_interstitial", "true") // Automatically close the success window. } baseURL.RawQuery = q.Encode() // and this actual baseURL. baseURL.Path = "cdn-cgi/access/cli" return baseURL.String(), nil } // transferRequest downloads the requested resource from the request URL func transferRequest(requestURL string, log *zerolog.Logger) ([]byte, string, error) { client := &http.Client{Timeout: clientTimeout} const pollAttempts = 10 // we do "long polling" on the endpoint to get the resource. for i := 0; i < pollAttempts; i++ { buf, key, err := poll(client, requestURL, log) if err != nil { return nil, "", err } else if len(buf) > 0 { return buf, key, nil } } return nil, "", errors.New("Failed to fetch resource") } // poll the endpoint for the request resource, waiting for the user interaction func poll(client *http.Client, requestURL string, log *zerolog.Logger) ([]byte, string, error) { req, err := http.NewRequest(http.MethodGet, requestURL, nil) if err != nil { return nil, "", err } req.Header.Set("User-Agent", userAgent) resp, err := client.Do(req) if err != nil { return nil, "", err } defer resp.Body.Close() // ignore everything other than server errors as the resource // may not exist until the user does the interaction if resp.StatusCode >= 500 { buf := new(bytes.Buffer) if _, err := io.Copy(buf, resp.Body); err != nil { return nil, "", err } return nil, "", fmt.Errorf("error on request %d: %s", resp.StatusCode, buf.String()) } if resp.StatusCode != 200 { log.Info().Msg("Waiting for login...") return nil, "", nil } buf := new(bytes.Buffer) if _, err := io.Copy(buf, resp.Body); err != nil { return nil, "", err } return buf.Bytes(), resp.Header.Get("service-public-key"), nil } ================================================ FILE: tracing/client.go ================================================ package tracing import ( "context" "encoding/base64" "errors" "sync" coltracepb "go.opentelemetry.io/proto/otlp/collector/trace/v1" tracepb "go.opentelemetry.io/proto/otlp/trace/v1" "google.golang.org/protobuf/proto" ) const ( MaxTraceAmount = 20 ) var ( errNoTraces = errors.New("no traces recorded to be exported") errNoopTracer = errors.New("noop tracer has no traces") ) type InMemoryClient interface { // Spans returns a copy of the list of in-memory stored spans as a base64 // encoded otlp protobuf string. Spans() (string, error) // ExportProtoSpans returns a copy of the list of in-memory stored spans as otlp // protobuf byte array and clears the in-memory spans. ExportProtoSpans() ([]byte, error) } // InMemoryOtlpClient is a client implementation for otlptrace.Client type InMemoryOtlpClient struct { mu sync.Mutex spans []*tracepb.ResourceSpans } func (mc *InMemoryOtlpClient) Start(_ context.Context) error { return nil } func (mc *InMemoryOtlpClient) Stop(_ context.Context) error { return nil } // UploadTraces adds the provided list of spans to the in-memory list. func (mc *InMemoryOtlpClient) UploadTraces(_ context.Context, protoSpans []*tracepb.ResourceSpans) error { mc.mu.Lock() defer mc.mu.Unlock() // Catch to make sure too many traces aren't being added to response header. // Returning nil makes sure we don't fail to send the traces we already recorded. if len(mc.spans)+len(protoSpans) > MaxTraceAmount { return nil } mc.spans = append(mc.spans, protoSpans...) return nil } // Spans returns the list of in-memory stored spans as a base64 encoded otlp protobuf string. func (mc *InMemoryOtlpClient) Spans() (string, error) { data, err := mc.ExportProtoSpans() if err != nil { return "", err } return base64.StdEncoding.EncodeToString(data), nil } // ProtoSpans returns the list of in-memory stored spans as the protobuf byte array. func (mc *InMemoryOtlpClient) ExportProtoSpans() ([]byte, error) { mc.mu.Lock() defer mc.mu.Unlock() if len(mc.spans) <= 0 { return nil, errNoTraces } pbRequest := &coltracepb.ExportTraceServiceRequest{ ResourceSpans: mc.spans, } serializedSpans, err := proto.Marshal(pbRequest) if err != nil { return nil, err } mc.spans = make([]*tracepb.ResourceSpans, 0) return serializedSpans, nil } // NoopOtlpClient is a client implementation for otlptrace.Client that does nothing type NoopOtlpClient struct{} func (mc *NoopOtlpClient) Start(_ context.Context) error { return nil } func (mc *NoopOtlpClient) Stop(_ context.Context) error { return nil } func (mc *NoopOtlpClient) UploadTraces(_ context.Context, _ []*tracepb.ResourceSpans) error { return nil } // Spans always returns no traces error func (mc *NoopOtlpClient) Spans() (string, error) { return "", errNoopTracer } // Spans always returns no traces error func (mc *NoopOtlpClient) ExportProtoSpans() ([]byte, error) { return nil, errNoopTracer } func (mc *NoopOtlpClient) ClearSpans() {} ================================================ FILE: tracing/client_test.go ================================================ package tracing import ( "context" "testing" "time" "github.com/stretchr/testify/assert" "go.opentelemetry.io/otel/exporters/otlp/otlptrace" semconv "go.opentelemetry.io/otel/semconv/v1.7.0" "go.opentelemetry.io/otel/trace" commonpb "go.opentelemetry.io/proto/otlp/common/v1" resourcepb "go.opentelemetry.io/proto/otlp/resource/v1" tracepb "go.opentelemetry.io/proto/otlp/trace/v1" ) const ( resourceSchemaUrl = "http://example.com/custom-resource-schema" instrumentSchemaUrl = semconv.SchemaURL ) var ( traceId = []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F} spanId = []byte{0xFF, 0xFE, 0xFD, 0xFC, 0xFB, 0xFA, 0xF9, 0xF8} parentSpanId = []byte{0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A, 0x09, 0x08} startTime = time.Date(2022, 4, 4, 0, 0, 0, 0, time.UTC) endTime = startTime.Add(5 * time.Second) traceState, _ = trace.ParseTraceState("key1=val1,key2=val2") instrScope = &commonpb.InstrumentationScope{Name: "go.opentelemetry.io/test/otel", Version: "v1.6.0"} otlpKeyValues = []*commonpb.KeyValue{ { Key: "string_key", Value: &commonpb.AnyValue{ Value: &commonpb.AnyValue_StringValue{ StringValue: "string value", }, }, }, { Key: "bool_key", Value: &commonpb.AnyValue{ Value: &commonpb.AnyValue_BoolValue{ BoolValue: true, }, }, }, } otlpResource = &resourcepb.Resource{ Attributes: []*commonpb.KeyValue{ { Key: "service.name", Value: &commonpb.AnyValue{ Value: &commonpb.AnyValue_StringValue{ StringValue: "service-name", }, }, }, }, } ) var _ otlptrace.Client = (*InMemoryOtlpClient)(nil) var _ InMemoryClient = (*InMemoryOtlpClient)(nil) var _ otlptrace.Client = (*NoopOtlpClient)(nil) var _ InMemoryClient = (*NoopOtlpClient)(nil) func TestUploadTraces(t *testing.T) { client := &InMemoryOtlpClient{} spans := createResourceSpans([]*tracepb.Span{createOtlpSpan(traceId)}) spans2 := createResourceSpans([]*tracepb.Span{createOtlpSpan(traceId)}) err := client.UploadTraces(context.Background(), spans) assert.NoError(t, err) err = client.UploadTraces(context.Background(), spans2) assert.NoError(t, err) assert.Len(t, client.spans, 2) } func TestSpans(t *testing.T) { client := &InMemoryOtlpClient{} spans := createResourceSpans([]*tracepb.Span{createOtlpSpan(traceId)}) err := client.UploadTraces(context.Background(), spans) assert.NoError(t, err) assert.Len(t, client.spans, 1) enc, err := client.Spans() assert.NoError(t, err) expected := "CsECCiAKHgoMc2VydmljZS5uYW1lEg4KDHNlcnZpY2UtbmFtZRLxAQonCh1nby5vcGVudGVsZW1ldHJ5LmlvL3Rlc3Qvb3RlbBIGdjEuNi4wEp0BChAAAQIDBAUGBwgJCgsMDQ4PEgj//v38+/r5+BoTa2V5MT12YWwxLGtleTI9dmFsMiIIDw4NDAsKCQgqCnRyYWNlX25hbWUwATkAANJvaYjiFkEA8teZaojiFkocCgpzdHJpbmdfa2V5Eg4KDHN0cmluZyB2YWx1ZUoOCghib29sX2tleRICEAF6EhIOc3RhdHVzIG1lc3NhZ2UYARomaHR0cHM6Ly9vcGVudGVsZW1ldHJ5LmlvL3NjaGVtYXMvMS43LjAaKWh0dHA6Ly9leGFtcGxlLmNvbS9jdXN0b20tcmVzb3VyY2Utc2NoZW1h" assert.Equal(t, expected, enc) } func TestSpansEmpty(t *testing.T) { client := &InMemoryOtlpClient{} err := client.UploadTraces(context.Background(), []*tracepb.ResourceSpans{}) assert.NoError(t, err) assert.Len(t, client.spans, 0) _, err = client.Spans() assert.ErrorIs(t, err, errNoTraces) } func TestSpansNil(t *testing.T) { client := &InMemoryOtlpClient{} err := client.UploadTraces(context.Background(), nil) assert.NoError(t, err) assert.Len(t, client.spans, 0) _, err = client.Spans() assert.ErrorIs(t, err, errNoTraces) } func TestSpansTooManySpans(t *testing.T) { client := &InMemoryOtlpClient{} for i := 0; i < MaxTraceAmount+1; i++ { spans := createResourceSpans([]*tracepb.Span{createOtlpSpan(traceId)}) err := client.UploadTraces(context.Background(), spans) assert.NoError(t, err) } assert.Len(t, client.spans, MaxTraceAmount) _, err := client.Spans() assert.NoError(t, err) } func createResourceSpans(spans []*tracepb.Span) []*tracepb.ResourceSpans { return []*tracepb.ResourceSpans{createResourceSpan(spans)} } func createResourceSpan(spans []*tracepb.Span) *tracepb.ResourceSpans { return &tracepb.ResourceSpans{ Resource: otlpResource, ScopeSpans: []*tracepb.ScopeSpans{ { Scope: instrScope, Spans: spans, SchemaUrl: instrumentSchemaUrl, }, }, SchemaUrl: resourceSchemaUrl, } } func createOtlpSpan(tid []byte) *tracepb.Span { return &tracepb.Span{ TraceId: tid, SpanId: spanId, TraceState: traceState.String(), ParentSpanId: parentSpanId, Name: "trace_name", Kind: tracepb.Span_SPAN_KIND_INTERNAL, StartTimeUnixNano: uint64(startTime.UnixNano()), EndTimeUnixNano: uint64(endTime.UnixNano()), Attributes: otlpKeyValues, DroppedAttributesCount: 0, Events: nil, DroppedEventsCount: 0, Links: nil, DroppedLinksCount: 0, Status: &tracepb.Status{ Message: "status message", Code: tracepb.Status_STATUS_CODE_OK, }, } } ================================================ FILE: tracing/identity.go ================================================ package tracing import ( "bytes" "encoding/binary" "fmt" "strconv" "strings" ) const ( // 16 bytes for tracing ID, 8 bytes for span ID and 1 byte for flags IdentityLength = 16 + 8 + 1 ) type Identity struct { // Based on https://www.jaegertracing.io/docs/1.36/client-libraries/#value // parent span ID is always 0 for our case traceIDUpper uint64 traceIDLower uint64 spanID uint64 flags uint8 } // TODO: TUN-6604 Remove this. To reconstruct into Jaeger propagation format, convert tracingContext to tracing.Identity func (tc *Identity) String() string { return fmt.Sprintf("%016x%016x:%x:0:%x", tc.traceIDUpper, tc.traceIDLower, tc.spanID, tc.flags) } func (tc *Identity) MarshalBinary() ([]byte, error) { buf := bytes.NewBuffer(make([]byte, 0, IdentityLength)) for _, field := range []interface{}{ tc.traceIDUpper, tc.traceIDLower, tc.spanID, tc.flags, } { if err := binary.Write(buf, binary.BigEndian, field); err != nil { return nil, err } } return buf.Bytes(), nil } func (tc *Identity) UnmarshalBinary(data []byte) error { if len(data) < IdentityLength { return fmt.Errorf("expect tracingContext to have at least %d bytes, got %d", IdentityLength, len(data)) } buf := bytes.NewBuffer(data) for _, field := range []interface{}{ &tc.traceIDUpper, &tc.traceIDLower, &tc.spanID, &tc.flags, } { if err := binary.Read(buf, binary.BigEndian, field); err != nil { return err } } return nil } func NewIdentity(trace string) (*Identity, error) { parts := strings.Split(trace, separator) if len(parts) != 4 { return nil, fmt.Errorf("trace '%s' doesn't have exactly 4 parts separated by %s", trace, separator) } const base = 16 tracingID, err := padTracingID(parts[0]) if err != nil { return nil, err } traceIDUpper, err := strconv.ParseUint(tracingID[:16], base, 64) if err != nil { return nil, fmt.Errorf("failed to parse first 16 bytes of tracing ID as uint64, err: %w", err) } traceIDLower, err := strconv.ParseUint(tracingID[16:], base, 64) if err != nil { return nil, fmt.Errorf("failed to parse last 16 bytes of tracing ID as uint64, err: %w", err) } spanID, err := strconv.ParseUint(parts[1], base, 64) if err != nil { return nil, fmt.Errorf("failed to parse span ID as uint64, err: %w", err) } flags, err := strconv.ParseUint(parts[3], base, 8) if err != nil { return nil, fmt.Errorf("failed to parse flag as uint8, err: %w", err) } return &Identity{ traceIDUpper: traceIDUpper, traceIDLower: traceIDLower, spanID: spanID, flags: uint8(flags), }, nil } func padTracingID(tracingID string) (string, error) { if len(tracingID) == 0 { return "", fmt.Errorf("missing tracing ID") } if len(tracingID) == traceID128bitsWidth { return tracingID, nil } // Correctly left pad the trace to a length of 32 left := traceID128bitsWidth - len(tracingID) paddedTracingID := strings.Repeat("0", left) + tracingID return paddedTracingID, nil } ================================================ FILE: tracing/identity_test.go ================================================ package tracing import ( "testing" "github.com/stretchr/testify/require" ) func TestNewIdentity(t *testing.T) { testCases := []struct { testCase string trace string expected string }{ { testCase: "full length trace", trace: "ec31ad8a01fde11fdcabe2efdce36873:52726f6cabc144f5:0:1", expected: "ec31ad8a01fde11fdcabe2efdce36873:52726f6cabc144f5:0:1", }, { testCase: "short trace ID", trace: "ad8a01fde11fdcabe2efdce36873:52726f6cabc144f5:0:1", expected: "0000ad8a01fde11fdcabe2efdce36873:52726f6cabc144f5:0:1", }, { testCase: "short trace ID with 0s in the middle", trace: "ad8a01fde11f000002efdce36873:52726f6cabc144f5:0:1", expected: "0000ad8a01fde11f000002efdce36873:52726f6cabc144f5:0:1", }, { testCase: "short trace ID with 0s in the beginning and middle", trace: "001ad8a01fde11fdcabe2efdce36873:52726f6cabc144f5:0:1", expected: "0001ad8a01fde11fdcabe2efdce36873:52726f6cabc144f5:0:1", }, { testCase: "no trace", trace: "", }, { testCase: "missing flags", trace: "ec31ad8a01fde11fdcabe2efdce36873:52726f6cabc144f5:0", }, { testCase: "missing separator", trace: "ec31ad8a01fde11fdcabe2efdce3687352726f6cabc144f501", }, } for _, testCase := range testCases { identity, err := NewIdentity(testCase.trace) if testCase.expected != "" { require.NoError(t, err) require.Equal(t, testCase.expected, identity.String()) serializedIdentity, err := identity.MarshalBinary() require.NoError(t, err) deserializedIdentity := new(Identity) err = deserializedIdentity.UnmarshalBinary(serializedIdentity) require.NoError(t, err) require.Equal(t, identity, deserializedIdentity) } else { require.Error(t, err) require.Nil(t, identity) } } } ================================================ FILE: tracing/tracing.go ================================================ package tracing import ( "context" "errors" "fmt" "math" "net/http" "os" "runtime" "strings" "github.com/rs/zerolog" otelContrib "go.opentelemetry.io/contrib/propagators/jaeger" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/exporters/otlp/otlptrace" "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/sdk/resource" tracesdk "go.opentelemetry.io/otel/sdk/trace" semconv "go.opentelemetry.io/otel/semconv/v1.7.0" "go.opentelemetry.io/otel/trace" ) const ( service = "cloudflared" tracerInstrumentName = "origin" TracerContextName = "cf-trace-id" TracerContextNameOverride = "uber-trace-id" IntCloudflaredTracingHeader = "cf-int-cloudflared-tracing" MaxErrorDescriptionLen = 100 traceHttpStatusCodeKey = "upstreamStatusCode" traceID128bitsWidth = 128 / 4 separator = ":" ) var ( CanonicalCloudflaredTracingHeader = http.CanonicalHeaderKey(IntCloudflaredTracingHeader) Http2TransportAttribute = trace.WithAttributes(transportAttributeKey.String("http2")) QuicTransportAttribute = trace.WithAttributes(transportAttributeKey.String("quic")) HostOSAttribute = semconv.HostTypeKey.String(runtime.GOOS) HostArchAttribute = semconv.HostArchKey.String(runtime.GOARCH) otelVersionAttribute attribute.KeyValue hostnameAttribute attribute.KeyValue cloudflaredVersionAttribute attribute.KeyValue serviceAttribute = semconv.ServiceNameKey.String(service) transportAttributeKey = attribute.Key("transport") otelVersionAttributeKey = attribute.Key("jaeger.version") errNoopTracerProvider = errors.New("noop tracer provider records no spans") ) func init() { // Register the jaeger propagator globally. otel.SetTextMapPropagator(otelContrib.Jaeger{}) otelVersionAttribute = otelVersionAttributeKey.String(fmt.Sprintf("go-otel-%s", otel.Version())) if hostname, err := os.Hostname(); err == nil { hostnameAttribute = attribute.String("hostname", hostname) } } func Init(version string) { cloudflaredVersionAttribute = semconv.ProcessRuntimeVersionKey.String(version) } type TracedHTTPRequest struct { *http.Request *cfdTracer ConnIndex uint8 // The connection index used to proxy the request } // NewTracedHTTPRequest creates a new tracer for the current HTTP request context. func NewTracedHTTPRequest(req *http.Request, connIndex uint8, log *zerolog.Logger) *TracedHTTPRequest { ctx, exists := extractTrace(req) if !exists { return &TracedHTTPRequest{req, &cfdTracer{trace.NewNoopTracerProvider(), &NoopOtlpClient{}, log}, connIndex} } return &TracedHTTPRequest{req.WithContext(ctx), newCfdTracer(ctx, log), connIndex} } func (tr *TracedHTTPRequest) ToTracedContext() *TracedContext { return &TracedContext{tr.Context(), tr.cfdTracer} } type TracedContext struct { context.Context *cfdTracer } // NewTracedContext creates a new tracer for the current context. func NewTracedContext(ctx context.Context, traceContext string, log *zerolog.Logger) *TracedContext { ctx, exists := extractTraceFromString(ctx, traceContext) if !exists { return &TracedContext{ctx, &cfdTracer{trace.NewNoopTracerProvider(), &NoopOtlpClient{}, log}} } return &TracedContext{ctx, newCfdTracer(ctx, log)} } type cfdTracer struct { trace.TracerProvider exporter InMemoryClient log *zerolog.Logger } // NewCfdTracer creates a new tracer for the current request context. func newCfdTracer(ctx context.Context, log *zerolog.Logger) *cfdTracer { mc := new(InMemoryOtlpClient) exp, err := otlptrace.New(ctx, mc) if err != nil { return &cfdTracer{trace.NewNoopTracerProvider(), &NoopOtlpClient{}, log} } tp := tracesdk.NewTracerProvider( // We want to dump to in-memory exporter immediately tracesdk.WithSyncer(exp), // Record information about this application in a Resource. tracesdk.WithResource(resource.NewWithAttributes( semconv.SchemaURL, serviceAttribute, otelVersionAttribute, hostnameAttribute, cloudflaredVersionAttribute, HostOSAttribute, HostArchAttribute, )), ) return &cfdTracer{tp, mc, log} } func (cft *cfdTracer) Tracer() trace.Tracer { return cft.TracerProvider.Tracer(tracerInstrumentName) } // GetSpans returns the spans as base64 encoded string of protobuf otlp traces. func (cft *cfdTracer) GetSpans() (enc string) { enc, err := cft.exporter.Spans() switch err { case nil: break case errNoTraces: cft.log.Trace().Err(err).Msgf("expected traces to be available") return case errNoopTracer: return // noop tracer has no traces default: cft.log.Debug().Err(err) return } return } // GetProtoSpans returns the spans as the otlp traces in protobuf byte array. func (cft *cfdTracer) GetProtoSpans() (proto []byte) { proto, err := cft.exporter.ExportProtoSpans() switch err { case nil: break case errNoTraces: cft.log.Trace().Err(err).Msgf("expected traces to be available") return case errNoopTracer: return // noop tracer has no traces default: cft.log.Debug().Err(err) return } return } // AddSpans assigns spans as base64 encoded protobuf otlp traces to provided // HTTP headers. func (cft *cfdTracer) AddSpans(headers http.Header) { if headers == nil { return } enc := cft.GetSpans() // No need to add header if no traces if enc == "" { return } headers[CanonicalCloudflaredTracingHeader] = []string{enc} } // End will set the OK status for the span and then end it. func End(span trace.Span) { endSpan(span, -1, codes.Ok, nil) } // EndWithErrorStatus will set a status for the span and then end it. func EndWithErrorStatus(span trace.Span, err error) { endSpan(span, -1, codes.Error, err) } // EndWithStatusCode will set a status for the span and then end it. func EndWithStatusCode(span trace.Span, statusCode int) { endSpan(span, statusCode, codes.Ok, nil) } // EndWithErrorStatus will set a status for the span and then end it. func endSpan(span trace.Span, upstreamStatusCode int, spanStatusCode codes.Code, err error) { if span == nil { return } if upstreamStatusCode > 0 { span.SetAttributes(attribute.Int(traceHttpStatusCodeKey, upstreamStatusCode)) } // add error to status buf cap description errDescription := "" if err != nil { errDescription = err.Error() l := int(math.Min(float64(len(errDescription)), MaxErrorDescriptionLen)) errDescription = errDescription[:l] } span.SetStatus(spanStatusCode, errDescription) span.End() } // extractTraceFromString will extract the trace information from the provided // propagated trace string context. func extractTraceFromString(ctx context.Context, trace string) (context.Context, bool) { if trace == "" { return ctx, false } // Jaeger specific separator parts := strings.Split(trace, separator) if len(parts) != 4 { return ctx, false } if parts[0] == "" { return ctx, false } // Correctly left pad the trace to a length of 32 if len(parts[0]) < traceID128bitsWidth { left := traceID128bitsWidth - len(parts[0]) parts[0] = strings.Repeat("0", left) + parts[0] trace = strings.Join(parts, separator) } // Override the 'cf-trace-id' as 'uber-trace-id' so the jaeger propagator can extract it. traceHeader := map[string]string{TracerContextNameOverride: trace} remoteCtx := otel.GetTextMapPropagator().Extract(ctx, propagation.MapCarrier(traceHeader)) return remoteCtx, true } // extractTrace attempts to check for a cf-trace-id from a request and return the // trace context with the provided http.Request. func extractTrace(req *http.Request) (context.Context, bool) { // Only add tracing for requests with appropriately tagged headers remoteTraces := req.Header.Values(TracerContextName) if len(remoteTraces) <= 0 { // Strip the cf-trace-id header req.Header.Del(TracerContextName) return nil, false } traceHeader := map[string]string{} for _, t := range remoteTraces { // Override the 'cf-trace-id' as 'uber-trace-id' so the jaeger propagator can extract it. // Last entry wins if multiple provided traceHeader[TracerContextNameOverride] = t } // Strip the cf-trace-id header req.Header.Del(TracerContextName) if traceHeader[TracerContextNameOverride] == "" { return nil, false } remoteCtx := otel.GetTextMapPropagator().Extract(req.Context(), propagation.MapCarrier(traceHeader)) return remoteCtx, true } func NewNoopSpan() trace.Span { return trace.SpanFromContext(nil) } ================================================ FILE: tracing/tracing_test.go ================================================ package tracing import ( "context" "net/http" "net/http/httptest" "testing" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" tracesdk "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/trace" tracepb "go.opentelemetry.io/proto/otlp/trace/v1" ) func TestNewCfTracer(t *testing.T) { log := zerolog.Nop() req := httptest.NewRequest("GET", "http://localhost", nil) req.Header.Add(TracerContextName, "14cb070dde8e51fc5ae8514e69ba42ca:b38f1bf5eae406f3:0:1") tr := NewTracedHTTPRequest(req, 0, &log) assert.NotNil(t, tr) assert.IsType(t, tracesdk.NewTracerProvider(), tr.TracerProvider) assert.IsType(t, &InMemoryOtlpClient{}, tr.exporter) } func TestNewCfTracerMultiple(t *testing.T) { log := zerolog.Nop() req := httptest.NewRequest("GET", "http://localhost", nil) req.Header.Add(TracerContextName, "1241ce3ecdefc68854e8514e69ba42ca:b38f1bf5eae406f3:0:1") req.Header.Add(TracerContextName, "14cb070dde8e51fc5ae8514e69ba42ca:b38f1bf5eae406f3:0:1") tr := NewTracedHTTPRequest(req, 0, &log) assert.NotNil(t, tr) assert.IsType(t, tracesdk.NewTracerProvider(), tr.TracerProvider) assert.IsType(t, &InMemoryOtlpClient{}, tr.exporter) } func TestNewCfTracerNilHeader(t *testing.T) { log := zerolog.Nop() req := httptest.NewRequest("GET", "http://localhost", nil) req.Header[http.CanonicalHeaderKey(TracerContextName)] = nil tr := NewTracedHTTPRequest(req, 0, &log) assert.NotNil(t, tr) assert.IsType(t, trace.NewNoopTracerProvider(), tr.TracerProvider) assert.IsType(t, &NoopOtlpClient{}, tr.exporter) } func TestNewCfTracerInvalidHeaders(t *testing.T) { log := zerolog.Nop() req := httptest.NewRequest("GET", "http://localhost", nil) for _, test := range [][]string{nil, {""}} { req.Header[http.CanonicalHeaderKey(TracerContextName)] = test tr := NewTracedHTTPRequest(req, 0, &log) assert.NotNil(t, tr) assert.IsType(t, trace.NewNoopTracerProvider(), tr.TracerProvider) assert.IsType(t, &NoopOtlpClient{}, tr.exporter) } } func TestAddingSpansWithNilMap(t *testing.T) { log := zerolog.Nop() req := httptest.NewRequest("GET", "http://localhost", nil) req.Header.Add(TracerContextName, "14cb070dde8e51fc5ae8514e69ba42ca:b38f1bf5eae406f3:0:1") tr := NewTracedHTTPRequest(req, 0, &log) exporter := tr.exporter.(*InMemoryOtlpClient) // add fake spans spans := createResourceSpans([]*tracepb.Span{createOtlpSpan(traceId)}) err := exporter.UploadTraces(context.Background(), spans) assert.NoError(t, err) // a panic shouldn't occur tr.AddSpans(nil) } func FuzzNewIdentity(f *testing.F) { f.Fuzz(func(t *testing.T, trace string) { _, _ = NewIdentity(trace) }) } ================================================ FILE: tunnelrpc/metrics/metrics.go ================================================ package metrics import ( "github.com/prometheus/client_golang/prometheus" ) const ( metricsNamespace = "cloudflared" rpcSubsystem = "rpc" ) // CloudflaredServer operation labels // CloudflaredServer is an extension of SessionManager with additional methods, but it's helpful // to visualize it separately in the metrics since they are technically different client/servers. const ( Cloudflared = "cloudflared" ) // ConfigurationManager operation labels const ( ConfigurationManager = "config" OperationUpdateConfiguration = "update_configuration" ) // SessionManager operation labels const ( SessionManager = "session" OperationRegisterUdpSession = "register_udp_session" OperationUnregisterUdpSession = "unregister_udp_session" ) // RegistrationServer operation labels const ( Registration = "registration" OperationRegisterConnection = "register_connection" OperationUnregisterConnection = "unregister_connection" OperationUpdateLocalConfiguration = "update_local_configuration" ) type rpcMetrics struct { serverOperations *prometheus.CounterVec serverFailures *prometheus.CounterVec serverOperationsLatency *prometheus.HistogramVec ClientOperations *prometheus.CounterVec ClientFailures *prometheus.CounterVec ClientOperationsLatency *prometheus.HistogramVec } var CapnpMetrics *rpcMetrics = &rpcMetrics{ serverOperations: prometheus.NewCounterVec( prometheus.CounterOpts{ Namespace: metricsNamespace, Subsystem: rpcSubsystem, Name: "server_operations", Help: "Number of rpc methods by handler served", }, []string{"handler", "method"}, ), serverFailures: prometheus.NewCounterVec( prometheus.CounterOpts{ Namespace: metricsNamespace, Subsystem: rpcSubsystem, Name: "server_failures", Help: "Number of rpc methods failures by handler served", }, []string{"handler", "method"}, ), serverOperationsLatency: prometheus.NewHistogramVec( prometheus.HistogramOpts{ Namespace: metricsNamespace, Subsystem: rpcSubsystem, Name: "server_latency_secs", Help: "Latency of rpc methods by handler served", // Bucket starts at 50ms, each bucket grows by a factor of 3, up to 5 buckets and is expressed as seconds: // 50ms, 150ms, 450ms, 1350ms, 4050ms Buckets: prometheus.ExponentialBuckets(0.05, 3, 5), }, []string{"handler", "method"}, ), ClientOperations: prometheus.NewCounterVec( prometheus.CounterOpts{ Namespace: metricsNamespace, Subsystem: rpcSubsystem, Name: "client_operations", Help: "Number of rpc methods by handler requested", }, []string{"handler", "method"}, ), ClientFailures: prometheus.NewCounterVec( prometheus.CounterOpts{ Namespace: metricsNamespace, Subsystem: rpcSubsystem, Name: "client_failures", Help: "Number of rpc method failures by handler requested", }, []string{"handler", "method"}, ), ClientOperationsLatency: prometheus.NewHistogramVec( prometheus.HistogramOpts{ Namespace: metricsNamespace, Subsystem: rpcSubsystem, Name: "client_latency_secs", Help: "Latency of rpc methods by handler requested", // Bucket starts at 50ms, each bucket grows by a factor of 3, up to 5 buckets and is expressed as seconds: // 50ms, 150ms, 450ms, 1350ms, 4050ms Buckets: prometheus.ExponentialBuckets(0.05, 3, 5), }, []string{"handler", "method"}, ), } func ObserveServerHandler(inner func() error, handler, method string) error { defer CapnpMetrics.serverOperations.WithLabelValues(handler, method).Inc() timer := prometheus.NewTimer(prometheus.ObserverFunc(func(s float64) { CapnpMetrics.serverOperationsLatency.WithLabelValues(handler, method).Observe(s) })) defer timer.ObserveDuration() err := inner() if err != nil { CapnpMetrics.serverFailures.WithLabelValues(handler, method).Inc() } return err } func NewClientOperationLatencyObserver(server string, method string) *prometheus.Timer { return prometheus.NewTimer(prometheus.ObserverFunc(func(s float64) { CapnpMetrics.ClientOperationsLatency.WithLabelValues(server, method).Observe(s) })) } func init() { prometheus.MustRegister(CapnpMetrics.serverOperations) prometheus.MustRegister(CapnpMetrics.serverFailures) prometheus.MustRegister(CapnpMetrics.serverOperationsLatency) prometheus.MustRegister(CapnpMetrics.ClientOperations) prometheus.MustRegister(CapnpMetrics.ClientFailures) prometheus.MustRegister(CapnpMetrics.ClientOperationsLatency) } ================================================ FILE: tunnelrpc/pogs/cloudflared_server.go ================================================ package pogs import ( capnp "zombiezen.com/go/capnproto2" "zombiezen.com/go/capnproto2/rpc" "github.com/cloudflare/cloudflared/tunnelrpc/proto" ) type CloudflaredServer interface { SessionManager ConfigurationManager } type CloudflaredServer_PogsImpl struct { SessionManager_PogsImpl ConfigurationManager_PogsImpl } func CloudflaredServer_ServerToClient(s SessionManager, c ConfigurationManager) proto.CloudflaredServer { return proto.CloudflaredServer_ServerToClient(CloudflaredServer_PogsImpl{ SessionManager_PogsImpl: SessionManager_PogsImpl{s}, ConfigurationManager_PogsImpl: ConfigurationManager_PogsImpl{c}, }) } type CloudflaredServer_PogsClient struct { SessionManager_PogsClient ConfigurationManager_PogsClient Client capnp.Client Conn *rpc.Conn } func NewCloudflaredServer_PogsClient(client capnp.Client, conn *rpc.Conn) CloudflaredServer_PogsClient { sessionManagerClient := SessionManager_PogsClient{ Client: client, Conn: conn, } configManagerClient := ConfigurationManager_PogsClient{ Client: client, Conn: conn, } return CloudflaredServer_PogsClient{ SessionManager_PogsClient: sessionManagerClient, ConfigurationManager_PogsClient: configManagerClient, Client: client, Conn: conn, } } func (c CloudflaredServer_PogsClient) Close() error { c.Client.Close() return c.Conn.Close() } ================================================ FILE: tunnelrpc/pogs/configuration_manager.go ================================================ package pogs import ( "context" "fmt" capnp "zombiezen.com/go/capnproto2" "zombiezen.com/go/capnproto2/rpc" "zombiezen.com/go/capnproto2/server" "github.com/cloudflare/cloudflared/tunnelrpc/metrics" "github.com/cloudflare/cloudflared/tunnelrpc/proto" ) type ConfigurationManager interface { // UpdateConfiguration is the call provided to cloudflared to load the latest remote configuration. UpdateConfiguration(ctx context.Context, version int32, config []byte) *UpdateConfigurationResponse } type ConfigurationManager_PogsImpl struct { impl ConfigurationManager } func ConfigurationManager_ServerToClient(c ConfigurationManager) proto.ConfigurationManager { return proto.ConfigurationManager_ServerToClient(ConfigurationManager_PogsImpl{c}) } func (i ConfigurationManager_PogsImpl) UpdateConfiguration(p proto.ConfigurationManager_updateConfiguration) error { return metrics.ObserveServerHandler(func() error { return i.updateConfiguration(p) }, metrics.ConfigurationManager, metrics.OperationUpdateConfiguration) } func (i ConfigurationManager_PogsImpl) updateConfiguration(p proto.ConfigurationManager_updateConfiguration) error { server.Ack(p.Options) version := p.Params.Version() config, err := p.Params.Config() if err != nil { return err } result, err := p.Results.NewResult() if err != nil { return err } updateResp := i.impl.UpdateConfiguration(p.Ctx, version, config) return updateResp.Marshal(result) } type ConfigurationManager_PogsClient struct { Client capnp.Client Conn *rpc.Conn } func (c ConfigurationManager_PogsClient) Close() error { c.Client.Close() return c.Conn.Close() } func (c ConfigurationManager_PogsClient) UpdateConfiguration(ctx context.Context, version int32, config []byte) (*UpdateConfigurationResponse, error) { client := proto.ConfigurationManager{Client: c.Client} promise := client.UpdateConfiguration(ctx, func(p proto.ConfigurationManager_updateConfiguration_Params) error { p.SetVersion(version) return p.SetConfig(config) }) result, err := promise.Result().Struct() if err != nil { return nil, wrapRPCError(err) } response := new(UpdateConfigurationResponse) err = response.Unmarshal(result) if err != nil { return nil, err } return response, nil } type UpdateConfigurationResponse struct { LastAppliedVersion int32 `json:"lastAppliedVersion"` Err error `json:"err"` } func (p *UpdateConfigurationResponse) Marshal(s proto.UpdateConfigurationResponse) error { s.SetLatestAppliedVersion(p.LastAppliedVersion) if p.Err != nil { return s.SetErr(p.Err.Error()) } return nil } func (p *UpdateConfigurationResponse) Unmarshal(s proto.UpdateConfigurationResponse) error { p.LastAppliedVersion = s.LatestAppliedVersion() respErr, err := s.Err() if err != nil { return err } if respErr != "" { p.Err = fmt.Errorf("%s", respErr) } return nil } ================================================ FILE: tunnelrpc/pogs/errors.go ================================================ package pogs import ( "fmt" "time" ) type RetryableError struct { err error Delay time.Duration } func (re *RetryableError) Error() string { return re.err.Error() } // RetryErrorAfter wraps err to indicate that client should retry after delay func RetryErrorAfter(err error, delay time.Duration) *RetryableError { return &RetryableError{ err: err, Delay: delay, } } func (re *RetryableError) Unwrap() error { return re.err } // RPCError is used to indicate errors returned by the RPC subsystem rather // than failure of a remote operation type RPCError struct { err error } func (re *RPCError) Error() string { return re.err.Error() } func wrapRPCError(err error) *RPCError { if err != nil { return &RPCError{ err: err, } } return nil } func newRPCError(format string, args ...interface{}) *RPCError { return &RPCError{ fmt.Errorf(format, args...), } } func (re *RPCError) Unwrap() error { return re.err } ================================================ FILE: tunnelrpc/pogs/quic_metadata_protocol.go ================================================ package pogs import ( "fmt" capnp "zombiezen.com/go/capnproto2" "zombiezen.com/go/capnproto2/pogs" "github.com/cloudflare/cloudflared/tunnelrpc/proto" ) // ConnectionType indicates the type of underlying connection proxied within the QUIC stream. type ConnectionType uint16 const ( ConnectionTypeHTTP ConnectionType = iota ConnectionTypeWebsocket ConnectionTypeTCP ) var ( // ErrorFlowConnectRateLimitedMetadata is the Metadata entry that allows to know if a request was rate limited on connect. ErrorFlowConnectRateLimitedMetadata = Metadata{Key: "FlowConnectRateLimited", Val: "true"} ) func (c ConnectionType) String() string { switch c { case ConnectionTypeHTTP: return "http" case ConnectionTypeWebsocket: return "ws" case ConnectionTypeTCP: return "tcp" } panic(fmt.Sprintf("invalid ConnectionType: %d", c)) } // ConnectRequest is the representation of metadata sent at the start of a QUIC application handshake. type ConnectRequest struct { Dest string `capnp:"dest"` Type ConnectionType `capnp:"type"` Metadata []Metadata `capnp:"metadata"` } // Metadata is a representation of key value based data sent via RequestMeta. type Metadata struct { Key string `capnp:"key"` Val string `capnp:"val"` } // MetadataMap returns a map format of []Metadata. func (r *ConnectRequest) MetadataMap() map[string]string { metadataMap := make(map[string]string) for _, metadata := range r.Metadata { metadataMap[metadata.Key] = metadata.Val } return metadataMap } func (r *ConnectRequest) FromPogs(msg *capnp.Message) error { metadata, err := proto.ReadRootConnectRequest(msg) if err != nil { return err } return pogs.Extract(r, proto.ConnectRequest_TypeID, metadata.Struct) } func (r *ConnectRequest) ToPogs() (*capnp.Message, error) { msg, seg, err := capnp.NewMessage(capnp.SingleSegment(nil)) if err != nil { return nil, err } root, err := proto.NewRootConnectRequest(seg) if err != nil { return nil, err } if err := pogs.Insert(proto.ConnectRequest_TypeID, root.Struct, r); err != nil { return nil, err } return msg, nil } // ConnectResponse is a representation of metadata sent as a response to a QUIC application handshake. type ConnectResponse struct { Error string `capnp:"error"` Metadata []Metadata `capnp:"metadata"` } func (r *ConnectResponse) FromPogs(msg *capnp.Message) error { metadata, err := proto.ReadRootConnectResponse(msg) if err != nil { return err } return pogs.Extract(r, proto.ConnectResponse_TypeID, metadata.Struct) } func (r *ConnectResponse) ToPogs() (*capnp.Message, error) { msg, seg, err := capnp.NewMessage(capnp.SingleSegment(nil)) if err != nil { return nil, err } root, err := proto.NewRootConnectResponse(seg) if err != nil { return nil, err } if err := pogs.Insert(proto.ConnectResponse_TypeID, root.Struct, r); err != nil { return nil, err } return msg, nil } ================================================ FILE: tunnelrpc/pogs/registration_server.go ================================================ package pogs import ( "context" "errors" "net" "time" "github.com/google/uuid" capnp "zombiezen.com/go/capnproto2" "zombiezen.com/go/capnproto2/pogs" "zombiezen.com/go/capnproto2/rpc" "zombiezen.com/go/capnproto2/server" "github.com/cloudflare/cloudflared/tunnelrpc/metrics" "github.com/cloudflare/cloudflared/tunnelrpc/proto" ) type RegistrationServer interface { // RegisterConnection is the call typically handled by the edge to initiate and authenticate a new connection // for cloudflared. RegisterConnection(ctx context.Context, auth TunnelAuth, tunnelID uuid.UUID, connIndex byte, options *ConnectionOptions) (*ConnectionDetails, error) // UnregisterConnection is the call typically handled by the edge to close an existing connection for cloudflared. UnregisterConnection(ctx context.Context) // UpdateLocalConfiguration is the call typically handled by the edge for cloudflared to provide the current // configuration it is operating with. UpdateLocalConfiguration(ctx context.Context, config []byte) error } type RegistrationServer_PogsImpl struct { impl RegistrationServer } func RegistrationServer_ServerToClient(s RegistrationServer) proto.RegistrationServer { return proto.RegistrationServer_ServerToClient(RegistrationServer_PogsImpl{s}) } func (i RegistrationServer_PogsImpl) RegisterConnection(p proto.RegistrationServer_registerConnection) error { return metrics.ObserveServerHandler(func() error { return i.registerConnection(p) }, metrics.Registration, metrics.OperationRegisterConnection) } func (i RegistrationServer_PogsImpl) registerConnection(p proto.RegistrationServer_registerConnection) error { server.Ack(p.Options) auth, err := p.Params.Auth() if err != nil { return err } var pogsAuth TunnelAuth err = pogsAuth.UnmarshalCapnproto(auth) if err != nil { return err } uuidBytes, err := p.Params.TunnelId() if err != nil { return err } tunnelID, err := uuid.FromBytes(uuidBytes) if err != nil { return err } connIndex := p.Params.ConnIndex() options, err := p.Params.Options() if err != nil { return err } var pogsOptions ConnectionOptions err = pogsOptions.UnmarshalCapnproto(options) if err != nil { return err } connDetails, callError := i.impl.RegisterConnection(p.Ctx, pogsAuth, tunnelID, connIndex, &pogsOptions) resp, err := p.Results.NewResult() if err != nil { return err } if callError != nil { if connError, err := resp.Result().NewError(); err != nil { return err } else { return MarshalError(connError, callError) } } if details, err := resp.Result().NewConnectionDetails(); err != nil { return err } else { return connDetails.MarshalCapnproto(details) } } func (i RegistrationServer_PogsImpl) UnregisterConnection(p proto.RegistrationServer_unregisterConnection) error { return metrics.ObserveServerHandler(func() error { server.Ack(p.Options) i.impl.UnregisterConnection(p.Ctx) return nil // No metrics will be reported for failure as this method has no return value }, metrics.Registration, metrics.OperationUnregisterConnection) } func (i RegistrationServer_PogsImpl) UpdateLocalConfiguration(p proto.RegistrationServer_updateLocalConfiguration) error { return metrics.ObserveServerHandler(func() error { return i.updateLocalConfiguration(p) }, metrics.Registration, metrics.OperationUpdateLocalConfiguration) } func (i RegistrationServer_PogsImpl) updateLocalConfiguration(c proto.RegistrationServer_updateLocalConfiguration) error { server.Ack(c.Options) configBytes, err := c.Params.Config() if err != nil { return err } return i.impl.UpdateLocalConfiguration(c.Ctx, configBytes) } type RegistrationServer_PogsClient struct { Client capnp.Client Conn *rpc.Conn } func NewRegistrationServer_PogsClient(client capnp.Client, conn *rpc.Conn) RegistrationServer_PogsClient { return RegistrationServer_PogsClient{ Client: client, Conn: conn, } } func (c RegistrationServer_PogsClient) Close() error { c.Client.Close() return c.Conn.Close() } func (c RegistrationServer_PogsClient) RegisterConnection(ctx context.Context, auth TunnelAuth, tunnelID uuid.UUID, connIndex byte, options *ConnectionOptions) (*ConnectionDetails, error) { client := proto.TunnelServer{Client: c.Client} promise := client.RegisterConnection(ctx, func(p proto.RegistrationServer_registerConnection_Params) error { tunnelAuth, err := p.NewAuth() if err != nil { return err } if err = auth.MarshalCapnproto(tunnelAuth); err != nil { return err } err = p.SetAuth(tunnelAuth) if err != nil { return err } err = p.SetTunnelId(tunnelID[:]) if err != nil { return err } p.SetConnIndex(connIndex) connectionOptions, err := p.NewOptions() if err != nil { return err } err = options.MarshalCapnproto(connectionOptions) if err != nil { return err } return nil }) response, err := promise.Result().Struct() if err != nil { return nil, wrapRPCError(err) } result := response.Result() switch result.Which() { case proto.ConnectionResponse_result_Which_error: resultError, err := result.Error() if err != nil { return nil, wrapRPCError(err) } cause, err := resultError.Cause() if err != nil { return nil, wrapRPCError(err) } err = errors.New(cause) if resultError.ShouldRetry() { err = RetryErrorAfter(err, time.Duration(resultError.RetryAfter())) } return nil, err case proto.ConnectionResponse_result_Which_connectionDetails: connDetails, err := result.ConnectionDetails() if err != nil { return nil, wrapRPCError(err) } details := new(ConnectionDetails) if err = details.UnmarshalCapnproto(connDetails); err != nil { return nil, wrapRPCError(err) } return details, nil } return nil, newRPCError("unknown result which %d", result.Which()) } func (c RegistrationServer_PogsClient) SendLocalConfiguration(ctx context.Context, config []byte) error { client := proto.TunnelServer{Client: c.Client} promise := client.UpdateLocalConfiguration(ctx, func(p proto.RegistrationServer_updateLocalConfiguration_Params) error { if err := p.SetConfig(config); err != nil { return err } return nil }) _, err := promise.Struct() if err != nil { return wrapRPCError(err) } return nil } func (c RegistrationServer_PogsClient) UnregisterConnection(ctx context.Context) error { client := proto.TunnelServer{Client: c.Client} promise := client.UnregisterConnection(ctx, func(p proto.RegistrationServer_unregisterConnection_Params) error { return nil }) _, err := promise.Struct() if err != nil { return wrapRPCError(err) } return nil } type ClientInfo struct { ClientID []byte `capnp:"clientId"` // must be a slice for capnp compatibility Features []string Version string Arch string } type ConnectionOptions struct { Client ClientInfo OriginLocalIP net.IP `capnp:"originLocalIp"` ReplaceExisting bool CompressionQuality uint8 NumPreviousAttempts uint8 } type TunnelAuth struct { AccountTag string TunnelSecret []byte } func (p *ConnectionOptions) MarshalCapnproto(s proto.ConnectionOptions) error { return pogs.Insert(proto.ConnectionOptions_TypeID, s.Struct, p) } func (p *ConnectionOptions) UnmarshalCapnproto(s proto.ConnectionOptions) error { return pogs.Extract(p, proto.ConnectionOptions_TypeID, s.Struct) } func (a *TunnelAuth) MarshalCapnproto(s proto.TunnelAuth) error { return pogs.Insert(proto.TunnelAuth_TypeID, s.Struct, a) } func (a *TunnelAuth) UnmarshalCapnproto(s proto.TunnelAuth) error { return pogs.Extract(a, proto.TunnelAuth_TypeID, s.Struct) } type ConnectionDetails struct { UUID uuid.UUID Location string TunnelIsRemotelyManaged bool } func (details *ConnectionDetails) MarshalCapnproto(s proto.ConnectionDetails) error { if err := s.SetUuid(details.UUID[:]); err != nil { return err } if err := s.SetLocationName(details.Location); err != nil { return err } s.SetTunnelIsRemotelyManaged(details.TunnelIsRemotelyManaged) return nil } func (details *ConnectionDetails) UnmarshalCapnproto(s proto.ConnectionDetails) error { uuidBytes, err := s.Uuid() if err != nil { return err } details.UUID, err = uuid.FromBytes(uuidBytes) if err != nil { return err } details.Location, err = s.LocationName() if err != nil { return err } details.TunnelIsRemotelyManaged = s.TunnelIsRemotelyManaged() return err } func MarshalError(s proto.ConnectionError, err error) error { if err := s.SetCause(err.Error()); err != nil { return err } if retryableErr, ok := err.(*RetryableError); ok { s.SetShouldRetry(true) s.SetRetryAfter(int64(retryableErr.Delay)) } return nil } ================================================ FILE: tunnelrpc/pogs/registration_server_test.go ================================================ package pogs import ( "context" "errors" "net" "testing" "time" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" capnp "zombiezen.com/go/capnproto2" "zombiezen.com/go/capnproto2/rpc" "github.com/cloudflare/cloudflared/tunnelrpc/proto" ) const testAccountTag = "abc123" func TestMarshalConnectionOptions(t *testing.T) { clientID := uuid.New() orig := ConnectionOptions{ Client: ClientInfo{ ClientID: clientID[:], Features: []string{"a", "b"}, Version: "1.2.3", Arch: "macos", }, OriginLocalIP: []byte{10, 2, 3, 4}, ReplaceExisting: false, CompressionQuality: 1, } _, seg, err := capnp.NewMessage(capnp.SingleSegment(nil)) require.NoError(t, err) capnpOpts, err := proto.NewConnectionOptions(seg) require.NoError(t, err) err = orig.MarshalCapnproto(capnpOpts) assert.NoError(t, err) var pogsOpts ConnectionOptions err = pogsOpts.UnmarshalCapnproto(capnpOpts) assert.NoError(t, err) assert.Equal(t, orig, pogsOpts) } func TestConnectionRegistrationRPC(t *testing.T) { p1, p2 := net.Pipe() t1, t2 := rpc.StreamTransport(p1), rpc.StreamTransport(p2) // Server-side testImpl := testConnectionRegistrationServer{} srv := RegistrationServer_ServerToClient(&testImpl) serverConn := rpc.NewConn(t1, rpc.MainInterface(srv.Client)) defer serverConn.Wait() ctx := context.Background() clientConn := rpc.NewConn(t2) defer clientConn.Close() client := RegistrationServer_PogsClient{ Client: clientConn.Bootstrap(ctx), Conn: clientConn, } defer client.Close() clientID := uuid.New() options := &ConnectionOptions{ Client: ClientInfo{ ClientID: clientID[:], Features: []string{"foo"}, Version: "1.2.3", Arch: "macos", }, OriginLocalIP: net.IP{10, 20, 30, 40}, ReplaceExisting: true, CompressionQuality: 0, } expectedDetails := ConnectionDetails{ UUID: uuid.New(), Location: "TEST", } testImpl.details = &expectedDetails testImpl.err = nil auth := TunnelAuth{ AccountTag: testAccountTag, TunnelSecret: []byte{1, 2, 3, 4}, } // success tunnelID := uuid.New() details, err := client.RegisterConnection(ctx, auth, tunnelID, 2, options) assert.NoError(t, err) assert.Equal(t, expectedDetails, *details) // regular error testImpl.details = nil testImpl.err = errors.New("internal") _, err = client.RegisterConnection(ctx, auth, tunnelID, 2, options) assert.EqualError(t, err, "internal") // retriable error testImpl.details = nil const delay = 27 * time.Second testImpl.err = RetryErrorAfter(errors.New("retryable"), delay) _, err = client.RegisterConnection(ctx, auth, tunnelID, 2, options) assert.EqualError(t, err, "retryable") re, ok := err.(*RetryableError) assert.True(t, ok) assert.Equal(t, delay, re.Delay) } type testConnectionRegistrationServer struct { details *ConnectionDetails err error } func (t *testConnectionRegistrationServer) UpdateLocalConfiguration(ctx context.Context, config []byte) error { // do nothing at this point return nil } func (t *testConnectionRegistrationServer) RegisterConnection(ctx context.Context, auth TunnelAuth, tunnelID uuid.UUID, connIndex byte, options *ConnectionOptions) (*ConnectionDetails, error) { if auth.AccountTag != testAccountTag { panic("bad account tag: " + auth.AccountTag) } if t.err != nil { return nil, t.err } if t.details != nil { return t.details, nil } panic("either details or err mush be set") } func (t *testConnectionRegistrationServer) UnregisterConnection(ctx context.Context) { panic("unimplemented: UnregisterConnection") } ================================================ FILE: tunnelrpc/pogs/session_manager.go ================================================ package pogs import ( "context" "fmt" "net" "time" "github.com/google/uuid" capnp "zombiezen.com/go/capnproto2" "zombiezen.com/go/capnproto2/rpc" "zombiezen.com/go/capnproto2/server" "github.com/cloudflare/cloudflared/tunnelrpc/metrics" "github.com/cloudflare/cloudflared/tunnelrpc/proto" ) type SessionManager interface { // RegisterUdpSession is the call provided to cloudflared to handle an incoming // capnproto RegisterUdpSession request from the edge. RegisterUdpSession(ctx context.Context, sessionID uuid.UUID, dstIP net.IP, dstPort uint16, closeAfterIdleHint time.Duration, traceContext string) (*RegisterUdpSessionResponse, error) // UnregisterUdpSession is the call provided to cloudflared to handle an incoming // capnproto UnregisterUdpSession request from the edge. UnregisterUdpSession(ctx context.Context, sessionID uuid.UUID, message string) error } type SessionManager_PogsImpl struct { impl SessionManager } func SessionManager_ServerToClient(s SessionManager) proto.SessionManager { return proto.SessionManager_ServerToClient(SessionManager_PogsImpl{s}) } func (i SessionManager_PogsImpl) RegisterUdpSession(p proto.SessionManager_registerUdpSession) error { return metrics.ObserveServerHandler(func() error { return i.registerUdpSession(p) }, metrics.SessionManager, metrics.OperationRegisterUdpSession) } func (i SessionManager_PogsImpl) registerUdpSession(p proto.SessionManager_registerUdpSession) error { server.Ack(p.Options) sessionIDRaw, err := p.Params.SessionId() if err != nil { return err } sessionID, err := uuid.FromBytes(sessionIDRaw) if err != nil { return err } dstIPRaw, err := p.Params.DstIp() if err != nil { return err } dstIP := net.IP(dstIPRaw) if dstIP == nil { return fmt.Errorf("%v is not valid IP", dstIPRaw) } dstPort := p.Params.DstPort() closeIdleAfterHint := time.Duration(p.Params.CloseAfterIdleHint()) traceContext, err := p.Params.TraceContext() if err != nil { return err } resp, registrationErr := i.impl.RegisterUdpSession(p.Ctx, sessionID, dstIP, dstPort, closeIdleAfterHint, traceContext) if registrationErr != nil { // Make sure to assign a response even if one is not returned from register if resp == nil { resp = &RegisterUdpSessionResponse{} } resp.Err = registrationErr } result, err := p.Results.NewResult() if err != nil { return err } return resp.Marshal(result) } func (i SessionManager_PogsImpl) UnregisterUdpSession(p proto.SessionManager_unregisterUdpSession) error { return metrics.ObserveServerHandler(func() error { return i.unregisterUdpSession(p) }, metrics.SessionManager, metrics.OperationUnregisterUdpSession) } func (i SessionManager_PogsImpl) unregisterUdpSession(p proto.SessionManager_unregisterUdpSession) error { server.Ack(p.Options) sessionIDRaw, err := p.Params.SessionId() if err != nil { return err } sessionID, err := uuid.FromBytes(sessionIDRaw) if err != nil { return err } message, err := p.Params.Message() if err != nil { return err } return i.impl.UnregisterUdpSession(p.Ctx, sessionID, message) } type RegisterUdpSessionResponse struct { Err error Spans []byte // Spans in protobuf format } func (p *RegisterUdpSessionResponse) Marshal(s proto.RegisterUdpSessionResponse) error { if p.Err != nil { return s.SetErr(p.Err.Error()) } if err := s.SetSpans(p.Spans); err != nil { return err } return nil } func (p *RegisterUdpSessionResponse) Unmarshal(s proto.RegisterUdpSessionResponse) error { respErr, err := s.Err() if err != nil { return err } if respErr != "" { p.Err = fmt.Errorf("%s", respErr) } p.Spans, err = s.Spans() if err != nil { return err } return nil } type SessionManager_PogsClient struct { Client capnp.Client Conn *rpc.Conn } func NewSessionManager_PogsClient(client capnp.Client, conn *rpc.Conn) SessionManager_PogsClient { return SessionManager_PogsClient{ Client: client, Conn: conn, } } func (c SessionManager_PogsClient) Close() error { c.Client.Close() return c.Conn.Close() } func (c SessionManager_PogsClient) RegisterUdpSession(ctx context.Context, sessionID uuid.UUID, dstIP net.IP, dstPort uint16, closeAfterIdleHint time.Duration, traceContext string) (*RegisterUdpSessionResponse, error) { client := proto.SessionManager{Client: c.Client} promise := client.RegisterUdpSession(ctx, func(p proto.SessionManager_registerUdpSession_Params) error { if err := p.SetSessionId(sessionID[:]); err != nil { return err } if err := p.SetDstIp(dstIP); err != nil { return err } p.SetDstPort(dstPort) p.SetCloseAfterIdleHint(int64(closeAfterIdleHint)) _ = p.SetTraceContext(traceContext) return nil }) result, err := promise.Result().Struct() if err != nil { return nil, wrapRPCError(err) } response := new(RegisterUdpSessionResponse) err = response.Unmarshal(result) if err != nil { return nil, err } return response, nil } func (c SessionManager_PogsClient) UnregisterUdpSession(ctx context.Context, sessionID uuid.UUID, message string) error { client := proto.SessionManager{Client: c.Client} promise := client.UnregisterUdpSession(ctx, func(p proto.SessionManager_unregisterUdpSession_Params) error { if err := p.SetSessionId(sessionID[:]); err != nil { return err } if err := p.SetMessage(message); err != nil { return err } return nil }) _, err := promise.Struct() return err } ================================================ FILE: tunnelrpc/pogs/tag.go ================================================ package pogs // Tag previously was a legacy tunnel capnp struct but was deprecated. To help reduce the amount of changes imposed // by removing this simple struct, it was copied out of the capnp and provided here instead. type Tag struct { Name string Value string } ================================================ FILE: tunnelrpc/proto/go.capnp ================================================ # Generate go.capnp.out with: # capnp compile -o- go.capnp > go.capnp.out # Must run inside this directory to preserve paths. @0xd12a1c51fedd6c88; annotation package(file) :Text; # The Go package name for the generated file. annotation import(file) :Text; # The Go import path that the generated file is accessible from. # Used to generate import statements and check if two types are in the # same package. annotation doc(struct, field, enum) :Text; # Adds a doc comment to the generated code. annotation tag(enumerant) :Text; # Changes the string representation of the enum in the generated code. annotation notag(enumerant) :Void; # Removes the string representation of the enum in the generated code. annotation customtype(field) :Text; # OBSOLETE, not used by code generator. annotation name(struct, field, union, enum, enumerant, interface, method, param, annotation, const, group) :Text; # Used to rename the element in the generated code. $package("capnp"); $import("zombiezen.com/go/capnproto2"); ================================================ FILE: tunnelrpc/proto/quic_metadata_protocol.capnp ================================================ using Go = import "go.capnp"; @0xb29021ef7421cc32; $Go.package("proto"); $Go.import("github.com/cloudflare/cloudflared/tunnelrpc"); struct ConnectRequest @0xc47116a1045e4061 { dest @0 :Text; type @1 :ConnectionType; metadata @2 :List(Metadata); } enum ConnectionType @0xc52e1bac26d379c8 { http @0; websocket @1; tcp @2; } struct Metadata @0xe1446b97bfd1cd37 { key @0 :Text; val @1 :Text; } struct ConnectResponse @0xb1032ec91cef8727 { error @0 :Text; metadata @1 :List(Metadata); } ================================================ FILE: tunnelrpc/proto/quic_metadata_protocol.capnp.go ================================================ // Code generated by capnpc-go. DO NOT EDIT. package proto import ( capnp "zombiezen.com/go/capnproto2" text "zombiezen.com/go/capnproto2/encoding/text" schemas "zombiezen.com/go/capnproto2/schemas" ) type ConnectRequest struct{ capnp.Struct } // ConnectRequest_TypeID is the unique identifier for the type ConnectRequest. const ConnectRequest_TypeID = 0xc47116a1045e4061 func NewConnectRequest(s *capnp.Segment) (ConnectRequest, error) { st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 2}) return ConnectRequest{st}, err } func NewRootConnectRequest(s *capnp.Segment) (ConnectRequest, error) { st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 2}) return ConnectRequest{st}, err } func ReadRootConnectRequest(msg *capnp.Message) (ConnectRequest, error) { root, err := msg.RootPtr() return ConnectRequest{root.Struct()}, err } func (s ConnectRequest) String() string { str, _ := text.Marshal(0xc47116a1045e4061, s.Struct) return str } func (s ConnectRequest) Dest() (string, error) { p, err := s.Struct.Ptr(0) return p.Text(), err } func (s ConnectRequest) HasDest() bool { p, err := s.Struct.Ptr(0) return p.IsValid() || err != nil } func (s ConnectRequest) DestBytes() ([]byte, error) { p, err := s.Struct.Ptr(0) return p.TextBytes(), err } func (s ConnectRequest) SetDest(v string) error { return s.Struct.SetText(0, v) } func (s ConnectRequest) Type() ConnectionType { return ConnectionType(s.Struct.Uint16(0)) } func (s ConnectRequest) SetType(v ConnectionType) { s.Struct.SetUint16(0, uint16(v)) } func (s ConnectRequest) Metadata() (Metadata_List, error) { p, err := s.Struct.Ptr(1) return Metadata_List{List: p.List()}, err } func (s ConnectRequest) HasMetadata() bool { p, err := s.Struct.Ptr(1) return p.IsValid() || err != nil } func (s ConnectRequest) SetMetadata(v Metadata_List) error { return s.Struct.SetPtr(1, v.List.ToPtr()) } // NewMetadata sets the metadata field to a newly // allocated Metadata_List, preferring placement in s's segment. func (s ConnectRequest) NewMetadata(n int32) (Metadata_List, error) { l, err := NewMetadata_List(s.Struct.Segment(), n) if err != nil { return Metadata_List{}, err } err = s.Struct.SetPtr(1, l.List.ToPtr()) return l, err } // ConnectRequest_List is a list of ConnectRequest. type ConnectRequest_List struct{ capnp.List } // NewConnectRequest creates a new list of ConnectRequest. func NewConnectRequest_List(s *capnp.Segment, sz int32) (ConnectRequest_List, error) { l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 8, PointerCount: 2}, sz) return ConnectRequest_List{l}, err } func (s ConnectRequest_List) At(i int) ConnectRequest { return ConnectRequest{s.List.Struct(i)} } func (s ConnectRequest_List) Set(i int, v ConnectRequest) error { return s.List.SetStruct(i, v.Struct) } func (s ConnectRequest_List) String() string { str, _ := text.MarshalList(0xc47116a1045e4061, s.List) return str } // ConnectRequest_Promise is a wrapper for a ConnectRequest promised by a client call. type ConnectRequest_Promise struct{ *capnp.Pipeline } func (p ConnectRequest_Promise) Struct() (ConnectRequest, error) { s, err := p.Pipeline.Struct() return ConnectRequest{s}, err } type ConnectionType uint16 // ConnectionType_TypeID is the unique identifier for the type ConnectionType. const ConnectionType_TypeID = 0xc52e1bac26d379c8 // Values of ConnectionType. const ( ConnectionType_http ConnectionType = 0 ConnectionType_websocket ConnectionType = 1 ConnectionType_tcp ConnectionType = 2 ) // String returns the enum's constant name. func (c ConnectionType) String() string { switch c { case ConnectionType_http: return "http" case ConnectionType_websocket: return "websocket" case ConnectionType_tcp: return "tcp" default: return "" } } // ConnectionTypeFromString returns the enum value with a name, // or the zero value if there's no such value. func ConnectionTypeFromString(c string) ConnectionType { switch c { case "http": return ConnectionType_http case "websocket": return ConnectionType_websocket case "tcp": return ConnectionType_tcp default: return 0 } } type ConnectionType_List struct{ capnp.List } func NewConnectionType_List(s *capnp.Segment, sz int32) (ConnectionType_List, error) { l, err := capnp.NewUInt16List(s, sz) return ConnectionType_List{l.List}, err } func (l ConnectionType_List) At(i int) ConnectionType { ul := capnp.UInt16List{List: l.List} return ConnectionType(ul.At(i)) } func (l ConnectionType_List) Set(i int, v ConnectionType) { ul := capnp.UInt16List{List: l.List} ul.Set(i, uint16(v)) } type Metadata struct{ capnp.Struct } // Metadata_TypeID is the unique identifier for the type Metadata. const Metadata_TypeID = 0xe1446b97bfd1cd37 func NewMetadata(s *capnp.Segment) (Metadata, error) { st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2}) return Metadata{st}, err } func NewRootMetadata(s *capnp.Segment) (Metadata, error) { st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2}) return Metadata{st}, err } func ReadRootMetadata(msg *capnp.Message) (Metadata, error) { root, err := msg.RootPtr() return Metadata{root.Struct()}, err } func (s Metadata) String() string { str, _ := text.Marshal(0xe1446b97bfd1cd37, s.Struct) return str } func (s Metadata) Key() (string, error) { p, err := s.Struct.Ptr(0) return p.Text(), err } func (s Metadata) HasKey() bool { p, err := s.Struct.Ptr(0) return p.IsValid() || err != nil } func (s Metadata) KeyBytes() ([]byte, error) { p, err := s.Struct.Ptr(0) return p.TextBytes(), err } func (s Metadata) SetKey(v string) error { return s.Struct.SetText(0, v) } func (s Metadata) Val() (string, error) { p, err := s.Struct.Ptr(1) return p.Text(), err } func (s Metadata) HasVal() bool { p, err := s.Struct.Ptr(1) return p.IsValid() || err != nil } func (s Metadata) ValBytes() ([]byte, error) { p, err := s.Struct.Ptr(1) return p.TextBytes(), err } func (s Metadata) SetVal(v string) error { return s.Struct.SetText(1, v) } // Metadata_List is a list of Metadata. type Metadata_List struct{ capnp.List } // NewMetadata creates a new list of Metadata. func NewMetadata_List(s *capnp.Segment, sz int32) (Metadata_List, error) { l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2}, sz) return Metadata_List{l}, err } func (s Metadata_List) At(i int) Metadata { return Metadata{s.List.Struct(i)} } func (s Metadata_List) Set(i int, v Metadata) error { return s.List.SetStruct(i, v.Struct) } func (s Metadata_List) String() string { str, _ := text.MarshalList(0xe1446b97bfd1cd37, s.List) return str } // Metadata_Promise is a wrapper for a Metadata promised by a client call. type Metadata_Promise struct{ *capnp.Pipeline } func (p Metadata_Promise) Struct() (Metadata, error) { s, err := p.Pipeline.Struct() return Metadata{s}, err } type ConnectResponse struct{ capnp.Struct } // ConnectResponse_TypeID is the unique identifier for the type ConnectResponse. const ConnectResponse_TypeID = 0xb1032ec91cef8727 func NewConnectResponse(s *capnp.Segment) (ConnectResponse, error) { st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2}) return ConnectResponse{st}, err } func NewRootConnectResponse(s *capnp.Segment) (ConnectResponse, error) { st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2}) return ConnectResponse{st}, err } func ReadRootConnectResponse(msg *capnp.Message) (ConnectResponse, error) { root, err := msg.RootPtr() return ConnectResponse{root.Struct()}, err } func (s ConnectResponse) String() string { str, _ := text.Marshal(0xb1032ec91cef8727, s.Struct) return str } func (s ConnectResponse) Error() (string, error) { p, err := s.Struct.Ptr(0) return p.Text(), err } func (s ConnectResponse) HasError() bool { p, err := s.Struct.Ptr(0) return p.IsValid() || err != nil } func (s ConnectResponse) ErrorBytes() ([]byte, error) { p, err := s.Struct.Ptr(0) return p.TextBytes(), err } func (s ConnectResponse) SetError(v string) error { return s.Struct.SetText(0, v) } func (s ConnectResponse) Metadata() (Metadata_List, error) { p, err := s.Struct.Ptr(1) return Metadata_List{List: p.List()}, err } func (s ConnectResponse) HasMetadata() bool { p, err := s.Struct.Ptr(1) return p.IsValid() || err != nil } func (s ConnectResponse) SetMetadata(v Metadata_List) error { return s.Struct.SetPtr(1, v.List.ToPtr()) } // NewMetadata sets the metadata field to a newly // allocated Metadata_List, preferring placement in s's segment. func (s ConnectResponse) NewMetadata(n int32) (Metadata_List, error) { l, err := NewMetadata_List(s.Struct.Segment(), n) if err != nil { return Metadata_List{}, err } err = s.Struct.SetPtr(1, l.List.ToPtr()) return l, err } // ConnectResponse_List is a list of ConnectResponse. type ConnectResponse_List struct{ capnp.List } // NewConnectResponse creates a new list of ConnectResponse. func NewConnectResponse_List(s *capnp.Segment, sz int32) (ConnectResponse_List, error) { l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2}, sz) return ConnectResponse_List{l}, err } func (s ConnectResponse_List) At(i int) ConnectResponse { return ConnectResponse{s.List.Struct(i)} } func (s ConnectResponse_List) Set(i int, v ConnectResponse) error { return s.List.SetStruct(i, v.Struct) } func (s ConnectResponse_List) String() string { str, _ := text.MarshalList(0xb1032ec91cef8727, s.List) return str } // ConnectResponse_Promise is a wrapper for a ConnectResponse promised by a client call. type ConnectResponse_Promise struct{ *capnp.Pipeline } func (p ConnectResponse_Promise) Struct() (ConnectResponse, error) { s, err := p.Pipeline.Struct() return ConnectResponse{s}, err } const schema_b29021ef7421cc32 = "x\xda\xb4\x911k\x14A\x1c\xc5\xdf\x9b\xcde-\x0e" + "\xf7\x86KlT\xc2\x05Q\x13\xdc\x8b\xc9\x09\xa2 \x1c" + "\x18A%\xc1\x9b`mX7\x83\x09w\xee\xce\xed\xce" + "\x19\xee\x13\xd8\xda\x89\xa5\xbd \x09X\xdb((h!" + "\x16\xd6\x0a66\xf9\x04\xb22\x0b\x9b\x83\x90B\x04\xbb" + "\xe1\xcd\x9by\xbf\xff\xff5\xbeu\xc5r\xed\x11\x01\xd5" + "\xa8M\x17\x17\x9e\x1e\x9c\xf9\xd8\xf6\xf6 C\x16+\x9f" + "Z\xf6\xa0\xf5l\x1f5\xe1\x03\xcb/~Q\xbe\xf1\x01" + "\xb9\xb7\x0b\x16Q\xf7\xc1\xd4\xcbS\xc3wP!\x8fZ" + ";-\xfe`\xf3\x06}\xa0y\x8d\xaf\xc1\xe2\xc3\xf8\xeb" + "\xf9W\xa7\xdb\xef!C11\x83\x9d\x9f\xceI\xf7\xa8" + "\xf9\x9b\xf7\xc0\xe2\xea\xe7/o\x9f\xf7W\xbf\x1fC\xd0" + "\x99\x15\xfbl\x86\xa5yA8\x08;J\x12=\xc8\xcc" + "t\xbcd\xb2\xd4\xa6K\xc3\xd1N\xbc\xf9X\xdbh+" + "\xb2\xd1f\xa9\xc5\xe9\xa0\x1dG&1\xd7o\xa6I\xa2" + "c\xbb\xa1s\x13\xa4I\xae{\xa4:\xe1M\x01S\x04" + "\xe4\xc2\x0a\xa0\xceyT\x97\x05%9C'\x86w\x01" + "u\xc9\xa3\xba-8\xa7\xb3,\xcdX\x87`\x1d,\xaa" + "\x14\x00<\x09\xf6<\xb21\xa1\x07\x9d\xf8\xaf\x80\xc3\x91" + "\xafs\xeb\xf8\xea\x87|\xb7\x16\x01\xd5\xf5\xa8\xd6\x04+" + "\xbc;N[\xf5\xa8z\x82Rp\x86\x02\x90\xeb\x8ey" + "\xcd\xa3\xda\x16\x0c\xb6tn+\xe4\xc0\x8e\x8df0)" + "\x03d\xf0_'\xd9I\x93\xfb\xfe\xd8\x94\x9b\xae\x97p" + "g\x17\xdd\x87rv\x03\xa0\x90r\x1e\x08\xb6\xad5\xc5" + "\xae~\x98\xa7q_\x83\xd6\xb7\xb19\x8c\xab\xfdU\xdc" + "\xba\xb6s\xe5\xc5\x91J\xe7\x8f\xab\xd4\x89\x17=\xaa+" + "\x82~_\x8f\xab\xed\xf8O\xa2Au\xfe\x13\x00\x00\xff" + "\xff\x1d\xce\xd1\xb0" func init() { schemas.Register(schema_b29021ef7421cc32, 0xb1032ec91cef8727, 0xc47116a1045e4061, 0xc52e1bac26d379c8, 0xe1446b97bfd1cd37) } ================================================ FILE: tunnelrpc/proto/tunnelrpc.capnp ================================================ using Go = import "go.capnp"; @0xdb8274f9144abc7e; $Go.package("proto"); $Go.import("github.com/cloudflare/cloudflared/tunnelrpc"); # === DEPRECATED Legacy Tunnel Authentication and Registration methods/servers === # # These structs and interfaces are no longer used but it is important to keep # them around to make sure backwards compatibility within the rpc protocol is # maintained. struct Authentication @0xc082ef6e0d42ed1d { # DEPRECATED: Legacy tunnel authentication mechanism key @0 :Text; email @1 :Text; originCAKey @2 :Text; } struct TunnelRegistration @0xf41a0f001ad49e46 { # DEPRECATED: Legacy tunnel authentication mechanism err @0 :Text; # the url to access the tunnel url @1 :Text; # Used to inform the client of actions taken. logLines @2 :List(Text); # In case of error, whether the client should attempt to reconnect. permanentFailure @3 :Bool; # Displayed to user tunnelID @4 :Text; # How long should this connection wait to retry in seconds, if the error wasn't permanent retryAfterSeconds @5 :UInt16; # A unique ID used to reconnect this tunnel. eventDigest @6 :Data; # A unique ID used to prove this tunnel was previously connected to a given metal. connDigest @7 :Data; } struct RegistrationOptions @0xc793e50592935b4a { # DEPRECATED: Legacy tunnel authentication mechanism # The tunnel client's unique identifier, used to verify a reconnection. clientId @0 :Text; # Information about the running binary. version @1 :Text; os @2 :Text; # What to do with existing tunnels for the given hostname. existingTunnelPolicy @3 :ExistingTunnelPolicy; # If using the balancing policy, identifies the LB pool to use. poolName @4 :Text; # Client-defined tags to associate with the tunnel tags @5 :List(Tag); # A unique identifier for a high-availability connection made by a single client. connectionId @6 :UInt8; # origin LAN IP originLocalIp @7 :Text; # whether Argo Tunnel client has been autoupdated isAutoupdated @8 :Bool; # whether Argo Tunnel client is run from a terminal runFromTerminal @9 :Bool; # cross stream compression setting, 0 - off, 3 - high compressionQuality @10 :UInt64; uuid @11 :Text; # number of previous attempts to send RegisterTunnel/ReconnectTunnel numPreviousAttempts @12 :UInt8; # Set of features this cloudflared knows it supports features @13 :List(Text); } enum ExistingTunnelPolicy @0x84cb9536a2cf6d3c { # DEPRECATED: Legacy tunnel registration mechanism ignore @0; disconnect @1; balance @2; } struct ServerInfo @0xf2c68e2547ec3866 { # DEPRECATED: Legacy tunnel registration mechanism locationName @0 :Text; } struct AuthenticateResponse @0x82c325a07ad22a65 { # DEPRECATED: Legacy tunnel registration mechanism permanentErr @0 :Text; retryableErr @1 :Text; jwt @2 :Data; hoursUntilRefresh @3 :UInt8; } interface TunnelServer @0xea58385c65416035 extends (RegistrationServer) { # DEPRECATED: Legacy tunnel authentication server registerTunnel @0 (originCert :Data, hostname :Text, options :RegistrationOptions) -> (result :TunnelRegistration); getServerInfo @1 () -> (result :ServerInfo); unregisterTunnel @2 (gracePeriodNanoSec :Int64) -> (); # obsoleteDeclarativeTunnelConnect RPC deprecated in TUN-3019 obsoleteDeclarativeTunnelConnect @3 () -> (); authenticate @4 (originCert :Data, hostname :Text, options :RegistrationOptions) -> (result :AuthenticateResponse); reconnectTunnel @5 (jwt :Data, eventDigest :Data, connDigest :Data, hostname :Text, options :RegistrationOptions) -> (result :TunnelRegistration); } struct Tag @0xcbd96442ae3bb01a { # DEPRECATED: Legacy tunnel additional HTTP header mechanism name @0 :Text; value @1 :Text; } # === End DEPRECATED Objects === struct ClientInfo @0x83ced0145b2f114b { # The tunnel client's unique identifier, used to verify a reconnection. clientId @0 :Data; # Set of features this cloudflared knows it supports features @1 :List(Text); # Information about the running binary. version @2 :Text; # Client OS and CPU info arch @3 :Text; } struct ConnectionOptions @0xb4bf9861fe035d04 { # client details client @0 :ClientInfo; # origin LAN IP originLocalIp @1 :Data; # What to do if connection already exists replaceExisting @2 :Bool; # cross stream compression setting, 0 - off, 3 - high compressionQuality @3 :UInt8; # number of previous attempts to send RegisterConnection numPreviousAttempts @4 :UInt8; } struct ConnectionResponse @0xdbaa9d03d52b62dc { result :union { error @0 :ConnectionError; connectionDetails @1 :ConnectionDetails; } } struct ConnectionError @0xf5f383d2785edb86 { cause @0 :Text; # How long should this connection wait to retry in ns retryAfter @1 :Int64; shouldRetry @2 :Bool; } struct ConnectionDetails @0xb5f39f082b9ac18a { # identifier of this connection uuid @0 :Data; # airport code of the colo where this connection landed locationName @1 :Text; # tells if the tunnel is remotely managed tunnelIsRemotelyManaged @2: Bool; } struct TunnelAuth @0x9496331ab9cd463f { accountTag @0 :Text; tunnelSecret @1 :Data; } interface RegistrationServer @0xf71695ec7fe85497 { registerConnection @0 (auth :TunnelAuth, tunnelId :Data, connIndex :UInt8, options :ConnectionOptions) -> (result :ConnectionResponse); unregisterConnection @1 () -> (); updateLocalConfiguration @2 (config :Data) -> (); } struct RegisterUdpSessionResponse @0xab6d5210c1f26687 { err @0 :Text; spans @1 :Data; } interface SessionManager @0x839445a59fb01686 { # Let the edge decide closeAfterIdle to make sure cloudflared doesn't close session before the edge closes its side registerUdpSession @0 (sessionId :Data, dstIp :Data, dstPort :UInt16, closeAfterIdleHint :Int64, traceContext :Text = "") -> (result :RegisterUdpSessionResponse); unregisterUdpSession @1 (sessionId :Data, message :Text) -> (); } struct UpdateConfigurationResponse @0xdb58ff694ba05cf9 { # Latest configuration that was applied successfully. The err field might be populated at the same time to indicate # that cloudflared is using an older configuration because the latest cannot be applied latestAppliedVersion @0 :Int32; # Any error encountered when trying to apply the last configuration err @1 :Text; } # ConfigurationManager defines RPC to manage cloudflared configuration remotely interface ConfigurationManager @0xb48edfbdaa25db04 { updateConfiguration @0 (version :Int32, config :Data) -> (result: UpdateConfigurationResponse); } interface CloudflaredServer @0xf548cef9dea2a4a1 extends(SessionManager, ConfigurationManager) {} ================================================ FILE: tunnelrpc/proto/tunnelrpc.capnp.go ================================================ // Code generated by capnpc-go. DO NOT EDIT. package proto import ( strconv "strconv" context "golang.org/x/net/context" capnp "zombiezen.com/go/capnproto2" text "zombiezen.com/go/capnproto2/encoding/text" schemas "zombiezen.com/go/capnproto2/schemas" server "zombiezen.com/go/capnproto2/server" ) type Authentication struct{ capnp.Struct } // Authentication_TypeID is the unique identifier for the type Authentication. const Authentication_TypeID = 0xc082ef6e0d42ed1d func NewAuthentication(s *capnp.Segment) (Authentication, error) { st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 3}) return Authentication{st}, err } func NewRootAuthentication(s *capnp.Segment) (Authentication, error) { st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 3}) return Authentication{st}, err } func ReadRootAuthentication(msg *capnp.Message) (Authentication, error) { root, err := msg.RootPtr() return Authentication{root.Struct()}, err } func (s Authentication) String() string { str, _ := text.Marshal(0xc082ef6e0d42ed1d, s.Struct) return str } func (s Authentication) Key() (string, error) { p, err := s.Struct.Ptr(0) return p.Text(), err } func (s Authentication) HasKey() bool { p, err := s.Struct.Ptr(0) return p.IsValid() || err != nil } func (s Authentication) KeyBytes() ([]byte, error) { p, err := s.Struct.Ptr(0) return p.TextBytes(), err } func (s Authentication) SetKey(v string) error { return s.Struct.SetText(0, v) } func (s Authentication) Email() (string, error) { p, err := s.Struct.Ptr(1) return p.Text(), err } func (s Authentication) HasEmail() bool { p, err := s.Struct.Ptr(1) return p.IsValid() || err != nil } func (s Authentication) EmailBytes() ([]byte, error) { p, err := s.Struct.Ptr(1) return p.TextBytes(), err } func (s Authentication) SetEmail(v string) error { return s.Struct.SetText(1, v) } func (s Authentication) OriginCAKey() (string, error) { p, err := s.Struct.Ptr(2) return p.Text(), err } func (s Authentication) HasOriginCAKey() bool { p, err := s.Struct.Ptr(2) return p.IsValid() || err != nil } func (s Authentication) OriginCAKeyBytes() ([]byte, error) { p, err := s.Struct.Ptr(2) return p.TextBytes(), err } func (s Authentication) SetOriginCAKey(v string) error { return s.Struct.SetText(2, v) } // Authentication_List is a list of Authentication. type Authentication_List struct{ capnp.List } // NewAuthentication creates a new list of Authentication. func NewAuthentication_List(s *capnp.Segment, sz int32) (Authentication_List, error) { l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 3}, sz) return Authentication_List{l}, err } func (s Authentication_List) At(i int) Authentication { return Authentication{s.List.Struct(i)} } func (s Authentication_List) Set(i int, v Authentication) error { return s.List.SetStruct(i, v.Struct) } func (s Authentication_List) String() string { str, _ := text.MarshalList(0xc082ef6e0d42ed1d, s.List) return str } // Authentication_Promise is a wrapper for a Authentication promised by a client call. type Authentication_Promise struct{ *capnp.Pipeline } func (p Authentication_Promise) Struct() (Authentication, error) { s, err := p.Pipeline.Struct() return Authentication{s}, err } type TunnelRegistration struct{ capnp.Struct } // TunnelRegistration_TypeID is the unique identifier for the type TunnelRegistration. const TunnelRegistration_TypeID = 0xf41a0f001ad49e46 func NewTunnelRegistration(s *capnp.Segment) (TunnelRegistration, error) { st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 6}) return TunnelRegistration{st}, err } func NewRootTunnelRegistration(s *capnp.Segment) (TunnelRegistration, error) { st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 6}) return TunnelRegistration{st}, err } func ReadRootTunnelRegistration(msg *capnp.Message) (TunnelRegistration, error) { root, err := msg.RootPtr() return TunnelRegistration{root.Struct()}, err } func (s TunnelRegistration) String() string { str, _ := text.Marshal(0xf41a0f001ad49e46, s.Struct) return str } func (s TunnelRegistration) Err() (string, error) { p, err := s.Struct.Ptr(0) return p.Text(), err } func (s TunnelRegistration) HasErr() bool { p, err := s.Struct.Ptr(0) return p.IsValid() || err != nil } func (s TunnelRegistration) ErrBytes() ([]byte, error) { p, err := s.Struct.Ptr(0) return p.TextBytes(), err } func (s TunnelRegistration) SetErr(v string) error { return s.Struct.SetText(0, v) } func (s TunnelRegistration) Url() (string, error) { p, err := s.Struct.Ptr(1) return p.Text(), err } func (s TunnelRegistration) HasUrl() bool { p, err := s.Struct.Ptr(1) return p.IsValid() || err != nil } func (s TunnelRegistration) UrlBytes() ([]byte, error) { p, err := s.Struct.Ptr(1) return p.TextBytes(), err } func (s TunnelRegistration) SetUrl(v string) error { return s.Struct.SetText(1, v) } func (s TunnelRegistration) LogLines() (capnp.TextList, error) { p, err := s.Struct.Ptr(2) return capnp.TextList{List: p.List()}, err } func (s TunnelRegistration) HasLogLines() bool { p, err := s.Struct.Ptr(2) return p.IsValid() || err != nil } func (s TunnelRegistration) SetLogLines(v capnp.TextList) error { return s.Struct.SetPtr(2, v.List.ToPtr()) } // NewLogLines sets the logLines field to a newly // allocated capnp.TextList, preferring placement in s's segment. func (s TunnelRegistration) NewLogLines(n int32) (capnp.TextList, error) { l, err := capnp.NewTextList(s.Struct.Segment(), n) if err != nil { return capnp.TextList{}, err } err = s.Struct.SetPtr(2, l.List.ToPtr()) return l, err } func (s TunnelRegistration) PermanentFailure() bool { return s.Struct.Bit(0) } func (s TunnelRegistration) SetPermanentFailure(v bool) { s.Struct.SetBit(0, v) } func (s TunnelRegistration) TunnelID() (string, error) { p, err := s.Struct.Ptr(3) return p.Text(), err } func (s TunnelRegistration) HasTunnelID() bool { p, err := s.Struct.Ptr(3) return p.IsValid() || err != nil } func (s TunnelRegistration) TunnelIDBytes() ([]byte, error) { p, err := s.Struct.Ptr(3) return p.TextBytes(), err } func (s TunnelRegistration) SetTunnelID(v string) error { return s.Struct.SetText(3, v) } func (s TunnelRegistration) RetryAfterSeconds() uint16 { return s.Struct.Uint16(2) } func (s TunnelRegistration) SetRetryAfterSeconds(v uint16) { s.Struct.SetUint16(2, v) } func (s TunnelRegistration) EventDigest() ([]byte, error) { p, err := s.Struct.Ptr(4) return []byte(p.Data()), err } func (s TunnelRegistration) HasEventDigest() bool { p, err := s.Struct.Ptr(4) return p.IsValid() || err != nil } func (s TunnelRegistration) SetEventDigest(v []byte) error { return s.Struct.SetData(4, v) } func (s TunnelRegistration) ConnDigest() ([]byte, error) { p, err := s.Struct.Ptr(5) return []byte(p.Data()), err } func (s TunnelRegistration) HasConnDigest() bool { p, err := s.Struct.Ptr(5) return p.IsValid() || err != nil } func (s TunnelRegistration) SetConnDigest(v []byte) error { return s.Struct.SetData(5, v) } // TunnelRegistration_List is a list of TunnelRegistration. type TunnelRegistration_List struct{ capnp.List } // NewTunnelRegistration creates a new list of TunnelRegistration. func NewTunnelRegistration_List(s *capnp.Segment, sz int32) (TunnelRegistration_List, error) { l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 8, PointerCount: 6}, sz) return TunnelRegistration_List{l}, err } func (s TunnelRegistration_List) At(i int) TunnelRegistration { return TunnelRegistration{s.List.Struct(i)} } func (s TunnelRegistration_List) Set(i int, v TunnelRegistration) error { return s.List.SetStruct(i, v.Struct) } func (s TunnelRegistration_List) String() string { str, _ := text.MarshalList(0xf41a0f001ad49e46, s.List) return str } // TunnelRegistration_Promise is a wrapper for a TunnelRegistration promised by a client call. type TunnelRegistration_Promise struct{ *capnp.Pipeline } func (p TunnelRegistration_Promise) Struct() (TunnelRegistration, error) { s, err := p.Pipeline.Struct() return TunnelRegistration{s}, err } type RegistrationOptions struct{ capnp.Struct } // RegistrationOptions_TypeID is the unique identifier for the type RegistrationOptions. const RegistrationOptions_TypeID = 0xc793e50592935b4a func NewRegistrationOptions(s *capnp.Segment) (RegistrationOptions, error) { st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 16, PointerCount: 8}) return RegistrationOptions{st}, err } func NewRootRegistrationOptions(s *capnp.Segment) (RegistrationOptions, error) { st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 16, PointerCount: 8}) return RegistrationOptions{st}, err } func ReadRootRegistrationOptions(msg *capnp.Message) (RegistrationOptions, error) { root, err := msg.RootPtr() return RegistrationOptions{root.Struct()}, err } func (s RegistrationOptions) String() string { str, _ := text.Marshal(0xc793e50592935b4a, s.Struct) return str } func (s RegistrationOptions) ClientId() (string, error) { p, err := s.Struct.Ptr(0) return p.Text(), err } func (s RegistrationOptions) HasClientId() bool { p, err := s.Struct.Ptr(0) return p.IsValid() || err != nil } func (s RegistrationOptions) ClientIdBytes() ([]byte, error) { p, err := s.Struct.Ptr(0) return p.TextBytes(), err } func (s RegistrationOptions) SetClientId(v string) error { return s.Struct.SetText(0, v) } func (s RegistrationOptions) Version() (string, error) { p, err := s.Struct.Ptr(1) return p.Text(), err } func (s RegistrationOptions) HasVersion() bool { p, err := s.Struct.Ptr(1) return p.IsValid() || err != nil } func (s RegistrationOptions) VersionBytes() ([]byte, error) { p, err := s.Struct.Ptr(1) return p.TextBytes(), err } func (s RegistrationOptions) SetVersion(v string) error { return s.Struct.SetText(1, v) } func (s RegistrationOptions) Os() (string, error) { p, err := s.Struct.Ptr(2) return p.Text(), err } func (s RegistrationOptions) HasOs() bool { p, err := s.Struct.Ptr(2) return p.IsValid() || err != nil } func (s RegistrationOptions) OsBytes() ([]byte, error) { p, err := s.Struct.Ptr(2) return p.TextBytes(), err } func (s RegistrationOptions) SetOs(v string) error { return s.Struct.SetText(2, v) } func (s RegistrationOptions) ExistingTunnelPolicy() ExistingTunnelPolicy { return ExistingTunnelPolicy(s.Struct.Uint16(0)) } func (s RegistrationOptions) SetExistingTunnelPolicy(v ExistingTunnelPolicy) { s.Struct.SetUint16(0, uint16(v)) } func (s RegistrationOptions) PoolName() (string, error) { p, err := s.Struct.Ptr(3) return p.Text(), err } func (s RegistrationOptions) HasPoolName() bool { p, err := s.Struct.Ptr(3) return p.IsValid() || err != nil } func (s RegistrationOptions) PoolNameBytes() ([]byte, error) { p, err := s.Struct.Ptr(3) return p.TextBytes(), err } func (s RegistrationOptions) SetPoolName(v string) error { return s.Struct.SetText(3, v) } func (s RegistrationOptions) Tags() (Tag_List, error) { p, err := s.Struct.Ptr(4) return Tag_List{List: p.List()}, err } func (s RegistrationOptions) HasTags() bool { p, err := s.Struct.Ptr(4) return p.IsValid() || err != nil } func (s RegistrationOptions) SetTags(v Tag_List) error { return s.Struct.SetPtr(4, v.List.ToPtr()) } // NewTags sets the tags field to a newly // allocated Tag_List, preferring placement in s's segment. func (s RegistrationOptions) NewTags(n int32) (Tag_List, error) { l, err := NewTag_List(s.Struct.Segment(), n) if err != nil { return Tag_List{}, err } err = s.Struct.SetPtr(4, l.List.ToPtr()) return l, err } func (s RegistrationOptions) ConnectionId() uint8 { return s.Struct.Uint8(2) } func (s RegistrationOptions) SetConnectionId(v uint8) { s.Struct.SetUint8(2, v) } func (s RegistrationOptions) OriginLocalIp() (string, error) { p, err := s.Struct.Ptr(5) return p.Text(), err } func (s RegistrationOptions) HasOriginLocalIp() bool { p, err := s.Struct.Ptr(5) return p.IsValid() || err != nil } func (s RegistrationOptions) OriginLocalIpBytes() ([]byte, error) { p, err := s.Struct.Ptr(5) return p.TextBytes(), err } func (s RegistrationOptions) SetOriginLocalIp(v string) error { return s.Struct.SetText(5, v) } func (s RegistrationOptions) IsAutoupdated() bool { return s.Struct.Bit(24) } func (s RegistrationOptions) SetIsAutoupdated(v bool) { s.Struct.SetBit(24, v) } func (s RegistrationOptions) RunFromTerminal() bool { return s.Struct.Bit(25) } func (s RegistrationOptions) SetRunFromTerminal(v bool) { s.Struct.SetBit(25, v) } func (s RegistrationOptions) CompressionQuality() uint64 { return s.Struct.Uint64(8) } func (s RegistrationOptions) SetCompressionQuality(v uint64) { s.Struct.SetUint64(8, v) } func (s RegistrationOptions) Uuid() (string, error) { p, err := s.Struct.Ptr(6) return p.Text(), err } func (s RegistrationOptions) HasUuid() bool { p, err := s.Struct.Ptr(6) return p.IsValid() || err != nil } func (s RegistrationOptions) UuidBytes() ([]byte, error) { p, err := s.Struct.Ptr(6) return p.TextBytes(), err } func (s RegistrationOptions) SetUuid(v string) error { return s.Struct.SetText(6, v) } func (s RegistrationOptions) NumPreviousAttempts() uint8 { return s.Struct.Uint8(4) } func (s RegistrationOptions) SetNumPreviousAttempts(v uint8) { s.Struct.SetUint8(4, v) } func (s RegistrationOptions) Features() (capnp.TextList, error) { p, err := s.Struct.Ptr(7) return capnp.TextList{List: p.List()}, err } func (s RegistrationOptions) HasFeatures() bool { p, err := s.Struct.Ptr(7) return p.IsValid() || err != nil } func (s RegistrationOptions) SetFeatures(v capnp.TextList) error { return s.Struct.SetPtr(7, v.List.ToPtr()) } // NewFeatures sets the features field to a newly // allocated capnp.TextList, preferring placement in s's segment. func (s RegistrationOptions) NewFeatures(n int32) (capnp.TextList, error) { l, err := capnp.NewTextList(s.Struct.Segment(), n) if err != nil { return capnp.TextList{}, err } err = s.Struct.SetPtr(7, l.List.ToPtr()) return l, err } // RegistrationOptions_List is a list of RegistrationOptions. type RegistrationOptions_List struct{ capnp.List } // NewRegistrationOptions creates a new list of RegistrationOptions. func NewRegistrationOptions_List(s *capnp.Segment, sz int32) (RegistrationOptions_List, error) { l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 16, PointerCount: 8}, sz) return RegistrationOptions_List{l}, err } func (s RegistrationOptions_List) At(i int) RegistrationOptions { return RegistrationOptions{s.List.Struct(i)} } func (s RegistrationOptions_List) Set(i int, v RegistrationOptions) error { return s.List.SetStruct(i, v.Struct) } func (s RegistrationOptions_List) String() string { str, _ := text.MarshalList(0xc793e50592935b4a, s.List) return str } // RegistrationOptions_Promise is a wrapper for a RegistrationOptions promised by a client call. type RegistrationOptions_Promise struct{ *capnp.Pipeline } func (p RegistrationOptions_Promise) Struct() (RegistrationOptions, error) { s, err := p.Pipeline.Struct() return RegistrationOptions{s}, err } type ExistingTunnelPolicy uint16 // ExistingTunnelPolicy_TypeID is the unique identifier for the type ExistingTunnelPolicy. const ExistingTunnelPolicy_TypeID = 0x84cb9536a2cf6d3c // Values of ExistingTunnelPolicy. const ( ExistingTunnelPolicy_ignore ExistingTunnelPolicy = 0 ExistingTunnelPolicy_disconnect ExistingTunnelPolicy = 1 ExistingTunnelPolicy_balance ExistingTunnelPolicy = 2 ) // String returns the enum's constant name. func (c ExistingTunnelPolicy) String() string { switch c { case ExistingTunnelPolicy_ignore: return "ignore" case ExistingTunnelPolicy_disconnect: return "disconnect" case ExistingTunnelPolicy_balance: return "balance" default: return "" } } // ExistingTunnelPolicyFromString returns the enum value with a name, // or the zero value if there's no such value. func ExistingTunnelPolicyFromString(c string) ExistingTunnelPolicy { switch c { case "ignore": return ExistingTunnelPolicy_ignore case "disconnect": return ExistingTunnelPolicy_disconnect case "balance": return ExistingTunnelPolicy_balance default: return 0 } } type ExistingTunnelPolicy_List struct{ capnp.List } func NewExistingTunnelPolicy_List(s *capnp.Segment, sz int32) (ExistingTunnelPolicy_List, error) { l, err := capnp.NewUInt16List(s, sz) return ExistingTunnelPolicy_List{l.List}, err } func (l ExistingTunnelPolicy_List) At(i int) ExistingTunnelPolicy { ul := capnp.UInt16List{List: l.List} return ExistingTunnelPolicy(ul.At(i)) } func (l ExistingTunnelPolicy_List) Set(i int, v ExistingTunnelPolicy) { ul := capnp.UInt16List{List: l.List} ul.Set(i, uint16(v)) } type ServerInfo struct{ capnp.Struct } // ServerInfo_TypeID is the unique identifier for the type ServerInfo. const ServerInfo_TypeID = 0xf2c68e2547ec3866 func NewServerInfo(s *capnp.Segment) (ServerInfo, error) { st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) return ServerInfo{st}, err } func NewRootServerInfo(s *capnp.Segment) (ServerInfo, error) { st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) return ServerInfo{st}, err } func ReadRootServerInfo(msg *capnp.Message) (ServerInfo, error) { root, err := msg.RootPtr() return ServerInfo{root.Struct()}, err } func (s ServerInfo) String() string { str, _ := text.Marshal(0xf2c68e2547ec3866, s.Struct) return str } func (s ServerInfo) LocationName() (string, error) { p, err := s.Struct.Ptr(0) return p.Text(), err } func (s ServerInfo) HasLocationName() bool { p, err := s.Struct.Ptr(0) return p.IsValid() || err != nil } func (s ServerInfo) LocationNameBytes() ([]byte, error) { p, err := s.Struct.Ptr(0) return p.TextBytes(), err } func (s ServerInfo) SetLocationName(v string) error { return s.Struct.SetText(0, v) } // ServerInfo_List is a list of ServerInfo. type ServerInfo_List struct{ capnp.List } // NewServerInfo creates a new list of ServerInfo. func NewServerInfo_List(s *capnp.Segment, sz int32) (ServerInfo_List, error) { l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}, sz) return ServerInfo_List{l}, err } func (s ServerInfo_List) At(i int) ServerInfo { return ServerInfo{s.List.Struct(i)} } func (s ServerInfo_List) Set(i int, v ServerInfo) error { return s.List.SetStruct(i, v.Struct) } func (s ServerInfo_List) String() string { str, _ := text.MarshalList(0xf2c68e2547ec3866, s.List) return str } // ServerInfo_Promise is a wrapper for a ServerInfo promised by a client call. type ServerInfo_Promise struct{ *capnp.Pipeline } func (p ServerInfo_Promise) Struct() (ServerInfo, error) { s, err := p.Pipeline.Struct() return ServerInfo{s}, err } type AuthenticateResponse struct{ capnp.Struct } // AuthenticateResponse_TypeID is the unique identifier for the type AuthenticateResponse. const AuthenticateResponse_TypeID = 0x82c325a07ad22a65 func NewAuthenticateResponse(s *capnp.Segment) (AuthenticateResponse, error) { st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 3}) return AuthenticateResponse{st}, err } func NewRootAuthenticateResponse(s *capnp.Segment) (AuthenticateResponse, error) { st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 3}) return AuthenticateResponse{st}, err } func ReadRootAuthenticateResponse(msg *capnp.Message) (AuthenticateResponse, error) { root, err := msg.RootPtr() return AuthenticateResponse{root.Struct()}, err } func (s AuthenticateResponse) String() string { str, _ := text.Marshal(0x82c325a07ad22a65, s.Struct) return str } func (s AuthenticateResponse) PermanentErr() (string, error) { p, err := s.Struct.Ptr(0) return p.Text(), err } func (s AuthenticateResponse) HasPermanentErr() bool { p, err := s.Struct.Ptr(0) return p.IsValid() || err != nil } func (s AuthenticateResponse) PermanentErrBytes() ([]byte, error) { p, err := s.Struct.Ptr(0) return p.TextBytes(), err } func (s AuthenticateResponse) SetPermanentErr(v string) error { return s.Struct.SetText(0, v) } func (s AuthenticateResponse) RetryableErr() (string, error) { p, err := s.Struct.Ptr(1) return p.Text(), err } func (s AuthenticateResponse) HasRetryableErr() bool { p, err := s.Struct.Ptr(1) return p.IsValid() || err != nil } func (s AuthenticateResponse) RetryableErrBytes() ([]byte, error) { p, err := s.Struct.Ptr(1) return p.TextBytes(), err } func (s AuthenticateResponse) SetRetryableErr(v string) error { return s.Struct.SetText(1, v) } func (s AuthenticateResponse) Jwt() ([]byte, error) { p, err := s.Struct.Ptr(2) return []byte(p.Data()), err } func (s AuthenticateResponse) HasJwt() bool { p, err := s.Struct.Ptr(2) return p.IsValid() || err != nil } func (s AuthenticateResponse) SetJwt(v []byte) error { return s.Struct.SetData(2, v) } func (s AuthenticateResponse) HoursUntilRefresh() uint8 { return s.Struct.Uint8(0) } func (s AuthenticateResponse) SetHoursUntilRefresh(v uint8) { s.Struct.SetUint8(0, v) } // AuthenticateResponse_List is a list of AuthenticateResponse. type AuthenticateResponse_List struct{ capnp.List } // NewAuthenticateResponse creates a new list of AuthenticateResponse. func NewAuthenticateResponse_List(s *capnp.Segment, sz int32) (AuthenticateResponse_List, error) { l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 8, PointerCount: 3}, sz) return AuthenticateResponse_List{l}, err } func (s AuthenticateResponse_List) At(i int) AuthenticateResponse { return AuthenticateResponse{s.List.Struct(i)} } func (s AuthenticateResponse_List) Set(i int, v AuthenticateResponse) error { return s.List.SetStruct(i, v.Struct) } func (s AuthenticateResponse_List) String() string { str, _ := text.MarshalList(0x82c325a07ad22a65, s.List) return str } // AuthenticateResponse_Promise is a wrapper for a AuthenticateResponse promised by a client call. type AuthenticateResponse_Promise struct{ *capnp.Pipeline } func (p AuthenticateResponse_Promise) Struct() (AuthenticateResponse, error) { s, err := p.Pipeline.Struct() return AuthenticateResponse{s}, err } type TunnelServer struct{ Client capnp.Client } // TunnelServer_TypeID is the unique identifier for the type TunnelServer. const TunnelServer_TypeID = 0xea58385c65416035 func (c TunnelServer) RegisterTunnel(ctx context.Context, params func(TunnelServer_registerTunnel_Params) error, opts ...capnp.CallOption) TunnelServer_registerTunnel_Results_Promise { if c.Client == nil { return TunnelServer_registerTunnel_Results_Promise{Pipeline: capnp.NewPipeline(capnp.ErrorAnswer(capnp.ErrNullClient))} } call := &capnp.Call{ Ctx: ctx, Method: capnp.Method{ InterfaceID: 0xea58385c65416035, MethodID: 0, InterfaceName: "tunnelrpc/proto/tunnelrpc.capnp:TunnelServer", MethodName: "registerTunnel", }, Options: capnp.NewCallOptions(opts), } if params != nil { call.ParamsSize = capnp.ObjectSize{DataSize: 0, PointerCount: 3} call.ParamsFunc = func(s capnp.Struct) error { return params(TunnelServer_registerTunnel_Params{Struct: s}) } } return TunnelServer_registerTunnel_Results_Promise{Pipeline: capnp.NewPipeline(c.Client.Call(call))} } func (c TunnelServer) GetServerInfo(ctx context.Context, params func(TunnelServer_getServerInfo_Params) error, opts ...capnp.CallOption) TunnelServer_getServerInfo_Results_Promise { if c.Client == nil { return TunnelServer_getServerInfo_Results_Promise{Pipeline: capnp.NewPipeline(capnp.ErrorAnswer(capnp.ErrNullClient))} } call := &capnp.Call{ Ctx: ctx, Method: capnp.Method{ InterfaceID: 0xea58385c65416035, MethodID: 1, InterfaceName: "tunnelrpc/proto/tunnelrpc.capnp:TunnelServer", MethodName: "getServerInfo", }, Options: capnp.NewCallOptions(opts), } if params != nil { call.ParamsSize = capnp.ObjectSize{DataSize: 0, PointerCount: 0} call.ParamsFunc = func(s capnp.Struct) error { return params(TunnelServer_getServerInfo_Params{Struct: s}) } } return TunnelServer_getServerInfo_Results_Promise{Pipeline: capnp.NewPipeline(c.Client.Call(call))} } func (c TunnelServer) UnregisterTunnel(ctx context.Context, params func(TunnelServer_unregisterTunnel_Params) error, opts ...capnp.CallOption) TunnelServer_unregisterTunnel_Results_Promise { if c.Client == nil { return TunnelServer_unregisterTunnel_Results_Promise{Pipeline: capnp.NewPipeline(capnp.ErrorAnswer(capnp.ErrNullClient))} } call := &capnp.Call{ Ctx: ctx, Method: capnp.Method{ InterfaceID: 0xea58385c65416035, MethodID: 2, InterfaceName: "tunnelrpc/proto/tunnelrpc.capnp:TunnelServer", MethodName: "unregisterTunnel", }, Options: capnp.NewCallOptions(opts), } if params != nil { call.ParamsSize = capnp.ObjectSize{DataSize: 8, PointerCount: 0} call.ParamsFunc = func(s capnp.Struct) error { return params(TunnelServer_unregisterTunnel_Params{Struct: s}) } } return TunnelServer_unregisterTunnel_Results_Promise{Pipeline: capnp.NewPipeline(c.Client.Call(call))} } func (c TunnelServer) ObsoleteDeclarativeTunnelConnect(ctx context.Context, params func(TunnelServer_obsoleteDeclarativeTunnelConnect_Params) error, opts ...capnp.CallOption) TunnelServer_obsoleteDeclarativeTunnelConnect_Results_Promise { if c.Client == nil { return TunnelServer_obsoleteDeclarativeTunnelConnect_Results_Promise{Pipeline: capnp.NewPipeline(capnp.ErrorAnswer(capnp.ErrNullClient))} } call := &capnp.Call{ Ctx: ctx, Method: capnp.Method{ InterfaceID: 0xea58385c65416035, MethodID: 3, InterfaceName: "tunnelrpc/proto/tunnelrpc.capnp:TunnelServer", MethodName: "obsoleteDeclarativeTunnelConnect", }, Options: capnp.NewCallOptions(opts), } if params != nil { call.ParamsSize = capnp.ObjectSize{DataSize: 0, PointerCount: 0} call.ParamsFunc = func(s capnp.Struct) error { return params(TunnelServer_obsoleteDeclarativeTunnelConnect_Params{Struct: s}) } } return TunnelServer_obsoleteDeclarativeTunnelConnect_Results_Promise{Pipeline: capnp.NewPipeline(c.Client.Call(call))} } func (c TunnelServer) Authenticate(ctx context.Context, params func(TunnelServer_authenticate_Params) error, opts ...capnp.CallOption) TunnelServer_authenticate_Results_Promise { if c.Client == nil { return TunnelServer_authenticate_Results_Promise{Pipeline: capnp.NewPipeline(capnp.ErrorAnswer(capnp.ErrNullClient))} } call := &capnp.Call{ Ctx: ctx, Method: capnp.Method{ InterfaceID: 0xea58385c65416035, MethodID: 4, InterfaceName: "tunnelrpc/proto/tunnelrpc.capnp:TunnelServer", MethodName: "authenticate", }, Options: capnp.NewCallOptions(opts), } if params != nil { call.ParamsSize = capnp.ObjectSize{DataSize: 0, PointerCount: 3} call.ParamsFunc = func(s capnp.Struct) error { return params(TunnelServer_authenticate_Params{Struct: s}) } } return TunnelServer_authenticate_Results_Promise{Pipeline: capnp.NewPipeline(c.Client.Call(call))} } func (c TunnelServer) ReconnectTunnel(ctx context.Context, params func(TunnelServer_reconnectTunnel_Params) error, opts ...capnp.CallOption) TunnelServer_reconnectTunnel_Results_Promise { if c.Client == nil { return TunnelServer_reconnectTunnel_Results_Promise{Pipeline: capnp.NewPipeline(capnp.ErrorAnswer(capnp.ErrNullClient))} } call := &capnp.Call{ Ctx: ctx, Method: capnp.Method{ InterfaceID: 0xea58385c65416035, MethodID: 5, InterfaceName: "tunnelrpc/proto/tunnelrpc.capnp:TunnelServer", MethodName: "reconnectTunnel", }, Options: capnp.NewCallOptions(opts), } if params != nil { call.ParamsSize = capnp.ObjectSize{DataSize: 0, PointerCount: 5} call.ParamsFunc = func(s capnp.Struct) error { return params(TunnelServer_reconnectTunnel_Params{Struct: s}) } } return TunnelServer_reconnectTunnel_Results_Promise{Pipeline: capnp.NewPipeline(c.Client.Call(call))} } func (c TunnelServer) RegisterConnection(ctx context.Context, params func(RegistrationServer_registerConnection_Params) error, opts ...capnp.CallOption) RegistrationServer_registerConnection_Results_Promise { if c.Client == nil { return RegistrationServer_registerConnection_Results_Promise{Pipeline: capnp.NewPipeline(capnp.ErrorAnswer(capnp.ErrNullClient))} } call := &capnp.Call{ Ctx: ctx, Method: capnp.Method{ InterfaceID: 0xf71695ec7fe85497, MethodID: 0, InterfaceName: "tunnelrpc/proto/tunnelrpc.capnp:RegistrationServer", MethodName: "registerConnection", }, Options: capnp.NewCallOptions(opts), } if params != nil { call.ParamsSize = capnp.ObjectSize{DataSize: 8, PointerCount: 3} call.ParamsFunc = func(s capnp.Struct) error { return params(RegistrationServer_registerConnection_Params{Struct: s}) } } return RegistrationServer_registerConnection_Results_Promise{Pipeline: capnp.NewPipeline(c.Client.Call(call))} } func (c TunnelServer) UnregisterConnection(ctx context.Context, params func(RegistrationServer_unregisterConnection_Params) error, opts ...capnp.CallOption) RegistrationServer_unregisterConnection_Results_Promise { if c.Client == nil { return RegistrationServer_unregisterConnection_Results_Promise{Pipeline: capnp.NewPipeline(capnp.ErrorAnswer(capnp.ErrNullClient))} } call := &capnp.Call{ Ctx: ctx, Method: capnp.Method{ InterfaceID: 0xf71695ec7fe85497, MethodID: 1, InterfaceName: "tunnelrpc/proto/tunnelrpc.capnp:RegistrationServer", MethodName: "unregisterConnection", }, Options: capnp.NewCallOptions(opts), } if params != nil { call.ParamsSize = capnp.ObjectSize{DataSize: 0, PointerCount: 0} call.ParamsFunc = func(s capnp.Struct) error { return params(RegistrationServer_unregisterConnection_Params{Struct: s}) } } return RegistrationServer_unregisterConnection_Results_Promise{Pipeline: capnp.NewPipeline(c.Client.Call(call))} } func (c TunnelServer) UpdateLocalConfiguration(ctx context.Context, params func(RegistrationServer_updateLocalConfiguration_Params) error, opts ...capnp.CallOption) RegistrationServer_updateLocalConfiguration_Results_Promise { if c.Client == nil { return RegistrationServer_updateLocalConfiguration_Results_Promise{Pipeline: capnp.NewPipeline(capnp.ErrorAnswer(capnp.ErrNullClient))} } call := &capnp.Call{ Ctx: ctx, Method: capnp.Method{ InterfaceID: 0xf71695ec7fe85497, MethodID: 2, InterfaceName: "tunnelrpc/proto/tunnelrpc.capnp:RegistrationServer", MethodName: "updateLocalConfiguration", }, Options: capnp.NewCallOptions(opts), } if params != nil { call.ParamsSize = capnp.ObjectSize{DataSize: 0, PointerCount: 1} call.ParamsFunc = func(s capnp.Struct) error { return params(RegistrationServer_updateLocalConfiguration_Params{Struct: s}) } } return RegistrationServer_updateLocalConfiguration_Results_Promise{Pipeline: capnp.NewPipeline(c.Client.Call(call))} } type TunnelServer_Server interface { RegisterTunnel(TunnelServer_registerTunnel) error GetServerInfo(TunnelServer_getServerInfo) error UnregisterTunnel(TunnelServer_unregisterTunnel) error ObsoleteDeclarativeTunnelConnect(TunnelServer_obsoleteDeclarativeTunnelConnect) error Authenticate(TunnelServer_authenticate) error ReconnectTunnel(TunnelServer_reconnectTunnel) error RegisterConnection(RegistrationServer_registerConnection) error UnregisterConnection(RegistrationServer_unregisterConnection) error UpdateLocalConfiguration(RegistrationServer_updateLocalConfiguration) error } func TunnelServer_ServerToClient(s TunnelServer_Server) TunnelServer { c, _ := s.(server.Closer) return TunnelServer{Client: server.New(TunnelServer_Methods(nil, s), c)} } func TunnelServer_Methods(methods []server.Method, s TunnelServer_Server) []server.Method { if cap(methods) == 0 { methods = make([]server.Method, 0, 9) } methods = append(methods, server.Method{ Method: capnp.Method{ InterfaceID: 0xea58385c65416035, MethodID: 0, InterfaceName: "tunnelrpc/proto/tunnelrpc.capnp:TunnelServer", MethodName: "registerTunnel", }, Impl: func(c context.Context, opts capnp.CallOptions, p, r capnp.Struct) error { call := TunnelServer_registerTunnel{c, opts, TunnelServer_registerTunnel_Params{Struct: p}, TunnelServer_registerTunnel_Results{Struct: r}} return s.RegisterTunnel(call) }, ResultsSize: capnp.ObjectSize{DataSize: 0, PointerCount: 1}, }) methods = append(methods, server.Method{ Method: capnp.Method{ InterfaceID: 0xea58385c65416035, MethodID: 1, InterfaceName: "tunnelrpc/proto/tunnelrpc.capnp:TunnelServer", MethodName: "getServerInfo", }, Impl: func(c context.Context, opts capnp.CallOptions, p, r capnp.Struct) error { call := TunnelServer_getServerInfo{c, opts, TunnelServer_getServerInfo_Params{Struct: p}, TunnelServer_getServerInfo_Results{Struct: r}} return s.GetServerInfo(call) }, ResultsSize: capnp.ObjectSize{DataSize: 0, PointerCount: 1}, }) methods = append(methods, server.Method{ Method: capnp.Method{ InterfaceID: 0xea58385c65416035, MethodID: 2, InterfaceName: "tunnelrpc/proto/tunnelrpc.capnp:TunnelServer", MethodName: "unregisterTunnel", }, Impl: func(c context.Context, opts capnp.CallOptions, p, r capnp.Struct) error { call := TunnelServer_unregisterTunnel{c, opts, TunnelServer_unregisterTunnel_Params{Struct: p}, TunnelServer_unregisterTunnel_Results{Struct: r}} return s.UnregisterTunnel(call) }, ResultsSize: capnp.ObjectSize{DataSize: 0, PointerCount: 0}, }) methods = append(methods, server.Method{ Method: capnp.Method{ InterfaceID: 0xea58385c65416035, MethodID: 3, InterfaceName: "tunnelrpc/proto/tunnelrpc.capnp:TunnelServer", MethodName: "obsoleteDeclarativeTunnelConnect", }, Impl: func(c context.Context, opts capnp.CallOptions, p, r capnp.Struct) error { call := TunnelServer_obsoleteDeclarativeTunnelConnect{c, opts, TunnelServer_obsoleteDeclarativeTunnelConnect_Params{Struct: p}, TunnelServer_obsoleteDeclarativeTunnelConnect_Results{Struct: r}} return s.ObsoleteDeclarativeTunnelConnect(call) }, ResultsSize: capnp.ObjectSize{DataSize: 0, PointerCount: 0}, }) methods = append(methods, server.Method{ Method: capnp.Method{ InterfaceID: 0xea58385c65416035, MethodID: 4, InterfaceName: "tunnelrpc/proto/tunnelrpc.capnp:TunnelServer", MethodName: "authenticate", }, Impl: func(c context.Context, opts capnp.CallOptions, p, r capnp.Struct) error { call := TunnelServer_authenticate{c, opts, TunnelServer_authenticate_Params{Struct: p}, TunnelServer_authenticate_Results{Struct: r}} return s.Authenticate(call) }, ResultsSize: capnp.ObjectSize{DataSize: 0, PointerCount: 1}, }) methods = append(methods, server.Method{ Method: capnp.Method{ InterfaceID: 0xea58385c65416035, MethodID: 5, InterfaceName: "tunnelrpc/proto/tunnelrpc.capnp:TunnelServer", MethodName: "reconnectTunnel", }, Impl: func(c context.Context, opts capnp.CallOptions, p, r capnp.Struct) error { call := TunnelServer_reconnectTunnel{c, opts, TunnelServer_reconnectTunnel_Params{Struct: p}, TunnelServer_reconnectTunnel_Results{Struct: r}} return s.ReconnectTunnel(call) }, ResultsSize: capnp.ObjectSize{DataSize: 0, PointerCount: 1}, }) methods = append(methods, server.Method{ Method: capnp.Method{ InterfaceID: 0xf71695ec7fe85497, MethodID: 0, InterfaceName: "tunnelrpc/proto/tunnelrpc.capnp:RegistrationServer", MethodName: "registerConnection", }, Impl: func(c context.Context, opts capnp.CallOptions, p, r capnp.Struct) error { call := RegistrationServer_registerConnection{c, opts, RegistrationServer_registerConnection_Params{Struct: p}, RegistrationServer_registerConnection_Results{Struct: r}} return s.RegisterConnection(call) }, ResultsSize: capnp.ObjectSize{DataSize: 0, PointerCount: 1}, }) methods = append(methods, server.Method{ Method: capnp.Method{ InterfaceID: 0xf71695ec7fe85497, MethodID: 1, InterfaceName: "tunnelrpc/proto/tunnelrpc.capnp:RegistrationServer", MethodName: "unregisterConnection", }, Impl: func(c context.Context, opts capnp.CallOptions, p, r capnp.Struct) error { call := RegistrationServer_unregisterConnection{c, opts, RegistrationServer_unregisterConnection_Params{Struct: p}, RegistrationServer_unregisterConnection_Results{Struct: r}} return s.UnregisterConnection(call) }, ResultsSize: capnp.ObjectSize{DataSize: 0, PointerCount: 0}, }) methods = append(methods, server.Method{ Method: capnp.Method{ InterfaceID: 0xf71695ec7fe85497, MethodID: 2, InterfaceName: "tunnelrpc/proto/tunnelrpc.capnp:RegistrationServer", MethodName: "updateLocalConfiguration", }, Impl: func(c context.Context, opts capnp.CallOptions, p, r capnp.Struct) error { call := RegistrationServer_updateLocalConfiguration{c, opts, RegistrationServer_updateLocalConfiguration_Params{Struct: p}, RegistrationServer_updateLocalConfiguration_Results{Struct: r}} return s.UpdateLocalConfiguration(call) }, ResultsSize: capnp.ObjectSize{DataSize: 0, PointerCount: 0}, }) return methods } // TunnelServer_registerTunnel holds the arguments for a server call to TunnelServer.registerTunnel. type TunnelServer_registerTunnel struct { Ctx context.Context Options capnp.CallOptions Params TunnelServer_registerTunnel_Params Results TunnelServer_registerTunnel_Results } // TunnelServer_getServerInfo holds the arguments for a server call to TunnelServer.getServerInfo. type TunnelServer_getServerInfo struct { Ctx context.Context Options capnp.CallOptions Params TunnelServer_getServerInfo_Params Results TunnelServer_getServerInfo_Results } // TunnelServer_unregisterTunnel holds the arguments for a server call to TunnelServer.unregisterTunnel. type TunnelServer_unregisterTunnel struct { Ctx context.Context Options capnp.CallOptions Params TunnelServer_unregisterTunnel_Params Results TunnelServer_unregisterTunnel_Results } // TunnelServer_obsoleteDeclarativeTunnelConnect holds the arguments for a server call to TunnelServer.obsoleteDeclarativeTunnelConnect. type TunnelServer_obsoleteDeclarativeTunnelConnect struct { Ctx context.Context Options capnp.CallOptions Params TunnelServer_obsoleteDeclarativeTunnelConnect_Params Results TunnelServer_obsoleteDeclarativeTunnelConnect_Results } // TunnelServer_authenticate holds the arguments for a server call to TunnelServer.authenticate. type TunnelServer_authenticate struct { Ctx context.Context Options capnp.CallOptions Params TunnelServer_authenticate_Params Results TunnelServer_authenticate_Results } // TunnelServer_reconnectTunnel holds the arguments for a server call to TunnelServer.reconnectTunnel. type TunnelServer_reconnectTunnel struct { Ctx context.Context Options capnp.CallOptions Params TunnelServer_reconnectTunnel_Params Results TunnelServer_reconnectTunnel_Results } type TunnelServer_registerTunnel_Params struct{ capnp.Struct } // TunnelServer_registerTunnel_Params_TypeID is the unique identifier for the type TunnelServer_registerTunnel_Params. const TunnelServer_registerTunnel_Params_TypeID = 0xb70431c0dc014915 func NewTunnelServer_registerTunnel_Params(s *capnp.Segment) (TunnelServer_registerTunnel_Params, error) { st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 3}) return TunnelServer_registerTunnel_Params{st}, err } func NewRootTunnelServer_registerTunnel_Params(s *capnp.Segment) (TunnelServer_registerTunnel_Params, error) { st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 3}) return TunnelServer_registerTunnel_Params{st}, err } func ReadRootTunnelServer_registerTunnel_Params(msg *capnp.Message) (TunnelServer_registerTunnel_Params, error) { root, err := msg.RootPtr() return TunnelServer_registerTunnel_Params{root.Struct()}, err } func (s TunnelServer_registerTunnel_Params) String() string { str, _ := text.Marshal(0xb70431c0dc014915, s.Struct) return str } func (s TunnelServer_registerTunnel_Params) OriginCert() ([]byte, error) { p, err := s.Struct.Ptr(0) return []byte(p.Data()), err } func (s TunnelServer_registerTunnel_Params) HasOriginCert() bool { p, err := s.Struct.Ptr(0) return p.IsValid() || err != nil } func (s TunnelServer_registerTunnel_Params) SetOriginCert(v []byte) error { return s.Struct.SetData(0, v) } func (s TunnelServer_registerTunnel_Params) Hostname() (string, error) { p, err := s.Struct.Ptr(1) return p.Text(), err } func (s TunnelServer_registerTunnel_Params) HasHostname() bool { p, err := s.Struct.Ptr(1) return p.IsValid() || err != nil } func (s TunnelServer_registerTunnel_Params) HostnameBytes() ([]byte, error) { p, err := s.Struct.Ptr(1) return p.TextBytes(), err } func (s TunnelServer_registerTunnel_Params) SetHostname(v string) error { return s.Struct.SetText(1, v) } func (s TunnelServer_registerTunnel_Params) Options() (RegistrationOptions, error) { p, err := s.Struct.Ptr(2) return RegistrationOptions{Struct: p.Struct()}, err } func (s TunnelServer_registerTunnel_Params) HasOptions() bool { p, err := s.Struct.Ptr(2) return p.IsValid() || err != nil } func (s TunnelServer_registerTunnel_Params) SetOptions(v RegistrationOptions) error { return s.Struct.SetPtr(2, v.Struct.ToPtr()) } // NewOptions sets the options field to a newly // allocated RegistrationOptions struct, preferring placement in s's segment. func (s TunnelServer_registerTunnel_Params) NewOptions() (RegistrationOptions, error) { ss, err := NewRegistrationOptions(s.Struct.Segment()) if err != nil { return RegistrationOptions{}, err } err = s.Struct.SetPtr(2, ss.Struct.ToPtr()) return ss, err } // TunnelServer_registerTunnel_Params_List is a list of TunnelServer_registerTunnel_Params. type TunnelServer_registerTunnel_Params_List struct{ capnp.List } // NewTunnelServer_registerTunnel_Params creates a new list of TunnelServer_registerTunnel_Params. func NewTunnelServer_registerTunnel_Params_List(s *capnp.Segment, sz int32) (TunnelServer_registerTunnel_Params_List, error) { l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 3}, sz) return TunnelServer_registerTunnel_Params_List{l}, err } func (s TunnelServer_registerTunnel_Params_List) At(i int) TunnelServer_registerTunnel_Params { return TunnelServer_registerTunnel_Params{s.List.Struct(i)} } func (s TunnelServer_registerTunnel_Params_List) Set(i int, v TunnelServer_registerTunnel_Params) error { return s.List.SetStruct(i, v.Struct) } func (s TunnelServer_registerTunnel_Params_List) String() string { str, _ := text.MarshalList(0xb70431c0dc014915, s.List) return str } // TunnelServer_registerTunnel_Params_Promise is a wrapper for a TunnelServer_registerTunnel_Params promised by a client call. type TunnelServer_registerTunnel_Params_Promise struct{ *capnp.Pipeline } func (p TunnelServer_registerTunnel_Params_Promise) Struct() (TunnelServer_registerTunnel_Params, error) { s, err := p.Pipeline.Struct() return TunnelServer_registerTunnel_Params{s}, err } func (p TunnelServer_registerTunnel_Params_Promise) Options() RegistrationOptions_Promise { return RegistrationOptions_Promise{Pipeline: p.Pipeline.GetPipeline(2)} } type TunnelServer_registerTunnel_Results struct{ capnp.Struct } // TunnelServer_registerTunnel_Results_TypeID is the unique identifier for the type TunnelServer_registerTunnel_Results. const TunnelServer_registerTunnel_Results_TypeID = 0xf2c122394f447e8e func NewTunnelServer_registerTunnel_Results(s *capnp.Segment) (TunnelServer_registerTunnel_Results, error) { st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) return TunnelServer_registerTunnel_Results{st}, err } func NewRootTunnelServer_registerTunnel_Results(s *capnp.Segment) (TunnelServer_registerTunnel_Results, error) { st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) return TunnelServer_registerTunnel_Results{st}, err } func ReadRootTunnelServer_registerTunnel_Results(msg *capnp.Message) (TunnelServer_registerTunnel_Results, error) { root, err := msg.RootPtr() return TunnelServer_registerTunnel_Results{root.Struct()}, err } func (s TunnelServer_registerTunnel_Results) String() string { str, _ := text.Marshal(0xf2c122394f447e8e, s.Struct) return str } func (s TunnelServer_registerTunnel_Results) Result() (TunnelRegistration, error) { p, err := s.Struct.Ptr(0) return TunnelRegistration{Struct: p.Struct()}, err } func (s TunnelServer_registerTunnel_Results) HasResult() bool { p, err := s.Struct.Ptr(0) return p.IsValid() || err != nil } func (s TunnelServer_registerTunnel_Results) SetResult(v TunnelRegistration) error { return s.Struct.SetPtr(0, v.Struct.ToPtr()) } // NewResult sets the result field to a newly // allocated TunnelRegistration struct, preferring placement in s's segment. func (s TunnelServer_registerTunnel_Results) NewResult() (TunnelRegistration, error) { ss, err := NewTunnelRegistration(s.Struct.Segment()) if err != nil { return TunnelRegistration{}, err } err = s.Struct.SetPtr(0, ss.Struct.ToPtr()) return ss, err } // TunnelServer_registerTunnel_Results_List is a list of TunnelServer_registerTunnel_Results. type TunnelServer_registerTunnel_Results_List struct{ capnp.List } // NewTunnelServer_registerTunnel_Results creates a new list of TunnelServer_registerTunnel_Results. func NewTunnelServer_registerTunnel_Results_List(s *capnp.Segment, sz int32) (TunnelServer_registerTunnel_Results_List, error) { l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}, sz) return TunnelServer_registerTunnel_Results_List{l}, err } func (s TunnelServer_registerTunnel_Results_List) At(i int) TunnelServer_registerTunnel_Results { return TunnelServer_registerTunnel_Results{s.List.Struct(i)} } func (s TunnelServer_registerTunnel_Results_List) Set(i int, v TunnelServer_registerTunnel_Results) error { return s.List.SetStruct(i, v.Struct) } func (s TunnelServer_registerTunnel_Results_List) String() string { str, _ := text.MarshalList(0xf2c122394f447e8e, s.List) return str } // TunnelServer_registerTunnel_Results_Promise is a wrapper for a TunnelServer_registerTunnel_Results promised by a client call. type TunnelServer_registerTunnel_Results_Promise struct{ *capnp.Pipeline } func (p TunnelServer_registerTunnel_Results_Promise) Struct() (TunnelServer_registerTunnel_Results, error) { s, err := p.Pipeline.Struct() return TunnelServer_registerTunnel_Results{s}, err } func (p TunnelServer_registerTunnel_Results_Promise) Result() TunnelRegistration_Promise { return TunnelRegistration_Promise{Pipeline: p.Pipeline.GetPipeline(0)} } type TunnelServer_getServerInfo_Params struct{ capnp.Struct } // TunnelServer_getServerInfo_Params_TypeID is the unique identifier for the type TunnelServer_getServerInfo_Params. const TunnelServer_getServerInfo_Params_TypeID = 0xdc3ed6801961e502 func NewTunnelServer_getServerInfo_Params(s *capnp.Segment) (TunnelServer_getServerInfo_Params, error) { st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0}) return TunnelServer_getServerInfo_Params{st}, err } func NewRootTunnelServer_getServerInfo_Params(s *capnp.Segment) (TunnelServer_getServerInfo_Params, error) { st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0}) return TunnelServer_getServerInfo_Params{st}, err } func ReadRootTunnelServer_getServerInfo_Params(msg *capnp.Message) (TunnelServer_getServerInfo_Params, error) { root, err := msg.RootPtr() return TunnelServer_getServerInfo_Params{root.Struct()}, err } func (s TunnelServer_getServerInfo_Params) String() string { str, _ := text.Marshal(0xdc3ed6801961e502, s.Struct) return str } // TunnelServer_getServerInfo_Params_List is a list of TunnelServer_getServerInfo_Params. type TunnelServer_getServerInfo_Params_List struct{ capnp.List } // NewTunnelServer_getServerInfo_Params creates a new list of TunnelServer_getServerInfo_Params. func NewTunnelServer_getServerInfo_Params_List(s *capnp.Segment, sz int32) (TunnelServer_getServerInfo_Params_List, error) { l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0}, sz) return TunnelServer_getServerInfo_Params_List{l}, err } func (s TunnelServer_getServerInfo_Params_List) At(i int) TunnelServer_getServerInfo_Params { return TunnelServer_getServerInfo_Params{s.List.Struct(i)} } func (s TunnelServer_getServerInfo_Params_List) Set(i int, v TunnelServer_getServerInfo_Params) error { return s.List.SetStruct(i, v.Struct) } func (s TunnelServer_getServerInfo_Params_List) String() string { str, _ := text.MarshalList(0xdc3ed6801961e502, s.List) return str } // TunnelServer_getServerInfo_Params_Promise is a wrapper for a TunnelServer_getServerInfo_Params promised by a client call. type TunnelServer_getServerInfo_Params_Promise struct{ *capnp.Pipeline } func (p TunnelServer_getServerInfo_Params_Promise) Struct() (TunnelServer_getServerInfo_Params, error) { s, err := p.Pipeline.Struct() return TunnelServer_getServerInfo_Params{s}, err } type TunnelServer_getServerInfo_Results struct{ capnp.Struct } // TunnelServer_getServerInfo_Results_TypeID is the unique identifier for the type TunnelServer_getServerInfo_Results. const TunnelServer_getServerInfo_Results_TypeID = 0xe3e37d096a5b564e func NewTunnelServer_getServerInfo_Results(s *capnp.Segment) (TunnelServer_getServerInfo_Results, error) { st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) return TunnelServer_getServerInfo_Results{st}, err } func NewRootTunnelServer_getServerInfo_Results(s *capnp.Segment) (TunnelServer_getServerInfo_Results, error) { st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) return TunnelServer_getServerInfo_Results{st}, err } func ReadRootTunnelServer_getServerInfo_Results(msg *capnp.Message) (TunnelServer_getServerInfo_Results, error) { root, err := msg.RootPtr() return TunnelServer_getServerInfo_Results{root.Struct()}, err } func (s TunnelServer_getServerInfo_Results) String() string { str, _ := text.Marshal(0xe3e37d096a5b564e, s.Struct) return str } func (s TunnelServer_getServerInfo_Results) Result() (ServerInfo, error) { p, err := s.Struct.Ptr(0) return ServerInfo{Struct: p.Struct()}, err } func (s TunnelServer_getServerInfo_Results) HasResult() bool { p, err := s.Struct.Ptr(0) return p.IsValid() || err != nil } func (s TunnelServer_getServerInfo_Results) SetResult(v ServerInfo) error { return s.Struct.SetPtr(0, v.Struct.ToPtr()) } // NewResult sets the result field to a newly // allocated ServerInfo struct, preferring placement in s's segment. func (s TunnelServer_getServerInfo_Results) NewResult() (ServerInfo, error) { ss, err := NewServerInfo(s.Struct.Segment()) if err != nil { return ServerInfo{}, err } err = s.Struct.SetPtr(0, ss.Struct.ToPtr()) return ss, err } // TunnelServer_getServerInfo_Results_List is a list of TunnelServer_getServerInfo_Results. type TunnelServer_getServerInfo_Results_List struct{ capnp.List } // NewTunnelServer_getServerInfo_Results creates a new list of TunnelServer_getServerInfo_Results. func NewTunnelServer_getServerInfo_Results_List(s *capnp.Segment, sz int32) (TunnelServer_getServerInfo_Results_List, error) { l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}, sz) return TunnelServer_getServerInfo_Results_List{l}, err } func (s TunnelServer_getServerInfo_Results_List) At(i int) TunnelServer_getServerInfo_Results { return TunnelServer_getServerInfo_Results{s.List.Struct(i)} } func (s TunnelServer_getServerInfo_Results_List) Set(i int, v TunnelServer_getServerInfo_Results) error { return s.List.SetStruct(i, v.Struct) } func (s TunnelServer_getServerInfo_Results_List) String() string { str, _ := text.MarshalList(0xe3e37d096a5b564e, s.List) return str } // TunnelServer_getServerInfo_Results_Promise is a wrapper for a TunnelServer_getServerInfo_Results promised by a client call. type TunnelServer_getServerInfo_Results_Promise struct{ *capnp.Pipeline } func (p TunnelServer_getServerInfo_Results_Promise) Struct() (TunnelServer_getServerInfo_Results, error) { s, err := p.Pipeline.Struct() return TunnelServer_getServerInfo_Results{s}, err } func (p TunnelServer_getServerInfo_Results_Promise) Result() ServerInfo_Promise { return ServerInfo_Promise{Pipeline: p.Pipeline.GetPipeline(0)} } type TunnelServer_unregisterTunnel_Params struct{ capnp.Struct } // TunnelServer_unregisterTunnel_Params_TypeID is the unique identifier for the type TunnelServer_unregisterTunnel_Params. const TunnelServer_unregisterTunnel_Params_TypeID = 0x9b87b390babc2ccf func NewTunnelServer_unregisterTunnel_Params(s *capnp.Segment) (TunnelServer_unregisterTunnel_Params, error) { st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 0}) return TunnelServer_unregisterTunnel_Params{st}, err } func NewRootTunnelServer_unregisterTunnel_Params(s *capnp.Segment) (TunnelServer_unregisterTunnel_Params, error) { st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 0}) return TunnelServer_unregisterTunnel_Params{st}, err } func ReadRootTunnelServer_unregisterTunnel_Params(msg *capnp.Message) (TunnelServer_unregisterTunnel_Params, error) { root, err := msg.RootPtr() return TunnelServer_unregisterTunnel_Params{root.Struct()}, err } func (s TunnelServer_unregisterTunnel_Params) String() string { str, _ := text.Marshal(0x9b87b390babc2ccf, s.Struct) return str } func (s TunnelServer_unregisterTunnel_Params) GracePeriodNanoSec() int64 { return int64(s.Struct.Uint64(0)) } func (s TunnelServer_unregisterTunnel_Params) SetGracePeriodNanoSec(v int64) { s.Struct.SetUint64(0, uint64(v)) } // TunnelServer_unregisterTunnel_Params_List is a list of TunnelServer_unregisterTunnel_Params. type TunnelServer_unregisterTunnel_Params_List struct{ capnp.List } // NewTunnelServer_unregisterTunnel_Params creates a new list of TunnelServer_unregisterTunnel_Params. func NewTunnelServer_unregisterTunnel_Params_List(s *capnp.Segment, sz int32) (TunnelServer_unregisterTunnel_Params_List, error) { l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 8, PointerCount: 0}, sz) return TunnelServer_unregisterTunnel_Params_List{l}, err } func (s TunnelServer_unregisterTunnel_Params_List) At(i int) TunnelServer_unregisterTunnel_Params { return TunnelServer_unregisterTunnel_Params{s.List.Struct(i)} } func (s TunnelServer_unregisterTunnel_Params_List) Set(i int, v TunnelServer_unregisterTunnel_Params) error { return s.List.SetStruct(i, v.Struct) } func (s TunnelServer_unregisterTunnel_Params_List) String() string { str, _ := text.MarshalList(0x9b87b390babc2ccf, s.List) return str } // TunnelServer_unregisterTunnel_Params_Promise is a wrapper for a TunnelServer_unregisterTunnel_Params promised by a client call. type TunnelServer_unregisterTunnel_Params_Promise struct{ *capnp.Pipeline } func (p TunnelServer_unregisterTunnel_Params_Promise) Struct() (TunnelServer_unregisterTunnel_Params, error) { s, err := p.Pipeline.Struct() return TunnelServer_unregisterTunnel_Params{s}, err } type TunnelServer_unregisterTunnel_Results struct{ capnp.Struct } // TunnelServer_unregisterTunnel_Results_TypeID is the unique identifier for the type TunnelServer_unregisterTunnel_Results. const TunnelServer_unregisterTunnel_Results_TypeID = 0xa29a916d4ebdd894 func NewTunnelServer_unregisterTunnel_Results(s *capnp.Segment) (TunnelServer_unregisterTunnel_Results, error) { st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0}) return TunnelServer_unregisterTunnel_Results{st}, err } func NewRootTunnelServer_unregisterTunnel_Results(s *capnp.Segment) (TunnelServer_unregisterTunnel_Results, error) { st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0}) return TunnelServer_unregisterTunnel_Results{st}, err } func ReadRootTunnelServer_unregisterTunnel_Results(msg *capnp.Message) (TunnelServer_unregisterTunnel_Results, error) { root, err := msg.RootPtr() return TunnelServer_unregisterTunnel_Results{root.Struct()}, err } func (s TunnelServer_unregisterTunnel_Results) String() string { str, _ := text.Marshal(0xa29a916d4ebdd894, s.Struct) return str } // TunnelServer_unregisterTunnel_Results_List is a list of TunnelServer_unregisterTunnel_Results. type TunnelServer_unregisterTunnel_Results_List struct{ capnp.List } // NewTunnelServer_unregisterTunnel_Results creates a new list of TunnelServer_unregisterTunnel_Results. func NewTunnelServer_unregisterTunnel_Results_List(s *capnp.Segment, sz int32) (TunnelServer_unregisterTunnel_Results_List, error) { l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0}, sz) return TunnelServer_unregisterTunnel_Results_List{l}, err } func (s TunnelServer_unregisterTunnel_Results_List) At(i int) TunnelServer_unregisterTunnel_Results { return TunnelServer_unregisterTunnel_Results{s.List.Struct(i)} } func (s TunnelServer_unregisterTunnel_Results_List) Set(i int, v TunnelServer_unregisterTunnel_Results) error { return s.List.SetStruct(i, v.Struct) } func (s TunnelServer_unregisterTunnel_Results_List) String() string { str, _ := text.MarshalList(0xa29a916d4ebdd894, s.List) return str } // TunnelServer_unregisterTunnel_Results_Promise is a wrapper for a TunnelServer_unregisterTunnel_Results promised by a client call. type TunnelServer_unregisterTunnel_Results_Promise struct{ *capnp.Pipeline } func (p TunnelServer_unregisterTunnel_Results_Promise) Struct() (TunnelServer_unregisterTunnel_Results, error) { s, err := p.Pipeline.Struct() return TunnelServer_unregisterTunnel_Results{s}, err } type TunnelServer_obsoleteDeclarativeTunnelConnect_Params struct{ capnp.Struct } // TunnelServer_obsoleteDeclarativeTunnelConnect_Params_TypeID is the unique identifier for the type TunnelServer_obsoleteDeclarativeTunnelConnect_Params. const TunnelServer_obsoleteDeclarativeTunnelConnect_Params_TypeID = 0xa766b24d4fe5da35 func NewTunnelServer_obsoleteDeclarativeTunnelConnect_Params(s *capnp.Segment) (TunnelServer_obsoleteDeclarativeTunnelConnect_Params, error) { st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0}) return TunnelServer_obsoleteDeclarativeTunnelConnect_Params{st}, err } func NewRootTunnelServer_obsoleteDeclarativeTunnelConnect_Params(s *capnp.Segment) (TunnelServer_obsoleteDeclarativeTunnelConnect_Params, error) { st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0}) return TunnelServer_obsoleteDeclarativeTunnelConnect_Params{st}, err } func ReadRootTunnelServer_obsoleteDeclarativeTunnelConnect_Params(msg *capnp.Message) (TunnelServer_obsoleteDeclarativeTunnelConnect_Params, error) { root, err := msg.RootPtr() return TunnelServer_obsoleteDeclarativeTunnelConnect_Params{root.Struct()}, err } func (s TunnelServer_obsoleteDeclarativeTunnelConnect_Params) String() string { str, _ := text.Marshal(0xa766b24d4fe5da35, s.Struct) return str } // TunnelServer_obsoleteDeclarativeTunnelConnect_Params_List is a list of TunnelServer_obsoleteDeclarativeTunnelConnect_Params. type TunnelServer_obsoleteDeclarativeTunnelConnect_Params_List struct{ capnp.List } // NewTunnelServer_obsoleteDeclarativeTunnelConnect_Params creates a new list of TunnelServer_obsoleteDeclarativeTunnelConnect_Params. func NewTunnelServer_obsoleteDeclarativeTunnelConnect_Params_List(s *capnp.Segment, sz int32) (TunnelServer_obsoleteDeclarativeTunnelConnect_Params_List, error) { l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0}, sz) return TunnelServer_obsoleteDeclarativeTunnelConnect_Params_List{l}, err } func (s TunnelServer_obsoleteDeclarativeTunnelConnect_Params_List) At(i int) TunnelServer_obsoleteDeclarativeTunnelConnect_Params { return TunnelServer_obsoleteDeclarativeTunnelConnect_Params{s.List.Struct(i)} } func (s TunnelServer_obsoleteDeclarativeTunnelConnect_Params_List) Set(i int, v TunnelServer_obsoleteDeclarativeTunnelConnect_Params) error { return s.List.SetStruct(i, v.Struct) } func (s TunnelServer_obsoleteDeclarativeTunnelConnect_Params_List) String() string { str, _ := text.MarshalList(0xa766b24d4fe5da35, s.List) return str } // TunnelServer_obsoleteDeclarativeTunnelConnect_Params_Promise is a wrapper for a TunnelServer_obsoleteDeclarativeTunnelConnect_Params promised by a client call. type TunnelServer_obsoleteDeclarativeTunnelConnect_Params_Promise struct{ *capnp.Pipeline } func (p TunnelServer_obsoleteDeclarativeTunnelConnect_Params_Promise) Struct() (TunnelServer_obsoleteDeclarativeTunnelConnect_Params, error) { s, err := p.Pipeline.Struct() return TunnelServer_obsoleteDeclarativeTunnelConnect_Params{s}, err } type TunnelServer_obsoleteDeclarativeTunnelConnect_Results struct{ capnp.Struct } // TunnelServer_obsoleteDeclarativeTunnelConnect_Results_TypeID is the unique identifier for the type TunnelServer_obsoleteDeclarativeTunnelConnect_Results. const TunnelServer_obsoleteDeclarativeTunnelConnect_Results_TypeID = 0xfeac5c8f4899ef7c func NewTunnelServer_obsoleteDeclarativeTunnelConnect_Results(s *capnp.Segment) (TunnelServer_obsoleteDeclarativeTunnelConnect_Results, error) { st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0}) return TunnelServer_obsoleteDeclarativeTunnelConnect_Results{st}, err } func NewRootTunnelServer_obsoleteDeclarativeTunnelConnect_Results(s *capnp.Segment) (TunnelServer_obsoleteDeclarativeTunnelConnect_Results, error) { st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0}) return TunnelServer_obsoleteDeclarativeTunnelConnect_Results{st}, err } func ReadRootTunnelServer_obsoleteDeclarativeTunnelConnect_Results(msg *capnp.Message) (TunnelServer_obsoleteDeclarativeTunnelConnect_Results, error) { root, err := msg.RootPtr() return TunnelServer_obsoleteDeclarativeTunnelConnect_Results{root.Struct()}, err } func (s TunnelServer_obsoleteDeclarativeTunnelConnect_Results) String() string { str, _ := text.Marshal(0xfeac5c8f4899ef7c, s.Struct) return str } // TunnelServer_obsoleteDeclarativeTunnelConnect_Results_List is a list of TunnelServer_obsoleteDeclarativeTunnelConnect_Results. type TunnelServer_obsoleteDeclarativeTunnelConnect_Results_List struct{ capnp.List } // NewTunnelServer_obsoleteDeclarativeTunnelConnect_Results creates a new list of TunnelServer_obsoleteDeclarativeTunnelConnect_Results. func NewTunnelServer_obsoleteDeclarativeTunnelConnect_Results_List(s *capnp.Segment, sz int32) (TunnelServer_obsoleteDeclarativeTunnelConnect_Results_List, error) { l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0}, sz) return TunnelServer_obsoleteDeclarativeTunnelConnect_Results_List{l}, err } func (s TunnelServer_obsoleteDeclarativeTunnelConnect_Results_List) At(i int) TunnelServer_obsoleteDeclarativeTunnelConnect_Results { return TunnelServer_obsoleteDeclarativeTunnelConnect_Results{s.List.Struct(i)} } func (s TunnelServer_obsoleteDeclarativeTunnelConnect_Results_List) Set(i int, v TunnelServer_obsoleteDeclarativeTunnelConnect_Results) error { return s.List.SetStruct(i, v.Struct) } func (s TunnelServer_obsoleteDeclarativeTunnelConnect_Results_List) String() string { str, _ := text.MarshalList(0xfeac5c8f4899ef7c, s.List) return str } // TunnelServer_obsoleteDeclarativeTunnelConnect_Results_Promise is a wrapper for a TunnelServer_obsoleteDeclarativeTunnelConnect_Results promised by a client call. type TunnelServer_obsoleteDeclarativeTunnelConnect_Results_Promise struct{ *capnp.Pipeline } func (p TunnelServer_obsoleteDeclarativeTunnelConnect_Results_Promise) Struct() (TunnelServer_obsoleteDeclarativeTunnelConnect_Results, error) { s, err := p.Pipeline.Struct() return TunnelServer_obsoleteDeclarativeTunnelConnect_Results{s}, err } type TunnelServer_authenticate_Params struct{ capnp.Struct } // TunnelServer_authenticate_Params_TypeID is the unique identifier for the type TunnelServer_authenticate_Params. const TunnelServer_authenticate_Params_TypeID = 0x85c8cea1ab1894f3 func NewTunnelServer_authenticate_Params(s *capnp.Segment) (TunnelServer_authenticate_Params, error) { st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 3}) return TunnelServer_authenticate_Params{st}, err } func NewRootTunnelServer_authenticate_Params(s *capnp.Segment) (TunnelServer_authenticate_Params, error) { st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 3}) return TunnelServer_authenticate_Params{st}, err } func ReadRootTunnelServer_authenticate_Params(msg *capnp.Message) (TunnelServer_authenticate_Params, error) { root, err := msg.RootPtr() return TunnelServer_authenticate_Params{root.Struct()}, err } func (s TunnelServer_authenticate_Params) String() string { str, _ := text.Marshal(0x85c8cea1ab1894f3, s.Struct) return str } func (s TunnelServer_authenticate_Params) OriginCert() ([]byte, error) { p, err := s.Struct.Ptr(0) return []byte(p.Data()), err } func (s TunnelServer_authenticate_Params) HasOriginCert() bool { p, err := s.Struct.Ptr(0) return p.IsValid() || err != nil } func (s TunnelServer_authenticate_Params) SetOriginCert(v []byte) error { return s.Struct.SetData(0, v) } func (s TunnelServer_authenticate_Params) Hostname() (string, error) { p, err := s.Struct.Ptr(1) return p.Text(), err } func (s TunnelServer_authenticate_Params) HasHostname() bool { p, err := s.Struct.Ptr(1) return p.IsValid() || err != nil } func (s TunnelServer_authenticate_Params) HostnameBytes() ([]byte, error) { p, err := s.Struct.Ptr(1) return p.TextBytes(), err } func (s TunnelServer_authenticate_Params) SetHostname(v string) error { return s.Struct.SetText(1, v) } func (s TunnelServer_authenticate_Params) Options() (RegistrationOptions, error) { p, err := s.Struct.Ptr(2) return RegistrationOptions{Struct: p.Struct()}, err } func (s TunnelServer_authenticate_Params) HasOptions() bool { p, err := s.Struct.Ptr(2) return p.IsValid() || err != nil } func (s TunnelServer_authenticate_Params) SetOptions(v RegistrationOptions) error { return s.Struct.SetPtr(2, v.Struct.ToPtr()) } // NewOptions sets the options field to a newly // allocated RegistrationOptions struct, preferring placement in s's segment. func (s TunnelServer_authenticate_Params) NewOptions() (RegistrationOptions, error) { ss, err := NewRegistrationOptions(s.Struct.Segment()) if err != nil { return RegistrationOptions{}, err } err = s.Struct.SetPtr(2, ss.Struct.ToPtr()) return ss, err } // TunnelServer_authenticate_Params_List is a list of TunnelServer_authenticate_Params. type TunnelServer_authenticate_Params_List struct{ capnp.List } // NewTunnelServer_authenticate_Params creates a new list of TunnelServer_authenticate_Params. func NewTunnelServer_authenticate_Params_List(s *capnp.Segment, sz int32) (TunnelServer_authenticate_Params_List, error) { l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 3}, sz) return TunnelServer_authenticate_Params_List{l}, err } func (s TunnelServer_authenticate_Params_List) At(i int) TunnelServer_authenticate_Params { return TunnelServer_authenticate_Params{s.List.Struct(i)} } func (s TunnelServer_authenticate_Params_List) Set(i int, v TunnelServer_authenticate_Params) error { return s.List.SetStruct(i, v.Struct) } func (s TunnelServer_authenticate_Params_List) String() string { str, _ := text.MarshalList(0x85c8cea1ab1894f3, s.List) return str } // TunnelServer_authenticate_Params_Promise is a wrapper for a TunnelServer_authenticate_Params promised by a client call. type TunnelServer_authenticate_Params_Promise struct{ *capnp.Pipeline } func (p TunnelServer_authenticate_Params_Promise) Struct() (TunnelServer_authenticate_Params, error) { s, err := p.Pipeline.Struct() return TunnelServer_authenticate_Params{s}, err } func (p TunnelServer_authenticate_Params_Promise) Options() RegistrationOptions_Promise { return RegistrationOptions_Promise{Pipeline: p.Pipeline.GetPipeline(2)} } type TunnelServer_authenticate_Results struct{ capnp.Struct } // TunnelServer_authenticate_Results_TypeID is the unique identifier for the type TunnelServer_authenticate_Results. const TunnelServer_authenticate_Results_TypeID = 0xfc5edf80e39c0796 func NewTunnelServer_authenticate_Results(s *capnp.Segment) (TunnelServer_authenticate_Results, error) { st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) return TunnelServer_authenticate_Results{st}, err } func NewRootTunnelServer_authenticate_Results(s *capnp.Segment) (TunnelServer_authenticate_Results, error) { st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) return TunnelServer_authenticate_Results{st}, err } func ReadRootTunnelServer_authenticate_Results(msg *capnp.Message) (TunnelServer_authenticate_Results, error) { root, err := msg.RootPtr() return TunnelServer_authenticate_Results{root.Struct()}, err } func (s TunnelServer_authenticate_Results) String() string { str, _ := text.Marshal(0xfc5edf80e39c0796, s.Struct) return str } func (s TunnelServer_authenticate_Results) Result() (AuthenticateResponse, error) { p, err := s.Struct.Ptr(0) return AuthenticateResponse{Struct: p.Struct()}, err } func (s TunnelServer_authenticate_Results) HasResult() bool { p, err := s.Struct.Ptr(0) return p.IsValid() || err != nil } func (s TunnelServer_authenticate_Results) SetResult(v AuthenticateResponse) error { return s.Struct.SetPtr(0, v.Struct.ToPtr()) } // NewResult sets the result field to a newly // allocated AuthenticateResponse struct, preferring placement in s's segment. func (s TunnelServer_authenticate_Results) NewResult() (AuthenticateResponse, error) { ss, err := NewAuthenticateResponse(s.Struct.Segment()) if err != nil { return AuthenticateResponse{}, err } err = s.Struct.SetPtr(0, ss.Struct.ToPtr()) return ss, err } // TunnelServer_authenticate_Results_List is a list of TunnelServer_authenticate_Results. type TunnelServer_authenticate_Results_List struct{ capnp.List } // NewTunnelServer_authenticate_Results creates a new list of TunnelServer_authenticate_Results. func NewTunnelServer_authenticate_Results_List(s *capnp.Segment, sz int32) (TunnelServer_authenticate_Results_List, error) { l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}, sz) return TunnelServer_authenticate_Results_List{l}, err } func (s TunnelServer_authenticate_Results_List) At(i int) TunnelServer_authenticate_Results { return TunnelServer_authenticate_Results{s.List.Struct(i)} } func (s TunnelServer_authenticate_Results_List) Set(i int, v TunnelServer_authenticate_Results) error { return s.List.SetStruct(i, v.Struct) } func (s TunnelServer_authenticate_Results_List) String() string { str, _ := text.MarshalList(0xfc5edf80e39c0796, s.List) return str } // TunnelServer_authenticate_Results_Promise is a wrapper for a TunnelServer_authenticate_Results promised by a client call. type TunnelServer_authenticate_Results_Promise struct{ *capnp.Pipeline } func (p TunnelServer_authenticate_Results_Promise) Struct() (TunnelServer_authenticate_Results, error) { s, err := p.Pipeline.Struct() return TunnelServer_authenticate_Results{s}, err } func (p TunnelServer_authenticate_Results_Promise) Result() AuthenticateResponse_Promise { return AuthenticateResponse_Promise{Pipeline: p.Pipeline.GetPipeline(0)} } type TunnelServer_reconnectTunnel_Params struct{ capnp.Struct } // TunnelServer_reconnectTunnel_Params_TypeID is the unique identifier for the type TunnelServer_reconnectTunnel_Params. const TunnelServer_reconnectTunnel_Params_TypeID = 0xa353a3556df74984 func NewTunnelServer_reconnectTunnel_Params(s *capnp.Segment) (TunnelServer_reconnectTunnel_Params, error) { st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 5}) return TunnelServer_reconnectTunnel_Params{st}, err } func NewRootTunnelServer_reconnectTunnel_Params(s *capnp.Segment) (TunnelServer_reconnectTunnel_Params, error) { st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 5}) return TunnelServer_reconnectTunnel_Params{st}, err } func ReadRootTunnelServer_reconnectTunnel_Params(msg *capnp.Message) (TunnelServer_reconnectTunnel_Params, error) { root, err := msg.RootPtr() return TunnelServer_reconnectTunnel_Params{root.Struct()}, err } func (s TunnelServer_reconnectTunnel_Params) String() string { str, _ := text.Marshal(0xa353a3556df74984, s.Struct) return str } func (s TunnelServer_reconnectTunnel_Params) Jwt() ([]byte, error) { p, err := s.Struct.Ptr(0) return []byte(p.Data()), err } func (s TunnelServer_reconnectTunnel_Params) HasJwt() bool { p, err := s.Struct.Ptr(0) return p.IsValid() || err != nil } func (s TunnelServer_reconnectTunnel_Params) SetJwt(v []byte) error { return s.Struct.SetData(0, v) } func (s TunnelServer_reconnectTunnel_Params) EventDigest() ([]byte, error) { p, err := s.Struct.Ptr(1) return []byte(p.Data()), err } func (s TunnelServer_reconnectTunnel_Params) HasEventDigest() bool { p, err := s.Struct.Ptr(1) return p.IsValid() || err != nil } func (s TunnelServer_reconnectTunnel_Params) SetEventDigest(v []byte) error { return s.Struct.SetData(1, v) } func (s TunnelServer_reconnectTunnel_Params) ConnDigest() ([]byte, error) { p, err := s.Struct.Ptr(2) return []byte(p.Data()), err } func (s TunnelServer_reconnectTunnel_Params) HasConnDigest() bool { p, err := s.Struct.Ptr(2) return p.IsValid() || err != nil } func (s TunnelServer_reconnectTunnel_Params) SetConnDigest(v []byte) error { return s.Struct.SetData(2, v) } func (s TunnelServer_reconnectTunnel_Params) Hostname() (string, error) { p, err := s.Struct.Ptr(3) return p.Text(), err } func (s TunnelServer_reconnectTunnel_Params) HasHostname() bool { p, err := s.Struct.Ptr(3) return p.IsValid() || err != nil } func (s TunnelServer_reconnectTunnel_Params) HostnameBytes() ([]byte, error) { p, err := s.Struct.Ptr(3) return p.TextBytes(), err } func (s TunnelServer_reconnectTunnel_Params) SetHostname(v string) error { return s.Struct.SetText(3, v) } func (s TunnelServer_reconnectTunnel_Params) Options() (RegistrationOptions, error) { p, err := s.Struct.Ptr(4) return RegistrationOptions{Struct: p.Struct()}, err } func (s TunnelServer_reconnectTunnel_Params) HasOptions() bool { p, err := s.Struct.Ptr(4) return p.IsValid() || err != nil } func (s TunnelServer_reconnectTunnel_Params) SetOptions(v RegistrationOptions) error { return s.Struct.SetPtr(4, v.Struct.ToPtr()) } // NewOptions sets the options field to a newly // allocated RegistrationOptions struct, preferring placement in s's segment. func (s TunnelServer_reconnectTunnel_Params) NewOptions() (RegistrationOptions, error) { ss, err := NewRegistrationOptions(s.Struct.Segment()) if err != nil { return RegistrationOptions{}, err } err = s.Struct.SetPtr(4, ss.Struct.ToPtr()) return ss, err } // TunnelServer_reconnectTunnel_Params_List is a list of TunnelServer_reconnectTunnel_Params. type TunnelServer_reconnectTunnel_Params_List struct{ capnp.List } // NewTunnelServer_reconnectTunnel_Params creates a new list of TunnelServer_reconnectTunnel_Params. func NewTunnelServer_reconnectTunnel_Params_List(s *capnp.Segment, sz int32) (TunnelServer_reconnectTunnel_Params_List, error) { l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 5}, sz) return TunnelServer_reconnectTunnel_Params_List{l}, err } func (s TunnelServer_reconnectTunnel_Params_List) At(i int) TunnelServer_reconnectTunnel_Params { return TunnelServer_reconnectTunnel_Params{s.List.Struct(i)} } func (s TunnelServer_reconnectTunnel_Params_List) Set(i int, v TunnelServer_reconnectTunnel_Params) error { return s.List.SetStruct(i, v.Struct) } func (s TunnelServer_reconnectTunnel_Params_List) String() string { str, _ := text.MarshalList(0xa353a3556df74984, s.List) return str } // TunnelServer_reconnectTunnel_Params_Promise is a wrapper for a TunnelServer_reconnectTunnel_Params promised by a client call. type TunnelServer_reconnectTunnel_Params_Promise struct{ *capnp.Pipeline } func (p TunnelServer_reconnectTunnel_Params_Promise) Struct() (TunnelServer_reconnectTunnel_Params, error) { s, err := p.Pipeline.Struct() return TunnelServer_reconnectTunnel_Params{s}, err } func (p TunnelServer_reconnectTunnel_Params_Promise) Options() RegistrationOptions_Promise { return RegistrationOptions_Promise{Pipeline: p.Pipeline.GetPipeline(4)} } type TunnelServer_reconnectTunnel_Results struct{ capnp.Struct } // TunnelServer_reconnectTunnel_Results_TypeID is the unique identifier for the type TunnelServer_reconnectTunnel_Results. const TunnelServer_reconnectTunnel_Results_TypeID = 0xd4d18de97bb12de3 func NewTunnelServer_reconnectTunnel_Results(s *capnp.Segment) (TunnelServer_reconnectTunnel_Results, error) { st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) return TunnelServer_reconnectTunnel_Results{st}, err } func NewRootTunnelServer_reconnectTunnel_Results(s *capnp.Segment) (TunnelServer_reconnectTunnel_Results, error) { st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) return TunnelServer_reconnectTunnel_Results{st}, err } func ReadRootTunnelServer_reconnectTunnel_Results(msg *capnp.Message) (TunnelServer_reconnectTunnel_Results, error) { root, err := msg.RootPtr() return TunnelServer_reconnectTunnel_Results{root.Struct()}, err } func (s TunnelServer_reconnectTunnel_Results) String() string { str, _ := text.Marshal(0xd4d18de97bb12de3, s.Struct) return str } func (s TunnelServer_reconnectTunnel_Results) Result() (TunnelRegistration, error) { p, err := s.Struct.Ptr(0) return TunnelRegistration{Struct: p.Struct()}, err } func (s TunnelServer_reconnectTunnel_Results) HasResult() bool { p, err := s.Struct.Ptr(0) return p.IsValid() || err != nil } func (s TunnelServer_reconnectTunnel_Results) SetResult(v TunnelRegistration) error { return s.Struct.SetPtr(0, v.Struct.ToPtr()) } // NewResult sets the result field to a newly // allocated TunnelRegistration struct, preferring placement in s's segment. func (s TunnelServer_reconnectTunnel_Results) NewResult() (TunnelRegistration, error) { ss, err := NewTunnelRegistration(s.Struct.Segment()) if err != nil { return TunnelRegistration{}, err } err = s.Struct.SetPtr(0, ss.Struct.ToPtr()) return ss, err } // TunnelServer_reconnectTunnel_Results_List is a list of TunnelServer_reconnectTunnel_Results. type TunnelServer_reconnectTunnel_Results_List struct{ capnp.List } // NewTunnelServer_reconnectTunnel_Results creates a new list of TunnelServer_reconnectTunnel_Results. func NewTunnelServer_reconnectTunnel_Results_List(s *capnp.Segment, sz int32) (TunnelServer_reconnectTunnel_Results_List, error) { l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}, sz) return TunnelServer_reconnectTunnel_Results_List{l}, err } func (s TunnelServer_reconnectTunnel_Results_List) At(i int) TunnelServer_reconnectTunnel_Results { return TunnelServer_reconnectTunnel_Results{s.List.Struct(i)} } func (s TunnelServer_reconnectTunnel_Results_List) Set(i int, v TunnelServer_reconnectTunnel_Results) error { return s.List.SetStruct(i, v.Struct) } func (s TunnelServer_reconnectTunnel_Results_List) String() string { str, _ := text.MarshalList(0xd4d18de97bb12de3, s.List) return str } // TunnelServer_reconnectTunnel_Results_Promise is a wrapper for a TunnelServer_reconnectTunnel_Results promised by a client call. type TunnelServer_reconnectTunnel_Results_Promise struct{ *capnp.Pipeline } func (p TunnelServer_reconnectTunnel_Results_Promise) Struct() (TunnelServer_reconnectTunnel_Results, error) { s, err := p.Pipeline.Struct() return TunnelServer_reconnectTunnel_Results{s}, err } func (p TunnelServer_reconnectTunnel_Results_Promise) Result() TunnelRegistration_Promise { return TunnelRegistration_Promise{Pipeline: p.Pipeline.GetPipeline(0)} } type Tag struct{ capnp.Struct } // Tag_TypeID is the unique identifier for the type Tag. const Tag_TypeID = 0xcbd96442ae3bb01a func NewTag(s *capnp.Segment) (Tag, error) { st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2}) return Tag{st}, err } func NewRootTag(s *capnp.Segment) (Tag, error) { st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2}) return Tag{st}, err } func ReadRootTag(msg *capnp.Message) (Tag, error) { root, err := msg.RootPtr() return Tag{root.Struct()}, err } func (s Tag) String() string { str, _ := text.Marshal(0xcbd96442ae3bb01a, s.Struct) return str } func (s Tag) Name() (string, error) { p, err := s.Struct.Ptr(0) return p.Text(), err } func (s Tag) HasName() bool { p, err := s.Struct.Ptr(0) return p.IsValid() || err != nil } func (s Tag) NameBytes() ([]byte, error) { p, err := s.Struct.Ptr(0) return p.TextBytes(), err } func (s Tag) SetName(v string) error { return s.Struct.SetText(0, v) } func (s Tag) Value() (string, error) { p, err := s.Struct.Ptr(1) return p.Text(), err } func (s Tag) HasValue() bool { p, err := s.Struct.Ptr(1) return p.IsValid() || err != nil } func (s Tag) ValueBytes() ([]byte, error) { p, err := s.Struct.Ptr(1) return p.TextBytes(), err } func (s Tag) SetValue(v string) error { return s.Struct.SetText(1, v) } // Tag_List is a list of Tag. type Tag_List struct{ capnp.List } // NewTag creates a new list of Tag. func NewTag_List(s *capnp.Segment, sz int32) (Tag_List, error) { l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2}, sz) return Tag_List{l}, err } func (s Tag_List) At(i int) Tag { return Tag{s.List.Struct(i)} } func (s Tag_List) Set(i int, v Tag) error { return s.List.SetStruct(i, v.Struct) } func (s Tag_List) String() string { str, _ := text.MarshalList(0xcbd96442ae3bb01a, s.List) return str } // Tag_Promise is a wrapper for a Tag promised by a client call. type Tag_Promise struct{ *capnp.Pipeline } func (p Tag_Promise) Struct() (Tag, error) { s, err := p.Pipeline.Struct() return Tag{s}, err } type ClientInfo struct{ capnp.Struct } // ClientInfo_TypeID is the unique identifier for the type ClientInfo. const ClientInfo_TypeID = 0x83ced0145b2f114b func NewClientInfo(s *capnp.Segment) (ClientInfo, error) { st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 4}) return ClientInfo{st}, err } func NewRootClientInfo(s *capnp.Segment) (ClientInfo, error) { st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 4}) return ClientInfo{st}, err } func ReadRootClientInfo(msg *capnp.Message) (ClientInfo, error) { root, err := msg.RootPtr() return ClientInfo{root.Struct()}, err } func (s ClientInfo) String() string { str, _ := text.Marshal(0x83ced0145b2f114b, s.Struct) return str } func (s ClientInfo) ClientId() ([]byte, error) { p, err := s.Struct.Ptr(0) return []byte(p.Data()), err } func (s ClientInfo) HasClientId() bool { p, err := s.Struct.Ptr(0) return p.IsValid() || err != nil } func (s ClientInfo) SetClientId(v []byte) error { return s.Struct.SetData(0, v) } func (s ClientInfo) Features() (capnp.TextList, error) { p, err := s.Struct.Ptr(1) return capnp.TextList{List: p.List()}, err } func (s ClientInfo) HasFeatures() bool { p, err := s.Struct.Ptr(1) return p.IsValid() || err != nil } func (s ClientInfo) SetFeatures(v capnp.TextList) error { return s.Struct.SetPtr(1, v.List.ToPtr()) } // NewFeatures sets the features field to a newly // allocated capnp.TextList, preferring placement in s's segment. func (s ClientInfo) NewFeatures(n int32) (capnp.TextList, error) { l, err := capnp.NewTextList(s.Struct.Segment(), n) if err != nil { return capnp.TextList{}, err } err = s.Struct.SetPtr(1, l.List.ToPtr()) return l, err } func (s ClientInfo) Version() (string, error) { p, err := s.Struct.Ptr(2) return p.Text(), err } func (s ClientInfo) HasVersion() bool { p, err := s.Struct.Ptr(2) return p.IsValid() || err != nil } func (s ClientInfo) VersionBytes() ([]byte, error) { p, err := s.Struct.Ptr(2) return p.TextBytes(), err } func (s ClientInfo) SetVersion(v string) error { return s.Struct.SetText(2, v) } func (s ClientInfo) Arch() (string, error) { p, err := s.Struct.Ptr(3) return p.Text(), err } func (s ClientInfo) HasArch() bool { p, err := s.Struct.Ptr(3) return p.IsValid() || err != nil } func (s ClientInfo) ArchBytes() ([]byte, error) { p, err := s.Struct.Ptr(3) return p.TextBytes(), err } func (s ClientInfo) SetArch(v string) error { return s.Struct.SetText(3, v) } // ClientInfo_List is a list of ClientInfo. type ClientInfo_List struct{ capnp.List } // NewClientInfo creates a new list of ClientInfo. func NewClientInfo_List(s *capnp.Segment, sz int32) (ClientInfo_List, error) { l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 4}, sz) return ClientInfo_List{l}, err } func (s ClientInfo_List) At(i int) ClientInfo { return ClientInfo{s.List.Struct(i)} } func (s ClientInfo_List) Set(i int, v ClientInfo) error { return s.List.SetStruct(i, v.Struct) } func (s ClientInfo_List) String() string { str, _ := text.MarshalList(0x83ced0145b2f114b, s.List) return str } // ClientInfo_Promise is a wrapper for a ClientInfo promised by a client call. type ClientInfo_Promise struct{ *capnp.Pipeline } func (p ClientInfo_Promise) Struct() (ClientInfo, error) { s, err := p.Pipeline.Struct() return ClientInfo{s}, err } type ConnectionOptions struct{ capnp.Struct } // ConnectionOptions_TypeID is the unique identifier for the type ConnectionOptions. const ConnectionOptions_TypeID = 0xb4bf9861fe035d04 func NewConnectionOptions(s *capnp.Segment) (ConnectionOptions, error) { st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 2}) return ConnectionOptions{st}, err } func NewRootConnectionOptions(s *capnp.Segment) (ConnectionOptions, error) { st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 2}) return ConnectionOptions{st}, err } func ReadRootConnectionOptions(msg *capnp.Message) (ConnectionOptions, error) { root, err := msg.RootPtr() return ConnectionOptions{root.Struct()}, err } func (s ConnectionOptions) String() string { str, _ := text.Marshal(0xb4bf9861fe035d04, s.Struct) return str } func (s ConnectionOptions) Client() (ClientInfo, error) { p, err := s.Struct.Ptr(0) return ClientInfo{Struct: p.Struct()}, err } func (s ConnectionOptions) HasClient() bool { p, err := s.Struct.Ptr(0) return p.IsValid() || err != nil } func (s ConnectionOptions) SetClient(v ClientInfo) error { return s.Struct.SetPtr(0, v.Struct.ToPtr()) } // NewClient sets the client field to a newly // allocated ClientInfo struct, preferring placement in s's segment. func (s ConnectionOptions) NewClient() (ClientInfo, error) { ss, err := NewClientInfo(s.Struct.Segment()) if err != nil { return ClientInfo{}, err } err = s.Struct.SetPtr(0, ss.Struct.ToPtr()) return ss, err } func (s ConnectionOptions) OriginLocalIp() ([]byte, error) { p, err := s.Struct.Ptr(1) return []byte(p.Data()), err } func (s ConnectionOptions) HasOriginLocalIp() bool { p, err := s.Struct.Ptr(1) return p.IsValid() || err != nil } func (s ConnectionOptions) SetOriginLocalIp(v []byte) error { return s.Struct.SetData(1, v) } func (s ConnectionOptions) ReplaceExisting() bool { return s.Struct.Bit(0) } func (s ConnectionOptions) SetReplaceExisting(v bool) { s.Struct.SetBit(0, v) } func (s ConnectionOptions) CompressionQuality() uint8 { return s.Struct.Uint8(1) } func (s ConnectionOptions) SetCompressionQuality(v uint8) { s.Struct.SetUint8(1, v) } func (s ConnectionOptions) NumPreviousAttempts() uint8 { return s.Struct.Uint8(2) } func (s ConnectionOptions) SetNumPreviousAttempts(v uint8) { s.Struct.SetUint8(2, v) } // ConnectionOptions_List is a list of ConnectionOptions. type ConnectionOptions_List struct{ capnp.List } // NewConnectionOptions creates a new list of ConnectionOptions. func NewConnectionOptions_List(s *capnp.Segment, sz int32) (ConnectionOptions_List, error) { l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 8, PointerCount: 2}, sz) return ConnectionOptions_List{l}, err } func (s ConnectionOptions_List) At(i int) ConnectionOptions { return ConnectionOptions{s.List.Struct(i)} } func (s ConnectionOptions_List) Set(i int, v ConnectionOptions) error { return s.List.SetStruct(i, v.Struct) } func (s ConnectionOptions_List) String() string { str, _ := text.MarshalList(0xb4bf9861fe035d04, s.List) return str } // ConnectionOptions_Promise is a wrapper for a ConnectionOptions promised by a client call. type ConnectionOptions_Promise struct{ *capnp.Pipeline } func (p ConnectionOptions_Promise) Struct() (ConnectionOptions, error) { s, err := p.Pipeline.Struct() return ConnectionOptions{s}, err } func (p ConnectionOptions_Promise) Client() ClientInfo_Promise { return ClientInfo_Promise{Pipeline: p.Pipeline.GetPipeline(0)} } type ConnectionResponse struct{ capnp.Struct } type ConnectionResponse_result ConnectionResponse type ConnectionResponse_result_Which uint16 const ( ConnectionResponse_result_Which_error ConnectionResponse_result_Which = 0 ConnectionResponse_result_Which_connectionDetails ConnectionResponse_result_Which = 1 ) func (w ConnectionResponse_result_Which) String() string { const s = "errorconnectionDetails" switch w { case ConnectionResponse_result_Which_error: return s[0:5] case ConnectionResponse_result_Which_connectionDetails: return s[5:22] } return "ConnectionResponse_result_Which(" + strconv.FormatUint(uint64(w), 10) + ")" } // ConnectionResponse_TypeID is the unique identifier for the type ConnectionResponse. const ConnectionResponse_TypeID = 0xdbaa9d03d52b62dc func NewConnectionResponse(s *capnp.Segment) (ConnectionResponse, error) { st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 1}) return ConnectionResponse{st}, err } func NewRootConnectionResponse(s *capnp.Segment) (ConnectionResponse, error) { st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 1}) return ConnectionResponse{st}, err } func ReadRootConnectionResponse(msg *capnp.Message) (ConnectionResponse, error) { root, err := msg.RootPtr() return ConnectionResponse{root.Struct()}, err } func (s ConnectionResponse) String() string { str, _ := text.Marshal(0xdbaa9d03d52b62dc, s.Struct) return str } func (s ConnectionResponse) Result() ConnectionResponse_result { return ConnectionResponse_result(s) } func (s ConnectionResponse_result) Which() ConnectionResponse_result_Which { return ConnectionResponse_result_Which(s.Struct.Uint16(0)) } func (s ConnectionResponse_result) Error() (ConnectionError, error) { if s.Struct.Uint16(0) != 0 { panic("Which() != error") } p, err := s.Struct.Ptr(0) return ConnectionError{Struct: p.Struct()}, err } func (s ConnectionResponse_result) HasError() bool { if s.Struct.Uint16(0) != 0 { return false } p, err := s.Struct.Ptr(0) return p.IsValid() || err != nil } func (s ConnectionResponse_result) SetError(v ConnectionError) error { s.Struct.SetUint16(0, 0) return s.Struct.SetPtr(0, v.Struct.ToPtr()) } // NewError sets the error field to a newly // allocated ConnectionError struct, preferring placement in s's segment. func (s ConnectionResponse_result) NewError() (ConnectionError, error) { s.Struct.SetUint16(0, 0) ss, err := NewConnectionError(s.Struct.Segment()) if err != nil { return ConnectionError{}, err } err = s.Struct.SetPtr(0, ss.Struct.ToPtr()) return ss, err } func (s ConnectionResponse_result) ConnectionDetails() (ConnectionDetails, error) { if s.Struct.Uint16(0) != 1 { panic("Which() != connectionDetails") } p, err := s.Struct.Ptr(0) return ConnectionDetails{Struct: p.Struct()}, err } func (s ConnectionResponse_result) HasConnectionDetails() bool { if s.Struct.Uint16(0) != 1 { return false } p, err := s.Struct.Ptr(0) return p.IsValid() || err != nil } func (s ConnectionResponse_result) SetConnectionDetails(v ConnectionDetails) error { s.Struct.SetUint16(0, 1) return s.Struct.SetPtr(0, v.Struct.ToPtr()) } // NewConnectionDetails sets the connectionDetails field to a newly // allocated ConnectionDetails struct, preferring placement in s's segment. func (s ConnectionResponse_result) NewConnectionDetails() (ConnectionDetails, error) { s.Struct.SetUint16(0, 1) ss, err := NewConnectionDetails(s.Struct.Segment()) if err != nil { return ConnectionDetails{}, err } err = s.Struct.SetPtr(0, ss.Struct.ToPtr()) return ss, err } // ConnectionResponse_List is a list of ConnectionResponse. type ConnectionResponse_List struct{ capnp.List } // NewConnectionResponse creates a new list of ConnectionResponse. func NewConnectionResponse_List(s *capnp.Segment, sz int32) (ConnectionResponse_List, error) { l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 8, PointerCount: 1}, sz) return ConnectionResponse_List{l}, err } func (s ConnectionResponse_List) At(i int) ConnectionResponse { return ConnectionResponse{s.List.Struct(i)} } func (s ConnectionResponse_List) Set(i int, v ConnectionResponse) error { return s.List.SetStruct(i, v.Struct) } func (s ConnectionResponse_List) String() string { str, _ := text.MarshalList(0xdbaa9d03d52b62dc, s.List) return str } // ConnectionResponse_Promise is a wrapper for a ConnectionResponse promised by a client call. type ConnectionResponse_Promise struct{ *capnp.Pipeline } func (p ConnectionResponse_Promise) Struct() (ConnectionResponse, error) { s, err := p.Pipeline.Struct() return ConnectionResponse{s}, err } func (p ConnectionResponse_Promise) Result() ConnectionResponse_result_Promise { return ConnectionResponse_result_Promise{p.Pipeline} } // ConnectionResponse_result_Promise is a wrapper for a ConnectionResponse_result promised by a client call. type ConnectionResponse_result_Promise struct{ *capnp.Pipeline } func (p ConnectionResponse_result_Promise) Struct() (ConnectionResponse_result, error) { s, err := p.Pipeline.Struct() return ConnectionResponse_result{s}, err } func (p ConnectionResponse_result_Promise) Error() ConnectionError_Promise { return ConnectionError_Promise{Pipeline: p.Pipeline.GetPipeline(0)} } func (p ConnectionResponse_result_Promise) ConnectionDetails() ConnectionDetails_Promise { return ConnectionDetails_Promise{Pipeline: p.Pipeline.GetPipeline(0)} } type ConnectionError struct{ capnp.Struct } // ConnectionError_TypeID is the unique identifier for the type ConnectionError. const ConnectionError_TypeID = 0xf5f383d2785edb86 func NewConnectionError(s *capnp.Segment) (ConnectionError, error) { st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 16, PointerCount: 1}) return ConnectionError{st}, err } func NewRootConnectionError(s *capnp.Segment) (ConnectionError, error) { st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 16, PointerCount: 1}) return ConnectionError{st}, err } func ReadRootConnectionError(msg *capnp.Message) (ConnectionError, error) { root, err := msg.RootPtr() return ConnectionError{root.Struct()}, err } func (s ConnectionError) String() string { str, _ := text.Marshal(0xf5f383d2785edb86, s.Struct) return str } func (s ConnectionError) Cause() (string, error) { p, err := s.Struct.Ptr(0) return p.Text(), err } func (s ConnectionError) HasCause() bool { p, err := s.Struct.Ptr(0) return p.IsValid() || err != nil } func (s ConnectionError) CauseBytes() ([]byte, error) { p, err := s.Struct.Ptr(0) return p.TextBytes(), err } func (s ConnectionError) SetCause(v string) error { return s.Struct.SetText(0, v) } func (s ConnectionError) RetryAfter() int64 { return int64(s.Struct.Uint64(0)) } func (s ConnectionError) SetRetryAfter(v int64) { s.Struct.SetUint64(0, uint64(v)) } func (s ConnectionError) ShouldRetry() bool { return s.Struct.Bit(64) } func (s ConnectionError) SetShouldRetry(v bool) { s.Struct.SetBit(64, v) } // ConnectionError_List is a list of ConnectionError. type ConnectionError_List struct{ capnp.List } // NewConnectionError creates a new list of ConnectionError. func NewConnectionError_List(s *capnp.Segment, sz int32) (ConnectionError_List, error) { l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 16, PointerCount: 1}, sz) return ConnectionError_List{l}, err } func (s ConnectionError_List) At(i int) ConnectionError { return ConnectionError{s.List.Struct(i)} } func (s ConnectionError_List) Set(i int, v ConnectionError) error { return s.List.SetStruct(i, v.Struct) } func (s ConnectionError_List) String() string { str, _ := text.MarshalList(0xf5f383d2785edb86, s.List) return str } // ConnectionError_Promise is a wrapper for a ConnectionError promised by a client call. type ConnectionError_Promise struct{ *capnp.Pipeline } func (p ConnectionError_Promise) Struct() (ConnectionError, error) { s, err := p.Pipeline.Struct() return ConnectionError{s}, err } type ConnectionDetails struct{ capnp.Struct } // ConnectionDetails_TypeID is the unique identifier for the type ConnectionDetails. const ConnectionDetails_TypeID = 0xb5f39f082b9ac18a func NewConnectionDetails(s *capnp.Segment) (ConnectionDetails, error) { st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 2}) return ConnectionDetails{st}, err } func NewRootConnectionDetails(s *capnp.Segment) (ConnectionDetails, error) { st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 2}) return ConnectionDetails{st}, err } func ReadRootConnectionDetails(msg *capnp.Message) (ConnectionDetails, error) { root, err := msg.RootPtr() return ConnectionDetails{root.Struct()}, err } func (s ConnectionDetails) String() string { str, _ := text.Marshal(0xb5f39f082b9ac18a, s.Struct) return str } func (s ConnectionDetails) Uuid() ([]byte, error) { p, err := s.Struct.Ptr(0) return []byte(p.Data()), err } func (s ConnectionDetails) HasUuid() bool { p, err := s.Struct.Ptr(0) return p.IsValid() || err != nil } func (s ConnectionDetails) SetUuid(v []byte) error { return s.Struct.SetData(0, v) } func (s ConnectionDetails) LocationName() (string, error) { p, err := s.Struct.Ptr(1) return p.Text(), err } func (s ConnectionDetails) HasLocationName() bool { p, err := s.Struct.Ptr(1) return p.IsValid() || err != nil } func (s ConnectionDetails) LocationNameBytes() ([]byte, error) { p, err := s.Struct.Ptr(1) return p.TextBytes(), err } func (s ConnectionDetails) SetLocationName(v string) error { return s.Struct.SetText(1, v) } func (s ConnectionDetails) TunnelIsRemotelyManaged() bool { return s.Struct.Bit(0) } func (s ConnectionDetails) SetTunnelIsRemotelyManaged(v bool) { s.Struct.SetBit(0, v) } // ConnectionDetails_List is a list of ConnectionDetails. type ConnectionDetails_List struct{ capnp.List } // NewConnectionDetails creates a new list of ConnectionDetails. func NewConnectionDetails_List(s *capnp.Segment, sz int32) (ConnectionDetails_List, error) { l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 8, PointerCount: 2}, sz) return ConnectionDetails_List{l}, err } func (s ConnectionDetails_List) At(i int) ConnectionDetails { return ConnectionDetails{s.List.Struct(i)} } func (s ConnectionDetails_List) Set(i int, v ConnectionDetails) error { return s.List.SetStruct(i, v.Struct) } func (s ConnectionDetails_List) String() string { str, _ := text.MarshalList(0xb5f39f082b9ac18a, s.List) return str } // ConnectionDetails_Promise is a wrapper for a ConnectionDetails promised by a client call. type ConnectionDetails_Promise struct{ *capnp.Pipeline } func (p ConnectionDetails_Promise) Struct() (ConnectionDetails, error) { s, err := p.Pipeline.Struct() return ConnectionDetails{s}, err } type TunnelAuth struct{ capnp.Struct } // TunnelAuth_TypeID is the unique identifier for the type TunnelAuth. const TunnelAuth_TypeID = 0x9496331ab9cd463f func NewTunnelAuth(s *capnp.Segment) (TunnelAuth, error) { st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2}) return TunnelAuth{st}, err } func NewRootTunnelAuth(s *capnp.Segment) (TunnelAuth, error) { st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2}) return TunnelAuth{st}, err } func ReadRootTunnelAuth(msg *capnp.Message) (TunnelAuth, error) { root, err := msg.RootPtr() return TunnelAuth{root.Struct()}, err } func (s TunnelAuth) String() string { str, _ := text.Marshal(0x9496331ab9cd463f, s.Struct) return str } func (s TunnelAuth) AccountTag() (string, error) { p, err := s.Struct.Ptr(0) return p.Text(), err } func (s TunnelAuth) HasAccountTag() bool { p, err := s.Struct.Ptr(0) return p.IsValid() || err != nil } func (s TunnelAuth) AccountTagBytes() ([]byte, error) { p, err := s.Struct.Ptr(0) return p.TextBytes(), err } func (s TunnelAuth) SetAccountTag(v string) error { return s.Struct.SetText(0, v) } func (s TunnelAuth) TunnelSecret() ([]byte, error) { p, err := s.Struct.Ptr(1) return []byte(p.Data()), err } func (s TunnelAuth) HasTunnelSecret() bool { p, err := s.Struct.Ptr(1) return p.IsValid() || err != nil } func (s TunnelAuth) SetTunnelSecret(v []byte) error { return s.Struct.SetData(1, v) } // TunnelAuth_List is a list of TunnelAuth. type TunnelAuth_List struct{ capnp.List } // NewTunnelAuth creates a new list of TunnelAuth. func NewTunnelAuth_List(s *capnp.Segment, sz int32) (TunnelAuth_List, error) { l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2}, sz) return TunnelAuth_List{l}, err } func (s TunnelAuth_List) At(i int) TunnelAuth { return TunnelAuth{s.List.Struct(i)} } func (s TunnelAuth_List) Set(i int, v TunnelAuth) error { return s.List.SetStruct(i, v.Struct) } func (s TunnelAuth_List) String() string { str, _ := text.MarshalList(0x9496331ab9cd463f, s.List) return str } // TunnelAuth_Promise is a wrapper for a TunnelAuth promised by a client call. type TunnelAuth_Promise struct{ *capnp.Pipeline } func (p TunnelAuth_Promise) Struct() (TunnelAuth, error) { s, err := p.Pipeline.Struct() return TunnelAuth{s}, err } type RegistrationServer struct{ Client capnp.Client } // RegistrationServer_TypeID is the unique identifier for the type RegistrationServer. const RegistrationServer_TypeID = 0xf71695ec7fe85497 func (c RegistrationServer) RegisterConnection(ctx context.Context, params func(RegistrationServer_registerConnection_Params) error, opts ...capnp.CallOption) RegistrationServer_registerConnection_Results_Promise { if c.Client == nil { return RegistrationServer_registerConnection_Results_Promise{Pipeline: capnp.NewPipeline(capnp.ErrorAnswer(capnp.ErrNullClient))} } call := &capnp.Call{ Ctx: ctx, Method: capnp.Method{ InterfaceID: 0xf71695ec7fe85497, MethodID: 0, InterfaceName: "tunnelrpc/proto/tunnelrpc.capnp:RegistrationServer", MethodName: "registerConnection", }, Options: capnp.NewCallOptions(opts), } if params != nil { call.ParamsSize = capnp.ObjectSize{DataSize: 8, PointerCount: 3} call.ParamsFunc = func(s capnp.Struct) error { return params(RegistrationServer_registerConnection_Params{Struct: s}) } } return RegistrationServer_registerConnection_Results_Promise{Pipeline: capnp.NewPipeline(c.Client.Call(call))} } func (c RegistrationServer) UnregisterConnection(ctx context.Context, params func(RegistrationServer_unregisterConnection_Params) error, opts ...capnp.CallOption) RegistrationServer_unregisterConnection_Results_Promise { if c.Client == nil { return RegistrationServer_unregisterConnection_Results_Promise{Pipeline: capnp.NewPipeline(capnp.ErrorAnswer(capnp.ErrNullClient))} } call := &capnp.Call{ Ctx: ctx, Method: capnp.Method{ InterfaceID: 0xf71695ec7fe85497, MethodID: 1, InterfaceName: "tunnelrpc/proto/tunnelrpc.capnp:RegistrationServer", MethodName: "unregisterConnection", }, Options: capnp.NewCallOptions(opts), } if params != nil { call.ParamsSize = capnp.ObjectSize{DataSize: 0, PointerCount: 0} call.ParamsFunc = func(s capnp.Struct) error { return params(RegistrationServer_unregisterConnection_Params{Struct: s}) } } return RegistrationServer_unregisterConnection_Results_Promise{Pipeline: capnp.NewPipeline(c.Client.Call(call))} } func (c RegistrationServer) UpdateLocalConfiguration(ctx context.Context, params func(RegistrationServer_updateLocalConfiguration_Params) error, opts ...capnp.CallOption) RegistrationServer_updateLocalConfiguration_Results_Promise { if c.Client == nil { return RegistrationServer_updateLocalConfiguration_Results_Promise{Pipeline: capnp.NewPipeline(capnp.ErrorAnswer(capnp.ErrNullClient))} } call := &capnp.Call{ Ctx: ctx, Method: capnp.Method{ InterfaceID: 0xf71695ec7fe85497, MethodID: 2, InterfaceName: "tunnelrpc/proto/tunnelrpc.capnp:RegistrationServer", MethodName: "updateLocalConfiguration", }, Options: capnp.NewCallOptions(opts), } if params != nil { call.ParamsSize = capnp.ObjectSize{DataSize: 0, PointerCount: 1} call.ParamsFunc = func(s capnp.Struct) error { return params(RegistrationServer_updateLocalConfiguration_Params{Struct: s}) } } return RegistrationServer_updateLocalConfiguration_Results_Promise{Pipeline: capnp.NewPipeline(c.Client.Call(call))} } type RegistrationServer_Server interface { RegisterConnection(RegistrationServer_registerConnection) error UnregisterConnection(RegistrationServer_unregisterConnection) error UpdateLocalConfiguration(RegistrationServer_updateLocalConfiguration) error } func RegistrationServer_ServerToClient(s RegistrationServer_Server) RegistrationServer { c, _ := s.(server.Closer) return RegistrationServer{Client: server.New(RegistrationServer_Methods(nil, s), c)} } func RegistrationServer_Methods(methods []server.Method, s RegistrationServer_Server) []server.Method { if cap(methods) == 0 { methods = make([]server.Method, 0, 3) } methods = append(methods, server.Method{ Method: capnp.Method{ InterfaceID: 0xf71695ec7fe85497, MethodID: 0, InterfaceName: "tunnelrpc/proto/tunnelrpc.capnp:RegistrationServer", MethodName: "registerConnection", }, Impl: func(c context.Context, opts capnp.CallOptions, p, r capnp.Struct) error { call := RegistrationServer_registerConnection{c, opts, RegistrationServer_registerConnection_Params{Struct: p}, RegistrationServer_registerConnection_Results{Struct: r}} return s.RegisterConnection(call) }, ResultsSize: capnp.ObjectSize{DataSize: 0, PointerCount: 1}, }) methods = append(methods, server.Method{ Method: capnp.Method{ InterfaceID: 0xf71695ec7fe85497, MethodID: 1, InterfaceName: "tunnelrpc/proto/tunnelrpc.capnp:RegistrationServer", MethodName: "unregisterConnection", }, Impl: func(c context.Context, opts capnp.CallOptions, p, r capnp.Struct) error { call := RegistrationServer_unregisterConnection{c, opts, RegistrationServer_unregisterConnection_Params{Struct: p}, RegistrationServer_unregisterConnection_Results{Struct: r}} return s.UnregisterConnection(call) }, ResultsSize: capnp.ObjectSize{DataSize: 0, PointerCount: 0}, }) methods = append(methods, server.Method{ Method: capnp.Method{ InterfaceID: 0xf71695ec7fe85497, MethodID: 2, InterfaceName: "tunnelrpc/proto/tunnelrpc.capnp:RegistrationServer", MethodName: "updateLocalConfiguration", }, Impl: func(c context.Context, opts capnp.CallOptions, p, r capnp.Struct) error { call := RegistrationServer_updateLocalConfiguration{c, opts, RegistrationServer_updateLocalConfiguration_Params{Struct: p}, RegistrationServer_updateLocalConfiguration_Results{Struct: r}} return s.UpdateLocalConfiguration(call) }, ResultsSize: capnp.ObjectSize{DataSize: 0, PointerCount: 0}, }) return methods } // RegistrationServer_registerConnection holds the arguments for a server call to RegistrationServer.registerConnection. type RegistrationServer_registerConnection struct { Ctx context.Context Options capnp.CallOptions Params RegistrationServer_registerConnection_Params Results RegistrationServer_registerConnection_Results } // RegistrationServer_unregisterConnection holds the arguments for a server call to RegistrationServer.unregisterConnection. type RegistrationServer_unregisterConnection struct { Ctx context.Context Options capnp.CallOptions Params RegistrationServer_unregisterConnection_Params Results RegistrationServer_unregisterConnection_Results } // RegistrationServer_updateLocalConfiguration holds the arguments for a server call to RegistrationServer.updateLocalConfiguration. type RegistrationServer_updateLocalConfiguration struct { Ctx context.Context Options capnp.CallOptions Params RegistrationServer_updateLocalConfiguration_Params Results RegistrationServer_updateLocalConfiguration_Results } type RegistrationServer_registerConnection_Params struct{ capnp.Struct } // RegistrationServer_registerConnection_Params_TypeID is the unique identifier for the type RegistrationServer_registerConnection_Params. const RegistrationServer_registerConnection_Params_TypeID = 0xe6646dec8feaa6ee func NewRegistrationServer_registerConnection_Params(s *capnp.Segment) (RegistrationServer_registerConnection_Params, error) { st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 3}) return RegistrationServer_registerConnection_Params{st}, err } func NewRootRegistrationServer_registerConnection_Params(s *capnp.Segment) (RegistrationServer_registerConnection_Params, error) { st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 3}) return RegistrationServer_registerConnection_Params{st}, err } func ReadRootRegistrationServer_registerConnection_Params(msg *capnp.Message) (RegistrationServer_registerConnection_Params, error) { root, err := msg.RootPtr() return RegistrationServer_registerConnection_Params{root.Struct()}, err } func (s RegistrationServer_registerConnection_Params) String() string { str, _ := text.Marshal(0xe6646dec8feaa6ee, s.Struct) return str } func (s RegistrationServer_registerConnection_Params) Auth() (TunnelAuth, error) { p, err := s.Struct.Ptr(0) return TunnelAuth{Struct: p.Struct()}, err } func (s RegistrationServer_registerConnection_Params) HasAuth() bool { p, err := s.Struct.Ptr(0) return p.IsValid() || err != nil } func (s RegistrationServer_registerConnection_Params) SetAuth(v TunnelAuth) error { return s.Struct.SetPtr(0, v.Struct.ToPtr()) } // NewAuth sets the auth field to a newly // allocated TunnelAuth struct, preferring placement in s's segment. func (s RegistrationServer_registerConnection_Params) NewAuth() (TunnelAuth, error) { ss, err := NewTunnelAuth(s.Struct.Segment()) if err != nil { return TunnelAuth{}, err } err = s.Struct.SetPtr(0, ss.Struct.ToPtr()) return ss, err } func (s RegistrationServer_registerConnection_Params) TunnelId() ([]byte, error) { p, err := s.Struct.Ptr(1) return []byte(p.Data()), err } func (s RegistrationServer_registerConnection_Params) HasTunnelId() bool { p, err := s.Struct.Ptr(1) return p.IsValid() || err != nil } func (s RegistrationServer_registerConnection_Params) SetTunnelId(v []byte) error { return s.Struct.SetData(1, v) } func (s RegistrationServer_registerConnection_Params) ConnIndex() uint8 { return s.Struct.Uint8(0) } func (s RegistrationServer_registerConnection_Params) SetConnIndex(v uint8) { s.Struct.SetUint8(0, v) } func (s RegistrationServer_registerConnection_Params) Options() (ConnectionOptions, error) { p, err := s.Struct.Ptr(2) return ConnectionOptions{Struct: p.Struct()}, err } func (s RegistrationServer_registerConnection_Params) HasOptions() bool { p, err := s.Struct.Ptr(2) return p.IsValid() || err != nil } func (s RegistrationServer_registerConnection_Params) SetOptions(v ConnectionOptions) error { return s.Struct.SetPtr(2, v.Struct.ToPtr()) } // NewOptions sets the options field to a newly // allocated ConnectionOptions struct, preferring placement in s's segment. func (s RegistrationServer_registerConnection_Params) NewOptions() (ConnectionOptions, error) { ss, err := NewConnectionOptions(s.Struct.Segment()) if err != nil { return ConnectionOptions{}, err } err = s.Struct.SetPtr(2, ss.Struct.ToPtr()) return ss, err } // RegistrationServer_registerConnection_Params_List is a list of RegistrationServer_registerConnection_Params. type RegistrationServer_registerConnection_Params_List struct{ capnp.List } // NewRegistrationServer_registerConnection_Params creates a new list of RegistrationServer_registerConnection_Params. func NewRegistrationServer_registerConnection_Params_List(s *capnp.Segment, sz int32) (RegistrationServer_registerConnection_Params_List, error) { l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 8, PointerCount: 3}, sz) return RegistrationServer_registerConnection_Params_List{l}, err } func (s RegistrationServer_registerConnection_Params_List) At(i int) RegistrationServer_registerConnection_Params { return RegistrationServer_registerConnection_Params{s.List.Struct(i)} } func (s RegistrationServer_registerConnection_Params_List) Set(i int, v RegistrationServer_registerConnection_Params) error { return s.List.SetStruct(i, v.Struct) } func (s RegistrationServer_registerConnection_Params_List) String() string { str, _ := text.MarshalList(0xe6646dec8feaa6ee, s.List) return str } // RegistrationServer_registerConnection_Params_Promise is a wrapper for a RegistrationServer_registerConnection_Params promised by a client call. type RegistrationServer_registerConnection_Params_Promise struct{ *capnp.Pipeline } func (p RegistrationServer_registerConnection_Params_Promise) Struct() (RegistrationServer_registerConnection_Params, error) { s, err := p.Pipeline.Struct() return RegistrationServer_registerConnection_Params{s}, err } func (p RegistrationServer_registerConnection_Params_Promise) Auth() TunnelAuth_Promise { return TunnelAuth_Promise{Pipeline: p.Pipeline.GetPipeline(0)} } func (p RegistrationServer_registerConnection_Params_Promise) Options() ConnectionOptions_Promise { return ConnectionOptions_Promise{Pipeline: p.Pipeline.GetPipeline(2)} } type RegistrationServer_registerConnection_Results struct{ capnp.Struct } // RegistrationServer_registerConnection_Results_TypeID is the unique identifier for the type RegistrationServer_registerConnection_Results. const RegistrationServer_registerConnection_Results_TypeID = 0xea50d822450d1f17 func NewRegistrationServer_registerConnection_Results(s *capnp.Segment) (RegistrationServer_registerConnection_Results, error) { st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) return RegistrationServer_registerConnection_Results{st}, err } func NewRootRegistrationServer_registerConnection_Results(s *capnp.Segment) (RegistrationServer_registerConnection_Results, error) { st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) return RegistrationServer_registerConnection_Results{st}, err } func ReadRootRegistrationServer_registerConnection_Results(msg *capnp.Message) (RegistrationServer_registerConnection_Results, error) { root, err := msg.RootPtr() return RegistrationServer_registerConnection_Results{root.Struct()}, err } func (s RegistrationServer_registerConnection_Results) String() string { str, _ := text.Marshal(0xea50d822450d1f17, s.Struct) return str } func (s RegistrationServer_registerConnection_Results) Result() (ConnectionResponse, error) { p, err := s.Struct.Ptr(0) return ConnectionResponse{Struct: p.Struct()}, err } func (s RegistrationServer_registerConnection_Results) HasResult() bool { p, err := s.Struct.Ptr(0) return p.IsValid() || err != nil } func (s RegistrationServer_registerConnection_Results) SetResult(v ConnectionResponse) error { return s.Struct.SetPtr(0, v.Struct.ToPtr()) } // NewResult sets the result field to a newly // allocated ConnectionResponse struct, preferring placement in s's segment. func (s RegistrationServer_registerConnection_Results) NewResult() (ConnectionResponse, error) { ss, err := NewConnectionResponse(s.Struct.Segment()) if err != nil { return ConnectionResponse{}, err } err = s.Struct.SetPtr(0, ss.Struct.ToPtr()) return ss, err } // RegistrationServer_registerConnection_Results_List is a list of RegistrationServer_registerConnection_Results. type RegistrationServer_registerConnection_Results_List struct{ capnp.List } // NewRegistrationServer_registerConnection_Results creates a new list of RegistrationServer_registerConnection_Results. func NewRegistrationServer_registerConnection_Results_List(s *capnp.Segment, sz int32) (RegistrationServer_registerConnection_Results_List, error) { l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}, sz) return RegistrationServer_registerConnection_Results_List{l}, err } func (s RegistrationServer_registerConnection_Results_List) At(i int) RegistrationServer_registerConnection_Results { return RegistrationServer_registerConnection_Results{s.List.Struct(i)} } func (s RegistrationServer_registerConnection_Results_List) Set(i int, v RegistrationServer_registerConnection_Results) error { return s.List.SetStruct(i, v.Struct) } func (s RegistrationServer_registerConnection_Results_List) String() string { str, _ := text.MarshalList(0xea50d822450d1f17, s.List) return str } // RegistrationServer_registerConnection_Results_Promise is a wrapper for a RegistrationServer_registerConnection_Results promised by a client call. type RegistrationServer_registerConnection_Results_Promise struct{ *capnp.Pipeline } func (p RegistrationServer_registerConnection_Results_Promise) Struct() (RegistrationServer_registerConnection_Results, error) { s, err := p.Pipeline.Struct() return RegistrationServer_registerConnection_Results{s}, err } func (p RegistrationServer_registerConnection_Results_Promise) Result() ConnectionResponse_Promise { return ConnectionResponse_Promise{Pipeline: p.Pipeline.GetPipeline(0)} } type RegistrationServer_unregisterConnection_Params struct{ capnp.Struct } // RegistrationServer_unregisterConnection_Params_TypeID is the unique identifier for the type RegistrationServer_unregisterConnection_Params. const RegistrationServer_unregisterConnection_Params_TypeID = 0xf9cb7f4431a307d0 func NewRegistrationServer_unregisterConnection_Params(s *capnp.Segment) (RegistrationServer_unregisterConnection_Params, error) { st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0}) return RegistrationServer_unregisterConnection_Params{st}, err } func NewRootRegistrationServer_unregisterConnection_Params(s *capnp.Segment) (RegistrationServer_unregisterConnection_Params, error) { st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0}) return RegistrationServer_unregisterConnection_Params{st}, err } func ReadRootRegistrationServer_unregisterConnection_Params(msg *capnp.Message) (RegistrationServer_unregisterConnection_Params, error) { root, err := msg.RootPtr() return RegistrationServer_unregisterConnection_Params{root.Struct()}, err } func (s RegistrationServer_unregisterConnection_Params) String() string { str, _ := text.Marshal(0xf9cb7f4431a307d0, s.Struct) return str } // RegistrationServer_unregisterConnection_Params_List is a list of RegistrationServer_unregisterConnection_Params. type RegistrationServer_unregisterConnection_Params_List struct{ capnp.List } // NewRegistrationServer_unregisterConnection_Params creates a new list of RegistrationServer_unregisterConnection_Params. func NewRegistrationServer_unregisterConnection_Params_List(s *capnp.Segment, sz int32) (RegistrationServer_unregisterConnection_Params_List, error) { l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0}, sz) return RegistrationServer_unregisterConnection_Params_List{l}, err } func (s RegistrationServer_unregisterConnection_Params_List) At(i int) RegistrationServer_unregisterConnection_Params { return RegistrationServer_unregisterConnection_Params{s.List.Struct(i)} } func (s RegistrationServer_unregisterConnection_Params_List) Set(i int, v RegistrationServer_unregisterConnection_Params) error { return s.List.SetStruct(i, v.Struct) } func (s RegistrationServer_unregisterConnection_Params_List) String() string { str, _ := text.MarshalList(0xf9cb7f4431a307d0, s.List) return str } // RegistrationServer_unregisterConnection_Params_Promise is a wrapper for a RegistrationServer_unregisterConnection_Params promised by a client call. type RegistrationServer_unregisterConnection_Params_Promise struct{ *capnp.Pipeline } func (p RegistrationServer_unregisterConnection_Params_Promise) Struct() (RegistrationServer_unregisterConnection_Params, error) { s, err := p.Pipeline.Struct() return RegistrationServer_unregisterConnection_Params{s}, err } type RegistrationServer_unregisterConnection_Results struct{ capnp.Struct } // RegistrationServer_unregisterConnection_Results_TypeID is the unique identifier for the type RegistrationServer_unregisterConnection_Results. const RegistrationServer_unregisterConnection_Results_TypeID = 0xb046e578094b1ead func NewRegistrationServer_unregisterConnection_Results(s *capnp.Segment) (RegistrationServer_unregisterConnection_Results, error) { st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0}) return RegistrationServer_unregisterConnection_Results{st}, err } func NewRootRegistrationServer_unregisterConnection_Results(s *capnp.Segment) (RegistrationServer_unregisterConnection_Results, error) { st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0}) return RegistrationServer_unregisterConnection_Results{st}, err } func ReadRootRegistrationServer_unregisterConnection_Results(msg *capnp.Message) (RegistrationServer_unregisterConnection_Results, error) { root, err := msg.RootPtr() return RegistrationServer_unregisterConnection_Results{root.Struct()}, err } func (s RegistrationServer_unregisterConnection_Results) String() string { str, _ := text.Marshal(0xb046e578094b1ead, s.Struct) return str } // RegistrationServer_unregisterConnection_Results_List is a list of RegistrationServer_unregisterConnection_Results. type RegistrationServer_unregisterConnection_Results_List struct{ capnp.List } // NewRegistrationServer_unregisterConnection_Results creates a new list of RegistrationServer_unregisterConnection_Results. func NewRegistrationServer_unregisterConnection_Results_List(s *capnp.Segment, sz int32) (RegistrationServer_unregisterConnection_Results_List, error) { l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0}, sz) return RegistrationServer_unregisterConnection_Results_List{l}, err } func (s RegistrationServer_unregisterConnection_Results_List) At(i int) RegistrationServer_unregisterConnection_Results { return RegistrationServer_unregisterConnection_Results{s.List.Struct(i)} } func (s RegistrationServer_unregisterConnection_Results_List) Set(i int, v RegistrationServer_unregisterConnection_Results) error { return s.List.SetStruct(i, v.Struct) } func (s RegistrationServer_unregisterConnection_Results_List) String() string { str, _ := text.MarshalList(0xb046e578094b1ead, s.List) return str } // RegistrationServer_unregisterConnection_Results_Promise is a wrapper for a RegistrationServer_unregisterConnection_Results promised by a client call. type RegistrationServer_unregisterConnection_Results_Promise struct{ *capnp.Pipeline } func (p RegistrationServer_unregisterConnection_Results_Promise) Struct() (RegistrationServer_unregisterConnection_Results, error) { s, err := p.Pipeline.Struct() return RegistrationServer_unregisterConnection_Results{s}, err } type RegistrationServer_updateLocalConfiguration_Params struct{ capnp.Struct } // RegistrationServer_updateLocalConfiguration_Params_TypeID is the unique identifier for the type RegistrationServer_updateLocalConfiguration_Params. const RegistrationServer_updateLocalConfiguration_Params_TypeID = 0xc5d6e311876a3604 func NewRegistrationServer_updateLocalConfiguration_Params(s *capnp.Segment) (RegistrationServer_updateLocalConfiguration_Params, error) { st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) return RegistrationServer_updateLocalConfiguration_Params{st}, err } func NewRootRegistrationServer_updateLocalConfiguration_Params(s *capnp.Segment) (RegistrationServer_updateLocalConfiguration_Params, error) { st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) return RegistrationServer_updateLocalConfiguration_Params{st}, err } func ReadRootRegistrationServer_updateLocalConfiguration_Params(msg *capnp.Message) (RegistrationServer_updateLocalConfiguration_Params, error) { root, err := msg.RootPtr() return RegistrationServer_updateLocalConfiguration_Params{root.Struct()}, err } func (s RegistrationServer_updateLocalConfiguration_Params) String() string { str, _ := text.Marshal(0xc5d6e311876a3604, s.Struct) return str } func (s RegistrationServer_updateLocalConfiguration_Params) Config() ([]byte, error) { p, err := s.Struct.Ptr(0) return []byte(p.Data()), err } func (s RegistrationServer_updateLocalConfiguration_Params) HasConfig() bool { p, err := s.Struct.Ptr(0) return p.IsValid() || err != nil } func (s RegistrationServer_updateLocalConfiguration_Params) SetConfig(v []byte) error { return s.Struct.SetData(0, v) } // RegistrationServer_updateLocalConfiguration_Params_List is a list of RegistrationServer_updateLocalConfiguration_Params. type RegistrationServer_updateLocalConfiguration_Params_List struct{ capnp.List } // NewRegistrationServer_updateLocalConfiguration_Params creates a new list of RegistrationServer_updateLocalConfiguration_Params. func NewRegistrationServer_updateLocalConfiguration_Params_List(s *capnp.Segment, sz int32) (RegistrationServer_updateLocalConfiguration_Params_List, error) { l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}, sz) return RegistrationServer_updateLocalConfiguration_Params_List{l}, err } func (s RegistrationServer_updateLocalConfiguration_Params_List) At(i int) RegistrationServer_updateLocalConfiguration_Params { return RegistrationServer_updateLocalConfiguration_Params{s.List.Struct(i)} } func (s RegistrationServer_updateLocalConfiguration_Params_List) Set(i int, v RegistrationServer_updateLocalConfiguration_Params) error { return s.List.SetStruct(i, v.Struct) } func (s RegistrationServer_updateLocalConfiguration_Params_List) String() string { str, _ := text.MarshalList(0xc5d6e311876a3604, s.List) return str } // RegistrationServer_updateLocalConfiguration_Params_Promise is a wrapper for a RegistrationServer_updateLocalConfiguration_Params promised by a client call. type RegistrationServer_updateLocalConfiguration_Params_Promise struct{ *capnp.Pipeline } func (p RegistrationServer_updateLocalConfiguration_Params_Promise) Struct() (RegistrationServer_updateLocalConfiguration_Params, error) { s, err := p.Pipeline.Struct() return RegistrationServer_updateLocalConfiguration_Params{s}, err } type RegistrationServer_updateLocalConfiguration_Results struct{ capnp.Struct } // RegistrationServer_updateLocalConfiguration_Results_TypeID is the unique identifier for the type RegistrationServer_updateLocalConfiguration_Results. const RegistrationServer_updateLocalConfiguration_Results_TypeID = 0xe5ceae5d6897d7be func NewRegistrationServer_updateLocalConfiguration_Results(s *capnp.Segment) (RegistrationServer_updateLocalConfiguration_Results, error) { st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0}) return RegistrationServer_updateLocalConfiguration_Results{st}, err } func NewRootRegistrationServer_updateLocalConfiguration_Results(s *capnp.Segment) (RegistrationServer_updateLocalConfiguration_Results, error) { st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0}) return RegistrationServer_updateLocalConfiguration_Results{st}, err } func ReadRootRegistrationServer_updateLocalConfiguration_Results(msg *capnp.Message) (RegistrationServer_updateLocalConfiguration_Results, error) { root, err := msg.RootPtr() return RegistrationServer_updateLocalConfiguration_Results{root.Struct()}, err } func (s RegistrationServer_updateLocalConfiguration_Results) String() string { str, _ := text.Marshal(0xe5ceae5d6897d7be, s.Struct) return str } // RegistrationServer_updateLocalConfiguration_Results_List is a list of RegistrationServer_updateLocalConfiguration_Results. type RegistrationServer_updateLocalConfiguration_Results_List struct{ capnp.List } // NewRegistrationServer_updateLocalConfiguration_Results creates a new list of RegistrationServer_updateLocalConfiguration_Results. func NewRegistrationServer_updateLocalConfiguration_Results_List(s *capnp.Segment, sz int32) (RegistrationServer_updateLocalConfiguration_Results_List, error) { l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0}, sz) return RegistrationServer_updateLocalConfiguration_Results_List{l}, err } func (s RegistrationServer_updateLocalConfiguration_Results_List) At(i int) RegistrationServer_updateLocalConfiguration_Results { return RegistrationServer_updateLocalConfiguration_Results{s.List.Struct(i)} } func (s RegistrationServer_updateLocalConfiguration_Results_List) Set(i int, v RegistrationServer_updateLocalConfiguration_Results) error { return s.List.SetStruct(i, v.Struct) } func (s RegistrationServer_updateLocalConfiguration_Results_List) String() string { str, _ := text.MarshalList(0xe5ceae5d6897d7be, s.List) return str } // RegistrationServer_updateLocalConfiguration_Results_Promise is a wrapper for a RegistrationServer_updateLocalConfiguration_Results promised by a client call. type RegistrationServer_updateLocalConfiguration_Results_Promise struct{ *capnp.Pipeline } func (p RegistrationServer_updateLocalConfiguration_Results_Promise) Struct() (RegistrationServer_updateLocalConfiguration_Results, error) { s, err := p.Pipeline.Struct() return RegistrationServer_updateLocalConfiguration_Results{s}, err } type RegisterUdpSessionResponse struct{ capnp.Struct } // RegisterUdpSessionResponse_TypeID is the unique identifier for the type RegisterUdpSessionResponse. const RegisterUdpSessionResponse_TypeID = 0xab6d5210c1f26687 func NewRegisterUdpSessionResponse(s *capnp.Segment) (RegisterUdpSessionResponse, error) { st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2}) return RegisterUdpSessionResponse{st}, err } func NewRootRegisterUdpSessionResponse(s *capnp.Segment) (RegisterUdpSessionResponse, error) { st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2}) return RegisterUdpSessionResponse{st}, err } func ReadRootRegisterUdpSessionResponse(msg *capnp.Message) (RegisterUdpSessionResponse, error) { root, err := msg.RootPtr() return RegisterUdpSessionResponse{root.Struct()}, err } func (s RegisterUdpSessionResponse) String() string { str, _ := text.Marshal(0xab6d5210c1f26687, s.Struct) return str } func (s RegisterUdpSessionResponse) Err() (string, error) { p, err := s.Struct.Ptr(0) return p.Text(), err } func (s RegisterUdpSessionResponse) HasErr() bool { p, err := s.Struct.Ptr(0) return p.IsValid() || err != nil } func (s RegisterUdpSessionResponse) ErrBytes() ([]byte, error) { p, err := s.Struct.Ptr(0) return p.TextBytes(), err } func (s RegisterUdpSessionResponse) SetErr(v string) error { return s.Struct.SetText(0, v) } func (s RegisterUdpSessionResponse) Spans() ([]byte, error) { p, err := s.Struct.Ptr(1) return []byte(p.Data()), err } func (s RegisterUdpSessionResponse) HasSpans() bool { p, err := s.Struct.Ptr(1) return p.IsValid() || err != nil } func (s RegisterUdpSessionResponse) SetSpans(v []byte) error { return s.Struct.SetData(1, v) } // RegisterUdpSessionResponse_List is a list of RegisterUdpSessionResponse. type RegisterUdpSessionResponse_List struct{ capnp.List } // NewRegisterUdpSessionResponse creates a new list of RegisterUdpSessionResponse. func NewRegisterUdpSessionResponse_List(s *capnp.Segment, sz int32) (RegisterUdpSessionResponse_List, error) { l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2}, sz) return RegisterUdpSessionResponse_List{l}, err } func (s RegisterUdpSessionResponse_List) At(i int) RegisterUdpSessionResponse { return RegisterUdpSessionResponse{s.List.Struct(i)} } func (s RegisterUdpSessionResponse_List) Set(i int, v RegisterUdpSessionResponse) error { return s.List.SetStruct(i, v.Struct) } func (s RegisterUdpSessionResponse_List) String() string { str, _ := text.MarshalList(0xab6d5210c1f26687, s.List) return str } // RegisterUdpSessionResponse_Promise is a wrapper for a RegisterUdpSessionResponse promised by a client call. type RegisterUdpSessionResponse_Promise struct{ *capnp.Pipeline } func (p RegisterUdpSessionResponse_Promise) Struct() (RegisterUdpSessionResponse, error) { s, err := p.Pipeline.Struct() return RegisterUdpSessionResponse{s}, err } type SessionManager struct{ Client capnp.Client } // SessionManager_TypeID is the unique identifier for the type SessionManager. const SessionManager_TypeID = 0x839445a59fb01686 func (c SessionManager) RegisterUdpSession(ctx context.Context, params func(SessionManager_registerUdpSession_Params) error, opts ...capnp.CallOption) SessionManager_registerUdpSession_Results_Promise { if c.Client == nil { return SessionManager_registerUdpSession_Results_Promise{Pipeline: capnp.NewPipeline(capnp.ErrorAnswer(capnp.ErrNullClient))} } call := &capnp.Call{ Ctx: ctx, Method: capnp.Method{ InterfaceID: 0x839445a59fb01686, MethodID: 0, InterfaceName: "tunnelrpc/proto/tunnelrpc.capnp:SessionManager", MethodName: "registerUdpSession", }, Options: capnp.NewCallOptions(opts), } if params != nil { call.ParamsSize = capnp.ObjectSize{DataSize: 16, PointerCount: 3} call.ParamsFunc = func(s capnp.Struct) error { return params(SessionManager_registerUdpSession_Params{Struct: s}) } } return SessionManager_registerUdpSession_Results_Promise{Pipeline: capnp.NewPipeline(c.Client.Call(call))} } func (c SessionManager) UnregisterUdpSession(ctx context.Context, params func(SessionManager_unregisterUdpSession_Params) error, opts ...capnp.CallOption) SessionManager_unregisterUdpSession_Results_Promise { if c.Client == nil { return SessionManager_unregisterUdpSession_Results_Promise{Pipeline: capnp.NewPipeline(capnp.ErrorAnswer(capnp.ErrNullClient))} } call := &capnp.Call{ Ctx: ctx, Method: capnp.Method{ InterfaceID: 0x839445a59fb01686, MethodID: 1, InterfaceName: "tunnelrpc/proto/tunnelrpc.capnp:SessionManager", MethodName: "unregisterUdpSession", }, Options: capnp.NewCallOptions(opts), } if params != nil { call.ParamsSize = capnp.ObjectSize{DataSize: 0, PointerCount: 2} call.ParamsFunc = func(s capnp.Struct) error { return params(SessionManager_unregisterUdpSession_Params{Struct: s}) } } return SessionManager_unregisterUdpSession_Results_Promise{Pipeline: capnp.NewPipeline(c.Client.Call(call))} } type SessionManager_Server interface { RegisterUdpSession(SessionManager_registerUdpSession) error UnregisterUdpSession(SessionManager_unregisterUdpSession) error } func SessionManager_ServerToClient(s SessionManager_Server) SessionManager { c, _ := s.(server.Closer) return SessionManager{Client: server.New(SessionManager_Methods(nil, s), c)} } func SessionManager_Methods(methods []server.Method, s SessionManager_Server) []server.Method { if cap(methods) == 0 { methods = make([]server.Method, 0, 2) } methods = append(methods, server.Method{ Method: capnp.Method{ InterfaceID: 0x839445a59fb01686, MethodID: 0, InterfaceName: "tunnelrpc/proto/tunnelrpc.capnp:SessionManager", MethodName: "registerUdpSession", }, Impl: func(c context.Context, opts capnp.CallOptions, p, r capnp.Struct) error { call := SessionManager_registerUdpSession{c, opts, SessionManager_registerUdpSession_Params{Struct: p}, SessionManager_registerUdpSession_Results{Struct: r}} return s.RegisterUdpSession(call) }, ResultsSize: capnp.ObjectSize{DataSize: 0, PointerCount: 1}, }) methods = append(methods, server.Method{ Method: capnp.Method{ InterfaceID: 0x839445a59fb01686, MethodID: 1, InterfaceName: "tunnelrpc/proto/tunnelrpc.capnp:SessionManager", MethodName: "unregisterUdpSession", }, Impl: func(c context.Context, opts capnp.CallOptions, p, r capnp.Struct) error { call := SessionManager_unregisterUdpSession{c, opts, SessionManager_unregisterUdpSession_Params{Struct: p}, SessionManager_unregisterUdpSession_Results{Struct: r}} return s.UnregisterUdpSession(call) }, ResultsSize: capnp.ObjectSize{DataSize: 0, PointerCount: 0}, }) return methods } // SessionManager_registerUdpSession holds the arguments for a server call to SessionManager.registerUdpSession. type SessionManager_registerUdpSession struct { Ctx context.Context Options capnp.CallOptions Params SessionManager_registerUdpSession_Params Results SessionManager_registerUdpSession_Results } // SessionManager_unregisterUdpSession holds the arguments for a server call to SessionManager.unregisterUdpSession. type SessionManager_unregisterUdpSession struct { Ctx context.Context Options capnp.CallOptions Params SessionManager_unregisterUdpSession_Params Results SessionManager_unregisterUdpSession_Results } type SessionManager_registerUdpSession_Params struct{ capnp.Struct } // SessionManager_registerUdpSession_Params_TypeID is the unique identifier for the type SessionManager_registerUdpSession_Params. const SessionManager_registerUdpSession_Params_TypeID = 0x904e297b87fbecea func NewSessionManager_registerUdpSession_Params(s *capnp.Segment) (SessionManager_registerUdpSession_Params, error) { st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 16, PointerCount: 3}) return SessionManager_registerUdpSession_Params{st}, err } func NewRootSessionManager_registerUdpSession_Params(s *capnp.Segment) (SessionManager_registerUdpSession_Params, error) { st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 16, PointerCount: 3}) return SessionManager_registerUdpSession_Params{st}, err } func ReadRootSessionManager_registerUdpSession_Params(msg *capnp.Message) (SessionManager_registerUdpSession_Params, error) { root, err := msg.RootPtr() return SessionManager_registerUdpSession_Params{root.Struct()}, err } func (s SessionManager_registerUdpSession_Params) String() string { str, _ := text.Marshal(0x904e297b87fbecea, s.Struct) return str } func (s SessionManager_registerUdpSession_Params) SessionId() ([]byte, error) { p, err := s.Struct.Ptr(0) return []byte(p.Data()), err } func (s SessionManager_registerUdpSession_Params) HasSessionId() bool { p, err := s.Struct.Ptr(0) return p.IsValid() || err != nil } func (s SessionManager_registerUdpSession_Params) SetSessionId(v []byte) error { return s.Struct.SetData(0, v) } func (s SessionManager_registerUdpSession_Params) DstIp() ([]byte, error) { p, err := s.Struct.Ptr(1) return []byte(p.Data()), err } func (s SessionManager_registerUdpSession_Params) HasDstIp() bool { p, err := s.Struct.Ptr(1) return p.IsValid() || err != nil } func (s SessionManager_registerUdpSession_Params) SetDstIp(v []byte) error { return s.Struct.SetData(1, v) } func (s SessionManager_registerUdpSession_Params) DstPort() uint16 { return s.Struct.Uint16(0) } func (s SessionManager_registerUdpSession_Params) SetDstPort(v uint16) { s.Struct.SetUint16(0, v) } func (s SessionManager_registerUdpSession_Params) CloseAfterIdleHint() int64 { return int64(s.Struct.Uint64(8)) } func (s SessionManager_registerUdpSession_Params) SetCloseAfterIdleHint(v int64) { s.Struct.SetUint64(8, uint64(v)) } func (s SessionManager_registerUdpSession_Params) TraceContext() (string, error) { p, err := s.Struct.Ptr(2) return p.Text(), err } func (s SessionManager_registerUdpSession_Params) HasTraceContext() bool { p, err := s.Struct.Ptr(2) return p.IsValid() || err != nil } func (s SessionManager_registerUdpSession_Params) TraceContextBytes() ([]byte, error) { p, err := s.Struct.Ptr(2) return p.TextBytes(), err } func (s SessionManager_registerUdpSession_Params) SetTraceContext(v string) error { return s.Struct.SetText(2, v) } // SessionManager_registerUdpSession_Params_List is a list of SessionManager_registerUdpSession_Params. type SessionManager_registerUdpSession_Params_List struct{ capnp.List } // NewSessionManager_registerUdpSession_Params creates a new list of SessionManager_registerUdpSession_Params. func NewSessionManager_registerUdpSession_Params_List(s *capnp.Segment, sz int32) (SessionManager_registerUdpSession_Params_List, error) { l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 16, PointerCount: 3}, sz) return SessionManager_registerUdpSession_Params_List{l}, err } func (s SessionManager_registerUdpSession_Params_List) At(i int) SessionManager_registerUdpSession_Params { return SessionManager_registerUdpSession_Params{s.List.Struct(i)} } func (s SessionManager_registerUdpSession_Params_List) Set(i int, v SessionManager_registerUdpSession_Params) error { return s.List.SetStruct(i, v.Struct) } func (s SessionManager_registerUdpSession_Params_List) String() string { str, _ := text.MarshalList(0x904e297b87fbecea, s.List) return str } // SessionManager_registerUdpSession_Params_Promise is a wrapper for a SessionManager_registerUdpSession_Params promised by a client call. type SessionManager_registerUdpSession_Params_Promise struct{ *capnp.Pipeline } func (p SessionManager_registerUdpSession_Params_Promise) Struct() (SessionManager_registerUdpSession_Params, error) { s, err := p.Pipeline.Struct() return SessionManager_registerUdpSession_Params{s}, err } type SessionManager_registerUdpSession_Results struct{ capnp.Struct } // SessionManager_registerUdpSession_Results_TypeID is the unique identifier for the type SessionManager_registerUdpSession_Results. const SessionManager_registerUdpSession_Results_TypeID = 0x8635c6b4f45bf5cd func NewSessionManager_registerUdpSession_Results(s *capnp.Segment) (SessionManager_registerUdpSession_Results, error) { st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) return SessionManager_registerUdpSession_Results{st}, err } func NewRootSessionManager_registerUdpSession_Results(s *capnp.Segment) (SessionManager_registerUdpSession_Results, error) { st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) return SessionManager_registerUdpSession_Results{st}, err } func ReadRootSessionManager_registerUdpSession_Results(msg *capnp.Message) (SessionManager_registerUdpSession_Results, error) { root, err := msg.RootPtr() return SessionManager_registerUdpSession_Results{root.Struct()}, err } func (s SessionManager_registerUdpSession_Results) String() string { str, _ := text.Marshal(0x8635c6b4f45bf5cd, s.Struct) return str } func (s SessionManager_registerUdpSession_Results) Result() (RegisterUdpSessionResponse, error) { p, err := s.Struct.Ptr(0) return RegisterUdpSessionResponse{Struct: p.Struct()}, err } func (s SessionManager_registerUdpSession_Results) HasResult() bool { p, err := s.Struct.Ptr(0) return p.IsValid() || err != nil } func (s SessionManager_registerUdpSession_Results) SetResult(v RegisterUdpSessionResponse) error { return s.Struct.SetPtr(0, v.Struct.ToPtr()) } // NewResult sets the result field to a newly // allocated RegisterUdpSessionResponse struct, preferring placement in s's segment. func (s SessionManager_registerUdpSession_Results) NewResult() (RegisterUdpSessionResponse, error) { ss, err := NewRegisterUdpSessionResponse(s.Struct.Segment()) if err != nil { return RegisterUdpSessionResponse{}, err } err = s.Struct.SetPtr(0, ss.Struct.ToPtr()) return ss, err } // SessionManager_registerUdpSession_Results_List is a list of SessionManager_registerUdpSession_Results. type SessionManager_registerUdpSession_Results_List struct{ capnp.List } // NewSessionManager_registerUdpSession_Results creates a new list of SessionManager_registerUdpSession_Results. func NewSessionManager_registerUdpSession_Results_List(s *capnp.Segment, sz int32) (SessionManager_registerUdpSession_Results_List, error) { l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}, sz) return SessionManager_registerUdpSession_Results_List{l}, err } func (s SessionManager_registerUdpSession_Results_List) At(i int) SessionManager_registerUdpSession_Results { return SessionManager_registerUdpSession_Results{s.List.Struct(i)} } func (s SessionManager_registerUdpSession_Results_List) Set(i int, v SessionManager_registerUdpSession_Results) error { return s.List.SetStruct(i, v.Struct) } func (s SessionManager_registerUdpSession_Results_List) String() string { str, _ := text.MarshalList(0x8635c6b4f45bf5cd, s.List) return str } // SessionManager_registerUdpSession_Results_Promise is a wrapper for a SessionManager_registerUdpSession_Results promised by a client call. type SessionManager_registerUdpSession_Results_Promise struct{ *capnp.Pipeline } func (p SessionManager_registerUdpSession_Results_Promise) Struct() (SessionManager_registerUdpSession_Results, error) { s, err := p.Pipeline.Struct() return SessionManager_registerUdpSession_Results{s}, err } func (p SessionManager_registerUdpSession_Results_Promise) Result() RegisterUdpSessionResponse_Promise { return RegisterUdpSessionResponse_Promise{Pipeline: p.Pipeline.GetPipeline(0)} } type SessionManager_unregisterUdpSession_Params struct{ capnp.Struct } // SessionManager_unregisterUdpSession_Params_TypeID is the unique identifier for the type SessionManager_unregisterUdpSession_Params. const SessionManager_unregisterUdpSession_Params_TypeID = 0x96b74375ce9b0ef6 func NewSessionManager_unregisterUdpSession_Params(s *capnp.Segment) (SessionManager_unregisterUdpSession_Params, error) { st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2}) return SessionManager_unregisterUdpSession_Params{st}, err } func NewRootSessionManager_unregisterUdpSession_Params(s *capnp.Segment) (SessionManager_unregisterUdpSession_Params, error) { st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2}) return SessionManager_unregisterUdpSession_Params{st}, err } func ReadRootSessionManager_unregisterUdpSession_Params(msg *capnp.Message) (SessionManager_unregisterUdpSession_Params, error) { root, err := msg.RootPtr() return SessionManager_unregisterUdpSession_Params{root.Struct()}, err } func (s SessionManager_unregisterUdpSession_Params) String() string { str, _ := text.Marshal(0x96b74375ce9b0ef6, s.Struct) return str } func (s SessionManager_unregisterUdpSession_Params) SessionId() ([]byte, error) { p, err := s.Struct.Ptr(0) return []byte(p.Data()), err } func (s SessionManager_unregisterUdpSession_Params) HasSessionId() bool { p, err := s.Struct.Ptr(0) return p.IsValid() || err != nil } func (s SessionManager_unregisterUdpSession_Params) SetSessionId(v []byte) error { return s.Struct.SetData(0, v) } func (s SessionManager_unregisterUdpSession_Params) Message() (string, error) { p, err := s.Struct.Ptr(1) return p.Text(), err } func (s SessionManager_unregisterUdpSession_Params) HasMessage() bool { p, err := s.Struct.Ptr(1) return p.IsValid() || err != nil } func (s SessionManager_unregisterUdpSession_Params) MessageBytes() ([]byte, error) { p, err := s.Struct.Ptr(1) return p.TextBytes(), err } func (s SessionManager_unregisterUdpSession_Params) SetMessage(v string) error { return s.Struct.SetText(1, v) } // SessionManager_unregisterUdpSession_Params_List is a list of SessionManager_unregisterUdpSession_Params. type SessionManager_unregisterUdpSession_Params_List struct{ capnp.List } // NewSessionManager_unregisterUdpSession_Params creates a new list of SessionManager_unregisterUdpSession_Params. func NewSessionManager_unregisterUdpSession_Params_List(s *capnp.Segment, sz int32) (SessionManager_unregisterUdpSession_Params_List, error) { l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2}, sz) return SessionManager_unregisterUdpSession_Params_List{l}, err } func (s SessionManager_unregisterUdpSession_Params_List) At(i int) SessionManager_unregisterUdpSession_Params { return SessionManager_unregisterUdpSession_Params{s.List.Struct(i)} } func (s SessionManager_unregisterUdpSession_Params_List) Set(i int, v SessionManager_unregisterUdpSession_Params) error { return s.List.SetStruct(i, v.Struct) } func (s SessionManager_unregisterUdpSession_Params_List) String() string { str, _ := text.MarshalList(0x96b74375ce9b0ef6, s.List) return str } // SessionManager_unregisterUdpSession_Params_Promise is a wrapper for a SessionManager_unregisterUdpSession_Params promised by a client call. type SessionManager_unregisterUdpSession_Params_Promise struct{ *capnp.Pipeline } func (p SessionManager_unregisterUdpSession_Params_Promise) Struct() (SessionManager_unregisterUdpSession_Params, error) { s, err := p.Pipeline.Struct() return SessionManager_unregisterUdpSession_Params{s}, err } type SessionManager_unregisterUdpSession_Results struct{ capnp.Struct } // SessionManager_unregisterUdpSession_Results_TypeID is the unique identifier for the type SessionManager_unregisterUdpSession_Results. const SessionManager_unregisterUdpSession_Results_TypeID = 0xf24ec4ab5891b676 func NewSessionManager_unregisterUdpSession_Results(s *capnp.Segment) (SessionManager_unregisterUdpSession_Results, error) { st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0}) return SessionManager_unregisterUdpSession_Results{st}, err } func NewRootSessionManager_unregisterUdpSession_Results(s *capnp.Segment) (SessionManager_unregisterUdpSession_Results, error) { st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0}) return SessionManager_unregisterUdpSession_Results{st}, err } func ReadRootSessionManager_unregisterUdpSession_Results(msg *capnp.Message) (SessionManager_unregisterUdpSession_Results, error) { root, err := msg.RootPtr() return SessionManager_unregisterUdpSession_Results{root.Struct()}, err } func (s SessionManager_unregisterUdpSession_Results) String() string { str, _ := text.Marshal(0xf24ec4ab5891b676, s.Struct) return str } // SessionManager_unregisterUdpSession_Results_List is a list of SessionManager_unregisterUdpSession_Results. type SessionManager_unregisterUdpSession_Results_List struct{ capnp.List } // NewSessionManager_unregisterUdpSession_Results creates a new list of SessionManager_unregisterUdpSession_Results. func NewSessionManager_unregisterUdpSession_Results_List(s *capnp.Segment, sz int32) (SessionManager_unregisterUdpSession_Results_List, error) { l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0}, sz) return SessionManager_unregisterUdpSession_Results_List{l}, err } func (s SessionManager_unregisterUdpSession_Results_List) At(i int) SessionManager_unregisterUdpSession_Results { return SessionManager_unregisterUdpSession_Results{s.List.Struct(i)} } func (s SessionManager_unregisterUdpSession_Results_List) Set(i int, v SessionManager_unregisterUdpSession_Results) error { return s.List.SetStruct(i, v.Struct) } func (s SessionManager_unregisterUdpSession_Results_List) String() string { str, _ := text.MarshalList(0xf24ec4ab5891b676, s.List) return str } // SessionManager_unregisterUdpSession_Results_Promise is a wrapper for a SessionManager_unregisterUdpSession_Results promised by a client call. type SessionManager_unregisterUdpSession_Results_Promise struct{ *capnp.Pipeline } func (p SessionManager_unregisterUdpSession_Results_Promise) Struct() (SessionManager_unregisterUdpSession_Results, error) { s, err := p.Pipeline.Struct() return SessionManager_unregisterUdpSession_Results{s}, err } type UpdateConfigurationResponse struct{ capnp.Struct } // UpdateConfigurationResponse_TypeID is the unique identifier for the type UpdateConfigurationResponse. const UpdateConfigurationResponse_TypeID = 0xdb58ff694ba05cf9 func NewUpdateConfigurationResponse(s *capnp.Segment) (UpdateConfigurationResponse, error) { st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 1}) return UpdateConfigurationResponse{st}, err } func NewRootUpdateConfigurationResponse(s *capnp.Segment) (UpdateConfigurationResponse, error) { st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 1}) return UpdateConfigurationResponse{st}, err } func ReadRootUpdateConfigurationResponse(msg *capnp.Message) (UpdateConfigurationResponse, error) { root, err := msg.RootPtr() return UpdateConfigurationResponse{root.Struct()}, err } func (s UpdateConfigurationResponse) String() string { str, _ := text.Marshal(0xdb58ff694ba05cf9, s.Struct) return str } func (s UpdateConfigurationResponse) LatestAppliedVersion() int32 { return int32(s.Struct.Uint32(0)) } func (s UpdateConfigurationResponse) SetLatestAppliedVersion(v int32) { s.Struct.SetUint32(0, uint32(v)) } func (s UpdateConfigurationResponse) Err() (string, error) { p, err := s.Struct.Ptr(0) return p.Text(), err } func (s UpdateConfigurationResponse) HasErr() bool { p, err := s.Struct.Ptr(0) return p.IsValid() || err != nil } func (s UpdateConfigurationResponse) ErrBytes() ([]byte, error) { p, err := s.Struct.Ptr(0) return p.TextBytes(), err } func (s UpdateConfigurationResponse) SetErr(v string) error { return s.Struct.SetText(0, v) } // UpdateConfigurationResponse_List is a list of UpdateConfigurationResponse. type UpdateConfigurationResponse_List struct{ capnp.List } // NewUpdateConfigurationResponse creates a new list of UpdateConfigurationResponse. func NewUpdateConfigurationResponse_List(s *capnp.Segment, sz int32) (UpdateConfigurationResponse_List, error) { l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 8, PointerCount: 1}, sz) return UpdateConfigurationResponse_List{l}, err } func (s UpdateConfigurationResponse_List) At(i int) UpdateConfigurationResponse { return UpdateConfigurationResponse{s.List.Struct(i)} } func (s UpdateConfigurationResponse_List) Set(i int, v UpdateConfigurationResponse) error { return s.List.SetStruct(i, v.Struct) } func (s UpdateConfigurationResponse_List) String() string { str, _ := text.MarshalList(0xdb58ff694ba05cf9, s.List) return str } // UpdateConfigurationResponse_Promise is a wrapper for a UpdateConfigurationResponse promised by a client call. type UpdateConfigurationResponse_Promise struct{ *capnp.Pipeline } func (p UpdateConfigurationResponse_Promise) Struct() (UpdateConfigurationResponse, error) { s, err := p.Pipeline.Struct() return UpdateConfigurationResponse{s}, err } type ConfigurationManager struct{ Client capnp.Client } // ConfigurationManager_TypeID is the unique identifier for the type ConfigurationManager. const ConfigurationManager_TypeID = 0xb48edfbdaa25db04 func (c ConfigurationManager) UpdateConfiguration(ctx context.Context, params func(ConfigurationManager_updateConfiguration_Params) error, opts ...capnp.CallOption) ConfigurationManager_updateConfiguration_Results_Promise { if c.Client == nil { return ConfigurationManager_updateConfiguration_Results_Promise{Pipeline: capnp.NewPipeline(capnp.ErrorAnswer(capnp.ErrNullClient))} } call := &capnp.Call{ Ctx: ctx, Method: capnp.Method{ InterfaceID: 0xb48edfbdaa25db04, MethodID: 0, InterfaceName: "tunnelrpc/proto/tunnelrpc.capnp:ConfigurationManager", MethodName: "updateConfiguration", }, Options: capnp.NewCallOptions(opts), } if params != nil { call.ParamsSize = capnp.ObjectSize{DataSize: 8, PointerCount: 1} call.ParamsFunc = func(s capnp.Struct) error { return params(ConfigurationManager_updateConfiguration_Params{Struct: s}) } } return ConfigurationManager_updateConfiguration_Results_Promise{Pipeline: capnp.NewPipeline(c.Client.Call(call))} } type ConfigurationManager_Server interface { UpdateConfiguration(ConfigurationManager_updateConfiguration) error } func ConfigurationManager_ServerToClient(s ConfigurationManager_Server) ConfigurationManager { c, _ := s.(server.Closer) return ConfigurationManager{Client: server.New(ConfigurationManager_Methods(nil, s), c)} } func ConfigurationManager_Methods(methods []server.Method, s ConfigurationManager_Server) []server.Method { if cap(methods) == 0 { methods = make([]server.Method, 0, 1) } methods = append(methods, server.Method{ Method: capnp.Method{ InterfaceID: 0xb48edfbdaa25db04, MethodID: 0, InterfaceName: "tunnelrpc/proto/tunnelrpc.capnp:ConfigurationManager", MethodName: "updateConfiguration", }, Impl: func(c context.Context, opts capnp.CallOptions, p, r capnp.Struct) error { call := ConfigurationManager_updateConfiguration{c, opts, ConfigurationManager_updateConfiguration_Params{Struct: p}, ConfigurationManager_updateConfiguration_Results{Struct: r}} return s.UpdateConfiguration(call) }, ResultsSize: capnp.ObjectSize{DataSize: 0, PointerCount: 1}, }) return methods } // ConfigurationManager_updateConfiguration holds the arguments for a server call to ConfigurationManager.updateConfiguration. type ConfigurationManager_updateConfiguration struct { Ctx context.Context Options capnp.CallOptions Params ConfigurationManager_updateConfiguration_Params Results ConfigurationManager_updateConfiguration_Results } type ConfigurationManager_updateConfiguration_Params struct{ capnp.Struct } // ConfigurationManager_updateConfiguration_Params_TypeID is the unique identifier for the type ConfigurationManager_updateConfiguration_Params. const ConfigurationManager_updateConfiguration_Params_TypeID = 0xb177ca2526a3ca76 func NewConfigurationManager_updateConfiguration_Params(s *capnp.Segment) (ConfigurationManager_updateConfiguration_Params, error) { st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 1}) return ConfigurationManager_updateConfiguration_Params{st}, err } func NewRootConfigurationManager_updateConfiguration_Params(s *capnp.Segment) (ConfigurationManager_updateConfiguration_Params, error) { st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 1}) return ConfigurationManager_updateConfiguration_Params{st}, err } func ReadRootConfigurationManager_updateConfiguration_Params(msg *capnp.Message) (ConfigurationManager_updateConfiguration_Params, error) { root, err := msg.RootPtr() return ConfigurationManager_updateConfiguration_Params{root.Struct()}, err } func (s ConfigurationManager_updateConfiguration_Params) String() string { str, _ := text.Marshal(0xb177ca2526a3ca76, s.Struct) return str } func (s ConfigurationManager_updateConfiguration_Params) Version() int32 { return int32(s.Struct.Uint32(0)) } func (s ConfigurationManager_updateConfiguration_Params) SetVersion(v int32) { s.Struct.SetUint32(0, uint32(v)) } func (s ConfigurationManager_updateConfiguration_Params) Config() ([]byte, error) { p, err := s.Struct.Ptr(0) return []byte(p.Data()), err } func (s ConfigurationManager_updateConfiguration_Params) HasConfig() bool { p, err := s.Struct.Ptr(0) return p.IsValid() || err != nil } func (s ConfigurationManager_updateConfiguration_Params) SetConfig(v []byte) error { return s.Struct.SetData(0, v) } // ConfigurationManager_updateConfiguration_Params_List is a list of ConfigurationManager_updateConfiguration_Params. type ConfigurationManager_updateConfiguration_Params_List struct{ capnp.List } // NewConfigurationManager_updateConfiguration_Params creates a new list of ConfigurationManager_updateConfiguration_Params. func NewConfigurationManager_updateConfiguration_Params_List(s *capnp.Segment, sz int32) (ConfigurationManager_updateConfiguration_Params_List, error) { l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 8, PointerCount: 1}, sz) return ConfigurationManager_updateConfiguration_Params_List{l}, err } func (s ConfigurationManager_updateConfiguration_Params_List) At(i int) ConfigurationManager_updateConfiguration_Params { return ConfigurationManager_updateConfiguration_Params{s.List.Struct(i)} } func (s ConfigurationManager_updateConfiguration_Params_List) Set(i int, v ConfigurationManager_updateConfiguration_Params) error { return s.List.SetStruct(i, v.Struct) } func (s ConfigurationManager_updateConfiguration_Params_List) String() string { str, _ := text.MarshalList(0xb177ca2526a3ca76, s.List) return str } // ConfigurationManager_updateConfiguration_Params_Promise is a wrapper for a ConfigurationManager_updateConfiguration_Params promised by a client call. type ConfigurationManager_updateConfiguration_Params_Promise struct{ *capnp.Pipeline } func (p ConfigurationManager_updateConfiguration_Params_Promise) Struct() (ConfigurationManager_updateConfiguration_Params, error) { s, err := p.Pipeline.Struct() return ConfigurationManager_updateConfiguration_Params{s}, err } type ConfigurationManager_updateConfiguration_Results struct{ capnp.Struct } // ConfigurationManager_updateConfiguration_Results_TypeID is the unique identifier for the type ConfigurationManager_updateConfiguration_Results. const ConfigurationManager_updateConfiguration_Results_TypeID = 0x958096448eb3373e func NewConfigurationManager_updateConfiguration_Results(s *capnp.Segment) (ConfigurationManager_updateConfiguration_Results, error) { st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) return ConfigurationManager_updateConfiguration_Results{st}, err } func NewRootConfigurationManager_updateConfiguration_Results(s *capnp.Segment) (ConfigurationManager_updateConfiguration_Results, error) { st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) return ConfigurationManager_updateConfiguration_Results{st}, err } func ReadRootConfigurationManager_updateConfiguration_Results(msg *capnp.Message) (ConfigurationManager_updateConfiguration_Results, error) { root, err := msg.RootPtr() return ConfigurationManager_updateConfiguration_Results{root.Struct()}, err } func (s ConfigurationManager_updateConfiguration_Results) String() string { str, _ := text.Marshal(0x958096448eb3373e, s.Struct) return str } func (s ConfigurationManager_updateConfiguration_Results) Result() (UpdateConfigurationResponse, error) { p, err := s.Struct.Ptr(0) return UpdateConfigurationResponse{Struct: p.Struct()}, err } func (s ConfigurationManager_updateConfiguration_Results) HasResult() bool { p, err := s.Struct.Ptr(0) return p.IsValid() || err != nil } func (s ConfigurationManager_updateConfiguration_Results) SetResult(v UpdateConfigurationResponse) error { return s.Struct.SetPtr(0, v.Struct.ToPtr()) } // NewResult sets the result field to a newly // allocated UpdateConfigurationResponse struct, preferring placement in s's segment. func (s ConfigurationManager_updateConfiguration_Results) NewResult() (UpdateConfigurationResponse, error) { ss, err := NewUpdateConfigurationResponse(s.Struct.Segment()) if err != nil { return UpdateConfigurationResponse{}, err } err = s.Struct.SetPtr(0, ss.Struct.ToPtr()) return ss, err } // ConfigurationManager_updateConfiguration_Results_List is a list of ConfigurationManager_updateConfiguration_Results. type ConfigurationManager_updateConfiguration_Results_List struct{ capnp.List } // NewConfigurationManager_updateConfiguration_Results creates a new list of ConfigurationManager_updateConfiguration_Results. func NewConfigurationManager_updateConfiguration_Results_List(s *capnp.Segment, sz int32) (ConfigurationManager_updateConfiguration_Results_List, error) { l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}, sz) return ConfigurationManager_updateConfiguration_Results_List{l}, err } func (s ConfigurationManager_updateConfiguration_Results_List) At(i int) ConfigurationManager_updateConfiguration_Results { return ConfigurationManager_updateConfiguration_Results{s.List.Struct(i)} } func (s ConfigurationManager_updateConfiguration_Results_List) Set(i int, v ConfigurationManager_updateConfiguration_Results) error { return s.List.SetStruct(i, v.Struct) } func (s ConfigurationManager_updateConfiguration_Results_List) String() string { str, _ := text.MarshalList(0x958096448eb3373e, s.List) return str } // ConfigurationManager_updateConfiguration_Results_Promise is a wrapper for a ConfigurationManager_updateConfiguration_Results promised by a client call. type ConfigurationManager_updateConfiguration_Results_Promise struct{ *capnp.Pipeline } func (p ConfigurationManager_updateConfiguration_Results_Promise) Struct() (ConfigurationManager_updateConfiguration_Results, error) { s, err := p.Pipeline.Struct() return ConfigurationManager_updateConfiguration_Results{s}, err } func (p ConfigurationManager_updateConfiguration_Results_Promise) Result() UpdateConfigurationResponse_Promise { return UpdateConfigurationResponse_Promise{Pipeline: p.Pipeline.GetPipeline(0)} } type CloudflaredServer struct{ Client capnp.Client } // CloudflaredServer_TypeID is the unique identifier for the type CloudflaredServer. const CloudflaredServer_TypeID = 0xf548cef9dea2a4a1 func (c CloudflaredServer) RegisterUdpSession(ctx context.Context, params func(SessionManager_registerUdpSession_Params) error, opts ...capnp.CallOption) SessionManager_registerUdpSession_Results_Promise { if c.Client == nil { return SessionManager_registerUdpSession_Results_Promise{Pipeline: capnp.NewPipeline(capnp.ErrorAnswer(capnp.ErrNullClient))} } call := &capnp.Call{ Ctx: ctx, Method: capnp.Method{ InterfaceID: 0x839445a59fb01686, MethodID: 0, InterfaceName: "tunnelrpc/proto/tunnelrpc.capnp:SessionManager", MethodName: "registerUdpSession", }, Options: capnp.NewCallOptions(opts), } if params != nil { call.ParamsSize = capnp.ObjectSize{DataSize: 16, PointerCount: 3} call.ParamsFunc = func(s capnp.Struct) error { return params(SessionManager_registerUdpSession_Params{Struct: s}) } } return SessionManager_registerUdpSession_Results_Promise{Pipeline: capnp.NewPipeline(c.Client.Call(call))} } func (c CloudflaredServer) UnregisterUdpSession(ctx context.Context, params func(SessionManager_unregisterUdpSession_Params) error, opts ...capnp.CallOption) SessionManager_unregisterUdpSession_Results_Promise { if c.Client == nil { return SessionManager_unregisterUdpSession_Results_Promise{Pipeline: capnp.NewPipeline(capnp.ErrorAnswer(capnp.ErrNullClient))} } call := &capnp.Call{ Ctx: ctx, Method: capnp.Method{ InterfaceID: 0x839445a59fb01686, MethodID: 1, InterfaceName: "tunnelrpc/proto/tunnelrpc.capnp:SessionManager", MethodName: "unregisterUdpSession", }, Options: capnp.NewCallOptions(opts), } if params != nil { call.ParamsSize = capnp.ObjectSize{DataSize: 0, PointerCount: 2} call.ParamsFunc = func(s capnp.Struct) error { return params(SessionManager_unregisterUdpSession_Params{Struct: s}) } } return SessionManager_unregisterUdpSession_Results_Promise{Pipeline: capnp.NewPipeline(c.Client.Call(call))} } func (c CloudflaredServer) UpdateConfiguration(ctx context.Context, params func(ConfigurationManager_updateConfiguration_Params) error, opts ...capnp.CallOption) ConfigurationManager_updateConfiguration_Results_Promise { if c.Client == nil { return ConfigurationManager_updateConfiguration_Results_Promise{Pipeline: capnp.NewPipeline(capnp.ErrorAnswer(capnp.ErrNullClient))} } call := &capnp.Call{ Ctx: ctx, Method: capnp.Method{ InterfaceID: 0xb48edfbdaa25db04, MethodID: 0, InterfaceName: "tunnelrpc/proto/tunnelrpc.capnp:ConfigurationManager", MethodName: "updateConfiguration", }, Options: capnp.NewCallOptions(opts), } if params != nil { call.ParamsSize = capnp.ObjectSize{DataSize: 8, PointerCount: 1} call.ParamsFunc = func(s capnp.Struct) error { return params(ConfigurationManager_updateConfiguration_Params{Struct: s}) } } return ConfigurationManager_updateConfiguration_Results_Promise{Pipeline: capnp.NewPipeline(c.Client.Call(call))} } type CloudflaredServer_Server interface { RegisterUdpSession(SessionManager_registerUdpSession) error UnregisterUdpSession(SessionManager_unregisterUdpSession) error UpdateConfiguration(ConfigurationManager_updateConfiguration) error } func CloudflaredServer_ServerToClient(s CloudflaredServer_Server) CloudflaredServer { c, _ := s.(server.Closer) return CloudflaredServer{Client: server.New(CloudflaredServer_Methods(nil, s), c)} } func CloudflaredServer_Methods(methods []server.Method, s CloudflaredServer_Server) []server.Method { if cap(methods) == 0 { methods = make([]server.Method, 0, 3) } methods = append(methods, server.Method{ Method: capnp.Method{ InterfaceID: 0x839445a59fb01686, MethodID: 0, InterfaceName: "tunnelrpc/proto/tunnelrpc.capnp:SessionManager", MethodName: "registerUdpSession", }, Impl: func(c context.Context, opts capnp.CallOptions, p, r capnp.Struct) error { call := SessionManager_registerUdpSession{c, opts, SessionManager_registerUdpSession_Params{Struct: p}, SessionManager_registerUdpSession_Results{Struct: r}} return s.RegisterUdpSession(call) }, ResultsSize: capnp.ObjectSize{DataSize: 0, PointerCount: 1}, }) methods = append(methods, server.Method{ Method: capnp.Method{ InterfaceID: 0x839445a59fb01686, MethodID: 1, InterfaceName: "tunnelrpc/proto/tunnelrpc.capnp:SessionManager", MethodName: "unregisterUdpSession", }, Impl: func(c context.Context, opts capnp.CallOptions, p, r capnp.Struct) error { call := SessionManager_unregisterUdpSession{c, opts, SessionManager_unregisterUdpSession_Params{Struct: p}, SessionManager_unregisterUdpSession_Results{Struct: r}} return s.UnregisterUdpSession(call) }, ResultsSize: capnp.ObjectSize{DataSize: 0, PointerCount: 0}, }) methods = append(methods, server.Method{ Method: capnp.Method{ InterfaceID: 0xb48edfbdaa25db04, MethodID: 0, InterfaceName: "tunnelrpc/proto/tunnelrpc.capnp:ConfigurationManager", MethodName: "updateConfiguration", }, Impl: func(c context.Context, opts capnp.CallOptions, p, r capnp.Struct) error { call := ConfigurationManager_updateConfiguration{c, opts, ConfigurationManager_updateConfiguration_Params{Struct: p}, ConfigurationManager_updateConfiguration_Results{Struct: r}} return s.UpdateConfiguration(call) }, ResultsSize: capnp.ObjectSize{DataSize: 0, PointerCount: 1}, }) return methods } const schema_db8274f9144abc7e = "x\xda\xccZ}t\x14\xd7u\xbfw\xde.#\xc9\x12" + "\xbb\xe3\xd9\x04!E^G\x07\xda\x9a\x04cAIm" + "\x9aD\x12\x96\x88\x85\x01k\xb4\x90\xe3\x83q\x8eG\xbb" + "O\xd2\xa8\xbb3\x9b\x99Y\x19\x11;|\x04\x8c\xf1\xb1" + "\x1dC\xc06Jh\x08\x8e\xdbS9IM\x8c\x9b\xa6" + "\xc7nM\x1a\xc7\x89\x1d\x13\xe3cR\x08N\xd3\x94\xd0" + "6>\xa4\xae\xbf\x9a\xc3i\xea\xe9\xb93;\x1f\xda\x15" + "H\x82\xf6\x9c\xfe\x07w\xef\xbc\xf7\xee\xef\xfd\xee}\xbf" + "w\x9f\xae{\xb1\xb6Ch\x8boN\x00(G\xe2\xb3" + "\x1c\xbe\xe0\xd5M\x07\xe7\xff\xfd6P\xaeFt>\xff" + "\xcc\xca\xd4y{\xdbi\x883\x11`\xc9\xe3\xe28\xca" + "\xcf\x8a\"\x80\xfc]\xf1_\x01\x9d{>\xf8\xe4W\x1f" + "\xef\xde\xfb\x05\x90\xaef\xa13\xe0\x92\x035\x9bP>" + "\\C\x9e\xdf\xac\xd9)7\xd4\x8a\x00\xce\xcd\xd2\xa2\xdb" + "R\xaf\x1c#\xef\xe8\xd01\x1a\xfa\xbd\x9a\xf5(\xd7\x92" + "\x9b\x1c\xaf\xa5\xa1?^\xf8\xc9\xa1\x8f\xed{i;H" + "W\x0b\x13\x86~\xabv\x1c\xe5\xda:\xd7\xb3\xee\x16@" + "\xe7\x9d\xbd\x8dO|\xed\xd8\x0fw\x80\xb4\x10\xa1\xbc\xd2" + "\x96\xba:\x01P^Z\xf7\x97\x80\xce\xcb\xef\xdd\xf6\xee" + "\x91\x1f,\xbd\x07\xa4E\xe4\x80\xe4p\xa2\xae\x8f\x1c\xde" + "\xaak\x07t\xde8\xf7_;?w\xcd\x9a\x87@Y" + "\x84\x82?\x84t\xc5J\x01p\xc9\xc2+\xd2\x08\xe8\xb4" + "\xafx\xf9\xbbMK\x1e\xde[\xb1v\x81<\x95\xfa\xf5" + "(\xf3zZ\x91Z\x7f'\xa0\xf3\xc9?z\xea\xc1\xae" + "\x87\xb7\xec\x03ii0\xe1\xf3\xf5\xf7\xd1\x84g\xebi" + "\xc2\xff\x9c\xfd\xe5c\xa5\x1b\xbf\xf3pyE\xee(\xf1" + "\x86\xf5\xe4\xf0\xe1\x06\x1a\xa1ud\xfe\x1d\xdf{\xfe\xa9" + "G@Y\x82\xe8\xbc\xde\xff\x91\x13\xec\xc0\xf8iX\x87" + "\"-p\xc9\xd6\x86W\x11P\xde\xe7\xfa\xfe\xe4\xa3\xcf" + "\xfc\xcdCO\xed\xfc2(\x0b\x11\x01\\8\xcf7," + "\xa0\xc1\xa4\xd94\xdb\xde\x93\xcf\xae)\xec\x1e;\xe4\x01" + "\xe4\xfe~\xc3\xec\xc5\x02\xc4\x9c\xed=\xbf-\xac{," + "\xf3X\x19\xba8\xfd\xd46\xbb\x95\xe2\xee\x99\xed\xc6\xbd" + "\xf4ggoY\xfd\xed\x81?\x8f|\xcb\x13\xe3\xf4\xed" + "\xce\x81\xb7\x8f&\xfb\x0aOL\x86\x08O\xfc\x0c\xe5\xad" + "\x09B\xe4\xee\x04\xad\xf1\x9bW\xdd\\\xbb\xf1\xec\x8a'" + "AZ\xe2\x0fs*\xb1\x8d\x86\x19y\xf1\xb1\xdf\x9b\xff" + "\xe2\x9d\x87AY\x8a\xe1\xee\xd0o(\xbf\xe5~\x1b;" + "=\x7f\xfc\xd9_\xfaw" + "G*\x09\xec\xaek4\xb9\x07=\xbf%\x0f$\xdd\xf8" + "\xee;:\xf6\x91\x9a\xaf\xbe\xf3\xf4\xa4\xeeOK{P" + "~Y\xa2\x09~$\x11\x93>\xd0\x83\xaf?\xd7\x16\xfb" + "N\x94j\x85+\x9bh\xad;\xae$\x87\x96\xdf,o" + "\xd0\xdf\xdc\xf6\\\x05(\xae\xe3|y\x13\xca7\xc84" + "\xdaR\x99\x9cc\x1f\x1b\xde)\x9d\xf9\xe9\xf3\x1e(^" + "\xe4\xc7\xe517r\x996n\xe5m_\xda\x13?\xfb" + "\xa5\x17hq\x91$\x88\xd7\xb8\xfcL\x1dB\xf9\x9a\x94" + ";rj\x0e\x03t\x9a\x9e\xfc\xe3o-\xcf\x9dzi" + "\xb2\x1dy|N+\xcaO\xcf\xa1\xc9\x0f\xcf!T\xcf" + ",<\xfc\xb9_?p\xfc\xb5r(\xee\xe4\x0d\x8d." + "k\xe67\xd2\xe4\xe77\x1c\xbcYsn=]\x89\x8c" + "\xeb\xd9\xdd\xf8\xcf(\xab\x8d4\xdc\xed\x8d4\\@\xd1" + "\xc9\xbc\x8f6\x8e\xa1|\xca\xf5>\xe1\x8e-\x9cU\xe7" + "n\xf9\xe9'_\x8f\xb0\xeaT\xe3\x95D\x875\x9f\xbe" + "m\xb8\xf6\xee3g\xa2\xcb:\xde\xe8\"\xfck\xf7\xd3" + "\xbf\xfd\x87G\x86n\xff\xd6\xb1\xb3\x11&\xd5\xce=D" + "\x9f\xfe\xfb\x9f\xbd\xf1\xc5s\x85\xdc\xbf\xb89\xe3\xefN" + "\xed\xdca7\xa4\xb9TS\xe6\xa4\x1b\xba[O\xf6\xbe" + "\x11\x05\x1c\x9bLrhi\xa2\xc1\x97\xde\xd1\xc97\\" + "\x7f\xeb\x1bUT\xfbD\xd30\xcaJ\x13}\xb0\xbai" + "'\xca\xbcy\x0e\x803\xf2W\xbbo}\xe2\xfbk\xde" + "\xf6\xd2\xd8]\xcb\xba\xe6~Z\xcb\x83\x9f\xef\xba\xe5\x86" + "\xd6\xa3oG\xc3P\x9a)\xb1d\xad\x99f\x1a\xb8\xfe" + "\xdc\xa7\xe6?\xf8\x83\xb7+\xf6\xcau\xdc\xd5\xbc\x1e\xe5" + "\x03\xcd\x04\xd7~r~s\xc5\x9f\xbe\xd6\x94hz\xb7" + "\x02\xdaY\xe4\xfbl\xf3\x18\xca'\x9a]\x98\x9a_ " + "F\x7f\xed\xeb\x87\xfe\xf1\xfc\xb1\x9b\xde\xab\x8a\xe1\xf9\x96" + "=(\xff\xbc\x85\x86=\xd5\"\xca\xa7Z~\x1f\xc0\xb9" + "\xe7\xf4g6\xbe\xfa\x85w\xde\xab\xa4\x98\x07|\xcb6" + "\x94\xcf\xba_\xfc\xb2\x85\x18\xfb\xc8\xda\x7f\xdb|n\xdf" + "\x07\x7f[5\xf6\xd6\xab\xc6P\xde\x7f\x15y\xee\xbb\xea" + "\x05\xf9\x9a4\xa5\xe2+\xe2cm]\x9b_:\x1f\xd9" + "*)\xbd\x89\xe0yX\xfc\xca\x99-\xbf\xf8\xcc\xef&" + "\x90/}\xa5\xbbSi\x82\xe7\xae7\xf7\xdf\xf4\xc5\x0d" + "\xdfx?B\x90\xee\xf4\xb7\xe9S\xbb\xa4\xeb\xa8Y67q]\xae" + "\xe8\x0e\xcd\x0c\xbd\x03\x9d\x92\xee\xfd\x80\xdc\xf4~H\xd0" + "\xa4\x1d\xd8\x8b\xd3X\xe2\x8dy\x8d\xebv\x8f\xce\x06\x8c" + "\x0a\xf4WN\x86\xfe\xca2\xfa\xdb#\xe8o]\x0e\xa0" + "\xdc\xc5P\xb9W@\x89\x95\xe1\xdf\xb1\x00@\xd9\xc2P" + "\xb9_@'\xebM\x92\x03\x80\x00\xd8\x01\xae\xda%\x93" + "[d\x9b\x0d\xd8\xcb\xd0\xc5\x7f6\xe0\xe6\x11nR\x04" + "\xfe~$T3;\x14\xec\xd9\x94\xcc\xea\xde\xa8Y\xb6" + "\xa6\x0f\xaeu\xed\xbdF\"\xafeG)\xb6zw\xb5" + "-\xcb\x00\x10\xa5\x0f\xac\x07@A\x92\x96\x03\xb4k\x83" + "\xbaar'\xa7YYC\xd79\xb0\xac\xbd\xb9_\xcd" + "\xabz\x96\x07\xd3\x89\x17\x9a\xce\x9b&\xc3\xcd\x11n^" + "\xabFX=\xafW5\xd5\x82\x05\xa0\xd4\x07\xa0v\xaf" + "\x07P\xba\x18*\xbd\x11PW\x13\xa8\xab\x18*\xb7F" + "@]G\xa0\xf62T6\x08\xe8\x18\xa66\xa8\xe97" + "r`f\x94\x99\x96\xad\xab\x05N\x00\x96\xc1\xd9l\x14" + "m\xcd\xd0-L\x86\x87\x12 &#\xb0\xd5L\xc5U" + "\x8f\xaa\xd7\xfa\\\xf3\xa9f\xe8\xf3\xfa\xb8U\xca\xdbh" + ")\xb1 \x9e\x86e\x00J\x0dC%%`\xbb\xe9\xfd" + "\x9e\x0c5\xc7\xff\xde\xdc\x01\x96\xa9`\xee\xbb\xfb\"\xb4" + "\xf3\xb1\xdc\xb18\xa4\x1d\x96\xa1\xdcEPng\xa8<" + "D\xfcD\x8f\x9f\x0f\x8c\x01(\x0f1T\xbe\"\xa0\x14" + "\x13R\x18C\x94\xf6Sqy\x94\xa1\xf2u\x01\x1d\xcb" + "\x9b\xba\x070\xe7c\x9e\xceYvO\xd1\xff\xdf\xe6\x9c" + "e\xf7\x1a\xa6\x8d\"\x08(\x02\xd1\xdc\xb0x\xe7\x00%" + "bO.\xcfo\xd2\x98nc\x1c\x04\x8c\x13\x08\xa6\x9a" + "\xe57\x1aT\x82\xf8F\xbb\xbcc a\x1d\xc0\xd4Y" + "\xea\x91\xac\xb3\xc4\xec!\xaf\x88\xf8 \\C\x84\xfa\x03" + "\x86\xca\x1fF@h\xa30\xaec\xa8|\\@G\xcd" + "f\x8d\x92n\xaf\x05\xa6\x0eV$Q\x86C\"k\xf2" + "\x90R\xfe\xb4\xb5\x17\xac\x16\x86>\xa0\x0d\x96L\xd5\x8e" + "lW\xa9\x98Sm>\xe1\xa72Wh\xc3\xa6\"K" + " `.\x91,~\xf5\xab\xa2\x0b+XQ\xa0\xfa&" + "\x03\x8a\x98\xf1Q\x86\xca\xf5\x93\xef\xf7\xe6\x02\xb7,u" + "\x90W\x15\x9fY\x17\x01H\xe7Y\x82\x80\x0e5:\xd3" + "\xaeu#E\x9b\xd6R\xef8\xdeb\x88\xa5\xf3\x18*" + "\xd7\x09\xd8\x80\xef;\xdej\x16\xee\x09\xb7-\xcdM\xd3" + "01\x19\x9e\xfcex\xb2\xe5\x09\xd0\xd0\xbb\xb8\xadj" + "y\xa4l\x0f\xf4q\x05\x88\xd3\xabZ!\x84\x9ey^" + "\xaf\x9a\xa0t\x8b\xee\x1d\xa5K\x92\xa1\xf2!\x01\x9dA" + "\xa2r/7Q3rkT\xdd\xc80\x9e\x0dy~" + "yS\xf7\xf1\xb4\xcb\x9c\x19\x8ec\xf220A\x04\xa6" + "H\x11D\xcaEk\xa8\x07\x02\x02l\xed\x0f\xcbEP" + "zwQN\xdd\xcbP\xd9\x1b9\xcfv\xaf\x8c\xd6\x8b" + "X\x0ac\x00\xd2~\xe2\xcf^\x86\xcaAa\xa2j\xe0" + "#\\\xb7\xbb\xb4A\x10\xb9\x15Zi\x89]\xda \x07" + "f]n\x19\xaf\x9d\x16*F\xbfe\xe4\xb9\xcd\xbbx" + "6\xafRf\x8ep\xef\xf72M\xfd\x8d\x9e\x9a\xd7}" + "U9\xe6\xf1\x9by\xa2-\x92g\xad!\xb5\x03\x98\x17" + ".\x0e\x93O\xe4\xa1\xd2J[EU\xb7\xaa\xca\x8f|" + "\xf1Ux%\xa6\x8a@a\xea\x05\xd5'\xf8\xfer\xcb" + "Y\xf9\xf8\x89\xc6\xb9<\x8c3\x08sY\x18f b" + "b `\x0c\xb0=\xeb\x0eX\x15k|\xbakK\xf8" + "\x0a2\xe6*H\xffB\x8e~\x17C\x92\x0e\x81 5" + "\x88\x8e\xbf~\xf4\xbf\x17\xab\xd4`|\xea\xf2u\x8bK" + "A\xb4h\xc6H\x12-\x9b,\x89\xccI\xce\xdcm\xd1" + "\x1c*\x9f\xb9\xbb\xc7\xc2t\xf1\xce\\\x00\xe9\xc0!\x00" + "\xe5 C\xe5\x1b\x02\xb6{B\x11\x93a\x13\xaa\xcc{" + "O\x01\xad2 \x9dU\xf3\xe1\x11\xec\x98\xbc\x98W\xb3" + "\xbc\x1b\xcb\xa2\x0f\x10A@t\x93\xadP4\xb9e\xa1" + "f\xe8JI\xcdk\xcc\x1e\x0d\x94\xbb^*\xf4\x9a|" + "DC\xa3du\xda6/\x88E\xdb\xaa\xd2\xf5\xd3\x80" + "\xc9\xaf\xc1\xae\xbe\x0ce\x1e\x89\xdf\x0e\x86\xca\xaa\x08L" + "=t*\xdf\xc4PY\x1b\xc2\xa4|\x0f@Y\xcbP" + "\xb9C\xc0D\xa9\xa4\x05'\x8f\x937\xb2\xee\xceCb" + "\x8dZ\xa8<\x80z,\xa1\x8f\x17\x0c\x9b\xe7G=\xd6" + "\xe6\xc2\xb8gZ7+\x0a\xbfwn\xfe\x7fR\xac\xb1" + "\xa9\xae\x90\xed\x1eR\x15[\xd0:\xd9\x16,\x8e\x04\xe3" + "\xaf{u\x7f\x18\x8c\xf8'|4(N\xbc@[\xeb" + "#_\x8e\xa8\x13\xc4\x9bC\x9f\xa9\xeb\xf1d%\xcbM" + "\xd0UFV\xcdWW\x19V\xb8\xa0\xbe\x9ei\x05\x89" + "NM\xe9,\x1a\xba\xcb\xd3\xeb\xfd\xe1\xe5Q\\\x09\x90" + "\xd9\x88\x0c3\xdb1\xc4I\xde\x8a\xcb\x012w\x91\xfd" + "^\x0c\xa1\x92w`\x13@f\x0b\xd9\xef\xc7\xe0\xaa-" + "\xef\xc2q\x80\xcc\xfdd~\x94\xdcc\xccMmy\x9f" + ";\xfc^\xb2\x1f${<\x96\xc28\x80|\x00\x17\x00" + "d\x1e%\xfb\x11\xb2\xcf\x12R8\x0b@>\x8c\xc3\x00" + "\x99'\xc9\xfe\x0c\xd9\xc5x\x0a\xdd.6\x9a\x00\x99\xbf" + "&\xfb\xf7\xc9^\xd3\x98\xc2\x1a\x00\xf9\xa8k\x7f\x8e\xec" + "?&{\xed\xdc\x14\xd6\x02\xc8?\xc2m\x00\x99\x1f\x92" + "\xfd5\xb2\xd7a\x8ad\xb6|\x1c\xc7\x002\xaf\x91\xfd" + "\x9f\xc8~\xc5\xac\x14^\x01 \xff\xdc]\xcfI\xb2\xff" + "\x8a\xec\xf5\xb1\x14is\xf9\x97x\x08 \xf3+\xb2\xff" + "\x07\xd9\x1b\xc4\x146\x00\xc8\xbfq\xe3:G\xf6\x1a\xa1" + "\xe2z\xeb\xf3\xba\xe2\x0e\xcb\x0c+\xe0\x0c/\xd7*\x9c" + "pC\xc5D\xd8L\x07\xc4\x04\xa0S4\x8c\xfc\x9a\x89" + "\xf9\x92\xb0\xd5A\xcb\xbf/'\xc3^\" \x19\x03u" + "\x08\x09C\xef\xc9\x05\x05\xad\xb2z\xfa+\xd1\xac\xce\x92" + "m\x94\x8a\x90&F\xe6\x82\x1ab\x96\xf4\x15\xa6QX" + "\x8b\xdc,h\xba\x9a\x9f\xa2\xaa\xd6\x82\x80\xb5P.`" + "\xfe\xd8\x17/\xb1\x17\xbe\xfd\x07\xbcf\x17\xe2\xb5\xb8V" + "\x1d\xac\x10\x1d\x0b\xa6\x10\x1d\x09=RD\xd3#j\xbe" + "T\xad\xe9/Mh\xf6q+AZc\xaa{\x8e\xdf" + "\x1d\xac(n\x17\x14\\\xeb\xaaU\x88\xab\xb8D\xbdJ" + "q\x8d\x87\x97\x18?\xf6\xa5\xad\x91\x1b`^\xb5\xb9e" + "w\x16\xb1\x98\xd7x\xee\xd3\xdcLD\x85IT\x8f\xcd" + "\xe4\xe4\x9b\xa0\xff\xdc\xe01\xf2 B \x08\xe5\xe0g" + "\x88\xf0 \xb7\xbd\x7f\xf5\xe8\x03\x86\xa7\xbc\xd0\xba\xac1" + "\\9\xc8\xa6\xde\xa3\xb0\xdd;]\xb5=\x93\xeaN\xab" + "\x10\xa37\x9b\x9a\x19\x8c:\x89\xc8\xf5/i\x91\x96\x1d" + "\xa5\xc1\x06\x86\xcaP$\x0d8\x9d\xd59\x86J1\x94" + "\x1d\x85\xbe\xb0_*1\xa1\xdc0\xa5\xf3\xbb\xc8P\xb9" + "K\xc0\x84Z\xb2\x870\x19\xbe\xa7M\x00db#\x8f" + "\xf2\xa1G\xcfq\xc0\x8d~zGN\xf5\xe0\xa1g\xba" + "\xd7\xfb\xe9\x05\xef_\x13\xa7\xdc\xd2\xe0\xc5c\xba\xaa\xc2" + "\xe7Q\x82\xa6&n7\xbaz\xdb\x7fSB\xff\xcd@" + ":\xbc\x09\x04\xe9/D\x0c\xdfI\xd0\x7f\x16\x91\x0e\x98" + " H\xfbD\x14\x82g?\xf4\x9f\xf7\xa4]\xf7\x81 " + "\xed\x10\x91\x05\xafv\xe8\xf7\xd1\xdbF\xeb\x10\x04\xe9n" + "\x11c\xc1{)\xfa]x\xe9\xb3\xc3 H\x9a\x88\xf1" + "\xe0A\x10\xfd\xe7!\xe9\xf6m H\xeb\xc2\x161\xb4" + "{qt\xa0\xe3\xe7\x02\xa4\xddl\x98\xd80\xf6\xbc\x00" + ":\xd0\xf1/\x8b\xecB\xb7E\xd7\xcb\xefpB\"\xab" + "\xda\xbc\x83\x14\xb8W\x10\xb1\\\x11\xa1\x03\x95\x18F\x1e" + "!\"\xfd\xad\xcb\xea\xe9T\xe5\xcf%)\\\x7f\x94K" + "\xac\xd7\x17i\xf5{\xe5\xa6\xdcG\x8f\x8c>\xecv\x80" + "Qi\x14\xa6P\xf5\x17)\xbb\xde\xe2\xc3\xd4`\x9e\xdc" + "\xbd:\x98\xe58\x15\xfc\x1f3TNFR\xff\x04\x19" + "_a\xa8\xbc\x1e\x91\xbb\xa7\xa8\x1e\x9cd\xa8\xbc\x1b\xbe" + "\x95\xbcu\x1f\x80\xf2.\xc3\xbe\x88z\x93\xfe\x9b\x1c\x7f" + "G\x1a\xc7\xd5n\xe8i\xb78\xee\x01\xc8\xd4\x90\xf6I" + "\xb9\xda-\xe6i7\x09\xfb\x012I\xb2\x7f(\xaa\xdd" + "\xe6\xe2z\x80L#\xd9\xe7\xe1\xc4\xdb\xbfX2Cy" + "\x9d7\x06Wi\xfa\xa4\x82\xc0\x7f\xbcA{\x85\xaa\xe5" + "K&\x87\xca\xdbNOWD\"y\xaf:^\x1b6" + "C\xe4\xcc\xa1\x15\xb4hg\xd0\x98\x99\xfa,\xcc\x1b\xa5" + "\xdc@^5y\xce\xdd}\xa4r\xd1\xcb\xe2J\x0dF" + "\xfe\xe6\x02 |\x1a\x8f\xa4\xc24N\xd8n\xd34L" + "\xa8\xb8\xd6,\x0e\xaf5\xc1\xadf}x\xb1\x94\x84\x8e" + "\xf2\xcd\xb2?\xbc\x8d\xa5\xb3j\xc9\xe2U\xf8\x00\xe3f" + "\xd0\xb6\xb3\x86\x8cR>\xd7\xc7A\xb4\xcd\xd1\xaa\xcbd" + "|\xba\xd5\x9ay5\xb3\xde\xad\x99\xfeS/\xfa/\xba" + "\x922\x06\x82\xb4\x9aj\xa6\xff\xea\x88\xfe\xdf\x1cH\x9d" + "\xe3 H\x9f\xa0\x9a\xe9\xbf\xb8\xa3\xff\x8a,\xb5\xbd\x08" + "\x82\xd4\x16y\x01\xf3Q\xaaz\x01\xf3~H\xd8\x9a\xf7" + "C\xf90\x16*Oc\xaae\xd1\x8eH\xcd\xe5\xb6\x9c" + "\xda\xbd\x16\xd1\xe5\xbc\x17M\xfb}%\xf8\xeb\x9f\xff\x9b" + "\xa6\xa0\x7f\xb6\xfeO\x00\x00\x00\xff\xff\xb4\x0bQ\xfc" func init() { schemas.Register(schema_db8274f9144abc7e, 0x82c325a07ad22a65, 0x839445a59fb01686, 0x83ced0145b2f114b, 0x84cb9536a2cf6d3c, 0x85c8cea1ab1894f3, 0x8635c6b4f45bf5cd, 0x904e297b87fbecea, 0x9496331ab9cd463f, 0x958096448eb3373e, 0x96b74375ce9b0ef6, 0x97b3c5c260257622, 0x9b87b390babc2ccf, 0xa29a916d4ebdd894, 0xa353a3556df74984, 0xa766b24d4fe5da35, 0xab6d5210c1f26687, 0xb046e578094b1ead, 0xb177ca2526a3ca76, 0xb48edfbdaa25db04, 0xb4bf9861fe035d04, 0xb5f39f082b9ac18a, 0xb70431c0dc014915, 0xc082ef6e0d42ed1d, 0xc5d6e311876a3604, 0xc793e50592935b4a, 0xcbd96442ae3bb01a, 0xd4d18de97bb12de3, 0xdb58ff694ba05cf9, 0xdbaa9d03d52b62dc, 0xdc3ed6801961e502, 0xe3e37d096a5b564e, 0xe5ceae5d6897d7be, 0xe6646dec8feaa6ee, 0xea50d822450d1f17, 0xea58385c65416035, 0xf24ec4ab5891b676, 0xf2c122394f447e8e, 0xf2c68e2547ec3866, 0xf41a0f001ad49e46, 0xf548cef9dea2a4a1, 0xf5f383d2785edb86, 0xf71695ec7fe85497, 0xf9cb7f4431a307d0, 0xfc5edf80e39c0796, 0xfeac5c8f4899ef7c) } ================================================ FILE: tunnelrpc/quic/cloudflared_client.go ================================================ package quic import ( "context" "fmt" "io" "net" "time" "zombiezen.com/go/capnproto2/rpc" "github.com/google/uuid" "github.com/cloudflare/cloudflared/tunnelrpc" "github.com/cloudflare/cloudflared/tunnelrpc/metrics" "github.com/cloudflare/cloudflared/tunnelrpc/pogs" ) // CloudflaredClient calls capnp rpc methods of SessionManager and ConfigurationManager. type CloudflaredClient struct { client pogs.CloudflaredServer_PogsClient transport rpc.Transport requestTimeout time.Duration } func NewCloudflaredClient(ctx context.Context, stream io.ReadWriteCloser, requestTimeout time.Duration) (*CloudflaredClient, error) { n, err := stream.Write(rpcStreamProtocolSignature[:]) if err != nil { return nil, err } if n != len(rpcStreamProtocolSignature) { return nil, fmt.Errorf("expect to write %d bytes for RPC stream protocol signature, wrote %d", len(rpcStreamProtocolSignature), n) } transport := tunnelrpc.SafeTransport(stream) conn := tunnelrpc.NewClientConn(transport) client := pogs.NewCloudflaredServer_PogsClient(conn.Bootstrap(ctx), conn) return &CloudflaredClient{ client: client, transport: transport, requestTimeout: requestTimeout, }, nil } func (c *CloudflaredClient) RegisterUdpSession(ctx context.Context, sessionID uuid.UUID, dstIP net.IP, dstPort uint16, closeIdleAfterHint time.Duration, traceContext string) (*pogs.RegisterUdpSessionResponse, error) { ctx, cancel := context.WithTimeout(ctx, c.requestTimeout) defer cancel() defer metrics.CapnpMetrics.ClientOperations.WithLabelValues(metrics.Cloudflared, metrics.OperationRegisterUdpSession).Inc() timer := metrics.NewClientOperationLatencyObserver(metrics.Cloudflared, metrics.OperationRegisterUdpSession) defer timer.ObserveDuration() resp, err := c.client.RegisterUdpSession(ctx, sessionID, dstIP, dstPort, closeIdleAfterHint, traceContext) if err != nil { metrics.CapnpMetrics.ClientFailures.WithLabelValues(metrics.Cloudflared, metrics.OperationRegisterUdpSession).Inc() } return resp, err } func (c *CloudflaredClient) UnregisterUdpSession(ctx context.Context, sessionID uuid.UUID, message string) error { ctx, cancel := context.WithTimeout(ctx, c.requestTimeout) defer cancel() defer metrics.CapnpMetrics.ClientOperations.WithLabelValues(metrics.Cloudflared, metrics.OperationUnregisterUdpSession).Inc() timer := metrics.NewClientOperationLatencyObserver(metrics.Cloudflared, metrics.OperationUnregisterUdpSession) defer timer.ObserveDuration() err := c.client.UnregisterUdpSession(ctx, sessionID, message) if err != nil { metrics.CapnpMetrics.ClientFailures.WithLabelValues(metrics.Cloudflared, metrics.OperationUnregisterUdpSession).Inc() } return err } func (c *CloudflaredClient) UpdateConfiguration(ctx context.Context, version int32, config []byte) (*pogs.UpdateConfigurationResponse, error) { ctx, cancel := context.WithTimeout(ctx, c.requestTimeout) defer cancel() defer metrics.CapnpMetrics.ClientOperations.WithLabelValues(metrics.Cloudflared, metrics.OperationUpdateConfiguration).Inc() timer := metrics.NewClientOperationLatencyObserver(metrics.Cloudflared, metrics.OperationUpdateConfiguration) defer timer.ObserveDuration() resp, err := c.client.UpdateConfiguration(ctx, version, config) if err != nil { metrics.CapnpMetrics.ClientFailures.WithLabelValues(metrics.Cloudflared, metrics.OperationUpdateConfiguration).Inc() } return resp, err } func (c *CloudflaredClient) Close() { _ = c.client.Close() _ = c.transport.Close() } ================================================ FILE: tunnelrpc/quic/cloudflared_server.go ================================================ package quic import ( "context" "fmt" "io" "time" "github.com/cloudflare/cloudflared/tunnelrpc" "github.com/cloudflare/cloudflared/tunnelrpc/pogs" ) // HandleRequestFunc wraps the proxied request from the upstream and also provides methods on the stream to // handle the response back. type HandleRequestFunc = func(ctx context.Context, stream *RequestServerStream) error // CloudflaredServer provides a handler interface for a client to provide methods to handle the different types of // requests that can be communicated by the stream. type CloudflaredServer struct { handleRequest HandleRequestFunc sessionManager pogs.SessionManager configManager pogs.ConfigurationManager responseTimeout time.Duration } func NewCloudflaredServer(handleRequest HandleRequestFunc, sessionManager pogs.SessionManager, configManager pogs.ConfigurationManager, responseTimeout time.Duration) *CloudflaredServer { return &CloudflaredServer{ handleRequest: handleRequest, sessionManager: sessionManager, configManager: configManager, responseTimeout: responseTimeout, } } // Serve executes the defined handlers in ServerStream on the provided stream if it is a proper RPC stream with the // correct preamble protocol signature. func (s *CloudflaredServer) Serve(ctx context.Context, stream io.ReadWriteCloser) error { signature, err := determineProtocol(stream) if err != nil { return err } switch signature { case dataStreamProtocolSignature: return s.handleRequest(ctx, &RequestServerStream{stream}) case rpcStreamProtocolSignature: return s.handleRPC(ctx, stream) default: return fmt.Errorf("unknown protocol %v", signature) } } func (s *CloudflaredServer) handleRPC(ctx context.Context, stream io.ReadWriteCloser) error { ctx, cancel := context.WithTimeout(ctx, s.responseTimeout) defer cancel() transport := tunnelrpc.SafeTransport(stream) defer transport.Close() main := pogs.CloudflaredServer_ServerToClient(s.sessionManager, s.configManager) rpcConn := tunnelrpc.NewServerConn(transport, main.Client) defer rpcConn.Close() // We ignore the errors here because if cloudflared fails to handle a request, we will just move on. select { case <-rpcConn.Done(): case <-ctx.Done(): } return nil } ================================================ FILE: tunnelrpc/quic/protocol.go ================================================ package quic import ( "fmt" "io" ) // protocolSignature defines the first 6 bytes of the stream, which is used to distinguish the type of stream. It // ensures whoever performs a handshake does not write data before writing the metadata. type protocolSignature [6]byte var ( // dataStreamProtocolSignature is a custom protocol signature for data stream dataStreamProtocolSignature = protocolSignature{0x0A, 0x36, 0xCD, 0x12, 0xA1, 0x3E} // rpcStreamProtocolSignature is a custom protocol signature for RPC stream rpcStreamProtocolSignature = protocolSignature{0x52, 0xBB, 0x82, 0x5C, 0xDB, 0x65} errDataStreamNotSupported = fmt.Errorf("data protocol not supported") errRPCStreamNotSupported = fmt.Errorf("rpc protocol not supported") ) type protocolVersion string const ( protocolV1 protocolVersion = "01" protocolVersionLength = 2 ) // determineProtocol reads the first 6 bytes from the stream to determine which protocol is spoken by the client. // The protocols are magic byte arrays understood by both sides of the stream. func determineProtocol(stream io.Reader) (protocolSignature, error) { signature, err := readSignature(stream) if err != nil { return protocolSignature{}, err } switch signature { case dataStreamProtocolSignature: return dataStreamProtocolSignature, nil case rpcStreamProtocolSignature: return rpcStreamProtocolSignature, nil default: return protocolSignature{}, fmt.Errorf("unknown signature %v", signature) } } func writeDataStreamPreamble(stream io.Writer) error { if err := writeSignature(stream, dataStreamProtocolSignature); err != nil { return err } return writeVersion(stream) } func writeVersion(stream io.Writer) error { _, err := stream.Write([]byte(protocolV1)[:protocolVersionLength]) return err } func readVersion(stream io.Reader) (string, error) { version := make([]byte, protocolVersionLength) _, err := stream.Read(version) return string(version), err } func readSignature(stream io.Reader) (protocolSignature, error) { var signature protocolSignature if _, err := io.ReadFull(stream, signature[:]); err != nil { return protocolSignature{}, err } return signature, nil } func writeSignature(stream io.Writer, signature protocolSignature) error { _, err := stream.Write(signature[:]) return err } ================================================ FILE: tunnelrpc/quic/request_client_stream.go ================================================ package quic import ( "fmt" "io" capnp "zombiezen.com/go/capnproto2" "github.com/cloudflare/cloudflared/tunnelrpc/pogs" ) // RequestClientStream is a stream to provide requests to the server. This operation is typically driven by the edge service. type RequestClientStream struct { io.ReadWriteCloser } // WriteConnectRequestData writes requestMeta to a stream. func (rcs *RequestClientStream) WriteConnectRequestData(dest string, connectionType pogs.ConnectionType, metadata ...pogs.Metadata) error { connectRequest := &pogs.ConnectRequest{ Dest: dest, Type: connectionType, Metadata: metadata, } msg, err := connectRequest.ToPogs() if err != nil { return err } if err := writeDataStreamPreamble(rcs); err != nil { return err } return capnp.NewEncoder(rcs).Encode(msg) } // ReadConnectResponseData reads the response from the rpc stream to a ConnectResponse. func (rcs *RequestClientStream) ReadConnectResponseData() (*pogs.ConnectResponse, error) { signature, err := determineProtocol(rcs) if err != nil { return nil, err } if signature != dataStreamProtocolSignature { return nil, fmt.Errorf("wrong protocol signature %v", signature) } // This is a NO-OP for now. We could cause a branching if we wanted to use multiple versions. if _, err := readVersion(rcs); err != nil { return nil, err } msg, err := capnp.NewDecoder(rcs).Decode() if err != nil { return nil, err } r := &pogs.ConnectResponse{} if err := r.FromPogs(msg); err != nil { return nil, err } return r, nil } ================================================ FILE: tunnelrpc/quic/request_server_stream.go ================================================ package quic import ( "io" capnp "zombiezen.com/go/capnproto2" "github.com/cloudflare/cloudflared/tunnelrpc/pogs" ) // RequestServerStream is a stream to serve requests type RequestServerStream struct { io.ReadWriteCloser } // ReadConnectRequestData reads the handshake data from a QUIC stream. func (rss *RequestServerStream) ReadConnectRequestData() (*pogs.ConnectRequest, error) { // This is a NO-OP for now. We could cause a branching if we wanted to use multiple versions. if _, err := readVersion(rss); err != nil { return nil, err } msg, err := capnp.NewDecoder(rss).Decode() if err != nil { return nil, err } r := &pogs.ConnectRequest{} if err := r.FromPogs(msg); err != nil { return nil, err } return r, nil } // WriteConnectResponseData writes response to a QUIC stream. func (rss *RequestServerStream) WriteConnectResponseData(respErr error, metadata ...pogs.Metadata) error { var connectResponse *pogs.ConnectResponse if respErr != nil { connectResponse = &pogs.ConnectResponse{ Error: respErr.Error(), Metadata: metadata, } } else { connectResponse = &pogs.ConnectResponse{ Metadata: metadata, } } msg, err := connectResponse.ToPogs() if err != nil { return err } if err := writeDataStreamPreamble(rss); err != nil { return err } return capnp.NewEncoder(rss).Encode(msg) } ================================================ FILE: tunnelrpc/quic/request_server_stream_test.go ================================================ package quic import ( "bytes" "context" "errors" "fmt" "io" "net" "testing" "time" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/cloudflare/cloudflared/tunnelrpc/pogs" ) const ( testCloseIdleAfterHint = time.Minute * 2 ) func TestConnectRequestData(t *testing.T) { tests := []struct { name string hostname string connectionType pogs.ConnectionType metadata []pogs.Metadata }{ { name: "Signature verified and request metadata is unmarshaled and read correctly", hostname: "tunnel.com", connectionType: pogs.ConnectionTypeHTTP, metadata: []pogs.Metadata{ { Key: "key", Val: "1234", }, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { b := &bytes.Buffer{} reqClientStream := RequestClientStream{noopCloser{b}} err := reqClientStream.WriteConnectRequestData(test.hostname, test.connectionType, test.metadata...) require.NoError(t, err) protocol, err := determineProtocol(b) require.NoError(t, err) require.Equal(t, dataStreamProtocolSignature, protocol) reqServerStream := RequestServerStream{&noopCloser{b}} reqMeta, err := reqServerStream.ReadConnectRequestData() require.NoError(t, err) assert.Equal(t, test.metadata, reqMeta.Metadata) assert.Equal(t, test.hostname, reqMeta.Dest) assert.Equal(t, test.connectionType, reqMeta.Type) }) } } func TestConnectResponseMeta(t *testing.T) { tests := []struct { name string err error metadata []pogs.Metadata }{ { name: "Signature verified and response metadata is unmarshaled and read correctly", metadata: []pogs.Metadata{ { Key: "key", Val: "1234", }, }, }, { name: "If error is not empty, other fields should be blank", err: errors.New("something happened"), metadata: []pogs.Metadata{ { Key: "key", Val: "1234", }, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { b := &bytes.Buffer{} reqServerStream := RequestServerStream{noopCloser{b}} err := reqServerStream.WriteConnectResponseData(test.err, test.metadata...) require.NoError(t, err) reqClientStream := RequestClientStream{noopCloser{b}} respMeta, err := reqClientStream.ReadConnectResponseData() require.NoError(t, err) require.Equal(t, test.metadata, respMeta.Metadata) }) } } func TestRegisterUdpSession(t *testing.T) { unregisterMessage := "closed by eyeball" tests := []struct { name string sessionRPCServer mockSessionRPCServer }{ { name: "RegisterUdpSession (no trace context)", sessionRPCServer: mockSessionRPCServer{ sessionID: uuid.New(), dstIP: net.IP{172, 16, 0, 1}, dstPort: 8000, closeIdleAfter: testCloseIdleAfterHint, unregisterMessage: unregisterMessage, traceContext: "", }, }, { name: "RegisterUdpSession (with trace context)", sessionRPCServer: mockSessionRPCServer{ sessionID: uuid.New(), dstIP: net.IP{172, 16, 0, 1}, dstPort: 8000, closeIdleAfter: testCloseIdleAfterHint, unregisterMessage: unregisterMessage, traceContext: "1241ce3ecdefc68854e8514e69ba42ca:b38f1bf5eae406f3:0:1", }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { clientStream, serverStream := newMockRPCStreams() sessionRegisteredChan := make(chan struct{}) go func() { ss := NewCloudflaredServer(nil, test.sessionRPCServer, nil, 10*time.Second) err := ss.Serve(t.Context(), serverStream) assert.NoError(t, err) serverStream.Close() close(sessionRegisteredChan) }() rpcClientStream, err := NewCloudflaredClient(t.Context(), clientStream, 5*time.Second) require.NoError(t, err) reg, err := rpcClientStream.RegisterUdpSession(t.Context(), test.sessionRPCServer.sessionID, test.sessionRPCServer.dstIP, test.sessionRPCServer.dstPort, testCloseIdleAfterHint, test.sessionRPCServer.traceContext) require.NoError(t, err) require.NoError(t, reg.Err) // Different sessionID, the RPC server should reject the registration reg, err = rpcClientStream.RegisterUdpSession(t.Context(), uuid.New(), test.sessionRPCServer.dstIP, test.sessionRPCServer.dstPort, testCloseIdleAfterHint, test.sessionRPCServer.traceContext) require.NoError(t, err) require.Error(t, reg.Err) require.NoError(t, rpcClientStream.UnregisterUdpSession(t.Context(), test.sessionRPCServer.sessionID, unregisterMessage)) // Different sessionID, the RPC server should reject the unregistration require.Error(t, rpcClientStream.UnregisterUdpSession(t.Context(), uuid.New(), unregisterMessage)) rpcClientStream.Close() <-sessionRegisteredChan }) } } func TestManageConfiguration(t *testing.T) { var ( version int32 = 168 config = []byte(t.Name()) ) clientStream, serverStream := newMockRPCStreams() configRPCServer := mockConfigRPCServer{ version: version, config: config, } updatedChan := make(chan struct{}) go func() { server := NewCloudflaredServer(nil, nil, configRPCServer, 10*time.Second) err := server.Serve(t.Context(), serverStream) assert.NoError(t, err) serverStream.Close() close(updatedChan) }() ctx, cancel := context.WithTimeout(t.Context(), time.Second) defer cancel() rpcClientStream, err := NewCloudflaredClient(ctx, clientStream, 5*time.Second) require.NoError(t, err) result, err := rpcClientStream.UpdateConfiguration(ctx, version, config) require.NoError(t, err) require.Equal(t, version, result.LastAppliedVersion) require.NoError(t, result.Err) rpcClientStream.Close() <-updatedChan } type mockSessionRPCServer struct { sessionID uuid.UUID dstIP net.IP dstPort uint16 closeIdleAfter time.Duration unregisterMessage string traceContext string } func (s mockSessionRPCServer) RegisterUdpSession(_ context.Context, sessionID uuid.UUID, dstIP net.IP, dstPort uint16, closeIdleAfter time.Duration, traceContext string) (*pogs.RegisterUdpSessionResponse, error) { if s.sessionID != sessionID { return nil, fmt.Errorf("expect session ID %s, got %s", s.sessionID, sessionID) } if !s.dstIP.Equal(dstIP) { return nil, fmt.Errorf("expect destination IP %s, got %s", s.dstIP, dstIP) } if s.dstPort != dstPort { return nil, fmt.Errorf("expect destination port %d, got %d", s.dstPort, dstPort) } if s.closeIdleAfter != closeIdleAfter { return nil, fmt.Errorf("expect closeIdleAfter %d, got %d", s.closeIdleAfter, closeIdleAfter) } if s.traceContext != traceContext { return nil, fmt.Errorf("expect traceContext %s, got %s", s.traceContext, traceContext) } return &pogs.RegisterUdpSessionResponse{}, nil } func (s mockSessionRPCServer) UnregisterUdpSession(_ context.Context, sessionID uuid.UUID, message string) error { if s.sessionID != sessionID { return fmt.Errorf("expect session ID %s, got %s", s.sessionID, sessionID) } if s.unregisterMessage != message { return fmt.Errorf("expect unregister message %s, got %s", s.unregisterMessage, message) } return nil } type mockConfigRPCServer struct { version int32 config []byte } func (s mockConfigRPCServer) UpdateConfiguration(_ context.Context, version int32, config []byte) *pogs.UpdateConfigurationResponse { if s.version != version { return &pogs.UpdateConfigurationResponse{ Err: fmt.Errorf("expect version %d, got %d", s.version, version), } } if !bytes.Equal(s.config, config) { return &pogs.UpdateConfigurationResponse{ Err: fmt.Errorf("expect config %v, got %v", s.config, config), } } return &pogs.UpdateConfigurationResponse{LastAppliedVersion: version} } type mockRPCStream struct { io.ReadCloser io.WriteCloser } func newMockRPCStreams() (client io.ReadWriteCloser, server io.ReadWriteCloser) { clientReader, serverWriter := io.Pipe() serverReader, clientWriter := io.Pipe() client = mockRPCStream{clientReader, clientWriter} server = mockRPCStream{serverReader, serverWriter} return } func (s mockRPCStream) Close() error { _ = s.ReadCloser.Close() _ = s.WriteCloser.Close() return nil } type noopCloser struct { io.ReadWriter } func (noopCloser) Close() error { return nil } ================================================ FILE: tunnelrpc/quic/session_client.go ================================================ package quic import ( "context" "fmt" "io" "net" "time" "github.com/google/uuid" "zombiezen.com/go/capnproto2/rpc" "github.com/cloudflare/cloudflared/tunnelrpc" "github.com/cloudflare/cloudflared/tunnelrpc/metrics" "github.com/cloudflare/cloudflared/tunnelrpc/pogs" ) // SessionClient calls capnp rpc methods of SessionManager. type SessionClient struct { client pogs.SessionManager_PogsClient transport rpc.Transport requestTimeout time.Duration } func NewSessionClient(ctx context.Context, stream io.ReadWriteCloser, requestTimeout time.Duration) (*SessionClient, error) { n, err := stream.Write(rpcStreamProtocolSignature[:]) if err != nil { return nil, err } if n != len(rpcStreamProtocolSignature) { return nil, fmt.Errorf("expect to write %d bytes for RPC stream protocol signature, wrote %d", len(rpcStreamProtocolSignature), n) } transport := tunnelrpc.SafeTransport(stream) conn := tunnelrpc.NewClientConn(transport) return &SessionClient{ client: pogs.NewSessionManager_PogsClient(conn.Bootstrap(ctx), conn), transport: transport, requestTimeout: requestTimeout, }, nil } func (c *SessionClient) RegisterUdpSession(ctx context.Context, sessionID uuid.UUID, dstIP net.IP, dstPort uint16, closeIdleAfterHint time.Duration, traceContext string) (*pogs.RegisterUdpSessionResponse, error) { ctx, cancel := context.WithTimeout(ctx, c.requestTimeout) defer cancel() defer metrics.CapnpMetrics.ClientOperations.WithLabelValues(metrics.SessionManager, metrics.OperationRegisterUdpSession).Inc() timer := metrics.NewClientOperationLatencyObserver(metrics.SessionManager, metrics.OperationRegisterUdpSession) defer timer.ObserveDuration() resp, err := c.client.RegisterUdpSession(ctx, sessionID, dstIP, dstPort, closeIdleAfterHint, traceContext) if err != nil { metrics.CapnpMetrics.ClientFailures.WithLabelValues(metrics.SessionManager, metrics.OperationRegisterUdpSession).Inc() } return resp, err } func (c *SessionClient) UnregisterUdpSession(ctx context.Context, sessionID uuid.UUID, message string) error { ctx, cancel := context.WithTimeout(ctx, c.requestTimeout) defer cancel() defer metrics.CapnpMetrics.ClientOperations.WithLabelValues(metrics.SessionManager, metrics.OperationUnregisterUdpSession).Inc() timer := metrics.NewClientOperationLatencyObserver(metrics.SessionManager, metrics.OperationUnregisterUdpSession) defer timer.ObserveDuration() err := c.client.UnregisterUdpSession(ctx, sessionID, message) if err != nil { metrics.CapnpMetrics.ClientFailures.WithLabelValues(metrics.SessionManager, metrics.OperationUnregisterUdpSession).Inc() } return err } func (c *SessionClient) Close() { _ = c.client.Close() _ = c.transport.Close() } ================================================ FILE: tunnelrpc/quic/session_server.go ================================================ package quic import ( "context" "fmt" "io" "time" "github.com/cloudflare/cloudflared/tunnelrpc" "github.com/cloudflare/cloudflared/tunnelrpc/pogs" ) // SessionManagerServer handles streams with the SessionManager RPCs. type SessionManagerServer struct { sessionManager pogs.SessionManager responseTimeout time.Duration } func NewSessionManagerServer(sessionManager pogs.SessionManager, responseTimeout time.Duration) *SessionManagerServer { return &SessionManagerServer{ sessionManager: sessionManager, responseTimeout: responseTimeout, } } func (s *SessionManagerServer) Serve(ctx context.Context, stream io.ReadWriteCloser) error { signature, err := determineProtocol(stream) if err != nil { return err } switch signature { case rpcStreamProtocolSignature: break case dataStreamProtocolSignature: return errDataStreamNotSupported default: return fmt.Errorf("unknown protocol %v", signature) } // Every new quic.Stream request aligns to a new RPC request, this is why there is a timeout for the server-side // of the RPC request. ctx, cancel := context.WithTimeout(ctx, s.responseTimeout) defer cancel() transport := tunnelrpc.SafeTransport(stream) defer transport.Close() main := pogs.SessionManager_ServerToClient(s.sessionManager) rpcConn := tunnelrpc.NewServerConn(transport, main.Client) defer rpcConn.Close() select { case <-rpcConn.Done(): return nil case <-ctx.Done(): return ctx.Err() } } ================================================ FILE: tunnelrpc/registration_client.go ================================================ package tunnelrpc import ( "context" "io" "net" "time" "github.com/google/uuid" "zombiezen.com/go/capnproto2/rpc" "github.com/cloudflare/cloudflared/tunnelrpc/metrics" "github.com/cloudflare/cloudflared/tunnelrpc/pogs" ) type RegistrationClient interface { RegisterConnection( ctx context.Context, auth pogs.TunnelAuth, tunnelID uuid.UUID, options *pogs.ConnectionOptions, connIndex uint8, edgeAddress net.IP, ) (*pogs.ConnectionDetails, error) SendLocalConfiguration(ctx context.Context, config []byte) error GracefulShutdown(ctx context.Context, gracePeriod time.Duration) error Close() } type registrationClient struct { client pogs.RegistrationServer_PogsClient transport rpc.Transport requestTimeout time.Duration } func NewRegistrationClient(ctx context.Context, stream io.ReadWriteCloser, requestTimeout time.Duration) RegistrationClient { transport := SafeTransport(stream) conn := NewClientConn(transport) client := pogs.NewRegistrationServer_PogsClient(conn.Bootstrap(ctx), conn) return ®istrationClient{ client: client, transport: transport, requestTimeout: requestTimeout, } } func (r *registrationClient) RegisterConnection( ctx context.Context, auth pogs.TunnelAuth, tunnelID uuid.UUID, options *pogs.ConnectionOptions, connIndex uint8, edgeAddress net.IP, ) (*pogs.ConnectionDetails, error) { ctx, cancel := context.WithTimeout(ctx, r.requestTimeout) defer cancel() defer metrics.CapnpMetrics.ClientOperations.WithLabelValues(metrics.Registration, metrics.OperationRegisterConnection).Inc() timer := metrics.NewClientOperationLatencyObserver(metrics.Registration, metrics.OperationRegisterConnection) defer timer.ObserveDuration() conn, err := r.client.RegisterConnection(ctx, auth, tunnelID, connIndex, options) if err != nil { metrics.CapnpMetrics.ClientFailures.WithLabelValues(metrics.Registration, metrics.OperationRegisterConnection).Inc() } return conn, err } func (r *registrationClient) SendLocalConfiguration(ctx context.Context, config []byte) error { ctx, cancel := context.WithTimeout(ctx, r.requestTimeout) defer cancel() defer metrics.CapnpMetrics.ClientOperations.WithLabelValues(metrics.Registration, metrics.OperationUpdateLocalConfiguration).Inc() timer := metrics.NewClientOperationLatencyObserver(metrics.Registration, metrics.OperationUpdateLocalConfiguration) defer timer.ObserveDuration() err := r.client.SendLocalConfiguration(ctx, config) if err != nil { metrics.CapnpMetrics.ClientFailures.WithLabelValues(metrics.Registration, metrics.OperationUpdateLocalConfiguration).Inc() } return err } func (r *registrationClient) GracefulShutdown(ctx context.Context, gracePeriod time.Duration) error { ctx, cancel := context.WithTimeout(ctx, gracePeriod) defer cancel() defer metrics.CapnpMetrics.ClientOperations.WithLabelValues(metrics.Registration, metrics.OperationUnregisterConnection).Inc() timer := metrics.NewClientOperationLatencyObserver(metrics.Registration, metrics.OperationUnregisterConnection) defer timer.ObserveDuration() err := r.client.UnregisterConnection(ctx) if err != nil { metrics.CapnpMetrics.ClientFailures.WithLabelValues(metrics.Registration, metrics.OperationUnregisterConnection).Inc() return err } return nil } func (r *registrationClient) Close() { // Closing the client will also close the connection _ = r.client.Close() // Closing the transport also closes the stream _ = r.transport.Close() } ================================================ FILE: tunnelrpc/registration_server.go ================================================ package tunnelrpc import ( "context" "io" "github.com/cloudflare/cloudflared/tunnelrpc/pogs" ) // RegistrationServer provides a handler interface for a client to provide methods to handle the different types of // requests that can be communicated by the stream. type RegistrationServer struct { registrationServer pogs.RegistrationServer } func NewRegistrationServer(registrationServer pogs.RegistrationServer) *RegistrationServer { return &RegistrationServer{ registrationServer: registrationServer, } } // Serve listens for all RegistrationServer RPCs, including UnregisterConnection until the underlying connection // is terminated. func (s *RegistrationServer) Serve(ctx context.Context, stream io.ReadWriteCloser) error { transport := SafeTransport(stream) defer transport.Close() main := pogs.RegistrationServer_ServerToClient(s.registrationServer) rpcConn := NewServerConn(transport, main.Client) select { case <-rpcConn.Done(): return rpcConn.Wait() case <-ctx.Done(): return ctx.Err() } } ================================================ FILE: tunnelrpc/utils.go ================================================ package tunnelrpc import ( "context" "io" "time" "github.com/pkg/errors" capnp "zombiezen.com/go/capnproto2" "zombiezen.com/go/capnproto2/rpc" ) const ( // These default values are here so that we give some time for the underlying connection/stream // to recover in the face of what we believe to be temporarily errors. // We don't want to be too aggressive, as the end result of giving a final error (non-temporary) // will result in the connection to be dropped. // In turn, the other side will probably reconnect, which will put again more pressure in the overall system. // So, the best solution is to give it some conservative time to recover. defaultSleepBetweenTemporaryError = 500 * time.Millisecond defaultMaxRetries = 3 ) type readWriterSafeTemporaryErrorCloser struct { io.ReadWriteCloser retries int sleepBetweenRetries time.Duration maxRetries int } func (r *readWriterSafeTemporaryErrorCloser) Read(p []byte) (n int, err error) { n, err = r.ReadWriteCloser.Read(p) // if there was a failure reading from the read closer, and the error is temporary, try again in some seconds // otherwise, just fail without a temporary error. if n == 0 && err != nil && isTemporaryError(err) { if r.retries >= r.maxRetries { return 0, errors.Wrap(err, "failed read from capnproto ReaderWriter after multiple temporary errors") } else { r.retries += 1 // sleep for some time to prevent quick read loops that cause exhaustion of CPU resources time.Sleep(r.sleepBetweenRetries) } } if err == nil { r.retries = 0 } return n, err } func SafeTransport(rw io.ReadWriteCloser) rpc.Transport { return rpc.StreamTransport(&readWriterSafeTemporaryErrorCloser{ ReadWriteCloser: rw, maxRetries: defaultMaxRetries, sleepBetweenRetries: defaultSleepBetweenTemporaryError, }) } // isTemporaryError reports whether e has a Temporary() method that // returns true. func isTemporaryError(e error) bool { type temp interface { Temporary() bool } t, ok := e.(temp) return ok && t.Temporary() } // NoopCapnpLogger provides a logger to discard all capnp rpc internal logging messages as // they are by default provided to stdout if no logger interface is provided. These logging // messages in cloudflared have typically not provided a high amount of pratical value // as the messages are extremely verbose and don't provide a good insight into the message // contents or rpc method names. type noopCapnpLogger struct{} func (noopCapnpLogger) Infof(ctx context.Context, format string, args ...interface{}) {} func (noopCapnpLogger) Errorf(ctx context.Context, format string, args ...interface{}) {} func NewClientConn(transport rpc.Transport) *rpc.Conn { return rpc.NewConn(transport, rpc.ConnLog(noopCapnpLogger{})) } func NewServerConn(transport rpc.Transport, client capnp.Client) *rpc.Conn { return rpc.NewConn(transport, rpc.MainInterface(client), rpc.ConnLog(noopCapnpLogger{})) } ================================================ FILE: tunnelstate/conntracker.go ================================================ package tunnelstate import ( "net" "sync" "github.com/rs/zerolog" "github.com/cloudflare/cloudflared/connection" ) type ConnTracker struct { mutex sync.RWMutex // int is the connection Index connectionInfo map[uint8]ConnectionInfo log *zerolog.Logger } type ConnectionInfo struct { IsConnected bool `json:"isConnected,omitempty"` Protocol connection.Protocol `json:"protocol,omitempty"` EdgeAddress net.IP `json:"edgeAddress,omitempty"` } // Convinience struct to extend the connection with its index. type IndexedConnectionInfo struct { ConnectionInfo Index uint8 `json:"index,omitempty"` } func NewConnTracker( log *zerolog.Logger, ) *ConnTracker { return &ConnTracker{ connectionInfo: make(map[uint8]ConnectionInfo, 0), log: log, } } func (ct *ConnTracker) OnTunnelEvent(c connection.Event) { switch c.EventType { case connection.Connected: ct.mutex.Lock() ci := ConnectionInfo{ IsConnected: true, Protocol: c.Protocol, EdgeAddress: c.EdgeAddress, } ct.connectionInfo[c.Index] = ci ct.mutex.Unlock() case connection.Disconnected, connection.Reconnecting, connection.RegisteringTunnel, connection.Unregistering: ct.mutex.Lock() ci := ct.connectionInfo[c.Index] ci.IsConnected = false ct.connectionInfo[c.Index] = ci ct.mutex.Unlock() default: ct.log.Error().Msgf("Unknown connection event case %v", c) } } func (ct *ConnTracker) CountActiveConns() uint { ct.mutex.RLock() defer ct.mutex.RUnlock() active := uint(0) for _, ci := range ct.connectionInfo { if ci.IsConnected { active++ } } return active } // HasConnectedWith checks if we've ever had a successful connection to the edge // with said protocol. func (ct *ConnTracker) HasConnectedWith(protocol connection.Protocol) bool { ct.mutex.RLock() defer ct.mutex.RUnlock() for _, ci := range ct.connectionInfo { if ci.Protocol == protocol { return true } } return false } // Returns the connection information iff it is connected this // also leverages the [IndexedConnectionInfo] to also provide the connection index func (ct *ConnTracker) GetActiveConnections() []IndexedConnectionInfo { ct.mutex.RLock() defer ct.mutex.RUnlock() connections := make([]IndexedConnectionInfo, 0) for key, value := range ct.connectionInfo { if value.IsConnected { info := IndexedConnectionInfo{value, key} connections = append(connections, info) } } return connections } ================================================ FILE: validation/validation.go ================================================ package validation import ( "context" "fmt" "net" "net/http" "net/url" "strings" "time" "github.com/coreos/go-oidc/v3/oidc" "github.com/pkg/errors" "golang.org/x/net/idna" ) const ( defaultScheme = "http" accessDomain = "cloudflareaccess.com" accessCertPath = "/cdn-cgi/access/certs" accessJwtHeader = "Cf-access-jwt-assertion" ) var ( supportedProtocols = []string{"http", "https", "rdp", "ssh", "smb", "tcp"} validationTimeout = time.Duration(30 * time.Second) ) func ValidateHostname(hostname string) (string, error) { if hostname == "" { return "", nil } // users gives url(contains schema) not just hostname if strings.Contains(hostname, ":") || strings.Contains(hostname, "%3A") { unescapeHostname, err := url.PathUnescape(hostname) if err != nil { return "", fmt.Errorf("Hostname(actually a URL) %s has invalid escape characters %s", hostname, unescapeHostname) } hostnameToURL, err := url.Parse(unescapeHostname) if err != nil { return "", fmt.Errorf("Hostname(actually a URL) %s has invalid format %s", hostname, hostnameToURL) } asciiHostname, err := idna.ToASCII(hostnameToURL.Hostname()) if err != nil { return "", fmt.Errorf("Hostname(actually a URL) %s has invalid ASCII encdoing %s", hostname, asciiHostname) } return asciiHostname, nil } asciiHostname, err := idna.ToASCII(hostname) if err != nil { return "", fmt.Errorf("Hostname %s has invalid ASCII encdoing %s", hostname, asciiHostname) } hostnameToURL, err := url.Parse(asciiHostname) if err != nil { return "", fmt.Errorf("Hostname %s is not valid", hostnameToURL) } return hostnameToURL.RequestURI(), nil } // ValidateUrl returns a validated version of `originUrl` with a scheme prepended (by default http://). // Note: when originUrl contains a scheme, the path is removed: // // ValidateUrl("https://localhost:8080/api/") => "https://localhost:8080" // // but when it does not, the path is preserved: // // ValidateUrl("localhost:8080/api/") => "http://localhost:8080/api/" // // This is arguably a bug, but changing it might break some cloudflared users. func ValidateUrl(originUrl string) (*url.URL, error) { urlStr, err := validateUrlString(originUrl) if err != nil { return nil, err } return url.Parse(urlStr) } func validateUrlString(originUrl string) (string, error) { if originUrl == "" { return "", fmt.Errorf("URL should not be empty") } if net.ParseIP(originUrl) != nil { return validateIP("", originUrl, "") } else if strings.HasPrefix(originUrl, "[") && strings.HasSuffix(originUrl, "]") { // ParseIP doesn't recoginze [::1] return validateIP("", originUrl[1:len(originUrl)-1], "") } host, port, err := net.SplitHostPort(originUrl) // user might pass in an ip address like 127.0.0.1 if err == nil && net.ParseIP(host) != nil { return validateIP("", host, port) } unescapedUrl, err := url.PathUnescape(originUrl) if err != nil { return "", fmt.Errorf("URL %s has invalid escape characters %s", originUrl, unescapedUrl) } parsedUrl, err := url.Parse(unescapedUrl) if err != nil { return "", fmt.Errorf("URL %s has invalid format", originUrl) } // if the url is in the form of host:port, IsAbs() will think host is the schema var hostname string hasScheme := parsedUrl.IsAbs() && parsedUrl.Host != "" if hasScheme { err := validateScheme(parsedUrl.Scheme) if err != nil { return "", err } // The earlier check for ip address will miss the case http://[::1] // and http://[::1]:8080 if net.ParseIP(parsedUrl.Hostname()) != nil { return validateIP(parsedUrl.Scheme, parsedUrl.Hostname(), parsedUrl.Port()) } hostname, err = ValidateHostname(parsedUrl.Hostname()) if err != nil { return "", fmt.Errorf("URL %s has invalid format", originUrl) } if parsedUrl.Port() != "" { return fmt.Sprintf("%s://%s", parsedUrl.Scheme, net.JoinHostPort(hostname, parsedUrl.Port())), nil } return fmt.Sprintf("%s://%s", parsedUrl.Scheme, hostname), nil } else { if host == "" { hostname, err = ValidateHostname(originUrl) if err != nil { return "", fmt.Errorf("URL no %s has invalid format", originUrl) } return fmt.Sprintf("%s://%s", defaultScheme, hostname), nil } else { hostname, err = ValidateHostname(host) if err != nil { return "", fmt.Errorf("URL %s has invalid format", originUrl) } // This is why the path is preserved when `originUrl` doesn't have a schema. // Using `parsedUrl.Port()` here, instead of `port`, would remove the path return fmt.Sprintf("%s://%s", defaultScheme, net.JoinHostPort(hostname, port)), nil } } } func validateScheme(scheme string) error { for _, protocol := range supportedProtocols { if scheme == protocol { return nil } } return fmt.Errorf("Currently Cloudflare Tunnel does not support %s protocol.", scheme) } func validateIP(scheme, host, port string) (string, error) { if scheme == "" { scheme = defaultScheme } if port != "" { return fmt.Sprintf("%s://%s", scheme, net.JoinHostPort(host, port)), nil } else if strings.Contains(host, ":") { // IPv6 return fmt.Sprintf("%s://[%s]", scheme, host), nil } return fmt.Sprintf("%s://%s", scheme, host), nil } // Access checks if a JWT from Cloudflare Access is valid. type Access struct { verifier *oidc.IDTokenVerifier } func NewAccessValidator(ctx context.Context, domain, issuer, applicationAUD string) (*Access, error) { domainURL, err := validateUrlString(domain) if err != nil { return nil, err } issuerURL, err := validateUrlString(issuer) if err != nil { return nil, err } // An issuerURL from Cloudflare Access will always use HTTPS. issuerURL = strings.Replace(issuerURL, "http:", "https:", 1) keySet := oidc.NewRemoteKeySet(ctx, domainURL+accessCertPath) return &Access{oidc.NewVerifier(issuerURL, keySet, &oidc.Config{ClientID: applicationAUD})}, nil } func (a *Access) Validate(ctx context.Context, jwt string) error { token, err := a.verifier.Verify(ctx, jwt) if err != nil { return errors.Wrapf(err, "token is invalid: %s", jwt) } // Perform extra sanity checks, just to be safe. if token == nil { return fmt.Errorf("token is nil: %s", jwt) } if !strings.HasSuffix(token.Issuer, accessDomain) { return fmt.Errorf("token has non-cloudflare issuer of %s: %s", token.Issuer, jwt) } return nil } func (a *Access) ValidateRequest(ctx context.Context, r *http.Request) error { return a.Validate(ctx, r.Header.Get(accessJwtHeader)) } ================================================ FILE: validation/validation_test.go ================================================ package validation import ( "bytes" "fmt" "io" "testing" "context" "crypto/tls" "crypto/x509" "net" "net/http" "net/http/httptest" "net/url" "strings" "github.com/stretchr/testify/assert" ) func TestValidateHostname(t *testing.T) { var inputHostname string hostname, err := ValidateHostname(inputHostname) assert.Equal(t, err, nil) assert.Empty(t, hostname) inputHostname = "hello.example.com" hostname, err = ValidateHostname(inputHostname) assert.Nil(t, err) assert.Equal(t, "hello.example.com", hostname) inputHostname = "http://hello.example.com" hostname, err = ValidateHostname(inputHostname) assert.Nil(t, err) assert.Equal(t, "hello.example.com", hostname) inputHostname = "bücher.example.com" hostname, err = ValidateHostname(inputHostname) assert.Nil(t, err) assert.Equal(t, "xn--bcher-kva.example.com", hostname) inputHostname = "http://bücher.example.com" hostname, err = ValidateHostname(inputHostname) assert.Nil(t, err) assert.Equal(t, "xn--bcher-kva.example.com", hostname) inputHostname = "http%3A%2F%2Fhello.example.com" hostname, err = ValidateHostname(inputHostname) assert.Nil(t, err) assert.Equal(t, "hello.example.com", hostname) } func TestValidateUrl(t *testing.T) { type testCase struct { input string expectedOutput string } testCases := []testCase{ {"http://localhost", "http://localhost"}, {"http://localhost/", "http://localhost"}, {"http://localhost/api", "http://localhost"}, {"http://localhost/api/", "http://localhost"}, {"https://localhost", "https://localhost"}, {"https://localhost/", "https://localhost"}, {"https://localhost/api", "https://localhost"}, {"https://localhost/api/", "https://localhost"}, {"https://localhost:8080", "https://localhost:8080"}, {"https://localhost:8080/", "https://localhost:8080"}, {"https://localhost:8080/api", "https://localhost:8080"}, {"https://localhost:8080/api/", "https://localhost:8080"}, {"localhost", "http://localhost"}, {"localhost/", "http://localhost/"}, {"localhost/api", "http://localhost/api"}, {"localhost/api/", "http://localhost/api/"}, {"localhost:8080", "http://localhost:8080"}, {"localhost:8080/", "http://localhost:8080/"}, {"localhost:8080/api", "http://localhost:8080/api"}, {"localhost:8080/api/", "http://localhost:8080/api/"}, {"localhost:8080/api/?asdf", "http://localhost:8080/api/?asdf"}, {"http://127.0.0.1:8080", "http://127.0.0.1:8080"}, {"127.0.0.1:8080", "http://127.0.0.1:8080"}, {"127.0.0.1", "http://127.0.0.1"}, {"https://127.0.0.1:8080", "https://127.0.0.1:8080"}, {"[::1]:8080", "http://[::1]:8080"}, {"http://[::1]", "http://[::1]"}, {"http://[::1]:8080", "http://[::1]:8080"}, {"[::1]", "http://[::1]"}, {"https://example.com", "https://example.com"}, {"example.com", "http://example.com"}, {"http://hello.example.com", "http://hello.example.com"}, {"hello.example.com", "http://hello.example.com"}, {"hello.example.com:8080", "http://hello.example.com:8080"}, {"https://hello.example.com:8080", "https://hello.example.com:8080"}, {"https://bücher.example.com", "https://xn--bcher-kva.example.com"}, {"bücher.example.com", "http://xn--bcher-kva.example.com"}, {"https%3A%2F%2Fhello.example.com", "https://hello.example.com"}, {"https://alex:12345@hello.example.com:8080", "https://hello.example.com:8080"}, } for i, testCase := range testCases { validUrl, err := ValidateUrl(testCase.input) assert.NoError(t, err, "test case %v", i) assert.Equal(t, testCase.expectedOutput, validUrl.String(), "test case %v", i) } validUrl, err := ValidateUrl("") assert.Equal(t, fmt.Errorf("URL should not be empty"), err) assert.Empty(t, validUrl) validUrl, err = ValidateUrl("ftp://alex:12345@hello.example.com:8080/robot.txt") assert.Equal(t, "Currently Cloudflare Tunnel does not support ftp protocol.", err.Error()) assert.Empty(t, validUrl) } func TestNewAccessValidatorOk(t *testing.T) { ctx := context.Background() url := "test.cloudflareaccess.com" access, err := NewAccessValidator(ctx, url, url, "") assert.NoError(t, err) assert.NotNil(t, access) assert.Error(t, access.Validate(ctx, "")) assert.Error(t, access.Validate(ctx, "invalid")) req := httptest.NewRequest("GET", "https://test.cloudflareaccess.com", nil) req.Header.Set(accessJwtHeader, "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c") assert.Error(t, access.ValidateRequest(ctx, req)) } func TestNewAccessValidatorErr(t *testing.T) { ctx := context.Background() urls := []string{ "", "ftp://test.cloudflareaccess.com", "wss://cloudflarenone.com", } for _, url := range urls { access, err := NewAccessValidator(ctx, url, url, "") assert.Error(t, err, url) assert.Nil(t, access) } } type testRoundTripper func(req *http.Request) (*http.Response, error) func (f testRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { return f(req) } func emptyResponse(statusCode int) *http.Response { return &http.Response{ StatusCode: statusCode, Body: io.NopCloser(bytes.NewReader(nil)), Header: make(http.Header), } } func createMockServerAndClient(handler http.Handler) (*httptest.Server, *http.Client, error) { client := http.DefaultClient server := httptest.NewServer(handler) client.Transport = &http.Transport{ Proxy: func(req *http.Request) (*url.URL, error) { return url.Parse(server.URL) }, } return server, client, nil } func createSecureMockServerAndClient(handler http.Handler) (*httptest.Server, *http.Client, error) { client := http.DefaultClient server := httptest.NewTLSServer(handler) cert, err := x509.ParseCertificate(server.TLS.Certificates[0].Certificate[0]) if err != nil { server.Close() return nil, nil, err } certpool := x509.NewCertPool() certpool.AddCert(cert) client.Transport = &http.Transport{ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { return net.Dial("tcp", server.URL[strings.LastIndex(server.URL, "/")+1:]) }, TLSClientConfig: &tls.Config{ RootCAs: certpool, }, } return server, client, nil } func FuzzNewAccessValidator(f *testing.F) { f.Fuzz(func(t *testing.T, domain string, issuer string, applicationAUD string) { ctx := context.Background() _, _ = NewAccessValidator(ctx, domain, issuer, applicationAUD) }) } ================================================ FILE: vendor/github.com/BurntSushi/toml/.gitignore ================================================ /toml.test /toml-test ================================================ FILE: vendor/github.com/BurntSushi/toml/COPYING ================================================ The MIT License (MIT) Copyright (c) 2013 TOML authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: vendor/github.com/BurntSushi/toml/README.md ================================================ TOML stands for Tom's Obvious, Minimal Language. This Go package provides a reflection interface similar to Go's standard library `json` and `xml` packages. Compatible with TOML version [v1.0.0](https://toml.io/en/v1.0.0). Documentation: https://godocs.io/github.com/BurntSushi/toml See the [releases page](https://github.com/BurntSushi/toml/releases) for a changelog; this information is also in the git tag annotations (e.g. `git show v0.4.0`). This library requires Go 1.13 or newer; add it to your go.mod with: % go get github.com/BurntSushi/toml@latest It also comes with a TOML validator CLI tool: % go install github.com/BurntSushi/toml/cmd/tomlv@latest % tomlv some-toml-file.toml ### Examples For the simplest example, consider some TOML file as just a list of keys and values: ```toml Age = 25 Cats = [ "Cauchy", "Plato" ] Pi = 3.14 Perfection = [ 6, 28, 496, 8128 ] DOB = 1987-07-05T05:45:00Z ``` Which can be decoded with: ```go type Config struct { Age int Cats []string Pi float64 Perfection []int DOB time.Time } var conf Config _, err := toml.Decode(tomlData, &conf) ``` You can also use struct tags if your struct field name doesn't map to a TOML key value directly: ```toml some_key_NAME = "wat" ``` ```go type TOML struct { ObscureKey string `toml:"some_key_NAME"` } ``` Beware that like other decoders **only exported fields** are considered when encoding and decoding; private fields are silently ignored. ### Using the `Marshaler` and `encoding.TextUnmarshaler` interfaces Here's an example that automatically parses values in a `mail.Address`: ```toml contacts = [ "Donald Duck ", "Scrooge McDuck ", ] ``` Can be decoded with: ```go // Create address type which satisfies the encoding.TextUnmarshaler interface. type address struct { *mail.Address } func (a *address) UnmarshalText(text []byte) error { var err error a.Address, err = mail.ParseAddress(string(text)) return err } // Decode it. func decode() { blob := ` contacts = [ "Donald Duck ", "Scrooge McDuck ", ] ` var contacts struct { Contacts []address } _, err := toml.Decode(blob, &contacts) if err != nil { log.Fatal(err) } for _, c := range contacts.Contacts { fmt.Printf("%#v\n", c.Address) } // Output: // &mail.Address{Name:"Donald Duck", Address:"donald@duckburg.com"} // &mail.Address{Name:"Scrooge McDuck", Address:"scrooge@duckburg.com"} } ``` To target TOML specifically you can implement `UnmarshalTOML` TOML interface in a similar way. ### More complex usage See the [`_example/`](/_example) directory for a more complex example. ================================================ FILE: vendor/github.com/BurntSushi/toml/decode.go ================================================ package toml import ( "bytes" "encoding" "encoding/json" "fmt" "io" "io/ioutil" "math" "os" "reflect" "strconv" "strings" "time" ) // Unmarshaler is the interface implemented by objects that can unmarshal a // TOML description of themselves. type Unmarshaler interface { UnmarshalTOML(interface{}) error } // Unmarshal decodes the contents of `data` in TOML format into a pointer `v`. func Unmarshal(data []byte, v interface{}) error { _, err := NewDecoder(bytes.NewReader(data)).Decode(v) return err } // Decode the TOML data in to the pointer v. // // See the documentation on Decoder for a description of the decoding process. func Decode(data string, v interface{}) (MetaData, error) { return NewDecoder(strings.NewReader(data)).Decode(v) } // DecodeFile is just like Decode, except it will automatically read the // contents of the file at path and decode it for you. func DecodeFile(path string, v interface{}) (MetaData, error) { fp, err := os.Open(path) if err != nil { return MetaData{}, err } defer fp.Close() return NewDecoder(fp).Decode(v) } // Primitive is a TOML value that hasn't been decoded into a Go value. // // This type can be used for any value, which will cause decoding to be delayed. // You can use the PrimitiveDecode() function to "manually" decode these values. // // NOTE: The underlying representation of a `Primitive` value is subject to // change. Do not rely on it. // // NOTE: Primitive values are still parsed, so using them will only avoid the // overhead of reflection. They can be useful when you don't know the exact type // of TOML data until runtime. type Primitive struct { undecoded interface{} context Key } // The significand precision for float32 and float64 is 24 and 53 bits; this is // the range a natural number can be stored in a float without loss of data. const ( maxSafeFloat32Int = 16777215 // 2^24-1 maxSafeFloat64Int = int64(9007199254740991) // 2^53-1 ) // Decoder decodes TOML data. // // TOML tables correspond to Go structs or maps (dealer's choice – they can be // used interchangeably). // // TOML table arrays correspond to either a slice of structs or a slice of maps. // // TOML datetimes correspond to Go time.Time values. Local datetimes are parsed // in the local timezone. // // time.Duration types are treated as nanoseconds if the TOML value is an // integer, or they're parsed with time.ParseDuration() if they're strings. // // All other TOML types (float, string, int, bool and array) correspond to the // obvious Go types. // // An exception to the above rules is if a type implements the TextUnmarshaler // interface, in which case any primitive TOML value (floats, strings, integers, // booleans, datetimes) will be converted to a []byte and given to the value's // UnmarshalText method. See the Unmarshaler example for a demonstration with // email addresses. // // Key mapping // // TOML keys can map to either keys in a Go map or field names in a Go struct. // The special `toml` struct tag can be used to map TOML keys to struct fields // that don't match the key name exactly (see the example). A case insensitive // match to struct names will be tried if an exact match can't be found. // // The mapping between TOML values and Go values is loose. That is, there may // exist TOML values that cannot be placed into your representation, and there // may be parts of your representation that do not correspond to TOML values. // This loose mapping can be made stricter by using the IsDefined and/or // Undecoded methods on the MetaData returned. // // This decoder does not handle cyclic types. Decode will not terminate if a // cyclic type is passed. type Decoder struct { r io.Reader } // NewDecoder creates a new Decoder. func NewDecoder(r io.Reader) *Decoder { return &Decoder{r: r} } var ( unmarshalToml = reflect.TypeOf((*Unmarshaler)(nil)).Elem() unmarshalText = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem() primitiveType = reflect.TypeOf((*Primitive)(nil)).Elem() ) // Decode TOML data in to the pointer `v`. func (dec *Decoder) Decode(v interface{}) (MetaData, error) { rv := reflect.ValueOf(v) if rv.Kind() != reflect.Ptr { s := "%q" if reflect.TypeOf(v) == nil { s = "%v" } return MetaData{}, fmt.Errorf("toml: cannot decode to non-pointer "+s, reflect.TypeOf(v)) } if rv.IsNil() { return MetaData{}, fmt.Errorf("toml: cannot decode to nil value of %q", reflect.TypeOf(v)) } // Check if this is a supported type: struct, map, interface{}, or something // that implements UnmarshalTOML or UnmarshalText. rv = indirect(rv) rt := rv.Type() if rv.Kind() != reflect.Struct && rv.Kind() != reflect.Map && !(rv.Kind() == reflect.Interface && rv.NumMethod() == 0) && !rt.Implements(unmarshalToml) && !rt.Implements(unmarshalText) { return MetaData{}, fmt.Errorf("toml: cannot decode to type %s", rt) } // TODO: parser should read from io.Reader? Or at the very least, make it // read from []byte rather than string data, err := ioutil.ReadAll(dec.r) if err != nil { return MetaData{}, err } p, err := parse(string(data)) if err != nil { return MetaData{}, err } md := MetaData{ mapping: p.mapping, keyInfo: p.keyInfo, keys: p.ordered, decoded: make(map[string]struct{}, len(p.ordered)), context: nil, data: data, } return md, md.unify(p.mapping, rv) } // PrimitiveDecode is just like the other `Decode*` functions, except it // decodes a TOML value that has already been parsed. Valid primitive values // can *only* be obtained from values filled by the decoder functions, // including this method. (i.e., `v` may contain more `Primitive` // values.) // // Meta data for primitive values is included in the meta data returned by // the `Decode*` functions with one exception: keys returned by the Undecoded // method will only reflect keys that were decoded. Namely, any keys hidden // behind a Primitive will be considered undecoded. Executing this method will // update the undecoded keys in the meta data. (See the example.) func (md *MetaData) PrimitiveDecode(primValue Primitive, v interface{}) error { md.context = primValue.context defer func() { md.context = nil }() return md.unify(primValue.undecoded, rvalue(v)) } // unify performs a sort of type unification based on the structure of `rv`, // which is the client representation. // // Any type mismatch produces an error. Finding a type that we don't know // how to handle produces an unsupported type error. func (md *MetaData) unify(data interface{}, rv reflect.Value) error { // Special case. Look for a `Primitive` value. // TODO: #76 would make this superfluous after implemented. if rv.Type() == primitiveType { // Save the undecoded data and the key context into the primitive // value. context := make(Key, len(md.context)) copy(context, md.context) rv.Set(reflect.ValueOf(Primitive{ undecoded: data, context: context, })) return nil } rvi := rv.Interface() if v, ok := rvi.(Unmarshaler); ok { return v.UnmarshalTOML(data) } if v, ok := rvi.(encoding.TextUnmarshaler); ok { return md.unifyText(data, v) } // TODO: // The behavior here is incorrect whenever a Go type satisfies the // encoding.TextUnmarshaler interface but also corresponds to a TOML hash or // array. In particular, the unmarshaler should only be applied to primitive // TOML values. But at this point, it will be applied to all kinds of values // and produce an incorrect error whenever those values are hashes or arrays // (including arrays of tables). k := rv.Kind() if k >= reflect.Int && k <= reflect.Uint64 { return md.unifyInt(data, rv) } switch k { case reflect.Ptr: elem := reflect.New(rv.Type().Elem()) err := md.unify(data, reflect.Indirect(elem)) if err != nil { return err } rv.Set(elem) return nil case reflect.Struct: return md.unifyStruct(data, rv) case reflect.Map: return md.unifyMap(data, rv) case reflect.Array: return md.unifyArray(data, rv) case reflect.Slice: return md.unifySlice(data, rv) case reflect.String: return md.unifyString(data, rv) case reflect.Bool: return md.unifyBool(data, rv) case reflect.Interface: if rv.NumMethod() > 0 { // Only support empty interfaces are supported. return md.e("unsupported type %s", rv.Type()) } return md.unifyAnything(data, rv) case reflect.Float32, reflect.Float64: return md.unifyFloat64(data, rv) } return md.e("unsupported type %s", rv.Kind()) } func (md *MetaData) unifyStruct(mapping interface{}, rv reflect.Value) error { tmap, ok := mapping.(map[string]interface{}) if !ok { if mapping == nil { return nil } return md.e("type mismatch for %s: expected table but found %T", rv.Type().String(), mapping) } for key, datum := range tmap { var f *field fields := cachedTypeFields(rv.Type()) for i := range fields { ff := &fields[i] if ff.name == key { f = ff break } if f == nil && strings.EqualFold(ff.name, key) { f = ff } } if f != nil { subv := rv for _, i := range f.index { subv = indirect(subv.Field(i)) } if isUnifiable(subv) { md.decoded[md.context.add(key).String()] = struct{}{} md.context = append(md.context, key) err := md.unify(datum, subv) if err != nil { return err } md.context = md.context[0 : len(md.context)-1] } else if f.name != "" { return md.e("cannot write unexported field %s.%s", rv.Type().String(), f.name) } } } return nil } func (md *MetaData) unifyMap(mapping interface{}, rv reflect.Value) error { keyType := rv.Type().Key().Kind() if keyType != reflect.String && keyType != reflect.Interface { return fmt.Errorf("toml: cannot decode to a map with non-string key type (%s in %q)", keyType, rv.Type()) } tmap, ok := mapping.(map[string]interface{}) if !ok { if tmap == nil { return nil } return md.badtype("map", mapping) } if rv.IsNil() { rv.Set(reflect.MakeMap(rv.Type())) } for k, v := range tmap { md.decoded[md.context.add(k).String()] = struct{}{} md.context = append(md.context, k) rvval := reflect.Indirect(reflect.New(rv.Type().Elem())) err := md.unify(v, indirect(rvval)) if err != nil { return err } md.context = md.context[0 : len(md.context)-1] rvkey := indirect(reflect.New(rv.Type().Key())) switch keyType { case reflect.Interface: rvkey.Set(reflect.ValueOf(k)) case reflect.String: rvkey.SetString(k) } rv.SetMapIndex(rvkey, rvval) } return nil } func (md *MetaData) unifyArray(data interface{}, rv reflect.Value) error { datav := reflect.ValueOf(data) if datav.Kind() != reflect.Slice { if !datav.IsValid() { return nil } return md.badtype("slice", data) } if l := datav.Len(); l != rv.Len() { return md.e("expected array length %d; got TOML array of length %d", rv.Len(), l) } return md.unifySliceArray(datav, rv) } func (md *MetaData) unifySlice(data interface{}, rv reflect.Value) error { datav := reflect.ValueOf(data) if datav.Kind() != reflect.Slice { if !datav.IsValid() { return nil } return md.badtype("slice", data) } n := datav.Len() if rv.IsNil() || rv.Cap() < n { rv.Set(reflect.MakeSlice(rv.Type(), n, n)) } rv.SetLen(n) return md.unifySliceArray(datav, rv) } func (md *MetaData) unifySliceArray(data, rv reflect.Value) error { l := data.Len() for i := 0; i < l; i++ { err := md.unify(data.Index(i).Interface(), indirect(rv.Index(i))) if err != nil { return err } } return nil } func (md *MetaData) unifyString(data interface{}, rv reflect.Value) error { _, ok := rv.Interface().(json.Number) if ok { if i, ok := data.(int64); ok { rv.SetString(strconv.FormatInt(i, 10)) } else if f, ok := data.(float64); ok { rv.SetString(strconv.FormatFloat(f, 'f', -1, 64)) } else { return md.badtype("string", data) } return nil } if s, ok := data.(string); ok { rv.SetString(s) return nil } return md.badtype("string", data) } func (md *MetaData) unifyFloat64(data interface{}, rv reflect.Value) error { rvk := rv.Kind() if num, ok := data.(float64); ok { switch rvk { case reflect.Float32: if num < -math.MaxFloat32 || num > math.MaxFloat32 { return md.parseErr(errParseRange{i: num, size: rvk.String()}) } fallthrough case reflect.Float64: rv.SetFloat(num) default: panic("bug") } return nil } if num, ok := data.(int64); ok { if (rvk == reflect.Float32 && (num < -maxSafeFloat32Int || num > maxSafeFloat32Int)) || (rvk == reflect.Float64 && (num < -maxSafeFloat64Int || num > maxSafeFloat64Int)) { return md.parseErr(errParseRange{i: num, size: rvk.String()}) } rv.SetFloat(float64(num)) return nil } return md.badtype("float", data) } func (md *MetaData) unifyInt(data interface{}, rv reflect.Value) error { _, ok := rv.Interface().(time.Duration) if ok { // Parse as string duration, and fall back to regular integer parsing // (as nanosecond) if this is not a string. if s, ok := data.(string); ok { dur, err := time.ParseDuration(s) if err != nil { return md.parseErr(errParseDuration{s}) } rv.SetInt(int64(dur)) return nil } } num, ok := data.(int64) if !ok { return md.badtype("integer", data) } rvk := rv.Kind() switch { case rvk >= reflect.Int && rvk <= reflect.Int64: if (rvk == reflect.Int8 && (num < math.MinInt8 || num > math.MaxInt8)) || (rvk == reflect.Int16 && (num < math.MinInt16 || num > math.MaxInt16)) || (rvk == reflect.Int32 && (num < math.MinInt32 || num > math.MaxInt32)) { return md.parseErr(errParseRange{i: num, size: rvk.String()}) } rv.SetInt(num) case rvk >= reflect.Uint && rvk <= reflect.Uint64: unum := uint64(num) if rvk == reflect.Uint8 && (num < 0 || unum > math.MaxUint8) || rvk == reflect.Uint16 && (num < 0 || unum > math.MaxUint16) || rvk == reflect.Uint32 && (num < 0 || unum > math.MaxUint32) { return md.parseErr(errParseRange{i: num, size: rvk.String()}) } rv.SetUint(unum) default: panic("unreachable") } return nil } func (md *MetaData) unifyBool(data interface{}, rv reflect.Value) error { if b, ok := data.(bool); ok { rv.SetBool(b) return nil } return md.badtype("boolean", data) } func (md *MetaData) unifyAnything(data interface{}, rv reflect.Value) error { rv.Set(reflect.ValueOf(data)) return nil } func (md *MetaData) unifyText(data interface{}, v encoding.TextUnmarshaler) error { var s string switch sdata := data.(type) { case Marshaler: text, err := sdata.MarshalTOML() if err != nil { return err } s = string(text) case encoding.TextMarshaler: text, err := sdata.MarshalText() if err != nil { return err } s = string(text) case fmt.Stringer: s = sdata.String() case string: s = sdata case bool: s = fmt.Sprintf("%v", sdata) case int64: s = fmt.Sprintf("%d", sdata) case float64: s = fmt.Sprintf("%f", sdata) default: return md.badtype("primitive (string-like)", data) } if err := v.UnmarshalText([]byte(s)); err != nil { return err } return nil } func (md *MetaData) badtype(dst string, data interface{}) error { return md.e("incompatible types: TOML value has type %T; destination has type %s", data, dst) } func (md *MetaData) parseErr(err error) error { k := md.context.String() return ParseError{ LastKey: k, Position: md.keyInfo[k].pos, Line: md.keyInfo[k].pos.Line, err: err, input: string(md.data), } } func (md *MetaData) e(format string, args ...interface{}) error { f := "toml: " if len(md.context) > 0 { f = fmt.Sprintf("toml: (last key %q): ", md.context) p := md.keyInfo[md.context.String()].pos if p.Line > 0 { f = fmt.Sprintf("toml: line %d (last key %q): ", p.Line, md.context) } } return fmt.Errorf(f+format, args...) } // rvalue returns a reflect.Value of `v`. All pointers are resolved. func rvalue(v interface{}) reflect.Value { return indirect(reflect.ValueOf(v)) } // indirect returns the value pointed to by a pointer. // // Pointers are followed until the value is not a pointer. New values are // allocated for each nil pointer. // // An exception to this rule is if the value satisfies an interface of interest // to us (like encoding.TextUnmarshaler). func indirect(v reflect.Value) reflect.Value { if v.Kind() != reflect.Ptr { if v.CanSet() { pv := v.Addr() pvi := pv.Interface() if _, ok := pvi.(encoding.TextUnmarshaler); ok { return pv } if _, ok := pvi.(Unmarshaler); ok { return pv } } return v } if v.IsNil() { v.Set(reflect.New(v.Type().Elem())) } return indirect(reflect.Indirect(v)) } func isUnifiable(rv reflect.Value) bool { if rv.CanSet() { return true } rvi := rv.Interface() if _, ok := rvi.(encoding.TextUnmarshaler); ok { return true } if _, ok := rvi.(Unmarshaler); ok { return true } return false } ================================================ FILE: vendor/github.com/BurntSushi/toml/decode_go116.go ================================================ //go:build go1.16 // +build go1.16 package toml import ( "io/fs" ) // DecodeFS is just like Decode, except it will automatically read the contents // of the file at `path` from a fs.FS instance. func DecodeFS(fsys fs.FS, path string, v interface{}) (MetaData, error) { fp, err := fsys.Open(path) if err != nil { return MetaData{}, err } defer fp.Close() return NewDecoder(fp).Decode(v) } ================================================ FILE: vendor/github.com/BurntSushi/toml/deprecated.go ================================================ package toml import ( "encoding" "io" ) // Deprecated: use encoding.TextMarshaler type TextMarshaler encoding.TextMarshaler // Deprecated: use encoding.TextUnmarshaler type TextUnmarshaler encoding.TextUnmarshaler // Deprecated: use MetaData.PrimitiveDecode. func PrimitiveDecode(primValue Primitive, v interface{}) error { md := MetaData{decoded: make(map[string]struct{})} return md.unify(primValue.undecoded, rvalue(v)) } // Deprecated: use NewDecoder(reader).Decode(&value). func DecodeReader(r io.Reader, v interface{}) (MetaData, error) { return NewDecoder(r).Decode(v) } ================================================ FILE: vendor/github.com/BurntSushi/toml/doc.go ================================================ /* Package toml implements decoding and encoding of TOML files. This package supports TOML v1.0.0, as listed on https://toml.io There is also support for delaying decoding with the Primitive type, and querying the set of keys in a TOML document with the MetaData type. The github.com/BurntSushi/toml/cmd/tomlv package implements a TOML validator, and can be used to verify if TOML document is valid. It can also be used to print the type of each key. */ package toml ================================================ FILE: vendor/github.com/BurntSushi/toml/encode.go ================================================ package toml import ( "bufio" "encoding" "encoding/json" "errors" "fmt" "io" "math" "reflect" "sort" "strconv" "strings" "time" "github.com/BurntSushi/toml/internal" ) type tomlEncodeError struct{ error } var ( errArrayNilElement = errors.New("toml: cannot encode array with nil element") errNonString = errors.New("toml: cannot encode a map with non-string key type") errNoKey = errors.New("toml: top-level values must be Go maps or structs") errAnything = errors.New("") // used in testing ) var dblQuotedReplacer = strings.NewReplacer( "\"", "\\\"", "\\", "\\\\", "\x00", `\u0000`, "\x01", `\u0001`, "\x02", `\u0002`, "\x03", `\u0003`, "\x04", `\u0004`, "\x05", `\u0005`, "\x06", `\u0006`, "\x07", `\u0007`, "\b", `\b`, "\t", `\t`, "\n", `\n`, "\x0b", `\u000b`, "\f", `\f`, "\r", `\r`, "\x0e", `\u000e`, "\x0f", `\u000f`, "\x10", `\u0010`, "\x11", `\u0011`, "\x12", `\u0012`, "\x13", `\u0013`, "\x14", `\u0014`, "\x15", `\u0015`, "\x16", `\u0016`, "\x17", `\u0017`, "\x18", `\u0018`, "\x19", `\u0019`, "\x1a", `\u001a`, "\x1b", `\u001b`, "\x1c", `\u001c`, "\x1d", `\u001d`, "\x1e", `\u001e`, "\x1f", `\u001f`, "\x7f", `\u007f`, ) var ( marshalToml = reflect.TypeOf((*Marshaler)(nil)).Elem() marshalText = reflect.TypeOf((*encoding.TextMarshaler)(nil)).Elem() timeType = reflect.TypeOf((*time.Time)(nil)).Elem() ) // Marshaler is the interface implemented by types that can marshal themselves // into valid TOML. type Marshaler interface { MarshalTOML() ([]byte, error) } // Encoder encodes a Go to a TOML document. // // The mapping between Go values and TOML values should be precisely the same as // for the Decode* functions. // // time.Time is encoded as a RFC 3339 string, and time.Duration as its string // representation. // // The toml.Marshaler and encoder.TextMarshaler interfaces are supported to // encoding the value as custom TOML. // // If you want to write arbitrary binary data then you will need to use // something like base64 since TOML does not have any binary types. // // When encoding TOML hashes (Go maps or structs), keys without any sub-hashes // are encoded first. // // Go maps will be sorted alphabetically by key for deterministic output. // // The toml struct tag can be used to provide the key name; if omitted the // struct field name will be used. If the "omitempty" option is present the // following value will be skipped: // // - arrays, slices, maps, and string with len of 0 // - struct with all zero values // - bool false // // If omitzero is given all int and float types with a value of 0 will be // skipped. // // Encoding Go values without a corresponding TOML representation will return an // error. Examples of this includes maps with non-string keys, slices with nil // elements, embedded non-struct types, and nested slices containing maps or // structs. (e.g. [][]map[string]string is not allowed but []map[string]string // is okay, as is []map[string][]string). // // NOTE: only exported keys are encoded due to the use of reflection. Unexported // keys are silently discarded. type Encoder struct { // String to use for a single indentation level; default is two spaces. Indent string w *bufio.Writer hasWritten bool // written any output to w yet? } // NewEncoder create a new Encoder. func NewEncoder(w io.Writer) *Encoder { return &Encoder{ w: bufio.NewWriter(w), Indent: " ", } } // Encode writes a TOML representation of the Go value to the Encoder's writer. // // An error is returned if the value given cannot be encoded to a valid TOML // document. func (enc *Encoder) Encode(v interface{}) error { rv := eindirect(reflect.ValueOf(v)) if err := enc.safeEncode(Key([]string{}), rv); err != nil { return err } return enc.w.Flush() } func (enc *Encoder) safeEncode(key Key, rv reflect.Value) (err error) { defer func() { if r := recover(); r != nil { if terr, ok := r.(tomlEncodeError); ok { err = terr.error return } panic(r) } }() enc.encode(key, rv) return nil } func (enc *Encoder) encode(key Key, rv reflect.Value) { // If we can marshal the type to text, then we use that. This prevents the // encoder for handling these types as generic structs (or whatever the // underlying type of a TextMarshaler is). switch { case isMarshaler(rv): enc.writeKeyValue(key, rv, false) return case rv.Type() == primitiveType: // TODO: #76 would make this superfluous after implemented. enc.encode(key, reflect.ValueOf(rv.Interface().(Primitive).undecoded)) return } k := rv.Kind() switch k { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64, reflect.String, reflect.Bool: enc.writeKeyValue(key, rv, false) case reflect.Array, reflect.Slice: if typeEqual(tomlArrayHash, tomlTypeOfGo(rv)) { enc.eArrayOfTables(key, rv) } else { enc.writeKeyValue(key, rv, false) } case reflect.Interface: if rv.IsNil() { return } enc.encode(key, rv.Elem()) case reflect.Map: if rv.IsNil() { return } enc.eTable(key, rv) case reflect.Ptr: if rv.IsNil() { return } enc.encode(key, rv.Elem()) case reflect.Struct: enc.eTable(key, rv) default: encPanic(fmt.Errorf("unsupported type for key '%s': %s", key, k)) } } // eElement encodes any value that can be an array element. func (enc *Encoder) eElement(rv reflect.Value) { switch v := rv.Interface().(type) { case time.Time: // Using TextMarshaler adds extra quotes, which we don't want. format := time.RFC3339Nano switch v.Location() { case internal.LocalDatetime: format = "2006-01-02T15:04:05.999999999" case internal.LocalDate: format = "2006-01-02" case internal.LocalTime: format = "15:04:05.999999999" } switch v.Location() { default: enc.wf(v.Format(format)) case internal.LocalDatetime, internal.LocalDate, internal.LocalTime: enc.wf(v.In(time.UTC).Format(format)) } return case Marshaler: s, err := v.MarshalTOML() if err != nil { encPanic(err) } if s == nil { encPanic(errors.New("MarshalTOML returned nil and no error")) } enc.w.Write(s) return case encoding.TextMarshaler: s, err := v.MarshalText() if err != nil { encPanic(err) } if s == nil { encPanic(errors.New("MarshalText returned nil and no error")) } enc.writeQuoted(string(s)) return case time.Duration: enc.writeQuoted(v.String()) return case json.Number: n, _ := rv.Interface().(json.Number) if n == "" { /// Useful zero value. enc.w.WriteByte('0') return } else if v, err := n.Int64(); err == nil { enc.eElement(reflect.ValueOf(v)) return } else if v, err := n.Float64(); err == nil { enc.eElement(reflect.ValueOf(v)) return } encPanic(errors.New(fmt.Sprintf("Unable to convert \"%s\" to neither int64 nor float64", n))) } switch rv.Kind() { case reflect.Ptr: enc.eElement(rv.Elem()) return case reflect.String: enc.writeQuoted(rv.String()) case reflect.Bool: enc.wf(strconv.FormatBool(rv.Bool())) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: enc.wf(strconv.FormatInt(rv.Int(), 10)) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: enc.wf(strconv.FormatUint(rv.Uint(), 10)) case reflect.Float32: f := rv.Float() if math.IsNaN(f) { enc.wf("nan") } else if math.IsInf(f, 0) { enc.wf("%cinf", map[bool]byte{true: '-', false: '+'}[math.Signbit(f)]) } else { enc.wf(floatAddDecimal(strconv.FormatFloat(f, 'f', -1, 32))) } case reflect.Float64: f := rv.Float() if math.IsNaN(f) { enc.wf("nan") } else if math.IsInf(f, 0) { enc.wf("%cinf", map[bool]byte{true: '-', false: '+'}[math.Signbit(f)]) } else { enc.wf(floatAddDecimal(strconv.FormatFloat(f, 'f', -1, 64))) } case reflect.Array, reflect.Slice: enc.eArrayOrSliceElement(rv) case reflect.Struct: enc.eStruct(nil, rv, true) case reflect.Map: enc.eMap(nil, rv, true) case reflect.Interface: enc.eElement(rv.Elem()) default: encPanic(fmt.Errorf("unexpected type: %T", rv.Interface())) } } // By the TOML spec, all floats must have a decimal with at least one number on // either side. func floatAddDecimal(fstr string) string { if !strings.Contains(fstr, ".") { return fstr + ".0" } return fstr } func (enc *Encoder) writeQuoted(s string) { enc.wf("\"%s\"", dblQuotedReplacer.Replace(s)) } func (enc *Encoder) eArrayOrSliceElement(rv reflect.Value) { length := rv.Len() enc.wf("[") for i := 0; i < length; i++ { elem := eindirect(rv.Index(i)) enc.eElement(elem) if i != length-1 { enc.wf(", ") } } enc.wf("]") } func (enc *Encoder) eArrayOfTables(key Key, rv reflect.Value) { if len(key) == 0 { encPanic(errNoKey) } for i := 0; i < rv.Len(); i++ { trv := eindirect(rv.Index(i)) if isNil(trv) { continue } enc.newline() enc.wf("%s[[%s]]", enc.indentStr(key), key) enc.newline() enc.eMapOrStruct(key, trv, false) } } func (enc *Encoder) eTable(key Key, rv reflect.Value) { if len(key) == 1 { // Output an extra newline between top-level tables. // (The newline isn't written if nothing else has been written though.) enc.newline() } if len(key) > 0 { enc.wf("%s[%s]", enc.indentStr(key), key) enc.newline() } enc.eMapOrStruct(key, rv, false) } func (enc *Encoder) eMapOrStruct(key Key, rv reflect.Value, inline bool) { switch rv.Kind() { case reflect.Map: enc.eMap(key, rv, inline) case reflect.Struct: enc.eStruct(key, rv, inline) default: // Should never happen? panic("eTable: unhandled reflect.Value Kind: " + rv.Kind().String()) } } func (enc *Encoder) eMap(key Key, rv reflect.Value, inline bool) { rt := rv.Type() if rt.Key().Kind() != reflect.String { encPanic(errNonString) } // Sort keys so that we have deterministic output. And write keys directly // underneath this key first, before writing sub-structs or sub-maps. var mapKeysDirect, mapKeysSub []string for _, mapKey := range rv.MapKeys() { k := mapKey.String() if typeIsTable(tomlTypeOfGo(eindirect(rv.MapIndex(mapKey)))) { mapKeysSub = append(mapKeysSub, k) } else { mapKeysDirect = append(mapKeysDirect, k) } } var writeMapKeys = func(mapKeys []string, trailC bool) { sort.Strings(mapKeys) for i, mapKey := range mapKeys { val := eindirect(rv.MapIndex(reflect.ValueOf(mapKey))) if isNil(val) { continue } if inline { enc.writeKeyValue(Key{mapKey}, val, true) if trailC || i != len(mapKeys)-1 { enc.wf(", ") } } else { enc.encode(key.add(mapKey), val) } } } if inline { enc.wf("{") } writeMapKeys(mapKeysDirect, len(mapKeysSub) > 0) writeMapKeys(mapKeysSub, false) if inline { enc.wf("}") } } const is32Bit = (32 << (^uint(0) >> 63)) == 32 func pointerTo(t reflect.Type) reflect.Type { if t.Kind() == reflect.Ptr { return pointerTo(t.Elem()) } return t } func (enc *Encoder) eStruct(key Key, rv reflect.Value, inline bool) { // Write keys for fields directly under this key first, because if we write // a field that creates a new table then all keys under it will be in that // table (not the one we're writing here). // // Fields is a [][]int: for fieldsDirect this always has one entry (the // struct index). For fieldsSub it contains two entries: the parent field // index from tv, and the field indexes for the fields of the sub. var ( rt = rv.Type() fieldsDirect, fieldsSub [][]int addFields func(rt reflect.Type, rv reflect.Value, start []int) ) addFields = func(rt reflect.Type, rv reflect.Value, start []int) { for i := 0; i < rt.NumField(); i++ { f := rt.Field(i) isEmbed := f.Anonymous && pointerTo(f.Type).Kind() == reflect.Struct if f.PkgPath != "" && !isEmbed { /// Skip unexported fields. continue } opts := getOptions(f.Tag) if opts.skip { continue } frv := eindirect(rv.Field(i)) // Treat anonymous struct fields with tag names as though they are // not anonymous, like encoding/json does. // // Non-struct anonymous fields use the normal encoding logic. if isEmbed { if getOptions(f.Tag).name == "" && frv.Kind() == reflect.Struct { addFields(frv.Type(), frv, append(start, f.Index...)) continue } } if typeIsTable(tomlTypeOfGo(frv)) { fieldsSub = append(fieldsSub, append(start, f.Index...)) } else { // Copy so it works correct on 32bit archs; not clear why this // is needed. See #314, and https://www.reddit.com/r/golang/comments/pnx8v4 // This also works fine on 64bit, but 32bit archs are somewhat // rare and this is a wee bit faster. if is32Bit { copyStart := make([]int, len(start)) copy(copyStart, start) fieldsDirect = append(fieldsDirect, append(copyStart, f.Index...)) } else { fieldsDirect = append(fieldsDirect, append(start, f.Index...)) } } } } addFields(rt, rv, nil) writeFields := func(fields [][]int) { for _, fieldIndex := range fields { fieldType := rt.FieldByIndex(fieldIndex) fieldVal := eindirect(rv.FieldByIndex(fieldIndex)) if isNil(fieldVal) { /// Don't write anything for nil fields. continue } opts := getOptions(fieldType.Tag) if opts.skip { continue } keyName := fieldType.Name if opts.name != "" { keyName = opts.name } if opts.omitempty && isEmpty(fieldVal) { continue } if opts.omitzero && isZero(fieldVal) { continue } if inline { enc.writeKeyValue(Key{keyName}, fieldVal, true) if fieldIndex[0] != len(fields)-1 { enc.wf(", ") } } else { enc.encode(key.add(keyName), fieldVal) } } } if inline { enc.wf("{") } writeFields(fieldsDirect) writeFields(fieldsSub) if inline { enc.wf("}") } } // tomlTypeOfGo returns the TOML type name of the Go value's type. // // It is used to determine whether the types of array elements are mixed (which // is forbidden). If the Go value is nil, then it is illegal for it to be an // array element, and valueIsNil is returned as true. // // The type may be `nil`, which means no concrete TOML type could be found. func tomlTypeOfGo(rv reflect.Value) tomlType { if isNil(rv) || !rv.IsValid() { return nil } if rv.Kind() == reflect.Struct { if rv.Type() == timeType { return tomlDatetime } if isMarshaler(rv) { return tomlString } return tomlHash } if isMarshaler(rv) { return tomlString } switch rv.Kind() { case reflect.Bool: return tomlBool case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return tomlInteger case reflect.Float32, reflect.Float64: return tomlFloat case reflect.Array, reflect.Slice: if isTableArray(rv) { return tomlArrayHash } return tomlArray case reflect.Ptr, reflect.Interface: return tomlTypeOfGo(rv.Elem()) case reflect.String: return tomlString case reflect.Map: return tomlHash default: encPanic(errors.New("unsupported type: " + rv.Kind().String())) panic("unreachable") } } func isMarshaler(rv reflect.Value) bool { return rv.Type().Implements(marshalText) || rv.Type().Implements(marshalToml) } // isTableArray reports if all entries in the array or slice are a table. func isTableArray(arr reflect.Value) bool { if isNil(arr) || !arr.IsValid() || arr.Len() == 0 { return false } ret := true for i := 0; i < arr.Len(); i++ { tt := tomlTypeOfGo(eindirect(arr.Index(i))) // Don't allow nil. if tt == nil { encPanic(errArrayNilElement) } if ret && !typeEqual(tomlHash, tt) { ret = false } } return ret } type tagOptions struct { skip bool // "-" name string omitempty bool omitzero bool } func getOptions(tag reflect.StructTag) tagOptions { t := tag.Get("toml") if t == "-" { return tagOptions{skip: true} } var opts tagOptions parts := strings.Split(t, ",") opts.name = parts[0] for _, s := range parts[1:] { switch s { case "omitempty": opts.omitempty = true case "omitzero": opts.omitzero = true } } return opts } func isZero(rv reflect.Value) bool { switch rv.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return rv.Int() == 0 case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return rv.Uint() == 0 case reflect.Float32, reflect.Float64: return rv.Float() == 0.0 } return false } func isEmpty(rv reflect.Value) bool { switch rv.Kind() { case reflect.Array, reflect.Slice, reflect.Map, reflect.String: return rv.Len() == 0 case reflect.Struct: return reflect.Zero(rv.Type()).Interface() == rv.Interface() case reflect.Bool: return !rv.Bool() } return false } func (enc *Encoder) newline() { if enc.hasWritten { enc.wf("\n") } } // Write a key/value pair: // // key = // // This is also used for "k = v" in inline tables; so something like this will // be written in three calls: // // ┌────────────────────┐ // │ ┌───┐ ┌─────┐│ // v v v v vv // key = {k = v, k2 = v2} // func (enc *Encoder) writeKeyValue(key Key, val reflect.Value, inline bool) { if len(key) == 0 { encPanic(errNoKey) } enc.wf("%s%s = ", enc.indentStr(key), key.maybeQuoted(len(key)-1)) enc.eElement(val) if !inline { enc.newline() } } func (enc *Encoder) wf(format string, v ...interface{}) { _, err := fmt.Fprintf(enc.w, format, v...) if err != nil { encPanic(err) } enc.hasWritten = true } func (enc *Encoder) indentStr(key Key) string { return strings.Repeat(enc.Indent, len(key)-1) } func encPanic(err error) { panic(tomlEncodeError{err}) } // Resolve any level of pointers to the actual value (e.g. **string → string). func eindirect(v reflect.Value) reflect.Value { if v.Kind() != reflect.Ptr && v.Kind() != reflect.Interface { if isMarshaler(v) { return v } if v.CanAddr() { /// Special case for marshalers; see #358. if pv := v.Addr(); isMarshaler(pv) { return pv } } return v } if v.IsNil() { return v } return eindirect(v.Elem()) } func isNil(rv reflect.Value) bool { switch rv.Kind() { case reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: return rv.IsNil() default: return false } } ================================================ FILE: vendor/github.com/BurntSushi/toml/error.go ================================================ package toml import ( "fmt" "strings" ) // ParseError is returned when there is an error parsing the TOML syntax. // // For example invalid syntax, duplicate keys, etc. // // In addition to the error message itself, you can also print detailed location // information with context by using ErrorWithPosition(): // // toml: error: Key 'fruit' was already created and cannot be used as an array. // // At line 4, column 2-7: // // 2 | fruit = [] // 3 | // 4 | [[fruit]] # Not allowed // ^^^^^ // // Furthermore, the ErrorWithUsage() can be used to print the above with some // more detailed usage guidance: // // toml: error: newlines not allowed within inline tables // // At line 1, column 18: // // 1 | x = [{ key = 42 # // ^ // // Error help: // // Inline tables must always be on a single line: // // table = {key = 42, second = 43} // // It is invalid to split them over multiple lines like so: // // # INVALID // table = { // key = 42, // second = 43 // } // // Use regular for this: // // [table] // key = 42 // second = 43 type ParseError struct { Message string // Short technical message. Usage string // Longer message with usage guidance; may be blank. Position Position // Position of the error LastKey string // Last parsed key, may be blank. Line int // Line the error occurred. Deprecated: use Position. err error input string } // Position of an error. type Position struct { Line int // Line number, starting at 1. Start int // Start of error, as byte offset starting at 0. Len int // Lenght in bytes. } func (pe ParseError) Error() string { msg := pe.Message if msg == "" { // Error from errorf() msg = pe.err.Error() } if pe.LastKey == "" { return fmt.Sprintf("toml: line %d: %s", pe.Position.Line, msg) } return fmt.Sprintf("toml: line %d (last key %q): %s", pe.Position.Line, pe.LastKey, msg) } // ErrorWithUsage() returns the error with detailed location context. // // See the documentation on ParseError. func (pe ParseError) ErrorWithPosition() string { if pe.input == "" { // Should never happen, but just in case. return pe.Error() } var ( lines = strings.Split(pe.input, "\n") col = pe.column(lines) b = new(strings.Builder) ) msg := pe.Message if msg == "" { msg = pe.err.Error() } // TODO: don't show control characters as literals? This may not show up // well everywhere. if pe.Position.Len == 1 { fmt.Fprintf(b, "toml: error: %s\n\nAt line %d, column %d:\n\n", msg, pe.Position.Line, col+1) } else { fmt.Fprintf(b, "toml: error: %s\n\nAt line %d, column %d-%d:\n\n", msg, pe.Position.Line, col, col+pe.Position.Len) } if pe.Position.Line > 2 { fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line-2, lines[pe.Position.Line-3]) } if pe.Position.Line > 1 { fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line-1, lines[pe.Position.Line-2]) } fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line, lines[pe.Position.Line-1]) fmt.Fprintf(b, "% 10s%s%s\n", "", strings.Repeat(" ", col), strings.Repeat("^", pe.Position.Len)) return b.String() } // ErrorWithUsage() returns the error with detailed location context and usage // guidance. // // See the documentation on ParseError. func (pe ParseError) ErrorWithUsage() string { m := pe.ErrorWithPosition() if u, ok := pe.err.(interface{ Usage() string }); ok && u.Usage() != "" { lines := strings.Split(strings.TrimSpace(u.Usage()), "\n") for i := range lines { if lines[i] != "" { lines[i] = " " + lines[i] } } return m + "Error help:\n\n" + strings.Join(lines, "\n") + "\n" } return m } func (pe ParseError) column(lines []string) int { var pos, col int for i := range lines { ll := len(lines[i]) + 1 // +1 for the removed newline if pos+ll >= pe.Position.Start { col = pe.Position.Start - pos if col < 0 { // Should never happen, but just in case. col = 0 } break } pos += ll } return col } type ( errLexControl struct{ r rune } errLexEscape struct{ r rune } errLexUTF8 struct{ b byte } errLexInvalidNum struct{ v string } errLexInvalidDate struct{ v string } errLexInlineTableNL struct{} errLexStringNL struct{} errParseRange struct { i interface{} // int or float size string // "int64", "uint16", etc. } errParseDuration struct{ d string } ) func (e errLexControl) Error() string { return fmt.Sprintf("TOML files cannot contain control characters: '0x%02x'", e.r) } func (e errLexControl) Usage() string { return "" } func (e errLexEscape) Error() string { return fmt.Sprintf(`invalid escape in string '\%c'`, e.r) } func (e errLexEscape) Usage() string { return usageEscape } func (e errLexUTF8) Error() string { return fmt.Sprintf("invalid UTF-8 byte: 0x%02x", e.b) } func (e errLexUTF8) Usage() string { return "" } func (e errLexInvalidNum) Error() string { return fmt.Sprintf("invalid number: %q", e.v) } func (e errLexInvalidNum) Usage() string { return "" } func (e errLexInvalidDate) Error() string { return fmt.Sprintf("invalid date: %q", e.v) } func (e errLexInvalidDate) Usage() string { return "" } func (e errLexInlineTableNL) Error() string { return "newlines not allowed within inline tables" } func (e errLexInlineTableNL) Usage() string { return usageInlineNewline } func (e errLexStringNL) Error() string { return "strings cannot contain newlines" } func (e errLexStringNL) Usage() string { return usageStringNewline } func (e errParseRange) Error() string { return fmt.Sprintf("%v is out of range for %s", e.i, e.size) } func (e errParseRange) Usage() string { return usageIntOverflow } func (e errParseDuration) Error() string { return fmt.Sprintf("invalid duration: %q", e.d) } func (e errParseDuration) Usage() string { return usageDuration } const usageEscape = ` A '\' inside a "-delimited string is interpreted as an escape character. The following escape sequences are supported: \b, \t, \n, \f, \r, \", \\, \uXXXX, and \UXXXXXXXX To prevent a '\' from being recognized as an escape character, use either: - a ' or '''-delimited string; escape characters aren't processed in them; or - write two backslashes to get a single backslash: '\\'. If you're trying to add a Windows path (e.g. "C:\Users\martin") then using '/' instead of '\' will usually also work: "C:/Users/martin". ` const usageInlineNewline = ` Inline tables must always be on a single line: table = {key = 42, second = 43} It is invalid to split them over multiple lines like so: # INVALID table = { key = 42, second = 43 } Use regular for this: [table] key = 42 second = 43 ` const usageStringNewline = ` Strings must always be on a single line, and cannot span more than one line: # INVALID string = "Hello, world!" Instead use """ or ''' to split strings over multiple lines: string = """Hello, world!""" ` const usageIntOverflow = ` This number is too large; this may be an error in the TOML, but it can also be a bug in the program that uses too small of an integer. The maximum and minimum values are: size │ lowest │ highest ───────┼────────────────┼────────── int8 │ -128 │ 127 int16 │ -32,768 │ 32,767 int32 │ -2,147,483,648 │ 2,147,483,647 int64 │ -9.2 × 10¹⁷ │ 9.2 × 10¹⁷ uint8 │ 0 │ 255 uint16 │ 0 │ 65535 uint32 │ 0 │ 4294967295 uint64 │ 0 │ 1.8 × 10¹⁸ int refers to int32 on 32-bit systems and int64 on 64-bit systems. ` const usageDuration = ` A duration must be as "number", without any spaces. Valid units are: ns nanoseconds (billionth of a second) us, µs microseconds (millionth of a second) ms milliseconds (thousands of a second) s seconds m minutes h hours You can combine multiple units; for example "5m10s" for 5 minutes and 10 seconds. ` ================================================ FILE: vendor/github.com/BurntSushi/toml/internal/tz.go ================================================ package internal import "time" // Timezones used for local datetime, date, and time TOML types. // // The exact way times and dates without a timezone should be interpreted is not // well-defined in the TOML specification and left to the implementation. These // defaults to current local timezone offset of the computer, but this can be // changed by changing these variables before decoding. // // TODO: // Ideally we'd like to offer people the ability to configure the used timezone // by setting Decoder.Timezone and Encoder.Timezone; however, this is a bit // tricky: the reason we use three different variables for this is to support // round-tripping – without these specific TZ names we wouldn't know which // format to use. // // There isn't a good way to encode this right now though, and passing this sort // of information also ties in to various related issues such as string format // encoding, encoding of comments, etc. // // So, for the time being, just put this in internal until we can write a good // comprehensive API for doing all of this. // // The reason they're exported is because they're referred from in e.g. // internal/tag. // // Note that this behaviour is valid according to the TOML spec as the exact // behaviour is left up to implementations. var ( localOffset = func() int { _, o := time.Now().Zone(); return o }() LocalDatetime = time.FixedZone("datetime-local", localOffset) LocalDate = time.FixedZone("date-local", localOffset) LocalTime = time.FixedZone("time-local", localOffset) ) ================================================ FILE: vendor/github.com/BurntSushi/toml/lex.go ================================================ package toml import ( "fmt" "reflect" "runtime" "strings" "unicode" "unicode/utf8" ) type itemType int const ( itemError itemType = iota itemNIL // used in the parser to indicate no type itemEOF itemText itemString itemRawString itemMultilineString itemRawMultilineString itemBool itemInteger itemFloat itemDatetime itemArray // the start of an array itemArrayEnd itemTableStart itemTableEnd itemArrayTableStart itemArrayTableEnd itemKeyStart itemKeyEnd itemCommentStart itemInlineTableStart itemInlineTableEnd ) const eof = 0 type stateFn func(lx *lexer) stateFn func (p Position) String() string { return fmt.Sprintf("at line %d; start %d; length %d", p.Line, p.Start, p.Len) } type lexer struct { input string start int pos int line int state stateFn items chan item // Allow for backing up up to 4 runes. This is necessary because TOML // contains 3-rune tokens (""" and '''). prevWidths [4]int nprev int // how many of prevWidths are in use atEOF bool // If we emit an eof, we can still back up, but it is not OK to call next again. // A stack of state functions used to maintain context. // // The idea is to reuse parts of the state machine in various places. For // example, values can appear at the top level or within arbitrarily nested // arrays. The last state on the stack is used after a value has been lexed. // Similarly for comments. stack []stateFn } type item struct { typ itemType val string err error pos Position } func (lx *lexer) nextItem() item { for { select { case item := <-lx.items: return item default: lx.state = lx.state(lx) //fmt.Printf(" STATE %-24s current: %-10s stack: %s\n", lx.state, lx.current(), lx.stack) } } } func lex(input string) *lexer { lx := &lexer{ input: input, state: lexTop, items: make(chan item, 10), stack: make([]stateFn, 0, 10), line: 1, } return lx } func (lx *lexer) push(state stateFn) { lx.stack = append(lx.stack, state) } func (lx *lexer) pop() stateFn { if len(lx.stack) == 0 { return lx.errorf("BUG in lexer: no states to pop") } last := lx.stack[len(lx.stack)-1] lx.stack = lx.stack[0 : len(lx.stack)-1] return last } func (lx *lexer) current() string { return lx.input[lx.start:lx.pos] } func (lx lexer) getPos() Position { p := Position{ Line: lx.line, Start: lx.start, Len: lx.pos - lx.start, } if p.Len <= 0 { p.Len = 1 } return p } func (lx *lexer) emit(typ itemType) { // Needed for multiline strings ending with an incomplete UTF-8 sequence. if lx.start > lx.pos { lx.error(errLexUTF8{lx.input[lx.pos]}) return } lx.items <- item{typ: typ, pos: lx.getPos(), val: lx.current()} lx.start = lx.pos } func (lx *lexer) emitTrim(typ itemType) { lx.items <- item{typ: typ, pos: lx.getPos(), val: strings.TrimSpace(lx.current())} lx.start = lx.pos } func (lx *lexer) next() (r rune) { if lx.atEOF { panic("BUG in lexer: next called after EOF") } if lx.pos >= len(lx.input) { lx.atEOF = true return eof } if lx.input[lx.pos] == '\n' { lx.line++ } lx.prevWidths[3] = lx.prevWidths[2] lx.prevWidths[2] = lx.prevWidths[1] lx.prevWidths[1] = lx.prevWidths[0] if lx.nprev < 4 { lx.nprev++ } r, w := utf8.DecodeRuneInString(lx.input[lx.pos:]) if r == utf8.RuneError { lx.error(errLexUTF8{lx.input[lx.pos]}) return utf8.RuneError } // Note: don't use peek() here, as this calls next(). if isControl(r) || (r == '\r' && (len(lx.input)-1 == lx.pos || lx.input[lx.pos+1] != '\n')) { lx.errorControlChar(r) return utf8.RuneError } lx.prevWidths[0] = w lx.pos += w return r } // ignore skips over the pending input before this point. func (lx *lexer) ignore() { lx.start = lx.pos } // backup steps back one rune. Can be called 4 times between calls to next. func (lx *lexer) backup() { if lx.atEOF { lx.atEOF = false return } if lx.nprev < 1 { panic("BUG in lexer: backed up too far") } w := lx.prevWidths[0] lx.prevWidths[0] = lx.prevWidths[1] lx.prevWidths[1] = lx.prevWidths[2] lx.prevWidths[2] = lx.prevWidths[3] lx.nprev-- lx.pos -= w if lx.pos < len(lx.input) && lx.input[lx.pos] == '\n' { lx.line-- } } // accept consumes the next rune if it's equal to `valid`. func (lx *lexer) accept(valid rune) bool { if lx.next() == valid { return true } lx.backup() return false } // peek returns but does not consume the next rune in the input. func (lx *lexer) peek() rune { r := lx.next() lx.backup() return r } // skip ignores all input that matches the given predicate. func (lx *lexer) skip(pred func(rune) bool) { for { r := lx.next() if pred(r) { continue } lx.backup() lx.ignore() return } } // error stops all lexing by emitting an error and returning `nil`. // // Note that any value that is a character is escaped if it's a special // character (newlines, tabs, etc.). func (lx *lexer) error(err error) stateFn { if lx.atEOF { return lx.errorPrevLine(err) } lx.items <- item{typ: itemError, pos: lx.getPos(), err: err} return nil } // errorfPrevline is like error(), but sets the position to the last column of // the previous line. // // This is so that unexpected EOF or NL errors don't show on a new blank line. func (lx *lexer) errorPrevLine(err error) stateFn { pos := lx.getPos() pos.Line-- pos.Len = 1 pos.Start = lx.pos - 1 lx.items <- item{typ: itemError, pos: pos, err: err} return nil } // errorPos is like error(), but allows explicitly setting the position. func (lx *lexer) errorPos(start, length int, err error) stateFn { pos := lx.getPos() pos.Start = start pos.Len = length lx.items <- item{typ: itemError, pos: pos, err: err} return nil } // errorf is like error, and creates a new error. func (lx *lexer) errorf(format string, values ...interface{}) stateFn { if lx.atEOF { pos := lx.getPos() pos.Line-- pos.Len = 1 pos.Start = lx.pos - 1 lx.items <- item{typ: itemError, pos: pos, err: fmt.Errorf(format, values...)} return nil } lx.items <- item{typ: itemError, pos: lx.getPos(), err: fmt.Errorf(format, values...)} return nil } func (lx *lexer) errorControlChar(cc rune) stateFn { return lx.errorPos(lx.pos-1, 1, errLexControl{cc}) } // lexTop consumes elements at the top level of TOML data. func lexTop(lx *lexer) stateFn { r := lx.next() if isWhitespace(r) || isNL(r) { return lexSkip(lx, lexTop) } switch r { case '#': lx.push(lexTop) return lexCommentStart case '[': return lexTableStart case eof: if lx.pos > lx.start { return lx.errorf("unexpected EOF") } lx.emit(itemEOF) return nil } // At this point, the only valid item can be a key, so we back up // and let the key lexer do the rest. lx.backup() lx.push(lexTopEnd) return lexKeyStart } // lexTopEnd is entered whenever a top-level item has been consumed. (A value // or a table.) It must see only whitespace, and will turn back to lexTop // upon a newline. If it sees EOF, it will quit the lexer successfully. func lexTopEnd(lx *lexer) stateFn { r := lx.next() switch { case r == '#': // a comment will read to a newline for us. lx.push(lexTop) return lexCommentStart case isWhitespace(r): return lexTopEnd case isNL(r): lx.ignore() return lexTop case r == eof: lx.emit(itemEOF) return nil } return lx.errorf( "expected a top-level item to end with a newline, comment, or EOF, but got %q instead", r) } // lexTable lexes the beginning of a table. Namely, it makes sure that // it starts with a character other than '.' and ']'. // It assumes that '[' has already been consumed. // It also handles the case that this is an item in an array of tables. // e.g., '[[name]]'. func lexTableStart(lx *lexer) stateFn { if lx.peek() == '[' { lx.next() lx.emit(itemArrayTableStart) lx.push(lexArrayTableEnd) } else { lx.emit(itemTableStart) lx.push(lexTableEnd) } return lexTableNameStart } func lexTableEnd(lx *lexer) stateFn { lx.emit(itemTableEnd) return lexTopEnd } func lexArrayTableEnd(lx *lexer) stateFn { if r := lx.next(); r != ']' { return lx.errorf("expected end of table array name delimiter ']', but got %q instead", r) } lx.emit(itemArrayTableEnd) return lexTopEnd } func lexTableNameStart(lx *lexer) stateFn { lx.skip(isWhitespace) switch r := lx.peek(); { case r == ']' || r == eof: return lx.errorf("unexpected end of table name (table names cannot be empty)") case r == '.': return lx.errorf("unexpected table separator (table names cannot be empty)") case r == '"' || r == '\'': lx.ignore() lx.push(lexTableNameEnd) return lexQuotedName default: lx.push(lexTableNameEnd) return lexBareName } } // lexTableNameEnd reads the end of a piece of a table name, optionally // consuming whitespace. func lexTableNameEnd(lx *lexer) stateFn { lx.skip(isWhitespace) switch r := lx.next(); { case isWhitespace(r): return lexTableNameEnd case r == '.': lx.ignore() return lexTableNameStart case r == ']': return lx.pop() default: return lx.errorf("expected '.' or ']' to end table name, but got %q instead", r) } } // lexBareName lexes one part of a key or table. // // It assumes that at least one valid character for the table has already been // read. // // Lexes only one part, e.g. only 'a' inside 'a.b'. func lexBareName(lx *lexer) stateFn { r := lx.next() if isBareKeyChar(r) { return lexBareName } lx.backup() lx.emit(itemText) return lx.pop() } // lexBareName lexes one part of a key or table. // // It assumes that at least one valid character for the table has already been // read. // // Lexes only one part, e.g. only '"a"' inside '"a".b'. func lexQuotedName(lx *lexer) stateFn { r := lx.next() switch { case isWhitespace(r): return lexSkip(lx, lexValue) case r == '"': lx.ignore() // ignore the '"' return lexString case r == '\'': lx.ignore() // ignore the "'" return lexRawString case r == eof: return lx.errorf("unexpected EOF; expected value") default: return lx.errorf("expected value but found %q instead", r) } } // lexKeyStart consumes all key parts until a '='. func lexKeyStart(lx *lexer) stateFn { lx.skip(isWhitespace) switch r := lx.peek(); { case r == '=' || r == eof: return lx.errorf("unexpected '=': key name appears blank") case r == '.': return lx.errorf("unexpected '.': keys cannot start with a '.'") case r == '"' || r == '\'': lx.ignore() fallthrough default: // Bare key lx.emit(itemKeyStart) return lexKeyNameStart } } func lexKeyNameStart(lx *lexer) stateFn { lx.skip(isWhitespace) switch r := lx.peek(); { case r == '=' || r == eof: return lx.errorf("unexpected '='") case r == '.': return lx.errorf("unexpected '.'") case r == '"' || r == '\'': lx.ignore() lx.push(lexKeyEnd) return lexQuotedName default: lx.push(lexKeyEnd) return lexBareName } } // lexKeyEnd consumes the end of a key and trims whitespace (up to the key // separator). func lexKeyEnd(lx *lexer) stateFn { lx.skip(isWhitespace) switch r := lx.next(); { case isWhitespace(r): return lexSkip(lx, lexKeyEnd) case r == eof: return lx.errorf("unexpected EOF; expected key separator '='") case r == '.': lx.ignore() return lexKeyNameStart case r == '=': lx.emit(itemKeyEnd) return lexSkip(lx, lexValue) default: return lx.errorf("expected '.' or '=', but got %q instead", r) } } // lexValue starts the consumption of a value anywhere a value is expected. // lexValue will ignore whitespace. // After a value is lexed, the last state on the next is popped and returned. func lexValue(lx *lexer) stateFn { // We allow whitespace to precede a value, but NOT newlines. // In array syntax, the array states are responsible for ignoring newlines. r := lx.next() switch { case isWhitespace(r): return lexSkip(lx, lexValue) case isDigit(r): lx.backup() // avoid an extra state and use the same as above return lexNumberOrDateStart } switch r { case '[': lx.ignore() lx.emit(itemArray) return lexArrayValue case '{': lx.ignore() lx.emit(itemInlineTableStart) return lexInlineTableValue case '"': if lx.accept('"') { if lx.accept('"') { lx.ignore() // Ignore """ return lexMultilineString } lx.backup() } lx.ignore() // ignore the '"' return lexString case '\'': if lx.accept('\'') { if lx.accept('\'') { lx.ignore() // Ignore """ return lexMultilineRawString } lx.backup() } lx.ignore() // ignore the "'" return lexRawString case '.': // special error case, be kind to users return lx.errorf("floats must start with a digit, not '.'") case 'i', 'n': if (lx.accept('n') && lx.accept('f')) || (lx.accept('a') && lx.accept('n')) { lx.emit(itemFloat) return lx.pop() } case '-', '+': return lexDecimalNumberStart } if unicode.IsLetter(r) { // Be permissive here; lexBool will give a nice error if the // user wrote something like // x = foo // (i.e. not 'true' or 'false' but is something else word-like.) lx.backup() return lexBool } if r == eof { return lx.errorf("unexpected EOF; expected value") } return lx.errorf("expected value but found %q instead", r) } // lexArrayValue consumes one value in an array. It assumes that '[' or ',' // have already been consumed. All whitespace and newlines are ignored. func lexArrayValue(lx *lexer) stateFn { r := lx.next() switch { case isWhitespace(r) || isNL(r): return lexSkip(lx, lexArrayValue) case r == '#': lx.push(lexArrayValue) return lexCommentStart case r == ',': return lx.errorf("unexpected comma") case r == ']': return lexArrayEnd } lx.backup() lx.push(lexArrayValueEnd) return lexValue } // lexArrayValueEnd consumes everything between the end of an array value and // the next value (or the end of the array): it ignores whitespace and newlines // and expects either a ',' or a ']'. func lexArrayValueEnd(lx *lexer) stateFn { switch r := lx.next(); { case isWhitespace(r) || isNL(r): return lexSkip(lx, lexArrayValueEnd) case r == '#': lx.push(lexArrayValueEnd) return lexCommentStart case r == ',': lx.ignore() return lexArrayValue // move on to the next value case r == ']': return lexArrayEnd default: return lx.errorf("expected a comma (',') or array terminator (']'), but got %s", runeOrEOF(r)) } } // lexArrayEnd finishes the lexing of an array. // It assumes that a ']' has just been consumed. func lexArrayEnd(lx *lexer) stateFn { lx.ignore() lx.emit(itemArrayEnd) return lx.pop() } // lexInlineTableValue consumes one key/value pair in an inline table. // It assumes that '{' or ',' have already been consumed. Whitespace is ignored. func lexInlineTableValue(lx *lexer) stateFn { r := lx.next() switch { case isWhitespace(r): return lexSkip(lx, lexInlineTableValue) case isNL(r): return lx.errorPrevLine(errLexInlineTableNL{}) case r == '#': lx.push(lexInlineTableValue) return lexCommentStart case r == ',': return lx.errorf("unexpected comma") case r == '}': return lexInlineTableEnd } lx.backup() lx.push(lexInlineTableValueEnd) return lexKeyStart } // lexInlineTableValueEnd consumes everything between the end of an inline table // key/value pair and the next pair (or the end of the table): // it ignores whitespace and expects either a ',' or a '}'. func lexInlineTableValueEnd(lx *lexer) stateFn { switch r := lx.next(); { case isWhitespace(r): return lexSkip(lx, lexInlineTableValueEnd) case isNL(r): return lx.errorPrevLine(errLexInlineTableNL{}) case r == '#': lx.push(lexInlineTableValueEnd) return lexCommentStart case r == ',': lx.ignore() lx.skip(isWhitespace) if lx.peek() == '}' { return lx.errorf("trailing comma not allowed in inline tables") } return lexInlineTableValue case r == '}': return lexInlineTableEnd default: return lx.errorf("expected a comma or an inline table terminator '}', but got %s instead", runeOrEOF(r)) } } func runeOrEOF(r rune) string { if r == eof { return "end of file" } return "'" + string(r) + "'" } // lexInlineTableEnd finishes the lexing of an inline table. // It assumes that a '}' has just been consumed. func lexInlineTableEnd(lx *lexer) stateFn { lx.ignore() lx.emit(itemInlineTableEnd) return lx.pop() } // lexString consumes the inner contents of a string. It assumes that the // beginning '"' has already been consumed and ignored. func lexString(lx *lexer) stateFn { r := lx.next() switch { case r == eof: return lx.errorf(`unexpected EOF; expected '"'`) case isNL(r): return lx.errorPrevLine(errLexStringNL{}) case r == '\\': lx.push(lexString) return lexStringEscape case r == '"': lx.backup() lx.emit(itemString) lx.next() lx.ignore() return lx.pop() } return lexString } // lexMultilineString consumes the inner contents of a string. It assumes that // the beginning '"""' has already been consumed and ignored. func lexMultilineString(lx *lexer) stateFn { r := lx.next() switch r { default: return lexMultilineString case eof: return lx.errorf(`unexpected EOF; expected '"""'`) case '\\': return lexMultilineStringEscape case '"': /// Found " → try to read two more "". if lx.accept('"') { if lx.accept('"') { /// Peek ahead: the string can contain " and "", including at the /// end: """str""""" /// 6 or more at the end, however, is an error. if lx.peek() == '"' { /// Check if we already lexed 5 's; if so we have 6 now, and /// that's just too many man! /// /// Second check is for the edge case: /// /// two quotes allowed. /// vv /// """lol \"""""" /// ^^ ^^^---- closing three /// escaped /// /// But ugly, but it works if strings.HasSuffix(lx.current(), `"""""`) && !strings.HasSuffix(lx.current(), `\"""""`) { return lx.errorf(`unexpected '""""""'`) } lx.backup() lx.backup() return lexMultilineString } lx.backup() /// backup: don't include the """ in the item. lx.backup() lx.backup() lx.emit(itemMultilineString) lx.next() /// Read over ''' again and discard it. lx.next() lx.next() lx.ignore() return lx.pop() } lx.backup() } return lexMultilineString } } // lexRawString consumes a raw string. Nothing can be escaped in such a string. // It assumes that the beginning "'" has already been consumed and ignored. func lexRawString(lx *lexer) stateFn { r := lx.next() switch { default: return lexRawString case r == eof: return lx.errorf(`unexpected EOF; expected "'"`) case isNL(r): return lx.errorPrevLine(errLexStringNL{}) case r == '\'': lx.backup() lx.emit(itemRawString) lx.next() lx.ignore() return lx.pop() } } // lexMultilineRawString consumes a raw string. Nothing can be escaped in such // a string. It assumes that the beginning "'''" has already been consumed and // ignored. func lexMultilineRawString(lx *lexer) stateFn { r := lx.next() switch r { default: return lexMultilineRawString case eof: return lx.errorf(`unexpected EOF; expected "'''"`) case '\'': /// Found ' → try to read two more ''. if lx.accept('\'') { if lx.accept('\'') { /// Peek ahead: the string can contain ' and '', including at the /// end: '''str''''' /// 6 or more at the end, however, is an error. if lx.peek() == '\'' { /// Check if we already lexed 5 's; if so we have 6 now, and /// that's just too many man! if strings.HasSuffix(lx.current(), "'''''") { return lx.errorf(`unexpected "''''''"`) } lx.backup() lx.backup() return lexMultilineRawString } lx.backup() /// backup: don't include the ''' in the item. lx.backup() lx.backup() lx.emit(itemRawMultilineString) lx.next() /// Read over ''' again and discard it. lx.next() lx.next() lx.ignore() return lx.pop() } lx.backup() } return lexMultilineRawString } } // lexMultilineStringEscape consumes an escaped character. It assumes that the // preceding '\\' has already been consumed. func lexMultilineStringEscape(lx *lexer) stateFn { if isNL(lx.next()) { /// \ escaping newline. return lexMultilineString } lx.backup() lx.push(lexMultilineString) return lexStringEscape(lx) } func lexStringEscape(lx *lexer) stateFn { r := lx.next() switch r { case 'b': fallthrough case 't': fallthrough case 'n': fallthrough case 'f': fallthrough case 'r': fallthrough case '"': fallthrough case ' ', '\t': // Inside """ .. """ strings you can use \ to escape newlines, and any // amount of whitespace can be between the \ and \n. fallthrough case '\\': return lx.pop() case 'u': return lexShortUnicodeEscape case 'U': return lexLongUnicodeEscape } return lx.error(errLexEscape{r}) } func lexShortUnicodeEscape(lx *lexer) stateFn { var r rune for i := 0; i < 4; i++ { r = lx.next() if !isHexadecimal(r) { return lx.errorf( `expected four hexadecimal digits after '\u', but got %q instead`, lx.current()) } } return lx.pop() } func lexLongUnicodeEscape(lx *lexer) stateFn { var r rune for i := 0; i < 8; i++ { r = lx.next() if !isHexadecimal(r) { return lx.errorf( `expected eight hexadecimal digits after '\U', but got %q instead`, lx.current()) } } return lx.pop() } // lexNumberOrDateStart processes the first character of a value which begins // with a digit. It exists to catch values starting with '0', so that // lexBaseNumberOrDate can differentiate base prefixed integers from other // types. func lexNumberOrDateStart(lx *lexer) stateFn { r := lx.next() switch r { case '0': return lexBaseNumberOrDate } if !isDigit(r) { // The only way to reach this state is if the value starts // with a digit, so specifically treat anything else as an // error. return lx.errorf("expected a digit but got %q", r) } return lexNumberOrDate } // lexNumberOrDate consumes either an integer, float or datetime. func lexNumberOrDate(lx *lexer) stateFn { r := lx.next() if isDigit(r) { return lexNumberOrDate } switch r { case '-', ':': return lexDatetime case '_': return lexDecimalNumber case '.', 'e', 'E': return lexFloat } lx.backup() lx.emit(itemInteger) return lx.pop() } // lexDatetime consumes a Datetime, to a first approximation. // The parser validates that it matches one of the accepted formats. func lexDatetime(lx *lexer) stateFn { r := lx.next() if isDigit(r) { return lexDatetime } switch r { case '-', ':', 'T', 't', ' ', '.', 'Z', 'z', '+': return lexDatetime } lx.backup() lx.emitTrim(itemDatetime) return lx.pop() } // lexHexInteger consumes a hexadecimal integer after seeing the '0x' prefix. func lexHexInteger(lx *lexer) stateFn { r := lx.next() if isHexadecimal(r) { return lexHexInteger } switch r { case '_': return lexHexInteger } lx.backup() lx.emit(itemInteger) return lx.pop() } // lexOctalInteger consumes an octal integer after seeing the '0o' prefix. func lexOctalInteger(lx *lexer) stateFn { r := lx.next() if isOctal(r) { return lexOctalInteger } switch r { case '_': return lexOctalInteger } lx.backup() lx.emit(itemInteger) return lx.pop() } // lexBinaryInteger consumes a binary integer after seeing the '0b' prefix. func lexBinaryInteger(lx *lexer) stateFn { r := lx.next() if isBinary(r) { return lexBinaryInteger } switch r { case '_': return lexBinaryInteger } lx.backup() lx.emit(itemInteger) return lx.pop() } // lexDecimalNumber consumes a decimal float or integer. func lexDecimalNumber(lx *lexer) stateFn { r := lx.next() if isDigit(r) { return lexDecimalNumber } switch r { case '.', 'e', 'E': return lexFloat case '_': return lexDecimalNumber } lx.backup() lx.emit(itemInteger) return lx.pop() } // lexDecimalNumber consumes the first digit of a number beginning with a sign. // It assumes the sign has already been consumed. Values which start with a sign // are only allowed to be decimal integers or floats. // // The special "nan" and "inf" values are also recognized. func lexDecimalNumberStart(lx *lexer) stateFn { r := lx.next() // Special error cases to give users better error messages switch r { case 'i': if !lx.accept('n') || !lx.accept('f') { return lx.errorf("invalid float: '%s'", lx.current()) } lx.emit(itemFloat) return lx.pop() case 'n': if !lx.accept('a') || !lx.accept('n') { return lx.errorf("invalid float: '%s'", lx.current()) } lx.emit(itemFloat) return lx.pop() case '0': p := lx.peek() switch p { case 'b', 'o', 'x': return lx.errorf("cannot use sign with non-decimal numbers: '%s%c'", lx.current(), p) } case '.': return lx.errorf("floats must start with a digit, not '.'") } if isDigit(r) { return lexDecimalNumber } return lx.errorf("expected a digit but got %q", r) } // lexBaseNumberOrDate differentiates between the possible values which // start with '0'. It assumes that before reaching this state, the initial '0' // has been consumed. func lexBaseNumberOrDate(lx *lexer) stateFn { r := lx.next() // Note: All datetimes start with at least two digits, so we don't // handle date characters (':', '-', etc.) here. if isDigit(r) { return lexNumberOrDate } switch r { case '_': // Can only be decimal, because there can't be an underscore // between the '0' and the base designator, and dates can't // contain underscores. return lexDecimalNumber case '.', 'e', 'E': return lexFloat case 'b': r = lx.peek() if !isBinary(r) { lx.errorf("not a binary number: '%s%c'", lx.current(), r) } return lexBinaryInteger case 'o': r = lx.peek() if !isOctal(r) { lx.errorf("not an octal number: '%s%c'", lx.current(), r) } return lexOctalInteger case 'x': r = lx.peek() if !isHexadecimal(r) { lx.errorf("not a hexidecimal number: '%s%c'", lx.current(), r) } return lexHexInteger } lx.backup() lx.emit(itemInteger) return lx.pop() } // lexFloat consumes the elements of a float. It allows any sequence of // float-like characters, so floats emitted by the lexer are only a first // approximation and must be validated by the parser. func lexFloat(lx *lexer) stateFn { r := lx.next() if isDigit(r) { return lexFloat } switch r { case '_', '.', '-', '+', 'e', 'E': return lexFloat } lx.backup() lx.emit(itemFloat) return lx.pop() } // lexBool consumes a bool string: 'true' or 'false. func lexBool(lx *lexer) stateFn { var rs []rune for { r := lx.next() if !unicode.IsLetter(r) { lx.backup() break } rs = append(rs, r) } s := string(rs) switch s { case "true", "false": lx.emit(itemBool) return lx.pop() } return lx.errorf("expected value but found %q instead", s) } // lexCommentStart begins the lexing of a comment. It will emit // itemCommentStart and consume no characters, passing control to lexComment. func lexCommentStart(lx *lexer) stateFn { lx.ignore() lx.emit(itemCommentStart) return lexComment } // lexComment lexes an entire comment. It assumes that '#' has been consumed. // It will consume *up to* the first newline character, and pass control // back to the last state on the stack. func lexComment(lx *lexer) stateFn { switch r := lx.next(); { case isNL(r) || r == eof: lx.backup() lx.emit(itemText) return lx.pop() default: return lexComment } } // lexSkip ignores all slurped input and moves on to the next state. func lexSkip(lx *lexer, nextState stateFn) stateFn { lx.ignore() return nextState } func (s stateFn) String() string { name := runtime.FuncForPC(reflect.ValueOf(s).Pointer()).Name() if i := strings.LastIndexByte(name, '.'); i > -1 { name = name[i+1:] } if s == nil { name = "" } return name + "()" } func (itype itemType) String() string { switch itype { case itemError: return "Error" case itemNIL: return "NIL" case itemEOF: return "EOF" case itemText: return "Text" case itemString, itemRawString, itemMultilineString, itemRawMultilineString: return "String" case itemBool: return "Bool" case itemInteger: return "Integer" case itemFloat: return "Float" case itemDatetime: return "DateTime" case itemTableStart: return "TableStart" case itemTableEnd: return "TableEnd" case itemKeyStart: return "KeyStart" case itemKeyEnd: return "KeyEnd" case itemArray: return "Array" case itemArrayEnd: return "ArrayEnd" case itemCommentStart: return "CommentStart" case itemInlineTableStart: return "InlineTableStart" case itemInlineTableEnd: return "InlineTableEnd" } panic(fmt.Sprintf("BUG: Unknown type '%d'.", int(itype))) } func (item item) String() string { return fmt.Sprintf("(%s, %s)", item.typ.String(), item.val) } func isWhitespace(r rune) bool { return r == '\t' || r == ' ' } func isNL(r rune) bool { return r == '\n' || r == '\r' } func isControl(r rune) bool { // Control characters except \t, \r, \n switch r { case '\t', '\r', '\n': return false default: return (r >= 0x00 && r <= 0x1f) || r == 0x7f } } func isDigit(r rune) bool { return r >= '0' && r <= '9' } func isBinary(r rune) bool { return r == '0' || r == '1' } func isOctal(r rune) bool { return r >= '0' && r <= '7' } func isHexadecimal(r rune) bool { return (r >= '0' && r <= '9') || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F') } func isBareKeyChar(r rune) bool { return (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '_' || r == '-' } ================================================ FILE: vendor/github.com/BurntSushi/toml/meta.go ================================================ package toml import ( "strings" ) // MetaData allows access to meta information about TOML data that's not // accessible otherwise. // // It allows checking if a key is defined in the TOML data, whether any keys // were undecoded, and the TOML type of a key. type MetaData struct { context Key // Used only during decoding. keyInfo map[string]keyInfo mapping map[string]interface{} keys []Key decoded map[string]struct{} data []byte // Input file; for errors. } // IsDefined reports if the key exists in the TOML data. // // The key should be specified hierarchically, for example to access the TOML // key "a.b.c" you would use IsDefined("a", "b", "c"). Keys are case sensitive. // // Returns false for an empty key. func (md *MetaData) IsDefined(key ...string) bool { if len(key) == 0 { return false } var ( hash map[string]interface{} ok bool hashOrVal interface{} = md.mapping ) for _, k := range key { if hash, ok = hashOrVal.(map[string]interface{}); !ok { return false } if hashOrVal, ok = hash[k]; !ok { return false } } return true } // Type returns a string representation of the type of the key specified. // // Type will return the empty string if given an empty key or a key that does // not exist. Keys are case sensitive. func (md *MetaData) Type(key ...string) string { if ki, ok := md.keyInfo[Key(key).String()]; ok { return ki.tomlType.typeString() } return "" } // Keys returns a slice of every key in the TOML data, including key groups. // // Each key is itself a slice, where the first element is the top of the // hierarchy and the last is the most specific. The list will have the same // order as the keys appeared in the TOML data. // // All keys returned are non-empty. func (md *MetaData) Keys() []Key { return md.keys } // Undecoded returns all keys that have not been decoded in the order in which // they appear in the original TOML document. // // This includes keys that haven't been decoded because of a Primitive value. // Once the Primitive value is decoded, the keys will be considered decoded. // // Also note that decoding into an empty interface will result in no decoding, // and so no keys will be considered decoded. // // In this sense, the Undecoded keys correspond to keys in the TOML document // that do not have a concrete type in your representation. func (md *MetaData) Undecoded() []Key { undecoded := make([]Key, 0, len(md.keys)) for _, key := range md.keys { if _, ok := md.decoded[key.String()]; !ok { undecoded = append(undecoded, key) } } return undecoded } // Key represents any TOML key, including key groups. Use (MetaData).Keys to get // values of this type. type Key []string func (k Key) String() string { ss := make([]string, len(k)) for i := range k { ss[i] = k.maybeQuoted(i) } return strings.Join(ss, ".") } func (k Key) maybeQuoted(i int) string { if k[i] == "" { return `""` } for _, c := range k[i] { if !isBareKeyChar(c) { return `"` + dblQuotedReplacer.Replace(k[i]) + `"` } } return k[i] } func (k Key) add(piece string) Key { newKey := make(Key, len(k)+1) copy(newKey, k) newKey[len(k)] = piece return newKey } ================================================ FILE: vendor/github.com/BurntSushi/toml/parse.go ================================================ package toml import ( "fmt" "strconv" "strings" "time" "unicode/utf8" "github.com/BurntSushi/toml/internal" ) type parser struct { lx *lexer context Key // Full key for the current hash in scope. currentKey string // Base key name for everything except hashes. pos Position // Current position in the TOML file. ordered []Key // List of keys in the order that they appear in the TOML data. keyInfo map[string]keyInfo // Map keyname → info about the TOML key. mapping map[string]interface{} // Map keyname → key value. implicits map[string]struct{} // Record implicit keys (e.g. "key.group.names"). } type keyInfo struct { pos Position tomlType tomlType } func parse(data string) (p *parser, err error) { defer func() { if r := recover(); r != nil { if pErr, ok := r.(ParseError); ok { pErr.input = data err = pErr return } panic(r) } }() // Read over BOM; do this here as the lexer calls utf8.DecodeRuneInString() // which mangles stuff. if strings.HasPrefix(data, "\xff\xfe") || strings.HasPrefix(data, "\xfe\xff") { data = data[2:] } // Examine first few bytes for NULL bytes; this probably means it's a UTF-16 // file (second byte in surrogate pair being NULL). Again, do this here to // avoid having to deal with UTF-8/16 stuff in the lexer. ex := 6 if len(data) < 6 { ex = len(data) } if i := strings.IndexRune(data[:ex], 0); i > -1 { return nil, ParseError{ Message: "files cannot contain NULL bytes; probably using UTF-16; TOML files must be UTF-8", Position: Position{Line: 1, Start: i, Len: 1}, Line: 1, input: data, } } p = &parser{ keyInfo: make(map[string]keyInfo), mapping: make(map[string]interface{}), lx: lex(data), ordered: make([]Key, 0), implicits: make(map[string]struct{}), } for { item := p.next() if item.typ == itemEOF { break } p.topLevel(item) } return p, nil } func (p *parser) panicErr(it item, err error) { panic(ParseError{ err: err, Position: it.pos, Line: it.pos.Len, LastKey: p.current(), }) } func (p *parser) panicItemf(it item, format string, v ...interface{}) { panic(ParseError{ Message: fmt.Sprintf(format, v...), Position: it.pos, Line: it.pos.Len, LastKey: p.current(), }) } func (p *parser) panicf(format string, v ...interface{}) { panic(ParseError{ Message: fmt.Sprintf(format, v...), Position: p.pos, Line: p.pos.Line, LastKey: p.current(), }) } func (p *parser) next() item { it := p.lx.nextItem() //fmt.Printf("ITEM %-18s line %-3d │ %q\n", it.typ, it.pos.Line, it.val) if it.typ == itemError { if it.err != nil { panic(ParseError{ Position: it.pos, Line: it.pos.Line, LastKey: p.current(), err: it.err, }) } p.panicItemf(it, "%s", it.val) } return it } func (p *parser) nextPos() item { it := p.next() p.pos = it.pos return it } func (p *parser) bug(format string, v ...interface{}) { panic(fmt.Sprintf("BUG: "+format+"\n\n", v...)) } func (p *parser) expect(typ itemType) item { it := p.next() p.assertEqual(typ, it.typ) return it } func (p *parser) assertEqual(expected, got itemType) { if expected != got { p.bug("Expected '%s' but got '%s'.", expected, got) } } func (p *parser) topLevel(item item) { switch item.typ { case itemCommentStart: // # .. p.expect(itemText) case itemTableStart: // [ .. ] name := p.nextPos() var key Key for ; name.typ != itemTableEnd && name.typ != itemEOF; name = p.next() { key = append(key, p.keyString(name)) } p.assertEqual(itemTableEnd, name.typ) p.addContext(key, false) p.setType("", tomlHash, item.pos) p.ordered = append(p.ordered, key) case itemArrayTableStart: // [[ .. ]] name := p.nextPos() var key Key for ; name.typ != itemArrayTableEnd && name.typ != itemEOF; name = p.next() { key = append(key, p.keyString(name)) } p.assertEqual(itemArrayTableEnd, name.typ) p.addContext(key, true) p.setType("", tomlArrayHash, item.pos) p.ordered = append(p.ordered, key) case itemKeyStart: // key = .. outerContext := p.context /// Read all the key parts (e.g. 'a' and 'b' in 'a.b') k := p.nextPos() var key Key for ; k.typ != itemKeyEnd && k.typ != itemEOF; k = p.next() { key = append(key, p.keyString(k)) } p.assertEqual(itemKeyEnd, k.typ) /// The current key is the last part. p.currentKey = key[len(key)-1] /// All the other parts (if any) are the context; need to set each part /// as implicit. context := key[:len(key)-1] for i := range context { p.addImplicitContext(append(p.context, context[i:i+1]...)) } /// Set value. vItem := p.next() val, typ := p.value(vItem, false) p.set(p.currentKey, val, typ, vItem.pos) p.ordered = append(p.ordered, p.context.add(p.currentKey)) /// Remove the context we added (preserving any context from [tbl] lines). p.context = outerContext p.currentKey = "" default: p.bug("Unexpected type at top level: %s", item.typ) } } // Gets a string for a key (or part of a key in a table name). func (p *parser) keyString(it item) string { switch it.typ { case itemText: return it.val case itemString, itemMultilineString, itemRawString, itemRawMultilineString: s, _ := p.value(it, false) return s.(string) default: p.bug("Unexpected key type: %s", it.typ) } panic("unreachable") } var datetimeRepl = strings.NewReplacer( "z", "Z", "t", "T", " ", "T") // value translates an expected value from the lexer into a Go value wrapped // as an empty interface. func (p *parser) value(it item, parentIsArray bool) (interface{}, tomlType) { switch it.typ { case itemString: return p.replaceEscapes(it, it.val), p.typeOfPrimitive(it) case itemMultilineString: return p.replaceEscapes(it, stripFirstNewline(p.stripEscapedNewlines(it.val))), p.typeOfPrimitive(it) case itemRawString: return it.val, p.typeOfPrimitive(it) case itemRawMultilineString: return stripFirstNewline(it.val), p.typeOfPrimitive(it) case itemInteger: return p.valueInteger(it) case itemFloat: return p.valueFloat(it) case itemBool: switch it.val { case "true": return true, p.typeOfPrimitive(it) case "false": return false, p.typeOfPrimitive(it) default: p.bug("Expected boolean value, but got '%s'.", it.val) } case itemDatetime: return p.valueDatetime(it) case itemArray: return p.valueArray(it) case itemInlineTableStart: return p.valueInlineTable(it, parentIsArray) default: p.bug("Unexpected value type: %s", it.typ) } panic("unreachable") } func (p *parser) valueInteger(it item) (interface{}, tomlType) { if !numUnderscoresOK(it.val) { p.panicItemf(it, "Invalid integer %q: underscores must be surrounded by digits", it.val) } if numHasLeadingZero(it.val) { p.panicItemf(it, "Invalid integer %q: cannot have leading zeroes", it.val) } num, err := strconv.ParseInt(it.val, 0, 64) if err != nil { // Distinguish integer values. Normally, it'd be a bug if the lexer // provides an invalid integer, but it's possible that the number is // out of range of valid values (which the lexer cannot determine). // So mark the former as a bug but the latter as a legitimate user // error. if e, ok := err.(*strconv.NumError); ok && e.Err == strconv.ErrRange { p.panicErr(it, errParseRange{i: it.val, size: "int64"}) } else { p.bug("Expected integer value, but got '%s'.", it.val) } } return num, p.typeOfPrimitive(it) } func (p *parser) valueFloat(it item) (interface{}, tomlType) { parts := strings.FieldsFunc(it.val, func(r rune) bool { switch r { case '.', 'e', 'E': return true } return false }) for _, part := range parts { if !numUnderscoresOK(part) { p.panicItemf(it, "Invalid float %q: underscores must be surrounded by digits", it.val) } } if len(parts) > 0 && numHasLeadingZero(parts[0]) { p.panicItemf(it, "Invalid float %q: cannot have leading zeroes", it.val) } if !numPeriodsOK(it.val) { // As a special case, numbers like '123.' or '1.e2', // which are valid as far as Go/strconv are concerned, // must be rejected because TOML says that a fractional // part consists of '.' followed by 1+ digits. p.panicItemf(it, "Invalid float %q: '.' must be followed by one or more digits", it.val) } val := strings.Replace(it.val, "_", "", -1) if val == "+nan" || val == "-nan" { // Go doesn't support this, but TOML spec does. val = "nan" } num, err := strconv.ParseFloat(val, 64) if err != nil { if e, ok := err.(*strconv.NumError); ok && e.Err == strconv.ErrRange { p.panicErr(it, errParseRange{i: it.val, size: "float64"}) } else { p.panicItemf(it, "Invalid float value: %q", it.val) } } return num, p.typeOfPrimitive(it) } var dtTypes = []struct { fmt string zone *time.Location }{ {time.RFC3339Nano, time.Local}, {"2006-01-02T15:04:05.999999999", internal.LocalDatetime}, {"2006-01-02", internal.LocalDate}, {"15:04:05.999999999", internal.LocalTime}, } func (p *parser) valueDatetime(it item) (interface{}, tomlType) { it.val = datetimeRepl.Replace(it.val) var ( t time.Time ok bool err error ) for _, dt := range dtTypes { t, err = time.ParseInLocation(dt.fmt, it.val, dt.zone) if err == nil { ok = true break } } if !ok { p.panicItemf(it, "Invalid TOML Datetime: %q.", it.val) } return t, p.typeOfPrimitive(it) } func (p *parser) valueArray(it item) (interface{}, tomlType) { p.setType(p.currentKey, tomlArray, it.pos) var ( types []tomlType // Initialize to a non-nil empty slice. This makes it consistent with // how S = [] decodes into a non-nil slice inside something like struct // { S []string }. See #338 array = []interface{}{} ) for it = p.next(); it.typ != itemArrayEnd; it = p.next() { if it.typ == itemCommentStart { p.expect(itemText) continue } val, typ := p.value(it, true) array = append(array, val) types = append(types, typ) // XXX: types isn't used here, we need it to record the accurate type // information. // // Not entirely sure how to best store this; could use "key[0]", // "key[1]" notation, or maybe store it on the Array type? } return array, tomlArray } func (p *parser) valueInlineTable(it item, parentIsArray bool) (interface{}, tomlType) { var ( hash = make(map[string]interface{}) outerContext = p.context outerKey = p.currentKey ) p.context = append(p.context, p.currentKey) prevContext := p.context p.currentKey = "" p.addImplicit(p.context) p.addContext(p.context, parentIsArray) /// Loop over all table key/value pairs. for it := p.next(); it.typ != itemInlineTableEnd; it = p.next() { if it.typ == itemCommentStart { p.expect(itemText) continue } /// Read all key parts. k := p.nextPos() var key Key for ; k.typ != itemKeyEnd && k.typ != itemEOF; k = p.next() { key = append(key, p.keyString(k)) } p.assertEqual(itemKeyEnd, k.typ) /// The current key is the last part. p.currentKey = key[len(key)-1] /// All the other parts (if any) are the context; need to set each part /// as implicit. context := key[:len(key)-1] for i := range context { p.addImplicitContext(append(p.context, context[i:i+1]...)) } /// Set the value. val, typ := p.value(p.next(), false) p.set(p.currentKey, val, typ, it.pos) p.ordered = append(p.ordered, p.context.add(p.currentKey)) hash[p.currentKey] = val /// Restore context. p.context = prevContext } p.context = outerContext p.currentKey = outerKey return hash, tomlHash } // numHasLeadingZero checks if this number has leading zeroes, allowing for '0', // +/- signs, and base prefixes. func numHasLeadingZero(s string) bool { if len(s) > 1 && s[0] == '0' && !(s[1] == 'b' || s[1] == 'o' || s[1] == 'x') { // Allow 0b, 0o, 0x return true } if len(s) > 2 && (s[0] == '-' || s[0] == '+') && s[1] == '0' { return true } return false } // numUnderscoresOK checks whether each underscore in s is surrounded by // characters that are not underscores. func numUnderscoresOK(s string) bool { switch s { case "nan", "+nan", "-nan", "inf", "-inf", "+inf": return true } accept := false for _, r := range s { if r == '_' { if !accept { return false } } // isHexadecimal is a superset of all the permissable characters // surrounding an underscore. accept = isHexadecimal(r) } return accept } // numPeriodsOK checks whether every period in s is followed by a digit. func numPeriodsOK(s string) bool { period := false for _, r := range s { if period && !isDigit(r) { return false } period = r == '.' } return !period } // Set the current context of the parser, where the context is either a hash or // an array of hashes, depending on the value of the `array` parameter. // // Establishing the context also makes sure that the key isn't a duplicate, and // will create implicit hashes automatically. func (p *parser) addContext(key Key, array bool) { var ok bool // Always start at the top level and drill down for our context. hashContext := p.mapping keyContext := make(Key, 0) // We only need implicit hashes for key[0:-1] for _, k := range key[0 : len(key)-1] { _, ok = hashContext[k] keyContext = append(keyContext, k) // No key? Make an implicit hash and move on. if !ok { p.addImplicit(keyContext) hashContext[k] = make(map[string]interface{}) } // If the hash context is actually an array of tables, then set // the hash context to the last element in that array. // // Otherwise, it better be a table, since this MUST be a key group (by // virtue of it not being the last element in a key). switch t := hashContext[k].(type) { case []map[string]interface{}: hashContext = t[len(t)-1] case map[string]interface{}: hashContext = t default: p.panicf("Key '%s' was already created as a hash.", keyContext) } } p.context = keyContext if array { // If this is the first element for this array, then allocate a new // list of tables for it. k := key[len(key)-1] if _, ok := hashContext[k]; !ok { hashContext[k] = make([]map[string]interface{}, 0, 4) } // Add a new table. But make sure the key hasn't already been used // for something else. if hash, ok := hashContext[k].([]map[string]interface{}); ok { hashContext[k] = append(hash, make(map[string]interface{})) } else { p.panicf("Key '%s' was already created and cannot be used as an array.", key) } } else { p.setValue(key[len(key)-1], make(map[string]interface{})) } p.context = append(p.context, key[len(key)-1]) } // set calls setValue and setType. func (p *parser) set(key string, val interface{}, typ tomlType, pos Position) { p.setValue(key, val) p.setType(key, typ, pos) } // setValue sets the given key to the given value in the current context. // It will make sure that the key hasn't already been defined, account for // implicit key groups. func (p *parser) setValue(key string, value interface{}) { var ( tmpHash interface{} ok bool hash = p.mapping keyContext Key ) for _, k := range p.context { keyContext = append(keyContext, k) if tmpHash, ok = hash[k]; !ok { p.bug("Context for key '%s' has not been established.", keyContext) } switch t := tmpHash.(type) { case []map[string]interface{}: // The context is a table of hashes. Pick the most recent table // defined as the current hash. hash = t[len(t)-1] case map[string]interface{}: hash = t default: p.panicf("Key '%s' has already been defined.", keyContext) } } keyContext = append(keyContext, key) if _, ok := hash[key]; ok { // Normally redefining keys isn't allowed, but the key could have been // defined implicitly and it's allowed to be redefined concretely. (See // the `valid/implicit-and-explicit-after.toml` in toml-test) // // But we have to make sure to stop marking it as an implicit. (So that // another redefinition provokes an error.) // // Note that since it has already been defined (as a hash), we don't // want to overwrite it. So our business is done. if p.isArray(keyContext) { p.removeImplicit(keyContext) hash[key] = value return } if p.isImplicit(keyContext) { p.removeImplicit(keyContext) return } // Otherwise, we have a concrete key trying to override a previous // key, which is *always* wrong. p.panicf("Key '%s' has already been defined.", keyContext) } hash[key] = value } // setType sets the type of a particular value at a given key. It should be // called immediately AFTER setValue. // // Note that if `key` is empty, then the type given will be applied to the // current context (which is either a table or an array of tables). func (p *parser) setType(key string, typ tomlType, pos Position) { keyContext := make(Key, 0, len(p.context)+1) keyContext = append(keyContext, p.context...) if len(key) > 0 { // allow type setting for hashes keyContext = append(keyContext, key) } // Special case to make empty keys ("" = 1) work. // Without it it will set "" rather than `""`. // TODO: why is this needed? And why is this only needed here? if len(keyContext) == 0 { keyContext = Key{""} } p.keyInfo[keyContext.String()] = keyInfo{tomlType: typ, pos: pos} } // Implicit keys need to be created when tables are implied in "a.b.c.d = 1" and // "[a.b.c]" (the "a", "b", and "c" hashes are never created explicitly). func (p *parser) addImplicit(key Key) { p.implicits[key.String()] = struct{}{} } func (p *parser) removeImplicit(key Key) { delete(p.implicits, key.String()) } func (p *parser) isImplicit(key Key) bool { _, ok := p.implicits[key.String()]; return ok } func (p *parser) isArray(key Key) bool { return p.keyInfo[key.String()].tomlType == tomlArray } func (p *parser) addImplicitContext(key Key) { p.addImplicit(key) p.addContext(key, false) } // current returns the full key name of the current context. func (p *parser) current() string { if len(p.currentKey) == 0 { return p.context.String() } if len(p.context) == 0 { return p.currentKey } return fmt.Sprintf("%s.%s", p.context, p.currentKey) } func stripFirstNewline(s string) string { if len(s) > 0 && s[0] == '\n' { return s[1:] } if len(s) > 1 && s[0] == '\r' && s[1] == '\n' { return s[2:] } return s } // Remove newlines inside triple-quoted strings if a line ends with "\". func (p *parser) stripEscapedNewlines(s string) string { split := strings.Split(s, "\n") if len(split) < 1 { return s } escNL := false // Keep track of the last non-blank line was escaped. for i, line := range split { line = strings.TrimRight(line, " \t\r") if len(line) == 0 || line[len(line)-1] != '\\' { split[i] = strings.TrimRight(split[i], "\r") if !escNL && i != len(split)-1 { split[i] += "\n" } continue } escBS := true for j := len(line) - 1; j >= 0 && line[j] == '\\'; j-- { escBS = !escBS } if escNL { line = strings.TrimLeft(line, " \t\r") } escNL = !escBS if escBS { split[i] += "\n" continue } if i == len(split)-1 { p.panicf("invalid escape: '\\ '") } split[i] = line[:len(line)-1] // Remove \ if len(split)-1 > i { split[i+1] = strings.TrimLeft(split[i+1], " \t\r") } } return strings.Join(split, "") } func (p *parser) replaceEscapes(it item, str string) string { replaced := make([]rune, 0, len(str)) s := []byte(str) r := 0 for r < len(s) { if s[r] != '\\' { c, size := utf8.DecodeRune(s[r:]) r += size replaced = append(replaced, c) continue } r += 1 if r >= len(s) { p.bug("Escape sequence at end of string.") return "" } switch s[r] { default: p.bug("Expected valid escape code after \\, but got %q.", s[r]) case ' ', '\t': p.panicItemf(it, "invalid escape: '\\%c'", s[r]) case 'b': replaced = append(replaced, rune(0x0008)) r += 1 case 't': replaced = append(replaced, rune(0x0009)) r += 1 case 'n': replaced = append(replaced, rune(0x000A)) r += 1 case 'f': replaced = append(replaced, rune(0x000C)) r += 1 case 'r': replaced = append(replaced, rune(0x000D)) r += 1 case '"': replaced = append(replaced, rune(0x0022)) r += 1 case '\\': replaced = append(replaced, rune(0x005C)) r += 1 case 'u': // At this point, we know we have a Unicode escape of the form // `uXXXX` at [r, r+5). (Because the lexer guarantees this // for us.) escaped := p.asciiEscapeToUnicode(it, s[r+1:r+5]) replaced = append(replaced, escaped) r += 5 case 'U': // At this point, we know we have a Unicode escape of the form // `uXXXX` at [r, r+9). (Because the lexer guarantees this // for us.) escaped := p.asciiEscapeToUnicode(it, s[r+1:r+9]) replaced = append(replaced, escaped) r += 9 } } return string(replaced) } func (p *parser) asciiEscapeToUnicode(it item, bs []byte) rune { s := string(bs) hex, err := strconv.ParseUint(strings.ToLower(s), 16, 32) if err != nil { p.bug("Could not parse '%s' as a hexadecimal number, but the lexer claims it's OK: %s", s, err) } if !utf8.ValidRune(rune(hex)) { p.panicItemf(it, "Escaped character '\\u%s' is not valid UTF-8.", s) } return rune(hex) } ================================================ FILE: vendor/github.com/BurntSushi/toml/type_fields.go ================================================ package toml // Struct field handling is adapted from code in encoding/json: // // Copyright 2010 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the Go distribution. import ( "reflect" "sort" "sync" ) // A field represents a single field found in a struct. type field struct { name string // the name of the field (`toml` tag included) tag bool // whether field has a `toml` tag index []int // represents the depth of an anonymous field typ reflect.Type // the type of the field } // byName sorts field by name, breaking ties with depth, // then breaking ties with "name came from toml tag", then // breaking ties with index sequence. type byName []field func (x byName) Len() int { return len(x) } func (x byName) Swap(i, j int) { x[i], x[j] = x[j], x[i] } func (x byName) Less(i, j int) bool { if x[i].name != x[j].name { return x[i].name < x[j].name } if len(x[i].index) != len(x[j].index) { return len(x[i].index) < len(x[j].index) } if x[i].tag != x[j].tag { return x[i].tag } return byIndex(x).Less(i, j) } // byIndex sorts field by index sequence. type byIndex []field func (x byIndex) Len() int { return len(x) } func (x byIndex) Swap(i, j int) { x[i], x[j] = x[j], x[i] } func (x byIndex) Less(i, j int) bool { for k, xik := range x[i].index { if k >= len(x[j].index) { return false } if xik != x[j].index[k] { return xik < x[j].index[k] } } return len(x[i].index) < len(x[j].index) } // typeFields returns a list of fields that TOML should recognize for the given // type. The algorithm is breadth-first search over the set of structs to // include - the top struct and then any reachable anonymous structs. func typeFields(t reflect.Type) []field { // Anonymous fields to explore at the current level and the next. current := []field{} next := []field{{typ: t}} // Count of queued names for current level and the next. var count map[reflect.Type]int var nextCount map[reflect.Type]int // Types already visited at an earlier level. visited := map[reflect.Type]bool{} // Fields found. var fields []field for len(next) > 0 { current, next = next, current[:0] count, nextCount = nextCount, map[reflect.Type]int{} for _, f := range current { if visited[f.typ] { continue } visited[f.typ] = true // Scan f.typ for fields to include. for i := 0; i < f.typ.NumField(); i++ { sf := f.typ.Field(i) if sf.PkgPath != "" && !sf.Anonymous { // unexported continue } opts := getOptions(sf.Tag) if opts.skip { continue } index := make([]int, len(f.index)+1) copy(index, f.index) index[len(f.index)] = i ft := sf.Type if ft.Name() == "" && ft.Kind() == reflect.Ptr { // Follow pointer. ft = ft.Elem() } // Record found field and index sequence. if opts.name != "" || !sf.Anonymous || ft.Kind() != reflect.Struct { tagged := opts.name != "" name := opts.name if name == "" { name = sf.Name } fields = append(fields, field{name, tagged, index, ft}) if count[f.typ] > 1 { // If there were multiple instances, add a second, // so that the annihilation code will see a duplicate. // It only cares about the distinction between 1 or 2, // so don't bother generating any more copies. fields = append(fields, fields[len(fields)-1]) } continue } // Record new anonymous struct to explore in next round. nextCount[ft]++ if nextCount[ft] == 1 { f := field{name: ft.Name(), index: index, typ: ft} next = append(next, f) } } } } sort.Sort(byName(fields)) // Delete all fields that are hidden by the Go rules for embedded fields, // except that fields with TOML tags are promoted. // The fields are sorted in primary order of name, secondary order // of field index length. Loop over names; for each name, delete // hidden fields by choosing the one dominant field that survives. out := fields[:0] for advance, i := 0, 0; i < len(fields); i += advance { // One iteration per name. // Find the sequence of fields with the name of this first field. fi := fields[i] name := fi.name for advance = 1; i+advance < len(fields); advance++ { fj := fields[i+advance] if fj.name != name { break } } if advance == 1 { // Only one field with this name out = append(out, fi) continue } dominant, ok := dominantField(fields[i : i+advance]) if ok { out = append(out, dominant) } } fields = out sort.Sort(byIndex(fields)) return fields } // dominantField looks through the fields, all of which are known to // have the same name, to find the single field that dominates the // others using Go's embedding rules, modified by the presence of // TOML tags. If there are multiple top-level fields, the boolean // will be false: This condition is an error in Go and we skip all // the fields. func dominantField(fields []field) (field, bool) { // The fields are sorted in increasing index-length order. The winner // must therefore be one with the shortest index length. Drop all // longer entries, which is easy: just truncate the slice. length := len(fields[0].index) tagged := -1 // Index of first tagged field. for i, f := range fields { if len(f.index) > length { fields = fields[:i] break } if f.tag { if tagged >= 0 { // Multiple tagged fields at the same level: conflict. // Return no field. return field{}, false } tagged = i } } if tagged >= 0 { return fields[tagged], true } // All remaining fields have the same length. If there's more than one, // we have a conflict (two fields named "X" at the same level) and we // return no field. if len(fields) > 1 { return field{}, false } return fields[0], true } var fieldCache struct { sync.RWMutex m map[reflect.Type][]field } // cachedTypeFields is like typeFields but uses a cache to avoid repeated work. func cachedTypeFields(t reflect.Type) []field { fieldCache.RLock() f := fieldCache.m[t] fieldCache.RUnlock() if f != nil { return f } // Compute fields without lock. // Might duplicate effort but won't hold other computations back. f = typeFields(t) if f == nil { f = []field{} } fieldCache.Lock() if fieldCache.m == nil { fieldCache.m = map[reflect.Type][]field{} } fieldCache.m[t] = f fieldCache.Unlock() return f } ================================================ FILE: vendor/github.com/BurntSushi/toml/type_toml.go ================================================ package toml // tomlType represents any Go type that corresponds to a TOML type. // While the first draft of the TOML spec has a simplistic type system that // probably doesn't need this level of sophistication, we seem to be militating // toward adding real composite types. type tomlType interface { typeString() string } // typeEqual accepts any two types and returns true if they are equal. func typeEqual(t1, t2 tomlType) bool { if t1 == nil || t2 == nil { return false } return t1.typeString() == t2.typeString() } func typeIsTable(t tomlType) bool { return typeEqual(t, tomlHash) || typeEqual(t, tomlArrayHash) } type tomlBaseType string func (btype tomlBaseType) typeString() string { return string(btype) } func (btype tomlBaseType) String() string { return btype.typeString() } var ( tomlInteger tomlBaseType = "Integer" tomlFloat tomlBaseType = "Float" tomlDatetime tomlBaseType = "Datetime" tomlString tomlBaseType = "String" tomlBool tomlBaseType = "Bool" tomlArray tomlBaseType = "Array" tomlHash tomlBaseType = "Hash" tomlArrayHash tomlBaseType = "ArrayHash" ) // typeOfPrimitive returns a tomlType of any primitive value in TOML. // Primitive values are: Integer, Float, Datetime, String and Bool. // // Passing a lexer item other than the following will cause a BUG message // to occur: itemString, itemBool, itemInteger, itemFloat, itemDatetime. func (p *parser) typeOfPrimitive(lexItem item) tomlType { switch lexItem.typ { case itemInteger: return tomlInteger case itemFloat: return tomlFloat case itemDatetime: return tomlDatetime case itemString: return tomlString case itemMultilineString: return tomlString case itemRawString: return tomlString case itemRawMultilineString: return tomlString case itemBool: return tomlBool } p.bug("Cannot infer primitive type of lex item '%s'.", lexItem) panic("unreachable") } ================================================ FILE: vendor/github.com/beorn7/perks/LICENSE ================================================ Copyright (C) 2013 Blake Mizerany Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: vendor/github.com/beorn7/perks/quantile/exampledata.txt ================================================ 8 5 26 12 5 235 13 6 28 30 3 3 3 3 5 2 33 7 2 4 7 12 14 5 8 3 10 4 5 3 6 6 209 20 3 10 14 3 4 6 8 5 11 7 3 2 3 3 212 5 222 4 10 10 5 6 3 8 3 10 254 220 2 3 5 24 5 4 222 7 3 3 223 8 15 12 14 14 3 2 2 3 13 3 11 4 4 6 5 7 13 5 3 5 2 5 3 5 2 7 15 17 14 3 6 6 3 17 5 4 7 6 4 4 8 6 8 3 9 3 6 3 4 5 3 3 660 4 6 10 3 6 3 2 5 13 2 4 4 10 4 8 4 3 7 9 9 3 10 37 3 13 4 12 3 6 10 8 5 21 2 3 8 3 2 3 3 4 12 2 4 8 8 4 3 2 20 1 6 32 2 11 6 18 3 8 11 3 212 3 4 2 6 7 12 11 3 2 16 10 6 4 6 3 2 7 3 2 2 2 2 5 6 4 3 10 3 4 6 5 3 4 4 5 6 4 3 4 4 5 7 5 5 3 2 7 2 4 12 4 5 6 2 4 4 8 4 15 13 7 16 5 3 23 5 5 7 3 2 9 8 7 5 8 11 4 10 76 4 47 4 3 2 7 4 2 3 37 10 4 2 20 5 4 4 10 10 4 3 7 23 240 7 13 5 5 3 3 2 5 4 2 8 7 19 2 23 8 7 2 5 3 8 3 8 13 5 5 5 2 3 23 4 9 8 4 3 3 5 220 2 3 4 6 14 3 53 6 2 5 18 6 3 219 6 5 2 5 3 6 5 15 4 3 17 3 2 4 7 2 3 3 4 4 3 2 664 6 3 23 5 5 16 5 8 2 4 2 24 12 3 2 3 5 8 3 5 4 3 14 3 5 8 2 3 7 9 4 2 3 6 8 4 3 4 6 5 3 3 6 3 19 4 4 6 3 6 3 5 22 5 4 4 3 8 11 4 9 7 6 13 4 4 4 6 17 9 3 3 3 4 3 221 5 11 3 4 2 12 6 3 5 7 5 7 4 9 7 14 37 19 217 16 3 5 2 2 7 19 7 6 7 4 24 5 11 4 7 7 9 13 3 4 3 6 28 4 4 5 5 2 5 6 4 4 6 10 5 4 3 2 3 3 6 5 5 4 3 2 3 7 4 6 18 16 8 16 4 5 8 6 9 13 1545 6 215 6 5 6 3 45 31 5 2 2 4 3 3 2 5 4 3 5 7 7 4 5 8 5 4 749 2 31 9 11 2 11 5 4 4 7 9 11 4 5 4 7 3 4 6 2 15 3 4 3 4 3 5 2 13 5 5 3 3 23 4 4 5 7 4 13 2 4 3 4 2 6 2 7 3 5 5 3 29 5 4 4 3 10 2 3 79 16 6 6 7 7 3 5 5 7 4 3 7 9 5 6 5 9 6 3 6 4 17 2 10 9 3 6 2 3 21 22 5 11 4 2 17 2 224 2 14 3 4 4 2 4 4 4 4 5 3 4 4 10 2 6 3 3 5 7 2 7 5 6 3 218 2 2 5 2 6 3 5 222 14 6 33 3 2 5 3 3 3 9 5 3 3 2 7 4 3 4 3 5 6 5 26 4 13 9 7 3 221 3 3 4 4 4 4 2 18 5 3 7 9 6 8 3 10 3 11 9 5 4 17 5 5 6 6 3 2 4 12 17 6 7 218 4 2 4 10 3 5 15 3 9 4 3 3 6 29 3 3 4 5 5 3 8 5 6 6 7 5 3 5 3 29 2 31 5 15 24 16 5 207 4 3 3 2 15 4 4 13 5 5 4 6 10 2 7 8 4 6 20 5 3 4 3 12 12 5 17 7 3 3 3 6 10 3 5 25 80 4 9 3 2 11 3 3 2 3 8 7 5 5 19 5 3 3 12 11 2 6 5 5 5 3 3 3 4 209 14 3 2 5 19 4 4 3 4 14 5 6 4 13 9 7 4 7 10 2 9 5 7 2 8 4 6 5 5 222 8 7 12 5 216 3 4 4 6 3 14 8 7 13 4 3 3 3 3 17 5 4 3 33 6 6 33 7 5 3 8 7 5 2 9 4 2 233 24 7 4 8 10 3 4 15 2 16 3 3 13 12 7 5 4 207 4 2 4 27 15 2 5 2 25 6 5 5 6 13 6 18 6 4 12 225 10 7 5 2 2 11 4 14 21 8 10 3 5 4 232 2 5 5 3 7 17 11 6 6 23 4 6 3 5 4 2 17 3 6 5 8 3 2 2 14 9 4 4 2 5 5 3 7 6 12 6 10 3 6 2 2 19 5 4 4 9 2 4 13 3 5 6 3 6 5 4 9 6 3 5 7 3 6 6 4 3 10 6 3 221 3 5 3 6 4 8 5 3 6 4 4 2 54 5 6 11 3 3 4 4 4 3 7 3 11 11 7 10 6 13 223 213 15 231 7 3 7 228 2 3 4 4 5 6 7 4 13 3 4 5 3 6 4 6 7 2 4 3 4 3 3 6 3 7 3 5 18 5 6 8 10 3 3 3 2 4 2 4 4 5 6 6 4 10 13 3 12 5 12 16 8 4 19 11 2 4 5 6 8 5 6 4 18 10 4 2 216 6 6 6 2 4 12 8 3 11 5 6 14 5 3 13 4 5 4 5 3 28 6 3 7 219 3 9 7 3 10 6 3 4 19 5 7 11 6 15 19 4 13 11 3 7 5 10 2 8 11 2 6 4 6 24 6 3 3 3 3 6 18 4 11 4 2 5 10 8 3 9 5 3 4 5 6 2 5 7 4 4 14 6 4 4 5 5 7 2 4 3 7 3 3 6 4 5 4 4 4 3 3 3 3 8 14 2 3 5 3 2 4 5 3 7 3 3 18 3 4 4 5 7 3 3 3 13 5 4 8 211 5 5 3 5 2 5 4 2 655 6 3 5 11 2 5 3 12 9 15 11 5 12 217 2 6 17 3 3 207 5 5 4 5 9 3 2 8 5 4 3 2 5 12 4 14 5 4 2 13 5 8 4 225 4 3 4 5 4 3 3 6 23 9 2 6 7 233 4 4 6 18 3 4 6 3 4 4 2 3 7 4 13 227 4 3 5 4 2 12 9 17 3 7 14 6 4 5 21 4 8 9 2 9 25 16 3 6 4 7 8 5 2 3 5 4 3 3 5 3 3 3 2 3 19 2 4 3 4 2 3 4 4 2 4 3 3 3 2 6 3 17 5 6 4 3 13 5 3 3 3 4 9 4 2 14 12 4 5 24 4 3 37 12 11 21 3 4 3 13 4 2 3 15 4 11 4 4 3 8 3 4 4 12 8 5 3 3 4 2 220 3 5 223 3 3 3 10 3 15 4 241 9 7 3 6 6 23 4 13 7 3 4 7 4 9 3 3 4 10 5 5 1 5 24 2 4 5 5 6 14 3 8 2 3 5 13 13 3 5 2 3 15 3 4 2 10 4 4 4 5 5 3 5 3 4 7 4 27 3 6 4 15 3 5 6 6 5 4 8 3 9 2 6 3 4 3 7 4 18 3 11 3 3 8 9 7 24 3 219 7 10 4 5 9 12 2 5 4 4 4 3 3 19 5 8 16 8 6 22 3 23 3 242 9 4 3 3 5 7 3 3 5 8 3 7 5 14 8 10 3 4 3 7 4 6 7 4 10 4 3 11 3 7 10 3 13 6 8 12 10 5 7 9 3 4 7 7 10 8 30 9 19 4 3 19 15 4 13 3 215 223 4 7 4 8 17 16 3 7 6 5 5 4 12 3 7 4 4 13 4 5 2 5 6 5 6 6 7 10 18 23 9 3 3 6 5 2 4 2 7 3 3 2 5 5 14 10 224 6 3 4 3 7 5 9 3 6 4 2 5 11 4 3 3 2 8 4 7 4 10 7 3 3 18 18 17 3 3 3 4 5 3 3 4 12 7 3 11 13 5 4 7 13 5 4 11 3 12 3 6 4 4 21 4 6 9 5 3 10 8 4 6 4 4 6 5 4 8 6 4 6 4 4 5 9 6 3 4 2 9 3 18 2 4 3 13 3 6 6 8 7 9 3 2 16 3 4 6 3 2 33 22 14 4 9 12 4 5 6 3 23 9 4 3 5 5 3 4 5 3 5 3 10 4 5 5 8 4 4 6 8 5 4 3 4 6 3 3 3 5 9 12 6 5 9 3 5 3 2 2 2 18 3 2 21 2 5 4 6 4 5 10 3 9 3 2 10 7 3 6 6 4 4 8 12 7 3 7 3 3 9 3 4 5 4 4 5 5 10 15 4 4 14 6 227 3 14 5 216 22 5 4 2 2 6 3 4 2 9 9 4 3 28 13 11 4 5 3 3 2 3 3 5 3 4 3 5 23 26 3 4 5 6 4 6 3 5 5 3 4 3 2 2 2 7 14 3 6 7 17 2 2 15 14 16 4 6 7 13 6 4 5 6 16 3 3 28 3 6 15 3 9 2 4 6 3 3 22 4 12 6 7 2 5 4 10 3 16 6 9 2 5 12 7 5 5 5 5 2 11 9 17 4 3 11 7 3 5 15 4 3 4 211 8 7 5 4 7 6 7 6 3 6 5 6 5 3 4 4 26 4 6 10 4 4 3 2 3 3 4 5 9 3 9 4 4 5 5 8 2 4 2 3 8 4 11 19 5 8 6 3 5 6 12 3 2 4 16 12 3 4 4 8 6 5 6 6 219 8 222 6 16 3 13 19 5 4 3 11 6 10 4 7 7 12 5 3 3 5 6 10 3 8 2 5 4 7 2 4 4 2 12 9 6 4 2 40 2 4 10 4 223 4 2 20 6 7 24 5 4 5 2 20 16 6 5 13 2 3 3 19 3 2 4 5 6 7 11 12 5 6 7 7 3 5 3 5 3 14 3 4 4 2 11 1 7 3 9 6 11 12 5 8 6 221 4 2 12 4 3 15 4 5 226 7 218 7 5 4 5 18 4 5 9 4 4 2 9 18 18 9 5 6 6 3 3 7 3 5 4 4 4 12 3 6 31 5 4 7 3 6 5 6 5 11 2 2 11 11 6 7 5 8 7 10 5 23 7 4 3 5 34 2 5 23 7 3 6 8 4 4 4 2 5 3 8 5 4 8 25 2 3 17 8 3 4 8 7 3 15 6 5 7 21 9 5 6 6 5 3 2 3 10 3 6 3 14 7 4 4 8 7 8 2 6 12 4 213 6 5 21 8 2 5 23 3 11 2 3 6 25 2 3 6 7 6 6 4 4 6 3 17 9 7 6 4 3 10 7 2 3 3 3 11 8 3 7 6 4 14 36 3 4 3 3 22 13 21 4 2 7 4 4 17 15 3 7 11 2 4 7 6 209 6 3 2 2 24 4 9 4 3 3 3 29 2 2 4 3 3 5 4 6 3 3 2 4 ================================================ FILE: vendor/github.com/beorn7/perks/quantile/stream.go ================================================ // Package quantile computes approximate quantiles over an unbounded data // stream within low memory and CPU bounds. // // A small amount of accuracy is traded to achieve the above properties. // // Multiple streams can be merged before calling Query to generate a single set // of results. This is meaningful when the streams represent the same type of // data. See Merge and Samples. // // For more detailed information about the algorithm used, see: // // Effective Computation of Biased Quantiles over Data Streams // // http://www.cs.rutgers.edu/~muthu/bquant.pdf package quantile import ( "math" "sort" ) // Sample holds an observed value and meta information for compression. JSON // tags have been added for convenience. type Sample struct { Value float64 `json:",string"` Width float64 `json:",string"` Delta float64 `json:",string"` } // Samples represents a slice of samples. It implements sort.Interface. type Samples []Sample func (a Samples) Len() int { return len(a) } func (a Samples) Less(i, j int) bool { return a[i].Value < a[j].Value } func (a Samples) Swap(i, j int) { a[i], a[j] = a[j], a[i] } type invariant func(s *stream, r float64) float64 // NewLowBiased returns an initialized Stream for low-biased quantiles // (e.g. 0.01, 0.1, 0.5) where the needed quantiles are not known a priori, but // error guarantees can still be given even for the lower ranks of the data // distribution. // // The provided epsilon is a relative error, i.e. the true quantile of a value // returned by a query is guaranteed to be within (1±Epsilon)*Quantile. // // See http://www.cs.rutgers.edu/~muthu/bquant.pdf for time, space, and error // properties. func NewLowBiased(epsilon float64) *Stream { ƒ := func(s *stream, r float64) float64 { return 2 * epsilon * r } return newStream(ƒ) } // NewHighBiased returns an initialized Stream for high-biased quantiles // (e.g. 0.01, 0.1, 0.5) where the needed quantiles are not known a priori, but // error guarantees can still be given even for the higher ranks of the data // distribution. // // The provided epsilon is a relative error, i.e. the true quantile of a value // returned by a query is guaranteed to be within 1-(1±Epsilon)*(1-Quantile). // // See http://www.cs.rutgers.edu/~muthu/bquant.pdf for time, space, and error // properties. func NewHighBiased(epsilon float64) *Stream { ƒ := func(s *stream, r float64) float64 { return 2 * epsilon * (s.n - r) } return newStream(ƒ) } // NewTargeted returns an initialized Stream concerned with a particular set of // quantile values that are supplied a priori. Knowing these a priori reduces // space and computation time. The targets map maps the desired quantiles to // their absolute errors, i.e. the true quantile of a value returned by a query // is guaranteed to be within (Quantile±Epsilon). // // See http://www.cs.rutgers.edu/~muthu/bquant.pdf for time, space, and error properties. func NewTargeted(targetMap map[float64]float64) *Stream { // Convert map to slice to avoid slow iterations on a map. // ƒ is called on the hot path, so converting the map to a slice // beforehand results in significant CPU savings. targets := targetMapToSlice(targetMap) ƒ := func(s *stream, r float64) float64 { var m = math.MaxFloat64 var f float64 for _, t := range targets { if t.quantile*s.n <= r { f = (2 * t.epsilon * r) / t.quantile } else { f = (2 * t.epsilon * (s.n - r)) / (1 - t.quantile) } if f < m { m = f } } return m } return newStream(ƒ) } type target struct { quantile float64 epsilon float64 } func targetMapToSlice(targetMap map[float64]float64) []target { targets := make([]target, 0, len(targetMap)) for quantile, epsilon := range targetMap { t := target{ quantile: quantile, epsilon: epsilon, } targets = append(targets, t) } return targets } // Stream computes quantiles for a stream of float64s. It is not thread-safe by // design. Take care when using across multiple goroutines. type Stream struct { *stream b Samples sorted bool } func newStream(ƒ invariant) *Stream { x := &stream{ƒ: ƒ} return &Stream{x, make(Samples, 0, 500), true} } // Insert inserts v into the stream. func (s *Stream) Insert(v float64) { s.insert(Sample{Value: v, Width: 1}) } func (s *Stream) insert(sample Sample) { s.b = append(s.b, sample) s.sorted = false if len(s.b) == cap(s.b) { s.flush() } } // Query returns the computed qth percentiles value. If s was created with // NewTargeted, and q is not in the set of quantiles provided a priori, Query // will return an unspecified result. func (s *Stream) Query(q float64) float64 { if !s.flushed() { // Fast path when there hasn't been enough data for a flush; // this also yields better accuracy for small sets of data. l := len(s.b) if l == 0 { return 0 } i := int(math.Ceil(float64(l) * q)) if i > 0 { i -= 1 } s.maybeSort() return s.b[i].Value } s.flush() return s.stream.query(q) } // Merge merges samples into the underlying streams samples. This is handy when // merging multiple streams from separate threads, database shards, etc. // // ATTENTION: This method is broken and does not yield correct results. The // underlying algorithm is not capable of merging streams correctly. func (s *Stream) Merge(samples Samples) { sort.Sort(samples) s.stream.merge(samples) } // Reset reinitializes and clears the list reusing the samples buffer memory. func (s *Stream) Reset() { s.stream.reset() s.b = s.b[:0] } // Samples returns stream samples held by s. func (s *Stream) Samples() Samples { if !s.flushed() { return s.b } s.flush() return s.stream.samples() } // Count returns the total number of samples observed in the stream // since initialization. func (s *Stream) Count() int { return len(s.b) + s.stream.count() } func (s *Stream) flush() { s.maybeSort() s.stream.merge(s.b) s.b = s.b[:0] } func (s *Stream) maybeSort() { if !s.sorted { s.sorted = true sort.Sort(s.b) } } func (s *Stream) flushed() bool { return len(s.stream.l) > 0 } type stream struct { n float64 l []Sample ƒ invariant } func (s *stream) reset() { s.l = s.l[:0] s.n = 0 } func (s *stream) insert(v float64) { s.merge(Samples{{v, 1, 0}}) } func (s *stream) merge(samples Samples) { // TODO(beorn7): This tries to merge not only individual samples, but // whole summaries. The paper doesn't mention merging summaries at // all. Unittests show that the merging is inaccurate. Find out how to // do merges properly. var r float64 i := 0 for _, sample := range samples { for ; i < len(s.l); i++ { c := s.l[i] if c.Value > sample.Value { // Insert at position i. s.l = append(s.l, Sample{}) copy(s.l[i+1:], s.l[i:]) s.l[i] = Sample{ sample.Value, sample.Width, math.Max(sample.Delta, math.Floor(s.ƒ(s, r))-1), // TODO(beorn7): How to calculate delta correctly? } i++ goto inserted } r += c.Width } s.l = append(s.l, Sample{sample.Value, sample.Width, 0}) i++ inserted: s.n += sample.Width r += sample.Width } s.compress() } func (s *stream) count() int { return int(s.n) } func (s *stream) query(q float64) float64 { t := math.Ceil(q * s.n) t += math.Ceil(s.ƒ(s, t) / 2) p := s.l[0] var r float64 for _, c := range s.l[1:] { r += p.Width if r+c.Width+c.Delta > t { return p.Value } p = c } return p.Value } func (s *stream) compress() { if len(s.l) < 2 { return } x := s.l[len(s.l)-1] xi := len(s.l) - 1 r := s.n - 1 - x.Width for i := len(s.l) - 2; i >= 0; i-- { c := s.l[i] if c.Width+x.Width+x.Delta <= s.ƒ(s, r) { x.Width += c.Width s.l[xi] = x // Remove element at i. copy(s.l[i:], s.l[i+1:]) s.l = s.l[:len(s.l)-1] xi -= 1 } else { x = c xi = i } r -= c.Width } } func (s *stream) samples() Samples { samples := make(Samples, len(s.l)) copy(samples, s.l) return samples } ================================================ FILE: vendor/github.com/cespare/xxhash/v2/LICENSE.txt ================================================ Copyright (c) 2016 Caleb Spare MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: vendor/github.com/cespare/xxhash/v2/README.md ================================================ # xxhash [![Go Reference](https://pkg.go.dev/badge/github.com/cespare/xxhash/v2.svg)](https://pkg.go.dev/github.com/cespare/xxhash/v2) [![Test](https://github.com/cespare/xxhash/actions/workflows/test.yml/badge.svg)](https://github.com/cespare/xxhash/actions/workflows/test.yml) xxhash is a Go implementation of the 64-bit [xxHash] algorithm, XXH64. This is a high-quality hashing algorithm that is much faster than anything in the Go standard library. This package provides a straightforward API: ``` func Sum64(b []byte) uint64 func Sum64String(s string) uint64 type Digest struct{ ... } func New() *Digest ``` The `Digest` type implements hash.Hash64. Its key methods are: ``` func (*Digest) Write([]byte) (int, error) func (*Digest) WriteString(string) (int, error) func (*Digest) Sum64() uint64 ``` The package is written with optimized pure Go and also contains even faster assembly implementations for amd64 and arm64. If desired, the `purego` build tag opts into using the Go code even on those architectures. [xxHash]: http://cyan4973.github.io/xxHash/ ## Compatibility This package is in a module and the latest code is in version 2 of the module. You need a version of Go with at least "minimal module compatibility" to use github.com/cespare/xxhash/v2: * 1.9.7+ for Go 1.9 * 1.10.3+ for Go 1.10 * Go 1.11 or later I recommend using the latest release of Go. ## Benchmarks Here are some quick benchmarks comparing the pure-Go and assembly implementations of Sum64. | input size | purego | asm | | ---------- | --------- | --------- | | 4 B | 1.3 GB/s | 1.2 GB/s | | 16 B | 2.9 GB/s | 3.5 GB/s | | 100 B | 6.9 GB/s | 8.1 GB/s | | 4 KB | 11.7 GB/s | 16.7 GB/s | | 10 MB | 12.0 GB/s | 17.3 GB/s | These numbers were generated on Ubuntu 20.04 with an Intel Xeon Platinum 8252C CPU using the following commands under Go 1.19.2: ``` benchstat <(go test -tags purego -benchtime 500ms -count 15 -bench 'Sum64$') benchstat <(go test -benchtime 500ms -count 15 -bench 'Sum64$') ``` ## Projects using this package - [InfluxDB](https://github.com/influxdata/influxdb) - [Prometheus](https://github.com/prometheus/prometheus) - [VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics) - [FreeCache](https://github.com/coocood/freecache) - [FastCache](https://github.com/VictoriaMetrics/fastcache) - [Ristretto](https://github.com/dgraph-io/ristretto) - [Badger](https://github.com/dgraph-io/badger) ================================================ FILE: vendor/github.com/cespare/xxhash/v2/testall.sh ================================================ #!/bin/bash set -eu -o pipefail # Small convenience script for running the tests with various combinations of # arch/tags. This assumes we're running on amd64 and have qemu available. go test ./... go test -tags purego ./... GOARCH=arm64 go test GOARCH=arm64 go test -tags purego ================================================ FILE: vendor/github.com/cespare/xxhash/v2/xxhash.go ================================================ // Package xxhash implements the 64-bit variant of xxHash (XXH64) as described // at http://cyan4973.github.io/xxHash/. package xxhash import ( "encoding/binary" "errors" "math/bits" ) const ( prime1 uint64 = 11400714785074694791 prime2 uint64 = 14029467366897019727 prime3 uint64 = 1609587929392839161 prime4 uint64 = 9650029242287828579 prime5 uint64 = 2870177450012600261 ) // Store the primes in an array as well. // // The consts are used when possible in Go code to avoid MOVs but we need a // contiguous array for the assembly code. var primes = [...]uint64{prime1, prime2, prime3, prime4, prime5} // Digest implements hash.Hash64. // // Note that a zero-valued Digest is not ready to receive writes. // Call Reset or create a Digest using New before calling other methods. type Digest struct { v1 uint64 v2 uint64 v3 uint64 v4 uint64 total uint64 mem [32]byte n int // how much of mem is used } // New creates a new Digest with a zero seed. func New() *Digest { return NewWithSeed(0) } // NewWithSeed creates a new Digest with the given seed. func NewWithSeed(seed uint64) *Digest { var d Digest d.ResetWithSeed(seed) return &d } // Reset clears the Digest's state so that it can be reused. // It uses a seed value of zero. func (d *Digest) Reset() { d.ResetWithSeed(0) } // ResetWithSeed clears the Digest's state so that it can be reused. // It uses the given seed to initialize the state. func (d *Digest) ResetWithSeed(seed uint64) { d.v1 = seed + prime1 + prime2 d.v2 = seed + prime2 d.v3 = seed d.v4 = seed - prime1 d.total = 0 d.n = 0 } // Size always returns 8 bytes. func (d *Digest) Size() int { return 8 } // BlockSize always returns 32 bytes. func (d *Digest) BlockSize() int { return 32 } // Write adds more data to d. It always returns len(b), nil. func (d *Digest) Write(b []byte) (n int, err error) { n = len(b) d.total += uint64(n) memleft := d.mem[d.n&(len(d.mem)-1):] if d.n+n < 32 { // This new data doesn't even fill the current block. copy(memleft, b) d.n += n return } if d.n > 0 { // Finish off the partial block. c := copy(memleft, b) d.v1 = round(d.v1, u64(d.mem[0:8])) d.v2 = round(d.v2, u64(d.mem[8:16])) d.v3 = round(d.v3, u64(d.mem[16:24])) d.v4 = round(d.v4, u64(d.mem[24:32])) b = b[c:] d.n = 0 } if len(b) >= 32 { // One or more full blocks left. nw := writeBlocks(d, b) b = b[nw:] } // Store any remaining partial block. copy(d.mem[:], b) d.n = len(b) return } // Sum appends the current hash to b and returns the resulting slice. func (d *Digest) Sum(b []byte) []byte { s := d.Sum64() return append( b, byte(s>>56), byte(s>>48), byte(s>>40), byte(s>>32), byte(s>>24), byte(s>>16), byte(s>>8), byte(s), ) } // Sum64 returns the current hash. func (d *Digest) Sum64() uint64 { var h uint64 if d.total >= 32 { v1, v2, v3, v4 := d.v1, d.v2, d.v3, d.v4 h = rol1(v1) + rol7(v2) + rol12(v3) + rol18(v4) h = mergeRound(h, v1) h = mergeRound(h, v2) h = mergeRound(h, v3) h = mergeRound(h, v4) } else { h = d.v3 + prime5 } h += d.total b := d.mem[:d.n&(len(d.mem)-1)] for ; len(b) >= 8; b = b[8:] { k1 := round(0, u64(b[:8])) h ^= k1 h = rol27(h)*prime1 + prime4 } if len(b) >= 4 { h ^= uint64(u32(b[:4])) * prime1 h = rol23(h)*prime2 + prime3 b = b[4:] } for ; len(b) > 0; b = b[1:] { h ^= uint64(b[0]) * prime5 h = rol11(h) * prime1 } h ^= h >> 33 h *= prime2 h ^= h >> 29 h *= prime3 h ^= h >> 32 return h } const ( magic = "xxh\x06" marshaledSize = len(magic) + 8*5 + 32 ) // MarshalBinary implements the encoding.BinaryMarshaler interface. func (d *Digest) MarshalBinary() ([]byte, error) { b := make([]byte, 0, marshaledSize) b = append(b, magic...) b = appendUint64(b, d.v1) b = appendUint64(b, d.v2) b = appendUint64(b, d.v3) b = appendUint64(b, d.v4) b = appendUint64(b, d.total) b = append(b, d.mem[:d.n]...) b = b[:len(b)+len(d.mem)-d.n] return b, nil } // UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. func (d *Digest) UnmarshalBinary(b []byte) error { if len(b) < len(magic) || string(b[:len(magic)]) != magic { return errors.New("xxhash: invalid hash state identifier") } if len(b) != marshaledSize { return errors.New("xxhash: invalid hash state size") } b = b[len(magic):] b, d.v1 = consumeUint64(b) b, d.v2 = consumeUint64(b) b, d.v3 = consumeUint64(b) b, d.v4 = consumeUint64(b) b, d.total = consumeUint64(b) copy(d.mem[:], b) d.n = int(d.total % uint64(len(d.mem))) return nil } func appendUint64(b []byte, x uint64) []byte { var a [8]byte binary.LittleEndian.PutUint64(a[:], x) return append(b, a[:]...) } func consumeUint64(b []byte) ([]byte, uint64) { x := u64(b) return b[8:], x } func u64(b []byte) uint64 { return binary.LittleEndian.Uint64(b) } func u32(b []byte) uint32 { return binary.LittleEndian.Uint32(b) } func round(acc, input uint64) uint64 { acc += input * prime2 acc = rol31(acc) acc *= prime1 return acc } func mergeRound(acc, val uint64) uint64 { val = round(0, val) acc ^= val acc = acc*prime1 + prime4 return acc } func rol1(x uint64) uint64 { return bits.RotateLeft64(x, 1) } func rol7(x uint64) uint64 { return bits.RotateLeft64(x, 7) } func rol11(x uint64) uint64 { return bits.RotateLeft64(x, 11) } func rol12(x uint64) uint64 { return bits.RotateLeft64(x, 12) } func rol18(x uint64) uint64 { return bits.RotateLeft64(x, 18) } func rol23(x uint64) uint64 { return bits.RotateLeft64(x, 23) } func rol27(x uint64) uint64 { return bits.RotateLeft64(x, 27) } func rol31(x uint64) uint64 { return bits.RotateLeft64(x, 31) } ================================================ FILE: vendor/github.com/cespare/xxhash/v2/xxhash_amd64.s ================================================ //go:build !appengine && gc && !purego // +build !appengine // +build gc // +build !purego #include "textflag.h" // Registers: #define h AX #define d AX #define p SI // pointer to advance through b #define n DX #define end BX // loop end #define v1 R8 #define v2 R9 #define v3 R10 #define v4 R11 #define x R12 #define prime1 R13 #define prime2 R14 #define prime4 DI #define round(acc, x) \ IMULQ prime2, x \ ADDQ x, acc \ ROLQ $31, acc \ IMULQ prime1, acc // round0 performs the operation x = round(0, x). #define round0(x) \ IMULQ prime2, x \ ROLQ $31, x \ IMULQ prime1, x // mergeRound applies a merge round on the two registers acc and x. // It assumes that prime1, prime2, and prime4 have been loaded. #define mergeRound(acc, x) \ round0(x) \ XORQ x, acc \ IMULQ prime1, acc \ ADDQ prime4, acc // blockLoop processes as many 32-byte blocks as possible, // updating v1, v2, v3, and v4. It assumes that there is at least one block // to process. #define blockLoop() \ loop: \ MOVQ +0(p), x \ round(v1, x) \ MOVQ +8(p), x \ round(v2, x) \ MOVQ +16(p), x \ round(v3, x) \ MOVQ +24(p), x \ round(v4, x) \ ADDQ $32, p \ CMPQ p, end \ JLE loop // func Sum64(b []byte) uint64 TEXT ·Sum64(SB), NOSPLIT|NOFRAME, $0-32 // Load fixed primes. MOVQ ·primes+0(SB), prime1 MOVQ ·primes+8(SB), prime2 MOVQ ·primes+24(SB), prime4 // Load slice. MOVQ b_base+0(FP), p MOVQ b_len+8(FP), n LEAQ (p)(n*1), end // The first loop limit will be len(b)-32. SUBQ $32, end // Check whether we have at least one block. CMPQ n, $32 JLT noBlocks // Set up initial state (v1, v2, v3, v4). MOVQ prime1, v1 ADDQ prime2, v1 MOVQ prime2, v2 XORQ v3, v3 XORQ v4, v4 SUBQ prime1, v4 blockLoop() MOVQ v1, h ROLQ $1, h MOVQ v2, x ROLQ $7, x ADDQ x, h MOVQ v3, x ROLQ $12, x ADDQ x, h MOVQ v4, x ROLQ $18, x ADDQ x, h mergeRound(h, v1) mergeRound(h, v2) mergeRound(h, v3) mergeRound(h, v4) JMP afterBlocks noBlocks: MOVQ ·primes+32(SB), h afterBlocks: ADDQ n, h ADDQ $24, end CMPQ p, end JG try4 loop8: MOVQ (p), x ADDQ $8, p round0(x) XORQ x, h ROLQ $27, h IMULQ prime1, h ADDQ prime4, h CMPQ p, end JLE loop8 try4: ADDQ $4, end CMPQ p, end JG try1 MOVL (p), x ADDQ $4, p IMULQ prime1, x XORQ x, h ROLQ $23, h IMULQ prime2, h ADDQ ·primes+16(SB), h try1: ADDQ $4, end CMPQ p, end JGE finalize loop1: MOVBQZX (p), x ADDQ $1, p IMULQ ·primes+32(SB), x XORQ x, h ROLQ $11, h IMULQ prime1, h CMPQ p, end JL loop1 finalize: MOVQ h, x SHRQ $33, x XORQ x, h IMULQ prime2, h MOVQ h, x SHRQ $29, x XORQ x, h IMULQ ·primes+16(SB), h MOVQ h, x SHRQ $32, x XORQ x, h MOVQ h, ret+24(FP) RET // func writeBlocks(d *Digest, b []byte) int TEXT ·writeBlocks(SB), NOSPLIT|NOFRAME, $0-40 // Load fixed primes needed for round. MOVQ ·primes+0(SB), prime1 MOVQ ·primes+8(SB), prime2 // Load slice. MOVQ b_base+8(FP), p MOVQ b_len+16(FP), n LEAQ (p)(n*1), end SUBQ $32, end // Load vN from d. MOVQ s+0(FP), d MOVQ 0(d), v1 MOVQ 8(d), v2 MOVQ 16(d), v3 MOVQ 24(d), v4 // We don't need to check the loop condition here; this function is // always called with at least one block of data to process. blockLoop() // Copy vN back to d. MOVQ v1, 0(d) MOVQ v2, 8(d) MOVQ v3, 16(d) MOVQ v4, 24(d) // The number of bytes written is p minus the old base pointer. SUBQ b_base+8(FP), p MOVQ p, ret+32(FP) RET ================================================ FILE: vendor/github.com/cespare/xxhash/v2/xxhash_arm64.s ================================================ //go:build !appengine && gc && !purego // +build !appengine // +build gc // +build !purego #include "textflag.h" // Registers: #define digest R1 #define h R2 // return value #define p R3 // input pointer #define n R4 // input length #define nblocks R5 // n / 32 #define prime1 R7 #define prime2 R8 #define prime3 R9 #define prime4 R10 #define prime5 R11 #define v1 R12 #define v2 R13 #define v3 R14 #define v4 R15 #define x1 R20 #define x2 R21 #define x3 R22 #define x4 R23 #define round(acc, x) \ MADD prime2, acc, x, acc \ ROR $64-31, acc \ MUL prime1, acc // round0 performs the operation x = round(0, x). #define round0(x) \ MUL prime2, x \ ROR $64-31, x \ MUL prime1, x #define mergeRound(acc, x) \ round0(x) \ EOR x, acc \ MADD acc, prime4, prime1, acc // blockLoop processes as many 32-byte blocks as possible, // updating v1, v2, v3, and v4. It assumes that n >= 32. #define blockLoop() \ LSR $5, n, nblocks \ PCALIGN $16 \ loop: \ LDP.P 16(p), (x1, x2) \ LDP.P 16(p), (x3, x4) \ round(v1, x1) \ round(v2, x2) \ round(v3, x3) \ round(v4, x4) \ SUB $1, nblocks \ CBNZ nblocks, loop // func Sum64(b []byte) uint64 TEXT ·Sum64(SB), NOSPLIT|NOFRAME, $0-32 LDP b_base+0(FP), (p, n) LDP ·primes+0(SB), (prime1, prime2) LDP ·primes+16(SB), (prime3, prime4) MOVD ·primes+32(SB), prime5 CMP $32, n CSEL LT, prime5, ZR, h // if n < 32 { h = prime5 } else { h = 0 } BLT afterLoop ADD prime1, prime2, v1 MOVD prime2, v2 MOVD $0, v3 NEG prime1, v4 blockLoop() ROR $64-1, v1, x1 ROR $64-7, v2, x2 ADD x1, x2 ROR $64-12, v3, x3 ROR $64-18, v4, x4 ADD x3, x4 ADD x2, x4, h mergeRound(h, v1) mergeRound(h, v2) mergeRound(h, v3) mergeRound(h, v4) afterLoop: ADD n, h TBZ $4, n, try8 LDP.P 16(p), (x1, x2) round0(x1) // NOTE: here and below, sequencing the EOR after the ROR (using a // rotated register) is worth a small but measurable speedup for small // inputs. ROR $64-27, h EOR x1 @> 64-27, h, h MADD h, prime4, prime1, h round0(x2) ROR $64-27, h EOR x2 @> 64-27, h, h MADD h, prime4, prime1, h try8: TBZ $3, n, try4 MOVD.P 8(p), x1 round0(x1) ROR $64-27, h EOR x1 @> 64-27, h, h MADD h, prime4, prime1, h try4: TBZ $2, n, try2 MOVWU.P 4(p), x2 MUL prime1, x2 ROR $64-23, h EOR x2 @> 64-23, h, h MADD h, prime3, prime2, h try2: TBZ $1, n, try1 MOVHU.P 2(p), x3 AND $255, x3, x1 LSR $8, x3, x2 MUL prime5, x1 ROR $64-11, h EOR x1 @> 64-11, h, h MUL prime1, h MUL prime5, x2 ROR $64-11, h EOR x2 @> 64-11, h, h MUL prime1, h try1: TBZ $0, n, finalize MOVBU (p), x4 MUL prime5, x4 ROR $64-11, h EOR x4 @> 64-11, h, h MUL prime1, h finalize: EOR h >> 33, h MUL prime2, h EOR h >> 29, h MUL prime3, h EOR h >> 32, h MOVD h, ret+24(FP) RET // func writeBlocks(d *Digest, b []byte) int TEXT ·writeBlocks(SB), NOSPLIT|NOFRAME, $0-40 LDP ·primes+0(SB), (prime1, prime2) // Load state. Assume v[1-4] are stored contiguously. MOVD d+0(FP), digest LDP 0(digest), (v1, v2) LDP 16(digest), (v3, v4) LDP b_base+8(FP), (p, n) blockLoop() // Store updated state. STP (v1, v2), 0(digest) STP (v3, v4), 16(digest) BIC $31, n MOVD n, ret+32(FP) RET ================================================ FILE: vendor/github.com/cespare/xxhash/v2/xxhash_asm.go ================================================ //go:build (amd64 || arm64) && !appengine && gc && !purego // +build amd64 arm64 // +build !appengine // +build gc // +build !purego package xxhash // Sum64 computes the 64-bit xxHash digest of b with a zero seed. // //go:noescape func Sum64(b []byte) uint64 //go:noescape func writeBlocks(d *Digest, b []byte) int ================================================ FILE: vendor/github.com/cespare/xxhash/v2/xxhash_other.go ================================================ //go:build (!amd64 && !arm64) || appengine || !gc || purego // +build !amd64,!arm64 appengine !gc purego package xxhash // Sum64 computes the 64-bit xxHash digest of b with a zero seed. func Sum64(b []byte) uint64 { // A simpler version would be // d := New() // d.Write(b) // return d.Sum64() // but this is faster, particularly for small inputs. n := len(b) var h uint64 if n >= 32 { v1 := primes[0] + prime2 v2 := prime2 v3 := uint64(0) v4 := -primes[0] for len(b) >= 32 { v1 = round(v1, u64(b[0:8:len(b)])) v2 = round(v2, u64(b[8:16:len(b)])) v3 = round(v3, u64(b[16:24:len(b)])) v4 = round(v4, u64(b[24:32:len(b)])) b = b[32:len(b):len(b)] } h = rol1(v1) + rol7(v2) + rol12(v3) + rol18(v4) h = mergeRound(h, v1) h = mergeRound(h, v2) h = mergeRound(h, v3) h = mergeRound(h, v4) } else { h = prime5 } h += uint64(n) for ; len(b) >= 8; b = b[8:] { k1 := round(0, u64(b[:8])) h ^= k1 h = rol27(h)*prime1 + prime4 } if len(b) >= 4 { h ^= uint64(u32(b[:4])) * prime1 h = rol23(h)*prime2 + prime3 b = b[4:] } for ; len(b) > 0; b = b[1:] { h ^= uint64(b[0]) * prime5 h = rol11(h) * prime1 } h ^= h >> 33 h *= prime2 h ^= h >> 29 h *= prime3 h ^= h >> 32 return h } func writeBlocks(d *Digest, b []byte) int { v1, v2, v3, v4 := d.v1, d.v2, d.v3, d.v4 n := len(b) for len(b) >= 32 { v1 = round(v1, u64(b[0:8:len(b)])) v2 = round(v2, u64(b[8:16:len(b)])) v3 = round(v3, u64(b[16:24:len(b)])) v4 = round(v4, u64(b[24:32:len(b)])) b = b[32:len(b):len(b)] } d.v1, d.v2, d.v3, d.v4 = v1, v2, v3, v4 return n - len(b) } ================================================ FILE: vendor/github.com/cespare/xxhash/v2/xxhash_safe.go ================================================ //go:build appengine // +build appengine // This file contains the safe implementations of otherwise unsafe-using code. package xxhash // Sum64String computes the 64-bit xxHash digest of s with a zero seed. func Sum64String(s string) uint64 { return Sum64([]byte(s)) } // WriteString adds more data to d. It always returns len(s), nil. func (d *Digest) WriteString(s string) (n int, err error) { return d.Write([]byte(s)) } ================================================ FILE: vendor/github.com/cespare/xxhash/v2/xxhash_unsafe.go ================================================ //go:build !appengine // +build !appengine // This file encapsulates usage of unsafe. // xxhash_safe.go contains the safe implementations. package xxhash import ( "unsafe" ) // In the future it's possible that compiler optimizations will make these // XxxString functions unnecessary by realizing that calls such as // Sum64([]byte(s)) don't need to copy s. See https://go.dev/issue/2205. // If that happens, even if we keep these functions they can be replaced with // the trivial safe code. // NOTE: The usual way of doing an unsafe string-to-[]byte conversion is: // // var b []byte // bh := (*reflect.SliceHeader)(unsafe.Pointer(&b)) // bh.Data = (*reflect.StringHeader)(unsafe.Pointer(&s)).Data // bh.Len = len(s) // bh.Cap = len(s) // // Unfortunately, as of Go 1.15.3 the inliner's cost model assigns a high enough // weight to this sequence of expressions that any function that uses it will // not be inlined. Instead, the functions below use a different unsafe // conversion designed to minimize the inliner weight and allow both to be // inlined. There is also a test (TestInlining) which verifies that these are // inlined. // // See https://github.com/golang/go/issues/42739 for discussion. // Sum64String computes the 64-bit xxHash digest of s with a zero seed. // It may be faster than Sum64([]byte(s)) by avoiding a copy. func Sum64String(s string) uint64 { b := *(*[]byte)(unsafe.Pointer(&sliceHeader{s, len(s)})) return Sum64(b) } // WriteString adds more data to d. It always returns len(s), nil. // It may be faster than Write([]byte(s)) by avoiding a copy. func (d *Digest) WriteString(s string) (n int, err error) { d.Write(*(*[]byte)(unsafe.Pointer(&sliceHeader{s, len(s)}))) // d.Write always returns len(s), nil. // Ignoring the return output and returning these fixed values buys a // savings of 6 in the inliner's cost model. return len(s), nil } // sliceHeader is similar to reflect.SliceHeader, but it assumes that the layout // of the first two words is the same as the layout of a string. type sliceHeader struct { s string cap int } ================================================ FILE: vendor/github.com/coreos/go-oidc/v3/LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: vendor/github.com/coreos/go-oidc/v3/NOTICE ================================================ CoreOS Project Copyright 2014 CoreOS, Inc This product includes software developed at CoreOS, Inc. (http://www.coreos.com/). ================================================ FILE: vendor/github.com/coreos/go-oidc/v3/oidc/jose.go ================================================ package oidc import jose "github.com/go-jose/go-jose/v4" // JOSE asymmetric signing algorithm values as defined by RFC 7518 // // see: https://tools.ietf.org/html/rfc7518#section-3.1 const ( RS256 = "RS256" // RSASSA-PKCS-v1.5 using SHA-256 RS384 = "RS384" // RSASSA-PKCS-v1.5 using SHA-384 RS512 = "RS512" // RSASSA-PKCS-v1.5 using SHA-512 ES256 = "ES256" // ECDSA using P-256 and SHA-256 ES384 = "ES384" // ECDSA using P-384 and SHA-384 ES512 = "ES512" // ECDSA using P-521 and SHA-512 PS256 = "PS256" // RSASSA-PSS using SHA256 and MGF1-SHA256 PS384 = "PS384" // RSASSA-PSS using SHA384 and MGF1-SHA384 PS512 = "PS512" // RSASSA-PSS using SHA512 and MGF1-SHA512 EdDSA = "EdDSA" // Ed25519 using SHA-512 ) var allAlgs = []jose.SignatureAlgorithm{ jose.RS256, jose.RS384, jose.RS512, jose.ES256, jose.ES384, jose.ES512, jose.PS256, jose.PS384, jose.PS512, jose.EdDSA, } ================================================ FILE: vendor/github.com/coreos/go-oidc/v3/oidc/jwks.go ================================================ package oidc import ( "context" "crypto" "crypto/ecdsa" "crypto/ed25519" "crypto/rsa" "errors" "fmt" "io" "net/http" "sync" jose "github.com/go-jose/go-jose/v4" ) // StaticKeySet is a verifier that validates JWT against a static set of public keys. type StaticKeySet struct { // PublicKeys used to verify the JWT. Supported types are *rsa.PublicKey and // *ecdsa.PublicKey. PublicKeys []crypto.PublicKey } // VerifySignature compares the signature against a static set of public keys. func (s *StaticKeySet) VerifySignature(ctx context.Context, jwt string) ([]byte, error) { // Algorithms are already checked by Verifier, so this parse method accepts // any algorithm. jws, err := jose.ParseSigned(jwt, allAlgs) if err != nil { return nil, fmt.Errorf("parsing jwt: %v", err) } for _, pub := range s.PublicKeys { switch pub.(type) { case *rsa.PublicKey: case *ecdsa.PublicKey: case ed25519.PublicKey: default: return nil, fmt.Errorf("invalid public key type provided: %T", pub) } payload, err := jws.Verify(pub) if err != nil { continue } return payload, nil } return nil, fmt.Errorf("no public keys able to verify jwt") } // NewRemoteKeySet returns a KeySet that can validate JSON web tokens by using HTTP // GETs to fetch JSON web token sets hosted at a remote URL. This is automatically // used by NewProvider using the URLs returned by OpenID Connect discovery, but is // exposed for providers that don't support discovery or to prevent round trips to the // discovery URL. // // The returned KeySet is a long lived verifier that caches keys based on any // keys change. Reuse a common remote key set instead of creating new ones as needed. func NewRemoteKeySet(ctx context.Context, jwksURL string) *RemoteKeySet { return newRemoteKeySet(ctx, jwksURL) } func newRemoteKeySet(ctx context.Context, jwksURL string) *RemoteKeySet { return &RemoteKeySet{ jwksURL: jwksURL, // For historical reasons, this package uses contexts for configuration, not just // cancellation. In hindsight, this was a bad idea. // // Attemps to reason about how cancels should work with background requests have // largely lead to confusion. Use the context here as a config bag-of-values and // ignore the cancel function. ctx: context.WithoutCancel(ctx), } } // RemoteKeySet is a KeySet implementation that validates JSON web tokens against // a jwks_uri endpoint. type RemoteKeySet struct { jwksURL string // Used for configuration. Cancelation is ignored. ctx context.Context // guard all other fields mu sync.RWMutex // inflight suppresses parallel execution of updateKeys and allows // multiple goroutines to wait for its result. inflight *inflight // A set of cached keys. cachedKeys []jose.JSONWebKey } // inflight is used to wait on some in-flight request from multiple goroutines. type inflight struct { doneCh chan struct{} keys []jose.JSONWebKey err error } func newInflight() *inflight { return &inflight{doneCh: make(chan struct{})} } // wait returns a channel that multiple goroutines can receive on. Once it returns // a value, the inflight request is done and result() can be inspected. func (i *inflight) wait() <-chan struct{} { return i.doneCh } // done can only be called by a single goroutine. It records the result of the // inflight request and signals other goroutines that the result is safe to // inspect. func (i *inflight) done(keys []jose.JSONWebKey, err error) { i.keys = keys i.err = err close(i.doneCh) } // result cannot be called until the wait() channel has returned a value. func (i *inflight) result() ([]jose.JSONWebKey, error) { return i.keys, i.err } // paresdJWTKey is a context key that allows common setups to avoid parsing the // JWT twice. It holds a *jose.JSONWebSignature value. var parsedJWTKey contextKey // VerifySignature validates a payload against a signature from the jwks_uri. // // Users MUST NOT call this method directly and should use an IDTokenVerifier // instead. This method skips critical validations such as 'alg' values and is // only exported to implement the KeySet interface. func (r *RemoteKeySet) VerifySignature(ctx context.Context, jwt string) ([]byte, error) { jws, ok := ctx.Value(parsedJWTKey).(*jose.JSONWebSignature) if !ok { // The algorithm values are already enforced by the Validator, which also sets // the context value above to pre-parsed signature. // // Practically, this codepath isn't called in normal use of this package, but // if it is, the algorithms have already been checked. var err error jws, err = jose.ParseSigned(jwt, allAlgs) if err != nil { return nil, fmt.Errorf("oidc: malformed jwt: %v", err) } } return r.verify(ctx, jws) } func (r *RemoteKeySet) verify(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error) { // We don't support JWTs signed with multiple signatures. keyID := "" for _, sig := range jws.Signatures { keyID = sig.Header.KeyID break } keys := r.keysFromCache() for _, key := range keys { if keyID == "" || key.KeyID == keyID { if payload, err := jws.Verify(&key); err == nil { return payload, nil } } } // If the kid doesn't match, check for new keys from the remote. This is the // strategy recommended by the spec. // // https://openid.net/specs/openid-connect-core-1_0.html#RotateSigKeys keys, err := r.keysFromRemote(ctx) if err != nil { return nil, fmt.Errorf("fetching keys %w", err) } for _, key := range keys { if keyID == "" || key.KeyID == keyID { if payload, err := jws.Verify(&key); err == nil { return payload, nil } } } return nil, errors.New("failed to verify id token signature") } func (r *RemoteKeySet) keysFromCache() (keys []jose.JSONWebKey) { r.mu.RLock() defer r.mu.RUnlock() return r.cachedKeys } // keysFromRemote syncs the key set from the remote set, records the values in the // cache, and returns the key set. func (r *RemoteKeySet) keysFromRemote(ctx context.Context) ([]jose.JSONWebKey, error) { // Need to lock to inspect the inflight request field. r.mu.Lock() // If there's not a current inflight request, create one. if r.inflight == nil { r.inflight = newInflight() // This goroutine has exclusive ownership over the current inflight // request. It releases the resource by nil'ing the inflight field // once the goroutine is done. go func() { // Sync keys and finish inflight when that's done. keys, err := r.updateKeys() r.inflight.done(keys, err) // Lock to update the keys and indicate that there is no longer an // inflight request. r.mu.Lock() defer r.mu.Unlock() if err == nil { r.cachedKeys = keys } // Free inflight so a different request can run. r.inflight = nil }() } inflight := r.inflight r.mu.Unlock() select { case <-ctx.Done(): return nil, ctx.Err() case <-inflight.wait(): return inflight.result() } } func (r *RemoteKeySet) updateKeys() ([]jose.JSONWebKey, error) { req, err := http.NewRequest("GET", r.jwksURL, nil) if err != nil { return nil, fmt.Errorf("oidc: can't create request: %v", err) } resp, err := doRequest(r.ctx, req) if err != nil { return nil, fmt.Errorf("oidc: get keys failed %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("unable to read response body: %v", err) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("oidc: get keys failed: %s %s", resp.Status, body) } var keySet jose.JSONWebKeySet err = unmarshalResp(resp, body, &keySet) if err != nil { return nil, fmt.Errorf("oidc: failed to decode keys: %v %s", err, body) } return keySet.Keys, nil } ================================================ FILE: vendor/github.com/coreos/go-oidc/v3/oidc/oidc.go ================================================ // Package oidc implements OpenID Connect client logic for the golang.org/x/oauth2 package. package oidc import ( "context" "crypto/sha256" "crypto/sha512" "encoding/base64" "encoding/json" "errors" "fmt" "hash" "io" "mime" "net/http" "strings" "sync" "time" "golang.org/x/oauth2" ) const ( // ScopeOpenID is the mandatory scope for all OpenID Connect OAuth2 requests. ScopeOpenID = "openid" // ScopeOfflineAccess is an optional scope defined by OpenID Connect for requesting // OAuth2 refresh tokens. // // Support for this scope differs between OpenID Connect providers. For instance // Google rejects it, favoring appending "access_type=offline" as part of the // authorization request instead. // // See: https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess ScopeOfflineAccess = "offline_access" ) var ( errNoAtHash = errors.New("id token did not have an access token hash") errInvalidAtHash = errors.New("access token hash does not match value in ID token") ) type contextKey int var issuerURLKey contextKey // ClientContext returns a new Context that carries the provided HTTP client. // // This method sets the same context key used by the golang.org/x/oauth2 package, // so the returned context works for that package too. // // myClient := &http.Client{} // ctx := oidc.ClientContext(parentContext, myClient) // // // This will use the custom client // provider, err := oidc.NewProvider(ctx, "https://accounts.example.com") func ClientContext(ctx context.Context, client *http.Client) context.Context { return context.WithValue(ctx, oauth2.HTTPClient, client) } func getClient(ctx context.Context) *http.Client { if c, ok := ctx.Value(oauth2.HTTPClient).(*http.Client); ok { return c } return nil } // InsecureIssuerURLContext allows discovery to work when the issuer_url reported // by upstream is mismatched with the discovery URL. This is meant for integration // with off-spec providers such as Azure. // // discoveryBaseURL := "https://login.microsoftonline.com/organizations/v2.0" // issuerURL := "https://login.microsoftonline.com/my-tenantid/v2.0" // // ctx := oidc.InsecureIssuerURLContext(parentContext, issuerURL) // // // Provider will be discovered with the discoveryBaseURL, but use issuerURL // // for future issuer validation. // provider, err := oidc.NewProvider(ctx, discoveryBaseURL) // // This is insecure because validating the correct issuer is critical for multi-tenant // providers. Any overrides here MUST be carefully reviewed. func InsecureIssuerURLContext(ctx context.Context, issuerURL string) context.Context { return context.WithValue(ctx, issuerURLKey, issuerURL) } func doRequest(ctx context.Context, req *http.Request) (*http.Response, error) { client := http.DefaultClient if c := getClient(ctx); c != nil { client = c } return client.Do(req.WithContext(ctx)) } // Provider represents an OpenID Connect server's configuration. type Provider struct { issuer string authURL string tokenURL string deviceAuthURL string userInfoURL string jwksURL string algorithms []string // Raw claims returned by the server. rawClaims []byte // Guards all of the following fields. mu sync.Mutex // HTTP client specified from the initial NewProvider request. This is used // when creating the common key set. client *http.Client // A key set that uses context.Background() and is shared between all code paths // that don't have a convinent way of supplying a unique context. commonRemoteKeySet KeySet } func (p *Provider) remoteKeySet() KeySet { p.mu.Lock() defer p.mu.Unlock() if p.commonRemoteKeySet == nil { ctx := context.Background() if p.client != nil { ctx = ClientContext(ctx, p.client) } p.commonRemoteKeySet = NewRemoteKeySet(ctx, p.jwksURL) } return p.commonRemoteKeySet } type providerJSON struct { Issuer string `json:"issuer"` AuthURL string `json:"authorization_endpoint"` TokenURL string `json:"token_endpoint"` DeviceAuthURL string `json:"device_authorization_endpoint"` JWKSURL string `json:"jwks_uri"` UserInfoURL string `json:"userinfo_endpoint"` Algorithms []string `json:"id_token_signing_alg_values_supported"` } // supportedAlgorithms is a list of algorithms explicitly supported by this // package. If a provider supports other algorithms, such as HS256 or none, // those values won't be passed to the IDTokenVerifier. var supportedAlgorithms = map[string]bool{ RS256: true, RS384: true, RS512: true, ES256: true, ES384: true, ES512: true, PS256: true, PS384: true, PS512: true, EdDSA: true, } // ProviderConfig allows direct creation of a [Provider] from metadata // configuration. This is intended for interop with providers that don't support // discovery, or host the JSON discovery document at an off-spec path. // // The ProviderConfig struct specifies JSON struct tags to support document // parsing. // // // Directly fetch the metadata document. // resp, err := http.Get("https://login.example.com/custom-metadata-path") // if err != nil { // // ... // } // defer resp.Body.Close() // // // Parse config from JSON metadata. // config := &oidc.ProviderConfig{} // if err := json.NewDecoder(resp.Body).Decode(config); err != nil { // // ... // } // p := config.NewProvider(context.Background()) // // For providers that implement discovery, use [NewProvider] instead. // // See: https://openid.net/specs/openid-connect-discovery-1_0.html type ProviderConfig struct { // IssuerURL is the identity of the provider, and the string it uses to sign // ID tokens with. For example "https://accounts.google.com". This value MUST // match ID tokens exactly. IssuerURL string `json:"issuer"` // AuthURL is the endpoint used by the provider to support the OAuth 2.0 // authorization endpoint. AuthURL string `json:"authorization_endpoint"` // TokenURL is the endpoint used by the provider to support the OAuth 2.0 // token endpoint. TokenURL string `json:"token_endpoint"` // DeviceAuthURL is the endpoint used by the provider to support the OAuth 2.0 // device authorization endpoint. DeviceAuthURL string `json:"device_authorization_endpoint"` // UserInfoURL is the endpoint used by the provider to support the OpenID // Connect UserInfo flow. // // https://openid.net/specs/openid-connect-core-1_0.html#UserInfo UserInfoURL string `json:"userinfo_endpoint"` // JWKSURL is the endpoint used by the provider to advertise public keys to // verify issued ID tokens. This endpoint is polled as new keys are made // available. JWKSURL string `json:"jwks_uri"` // Algorithms, if provided, indicate a list of JWT algorithms allowed to sign // ID tokens. If not provided, this defaults to the algorithms advertised by // the JWK endpoint, then the set of algorithms supported by this package. Algorithms []string `json:"id_token_signing_alg_values_supported"` } // NewProvider initializes a provider from a set of endpoints, rather than // through discovery. // // The provided context is only used for [http.Client] configuration through // [ClientContext], not cancelation. func (p *ProviderConfig) NewProvider(ctx context.Context) *Provider { return &Provider{ issuer: p.IssuerURL, authURL: p.AuthURL, tokenURL: p.TokenURL, deviceAuthURL: p.DeviceAuthURL, userInfoURL: p.UserInfoURL, jwksURL: p.JWKSURL, algorithms: p.Algorithms, client: getClient(ctx), } } // NewProvider uses the OpenID Connect discovery mechanism to construct a Provider. // The issuer is the URL identifier for the service. For example: "https://accounts.google.com" // or "https://login.salesforce.com". // // OpenID Connect providers that don't implement discovery or host the discovery // document at a non-spec complaint path (such as requiring a URL parameter), // should use [ProviderConfig] instead. // // See: https://openid.net/specs/openid-connect-discovery-1_0.html func NewProvider(ctx context.Context, issuer string) (*Provider, error) { wellKnown := strings.TrimSuffix(issuer, "/") + "/.well-known/openid-configuration" req, err := http.NewRequest("GET", wellKnown, nil) if err != nil { return nil, err } resp, err := doRequest(ctx, req) if err != nil { return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("unable to read response body: %v", err) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("%s: %s", resp.Status, body) } var p providerJSON err = unmarshalResp(resp, body, &p) if err != nil { return nil, fmt.Errorf("oidc: failed to decode provider discovery object: %v", err) } issuerURL, skipIssuerValidation := ctx.Value(issuerURLKey).(string) if !skipIssuerValidation { issuerURL = issuer } if p.Issuer != issuerURL && !skipIssuerValidation { return nil, fmt.Errorf("oidc: issuer URL provided to client (%q) did not match the issuer URL returned by provider (%q)", issuer, p.Issuer) } var algs []string for _, a := range p.Algorithms { if supportedAlgorithms[a] { algs = append(algs, a) } } return &Provider{ issuer: issuerURL, authURL: p.AuthURL, tokenURL: p.TokenURL, deviceAuthURL: p.DeviceAuthURL, userInfoURL: p.UserInfoURL, jwksURL: p.JWKSURL, algorithms: algs, rawClaims: body, client: getClient(ctx), }, nil } // Claims unmarshals raw fields returned by the server during discovery. // // var claims struct { // ScopesSupported []string `json:"scopes_supported"` // ClaimsSupported []string `json:"claims_supported"` // } // // if err := provider.Claims(&claims); err != nil { // // handle unmarshaling error // } // // For a list of fields defined by the OpenID Connect spec see: // https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata func (p *Provider) Claims(v interface{}) error { if p.rawClaims == nil { return errors.New("oidc: claims not set") } return json.Unmarshal(p.rawClaims, v) } // Endpoint returns the OAuth2 auth and token endpoints for the given provider. func (p *Provider) Endpoint() oauth2.Endpoint { return oauth2.Endpoint{AuthURL: p.authURL, DeviceAuthURL: p.deviceAuthURL, TokenURL: p.tokenURL} } // UserInfoEndpoint returns the OpenID Connect userinfo endpoint for the given // provider. func (p *Provider) UserInfoEndpoint() string { return p.userInfoURL } // UserInfo represents the OpenID Connect userinfo claims. type UserInfo struct { Subject string `json:"sub"` Profile string `json:"profile"` Email string `json:"email"` EmailVerified bool `json:"email_verified"` claims []byte } type userInfoRaw struct { Subject string `json:"sub"` Profile string `json:"profile"` Email string `json:"email"` // Handle providers that return email_verified as a string // https://forums.aws.amazon.com/thread.jspa?messageID=949441󧳁 and // https://discuss.elastic.co/t/openid-error-after-authenticating-against-aws-cognito/206018/11 EmailVerified stringAsBool `json:"email_verified"` } // Claims unmarshals the raw JSON object claims into the provided object. func (u *UserInfo) Claims(v interface{}) error { if u.claims == nil { return errors.New("oidc: claims not set") } return json.Unmarshal(u.claims, v) } // UserInfo uses the token source to query the provider's user info endpoint. func (p *Provider) UserInfo(ctx context.Context, tokenSource oauth2.TokenSource) (*UserInfo, error) { if p.userInfoURL == "" { return nil, errors.New("oidc: user info endpoint is not supported by this provider") } req, err := http.NewRequest("GET", p.userInfoURL, nil) if err != nil { return nil, fmt.Errorf("oidc: create GET request: %v", err) } token, err := tokenSource.Token() if err != nil { return nil, fmt.Errorf("oidc: get access token: %v", err) } token.SetAuthHeader(req) resp, err := doRequest(ctx, req) if err != nil { return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("%s: %s", resp.Status, body) } ct := resp.Header.Get("Content-Type") mediaType, _, parseErr := mime.ParseMediaType(ct) if parseErr == nil && mediaType == "application/jwt" { payload, err := p.remoteKeySet().VerifySignature(ctx, string(body)) if err != nil { return nil, fmt.Errorf("oidc: invalid userinfo jwt signature %v", err) } body = payload } var userInfo userInfoRaw if err := json.Unmarshal(body, &userInfo); err != nil { return nil, fmt.Errorf("oidc: failed to decode userinfo: %v", err) } return &UserInfo{ Subject: userInfo.Subject, Profile: userInfo.Profile, Email: userInfo.Email, EmailVerified: bool(userInfo.EmailVerified), claims: body, }, nil } // IDToken is an OpenID Connect extension that provides a predictable representation // of an authorization event. // // The ID Token only holds fields OpenID Connect requires. To access additional // claims returned by the server, use the Claims method. type IDToken struct { // The URL of the server which issued this token. OpenID Connect // requires this value always be identical to the URL used for // initial discovery. // // Note: Because of a known issue with Google Accounts' implementation // this value may differ when using Google. // // See: https://developers.google.com/identity/protocols/OpenIDConnect#obtainuserinfo Issuer string // The client ID, or set of client IDs, that this token is issued for. For // common uses, this is the client that initialized the auth flow. // // This package ensures the audience contains an expected value. Audience []string // A unique string which identifies the end user. Subject string // Expiry of the token. Ths package will not process tokens that have // expired unless that validation is explicitly turned off. Expiry time.Time // When the token was issued by the provider. IssuedAt time.Time // Initial nonce provided during the authentication redirect. // // This package does NOT provided verification on the value of this field // and it's the user's responsibility to ensure it contains a valid value. Nonce string // at_hash claim, if set in the ID token. Callers can verify an access token // that corresponds to the ID token using the VerifyAccessToken method. AccessTokenHash string // signature algorithm used for ID token, needed to compute a verification hash of an // access token sigAlgorithm string // Raw payload of the id_token. claims []byte // Map of distributed claim names to claim sources distributedClaims map[string]claimSource } // Claims unmarshals the raw JSON payload of the ID Token into a provided struct. // // idToken, err := idTokenVerifier.Verify(rawIDToken) // if err != nil { // // handle error // } // var claims struct { // Email string `json:"email"` // EmailVerified bool `json:"email_verified"` // } // if err := idToken.Claims(&claims); err != nil { // // handle error // } func (i *IDToken) Claims(v interface{}) error { if i.claims == nil { return errors.New("oidc: claims not set") } return json.Unmarshal(i.claims, v) } // VerifyAccessToken verifies that the hash of the access token that corresponds to the iD token // matches the hash in the id token. It returns an error if the hashes don't match. // It is the caller's responsibility to ensure that the optional access token hash is present for the ID token // before calling this method. See https://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken func (i *IDToken) VerifyAccessToken(accessToken string) error { if i.AccessTokenHash == "" { return errNoAtHash } var h hash.Hash switch i.sigAlgorithm { case RS256, ES256, PS256: h = sha256.New() case RS384, ES384, PS384: h = sha512.New384() case RS512, ES512, PS512, EdDSA: h = sha512.New() default: return fmt.Errorf("oidc: unsupported signing algorithm %q", i.sigAlgorithm) } h.Write([]byte(accessToken)) // hash documents that Write will never return an error sum := h.Sum(nil)[:h.Size()/2] actual := base64.RawURLEncoding.EncodeToString(sum) if actual != i.AccessTokenHash { return errInvalidAtHash } return nil } type idToken struct { Issuer string `json:"iss"` Subject string `json:"sub"` Audience audience `json:"aud"` Expiry jsonTime `json:"exp"` IssuedAt jsonTime `json:"iat"` NotBefore *jsonTime `json:"nbf"` Nonce string `json:"nonce"` AtHash string `json:"at_hash"` ClaimNames map[string]string `json:"_claim_names"` ClaimSources map[string]claimSource `json:"_claim_sources"` } type claimSource struct { Endpoint string `json:"endpoint"` AccessToken string `json:"access_token"` } type stringAsBool bool func (sb *stringAsBool) UnmarshalJSON(b []byte) error { switch string(b) { case "true", `"true"`: *sb = true case "false", `"false"`: *sb = false default: return errors.New("invalid value for boolean") } return nil } type audience []string func (a *audience) UnmarshalJSON(b []byte) error { var s string if json.Unmarshal(b, &s) == nil { *a = audience{s} return nil } var auds []string if err := json.Unmarshal(b, &auds); err != nil { return err } *a = auds return nil } type jsonTime time.Time func (j *jsonTime) UnmarshalJSON(b []byte) error { var n json.Number if err := json.Unmarshal(b, &n); err != nil { return err } var unix int64 if t, err := n.Int64(); err == nil { unix = t } else { f, err := n.Float64() if err != nil { return err } unix = int64(f) } *j = jsonTime(time.Unix(unix, 0)) return nil } func unmarshalResp(r *http.Response, body []byte, v interface{}) error { err := json.Unmarshal(body, &v) if err == nil { return nil } ct := r.Header.Get("Content-Type") mediaType, _, parseErr := mime.ParseMediaType(ct) if parseErr == nil && mediaType == "application/json" { return fmt.Errorf("got Content-Type = application/json, but could not unmarshal as JSON: %v", err) } return fmt.Errorf("expected Content-Type = application/json, got %q: %v", ct, err) } ================================================ FILE: vendor/github.com/coreos/go-oidc/v3/oidc/verify.go ================================================ package oidc import ( "context" "encoding/json" "fmt" "io" "net/http" "time" jose "github.com/go-jose/go-jose/v4" "golang.org/x/oauth2" ) const ( issuerGoogleAccounts = "https://accounts.google.com" issuerGoogleAccountsNoScheme = "accounts.google.com" ) // TokenExpiredError indicates that Verify failed because the token was expired. This // error does NOT indicate that the token is not also invalid for other reasons. Other // checks might have failed if the expiration check had not failed. type TokenExpiredError struct { // Expiry is the time when the token expired. Expiry time.Time } func (e *TokenExpiredError) Error() string { return fmt.Sprintf("oidc: token is expired (Token Expiry: %v)", e.Expiry) } // KeySet is a set of publc JSON Web Keys that can be used to validate the signature // of JSON web tokens. This is expected to be backed by a remote key set through // provider metadata discovery or an in-memory set of keys delivered out-of-band. type KeySet interface { // VerifySignature parses the JSON web token, verifies the signature, and returns // the raw payload. Header and claim fields are validated by other parts of the // package. For example, the KeySet does not need to check values such as signature // algorithm, issuer, and audience since the IDTokenVerifier validates these values // independently. // // If VerifySignature makes HTTP requests to verify the token, it's expected to // use any HTTP client associated with the context through ClientContext. VerifySignature(ctx context.Context, jwt string) (payload []byte, err error) } // IDTokenVerifier provides verification for ID Tokens. type IDTokenVerifier struct { keySet KeySet config *Config issuer string } // NewVerifier returns a verifier manually constructed from a key set and issuer URL. // // It's easier to use provider discovery to construct an IDTokenVerifier than creating // one directly. This method is intended to be used with provider that don't support // metadata discovery, or avoiding round trips when the key set URL is already known. // // This constructor can be used to create a verifier directly using the issuer URL and // JSON Web Key Set URL without using discovery: // // keySet := oidc.NewRemoteKeySet(ctx, "https://www.googleapis.com/oauth2/v3/certs") // verifier := oidc.NewVerifier("https://accounts.google.com", keySet, config) // // Or a static key set (e.g. for testing): // // keySet := &oidc.StaticKeySet{PublicKeys: []crypto.PublicKey{pub1, pub2}} // verifier := oidc.NewVerifier("https://accounts.google.com", keySet, config) func NewVerifier(issuerURL string, keySet KeySet, config *Config) *IDTokenVerifier { return &IDTokenVerifier{keySet: keySet, config: config, issuer: issuerURL} } // Config is the configuration for an IDTokenVerifier. type Config struct { // Expected audience of the token. For a majority of the cases this is expected to be // the ID of the client that initialized the login flow. It may occasionally differ if // the provider supports the authorizing party (azp) claim. // // If not provided, users must explicitly set SkipClientIDCheck. ClientID string // If specified, only this set of algorithms may be used to sign the JWT. // // If the IDTokenVerifier is created from a provider with (*Provider).Verifier, this // defaults to the set of algorithms the provider supports. Otherwise this values // defaults to RS256. SupportedSigningAlgs []string // If true, no ClientID check performed. Must be true if ClientID field is empty. SkipClientIDCheck bool // If true, token expiry is not checked. SkipExpiryCheck bool // SkipIssuerCheck is intended for specialized cases where the the caller wishes to // defer issuer validation. When enabled, callers MUST independently verify the Token's // Issuer is a known good value. // // Mismatched issuers often indicate client mis-configuration. If mismatches are // unexpected, evaluate if the provided issuer URL is incorrect instead of enabling // this option. SkipIssuerCheck bool // Time function to check Token expiry. Defaults to time.Now Now func() time.Time // InsecureSkipSignatureCheck causes this package to skip JWT signature validation. // It's intended for special cases where providers (such as Azure), use the "none" // algorithm. // // This option can only be enabled safely when the ID Token is received directly // from the provider after the token exchange. // // This option MUST NOT be used when receiving an ID Token from sources other // than the token endpoint. InsecureSkipSignatureCheck bool } // VerifierContext returns an IDTokenVerifier that uses the provider's key set to // verify JWTs. As opposed to Verifier, the context is used to configure requests // to the upstream JWKs endpoint. The provided context's cancellation is ignored. func (p *Provider) VerifierContext(ctx context.Context, config *Config) *IDTokenVerifier { return p.newVerifier(NewRemoteKeySet(ctx, p.jwksURL), config) } // Verifier returns an IDTokenVerifier that uses the provider's key set to verify JWTs. // // The returned verifier uses a background context for all requests to the upstream // JWKs endpoint. To control that context, use VerifierContext instead. func (p *Provider) Verifier(config *Config) *IDTokenVerifier { return p.newVerifier(p.remoteKeySet(), config) } func (p *Provider) newVerifier(keySet KeySet, config *Config) *IDTokenVerifier { if len(config.SupportedSigningAlgs) == 0 && len(p.algorithms) > 0 { // Make a copy so we don't modify the config values. cp := &Config{} *cp = *config cp.SupportedSigningAlgs = p.algorithms config = cp } return NewVerifier(p.issuer, keySet, config) } func contains(sli []string, ele string) bool { for _, s := range sli { if s == ele { return true } } return false } // Returns the Claims from the distributed JWT token func resolveDistributedClaim(ctx context.Context, verifier *IDTokenVerifier, src claimSource) ([]byte, error) { req, err := http.NewRequest("GET", src.Endpoint, nil) if err != nil { return nil, fmt.Errorf("malformed request: %v", err) } if src.AccessToken != "" { req.Header.Set("Authorization", "Bearer "+src.AccessToken) } resp, err := doRequest(ctx, req) if err != nil { return nil, fmt.Errorf("oidc: Request to endpoint failed: %v", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("unable to read response body: %v", err) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("oidc: request failed: %v", resp.StatusCode) } token, err := verifier.Verify(ctx, string(body)) if err != nil { return nil, fmt.Errorf("malformed response body: %v", err) } return token.claims, nil } // Verify parses a raw ID Token, verifies it's been signed by the provider, performs // any additional checks depending on the Config, and returns the payload. // // Verify does NOT do nonce validation, which is the callers responsibility. // // See: https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation // // oauth2Token, err := oauth2Config.Exchange(ctx, r.URL.Query().Get("code")) // if err != nil { // // handle error // } // // // Extract the ID Token from oauth2 token. // rawIDToken, ok := oauth2Token.Extra("id_token").(string) // if !ok { // // handle error // } // // token, err := verifier.Verify(ctx, rawIDToken) func (v *IDTokenVerifier) Verify(ctx context.Context, rawIDToken string) (*IDToken, error) { var supportedSigAlgs []jose.SignatureAlgorithm for _, alg := range v.config.SupportedSigningAlgs { supportedSigAlgs = append(supportedSigAlgs, jose.SignatureAlgorithm(alg)) } if len(supportedSigAlgs) == 0 { // If no algorithms were specified by both the config and discovery, default // to the one mandatory algorithm "RS256". supportedSigAlgs = []jose.SignatureAlgorithm{jose.RS256} } if v.config.InsecureSkipSignatureCheck { // "none" is a required value to even parse a JWT with the "none" algorithm // using go-jose. supportedSigAlgs = append(supportedSigAlgs, "none") } // Parse and verify the signature first. This at least forces the user to have // a valid, signed ID token before we do any other processing. jws, err := jose.ParseSigned(rawIDToken, supportedSigAlgs) if err != nil { return nil, fmt.Errorf("oidc: malformed jwt: %v", err) } switch len(jws.Signatures) { case 0: return nil, fmt.Errorf("oidc: id token not signed") case 1: default: return nil, fmt.Errorf("oidc: multiple signatures on id token not supported") } sig := jws.Signatures[0] var payload []byte if v.config.InsecureSkipSignatureCheck { // Yolo mode. payload = jws.UnsafePayloadWithoutVerification() } else { // The JWT is attached here for the happy path to avoid the verifier from // having to parse the JWT twice. ctx = context.WithValue(ctx, parsedJWTKey, jws) payload, err = v.keySet.VerifySignature(ctx, rawIDToken) if err != nil { return nil, fmt.Errorf("failed to verify signature: %v", err) } } var token idToken if err := json.Unmarshal(payload, &token); err != nil { return nil, fmt.Errorf("oidc: failed to unmarshal claims: %v", err) } distributedClaims := make(map[string]claimSource) //step through the token to map claim names to claim sources" for cn, src := range token.ClaimNames { if src == "" { return nil, fmt.Errorf("oidc: failed to obtain source from claim name") } s, ok := token.ClaimSources[src] if !ok { return nil, fmt.Errorf("oidc: source does not exist") } distributedClaims[cn] = s } t := &IDToken{ Issuer: token.Issuer, Subject: token.Subject, Audience: []string(token.Audience), Expiry: time.Time(token.Expiry), IssuedAt: time.Time(token.IssuedAt), Nonce: token.Nonce, AccessTokenHash: token.AtHash, claims: payload, distributedClaims: distributedClaims, sigAlgorithm: sig.Header.Algorithm, } // Check issuer. if !v.config.SkipIssuerCheck && t.Issuer != v.issuer { // Google sometimes returns "accounts.google.com" as the issuer claim instead of // the required "https://accounts.google.com". Detect this case and allow it only // for Google. // // We will not add hooks to let other providers go off spec like this. if !(v.issuer == issuerGoogleAccounts && t.Issuer == issuerGoogleAccountsNoScheme) { return nil, fmt.Errorf("oidc: id token issued by a different provider, expected %q got %q", v.issuer, t.Issuer) } } // If a client ID has been provided, make sure it's part of the audience. SkipClientIDCheck must be true if ClientID is empty. // // This check DOES NOT ensure that the ClientID is the party to which the ID Token was issued (i.e. Authorized party). if !v.config.SkipClientIDCheck { if v.config.ClientID != "" { if !contains(t.Audience, v.config.ClientID) { return nil, fmt.Errorf("oidc: expected audience %q got %q", v.config.ClientID, t.Audience) } } else { return nil, fmt.Errorf("oidc: invalid configuration, clientID must be provided or SkipClientIDCheck must be set") } } // If a SkipExpiryCheck is false, make sure token is not expired. if !v.config.SkipExpiryCheck { now := time.Now if v.config.Now != nil { now = v.config.Now } nowTime := now() if t.Expiry.Before(nowTime) { return nil, &TokenExpiredError{Expiry: t.Expiry} } // If nbf claim is provided in token, ensure that it is indeed in the past. if token.NotBefore != nil { nbfTime := time.Time(*token.NotBefore) // Set to 5 minutes since this is what other OpenID Connect providers do to deal with clock skew. // https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/6.12.2/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs#L149-L153 leeway := 5 * time.Minute if nowTime.Add(leeway).Before(nbfTime) { return nil, fmt.Errorf("oidc: current time %v before the nbf (not before) time: %v", nowTime, nbfTime) } } } return t, nil } // Nonce returns an auth code option which requires the ID Token created by the // OpenID Connect provider to contain the specified nonce. func Nonce(nonce string) oauth2.AuthCodeOption { return oauth2.SetAuthURLParam("nonce", nonce) } ================================================ FILE: vendor/github.com/coreos/go-systemd/v22/LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: You must give any other recipients of the Work or Derivative Works a copy of this License; and You must cause any modified files to carry prominent notices stating that You changed the files; and You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: vendor/github.com/coreos/go-systemd/v22/NOTICE ================================================ CoreOS Project Copyright 2018 CoreOS, Inc This product includes software developed at CoreOS, Inc. (http://www.coreos.com/). ================================================ FILE: vendor/github.com/coreos/go-systemd/v22/daemon/sdnotify.go ================================================ // Copyright 2014 Docker, Inc. // Copyright 2015-2018 CoreOS, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // Package daemon provides a Go implementation of the sd_notify protocol. // It can be used to inform systemd of service start-up completion, watchdog // events, and other status changes. // // https://www.freedesktop.org/software/systemd/man/sd_notify.html#Description package daemon import ( "net" "os" ) const ( // SdNotifyReady tells the service manager that service startup is finished // or the service finished loading its configuration. SdNotifyReady = "READY=1" // SdNotifyStopping tells the service manager that the service is beginning // its shutdown. SdNotifyStopping = "STOPPING=1" // SdNotifyReloading tells the service manager that this service is // reloading its configuration. Note that you must call SdNotifyReady when // it completed reloading. SdNotifyReloading = "RELOADING=1" // SdNotifyWatchdog tells the service manager to update the watchdog // timestamp for the service. SdNotifyWatchdog = "WATCHDOG=1" ) // SdNotify sends a message to the init daemon. It is common to ignore the error. // If `unsetEnvironment` is true, the environment variable `NOTIFY_SOCKET` // will be unconditionally unset. // // It returns one of the following: // (false, nil) - notification not supported (i.e. NOTIFY_SOCKET is unset) // (false, err) - notification supported, but failure happened (e.g. error connecting to NOTIFY_SOCKET or while sending data) // (true, nil) - notification supported, data has been sent func SdNotify(unsetEnvironment bool, state string) (bool, error) { socketAddr := &net.UnixAddr{ Name: os.Getenv("NOTIFY_SOCKET"), Net: "unixgram", } // NOTIFY_SOCKET not set if socketAddr.Name == "" { return false, nil } if unsetEnvironment { if err := os.Unsetenv("NOTIFY_SOCKET"); err != nil { return false, err } } conn, err := net.DialUnix(socketAddr.Net, nil, socketAddr) // Error connecting to NOTIFY_SOCKET if err != nil { return false, err } defer conn.Close() if _, err = conn.Write([]byte(state)); err != nil { return false, err } return true, nil } ================================================ FILE: vendor/github.com/coreos/go-systemd/v22/daemon/watchdog.go ================================================ // Copyright 2016 CoreOS, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package daemon import ( "fmt" "os" "strconv" "time" ) // SdWatchdogEnabled returns watchdog information for a service. // Processes should call daemon.SdNotify(false, daemon.SdNotifyWatchdog) every // time / 2. // If `unsetEnvironment` is true, the environment variables `WATCHDOG_USEC` and // `WATCHDOG_PID` will be unconditionally unset. // // It returns one of the following: // (0, nil) - watchdog isn't enabled or we aren't the watched PID. // (0, err) - an error happened (e.g. error converting time). // (time, nil) - watchdog is enabled and we can send ping. time is delay // before inactive service will be killed. func SdWatchdogEnabled(unsetEnvironment bool) (time.Duration, error) { wusec := os.Getenv("WATCHDOG_USEC") wpid := os.Getenv("WATCHDOG_PID") if unsetEnvironment { wusecErr := os.Unsetenv("WATCHDOG_USEC") wpidErr := os.Unsetenv("WATCHDOG_PID") if wusecErr != nil { return 0, wusecErr } if wpidErr != nil { return 0, wpidErr } } if wusec == "" { return 0, nil } s, err := strconv.Atoi(wusec) if err != nil { return 0, fmt.Errorf("error converting WATCHDOG_USEC: %s", err) } if s <= 0 { return 0, fmt.Errorf("error WATCHDOG_USEC must be a positive number") } interval := time.Duration(s) * time.Microsecond if wpid == "" { return interval, nil } p, err := strconv.Atoi(wpid) if err != nil { return 0, fmt.Errorf("error converting WATCHDOG_PID: %s", err) } if os.Getpid() != p { return 0, nil } return interval, nil } ================================================ FILE: vendor/github.com/cpuguy83/go-md2man/v2/LICENSE.md ================================================ The MIT License (MIT) Copyright (c) 2014 Brian Goff Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: vendor/github.com/cpuguy83/go-md2man/v2/md2man/md2man.go ================================================ package md2man import ( "github.com/russross/blackfriday/v2" ) // Render converts a markdown document into a roff formatted document. func Render(doc []byte) []byte { renderer := NewRoffRenderer() return blackfriday.Run(doc, []blackfriday.Option{blackfriday.WithRenderer(renderer), blackfriday.WithExtensions(renderer.GetExtensions())}...) } ================================================ FILE: vendor/github.com/cpuguy83/go-md2man/v2/md2man/roff.go ================================================ package md2man import ( "fmt" "io" "os" "strings" "github.com/russross/blackfriday/v2" ) // roffRenderer implements the blackfriday.Renderer interface for creating // roff format (manpages) from markdown text type roffRenderer struct { extensions blackfriday.Extensions listCounters []int firstHeader bool defineTerm bool listDepth int } const ( titleHeader = ".TH " topLevelHeader = "\n\n.SH " secondLevelHdr = "\n.SH " otherHeader = "\n.SS " crTag = "\n" emphTag = "\\fI" emphCloseTag = "\\fP" strongTag = "\\fB" strongCloseTag = "\\fP" breakTag = "\n.br\n" paraTag = "\n.PP\n" hruleTag = "\n.ti 0\n\\l'\\n(.lu'\n" linkTag = "\n\\[la]" linkCloseTag = "\\[ra]" codespanTag = "\\fB\\fC" codespanCloseTag = "\\fR" codeTag = "\n.PP\n.RS\n\n.nf\n" codeCloseTag = "\n.fi\n.RE\n" quoteTag = "\n.PP\n.RS\n" quoteCloseTag = "\n.RE\n" listTag = "\n.RS\n" listCloseTag = "\n.RE\n" arglistTag = "\n.TP\n" tableStart = "\n.TS\nallbox;\n" tableEnd = ".TE\n" tableCellStart = "T{\n" tableCellEnd = "\nT}\n" ) // NewRoffRenderer creates a new blackfriday Renderer for generating roff documents // from markdown func NewRoffRenderer() *roffRenderer { // nolint: golint var extensions blackfriday.Extensions extensions |= blackfriday.NoIntraEmphasis extensions |= blackfriday.Tables extensions |= blackfriday.FencedCode extensions |= blackfriday.SpaceHeadings extensions |= blackfriday.Footnotes extensions |= blackfriday.Titleblock extensions |= blackfriday.DefinitionLists return &roffRenderer{ extensions: extensions, } } // GetExtensions returns the list of extensions used by this renderer implementation func (r *roffRenderer) GetExtensions() blackfriday.Extensions { return r.extensions } // RenderHeader handles outputting the header at document start func (r *roffRenderer) RenderHeader(w io.Writer, ast *blackfriday.Node) { // disable hyphenation out(w, ".nh\n") } // RenderFooter handles outputting the footer at the document end; the roff // renderer has no footer information func (r *roffRenderer) RenderFooter(w io.Writer, ast *blackfriday.Node) { } // RenderNode is called for each node in a markdown document; based on the node // type the equivalent roff output is sent to the writer func (r *roffRenderer) RenderNode(w io.Writer, node *blackfriday.Node, entering bool) blackfriday.WalkStatus { var walkAction = blackfriday.GoToNext switch node.Type { case blackfriday.Text: r.handleText(w, node, entering) case blackfriday.Softbreak: out(w, crTag) case blackfriday.Hardbreak: out(w, breakTag) case blackfriday.Emph: if entering { out(w, emphTag) } else { out(w, emphCloseTag) } case blackfriday.Strong: if entering { out(w, strongTag) } else { out(w, strongCloseTag) } case blackfriday.Link: if !entering { out(w, linkTag+string(node.LinkData.Destination)+linkCloseTag) } case blackfriday.Image: // ignore images walkAction = blackfriday.SkipChildren case blackfriday.Code: out(w, codespanTag) escapeSpecialChars(w, node.Literal) out(w, codespanCloseTag) case blackfriday.Document: break case blackfriday.Paragraph: // roff .PP markers break lists if r.listDepth > 0 { return blackfriday.GoToNext } if entering { out(w, paraTag) } else { out(w, crTag) } case blackfriday.BlockQuote: if entering { out(w, quoteTag) } else { out(w, quoteCloseTag) } case blackfriday.Heading: r.handleHeading(w, node, entering) case blackfriday.HorizontalRule: out(w, hruleTag) case blackfriday.List: r.handleList(w, node, entering) case blackfriday.Item: r.handleItem(w, node, entering) case blackfriday.CodeBlock: out(w, codeTag) escapeSpecialChars(w, node.Literal) out(w, codeCloseTag) case blackfriday.Table: r.handleTable(w, node, entering) case blackfriday.TableCell: r.handleTableCell(w, node, entering) case blackfriday.TableHead: case blackfriday.TableBody: case blackfriday.TableRow: // no action as cell entries do all the nroff formatting return blackfriday.GoToNext default: fmt.Fprintln(os.Stderr, "WARNING: go-md2man does not handle node type "+node.Type.String()) } return walkAction } func (r *roffRenderer) handleText(w io.Writer, node *blackfriday.Node, entering bool) { var ( start, end string ) // handle special roff table cell text encapsulation if node.Parent.Type == blackfriday.TableCell { if len(node.Literal) > 30 { start = tableCellStart end = tableCellEnd } else { // end rows that aren't terminated by "tableCellEnd" with a cr if end of row if node.Parent.Next == nil && !node.Parent.IsHeader { end = crTag } } } out(w, start) escapeSpecialChars(w, node.Literal) out(w, end) } func (r *roffRenderer) handleHeading(w io.Writer, node *blackfriday.Node, entering bool) { if entering { switch node.Level { case 1: if !r.firstHeader { out(w, titleHeader) r.firstHeader = true break } out(w, topLevelHeader) case 2: out(w, secondLevelHdr) default: out(w, otherHeader) } } } func (r *roffRenderer) handleList(w io.Writer, node *blackfriday.Node, entering bool) { openTag := listTag closeTag := listCloseTag if node.ListFlags&blackfriday.ListTypeDefinition != 0 { // tags for definition lists handled within Item node openTag = "" closeTag = "" } if entering { r.listDepth++ if node.ListFlags&blackfriday.ListTypeOrdered != 0 { r.listCounters = append(r.listCounters, 1) } out(w, openTag) } else { if node.ListFlags&blackfriday.ListTypeOrdered != 0 { r.listCounters = r.listCounters[:len(r.listCounters)-1] } out(w, closeTag) r.listDepth-- } } func (r *roffRenderer) handleItem(w io.Writer, node *blackfriday.Node, entering bool) { if entering { if node.ListFlags&blackfriday.ListTypeOrdered != 0 { out(w, fmt.Sprintf(".IP \"%3d.\" 5\n", r.listCounters[len(r.listCounters)-1])) r.listCounters[len(r.listCounters)-1]++ } else if node.ListFlags&blackfriday.ListTypeDefinition != 0 { // state machine for handling terms and following definitions // since blackfriday does not distinguish them properly, nor // does it seperate them into separate lists as it should if !r.defineTerm { out(w, arglistTag) r.defineTerm = true } else { r.defineTerm = false } } else { out(w, ".IP \\(bu 2\n") } } else { out(w, "\n") } } func (r *roffRenderer) handleTable(w io.Writer, node *blackfriday.Node, entering bool) { if entering { out(w, tableStart) //call walker to count cells (and rows?) so format section can be produced columns := countColumns(node) out(w, strings.Repeat("l ", columns)+"\n") out(w, strings.Repeat("l ", columns)+".\n") } else { out(w, tableEnd) } } func (r *roffRenderer) handleTableCell(w io.Writer, node *blackfriday.Node, entering bool) { var ( start, end string ) if node.IsHeader { start = codespanTag end = codespanCloseTag } if entering { if node.Prev != nil && node.Prev.Type == blackfriday.TableCell { out(w, "\t"+start) } else { out(w, start) } } else { // need to carriage return if we are at the end of the header row if node.IsHeader && node.Next == nil { end = end + crTag } out(w, end) } } // because roff format requires knowing the column count before outputting any table // data we need to walk a table tree and count the columns func countColumns(node *blackfriday.Node) int { var columns int node.Walk(func(node *blackfriday.Node, entering bool) blackfriday.WalkStatus { switch node.Type { case blackfriday.TableRow: if !entering { return blackfriday.Terminate } case blackfriday.TableCell: if entering { columns++ } default: } return blackfriday.GoToNext }) return columns } func out(w io.Writer, output string) { io.WriteString(w, output) // nolint: errcheck } func needsBackslash(c byte) bool { for _, r := range []byte("-_&\\~") { if c == r { return true } } return false } func escapeSpecialChars(w io.Writer, text []byte) { for i := 0; i < len(text); i++ { // escape initial apostrophe or period if len(text) >= 1 && (text[0] == '\'' || text[0] == '.') { out(w, "\\&") } // directly copy normal characters org := i for i < len(text) && !needsBackslash(text[i]) { i++ } if i > org { w.Write(text[org:i]) // nolint: errcheck } // escape a character if i >= len(text) { break } w.Write([]byte{'\\', text[i]}) // nolint: errcheck } } ================================================ FILE: vendor/github.com/davecgh/go-spew/LICENSE ================================================ ISC License Copyright (c) 2012-2016 Dave Collins Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ================================================ FILE: vendor/github.com/davecgh/go-spew/spew/bypass.go ================================================ // Copyright (c) 2015-2016 Dave Collins // // Permission to use, copy, modify, and distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. // NOTE: Due to the following build constraints, this file will only be compiled // when the code is not running on Google App Engine, compiled by GopherJS, and // "-tags safe" is not added to the go build command line. The "disableunsafe" // tag is deprecated and thus should not be used. // Go versions prior to 1.4 are disabled because they use a different layout // for interfaces which make the implementation of unsafeReflectValue more complex. // +build !js,!appengine,!safe,!disableunsafe,go1.4 package spew import ( "reflect" "unsafe" ) const ( // UnsafeDisabled is a build-time constant which specifies whether or // not access to the unsafe package is available. UnsafeDisabled = false // ptrSize is the size of a pointer on the current arch. ptrSize = unsafe.Sizeof((*byte)(nil)) ) type flag uintptr var ( // flagRO indicates whether the value field of a reflect.Value // is read-only. flagRO flag // flagAddr indicates whether the address of the reflect.Value's // value may be taken. flagAddr flag ) // flagKindMask holds the bits that make up the kind // part of the flags field. In all the supported versions, // it is in the lower 5 bits. const flagKindMask = flag(0x1f) // Different versions of Go have used different // bit layouts for the flags type. This table // records the known combinations. var okFlags = []struct { ro, addr flag }{{ // From Go 1.4 to 1.5 ro: 1 << 5, addr: 1 << 7, }, { // Up to Go tip. ro: 1<<5 | 1<<6, addr: 1 << 8, }} var flagValOffset = func() uintptr { field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag") if !ok { panic("reflect.Value has no flag field") } return field.Offset }() // flagField returns a pointer to the flag field of a reflect.Value. func flagField(v *reflect.Value) *flag { return (*flag)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + flagValOffset)) } // unsafeReflectValue converts the passed reflect.Value into a one that bypasses // the typical safety restrictions preventing access to unaddressable and // unexported data. It works by digging the raw pointer to the underlying // value out of the protected value and generating a new unprotected (unsafe) // reflect.Value to it. // // This allows us to check for implementations of the Stringer and error // interfaces to be used for pretty printing ordinarily unaddressable and // inaccessible values such as unexported struct fields. func unsafeReflectValue(v reflect.Value) reflect.Value { if !v.IsValid() || (v.CanInterface() && v.CanAddr()) { return v } flagFieldPtr := flagField(&v) *flagFieldPtr &^= flagRO *flagFieldPtr |= flagAddr return v } // Sanity checks against future reflect package changes // to the type or semantics of the Value.flag field. func init() { field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag") if !ok { panic("reflect.Value has no flag field") } if field.Type.Kind() != reflect.TypeOf(flag(0)).Kind() { panic("reflect.Value flag field has changed kind") } type t0 int var t struct { A t0 // t0 will have flagEmbedRO set. t0 // a will have flagStickyRO set a t0 } vA := reflect.ValueOf(t).FieldByName("A") va := reflect.ValueOf(t).FieldByName("a") vt0 := reflect.ValueOf(t).FieldByName("t0") // Infer flagRO from the difference between the flags // for the (otherwise identical) fields in t. flagPublic := *flagField(&vA) flagWithRO := *flagField(&va) | *flagField(&vt0) flagRO = flagPublic ^ flagWithRO // Infer flagAddr from the difference between a value // taken from a pointer and not. vPtrA := reflect.ValueOf(&t).Elem().FieldByName("A") flagNoPtr := *flagField(&vA) flagPtr := *flagField(&vPtrA) flagAddr = flagNoPtr ^ flagPtr // Check that the inferred flags tally with one of the known versions. for _, f := range okFlags { if flagRO == f.ro && flagAddr == f.addr { return } } panic("reflect.Value read-only flag has changed semantics") } ================================================ FILE: vendor/github.com/davecgh/go-spew/spew/bypasssafe.go ================================================ // Copyright (c) 2015-2016 Dave Collins // // Permission to use, copy, modify, and distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. // NOTE: Due to the following build constraints, this file will only be compiled // when the code is running on Google App Engine, compiled by GopherJS, or // "-tags safe" is added to the go build command line. The "disableunsafe" // tag is deprecated and thus should not be used. // +build js appengine safe disableunsafe !go1.4 package spew import "reflect" const ( // UnsafeDisabled is a build-time constant which specifies whether or // not access to the unsafe package is available. UnsafeDisabled = true ) // unsafeReflectValue typically converts the passed reflect.Value into a one // that bypasses the typical safety restrictions preventing access to // unaddressable and unexported data. However, doing this relies on access to // the unsafe package. This is a stub version which simply returns the passed // reflect.Value when the unsafe package is not available. func unsafeReflectValue(v reflect.Value) reflect.Value { return v } ================================================ FILE: vendor/github.com/davecgh/go-spew/spew/common.go ================================================ /* * Copyright (c) 2013-2016 Dave Collins * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ package spew import ( "bytes" "fmt" "io" "reflect" "sort" "strconv" ) // Some constants in the form of bytes to avoid string overhead. This mirrors // the technique used in the fmt package. var ( panicBytes = []byte("(PANIC=") plusBytes = []byte("+") iBytes = []byte("i") trueBytes = []byte("true") falseBytes = []byte("false") interfaceBytes = []byte("(interface {})") commaNewlineBytes = []byte(",\n") newlineBytes = []byte("\n") openBraceBytes = []byte("{") openBraceNewlineBytes = []byte("{\n") closeBraceBytes = []byte("}") asteriskBytes = []byte("*") colonBytes = []byte(":") colonSpaceBytes = []byte(": ") openParenBytes = []byte("(") closeParenBytes = []byte(")") spaceBytes = []byte(" ") pointerChainBytes = []byte("->") nilAngleBytes = []byte("") maxNewlineBytes = []byte("\n") maxShortBytes = []byte("") circularBytes = []byte("") circularShortBytes = []byte("") invalidAngleBytes = []byte("") openBracketBytes = []byte("[") closeBracketBytes = []byte("]") percentBytes = []byte("%") precisionBytes = []byte(".") openAngleBytes = []byte("<") closeAngleBytes = []byte(">") openMapBytes = []byte("map[") closeMapBytes = []byte("]") lenEqualsBytes = []byte("len=") capEqualsBytes = []byte("cap=") ) // hexDigits is used to map a decimal value to a hex digit. var hexDigits = "0123456789abcdef" // catchPanic handles any panics that might occur during the handleMethods // calls. func catchPanic(w io.Writer, v reflect.Value) { if err := recover(); err != nil { w.Write(panicBytes) fmt.Fprintf(w, "%v", err) w.Write(closeParenBytes) } } // handleMethods attempts to call the Error and String methods on the underlying // type the passed reflect.Value represents and outputes the result to Writer w. // // It handles panics in any called methods by catching and displaying the error // as the formatted value. func handleMethods(cs *ConfigState, w io.Writer, v reflect.Value) (handled bool) { // We need an interface to check if the type implements the error or // Stringer interface. However, the reflect package won't give us an // interface on certain things like unexported struct fields in order // to enforce visibility rules. We use unsafe, when it's available, // to bypass these restrictions since this package does not mutate the // values. if !v.CanInterface() { if UnsafeDisabled { return false } v = unsafeReflectValue(v) } // Choose whether or not to do error and Stringer interface lookups against // the base type or a pointer to the base type depending on settings. // Technically calling one of these methods with a pointer receiver can // mutate the value, however, types which choose to satisify an error or // Stringer interface with a pointer receiver should not be mutating their // state inside these interface methods. if !cs.DisablePointerMethods && !UnsafeDisabled && !v.CanAddr() { v = unsafeReflectValue(v) } if v.CanAddr() { v = v.Addr() } // Is it an error or Stringer? switch iface := v.Interface().(type) { case error: defer catchPanic(w, v) if cs.ContinueOnMethod { w.Write(openParenBytes) w.Write([]byte(iface.Error())) w.Write(closeParenBytes) w.Write(spaceBytes) return false } w.Write([]byte(iface.Error())) return true case fmt.Stringer: defer catchPanic(w, v) if cs.ContinueOnMethod { w.Write(openParenBytes) w.Write([]byte(iface.String())) w.Write(closeParenBytes) w.Write(spaceBytes) return false } w.Write([]byte(iface.String())) return true } return false } // printBool outputs a boolean value as true or false to Writer w. func printBool(w io.Writer, val bool) { if val { w.Write(trueBytes) } else { w.Write(falseBytes) } } // printInt outputs a signed integer value to Writer w. func printInt(w io.Writer, val int64, base int) { w.Write([]byte(strconv.FormatInt(val, base))) } // printUint outputs an unsigned integer value to Writer w. func printUint(w io.Writer, val uint64, base int) { w.Write([]byte(strconv.FormatUint(val, base))) } // printFloat outputs a floating point value using the specified precision, // which is expected to be 32 or 64bit, to Writer w. func printFloat(w io.Writer, val float64, precision int) { w.Write([]byte(strconv.FormatFloat(val, 'g', -1, precision))) } // printComplex outputs a complex value using the specified float precision // for the real and imaginary parts to Writer w. func printComplex(w io.Writer, c complex128, floatPrecision int) { r := real(c) w.Write(openParenBytes) w.Write([]byte(strconv.FormatFloat(r, 'g', -1, floatPrecision))) i := imag(c) if i >= 0 { w.Write(plusBytes) } w.Write([]byte(strconv.FormatFloat(i, 'g', -1, floatPrecision))) w.Write(iBytes) w.Write(closeParenBytes) } // printHexPtr outputs a uintptr formatted as hexadecimal with a leading '0x' // prefix to Writer w. func printHexPtr(w io.Writer, p uintptr) { // Null pointer. num := uint64(p) if num == 0 { w.Write(nilAngleBytes) return } // Max uint64 is 16 bytes in hex + 2 bytes for '0x' prefix buf := make([]byte, 18) // It's simpler to construct the hex string right to left. base := uint64(16) i := len(buf) - 1 for num >= base { buf[i] = hexDigits[num%base] num /= base i-- } buf[i] = hexDigits[num] // Add '0x' prefix. i-- buf[i] = 'x' i-- buf[i] = '0' // Strip unused leading bytes. buf = buf[i:] w.Write(buf) } // valuesSorter implements sort.Interface to allow a slice of reflect.Value // elements to be sorted. type valuesSorter struct { values []reflect.Value strings []string // either nil or same len and values cs *ConfigState } // newValuesSorter initializes a valuesSorter instance, which holds a set of // surrogate keys on which the data should be sorted. It uses flags in // ConfigState to decide if and how to populate those surrogate keys. func newValuesSorter(values []reflect.Value, cs *ConfigState) sort.Interface { vs := &valuesSorter{values: values, cs: cs} if canSortSimply(vs.values[0].Kind()) { return vs } if !cs.DisableMethods { vs.strings = make([]string, len(values)) for i := range vs.values { b := bytes.Buffer{} if !handleMethods(cs, &b, vs.values[i]) { vs.strings = nil break } vs.strings[i] = b.String() } } if vs.strings == nil && cs.SpewKeys { vs.strings = make([]string, len(values)) for i := range vs.values { vs.strings[i] = Sprintf("%#v", vs.values[i].Interface()) } } return vs } // canSortSimply tests whether a reflect.Kind is a primitive that can be sorted // directly, or whether it should be considered for sorting by surrogate keys // (if the ConfigState allows it). func canSortSimply(kind reflect.Kind) bool { // This switch parallels valueSortLess, except for the default case. switch kind { case reflect.Bool: return true case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: return true case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: return true case reflect.Float32, reflect.Float64: return true case reflect.String: return true case reflect.Uintptr: return true case reflect.Array: return true } return false } // Len returns the number of values in the slice. It is part of the // sort.Interface implementation. func (s *valuesSorter) Len() int { return len(s.values) } // Swap swaps the values at the passed indices. It is part of the // sort.Interface implementation. func (s *valuesSorter) Swap(i, j int) { s.values[i], s.values[j] = s.values[j], s.values[i] if s.strings != nil { s.strings[i], s.strings[j] = s.strings[j], s.strings[i] } } // valueSortLess returns whether the first value should sort before the second // value. It is used by valueSorter.Less as part of the sort.Interface // implementation. func valueSortLess(a, b reflect.Value) bool { switch a.Kind() { case reflect.Bool: return !a.Bool() && b.Bool() case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: return a.Int() < b.Int() case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: return a.Uint() < b.Uint() case reflect.Float32, reflect.Float64: return a.Float() < b.Float() case reflect.String: return a.String() < b.String() case reflect.Uintptr: return a.Uint() < b.Uint() case reflect.Array: // Compare the contents of both arrays. l := a.Len() for i := 0; i < l; i++ { av := a.Index(i) bv := b.Index(i) if av.Interface() == bv.Interface() { continue } return valueSortLess(av, bv) } } return a.String() < b.String() } // Less returns whether the value at index i should sort before the // value at index j. It is part of the sort.Interface implementation. func (s *valuesSorter) Less(i, j int) bool { if s.strings == nil { return valueSortLess(s.values[i], s.values[j]) } return s.strings[i] < s.strings[j] } // sortValues is a sort function that handles both native types and any type that // can be converted to error or Stringer. Other inputs are sorted according to // their Value.String() value to ensure display stability. func sortValues(values []reflect.Value, cs *ConfigState) { if len(values) == 0 { return } sort.Sort(newValuesSorter(values, cs)) } ================================================ FILE: vendor/github.com/davecgh/go-spew/spew/config.go ================================================ /* * Copyright (c) 2013-2016 Dave Collins * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ package spew import ( "bytes" "fmt" "io" "os" ) // ConfigState houses the configuration options used by spew to format and // display values. There is a global instance, Config, that is used to control // all top-level Formatter and Dump functionality. Each ConfigState instance // provides methods equivalent to the top-level functions. // // The zero value for ConfigState provides no indentation. You would typically // want to set it to a space or a tab. // // Alternatively, you can use NewDefaultConfig to get a ConfigState instance // with default settings. See the documentation of NewDefaultConfig for default // values. type ConfigState struct { // Indent specifies the string to use for each indentation level. The // global config instance that all top-level functions use set this to a // single space by default. If you would like more indentation, you might // set this to a tab with "\t" or perhaps two spaces with " ". Indent string // MaxDepth controls the maximum number of levels to descend into nested // data structures. The default, 0, means there is no limit. // // NOTE: Circular data structures are properly detected, so it is not // necessary to set this value unless you specifically want to limit deeply // nested data structures. MaxDepth int // DisableMethods specifies whether or not error and Stringer interfaces are // invoked for types that implement them. DisableMethods bool // DisablePointerMethods specifies whether or not to check for and invoke // error and Stringer interfaces on types which only accept a pointer // receiver when the current type is not a pointer. // // NOTE: This might be an unsafe action since calling one of these methods // with a pointer receiver could technically mutate the value, however, // in practice, types which choose to satisify an error or Stringer // interface with a pointer receiver should not be mutating their state // inside these interface methods. As a result, this option relies on // access to the unsafe package, so it will not have any effect when // running in environments without access to the unsafe package such as // Google App Engine or with the "safe" build tag specified. DisablePointerMethods bool // DisablePointerAddresses specifies whether to disable the printing of // pointer addresses. This is useful when diffing data structures in tests. DisablePointerAddresses bool // DisableCapacities specifies whether to disable the printing of capacities // for arrays, slices, maps and channels. This is useful when diffing // data structures in tests. DisableCapacities bool // ContinueOnMethod specifies whether or not recursion should continue once // a custom error or Stringer interface is invoked. The default, false, // means it will print the results of invoking the custom error or Stringer // interface and return immediately instead of continuing to recurse into // the internals of the data type. // // NOTE: This flag does not have any effect if method invocation is disabled // via the DisableMethods or DisablePointerMethods options. ContinueOnMethod bool // SortKeys specifies map keys should be sorted before being printed. Use // this to have a more deterministic, diffable output. Note that only // native types (bool, int, uint, floats, uintptr and string) and types // that support the error or Stringer interfaces (if methods are // enabled) are supported, with other types sorted according to the // reflect.Value.String() output which guarantees display stability. SortKeys bool // SpewKeys specifies that, as a last resort attempt, map keys should // be spewed to strings and sorted by those strings. This is only // considered if SortKeys is true. SpewKeys bool } // Config is the active configuration of the top-level functions. // The configuration can be changed by modifying the contents of spew.Config. var Config = ConfigState{Indent: " "} // Errorf is a wrapper for fmt.Errorf that treats each argument as if it were // passed with a Formatter interface returned by c.NewFormatter. It returns // the formatted string as a value that satisfies error. See NewFormatter // for formatting details. // // This function is shorthand for the following syntax: // // fmt.Errorf(format, c.NewFormatter(a), c.NewFormatter(b)) func (c *ConfigState) Errorf(format string, a ...interface{}) (err error) { return fmt.Errorf(format, c.convertArgs(a)...) } // Fprint is a wrapper for fmt.Fprint that treats each argument as if it were // passed with a Formatter interface returned by c.NewFormatter. It returns // the number of bytes written and any write error encountered. See // NewFormatter for formatting details. // // This function is shorthand for the following syntax: // // fmt.Fprint(w, c.NewFormatter(a), c.NewFormatter(b)) func (c *ConfigState) Fprint(w io.Writer, a ...interface{}) (n int, err error) { return fmt.Fprint(w, c.convertArgs(a)...) } // Fprintf is a wrapper for fmt.Fprintf that treats each argument as if it were // passed with a Formatter interface returned by c.NewFormatter. It returns // the number of bytes written and any write error encountered. See // NewFormatter for formatting details. // // This function is shorthand for the following syntax: // // fmt.Fprintf(w, format, c.NewFormatter(a), c.NewFormatter(b)) func (c *ConfigState) Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) { return fmt.Fprintf(w, format, c.convertArgs(a)...) } // Fprintln is a wrapper for fmt.Fprintln that treats each argument as if it // passed with a Formatter interface returned by c.NewFormatter. See // NewFormatter for formatting details. // // This function is shorthand for the following syntax: // // fmt.Fprintln(w, c.NewFormatter(a), c.NewFormatter(b)) func (c *ConfigState) Fprintln(w io.Writer, a ...interface{}) (n int, err error) { return fmt.Fprintln(w, c.convertArgs(a)...) } // Print is a wrapper for fmt.Print that treats each argument as if it were // passed with a Formatter interface returned by c.NewFormatter. It returns // the number of bytes written and any write error encountered. See // NewFormatter for formatting details. // // This function is shorthand for the following syntax: // // fmt.Print(c.NewFormatter(a), c.NewFormatter(b)) func (c *ConfigState) Print(a ...interface{}) (n int, err error) { return fmt.Print(c.convertArgs(a)...) } // Printf is a wrapper for fmt.Printf that treats each argument as if it were // passed with a Formatter interface returned by c.NewFormatter. It returns // the number of bytes written and any write error encountered. See // NewFormatter for formatting details. // // This function is shorthand for the following syntax: // // fmt.Printf(format, c.NewFormatter(a), c.NewFormatter(b)) func (c *ConfigState) Printf(format string, a ...interface{}) (n int, err error) { return fmt.Printf(format, c.convertArgs(a)...) } // Println is a wrapper for fmt.Println that treats each argument as if it were // passed with a Formatter interface returned by c.NewFormatter. It returns // the number of bytes written and any write error encountered. See // NewFormatter for formatting details. // // This function is shorthand for the following syntax: // // fmt.Println(c.NewFormatter(a), c.NewFormatter(b)) func (c *ConfigState) Println(a ...interface{}) (n int, err error) { return fmt.Println(c.convertArgs(a)...) } // Sprint is a wrapper for fmt.Sprint that treats each argument as if it were // passed with a Formatter interface returned by c.NewFormatter. It returns // the resulting string. See NewFormatter for formatting details. // // This function is shorthand for the following syntax: // // fmt.Sprint(c.NewFormatter(a), c.NewFormatter(b)) func (c *ConfigState) Sprint(a ...interface{}) string { return fmt.Sprint(c.convertArgs(a)...) } // Sprintf is a wrapper for fmt.Sprintf that treats each argument as if it were // passed with a Formatter interface returned by c.NewFormatter. It returns // the resulting string. See NewFormatter for formatting details. // // This function is shorthand for the following syntax: // // fmt.Sprintf(format, c.NewFormatter(a), c.NewFormatter(b)) func (c *ConfigState) Sprintf(format string, a ...interface{}) string { return fmt.Sprintf(format, c.convertArgs(a)...) } // Sprintln is a wrapper for fmt.Sprintln that treats each argument as if it // were passed with a Formatter interface returned by c.NewFormatter. It // returns the resulting string. See NewFormatter for formatting details. // // This function is shorthand for the following syntax: // // fmt.Sprintln(c.NewFormatter(a), c.NewFormatter(b)) func (c *ConfigState) Sprintln(a ...interface{}) string { return fmt.Sprintln(c.convertArgs(a)...) } /* NewFormatter returns a custom formatter that satisfies the fmt.Formatter interface. As a result, it integrates cleanly with standard fmt package printing functions. The formatter is useful for inline printing of smaller data types similar to the standard %v format specifier. The custom formatter only responds to the %v (most compact), %+v (adds pointer addresses), %#v (adds types), and %#+v (adds types and pointer addresses) verb combinations. Any other verbs such as %x and %q will be sent to the the standard fmt package for formatting. In addition, the custom formatter ignores the width and precision arguments (however they will still work on the format specifiers not handled by the custom formatter). Typically this function shouldn't be called directly. It is much easier to make use of the custom formatter by calling one of the convenience functions such as c.Printf, c.Println, or c.Printf. */ func (c *ConfigState) NewFormatter(v interface{}) fmt.Formatter { return newFormatter(c, v) } // Fdump formats and displays the passed arguments to io.Writer w. It formats // exactly the same as Dump. func (c *ConfigState) Fdump(w io.Writer, a ...interface{}) { fdump(c, w, a...) } /* Dump displays the passed parameters to standard out with newlines, customizable indentation, and additional debug information such as complete types and all pointer addresses used to indirect to the final value. It provides the following features over the built-in printing facilities provided by the fmt package: * Pointers are dereferenced and followed * Circular data structures are detected and handled properly * Custom Stringer/error interfaces are optionally invoked, including on unexported types * Custom types which only implement the Stringer/error interfaces via a pointer receiver are optionally invoked when passing non-pointer variables * Byte arrays and slices are dumped like the hexdump -C command which includes offsets, byte values in hex, and ASCII output The configuration options are controlled by modifying the public members of c. See ConfigState for options documentation. See Fdump if you would prefer dumping to an arbitrary io.Writer or Sdump to get the formatted result as a string. */ func (c *ConfigState) Dump(a ...interface{}) { fdump(c, os.Stdout, a...) } // Sdump returns a string with the passed arguments formatted exactly the same // as Dump. func (c *ConfigState) Sdump(a ...interface{}) string { var buf bytes.Buffer fdump(c, &buf, a...) return buf.String() } // convertArgs accepts a slice of arguments and returns a slice of the same // length with each argument converted to a spew Formatter interface using // the ConfigState associated with s. func (c *ConfigState) convertArgs(args []interface{}) (formatters []interface{}) { formatters = make([]interface{}, len(args)) for index, arg := range args { formatters[index] = newFormatter(c, arg) } return formatters } // NewDefaultConfig returns a ConfigState with the following default settings. // // Indent: " " // MaxDepth: 0 // DisableMethods: false // DisablePointerMethods: false // ContinueOnMethod: false // SortKeys: false func NewDefaultConfig() *ConfigState { return &ConfigState{Indent: " "} } ================================================ FILE: vendor/github.com/davecgh/go-spew/spew/doc.go ================================================ /* * Copyright (c) 2013-2016 Dave Collins * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ /* Package spew implements a deep pretty printer for Go data structures to aid in debugging. A quick overview of the additional features spew provides over the built-in printing facilities for Go data types are as follows: * Pointers are dereferenced and followed * Circular data structures are detected and handled properly * Custom Stringer/error interfaces are optionally invoked, including on unexported types * Custom types which only implement the Stringer/error interfaces via a pointer receiver are optionally invoked when passing non-pointer variables * Byte arrays and slices are dumped like the hexdump -C command which includes offsets, byte values in hex, and ASCII output (only when using Dump style) There are two different approaches spew allows for dumping Go data structures: * Dump style which prints with newlines, customizable indentation, and additional debug information such as types and all pointer addresses used to indirect to the final value * A custom Formatter interface that integrates cleanly with the standard fmt package and replaces %v, %+v, %#v, and %#+v to provide inline printing similar to the default %v while providing the additional functionality outlined above and passing unsupported format verbs such as %x and %q along to fmt Quick Start This section demonstrates how to quickly get started with spew. See the sections below for further details on formatting and configuration options. To dump a variable with full newlines, indentation, type, and pointer information use Dump, Fdump, or Sdump: spew.Dump(myVar1, myVar2, ...) spew.Fdump(someWriter, myVar1, myVar2, ...) str := spew.Sdump(myVar1, myVar2, ...) Alternatively, if you would prefer to use format strings with a compacted inline printing style, use the convenience wrappers Printf, Fprintf, etc with %v (most compact), %+v (adds pointer addresses), %#v (adds types), or %#+v (adds types and pointer addresses): spew.Printf("myVar1: %v -- myVar2: %+v", myVar1, myVar2) spew.Printf("myVar3: %#v -- myVar4: %#+v", myVar3, myVar4) spew.Fprintf(someWriter, "myVar1: %v -- myVar2: %+v", myVar1, myVar2) spew.Fprintf(someWriter, "myVar3: %#v -- myVar4: %#+v", myVar3, myVar4) Configuration Options Configuration of spew is handled by fields in the ConfigState type. For convenience, all of the top-level functions use a global state available via the spew.Config global. It is also possible to create a ConfigState instance that provides methods equivalent to the top-level functions. This allows concurrent configuration options. See the ConfigState documentation for more details. The following configuration options are available: * Indent String to use for each indentation level for Dump functions. It is a single space by default. A popular alternative is "\t". * MaxDepth Maximum number of levels to descend into nested data structures. There is no limit by default. * DisableMethods Disables invocation of error and Stringer interface methods. Method invocation is enabled by default. * DisablePointerMethods Disables invocation of error and Stringer interface methods on types which only accept pointer receivers from non-pointer variables. Pointer method invocation is enabled by default. * DisablePointerAddresses DisablePointerAddresses specifies whether to disable the printing of pointer addresses. This is useful when diffing data structures in tests. * DisableCapacities DisableCapacities specifies whether to disable the printing of capacities for arrays, slices, maps and channels. This is useful when diffing data structures in tests. * ContinueOnMethod Enables recursion into types after invoking error and Stringer interface methods. Recursion after method invocation is disabled by default. * SortKeys Specifies map keys should be sorted before being printed. Use this to have a more deterministic, diffable output. Note that only native types (bool, int, uint, floats, uintptr and string) and types which implement error or Stringer interfaces are supported with other types sorted according to the reflect.Value.String() output which guarantees display stability. Natural map order is used by default. * SpewKeys Specifies that, as a last resort attempt, map keys should be spewed to strings and sorted by those strings. This is only considered if SortKeys is true. Dump Usage Simply call spew.Dump with a list of variables you want to dump: spew.Dump(myVar1, myVar2, ...) You may also call spew.Fdump if you would prefer to output to an arbitrary io.Writer. For example, to dump to standard error: spew.Fdump(os.Stderr, myVar1, myVar2, ...) A third option is to call spew.Sdump to get the formatted output as a string: str := spew.Sdump(myVar1, myVar2, ...) Sample Dump Output See the Dump example for details on the setup of the types and variables being shown here. (main.Foo) { unexportedField: (*main.Bar)(0xf84002e210)({ flag: (main.Flag) flagTwo, data: (uintptr) }), ExportedField: (map[interface {}]interface {}) (len=1) { (string) (len=3) "one": (bool) true } } Byte (and uint8) arrays and slices are displayed uniquely like the hexdump -C command as shown. ([]uint8) (len=32 cap=32) { 00000000 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 |............... | 00000010 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 |!"#$%&'()*+,-./0| 00000020 31 32 |12| } Custom Formatter Spew provides a custom formatter that implements the fmt.Formatter interface so that it integrates cleanly with standard fmt package printing functions. The formatter is useful for inline printing of smaller data types similar to the standard %v format specifier. The custom formatter only responds to the %v (most compact), %+v (adds pointer addresses), %#v (adds types), or %#+v (adds types and pointer addresses) verb combinations. Any other verbs such as %x and %q will be sent to the the standard fmt package for formatting. In addition, the custom formatter ignores the width and precision arguments (however they will still work on the format specifiers not handled by the custom formatter). Custom Formatter Usage The simplest way to make use of the spew custom formatter is to call one of the convenience functions such as spew.Printf, spew.Println, or spew.Printf. The functions have syntax you are most likely already familiar with: spew.Printf("myVar1: %v -- myVar2: %+v", myVar1, myVar2) spew.Printf("myVar3: %#v -- myVar4: %#+v", myVar3, myVar4) spew.Println(myVar, myVar2) spew.Fprintf(os.Stderr, "myVar1: %v -- myVar2: %+v", myVar1, myVar2) spew.Fprintf(os.Stderr, "myVar3: %#v -- myVar4: %#+v", myVar3, myVar4) See the Index for the full list convenience functions. Sample Formatter Output Double pointer to a uint8: %v: <**>5 %+v: <**>(0xf8400420d0->0xf8400420c8)5 %#v: (**uint8)5 %#+v: (**uint8)(0xf8400420d0->0xf8400420c8)5 Pointer to circular struct with a uint8 field and a pointer to itself: %v: <*>{1 <*>} %+v: <*>(0xf84003e260){ui8:1 c:<*>(0xf84003e260)} %#v: (*main.circular){ui8:(uint8)1 c:(*main.circular)} %#+v: (*main.circular)(0xf84003e260){ui8:(uint8)1 c:(*main.circular)(0xf84003e260)} See the Printf example for details on the setup of variables being shown here. Errors Since it is possible for custom Stringer/error interfaces to panic, spew detects them and handles them internally by printing the panic information inline with the output. Since spew is intended to provide deep pretty printing capabilities on structures, it intentionally does not return any errors. */ package spew ================================================ FILE: vendor/github.com/davecgh/go-spew/spew/dump.go ================================================ /* * Copyright (c) 2013-2016 Dave Collins * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ package spew import ( "bytes" "encoding/hex" "fmt" "io" "os" "reflect" "regexp" "strconv" "strings" ) var ( // uint8Type is a reflect.Type representing a uint8. It is used to // convert cgo types to uint8 slices for hexdumping. uint8Type = reflect.TypeOf(uint8(0)) // cCharRE is a regular expression that matches a cgo char. // It is used to detect character arrays to hexdump them. cCharRE = regexp.MustCompile(`^.*\._Ctype_char$`) // cUnsignedCharRE is a regular expression that matches a cgo unsigned // char. It is used to detect unsigned character arrays to hexdump // them. cUnsignedCharRE = regexp.MustCompile(`^.*\._Ctype_unsignedchar$`) // cUint8tCharRE is a regular expression that matches a cgo uint8_t. // It is used to detect uint8_t arrays to hexdump them. cUint8tCharRE = regexp.MustCompile(`^.*\._Ctype_uint8_t$`) ) // dumpState contains information about the state of a dump operation. type dumpState struct { w io.Writer depth int pointers map[uintptr]int ignoreNextType bool ignoreNextIndent bool cs *ConfigState } // indent performs indentation according to the depth level and cs.Indent // option. func (d *dumpState) indent() { if d.ignoreNextIndent { d.ignoreNextIndent = false return } d.w.Write(bytes.Repeat([]byte(d.cs.Indent), d.depth)) } // unpackValue returns values inside of non-nil interfaces when possible. // This is useful for data types like structs, arrays, slices, and maps which // can contain varying types packed inside an interface. func (d *dumpState) unpackValue(v reflect.Value) reflect.Value { if v.Kind() == reflect.Interface && !v.IsNil() { v = v.Elem() } return v } // dumpPtr handles formatting of pointers by indirecting them as necessary. func (d *dumpState) dumpPtr(v reflect.Value) { // Remove pointers at or below the current depth from map used to detect // circular refs. for k, depth := range d.pointers { if depth >= d.depth { delete(d.pointers, k) } } // Keep list of all dereferenced pointers to show later. pointerChain := make([]uintptr, 0) // Figure out how many levels of indirection there are by dereferencing // pointers and unpacking interfaces down the chain while detecting circular // references. nilFound := false cycleFound := false indirects := 0 ve := v for ve.Kind() == reflect.Ptr { if ve.IsNil() { nilFound = true break } indirects++ addr := ve.Pointer() pointerChain = append(pointerChain, addr) if pd, ok := d.pointers[addr]; ok && pd < d.depth { cycleFound = true indirects-- break } d.pointers[addr] = d.depth ve = ve.Elem() if ve.Kind() == reflect.Interface { if ve.IsNil() { nilFound = true break } ve = ve.Elem() } } // Display type information. d.w.Write(openParenBytes) d.w.Write(bytes.Repeat(asteriskBytes, indirects)) d.w.Write([]byte(ve.Type().String())) d.w.Write(closeParenBytes) // Display pointer information. if !d.cs.DisablePointerAddresses && len(pointerChain) > 0 { d.w.Write(openParenBytes) for i, addr := range pointerChain { if i > 0 { d.w.Write(pointerChainBytes) } printHexPtr(d.w, addr) } d.w.Write(closeParenBytes) } // Display dereferenced value. d.w.Write(openParenBytes) switch { case nilFound: d.w.Write(nilAngleBytes) case cycleFound: d.w.Write(circularBytes) default: d.ignoreNextType = true d.dump(ve) } d.w.Write(closeParenBytes) } // dumpSlice handles formatting of arrays and slices. Byte (uint8 under // reflection) arrays and slices are dumped in hexdump -C fashion. func (d *dumpState) dumpSlice(v reflect.Value) { // Determine whether this type should be hex dumped or not. Also, // for types which should be hexdumped, try to use the underlying data // first, then fall back to trying to convert them to a uint8 slice. var buf []uint8 doConvert := false doHexDump := false numEntries := v.Len() if numEntries > 0 { vt := v.Index(0).Type() vts := vt.String() switch { // C types that need to be converted. case cCharRE.MatchString(vts): fallthrough case cUnsignedCharRE.MatchString(vts): fallthrough case cUint8tCharRE.MatchString(vts): doConvert = true // Try to use existing uint8 slices and fall back to converting // and copying if that fails. case vt.Kind() == reflect.Uint8: // We need an addressable interface to convert the type // to a byte slice. However, the reflect package won't // give us an interface on certain things like // unexported struct fields in order to enforce // visibility rules. We use unsafe, when available, to // bypass these restrictions since this package does not // mutate the values. vs := v if !vs.CanInterface() || !vs.CanAddr() { vs = unsafeReflectValue(vs) } if !UnsafeDisabled { vs = vs.Slice(0, numEntries) // Use the existing uint8 slice if it can be // type asserted. iface := vs.Interface() if slice, ok := iface.([]uint8); ok { buf = slice doHexDump = true break } } // The underlying data needs to be converted if it can't // be type asserted to a uint8 slice. doConvert = true } // Copy and convert the underlying type if needed. if doConvert && vt.ConvertibleTo(uint8Type) { // Convert and copy each element into a uint8 byte // slice. buf = make([]uint8, numEntries) for i := 0; i < numEntries; i++ { vv := v.Index(i) buf[i] = uint8(vv.Convert(uint8Type).Uint()) } doHexDump = true } } // Hexdump the entire slice as needed. if doHexDump { indent := strings.Repeat(d.cs.Indent, d.depth) str := indent + hex.Dump(buf) str = strings.Replace(str, "\n", "\n"+indent, -1) str = strings.TrimRight(str, d.cs.Indent) d.w.Write([]byte(str)) return } // Recursively call dump for each item. for i := 0; i < numEntries; i++ { d.dump(d.unpackValue(v.Index(i))) if i < (numEntries - 1) { d.w.Write(commaNewlineBytes) } else { d.w.Write(newlineBytes) } } } // dump is the main workhorse for dumping a value. It uses the passed reflect // value to figure out what kind of object we are dealing with and formats it // appropriately. It is a recursive function, however circular data structures // are detected and handled properly. func (d *dumpState) dump(v reflect.Value) { // Handle invalid reflect values immediately. kind := v.Kind() if kind == reflect.Invalid { d.w.Write(invalidAngleBytes) return } // Handle pointers specially. if kind == reflect.Ptr { d.indent() d.dumpPtr(v) return } // Print type information unless already handled elsewhere. if !d.ignoreNextType { d.indent() d.w.Write(openParenBytes) d.w.Write([]byte(v.Type().String())) d.w.Write(closeParenBytes) d.w.Write(spaceBytes) } d.ignoreNextType = false // Display length and capacity if the built-in len and cap functions // work with the value's kind and the len/cap itself is non-zero. valueLen, valueCap := 0, 0 switch v.Kind() { case reflect.Array, reflect.Slice, reflect.Chan: valueLen, valueCap = v.Len(), v.Cap() case reflect.Map, reflect.String: valueLen = v.Len() } if valueLen != 0 || !d.cs.DisableCapacities && valueCap != 0 { d.w.Write(openParenBytes) if valueLen != 0 { d.w.Write(lenEqualsBytes) printInt(d.w, int64(valueLen), 10) } if !d.cs.DisableCapacities && valueCap != 0 { if valueLen != 0 { d.w.Write(spaceBytes) } d.w.Write(capEqualsBytes) printInt(d.w, int64(valueCap), 10) } d.w.Write(closeParenBytes) d.w.Write(spaceBytes) } // Call Stringer/error interfaces if they exist and the handle methods flag // is enabled if !d.cs.DisableMethods { if (kind != reflect.Invalid) && (kind != reflect.Interface) { if handled := handleMethods(d.cs, d.w, v); handled { return } } } switch kind { case reflect.Invalid: // Do nothing. We should never get here since invalid has already // been handled above. case reflect.Bool: printBool(d.w, v.Bool()) case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: printInt(d.w, v.Int(), 10) case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: printUint(d.w, v.Uint(), 10) case reflect.Float32: printFloat(d.w, v.Float(), 32) case reflect.Float64: printFloat(d.w, v.Float(), 64) case reflect.Complex64: printComplex(d.w, v.Complex(), 32) case reflect.Complex128: printComplex(d.w, v.Complex(), 64) case reflect.Slice: if v.IsNil() { d.w.Write(nilAngleBytes) break } fallthrough case reflect.Array: d.w.Write(openBraceNewlineBytes) d.depth++ if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) { d.indent() d.w.Write(maxNewlineBytes) } else { d.dumpSlice(v) } d.depth-- d.indent() d.w.Write(closeBraceBytes) case reflect.String: d.w.Write([]byte(strconv.Quote(v.String()))) case reflect.Interface: // The only time we should get here is for nil interfaces due to // unpackValue calls. if v.IsNil() { d.w.Write(nilAngleBytes) } case reflect.Ptr: // Do nothing. We should never get here since pointers have already // been handled above. case reflect.Map: // nil maps should be indicated as different than empty maps if v.IsNil() { d.w.Write(nilAngleBytes) break } d.w.Write(openBraceNewlineBytes) d.depth++ if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) { d.indent() d.w.Write(maxNewlineBytes) } else { numEntries := v.Len() keys := v.MapKeys() if d.cs.SortKeys { sortValues(keys, d.cs) } for i, key := range keys { d.dump(d.unpackValue(key)) d.w.Write(colonSpaceBytes) d.ignoreNextIndent = true d.dump(d.unpackValue(v.MapIndex(key))) if i < (numEntries - 1) { d.w.Write(commaNewlineBytes) } else { d.w.Write(newlineBytes) } } } d.depth-- d.indent() d.w.Write(closeBraceBytes) case reflect.Struct: d.w.Write(openBraceNewlineBytes) d.depth++ if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) { d.indent() d.w.Write(maxNewlineBytes) } else { vt := v.Type() numFields := v.NumField() for i := 0; i < numFields; i++ { d.indent() vtf := vt.Field(i) d.w.Write([]byte(vtf.Name)) d.w.Write(colonSpaceBytes) d.ignoreNextIndent = true d.dump(d.unpackValue(v.Field(i))) if i < (numFields - 1) { d.w.Write(commaNewlineBytes) } else { d.w.Write(newlineBytes) } } } d.depth-- d.indent() d.w.Write(closeBraceBytes) case reflect.Uintptr: printHexPtr(d.w, uintptr(v.Uint())) case reflect.UnsafePointer, reflect.Chan, reflect.Func: printHexPtr(d.w, v.Pointer()) // There were not any other types at the time this code was written, but // fall back to letting the default fmt package handle it in case any new // types are added. default: if v.CanInterface() { fmt.Fprintf(d.w, "%v", v.Interface()) } else { fmt.Fprintf(d.w, "%v", v.String()) } } } // fdump is a helper function to consolidate the logic from the various public // methods which take varying writers and config states. func fdump(cs *ConfigState, w io.Writer, a ...interface{}) { for _, arg := range a { if arg == nil { w.Write(interfaceBytes) w.Write(spaceBytes) w.Write(nilAngleBytes) w.Write(newlineBytes) continue } d := dumpState{w: w, cs: cs} d.pointers = make(map[uintptr]int) d.dump(reflect.ValueOf(arg)) d.w.Write(newlineBytes) } } // Fdump formats and displays the passed arguments to io.Writer w. It formats // exactly the same as Dump. func Fdump(w io.Writer, a ...interface{}) { fdump(&Config, w, a...) } // Sdump returns a string with the passed arguments formatted exactly the same // as Dump. func Sdump(a ...interface{}) string { var buf bytes.Buffer fdump(&Config, &buf, a...) return buf.String() } /* Dump displays the passed parameters to standard out with newlines, customizable indentation, and additional debug information such as complete types and all pointer addresses used to indirect to the final value. It provides the following features over the built-in printing facilities provided by the fmt package: * Pointers are dereferenced and followed * Circular data structures are detected and handled properly * Custom Stringer/error interfaces are optionally invoked, including on unexported types * Custom types which only implement the Stringer/error interfaces via a pointer receiver are optionally invoked when passing non-pointer variables * Byte arrays and slices are dumped like the hexdump -C command which includes offsets, byte values in hex, and ASCII output The configuration options are controlled by an exported package global, spew.Config. See ConfigState for options documentation. See Fdump if you would prefer dumping to an arbitrary io.Writer or Sdump to get the formatted result as a string. */ func Dump(a ...interface{}) { fdump(&Config, os.Stdout, a...) } ================================================ FILE: vendor/github.com/davecgh/go-spew/spew/format.go ================================================ /* * Copyright (c) 2013-2016 Dave Collins * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ package spew import ( "bytes" "fmt" "reflect" "strconv" "strings" ) // supportedFlags is a list of all the character flags supported by fmt package. const supportedFlags = "0-+# " // formatState implements the fmt.Formatter interface and contains information // about the state of a formatting operation. The NewFormatter function can // be used to get a new Formatter which can be used directly as arguments // in standard fmt package printing calls. type formatState struct { value interface{} fs fmt.State depth int pointers map[uintptr]int ignoreNextType bool cs *ConfigState } // buildDefaultFormat recreates the original format string without precision // and width information to pass in to fmt.Sprintf in the case of an // unrecognized type. Unless new types are added to the language, this // function won't ever be called. func (f *formatState) buildDefaultFormat() (format string) { buf := bytes.NewBuffer(percentBytes) for _, flag := range supportedFlags { if f.fs.Flag(int(flag)) { buf.WriteRune(flag) } } buf.WriteRune('v') format = buf.String() return format } // constructOrigFormat recreates the original format string including precision // and width information to pass along to the standard fmt package. This allows // automatic deferral of all format strings this package doesn't support. func (f *formatState) constructOrigFormat(verb rune) (format string) { buf := bytes.NewBuffer(percentBytes) for _, flag := range supportedFlags { if f.fs.Flag(int(flag)) { buf.WriteRune(flag) } } if width, ok := f.fs.Width(); ok { buf.WriteString(strconv.Itoa(width)) } if precision, ok := f.fs.Precision(); ok { buf.Write(precisionBytes) buf.WriteString(strconv.Itoa(precision)) } buf.WriteRune(verb) format = buf.String() return format } // unpackValue returns values inside of non-nil interfaces when possible and // ensures that types for values which have been unpacked from an interface // are displayed when the show types flag is also set. // This is useful for data types like structs, arrays, slices, and maps which // can contain varying types packed inside an interface. func (f *formatState) unpackValue(v reflect.Value) reflect.Value { if v.Kind() == reflect.Interface { f.ignoreNextType = false if !v.IsNil() { v = v.Elem() } } return v } // formatPtr handles formatting of pointers by indirecting them as necessary. func (f *formatState) formatPtr(v reflect.Value) { // Display nil if top level pointer is nil. showTypes := f.fs.Flag('#') if v.IsNil() && (!showTypes || f.ignoreNextType) { f.fs.Write(nilAngleBytes) return } // Remove pointers at or below the current depth from map used to detect // circular refs. for k, depth := range f.pointers { if depth >= f.depth { delete(f.pointers, k) } } // Keep list of all dereferenced pointers to possibly show later. pointerChain := make([]uintptr, 0) // Figure out how many levels of indirection there are by derferencing // pointers and unpacking interfaces down the chain while detecting circular // references. nilFound := false cycleFound := false indirects := 0 ve := v for ve.Kind() == reflect.Ptr { if ve.IsNil() { nilFound = true break } indirects++ addr := ve.Pointer() pointerChain = append(pointerChain, addr) if pd, ok := f.pointers[addr]; ok && pd < f.depth { cycleFound = true indirects-- break } f.pointers[addr] = f.depth ve = ve.Elem() if ve.Kind() == reflect.Interface { if ve.IsNil() { nilFound = true break } ve = ve.Elem() } } // Display type or indirection level depending on flags. if showTypes && !f.ignoreNextType { f.fs.Write(openParenBytes) f.fs.Write(bytes.Repeat(asteriskBytes, indirects)) f.fs.Write([]byte(ve.Type().String())) f.fs.Write(closeParenBytes) } else { if nilFound || cycleFound { indirects += strings.Count(ve.Type().String(), "*") } f.fs.Write(openAngleBytes) f.fs.Write([]byte(strings.Repeat("*", indirects))) f.fs.Write(closeAngleBytes) } // Display pointer information depending on flags. if f.fs.Flag('+') && (len(pointerChain) > 0) { f.fs.Write(openParenBytes) for i, addr := range pointerChain { if i > 0 { f.fs.Write(pointerChainBytes) } printHexPtr(f.fs, addr) } f.fs.Write(closeParenBytes) } // Display dereferenced value. switch { case nilFound: f.fs.Write(nilAngleBytes) case cycleFound: f.fs.Write(circularShortBytes) default: f.ignoreNextType = true f.format(ve) } } // format is the main workhorse for providing the Formatter interface. It // uses the passed reflect value to figure out what kind of object we are // dealing with and formats it appropriately. It is a recursive function, // however circular data structures are detected and handled properly. func (f *formatState) format(v reflect.Value) { // Handle invalid reflect values immediately. kind := v.Kind() if kind == reflect.Invalid { f.fs.Write(invalidAngleBytes) return } // Handle pointers specially. if kind == reflect.Ptr { f.formatPtr(v) return } // Print type information unless already handled elsewhere. if !f.ignoreNextType && f.fs.Flag('#') { f.fs.Write(openParenBytes) f.fs.Write([]byte(v.Type().String())) f.fs.Write(closeParenBytes) } f.ignoreNextType = false // Call Stringer/error interfaces if they exist and the handle methods // flag is enabled. if !f.cs.DisableMethods { if (kind != reflect.Invalid) && (kind != reflect.Interface) { if handled := handleMethods(f.cs, f.fs, v); handled { return } } } switch kind { case reflect.Invalid: // Do nothing. We should never get here since invalid has already // been handled above. case reflect.Bool: printBool(f.fs, v.Bool()) case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: printInt(f.fs, v.Int(), 10) case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: printUint(f.fs, v.Uint(), 10) case reflect.Float32: printFloat(f.fs, v.Float(), 32) case reflect.Float64: printFloat(f.fs, v.Float(), 64) case reflect.Complex64: printComplex(f.fs, v.Complex(), 32) case reflect.Complex128: printComplex(f.fs, v.Complex(), 64) case reflect.Slice: if v.IsNil() { f.fs.Write(nilAngleBytes) break } fallthrough case reflect.Array: f.fs.Write(openBracketBytes) f.depth++ if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) { f.fs.Write(maxShortBytes) } else { numEntries := v.Len() for i := 0; i < numEntries; i++ { if i > 0 { f.fs.Write(spaceBytes) } f.ignoreNextType = true f.format(f.unpackValue(v.Index(i))) } } f.depth-- f.fs.Write(closeBracketBytes) case reflect.String: f.fs.Write([]byte(v.String())) case reflect.Interface: // The only time we should get here is for nil interfaces due to // unpackValue calls. if v.IsNil() { f.fs.Write(nilAngleBytes) } case reflect.Ptr: // Do nothing. We should never get here since pointers have already // been handled above. case reflect.Map: // nil maps should be indicated as different than empty maps if v.IsNil() { f.fs.Write(nilAngleBytes) break } f.fs.Write(openMapBytes) f.depth++ if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) { f.fs.Write(maxShortBytes) } else { keys := v.MapKeys() if f.cs.SortKeys { sortValues(keys, f.cs) } for i, key := range keys { if i > 0 { f.fs.Write(spaceBytes) } f.ignoreNextType = true f.format(f.unpackValue(key)) f.fs.Write(colonBytes) f.ignoreNextType = true f.format(f.unpackValue(v.MapIndex(key))) } } f.depth-- f.fs.Write(closeMapBytes) case reflect.Struct: numFields := v.NumField() f.fs.Write(openBraceBytes) f.depth++ if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) { f.fs.Write(maxShortBytes) } else { vt := v.Type() for i := 0; i < numFields; i++ { if i > 0 { f.fs.Write(spaceBytes) } vtf := vt.Field(i) if f.fs.Flag('+') || f.fs.Flag('#') { f.fs.Write([]byte(vtf.Name)) f.fs.Write(colonBytes) } f.format(f.unpackValue(v.Field(i))) } } f.depth-- f.fs.Write(closeBraceBytes) case reflect.Uintptr: printHexPtr(f.fs, uintptr(v.Uint())) case reflect.UnsafePointer, reflect.Chan, reflect.Func: printHexPtr(f.fs, v.Pointer()) // There were not any other types at the time this code was written, but // fall back to letting the default fmt package handle it if any get added. default: format := f.buildDefaultFormat() if v.CanInterface() { fmt.Fprintf(f.fs, format, v.Interface()) } else { fmt.Fprintf(f.fs, format, v.String()) } } } // Format satisfies the fmt.Formatter interface. See NewFormatter for usage // details. func (f *formatState) Format(fs fmt.State, verb rune) { f.fs = fs // Use standard formatting for verbs that are not v. if verb != 'v' { format := f.constructOrigFormat(verb) fmt.Fprintf(fs, format, f.value) return } if f.value == nil { if fs.Flag('#') { fs.Write(interfaceBytes) } fs.Write(nilAngleBytes) return } f.format(reflect.ValueOf(f.value)) } // newFormatter is a helper function to consolidate the logic from the various // public methods which take varying config states. func newFormatter(cs *ConfigState, v interface{}) fmt.Formatter { fs := &formatState{value: v, cs: cs} fs.pointers = make(map[uintptr]int) return fs } /* NewFormatter returns a custom formatter that satisfies the fmt.Formatter interface. As a result, it integrates cleanly with standard fmt package printing functions. The formatter is useful for inline printing of smaller data types similar to the standard %v format specifier. The custom formatter only responds to the %v (most compact), %+v (adds pointer addresses), %#v (adds types), or %#+v (adds types and pointer addresses) verb combinations. Any other verbs such as %x and %q will be sent to the the standard fmt package for formatting. In addition, the custom formatter ignores the width and precision arguments (however they will still work on the format specifiers not handled by the custom formatter). Typically this function shouldn't be called directly. It is much easier to make use of the custom formatter by calling one of the convenience functions such as Printf, Println, or Fprintf. */ func NewFormatter(v interface{}) fmt.Formatter { return newFormatter(&Config, v) } ================================================ FILE: vendor/github.com/davecgh/go-spew/spew/spew.go ================================================ /* * Copyright (c) 2013-2016 Dave Collins * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ package spew import ( "fmt" "io" ) // Errorf is a wrapper for fmt.Errorf that treats each argument as if it were // passed with a default Formatter interface returned by NewFormatter. It // returns the formatted string as a value that satisfies error. See // NewFormatter for formatting details. // // This function is shorthand for the following syntax: // // fmt.Errorf(format, spew.NewFormatter(a), spew.NewFormatter(b)) func Errorf(format string, a ...interface{}) (err error) { return fmt.Errorf(format, convertArgs(a)...) } // Fprint is a wrapper for fmt.Fprint that treats each argument as if it were // passed with a default Formatter interface returned by NewFormatter. It // returns the number of bytes written and any write error encountered. See // NewFormatter for formatting details. // // This function is shorthand for the following syntax: // // fmt.Fprint(w, spew.NewFormatter(a), spew.NewFormatter(b)) func Fprint(w io.Writer, a ...interface{}) (n int, err error) { return fmt.Fprint(w, convertArgs(a)...) } // Fprintf is a wrapper for fmt.Fprintf that treats each argument as if it were // passed with a default Formatter interface returned by NewFormatter. It // returns the number of bytes written and any write error encountered. See // NewFormatter for formatting details. // // This function is shorthand for the following syntax: // // fmt.Fprintf(w, format, spew.NewFormatter(a), spew.NewFormatter(b)) func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) { return fmt.Fprintf(w, format, convertArgs(a)...) } // Fprintln is a wrapper for fmt.Fprintln that treats each argument as if it // passed with a default Formatter interface returned by NewFormatter. See // NewFormatter for formatting details. // // This function is shorthand for the following syntax: // // fmt.Fprintln(w, spew.NewFormatter(a), spew.NewFormatter(b)) func Fprintln(w io.Writer, a ...interface{}) (n int, err error) { return fmt.Fprintln(w, convertArgs(a)...) } // Print is a wrapper for fmt.Print that treats each argument as if it were // passed with a default Formatter interface returned by NewFormatter. It // returns the number of bytes written and any write error encountered. See // NewFormatter for formatting details. // // This function is shorthand for the following syntax: // // fmt.Print(spew.NewFormatter(a), spew.NewFormatter(b)) func Print(a ...interface{}) (n int, err error) { return fmt.Print(convertArgs(a)...) } // Printf is a wrapper for fmt.Printf that treats each argument as if it were // passed with a default Formatter interface returned by NewFormatter. It // returns the number of bytes written and any write error encountered. See // NewFormatter for formatting details. // // This function is shorthand for the following syntax: // // fmt.Printf(format, spew.NewFormatter(a), spew.NewFormatter(b)) func Printf(format string, a ...interface{}) (n int, err error) { return fmt.Printf(format, convertArgs(a)...) } // Println is a wrapper for fmt.Println that treats each argument as if it were // passed with a default Formatter interface returned by NewFormatter. It // returns the number of bytes written and any write error encountered. See // NewFormatter for formatting details. // // This function is shorthand for the following syntax: // // fmt.Println(spew.NewFormatter(a), spew.NewFormatter(b)) func Println(a ...interface{}) (n int, err error) { return fmt.Println(convertArgs(a)...) } // Sprint is a wrapper for fmt.Sprint that treats each argument as if it were // passed with a default Formatter interface returned by NewFormatter. It // returns the resulting string. See NewFormatter for formatting details. // // This function is shorthand for the following syntax: // // fmt.Sprint(spew.NewFormatter(a), spew.NewFormatter(b)) func Sprint(a ...interface{}) string { return fmt.Sprint(convertArgs(a)...) } // Sprintf is a wrapper for fmt.Sprintf that treats each argument as if it were // passed with a default Formatter interface returned by NewFormatter. It // returns the resulting string. See NewFormatter for formatting details. // // This function is shorthand for the following syntax: // // fmt.Sprintf(format, spew.NewFormatter(a), spew.NewFormatter(b)) func Sprintf(format string, a ...interface{}) string { return fmt.Sprintf(format, convertArgs(a)...) } // Sprintln is a wrapper for fmt.Sprintln that treats each argument as if it // were passed with a default Formatter interface returned by NewFormatter. It // returns the resulting string. See NewFormatter for formatting details. // // This function is shorthand for the following syntax: // // fmt.Sprintln(spew.NewFormatter(a), spew.NewFormatter(b)) func Sprintln(a ...interface{}) string { return fmt.Sprintln(convertArgs(a)...) } // convertArgs accepts a slice of arguments and returns a slice of the same // length with each argument converted to a default spew Formatter interface. func convertArgs(args []interface{}) (formatters []interface{}) { formatters = make([]interface{}, len(args)) for index, arg := range args { formatters[index] = NewFormatter(arg) } return formatters } ================================================ FILE: vendor/github.com/facebookgo/grace/gracenet/net.go ================================================ // Package gracenet provides a family of Listen functions that either open a // fresh connection or provide an inherited connection from when the process // was started. The behave like their counterparts in the net package, but // transparently provide support for graceful restarts without dropping // connections. This is provided in a systemd socket activation compatible form // to allow using socket activation. // // BUG: Doesn't handle closing of listeners. package gracenet import ( "fmt" "net" "os" "os/exec" "strconv" "strings" "sync" ) const ( // Used to indicate a graceful restart in the new process. envCountKey = "LISTEN_FDS" envCountKeyPrefix = envCountKey + "=" ) // In order to keep the working directory the same as when we started we record // it at startup. var originalWD, _ = os.Getwd() // Net provides the family of Listen functions and maintains the associated // state. Typically you will have only once instance of Net per application. type Net struct { inherited []net.Listener active []net.Listener mutex sync.Mutex inheritOnce sync.Once // used in tests to override the default behavior of starting from fd 3. fdStart int } func (n *Net) inherit() error { var retErr error n.inheritOnce.Do(func() { n.mutex.Lock() defer n.mutex.Unlock() countStr := os.Getenv(envCountKey) if countStr == "" { return } count, err := strconv.Atoi(countStr) if err != nil { retErr = fmt.Errorf("found invalid count value: %s=%s", envCountKey, countStr) return } // In tests this may be overridden. fdStart := n.fdStart if fdStart == 0 { // In normal operations if we are inheriting, the listeners will begin at // fd 3. fdStart = 3 } for i := fdStart; i < fdStart+count; i++ { file := os.NewFile(uintptr(i), "listener") l, err := net.FileListener(file) if err != nil { file.Close() retErr = fmt.Errorf("error inheriting socket fd %d: %s", i, err) return } if err := file.Close(); err != nil { retErr = fmt.Errorf("error closing inherited socket fd %d: %s", i, err) return } n.inherited = append(n.inherited, l) } }) return retErr } // Listen announces on the local network address laddr. The network net must be // a stream-oriented network: "tcp", "tcp4", "tcp6", "unix" or "unixpacket". It // returns an inherited net.Listener for the matching network and address, or // creates a new one using net.Listen. func (n *Net) Listen(nett, laddr string) (net.Listener, error) { switch nett { default: return nil, net.UnknownNetworkError(nett) case "tcp", "tcp4", "tcp6": addr, err := net.ResolveTCPAddr(nett, laddr) if err != nil { return nil, err } return n.ListenTCP(nett, addr) case "unix", "unixpacket", "invalid_unix_net_for_test": addr, err := net.ResolveUnixAddr(nett, laddr) if err != nil { return nil, err } return n.ListenUnix(nett, addr) } } // ListenTCP announces on the local network address laddr. The network net must // be: "tcp", "tcp4" or "tcp6". It returns an inherited net.Listener for the // matching network and address, or creates a new one using net.ListenTCP. func (n *Net) ListenTCP(nett string, laddr *net.TCPAddr) (*net.TCPListener, error) { if err := n.inherit(); err != nil { return nil, err } n.mutex.Lock() defer n.mutex.Unlock() // look for an inherited listener for i, l := range n.inherited { if l == nil { // we nil used inherited listeners continue } if isSameAddr(l.Addr(), laddr) { n.inherited[i] = nil n.active = append(n.active, l) return l.(*net.TCPListener), nil } } // make a fresh listener l, err := net.ListenTCP(nett, laddr) if err != nil { return nil, err } n.active = append(n.active, l) return l, nil } // ListenUnix announces on the local network address laddr. The network net // must be a: "unix" or "unixpacket". It returns an inherited net.Listener for // the matching network and address, or creates a new one using net.ListenUnix. func (n *Net) ListenUnix(nett string, laddr *net.UnixAddr) (*net.UnixListener, error) { if err := n.inherit(); err != nil { return nil, err } n.mutex.Lock() defer n.mutex.Unlock() // look for an inherited listener for i, l := range n.inherited { if l == nil { // we nil used inherited listeners continue } if isSameAddr(l.Addr(), laddr) { n.inherited[i] = nil n.active = append(n.active, l) return l.(*net.UnixListener), nil } } // make a fresh listener l, err := net.ListenUnix(nett, laddr) if err != nil { return nil, err } n.active = append(n.active, l) return l, nil } // activeListeners returns a snapshot copy of the active listeners. func (n *Net) activeListeners() ([]net.Listener, error) { n.mutex.Lock() defer n.mutex.Unlock() ls := make([]net.Listener, len(n.active)) copy(ls, n.active) return ls, nil } func isSameAddr(a1, a2 net.Addr) bool { if a1.Network() != a2.Network() { return false } a1s := a1.String() a2s := a2.String() if a1s == a2s { return true } // This allows for ipv6 vs ipv4 local addresses to compare as equal. This // scenario is common when listening on localhost. const ipv6prefix = "[::]" a1s = strings.TrimPrefix(a1s, ipv6prefix) a2s = strings.TrimPrefix(a2s, ipv6prefix) const ipv4prefix = "0.0.0.0" a1s = strings.TrimPrefix(a1s, ipv4prefix) a2s = strings.TrimPrefix(a2s, ipv4prefix) return a1s == a2s } // StartProcess starts a new process passing it the active listeners. It // doesn't fork, but starts a new process using the same environment and // arguments as when it was originally started. This allows for a newly // deployed binary to be started. It returns the pid of the newly started // process when successful. func (n *Net) StartProcess() (int, error) { listeners, err := n.activeListeners() if err != nil { return 0, err } // Extract the fds from the listeners. files := make([]*os.File, len(listeners)) for i, l := range listeners { files[i], err = l.(filer).File() if err != nil { return 0, err } defer files[i].Close() } // Use the original binary location. This works with symlinks such that if // the file it points to has been changed we will use the updated symlink. argv0, err := exec.LookPath(os.Args[0]) if err != nil { return 0, err } // Pass on the environment and replace the old count key with the new one. var env []string for _, v := range os.Environ() { if !strings.HasPrefix(v, envCountKeyPrefix) { env = append(env, v) } } env = append(env, fmt.Sprintf("%s%d", envCountKeyPrefix, len(listeners))) allFiles := append([]*os.File{os.Stdin, os.Stdout, os.Stderr}, files...) process, err := os.StartProcess(argv0, os.Args, &os.ProcAttr{ Dir: originalWD, Env: env, Files: allFiles, }) if err != nil { return 0, err } return process.Pid, nil } type filer interface { File() (*os.File, error) } ================================================ FILE: vendor/github.com/fortytw2/leaktest/.travis.yml ================================================ language: go go: - 1.8 - 1.9 - "1.10" - "1.11" - tip script: - go test -v -race -parallel 5 -coverprofile=coverage.txt -covermode=atomic ./ - go test github.com/fortytw2/leaktest -run ^TestEmptyLeak$ before_install: - pip install --user codecov after_success: - codecov ================================================ FILE: vendor/github.com/fortytw2/leaktest/LICENSE ================================================ Copyright (c) 2012 The Go Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: vendor/github.com/fortytw2/leaktest/README.md ================================================ ## Leaktest [![Build Status](https://travis-ci.org/fortytw2/leaktest.svg?branch=master)](https://travis-ci.org/fortytw2/leaktest) [![codecov](https://codecov.io/gh/fortytw2/leaktest/branch/master/graph/badge.svg)](https://codecov.io/gh/fortytw2/leaktest) [![Sourcegraph](https://sourcegraph.com/github.com/fortytw2/leaktest/-/badge.svg)](https://sourcegraph.com/github.com/fortytw2/leaktest?badge) [![Documentation](https://godoc.org/github.com/fortytw2/gpt?status.svg)](http://godoc.org/github.com/fortytw2/leaktest) Refactored, tested variant of the goroutine leak detector found in both `net/http` tests and the `cockroachdb` source tree. Takes a snapshot of running goroutines at the start of a test, and at the end - compares the two and _voila_. Ignores runtime/sys goroutines. Doesn't play nice with `t.Parallel()` right now, but there are plans to do so. ### Installation Go 1.7+ ``` go get -u github.com/fortytw2/leaktest ``` Go 1.5/1.6 need to use the tag `v1.0.0`, as newer versions depend on `context.Context`. ### Example These tests fail, because they leak a goroutine ```go // Default "Check" will poll for 5 seconds to check that all // goroutines are cleaned up func TestPool(t *testing.T) { defer leaktest.Check(t)() go func() { for { time.Sleep(time.Second) } }() } // Helper function to timeout after X duration func TestPoolTimeout(t *testing.T) { defer leaktest.CheckTimeout(t, time.Second)() go func() { for { time.Sleep(time.Second) } }() } // Use Go 1.7+ context.Context for cancellation func TestPoolContext(t *testing.T) { ctx, _ := context.WithTimeout(context.Background(), time.Second) defer leaktest.CheckContext(ctx, t)() go func() { for { time.Sleep(time.Second) } }() } ``` ## LICENSE Same BSD-style as Go, see LICENSE ================================================ FILE: vendor/github.com/fortytw2/leaktest/leaktest.go ================================================ // Copyright 2013 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Package leaktest provides tools to detect leaked goroutines in tests. // To use it, call "defer leaktest.Check(t)()" at the beginning of each // test that may use goroutines. // copied out of the cockroachdb source tree with slight modifications to be // more re-useable package leaktest import ( "context" "fmt" "runtime" "sort" "strconv" "strings" "time" ) type goroutine struct { id uint64 stack string } type goroutineByID []*goroutine func (g goroutineByID) Len() int { return len(g) } func (g goroutineByID) Less(i, j int) bool { return g[i].id < g[j].id } func (g goroutineByID) Swap(i, j int) { g[i], g[j] = g[j], g[i] } func interestingGoroutine(g string) (*goroutine, error) { sl := strings.SplitN(g, "\n", 2) if len(sl) != 2 { return nil, fmt.Errorf("error parsing stack: %q", g) } stack := strings.TrimSpace(sl[1]) if strings.HasPrefix(stack, "testing.RunTests") { return nil, nil } if stack == "" || // Ignore HTTP keep alives strings.Contains(stack, ").readLoop(") || strings.Contains(stack, ").writeLoop(") || // Below are the stacks ignored by the upstream leaktest code. strings.Contains(stack, "testing.Main(") || strings.Contains(stack, "testing.(*T).Run(") || strings.Contains(stack, "runtime.goexit") || strings.Contains(stack, "created by runtime.gc") || strings.Contains(stack, "interestingGoroutines") || strings.Contains(stack, "runtime.MHeap_Scavenger") || strings.Contains(stack, "signal.signal_recv") || strings.Contains(stack, "sigterm.handler") || strings.Contains(stack, "runtime_mcall") || strings.Contains(stack, "goroutine in C code") { return nil, nil } // Parse the goroutine's ID from the header line. h := strings.SplitN(sl[0], " ", 3) if len(h) < 3 { return nil, fmt.Errorf("error parsing stack header: %q", sl[0]) } id, err := strconv.ParseUint(h[1], 10, 64) if err != nil { return nil, fmt.Errorf("error parsing goroutine id: %s", err) } return &goroutine{id: id, stack: strings.TrimSpace(g)}, nil } // interestingGoroutines returns all goroutines we care about for the purpose // of leak checking. It excludes testing or runtime ones. func interestingGoroutines(t ErrorReporter) []*goroutine { buf := make([]byte, 2<<20) buf = buf[:runtime.Stack(buf, true)] var gs []*goroutine for _, g := range strings.Split(string(buf), "\n\n") { gr, err := interestingGoroutine(g) if err != nil { t.Errorf("leaktest: %s", err) continue } else if gr == nil { continue } gs = append(gs, gr) } sort.Sort(goroutineByID(gs)) return gs } // ErrorReporter is a tiny subset of a testing.TB to make testing not such a // massive pain type ErrorReporter interface { Errorf(format string, args ...interface{}) } // Check snapshots the currently-running goroutines and returns a // function to be run at the end of tests to see whether any // goroutines leaked, waiting up to 5 seconds in error conditions func Check(t ErrorReporter) func() { return CheckTimeout(t, 5*time.Second) } // CheckTimeout is the same as Check, but with a configurable timeout func CheckTimeout(t ErrorReporter, dur time.Duration) func() { ctx, cancel := context.WithCancel(context.Background()) fn := CheckContext(ctx, t) return func() { timer := time.AfterFunc(dur, cancel) fn() // Remember to clean up the timer and context timer.Stop() cancel() } } // CheckContext is the same as Check, but uses a context.Context for // cancellation and timeout control func CheckContext(ctx context.Context, t ErrorReporter) func() { orig := map[uint64]bool{} for _, g := range interestingGoroutines(t) { orig[g.id] = true } return func() { var leaked []string for { select { case <-ctx.Done(): t.Errorf("leaktest: timed out checking goroutines") default: leaked = make([]string, 0) for _, g := range interestingGoroutines(t) { if !orig[g.id] { leaked = append(leaked, g.stack) } } if len(leaked) == 0 { return } // don't spin needlessly time.Sleep(time.Millisecond * 50) continue } break } for _, g := range leaked { t.Errorf("leaktest: leaked goroutine: %v", g) } } } ================================================ FILE: vendor/github.com/fsnotify/fsnotify/.editorconfig ================================================ root = true [*.go] indent_style = tab indent_size = 4 insert_final_newline = true [*.{yml,yaml}] indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true ================================================ FILE: vendor/github.com/fsnotify/fsnotify/.gitattributes ================================================ go.sum linguist-generated ================================================ FILE: vendor/github.com/fsnotify/fsnotify/.gitignore ================================================ # Setup a Global .gitignore for OS and editor generated files: # https://help.github.com/articles/ignoring-files # git config --global core.excludesfile ~/.gitignore_global .vagrant *.sublime-project ================================================ FILE: vendor/github.com/fsnotify/fsnotify/.travis.yml ================================================ sudo: false language: go go: - "stable" - "1.11.x" - "1.10.x" - "1.9.x" matrix: include: - go: "stable" env: GOLINT=true allow_failures: - go: tip fast_finish: true before_install: - if [ ! -z "${GOLINT}" ]; then go get -u golang.org/x/lint/golint; fi script: - go test --race ./... after_script: - test -z "$(gofmt -s -l -w . | tee /dev/stderr)" - if [ ! -z "${GOLINT}" ]; then echo running golint; golint --set_exit_status ./...; else echo skipping golint; fi - go vet ./... os: - linux - osx - windows notifications: email: false ================================================ FILE: vendor/github.com/fsnotify/fsnotify/AUTHORS ================================================ # Names should be added to this file as # Name or Organization # The email address is not required for organizations. # You can update this list using the following command: # # $ git shortlog -se | awk '{print $2 " " $3 " " $4}' # Please keep the list sorted. Aaron L Adrien Bustany Amit Krishnan Anmol Sethi Bjørn Erik Pedersen Bruno Bigras Caleb Spare Case Nelson Chris Howey Christoffer Buchholz Daniel Wagner-Hall Dave Cheney Evan Phoenix Francisco Souza Hari haran John C Barstow Kelvin Fo Ken-ichirou MATSUZAWA Matt Layher Nathan Youngman Nickolai Zeldovich Patrick Paul Hammond Pawel Knap Pieter Droogendijk Pursuit92 Riku Voipio Rob Figueiredo Rodrigo Chiossi Slawek Ligus Soge Zhang Tiffany Jernigan Tilak Sharma Tom Payne Travis Cline Tudor Golubenco Vahe Khachikyan Yukang bronze1man debrando henrikedwards 铁哥 ================================================ FILE: vendor/github.com/fsnotify/fsnotify/CHANGELOG.md ================================================ # Changelog ## v1.4.7 / 2018-01-09 * BSD/macOS: Fix possible deadlock on closing the watcher on kqueue (thanks @nhooyr and @glycerine) * Tests: Fix missing verb on format string (thanks @rchiossi) * Linux: Fix deadlock in Remove (thanks @aarondl) * Linux: Watch.Add improvements (avoid race, fix consistency, reduce garbage) (thanks @twpayne) * Docs: Moved FAQ into the README (thanks @vahe) * Linux: Properly handle inotify's IN_Q_OVERFLOW event (thanks @zeldovich) * Docs: replace references to OS X with macOS ## v1.4.2 / 2016-10-10 * Linux: use InotifyInit1 with IN_CLOEXEC to stop leaking a file descriptor to a child process when using fork/exec [#178](https://github.com/fsnotify/fsnotify/pull/178) (thanks @pattyshack) ## v1.4.1 / 2016-10-04 * Fix flaky inotify stress test on Linux [#177](https://github.com/fsnotify/fsnotify/pull/177) (thanks @pattyshack) ## v1.4.0 / 2016-10-01 * add a String() method to Event.Op [#165](https://github.com/fsnotify/fsnotify/pull/165) (thanks @oozie) ## v1.3.1 / 2016-06-28 * Windows: fix for double backslash when watching the root of a drive [#151](https://github.com/fsnotify/fsnotify/issues/151) (thanks @brunoqc) ## v1.3.0 / 2016-04-19 * Support linux/arm64 by [patching](https://go-review.googlesource.com/#/c/21971/) x/sys/unix and switching to to it from syscall (thanks @suihkulokki) [#135](https://github.com/fsnotify/fsnotify/pull/135) ## v1.2.10 / 2016-03-02 * Fix golint errors in windows.go [#121](https://github.com/fsnotify/fsnotify/pull/121) (thanks @tiffanyfj) ## v1.2.9 / 2016-01-13 kqueue: Fix logic for CREATE after REMOVE [#111](https://github.com/fsnotify/fsnotify/pull/111) (thanks @bep) ## v1.2.8 / 2015-12-17 * kqueue: fix race condition in Close [#105](https://github.com/fsnotify/fsnotify/pull/105) (thanks @djui for reporting the issue and @ppknap for writing a failing test) * inotify: fix race in test * enable race detection for continuous integration (Linux, Mac, Windows) ## v1.2.5 / 2015-10-17 * inotify: use epoll_create1 for arm64 support (requires Linux 2.6.27 or later) [#100](https://github.com/fsnotify/fsnotify/pull/100) (thanks @suihkulokki) * inotify: fix path leaks [#73](https://github.com/fsnotify/fsnotify/pull/73) (thanks @chamaken) * kqueue: watch for rename events on subdirectories [#83](https://github.com/fsnotify/fsnotify/pull/83) (thanks @guotie) * kqueue: avoid infinite loops from symlinks cycles [#101](https://github.com/fsnotify/fsnotify/pull/101) (thanks @illicitonion) ## v1.2.1 / 2015-10-14 * kqueue: don't watch named pipes [#98](https://github.com/fsnotify/fsnotify/pull/98) (thanks @evanphx) ## v1.2.0 / 2015-02-08 * inotify: use epoll to wake up readEvents [#66](https://github.com/fsnotify/fsnotify/pull/66) (thanks @PieterD) * inotify: closing watcher should now always shut down goroutine [#63](https://github.com/fsnotify/fsnotify/pull/63) (thanks @PieterD) * kqueue: close kqueue after removing watches, fixes [#59](https://github.com/fsnotify/fsnotify/issues/59) ## v1.1.1 / 2015-02-05 * inotify: Retry read on EINTR [#61](https://github.com/fsnotify/fsnotify/issues/61) (thanks @PieterD) ## v1.1.0 / 2014-12-12 * kqueue: rework internals [#43](https://github.com/fsnotify/fsnotify/pull/43) * add low-level functions * only need to store flags on directories * less mutexes [#13](https://github.com/fsnotify/fsnotify/issues/13) * done can be an unbuffered channel * remove calls to os.NewSyscallError * More efficient string concatenation for Event.String() [#52](https://github.com/fsnotify/fsnotify/pull/52) (thanks @mdlayher) * kqueue: fix regression in rework causing subdirectories to be watched [#48](https://github.com/fsnotify/fsnotify/issues/48) * kqueue: cleanup internal watch before sending remove event [#51](https://github.com/fsnotify/fsnotify/issues/51) ## v1.0.4 / 2014-09-07 * kqueue: add dragonfly to the build tags. * Rename source code files, rearrange code so exported APIs are at the top. * Add done channel to example code. [#37](https://github.com/fsnotify/fsnotify/pull/37) (thanks @chenyukang) ## v1.0.3 / 2014-08-19 * [Fix] Windows MOVED_TO now translates to Create like on BSD and Linux. [#36](https://github.com/fsnotify/fsnotify/issues/36) ## v1.0.2 / 2014-08-17 * [Fix] Missing create events on macOS. [#14](https://github.com/fsnotify/fsnotify/issues/14) (thanks @zhsso) * [Fix] Make ./path and path equivalent. (thanks @zhsso) ## v1.0.0 / 2014-08-15 * [API] Remove AddWatch on Windows, use Add. * Improve documentation for exported identifiers. [#30](https://github.com/fsnotify/fsnotify/issues/30) * Minor updates based on feedback from golint. ## dev / 2014-07-09 * Moved to [github.com/fsnotify/fsnotify](https://github.com/fsnotify/fsnotify). * Use os.NewSyscallError instead of returning errno (thanks @hariharan-uno) ## dev / 2014-07-04 * kqueue: fix incorrect mutex used in Close() * Update example to demonstrate usage of Op. ## dev / 2014-06-28 * [API] Don't set the Write Op for attribute notifications [#4](https://github.com/fsnotify/fsnotify/issues/4) * Fix for String() method on Event (thanks Alex Brainman) * Don't build on Plan 9 or Solaris (thanks @4ad) ## dev / 2014-06-21 * Events channel of type Event rather than *Event. * [internal] use syscall constants directly for inotify and kqueue. * [internal] kqueue: rename events to kevents and fileEvent to event. ## dev / 2014-06-19 * Go 1.3+ required on Windows (uses syscall.ERROR_MORE_DATA internally). * [internal] remove cookie from Event struct (unused). * [internal] Event struct has the same definition across every OS. * [internal] remove internal watch and removeWatch methods. ## dev / 2014-06-12 * [API] Renamed Watch() to Add() and RemoveWatch() to Remove(). * [API] Pluralized channel names: Events and Errors. * [API] Renamed FileEvent struct to Event. * [API] Op constants replace methods like IsCreate(). ## dev / 2014-06-12 * Fix data race on kevent buffer (thanks @tilaks) [#98](https://github.com/howeyc/fsnotify/pull/98) ## dev / 2014-05-23 * [API] Remove current implementation of WatchFlags. * current implementation doesn't take advantage of OS for efficiency * provides little benefit over filtering events as they are received, but has extra bookkeeping and mutexes * no tests for the current implementation * not fully implemented on Windows [#93](https://github.com/howeyc/fsnotify/issues/93#issuecomment-39285195) ## v0.9.3 / 2014-12-31 * kqueue: cleanup internal watch before sending remove event [#51](https://github.com/fsnotify/fsnotify/issues/51) ## v0.9.2 / 2014-08-17 * [Backport] Fix missing create events on macOS. [#14](https://github.com/fsnotify/fsnotify/issues/14) (thanks @zhsso) ## v0.9.1 / 2014-06-12 * Fix data race on kevent buffer (thanks @tilaks) [#98](https://github.com/howeyc/fsnotify/pull/98) ## v0.9.0 / 2014-01-17 * IsAttrib() for events that only concern a file's metadata [#79][] (thanks @abustany) * [Fix] kqueue: fix deadlock [#77][] (thanks @cespare) * [NOTICE] Development has moved to `code.google.com/p/go.exp/fsnotify` in preparation for inclusion in the Go standard library. ## v0.8.12 / 2013-11-13 * [API] Remove FD_SET and friends from Linux adapter ## v0.8.11 / 2013-11-02 * [Doc] Add Changelog [#72][] (thanks @nathany) * [Doc] Spotlight and double modify events on macOS [#62][] (reported by @paulhammond) ## v0.8.10 / 2013-10-19 * [Fix] kqueue: remove file watches when parent directory is removed [#71][] (reported by @mdwhatcott) * [Fix] kqueue: race between Close and readEvents [#70][] (reported by @bernerdschaefer) * [Doc] specify OS-specific limits in README (thanks @debrando) ## v0.8.9 / 2013-09-08 * [Doc] Contributing (thanks @nathany) * [Doc] update package path in example code [#63][] (thanks @paulhammond) * [Doc] GoCI badge in README (Linux only) [#60][] * [Doc] Cross-platform testing with Vagrant [#59][] (thanks @nathany) ## v0.8.8 / 2013-06-17 * [Fix] Windows: handle `ERROR_MORE_DATA` on Windows [#49][] (thanks @jbowtie) ## v0.8.7 / 2013-06-03 * [API] Make syscall flags internal * [Fix] inotify: ignore event changes * [Fix] race in symlink test [#45][] (reported by @srid) * [Fix] tests on Windows * lower case error messages ## v0.8.6 / 2013-05-23 * kqueue: Use EVT_ONLY flag on Darwin * [Doc] Update README with full example ## v0.8.5 / 2013-05-09 * [Fix] inotify: allow monitoring of "broken" symlinks (thanks @tsg) ## v0.8.4 / 2013-04-07 * [Fix] kqueue: watch all file events [#40][] (thanks @ChrisBuchholz) ## v0.8.3 / 2013-03-13 * [Fix] inoitfy/kqueue memory leak [#36][] (reported by @nbkolchin) * [Fix] kqueue: use fsnFlags for watching a directory [#33][] (reported by @nbkolchin) ## v0.8.2 / 2013-02-07 * [Doc] add Authors * [Fix] fix data races for map access [#29][] (thanks @fsouza) ## v0.8.1 / 2013-01-09 * [Fix] Windows path separators * [Doc] BSD License ## v0.8.0 / 2012-11-09 * kqueue: directory watching improvements (thanks @vmirage) * inotify: add `IN_MOVED_TO` [#25][] (requested by @cpisto) * [Fix] kqueue: deleting watched directory [#24][] (reported by @jakerr) ## v0.7.4 / 2012-10-09 * [Fix] inotify: fixes from https://codereview.appspot.com/5418045/ (ugorji) * [Fix] kqueue: preserve watch flags when watching for delete [#21][] (reported by @robfig) * [Fix] kqueue: watch the directory even if it isn't a new watch (thanks @robfig) * [Fix] kqueue: modify after recreation of file ## v0.7.3 / 2012-09-27 * [Fix] kqueue: watch with an existing folder inside the watched folder (thanks @vmirage) * [Fix] kqueue: no longer get duplicate CREATE events ## v0.7.2 / 2012-09-01 * kqueue: events for created directories ## v0.7.1 / 2012-07-14 * [Fix] for renaming files ## v0.7.0 / 2012-07-02 * [Feature] FSNotify flags * [Fix] inotify: Added file name back to event path ## v0.6.0 / 2012-06-06 * kqueue: watch files after directory created (thanks @tmc) ## v0.5.1 / 2012-05-22 * [Fix] inotify: remove all watches before Close() ## v0.5.0 / 2012-05-03 * [API] kqueue: return errors during watch instead of sending over channel * kqueue: match symlink behavior on Linux * inotify: add `DELETE_SELF` (requested by @taralx) * [Fix] kqueue: handle EINTR (reported by @robfig) * [Doc] Godoc example [#1][] (thanks @davecheney) ## v0.4.0 / 2012-03-30 * Go 1 released: build with go tool * [Feature] Windows support using winfsnotify * Windows does not have attribute change notifications * Roll attribute notifications into IsModify ## v0.3.0 / 2012-02-19 * kqueue: add files when watch directory ## v0.2.0 / 2011-12-30 * update to latest Go weekly code ## v0.1.0 / 2011-10-19 * kqueue: add watch on file creation to match inotify * kqueue: create file event * inotify: ignore `IN_IGNORED` events * event String() * linux: common FileEvent functions * initial commit [#79]: https://github.com/howeyc/fsnotify/pull/79 [#77]: https://github.com/howeyc/fsnotify/pull/77 [#72]: https://github.com/howeyc/fsnotify/issues/72 [#71]: https://github.com/howeyc/fsnotify/issues/71 [#70]: https://github.com/howeyc/fsnotify/issues/70 [#63]: https://github.com/howeyc/fsnotify/issues/63 [#62]: https://github.com/howeyc/fsnotify/issues/62 [#60]: https://github.com/howeyc/fsnotify/issues/60 [#59]: https://github.com/howeyc/fsnotify/issues/59 [#49]: https://github.com/howeyc/fsnotify/issues/49 [#45]: https://github.com/howeyc/fsnotify/issues/45 [#40]: https://github.com/howeyc/fsnotify/issues/40 [#36]: https://github.com/howeyc/fsnotify/issues/36 [#33]: https://github.com/howeyc/fsnotify/issues/33 [#29]: https://github.com/howeyc/fsnotify/issues/29 [#25]: https://github.com/howeyc/fsnotify/issues/25 [#24]: https://github.com/howeyc/fsnotify/issues/24 [#21]: https://github.com/howeyc/fsnotify/issues/21 ================================================ FILE: vendor/github.com/fsnotify/fsnotify/CONTRIBUTING.md ================================================ # Contributing ## Issues * Request features and report bugs using the [GitHub Issue Tracker](https://github.com/fsnotify/fsnotify/issues). * Please indicate the platform you are using fsnotify on. * A code example to reproduce the problem is appreciated. ## Pull Requests ### Contributor License Agreement fsnotify is derived from code in the [golang.org/x/exp](https://godoc.org/golang.org/x/exp) package and it may be included [in the standard library](https://github.com/fsnotify/fsnotify/issues/1) in the future. Therefore fsnotify carries the same [LICENSE](https://github.com/fsnotify/fsnotify/blob/master/LICENSE) as Go. Contributors retain their copyright, so you need to fill out a short form before we can accept your contribution: [Google Individual Contributor License Agreement](https://developers.google.com/open-source/cla/individual). Please indicate that you have signed the CLA in your pull request. ### How fsnotify is Developed * Development is done on feature branches. * Tests are run on BSD, Linux, macOS and Windows. * Pull requests are reviewed and [applied to master][am] using [hub][]. * Maintainers may modify or squash commits rather than asking contributors to. * To issue a new release, the maintainers will: * Update the CHANGELOG * Tag a version, which will become available through gopkg.in. ### How to Fork For smooth sailing, always use the original import path. Installing with `go get` makes this easy. 1. Install from GitHub (`go get -u github.com/fsnotify/fsnotify`) 2. Create your feature branch (`git checkout -b my-new-feature`) 3. Ensure everything works and the tests pass (see below) 4. Commit your changes (`git commit -am 'Add some feature'`) Contribute upstream: 1. Fork fsnotify on GitHub 2. Add your remote (`git remote add fork git@github.com:mycompany/repo.git`) 3. Push to the branch (`git push fork my-new-feature`) 4. Create a new Pull Request on GitHub This workflow is [thoroughly explained by Katrina Owen](https://splice.com/blog/contributing-open-source-git-repositories-go/). ### Testing fsnotify uses build tags to compile different code on Linux, BSD, macOS, and Windows. Before doing a pull request, please do your best to test your changes on multiple platforms, and list which platforms you were able/unable to test on. To aid in cross-platform testing there is a Vagrantfile for Linux and BSD. * Install [Vagrant](http://www.vagrantup.com/) and [VirtualBox](https://www.virtualbox.org/) * Setup [Vagrant Gopher](https://github.com/nathany/vagrant-gopher) in your `src` folder. * Run `vagrant up` from the project folder. You can also setup just one box with `vagrant up linux` or `vagrant up bsd` (note: the BSD box doesn't support Windows hosts at this time, and NFS may prompt for your host OS password) * Once setup, you can run the test suite on a given OS with a single command `vagrant ssh linux -c 'cd fsnotify/fsnotify; go test'`. * When you're done, you will want to halt or destroy the Vagrant boxes. Notice: fsnotify file system events won't trigger in shared folders. The tests get around this limitation by using the /tmp directory. Right now there is no equivalent solution for Windows and macOS, but there are Windows VMs [freely available from Microsoft](http://www.modern.ie/en-us/virtualization-tools#downloads). ### Maintainers Help maintaining fsnotify is welcome. To be a maintainer: * Submit a pull request and sign the CLA as above. * You must be able to run the test suite on Mac, Windows, Linux and BSD. To keep master clean, the fsnotify project uses the "apply mail" workflow outlined in Nathaniel Talbott's post ["Merge pull request" Considered Harmful][am]. This requires installing [hub][]. All code changes should be internal pull requests. Releases are tagged using [Semantic Versioning](http://semver.org/). [hub]: https://github.com/github/hub [am]: http://blog.spreedly.com/2014/06/24/merge-pull-request-considered-harmful/#.VGa5yZPF_Zs ================================================ FILE: vendor/github.com/fsnotify/fsnotify/LICENSE ================================================ Copyright (c) 2012 The Go Authors. All rights reserved. Copyright (c) 2012-2019 fsnotify Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: vendor/github.com/fsnotify/fsnotify/README.md ================================================ # File system notifications for Go [![GoDoc](https://godoc.org/github.com/fsnotify/fsnotify?status.svg)](https://godoc.org/github.com/fsnotify/fsnotify) [![Go Report Card](https://goreportcard.com/badge/github.com/fsnotify/fsnotify)](https://goreportcard.com/report/github.com/fsnotify/fsnotify) fsnotify utilizes [golang.org/x/sys](https://godoc.org/golang.org/x/sys) rather than `syscall` from the standard library. Ensure you have the latest version installed by running: ```console go get -u golang.org/x/sys/... ``` Cross platform: Windows, Linux, BSD and macOS. | Adapter | OS | Status | | --------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | inotify | Linux 2.6.27 or later, Android\* | Supported [![Build Status](https://travis-ci.org/fsnotify/fsnotify.svg?branch=master)](https://travis-ci.org/fsnotify/fsnotify) | | kqueue | BSD, macOS, iOS\* | Supported [![Build Status](https://travis-ci.org/fsnotify/fsnotify.svg?branch=master)](https://travis-ci.org/fsnotify/fsnotify) | | ReadDirectoryChangesW | Windows | Supported [![Build Status](https://travis-ci.org/fsnotify/fsnotify.svg?branch=master)](https://travis-ci.org/fsnotify/fsnotify) | | FSEvents | macOS | [Planned](https://github.com/fsnotify/fsnotify/issues/11) | | FEN | Solaris 11 | [In Progress](https://github.com/fsnotify/fsnotify/issues/12) | | fanotify | Linux 2.6.37+ | [Planned](https://github.com/fsnotify/fsnotify/issues/114) | | USN Journals | Windows | [Maybe](https://github.com/fsnotify/fsnotify/issues/53) | | Polling | *All* | [Maybe](https://github.com/fsnotify/fsnotify/issues/9) | \* Android and iOS are untested. Please see [the documentation](https://godoc.org/github.com/fsnotify/fsnotify) and consult the [FAQ](#faq) for usage information. ## API stability fsnotify is a fork of [howeyc/fsnotify](https://godoc.org/github.com/howeyc/fsnotify) with a new API as of v1.0. The API is based on [this design document](http://goo.gl/MrYxyA). All [releases](https://github.com/fsnotify/fsnotify/releases) are tagged based on [Semantic Versioning](http://semver.org/). Further API changes are [planned](https://github.com/fsnotify/fsnotify/milestones), and will be tagged with a new major revision number. Go 1.6 supports dependencies located in the `vendor/` folder. Unless you are creating a library, it is recommended that you copy fsnotify into `vendor/github.com/fsnotify/fsnotify` within your project, and likewise for `golang.org/x/sys`. ## Usage ```go package main import ( "log" "github.com/fsnotify/fsnotify" ) func main() { watcher, err := fsnotify.NewWatcher() if err != nil { log.Fatal(err) } defer watcher.Close() done := make(chan bool) go func() { for { select { case event, ok := <-watcher.Events: if !ok { return } log.Println("event:", event) if event.Op&fsnotify.Write == fsnotify.Write { log.Println("modified file:", event.Name) } case err, ok := <-watcher.Errors: if !ok { return } log.Println("error:", err) } } }() err = watcher.Add("/tmp/foo") if err != nil { log.Fatal(err) } <-done } ``` ## Contributing Please refer to [CONTRIBUTING][] before opening an issue or pull request. ## Example See [example_test.go](https://github.com/fsnotify/fsnotify/blob/master/example_test.go). ## FAQ **When a file is moved to another directory is it still being watched?** No (it shouldn't be, unless you are watching where it was moved to). **When I watch a directory, are all subdirectories watched as well?** No, you must add watches for any directory you want to watch (a recursive watcher is on the roadmap [#18][]). **Do I have to watch the Error and Event channels in a separate goroutine?** As of now, yes. Looking into making this single-thread friendly (see [howeyc #7][#7]) **Why am I receiving multiple events for the same file on OS X?** Spotlight indexing on OS X can result in multiple events (see [howeyc #62][#62]). A temporary workaround is to add your folder(s) to the *Spotlight Privacy settings* until we have a native FSEvents implementation (see [#11][]). **How many files can be watched at once?** There are OS-specific limits as to how many watches can be created: * Linux: /proc/sys/fs/inotify/max_user_watches contains the limit, reaching this limit results in a "no space left on device" error. * BSD / OSX: sysctl variables "kern.maxfiles" and "kern.maxfilesperproc", reaching these limits results in a "too many open files" error. **Why don't notifications work with NFS filesystems or filesystem in userspace (FUSE)?** fsnotify requires support from underlying OS to work. The current NFS protocol does not provide network level support for file notifications. [#62]: https://github.com/howeyc/fsnotify/issues/62 [#18]: https://github.com/fsnotify/fsnotify/issues/18 [#11]: https://github.com/fsnotify/fsnotify/issues/11 [#7]: https://github.com/howeyc/fsnotify/issues/7 [contributing]: https://github.com/fsnotify/fsnotify/blob/master/CONTRIBUTING.md ## Related Projects * [notify](https://github.com/rjeczalik/notify) * [fsevents](https://github.com/fsnotify/fsevents) ================================================ FILE: vendor/github.com/fsnotify/fsnotify/fen.go ================================================ // Copyright 2010 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // +build solaris package fsnotify import ( "errors" ) // Watcher watches a set of files, delivering events to a channel. type Watcher struct { Events chan Event Errors chan error } // NewWatcher establishes a new watcher with the underlying OS and begins waiting for events. func NewWatcher() (*Watcher, error) { return nil, errors.New("FEN based watcher not yet supported for fsnotify\n") } // Close removes all watches and closes the events channel. func (w *Watcher) Close() error { return nil } // Add starts watching the named file or directory (non-recursively). func (w *Watcher) Add(name string) error { return nil } // Remove stops watching the the named file or directory (non-recursively). func (w *Watcher) Remove(name string) error { return nil } ================================================ FILE: vendor/github.com/fsnotify/fsnotify/fsnotify.go ================================================ // Copyright 2012 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // +build !plan9 // Package fsnotify provides a platform-independent interface for file system notifications. package fsnotify import ( "bytes" "errors" "fmt" ) // Event represents a single file system notification. type Event struct { Name string // Relative path to the file or directory. Op Op // File operation that triggered the event. } // Op describes a set of file operations. type Op uint32 // These are the generalized file operations that can trigger a notification. const ( Create Op = 1 << iota Write Remove Rename Chmod ) func (op Op) String() string { // Use a buffer for efficient string concatenation var buffer bytes.Buffer if op&Create == Create { buffer.WriteString("|CREATE") } if op&Remove == Remove { buffer.WriteString("|REMOVE") } if op&Write == Write { buffer.WriteString("|WRITE") } if op&Rename == Rename { buffer.WriteString("|RENAME") } if op&Chmod == Chmod { buffer.WriteString("|CHMOD") } if buffer.Len() == 0 { return "" } return buffer.String()[1:] // Strip leading pipe } // String returns a string representation of the event in the form // "file: REMOVE|WRITE|..." func (e Event) String() string { return fmt.Sprintf("%q: %s", e.Name, e.Op.String()) } // Common errors that can be reported by a watcher var ( ErrEventOverflow = errors.New("fsnotify queue overflow") ) ================================================ FILE: vendor/github.com/fsnotify/fsnotify/inotify.go ================================================ // Copyright 2010 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // +build linux package fsnotify import ( "errors" "fmt" "io" "os" "path/filepath" "strings" "sync" "unsafe" "golang.org/x/sys/unix" ) // Watcher watches a set of files, delivering events to a channel. type Watcher struct { Events chan Event Errors chan error mu sync.Mutex // Map access fd int poller *fdPoller watches map[string]*watch // Map of inotify watches (key: path) paths map[int]string // Map of watched paths (key: watch descriptor) done chan struct{} // Channel for sending a "quit message" to the reader goroutine doneResp chan struct{} // Channel to respond to Close } // NewWatcher establishes a new watcher with the underlying OS and begins waiting for events. func NewWatcher() (*Watcher, error) { // Create inotify fd fd, errno := unix.InotifyInit1(unix.IN_CLOEXEC) if fd == -1 { return nil, errno } // Create epoll poller, err := newFdPoller(fd) if err != nil { unix.Close(fd) return nil, err } w := &Watcher{ fd: fd, poller: poller, watches: make(map[string]*watch), paths: make(map[int]string), Events: make(chan Event), Errors: make(chan error), done: make(chan struct{}), doneResp: make(chan struct{}), } go w.readEvents() return w, nil } func (w *Watcher) isClosed() bool { select { case <-w.done: return true default: return false } } // Close removes all watches and closes the events channel. func (w *Watcher) Close() error { if w.isClosed() { return nil } // Send 'close' signal to goroutine, and set the Watcher to closed. close(w.done) // Wake up goroutine w.poller.wake() // Wait for goroutine to close <-w.doneResp return nil } // Add starts watching the named file or directory (non-recursively). func (w *Watcher) Add(name string) error { name = filepath.Clean(name) if w.isClosed() { return errors.New("inotify instance already closed") } const agnosticEvents = unix.IN_MOVED_TO | unix.IN_MOVED_FROM | unix.IN_CREATE | unix.IN_ATTRIB | unix.IN_MODIFY | unix.IN_MOVE_SELF | unix.IN_DELETE | unix.IN_DELETE_SELF var flags uint32 = agnosticEvents w.mu.Lock() defer w.mu.Unlock() watchEntry := w.watches[name] if watchEntry != nil { flags |= watchEntry.flags | unix.IN_MASK_ADD } wd, errno := unix.InotifyAddWatch(w.fd, name, flags) if wd == -1 { return errno } if watchEntry == nil { w.watches[name] = &watch{wd: uint32(wd), flags: flags} w.paths[wd] = name } else { watchEntry.wd = uint32(wd) watchEntry.flags = flags } return nil } // Remove stops watching the named file or directory (non-recursively). func (w *Watcher) Remove(name string) error { name = filepath.Clean(name) // Fetch the watch. w.mu.Lock() defer w.mu.Unlock() watch, ok := w.watches[name] // Remove it from inotify. if !ok { return fmt.Errorf("can't remove non-existent inotify watch for: %s", name) } // We successfully removed the watch if InotifyRmWatch doesn't return an // error, we need to clean up our internal state to ensure it matches // inotify's kernel state. delete(w.paths, int(watch.wd)) delete(w.watches, name) // inotify_rm_watch will return EINVAL if the file has been deleted; // the inotify will already have been removed. // watches and pathes are deleted in ignoreLinux() implicitly and asynchronously // by calling inotify_rm_watch() below. e.g. readEvents() goroutine receives IN_IGNORE // so that EINVAL means that the wd is being rm_watch()ed or its file removed // by another thread and we have not received IN_IGNORE event. success, errno := unix.InotifyRmWatch(w.fd, watch.wd) if success == -1 { // TODO: Perhaps it's not helpful to return an error here in every case. // the only two possible errors are: // EBADF, which happens when w.fd is not a valid file descriptor of any kind. // EINVAL, which is when fd is not an inotify descriptor or wd is not a valid watch descriptor. // Watch descriptors are invalidated when they are removed explicitly or implicitly; // explicitly by inotify_rm_watch, implicitly when the file they are watching is deleted. return errno } return nil } type watch struct { wd uint32 // Watch descriptor (as returned by the inotify_add_watch() syscall) flags uint32 // inotify flags of this watch (see inotify(7) for the list of valid flags) } // readEvents reads from the inotify file descriptor, converts the // received events into Event objects and sends them via the Events channel func (w *Watcher) readEvents() { var ( buf [unix.SizeofInotifyEvent * 4096]byte // Buffer for a maximum of 4096 raw events n int // Number of bytes read with read() errno error // Syscall errno ok bool // For poller.wait ) defer close(w.doneResp) defer close(w.Errors) defer close(w.Events) defer unix.Close(w.fd) defer w.poller.close() for { // See if we have been closed. if w.isClosed() { return } ok, errno = w.poller.wait() if errno != nil { select { case w.Errors <- errno: case <-w.done: return } continue } if !ok { continue } n, errno = unix.Read(w.fd, buf[:]) // If a signal interrupted execution, see if we've been asked to close, and try again. // http://man7.org/linux/man-pages/man7/signal.7.html : // "Before Linux 3.8, reads from an inotify(7) file descriptor were not restartable" if errno == unix.EINTR { continue } // unix.Read might have been woken up by Close. If so, we're done. if w.isClosed() { return } if n < unix.SizeofInotifyEvent { var err error if n == 0 { // If EOF is received. This should really never happen. err = io.EOF } else if n < 0 { // If an error occurred while reading. err = errno } else { // Read was too short. err = errors.New("notify: short read in readEvents()") } select { case w.Errors <- err: case <-w.done: return } continue } var offset uint32 // We don't know how many events we just read into the buffer // While the offset points to at least one whole event... for offset <= uint32(n-unix.SizeofInotifyEvent) { // Point "raw" to the event in the buffer raw := (*unix.InotifyEvent)(unsafe.Pointer(&buf[offset])) mask := uint32(raw.Mask) nameLen := uint32(raw.Len) if mask&unix.IN_Q_OVERFLOW != 0 { select { case w.Errors <- ErrEventOverflow: case <-w.done: return } } // If the event happened to the watched directory or the watched file, the kernel // doesn't append the filename to the event, but we would like to always fill the // the "Name" field with a valid filename. We retrieve the path of the watch from // the "paths" map. w.mu.Lock() name, ok := w.paths[int(raw.Wd)] // IN_DELETE_SELF occurs when the file/directory being watched is removed. // This is a sign to clean up the maps, otherwise we are no longer in sync // with the inotify kernel state which has already deleted the watch // automatically. if ok && mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF { delete(w.paths, int(raw.Wd)) delete(w.watches, name) } w.mu.Unlock() if nameLen > 0 { // Point "bytes" at the first byte of the filename bytes := (*[unix.PathMax]byte)(unsafe.Pointer(&buf[offset+unix.SizeofInotifyEvent])) // The filename is padded with NULL bytes. TrimRight() gets rid of those. name += "/" + strings.TrimRight(string(bytes[0:nameLen]), "\000") } event := newEvent(name, mask) // Send the events that are not ignored on the events channel if !event.ignoreLinux(mask) { select { case w.Events <- event: case <-w.done: return } } // Move to the next event in the buffer offset += unix.SizeofInotifyEvent + nameLen } } } // Certain types of events can be "ignored" and not sent over the Events // channel. Such as events marked ignore by the kernel, or MODIFY events // against files that do not exist. func (e *Event) ignoreLinux(mask uint32) bool { // Ignore anything the inotify API says to ignore if mask&unix.IN_IGNORED == unix.IN_IGNORED { return true } // If the event is not a DELETE or RENAME, the file must exist. // Otherwise the event is ignored. // *Note*: this was put in place because it was seen that a MODIFY // event was sent after the DELETE. This ignores that MODIFY and // assumes a DELETE will come or has come if the file doesn't exist. if !(e.Op&Remove == Remove || e.Op&Rename == Rename) { _, statErr := os.Lstat(e.Name) return os.IsNotExist(statErr) } return false } // newEvent returns an platform-independent Event based on an inotify mask. func newEvent(name string, mask uint32) Event { e := Event{Name: name} if mask&unix.IN_CREATE == unix.IN_CREATE || mask&unix.IN_MOVED_TO == unix.IN_MOVED_TO { e.Op |= Create } if mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF || mask&unix.IN_DELETE == unix.IN_DELETE { e.Op |= Remove } if mask&unix.IN_MODIFY == unix.IN_MODIFY { e.Op |= Write } if mask&unix.IN_MOVE_SELF == unix.IN_MOVE_SELF || mask&unix.IN_MOVED_FROM == unix.IN_MOVED_FROM { e.Op |= Rename } if mask&unix.IN_ATTRIB == unix.IN_ATTRIB { e.Op |= Chmod } return e } ================================================ FILE: vendor/github.com/fsnotify/fsnotify/inotify_poller.go ================================================ // Copyright 2015 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // +build linux package fsnotify import ( "errors" "golang.org/x/sys/unix" ) type fdPoller struct { fd int // File descriptor (as returned by the inotify_init() syscall) epfd int // Epoll file descriptor pipe [2]int // Pipe for waking up } func emptyPoller(fd int) *fdPoller { poller := new(fdPoller) poller.fd = fd poller.epfd = -1 poller.pipe[0] = -1 poller.pipe[1] = -1 return poller } // Create a new inotify poller. // This creates an inotify handler, and an epoll handler. func newFdPoller(fd int) (*fdPoller, error) { var errno error poller := emptyPoller(fd) defer func() { if errno != nil { poller.close() } }() poller.fd = fd // Create epoll fd poller.epfd, errno = unix.EpollCreate1(unix.EPOLL_CLOEXEC) if poller.epfd == -1 { return nil, errno } // Create pipe; pipe[0] is the read end, pipe[1] the write end. errno = unix.Pipe2(poller.pipe[:], unix.O_NONBLOCK|unix.O_CLOEXEC) if errno != nil { return nil, errno } // Register inotify fd with epoll event := unix.EpollEvent{ Fd: int32(poller.fd), Events: unix.EPOLLIN, } errno = unix.EpollCtl(poller.epfd, unix.EPOLL_CTL_ADD, poller.fd, &event) if errno != nil { return nil, errno } // Register pipe fd with epoll event = unix.EpollEvent{ Fd: int32(poller.pipe[0]), Events: unix.EPOLLIN, } errno = unix.EpollCtl(poller.epfd, unix.EPOLL_CTL_ADD, poller.pipe[0], &event) if errno != nil { return nil, errno } return poller, nil } // Wait using epoll. // Returns true if something is ready to be read, // false if there is not. func (poller *fdPoller) wait() (bool, error) { // 3 possible events per fd, and 2 fds, makes a maximum of 6 events. // I don't know whether epoll_wait returns the number of events returned, // or the total number of events ready. // I decided to catch both by making the buffer one larger than the maximum. events := make([]unix.EpollEvent, 7) for { n, errno := unix.EpollWait(poller.epfd, events, -1) if n == -1 { if errno == unix.EINTR { continue } return false, errno } if n == 0 { // If there are no events, try again. continue } if n > 6 { // This should never happen. More events were returned than should be possible. return false, errors.New("epoll_wait returned more events than I know what to do with") } ready := events[:n] epollhup := false epollerr := false epollin := false for _, event := range ready { if event.Fd == int32(poller.fd) { if event.Events&unix.EPOLLHUP != 0 { // This should not happen, but if it does, treat it as a wakeup. epollhup = true } if event.Events&unix.EPOLLERR != 0 { // If an error is waiting on the file descriptor, we should pretend // something is ready to read, and let unix.Read pick up the error. epollerr = true } if event.Events&unix.EPOLLIN != 0 { // There is data to read. epollin = true } } if event.Fd == int32(poller.pipe[0]) { if event.Events&unix.EPOLLHUP != 0 { // Write pipe descriptor was closed, by us. This means we're closing down the // watcher, and we should wake up. } if event.Events&unix.EPOLLERR != 0 { // If an error is waiting on the pipe file descriptor. // This is an absolute mystery, and should never ever happen. return false, errors.New("Error on the pipe descriptor.") } if event.Events&unix.EPOLLIN != 0 { // This is a regular wakeup, so we have to clear the buffer. err := poller.clearWake() if err != nil { return false, err } } } } if epollhup || epollerr || epollin { return true, nil } return false, nil } } // Close the write end of the poller. func (poller *fdPoller) wake() error { buf := make([]byte, 1) n, errno := unix.Write(poller.pipe[1], buf) if n == -1 { if errno == unix.EAGAIN { // Buffer is full, poller will wake. return nil } return errno } return nil } func (poller *fdPoller) clearWake() error { // You have to be woken up a LOT in order to get to 100! buf := make([]byte, 100) n, errno := unix.Read(poller.pipe[0], buf) if n == -1 { if errno == unix.EAGAIN { // Buffer is empty, someone else cleared our wake. return nil } return errno } return nil } // Close all poller file descriptors, but not the one passed to it. func (poller *fdPoller) close() { if poller.pipe[1] != -1 { unix.Close(poller.pipe[1]) } if poller.pipe[0] != -1 { unix.Close(poller.pipe[0]) } if poller.epfd != -1 { unix.Close(poller.epfd) } } ================================================ FILE: vendor/github.com/fsnotify/fsnotify/kqueue.go ================================================ // Copyright 2010 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // +build freebsd openbsd netbsd dragonfly darwin package fsnotify import ( "errors" "fmt" "io/ioutil" "os" "path/filepath" "sync" "time" "golang.org/x/sys/unix" ) // Watcher watches a set of files, delivering events to a channel. type Watcher struct { Events chan Event Errors chan error done chan struct{} // Channel for sending a "quit message" to the reader goroutine kq int // File descriptor (as returned by the kqueue() syscall). mu sync.Mutex // Protects access to watcher data watches map[string]int // Map of watched file descriptors (key: path). externalWatches map[string]bool // Map of watches added by user of the library. dirFlags map[string]uint32 // Map of watched directories to fflags used in kqueue. paths map[int]pathInfo // Map file descriptors to path names for processing kqueue events. fileExists map[string]bool // Keep track of if we know this file exists (to stop duplicate create events). isClosed bool // Set to true when Close() is first called } type pathInfo struct { name string isDir bool } // NewWatcher establishes a new watcher with the underlying OS and begins waiting for events. func NewWatcher() (*Watcher, error) { kq, err := kqueue() if err != nil { return nil, err } w := &Watcher{ kq: kq, watches: make(map[string]int), dirFlags: make(map[string]uint32), paths: make(map[int]pathInfo), fileExists: make(map[string]bool), externalWatches: make(map[string]bool), Events: make(chan Event), Errors: make(chan error), done: make(chan struct{}), } go w.readEvents() return w, nil } // Close removes all watches and closes the events channel. func (w *Watcher) Close() error { w.mu.Lock() if w.isClosed { w.mu.Unlock() return nil } w.isClosed = true // copy paths to remove while locked var pathsToRemove = make([]string, 0, len(w.watches)) for name := range w.watches { pathsToRemove = append(pathsToRemove, name) } w.mu.Unlock() // unlock before calling Remove, which also locks for _, name := range pathsToRemove { w.Remove(name) } // send a "quit" message to the reader goroutine close(w.done) return nil } // Add starts watching the named file or directory (non-recursively). func (w *Watcher) Add(name string) error { w.mu.Lock() w.externalWatches[name] = true w.mu.Unlock() _, err := w.addWatch(name, noteAllEvents) return err } // Remove stops watching the the named file or directory (non-recursively). func (w *Watcher) Remove(name string) error { name = filepath.Clean(name) w.mu.Lock() watchfd, ok := w.watches[name] w.mu.Unlock() if !ok { return fmt.Errorf("can't remove non-existent kevent watch for: %s", name) } const registerRemove = unix.EV_DELETE if err := register(w.kq, []int{watchfd}, registerRemove, 0); err != nil { return err } unix.Close(watchfd) w.mu.Lock() isDir := w.paths[watchfd].isDir delete(w.watches, name) delete(w.paths, watchfd) delete(w.dirFlags, name) w.mu.Unlock() // Find all watched paths that are in this directory that are not external. if isDir { var pathsToRemove []string w.mu.Lock() for _, path := range w.paths { wdir, _ := filepath.Split(path.name) if filepath.Clean(wdir) == name { if !w.externalWatches[path.name] { pathsToRemove = append(pathsToRemove, path.name) } } } w.mu.Unlock() for _, name := range pathsToRemove { // Since these are internal, not much sense in propagating error // to the user, as that will just confuse them with an error about // a path they did not explicitly watch themselves. w.Remove(name) } } return nil } // Watch all events (except NOTE_EXTEND, NOTE_LINK, NOTE_REVOKE) const noteAllEvents = unix.NOTE_DELETE | unix.NOTE_WRITE | unix.NOTE_ATTRIB | unix.NOTE_RENAME // keventWaitTime to block on each read from kevent var keventWaitTime = durationToTimespec(100 * time.Millisecond) // addWatch adds name to the watched file set. // The flags are interpreted as described in kevent(2). // Returns the real path to the file which was added, if any, which may be different from the one passed in the case of symlinks. func (w *Watcher) addWatch(name string, flags uint32) (string, error) { var isDir bool // Make ./name and name equivalent name = filepath.Clean(name) w.mu.Lock() if w.isClosed { w.mu.Unlock() return "", errors.New("kevent instance already closed") } watchfd, alreadyWatching := w.watches[name] // We already have a watch, but we can still override flags. if alreadyWatching { isDir = w.paths[watchfd].isDir } w.mu.Unlock() if !alreadyWatching { fi, err := os.Lstat(name) if err != nil { return "", err } // Don't watch sockets. if fi.Mode()&os.ModeSocket == os.ModeSocket { return "", nil } // Don't watch named pipes. if fi.Mode()&os.ModeNamedPipe == os.ModeNamedPipe { return "", nil } // Follow Symlinks // Unfortunately, Linux can add bogus symlinks to watch list without // issue, and Windows can't do symlinks period (AFAIK). To maintain // consistency, we will act like everything is fine. There will simply // be no file events for broken symlinks. // Hence the returns of nil on errors. if fi.Mode()&os.ModeSymlink == os.ModeSymlink { name, err = filepath.EvalSymlinks(name) if err != nil { return "", nil } w.mu.Lock() _, alreadyWatching = w.watches[name] w.mu.Unlock() if alreadyWatching { return name, nil } fi, err = os.Lstat(name) if err != nil { return "", nil } } watchfd, err = unix.Open(name, openMode, 0700) if watchfd == -1 { return "", err } isDir = fi.IsDir() } const registerAdd = unix.EV_ADD | unix.EV_CLEAR | unix.EV_ENABLE if err := register(w.kq, []int{watchfd}, registerAdd, flags); err != nil { unix.Close(watchfd) return "", err } if !alreadyWatching { w.mu.Lock() w.watches[name] = watchfd w.paths[watchfd] = pathInfo{name: name, isDir: isDir} w.mu.Unlock() } if isDir { // Watch the directory if it has not been watched before, // or if it was watched before, but perhaps only a NOTE_DELETE (watchDirectoryFiles) w.mu.Lock() watchDir := (flags&unix.NOTE_WRITE) == unix.NOTE_WRITE && (!alreadyWatching || (w.dirFlags[name]&unix.NOTE_WRITE) != unix.NOTE_WRITE) // Store flags so this watch can be updated later w.dirFlags[name] = flags w.mu.Unlock() if watchDir { if err := w.watchDirectoryFiles(name); err != nil { return "", err } } } return name, nil } // readEvents reads from kqueue and converts the received kevents into // Event values that it sends down the Events channel. func (w *Watcher) readEvents() { eventBuffer := make([]unix.Kevent_t, 10) loop: for { // See if there is a message on the "done" channel select { case <-w.done: break loop default: } // Get new events kevents, err := read(w.kq, eventBuffer, &keventWaitTime) // EINTR is okay, the syscall was interrupted before timeout expired. if err != nil && err != unix.EINTR { select { case w.Errors <- err: case <-w.done: break loop } continue } // Flush the events we received to the Events channel for len(kevents) > 0 { kevent := &kevents[0] watchfd := int(kevent.Ident) mask := uint32(kevent.Fflags) w.mu.Lock() path := w.paths[watchfd] w.mu.Unlock() event := newEvent(path.name, mask) if path.isDir && !(event.Op&Remove == Remove) { // Double check to make sure the directory exists. This can happen when // we do a rm -fr on a recursively watched folders and we receive a // modification event first but the folder has been deleted and later // receive the delete event if _, err := os.Lstat(event.Name); os.IsNotExist(err) { // mark is as delete event event.Op |= Remove } } if event.Op&Rename == Rename || event.Op&Remove == Remove { w.Remove(event.Name) w.mu.Lock() delete(w.fileExists, event.Name) w.mu.Unlock() } if path.isDir && event.Op&Write == Write && !(event.Op&Remove == Remove) { w.sendDirectoryChangeEvents(event.Name) } else { // Send the event on the Events channel. select { case w.Events <- event: case <-w.done: break loop } } if event.Op&Remove == Remove { // Look for a file that may have overwritten this. // For example, mv f1 f2 will delete f2, then create f2. if path.isDir { fileDir := filepath.Clean(event.Name) w.mu.Lock() _, found := w.watches[fileDir] w.mu.Unlock() if found { // make sure the directory exists before we watch for changes. When we // do a recursive watch and perform rm -fr, the parent directory might // have gone missing, ignore the missing directory and let the // upcoming delete event remove the watch from the parent directory. if _, err := os.Lstat(fileDir); err == nil { w.sendDirectoryChangeEvents(fileDir) } } } else { filePath := filepath.Clean(event.Name) if fileInfo, err := os.Lstat(filePath); err == nil { w.sendFileCreatedEventIfNew(filePath, fileInfo) } } } // Move to next event kevents = kevents[1:] } } // cleanup err := unix.Close(w.kq) if err != nil { // only way the previous loop breaks is if w.done was closed so we need to async send to w.Errors. select { case w.Errors <- err: default: } } close(w.Events) close(w.Errors) } // newEvent returns an platform-independent Event based on kqueue Fflags. func newEvent(name string, mask uint32) Event { e := Event{Name: name} if mask&unix.NOTE_DELETE == unix.NOTE_DELETE { e.Op |= Remove } if mask&unix.NOTE_WRITE == unix.NOTE_WRITE { e.Op |= Write } if mask&unix.NOTE_RENAME == unix.NOTE_RENAME { e.Op |= Rename } if mask&unix.NOTE_ATTRIB == unix.NOTE_ATTRIB { e.Op |= Chmod } return e } func newCreateEvent(name string) Event { return Event{Name: name, Op: Create} } // watchDirectoryFiles to mimic inotify when adding a watch on a directory func (w *Watcher) watchDirectoryFiles(dirPath string) error { // Get all files files, err := ioutil.ReadDir(dirPath) if err != nil { return err } for _, fileInfo := range files { filePath := filepath.Join(dirPath, fileInfo.Name()) filePath, err = w.internalWatch(filePath, fileInfo) if err != nil { return err } w.mu.Lock() w.fileExists[filePath] = true w.mu.Unlock() } return nil } // sendDirectoryEvents searches the directory for newly created files // and sends them over the event channel. This functionality is to have // the BSD version of fsnotify match Linux inotify which provides a // create event for files created in a watched directory. func (w *Watcher) sendDirectoryChangeEvents(dirPath string) { // Get all files files, err := ioutil.ReadDir(dirPath) if err != nil { select { case w.Errors <- err: case <-w.done: return } } // Search for new files for _, fileInfo := range files { filePath := filepath.Join(dirPath, fileInfo.Name()) err := w.sendFileCreatedEventIfNew(filePath, fileInfo) if err != nil { return } } } // sendFileCreatedEvent sends a create event if the file isn't already being tracked. func (w *Watcher) sendFileCreatedEventIfNew(filePath string, fileInfo os.FileInfo) (err error) { w.mu.Lock() _, doesExist := w.fileExists[filePath] w.mu.Unlock() if !doesExist { // Send create event select { case w.Events <- newCreateEvent(filePath): case <-w.done: return } } // like watchDirectoryFiles (but without doing another ReadDir) filePath, err = w.internalWatch(filePath, fileInfo) if err != nil { return err } w.mu.Lock() w.fileExists[filePath] = true w.mu.Unlock() return nil } func (w *Watcher) internalWatch(name string, fileInfo os.FileInfo) (string, error) { if fileInfo.IsDir() { // mimic Linux providing delete events for subdirectories // but preserve the flags used if currently watching subdirectory w.mu.Lock() flags := w.dirFlags[name] w.mu.Unlock() flags |= unix.NOTE_DELETE | unix.NOTE_RENAME return w.addWatch(name, flags) } // watch file to mimic Linux inotify return w.addWatch(name, noteAllEvents) } // kqueue creates a new kernel event queue and returns a descriptor. func kqueue() (kq int, err error) { kq, err = unix.Kqueue() if kq == -1 { return kq, err } return kq, nil } // register events with the queue func register(kq int, fds []int, flags int, fflags uint32) error { changes := make([]unix.Kevent_t, len(fds)) for i, fd := range fds { // SetKevent converts int to the platform-specific types: unix.SetKevent(&changes[i], fd, unix.EVFILT_VNODE, flags) changes[i].Fflags = fflags } // register the events success, err := unix.Kevent(kq, changes, nil, nil) if success == -1 { return err } return nil } // read retrieves pending events, or waits until an event occurs. // A timeout of nil blocks indefinitely, while 0 polls the queue. func read(kq int, events []unix.Kevent_t, timeout *unix.Timespec) ([]unix.Kevent_t, error) { n, err := unix.Kevent(kq, nil, events, timeout) if err != nil { return nil, err } return events[0:n], nil } // durationToTimespec prepares a timeout value func durationToTimespec(d time.Duration) unix.Timespec { return unix.NsecToTimespec(d.Nanoseconds()) } ================================================ FILE: vendor/github.com/fsnotify/fsnotify/open_mode_bsd.go ================================================ // Copyright 2013 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // +build freebsd openbsd netbsd dragonfly package fsnotify import "golang.org/x/sys/unix" const openMode = unix.O_NONBLOCK | unix.O_RDONLY | unix.O_CLOEXEC ================================================ FILE: vendor/github.com/fsnotify/fsnotify/open_mode_darwin.go ================================================ // Copyright 2013 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // +build darwin package fsnotify import "golang.org/x/sys/unix" // note: this constant is not defined on BSD const openMode = unix.O_EVTONLY | unix.O_CLOEXEC ================================================ FILE: vendor/github.com/fsnotify/fsnotify/windows.go ================================================ // Copyright 2011 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // +build windows package fsnotify import ( "errors" "fmt" "os" "path/filepath" "runtime" "sync" "syscall" "unsafe" ) // Watcher watches a set of files, delivering events to a channel. type Watcher struct { Events chan Event Errors chan error isClosed bool // Set to true when Close() is first called mu sync.Mutex // Map access port syscall.Handle // Handle to completion port watches watchMap // Map of watches (key: i-number) input chan *input // Inputs to the reader are sent on this channel quit chan chan<- error } // NewWatcher establishes a new watcher with the underlying OS and begins waiting for events. func NewWatcher() (*Watcher, error) { port, e := syscall.CreateIoCompletionPort(syscall.InvalidHandle, 0, 0, 0) if e != nil { return nil, os.NewSyscallError("CreateIoCompletionPort", e) } w := &Watcher{ port: port, watches: make(watchMap), input: make(chan *input, 1), Events: make(chan Event, 50), Errors: make(chan error), quit: make(chan chan<- error, 1), } go w.readEvents() return w, nil } // Close removes all watches and closes the events channel. func (w *Watcher) Close() error { if w.isClosed { return nil } w.isClosed = true // Send "quit" message to the reader goroutine ch := make(chan error) w.quit <- ch if err := w.wakeupReader(); err != nil { return err } return <-ch } // Add starts watching the named file or directory (non-recursively). func (w *Watcher) Add(name string) error { if w.isClosed { return errors.New("watcher already closed") } in := &input{ op: opAddWatch, path: filepath.Clean(name), flags: sysFSALLEVENTS, reply: make(chan error), } w.input <- in if err := w.wakeupReader(); err != nil { return err } return <-in.reply } // Remove stops watching the the named file or directory (non-recursively). func (w *Watcher) Remove(name string) error { in := &input{ op: opRemoveWatch, path: filepath.Clean(name), reply: make(chan error), } w.input <- in if err := w.wakeupReader(); err != nil { return err } return <-in.reply } const ( // Options for AddWatch sysFSONESHOT = 0x80000000 sysFSONLYDIR = 0x1000000 // Events sysFSACCESS = 0x1 sysFSALLEVENTS = 0xfff sysFSATTRIB = 0x4 sysFSCLOSE = 0x18 sysFSCREATE = 0x100 sysFSDELETE = 0x200 sysFSDELETESELF = 0x400 sysFSMODIFY = 0x2 sysFSMOVE = 0xc0 sysFSMOVEDFROM = 0x40 sysFSMOVEDTO = 0x80 sysFSMOVESELF = 0x800 // Special events sysFSIGNORED = 0x8000 sysFSQOVERFLOW = 0x4000 ) func newEvent(name string, mask uint32) Event { e := Event{Name: name} if mask&sysFSCREATE == sysFSCREATE || mask&sysFSMOVEDTO == sysFSMOVEDTO { e.Op |= Create } if mask&sysFSDELETE == sysFSDELETE || mask&sysFSDELETESELF == sysFSDELETESELF { e.Op |= Remove } if mask&sysFSMODIFY == sysFSMODIFY { e.Op |= Write } if mask&sysFSMOVE == sysFSMOVE || mask&sysFSMOVESELF == sysFSMOVESELF || mask&sysFSMOVEDFROM == sysFSMOVEDFROM { e.Op |= Rename } if mask&sysFSATTRIB == sysFSATTRIB { e.Op |= Chmod } return e } const ( opAddWatch = iota opRemoveWatch ) const ( provisional uint64 = 1 << (32 + iota) ) type input struct { op int path string flags uint32 reply chan error } type inode struct { handle syscall.Handle volume uint32 index uint64 } type watch struct { ov syscall.Overlapped ino *inode // i-number path string // Directory path mask uint64 // Directory itself is being watched with these notify flags names map[string]uint64 // Map of names being watched and their notify flags rename string // Remembers the old name while renaming a file buf [4096]byte } type indexMap map[uint64]*watch type watchMap map[uint32]indexMap func (w *Watcher) wakeupReader() error { e := syscall.PostQueuedCompletionStatus(w.port, 0, 0, nil) if e != nil { return os.NewSyscallError("PostQueuedCompletionStatus", e) } return nil } func getDir(pathname string) (dir string, err error) { attr, e := syscall.GetFileAttributes(syscall.StringToUTF16Ptr(pathname)) if e != nil { return "", os.NewSyscallError("GetFileAttributes", e) } if attr&syscall.FILE_ATTRIBUTE_DIRECTORY != 0 { dir = pathname } else { dir, _ = filepath.Split(pathname) dir = filepath.Clean(dir) } return } func getIno(path string) (ino *inode, err error) { h, e := syscall.CreateFile(syscall.StringToUTF16Ptr(path), syscall.FILE_LIST_DIRECTORY, syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE, nil, syscall.OPEN_EXISTING, syscall.FILE_FLAG_BACKUP_SEMANTICS|syscall.FILE_FLAG_OVERLAPPED, 0) if e != nil { return nil, os.NewSyscallError("CreateFile", e) } var fi syscall.ByHandleFileInformation if e = syscall.GetFileInformationByHandle(h, &fi); e != nil { syscall.CloseHandle(h) return nil, os.NewSyscallError("GetFileInformationByHandle", e) } ino = &inode{ handle: h, volume: fi.VolumeSerialNumber, index: uint64(fi.FileIndexHigh)<<32 | uint64(fi.FileIndexLow), } return ino, nil } // Must run within the I/O thread. func (m watchMap) get(ino *inode) *watch { if i := m[ino.volume]; i != nil { return i[ino.index] } return nil } // Must run within the I/O thread. func (m watchMap) set(ino *inode, watch *watch) { i := m[ino.volume] if i == nil { i = make(indexMap) m[ino.volume] = i } i[ino.index] = watch } // Must run within the I/O thread. func (w *Watcher) addWatch(pathname string, flags uint64) error { dir, err := getDir(pathname) if err != nil { return err } if flags&sysFSONLYDIR != 0 && pathname != dir { return nil } ino, err := getIno(dir) if err != nil { return err } w.mu.Lock() watchEntry := w.watches.get(ino) w.mu.Unlock() if watchEntry == nil { if _, e := syscall.CreateIoCompletionPort(ino.handle, w.port, 0, 0); e != nil { syscall.CloseHandle(ino.handle) return os.NewSyscallError("CreateIoCompletionPort", e) } watchEntry = &watch{ ino: ino, path: dir, names: make(map[string]uint64), } w.mu.Lock() w.watches.set(ino, watchEntry) w.mu.Unlock() flags |= provisional } else { syscall.CloseHandle(ino.handle) } if pathname == dir { watchEntry.mask |= flags } else { watchEntry.names[filepath.Base(pathname)] |= flags } if err = w.startRead(watchEntry); err != nil { return err } if pathname == dir { watchEntry.mask &= ^provisional } else { watchEntry.names[filepath.Base(pathname)] &= ^provisional } return nil } // Must run within the I/O thread. func (w *Watcher) remWatch(pathname string) error { dir, err := getDir(pathname) if err != nil { return err } ino, err := getIno(dir) if err != nil { return err } w.mu.Lock() watch := w.watches.get(ino) w.mu.Unlock() if watch == nil { return fmt.Errorf("can't remove non-existent watch for: %s", pathname) } if pathname == dir { w.sendEvent(watch.path, watch.mask&sysFSIGNORED) watch.mask = 0 } else { name := filepath.Base(pathname) w.sendEvent(filepath.Join(watch.path, name), watch.names[name]&sysFSIGNORED) delete(watch.names, name) } return w.startRead(watch) } // Must run within the I/O thread. func (w *Watcher) deleteWatch(watch *watch) { for name, mask := range watch.names { if mask&provisional == 0 { w.sendEvent(filepath.Join(watch.path, name), mask&sysFSIGNORED) } delete(watch.names, name) } if watch.mask != 0 { if watch.mask&provisional == 0 { w.sendEvent(watch.path, watch.mask&sysFSIGNORED) } watch.mask = 0 } } // Must run within the I/O thread. func (w *Watcher) startRead(watch *watch) error { if e := syscall.CancelIo(watch.ino.handle); e != nil { w.Errors <- os.NewSyscallError("CancelIo", e) w.deleteWatch(watch) } mask := toWindowsFlags(watch.mask) for _, m := range watch.names { mask |= toWindowsFlags(m) } if mask == 0 { if e := syscall.CloseHandle(watch.ino.handle); e != nil { w.Errors <- os.NewSyscallError("CloseHandle", e) } w.mu.Lock() delete(w.watches[watch.ino.volume], watch.ino.index) w.mu.Unlock() return nil } e := syscall.ReadDirectoryChanges(watch.ino.handle, &watch.buf[0], uint32(unsafe.Sizeof(watch.buf)), false, mask, nil, &watch.ov, 0) if e != nil { err := os.NewSyscallError("ReadDirectoryChanges", e) if e == syscall.ERROR_ACCESS_DENIED && watch.mask&provisional == 0 { // Watched directory was probably removed if w.sendEvent(watch.path, watch.mask&sysFSDELETESELF) { if watch.mask&sysFSONESHOT != 0 { watch.mask = 0 } } err = nil } w.deleteWatch(watch) w.startRead(watch) return err } return nil } // readEvents reads from the I/O completion port, converts the // received events into Event objects and sends them via the Events channel. // Entry point to the I/O thread. func (w *Watcher) readEvents() { var ( n, key uint32 ov *syscall.Overlapped ) runtime.LockOSThread() for { e := syscall.GetQueuedCompletionStatus(w.port, &n, &key, &ov, syscall.INFINITE) watch := (*watch)(unsafe.Pointer(ov)) if watch == nil { select { case ch := <-w.quit: w.mu.Lock() var indexes []indexMap for _, index := range w.watches { indexes = append(indexes, index) } w.mu.Unlock() for _, index := range indexes { for _, watch := range index { w.deleteWatch(watch) w.startRead(watch) } } var err error if e := syscall.CloseHandle(w.port); e != nil { err = os.NewSyscallError("CloseHandle", e) } close(w.Events) close(w.Errors) ch <- err return case in := <-w.input: switch in.op { case opAddWatch: in.reply <- w.addWatch(in.path, uint64(in.flags)) case opRemoveWatch: in.reply <- w.remWatch(in.path) } default: } continue } switch e { case syscall.ERROR_MORE_DATA: if watch == nil { w.Errors <- errors.New("ERROR_MORE_DATA has unexpectedly null lpOverlapped buffer") } else { // The i/o succeeded but the buffer is full. // In theory we should be building up a full packet. // In practice we can get away with just carrying on. n = uint32(unsafe.Sizeof(watch.buf)) } case syscall.ERROR_ACCESS_DENIED: // Watched directory was probably removed w.sendEvent(watch.path, watch.mask&sysFSDELETESELF) w.deleteWatch(watch) w.startRead(watch) continue case syscall.ERROR_OPERATION_ABORTED: // CancelIo was called on this handle continue default: w.Errors <- os.NewSyscallError("GetQueuedCompletionPort", e) continue case nil: } var offset uint32 for { if n == 0 { w.Events <- newEvent("", sysFSQOVERFLOW) w.Errors <- errors.New("short read in readEvents()") break } // Point "raw" to the event in the buffer raw := (*syscall.FileNotifyInformation)(unsafe.Pointer(&watch.buf[offset])) buf := (*[syscall.MAX_PATH]uint16)(unsafe.Pointer(&raw.FileName)) name := syscall.UTF16ToString(buf[:raw.FileNameLength/2]) fullname := filepath.Join(watch.path, name) var mask uint64 switch raw.Action { case syscall.FILE_ACTION_REMOVED: mask = sysFSDELETESELF case syscall.FILE_ACTION_MODIFIED: mask = sysFSMODIFY case syscall.FILE_ACTION_RENAMED_OLD_NAME: watch.rename = name case syscall.FILE_ACTION_RENAMED_NEW_NAME: if watch.names[watch.rename] != 0 { watch.names[name] |= watch.names[watch.rename] delete(watch.names, watch.rename) mask = sysFSMOVESELF } } sendNameEvent := func() { if w.sendEvent(fullname, watch.names[name]&mask) { if watch.names[name]&sysFSONESHOT != 0 { delete(watch.names, name) } } } if raw.Action != syscall.FILE_ACTION_RENAMED_NEW_NAME { sendNameEvent() } if raw.Action == syscall.FILE_ACTION_REMOVED { w.sendEvent(fullname, watch.names[name]&sysFSIGNORED) delete(watch.names, name) } if w.sendEvent(fullname, watch.mask&toFSnotifyFlags(raw.Action)) { if watch.mask&sysFSONESHOT != 0 { watch.mask = 0 } } if raw.Action == syscall.FILE_ACTION_RENAMED_NEW_NAME { fullname = filepath.Join(watch.path, watch.rename) sendNameEvent() } // Move to the next event in the buffer if raw.NextEntryOffset == 0 { break } offset += raw.NextEntryOffset // Error! if offset >= n { w.Errors <- errors.New("Windows system assumed buffer larger than it is, events have likely been missed.") break } } if err := w.startRead(watch); err != nil { w.Errors <- err } } } func (w *Watcher) sendEvent(name string, mask uint64) bool { if mask == 0 { return false } event := newEvent(name, uint32(mask)) select { case ch := <-w.quit: w.quit <- ch case w.Events <- event: } return true } func toWindowsFlags(mask uint64) uint32 { var m uint32 if mask&sysFSACCESS != 0 { m |= syscall.FILE_NOTIFY_CHANGE_LAST_ACCESS } if mask&sysFSMODIFY != 0 { m |= syscall.FILE_NOTIFY_CHANGE_LAST_WRITE } if mask&sysFSATTRIB != 0 { m |= syscall.FILE_NOTIFY_CHANGE_ATTRIBUTES } if mask&(sysFSMOVE|sysFSCREATE|sysFSDELETE) != 0 { m |= syscall.FILE_NOTIFY_CHANGE_FILE_NAME | syscall.FILE_NOTIFY_CHANGE_DIR_NAME } return m } func toFSnotifyFlags(action uint32) uint64 { switch action { case syscall.FILE_ACTION_ADDED: return sysFSCREATE case syscall.FILE_ACTION_REMOVED: return sysFSDELETE case syscall.FILE_ACTION_MODIFIED: return sysFSMODIFY case syscall.FILE_ACTION_RENAMED_OLD_NAME: return sysFSMOVEDFROM case syscall.FILE_ACTION_RENAMED_NEW_NAME: return sysFSMOVEDTO } return 0 } ================================================ FILE: vendor/github.com/getsentry/sentry-go/.codecov.yml ================================================ codecov: # across notify: # Do not notify until at least this number of reports have been uploaded # from the CI pipeline. We normally have more than that number, but 6 # should be enough to get a first notification. after_n_builds: 6 coverage: status: project: default: # Do not fail the commit status if the coverage was reduced up to this value threshold: 0.5% patch: default: informational: true ignore: - "log_fallback.go" - "internal/testutils" ================================================ FILE: vendor/github.com/getsentry/sentry-go/.craft.yml ================================================ minVersion: 2.14.0 changelog: policy: auto versioning: policy: auto artifactProvider: name: none targets: - name: github tagPrefix: v - name: github tagPrefix: otel/v tagOnly: true - name: github tagPrefix: echo/v tagOnly: true - name: github tagPrefix: fasthttp/v tagOnly: true - name: github tagPrefix: fiber/v tagOnly: true - name: github tagPrefix: gin/v tagOnly: true - name: github tagPrefix: iris/v tagOnly: true - name: github tagPrefix: negroni/v tagOnly: true - name: github tagPrefix: logrus/v tagOnly: true - name: github tagPrefix: slog/v tagOnly: true - name: github tagPrefix: zerolog/v tagOnly: true - name: github tagPrefix: zap/v tagOnly: true - name: registry sdks: github:getsentry/sentry-go: ================================================ FILE: vendor/github.com/getsentry/sentry-go/.gitattributes ================================================ # Tell Git to use LF for line endings on all platforms. # Required to have correct test data on Windows. # https://github.com/mvdan/github-actions-golang#caveats # https://github.com/actions/checkout/issues/135#issuecomment-613361104 * text eol=lf ================================================ FILE: vendor/github.com/getsentry/sentry-go/.gitignore ================================================ # Code coverage artifacts coverage.txt coverage.out coverage.html .coverage/ # Just my personal way of tracking stuff — Kamil FIXME.md TODO.md !NOTES.md # IDE system files .idea .vscode # Local Claude Code settings that should not be committed .claude/settings.local.json ================================================ FILE: vendor/github.com/getsentry/sentry-go/.golangci.yml ================================================ version: "2" linters: default: none enable: - bodyclose - dogsled - dupl - errcheck - gochecknoinits - goconst - gocritic - gocyclo - godot - gosec - govet - ineffassign - misspell - nakedret - prealloc - revive - staticcheck - unconvert - unparam - unused - whitespace exclusions: generated: lax presets: - comments - common-false-positives - legacy - std-error-handling rules: - linters: - goconst - prealloc path: _test\.go - linters: - gosec path: _test\.go text: 'G306:' - linters: - unused path: errors_test\.go - linters: - bodyclose - errcheck path: http/example_test\.go paths: - third_party$ - builtin$ - examples$ formatters: enable: - gofmt - goimports exclusions: generated: lax paths: - third_party$ - builtin$ - examples$ ================================================ FILE: vendor/github.com/getsentry/sentry-go/CHANGELOG.md ================================================ # Changelog ## 0.43.0 ### Breaking Changes 🛠 - Add support for go 1.26 by @giortzisg in [#1193](https://github.com/getsentry/sentry-go/pull/1193) - bump minimum supported go version to 1.24 - change type signature of attributes for Logs and Metrics. by @giortzisg in [#1205](https://github.com/getsentry/sentry-go/pull/1205) - users are not supposed to modify Attributes directly on the Log/Metric itself, but this is still is a breaking change on the type. - Send uint64 overflowing attributes as numbers. by @giortzisg in [#1198](https://github.com/getsentry/sentry-go/pull/1198) - The SDK was converting overflowing uint64 attributes to strings for slog and logrus integrations. To eliminate double types for these attributes, the SDK now sends the overflowing attribute as is, and lets the server handle the overflow appropriately. - It is expected that overflowing unsigned integers would now get dropped, instead of converted to strings. ### New Features ✨ - Add zap logging integration by @giortzisg in [#1184](https://github.com/getsentry/sentry-go/pull/1184) - Log specific message for RequestEntityTooLarge by @giortzisg in [#1185](https://github.com/getsentry/sentry-go/pull/1185) ### Bug Fixes 🐛 - Improve otel span map cleanup performance by @giortzisg in [#1200](https://github.com/getsentry/sentry-go/pull/1200) - Ensure correct signal delivery on multi-client setups by @giortzisg in [#1190](https://github.com/getsentry/sentry-go/pull/1190) ### Internal Changes 🔧 #### Deps - Bump golang.org/x/crypto to 0.48.0 by @giortzisg in [#1196](https://github.com/getsentry/sentry-go/pull/1196) - Use go1.24.0 by @giortzisg in [#1195](https://github.com/getsentry/sentry-go/pull/1195) - Bump github.com/gofiber/fiber/v2 from 2.52.9 to 2.52.11 in /fiber by @dependabot in [#1191](https://github.com/getsentry/sentry-go/pull/1191) - Bump getsentry/craft from 2.19.0 to 2.20.1 by @dependabot in [#1187](https://github.com/getsentry/sentry-go/pull/1187) #### Other - Add omitzero and remove custom serialization by @giortzisg in [#1197](https://github.com/getsentry/sentry-go/pull/1197) - Rename Telemetry Processor components by @giortzisg in [#1186](https://github.com/getsentry/sentry-go/pull/1186) ## 0.42.0 ### Breaking Changes 🛠 - refactor Telemetry Processor to use TelemetryItem instead of ItemConvertible by @giortzisg in [#1180](https://github.com/getsentry/sentry-go/pull/1180) - remove ToEnvelopeItem from single log items - rename TelemetryBuffer to Telemetry Processor to adhere to spec - remove unsed ToEnvelopeItem(dsn) from Event. ### New Features ✨ - Add metric support by @aldy505 in [#1151](https://github.com/getsentry/sentry-go/pull/1151) - support for three metric methods (counter, gauge, distribution) - custom metric units - unexport batchlogger ### Internal Changes 🔧 #### Release - Fix changelog-preview permissions by @BYK in [#1181](https://github.com/getsentry/sentry-go/pull/1181) - Switch from action-prepare-release to Craft by @BYK in [#1167](https://github.com/getsentry/sentry-go/pull/1167) #### Other - (repo) Add Claude Code settings with basic permissions by @philipphofmann in [#1175](https://github.com/getsentry/sentry-go/pull/1175) - Update release and changelog-preview workflows by @giortzisg in [#1177](https://github.com/getsentry/sentry-go/pull/1177) - Bump echo to 4.10.1 by @giortzisg in [#1174](https://github.com/getsentry/sentry-go/pull/1174) ## 0.41.0 The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.41.0. ### Features - Add HTTP client integration for distributed tracing via `sentryhttpclient` package ([#876](https://github.com/getsentry/sentry-go/pull/876)) - Provides an `http.RoundTripper` implementation that automatically creates spans for outgoing HTTP requests - Supports trace propagation targets configuration via `WithTracePropagationTargets` option - Example usage: ```go import sentryhttpclient "github.com/getsentry/sentry-go/httpclient" roundTripper := sentryhttpclient.NewSentryRoundTripper(nil) client := &http.Client{ Transport: roundTripper, } ``` - Add `ClientOptions.PropagateTraceparent` option to control W3C `traceparent` header propagation in outgoing HTTP requests ([#1161](https://github.com/getsentry/sentry-go/pull/1161)) - Add `SpanID` field to structured logs ([#1169](https://github.com/getsentry/sentry-go/pull/1169)) ## 0.40.0 The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.40.0. ### Bug Fixes - Disable `DisableTelemetryBuffer` flag and noop Telemetry Buffer, to prevent a panic at runtime ([#1149](https://github.com/getsentry/sentry-go/pull/1149)). ## 0.39.0 The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.39.0. ### Features - Drop events from the telemetry buffer when rate-limited or transport is full, allowing the buffer queue to empty itself under load ([#1138](https://github.com/getsentry/sentry-go/pull/1138)). ### Bug Fixes - Fix scheduler's `hasWork()` method to check if buffers are ready to flush. The previous implementation was causing CPU spikes ([#1143](https://github.com/getsentry/sentry-go/pull/1143)). ## 0.38.0 ### Breaking Changes ### Features - Introduce a new async envelope transport and telemetry buffer to prioritize and batch events ([#1094](https://github.com/getsentry/sentry-go/pull/1094), [#1093](https://github.com/getsentry/sentry-go/pull/1093), [#1107](https://github.com/getsentry/sentry-go/pull/1107)). - Advantages: - Prioritized, per-category buffers (errors, transactions, logs, check-ins) reduce starvation and improve resilience under load - Batching for high-volume logs (up to 100 items or 5s) cuts network overhead - Bounded memory with eviction policies - Improved flush behavior with context-aware flushing - Add `ClientOptions.DisableTelemetryBuffer` to opt out and fall back to the legacy transport layer (`HTTPTransport` / `HTTPSyncTransport`). ```go err := sentry.Init(sentry.ClientOptions{ Dsn: "__DSN__", DisableTelemetryBuffer: true, // fallback to legacy transport }) ``` ### Notes - If a custom `Transport` is provided, the SDK automatically disables the telemetry buffer and uses the legacy transport for compatibility. ## 0.37.0 The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.37.0. ### Breaking Changes - Behavioral change for the `TraceIgnoreStatusCodes` option. The option now defaults to ignoring 404 status codes ([#1122](https://github.com/getsentry/sentry-go/pull/1122)). ### Features - Add `sentry.origin` attribute to structured logs to identify log origin for `slog` and `logrus` integrations (`auto.log.slog`, `auto.log.logrus`) ([#1121](https://github.com/getsentry/sentry-go/pull/1121)). ### Bug Fixes - Fix `slog` event handler to use the initial context, ensuring events use the correct hub/span when the emission context lacks one ([#1133](https://github.com/getsentry/sentry-go/pull/1133)). - Improve exception chain processing by checking pointer values when tracking visited errors, avoiding instability for certain wrapped errors ([#1132](https://github.com/getsentry/sentry-go/pull/1132)). ### Misc - Bump `golang.org/x/net` to v0.38.0 ([#1126](https://github.com/getsentry/sentry-go/pull/1126)). ## 0.36.2 The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.36.2. ### Bug Fixes - Fix context propagation for logs to ensure logger instances correctly inherit span and hub information from their creation context ([#1118](https://github.com/getsentry/sentry-go/pull/1118)) - Logs now properly propagate trace context from the logger's original context, even when emitted in a different context - The logger will first check the emission context, then fall back to its creation context, and finally to the current hub ## 0.36.1 The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.36.1. ### Bug Fixes - Prevent panic when converting error chains containing non-comparable error types by using a safe fallback for visited detection in exception conversion ([#1113](https://github.com/getsentry/sentry-go/pull/1113)) ## 0.36.0 The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.36.0. ### Breaking Changes - Behavioral change for the `MaxBreadcrumbs` client option. Removed the hard limit of 100 breadcrumbs, allowing users to set a larger limit and also changed the default limit from 30 to 100 ([#1106](https://github.com/getsentry/sentry-go/pull/1106))) - The changes to error handling ([#1075](https://github.com/getsentry/sentry-go/pull/1075)) will affect issue grouping. It is expected that any wrapped and complex errors will be grouped under a new issue group. ### Features - Add support for improved issue grouping with enhanced error chain handling ([#1075](https://github.com/getsentry/sentry-go/pull/1075)) The SDK now provides better handling of complex error scenarios, particularly when dealing with multiple related errors or error chains. This feature automatically detects and properly structures errors created with Go's `errors.Join()` function and other multi-error patterns. ```go // Multiple errors are now properly grouped and displayed in Sentry err1 := errors.New("err1") err2 := errors.New("err2") combinedErr := errors.Join(err1, err2) // When captured, these will be shown as related exceptions in Sentry sentry.CaptureException(combinedErr) ``` - Add `TraceIgnoreStatusCodes` option to allow filtering of HTTP transactions based on status codes ([#1089](https://github.com/getsentry/sentry-go/pull/1089)) - Configure which HTTP status codes should not be traced by providing single codes or ranges - Example: `TraceIgnoreStatusCodes: [][]int{{404}, {500, 599}}` ignores 404 and server errors 500-599 ### Bug Fixes - Fix logs being incorrectly filtered by `BeforeSend` callback ([#1109](https://github.com/getsentry/sentry-go/pull/1109)) - Logs now bypass the `processEvent` method and are sent directly to the transport - This ensures logs are only filtered by `BeforeSendLog`, not by the error/message `BeforeSend` callback ### Misc - Add support for Go 1.25 and drop support for Go 1.22 ([#1103](https://github.com/getsentry/sentry-go/pull/1103)) ## 0.35.3 The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.35.3. ### Bug Fixes - Add missing rate limit categories ([#1082](https://github.com/getsentry/sentry-go/pull/1082)) ## 0.35.2 The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.35.2. ### Bug Fixes - Fix OpenTelemetry spans being created as transactions instead of child spans ([#1073](https://github.com/getsentry/sentry-go/pull/1073)) ### Misc - Add `MockTransport` to test clients for improved testing ([#1071](https://github.com/getsentry/sentry-go/pull/1071)) ## 0.35.1 The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.35.1. ### Bug Fixes - Fix race conditions when accessing the scope during logging operations ([#1050](https://github.com/getsentry/sentry-go/pull/1050)) - Fix nil pointer dereference with malformed URLs when tracing is enabled in `fasthttp` and `fiber` integrations ([#1055](https://github.com/getsentry/sentry-go/pull/1055)) ### Misc - Bump `github.com/gofiber/fiber/v2` from 2.52.5 to 2.52.9 in `/fiber` ([#1067](https://github.com/getsentry/sentry-go/pull/1067)) ## 0.35.0 The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.35.0. ### Breaking Changes - Changes to the logging API ([#1046](https://github.com/getsentry/sentry-go/pull/1046)) The logging API now supports a fluent interface for structured logging with attributes: ```go // usage before logger := sentry.NewLogger(ctx) // attributes weren't being set permanently logger.SetAttributes( attribute.String("version", "1.0.0"), ) logger.Infof(ctx, "Message with parameters %d and %d", 1, 2) // new behavior ctx := context.Background() logger := sentry.NewLogger(ctx) // Set permanent attributes on the logger logger.SetAttributes( attribute.String("version", "1.0.0"), ) // Chain attributes on individual log entries logger.Info(). String("key.string", "value"). Int("key.int", 42). Bool("key.bool", true). Emitf("Message with parameters %d and %d", 1, 2) ``` ### Bug Fixes - Correctly serialize `FailureIssueThreshold` and `RecoveryThreshold` onto check-in payloads ([#1060](https://github.com/getsentry/sentry-go/pull/1060)) ## 0.34.1 The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.34.1. ### Bug Fixes - Allow flush to be used multiple times without issues, particularly for the batch logger ([#1051](https://github.com/getsentry/sentry-go/pull/1051)) - Fix race condition in `Scope.GetSpan()` method by adding proper mutex locking ([#1044](https://github.com/getsentry/sentry-go/pull/1044)) - Guard transport on `Close()` to prevent panic when called multiple times ([#1044](https://github.com/getsentry/sentry-go/pull/1044)) ## 0.34.0 The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.34.0. ### Breaking Changes - Logrus structured logging support replaces the `sentrylogrus.Hook` signature from a `*Hook` to an interface. ```go var hook *sentrylogrus.Hook hook = sentrylogrus.New( // ... your setup ) // should change the definition to var hook sentrylogrus.Hook hook = sentrylogrus.New( // ... your setup ) ``` ### Features - Structured logging support for [slog](https://pkg.go.dev/log/slog). ([#1033](https://github.com/getsentry/sentry-go/pull/1033)) ```go ctx := context.Background() handler := sentryslog.Option{ EventLevel: []slog.Level{slog.LevelError, sentryslog.LevelFatal}, // Only Error and Fatal as events LogLevel: []slog.Level{slog.LevelWarn, slog.LevelInfo}, // Only Warn and Info as logs }.NewSentryHandler(ctx) logger := slog.New(handler) logger.Info("hello")) ``` - Structured logging support for [logrus](https://github.com/sirupsen/logrus). ([#1036](https://github.com/getsentry/sentry-go/pull/1036)) ```go logHook, _ := sentrylogrus.NewLogHook( []logrus.Level{logrus.InfoLevel, logrus.WarnLevel}, sentry.ClientOptions{ Dsn: "your-dsn", EnableLogs: true, // Required for log entries }) defer logHook.Flush(5 * time.Secod) logrus.RegisterExitHandler(func() { logHook.Flush(5 * time.Second) }) logger := logrus.New() logger.AddHook(logHook) logger.Infof("hello") ``` - Add support for flushing events with context using `FlushWithContext()`. ([#935](https://github.com/getsentry/sentry-go/pull/935)) ```go ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if !sentry.FlushWithContext(ctx) { // Handle timeout or cancellation } ``` - Add support for custom fingerprints in slog integration. ([#1039](https://github.com/getsentry/sentry-go/pull/1039)) ### Deprecations - Slog structured logging support replaces `Level` option with `EventLevel` and `LogLevel` options, for specifying fine-grained levels for capturing events and logs. ```go handler := sentryslog.Option{ EventLevel: []slog.Level{slog.LevelWarn, slog.LevelError, sentryslog.LevelFatal}, LogLevel: []slog.Level{slog.LevelDebug, slog.LevelInfo, slog.LevelWarn, slog.LevelError, sentryslog.LevelFatal}, }.NewSentryHandler(ctx) ``` - Logrus structured logging support replaces `New` and `NewFromClient` functions to `NewEventHook`, `NewEventHookFromClient`, to match the newly added `NewLogHook` functions, and specify the hook type being created each time. ```go logHook, err := sentrylogrus.NewLogHook( []logrus.Level{logrus.InfoLevel}, sentry.ClientOptions{}) eventHook, err := sentrylogrus.NewEventHook([]logrus.Level{ logrus.ErrorLevel, logrus.FatalLevel, logrus.PanicLevel, }, sentry.ClientOptions{}) ``` ### Bug Fixes - Fix issue where `ContinueTrace()` would panic when `sentry-trace` header does not exist. ([#1026](https://github.com/getsentry/sentry-go/pull/1026)) - Fix incorrect log level signature in structured logging. ([#1034](https://github.com/getsentry/sentry-go/pull/1034)) - Remove `sentry.origin` attribute from Sentry logger to prevent confusion in spans. ([#1038](https://github.com/getsentry/sentry-go/pull/1038)) - Don't gate user information behind `SendDefaultPII` flag for logs. ([#1032](https://github.com/getsentry/sentry-go/pull/1032)) ### Misc - Add more sensitive HTTP headers to the default list of headers that are scrubbed by default. ([#1008](https://github.com/getsentry/sentry-go/pull/1008)) ## 0.33.0 The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.33.0. ### Breaking Changes - Rename the internal `Logger` to `DebugLogger`. This feature was only used when you set `Debug: True` in your `sentry.Init()` call. If you haven't used the Logger directly, no changes are necessary. ([#1012](https://github.com/getsentry/sentry-go/issues/1012)) ### Features - Add support for [Structured Logging](https://docs.sentry.io/product/explore/logs/). ([#1010](https://github.com/getsentry/sentry-go/issues/1010)) ```go logger := sentry.NewLogger(ctx) logger.Info(ctx, "Hello, Logs!") ``` You can learn more about Sentry Logs on our [docs](https://docs.sentry.io/product/explore/logs/) and the [examples](https://github.com/getsentry/sentry-go/blob/master/_examples/logs/main.go). - Add new attributes APIs, which are currently only exposed on logs. ([#1007](https://github.com/getsentry/sentry-go/issues/1007)) ### Bug Fixes - Do not push a new scope on `StartSpan`. ([#1013](https://github.com/getsentry/sentry-go/issues/1013)) - Fix an issue where the propagated smapling decision wasn't used. ([#995](https://github.com/getsentry/sentry-go/issues/995)) - [Otel] Prefer `httpRoute` over `httpTarget` for span descriptions. ([#1002](https://github.com/getsentry/sentry-go/issues/1002)) ### Misc - Update `github.com/stretchr/testify` to v1.8.4. ([#988](https://github.com/getsentry/sentry-go/issues/988)) ## 0.32.0 The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.32.0. ### Breaking Changes - Bump the minimum Go version to 1.22. The supported versions are 1.22, 1.23 and 1.24. ([#967](https://github.com/getsentry/sentry-go/issues/967)) - Setting any values on `span.Extra` has no effect anymore. Use `SetData(name string, value interface{})` instead. ([#864](https://github.com/getsentry/sentry-go/pull/864)) ### Features - Add a `MockTransport` and `MockScope`. ([#972](https://github.com/getsentry/sentry-go/pull/972)) ### Bug Fixes - Fix writing `*http.Request` in the Logrus JSONFormatter. ([#955](https://github.com/getsentry/sentry-go/issues/955)) ### Misc - Transaction `data` attributes are now seralized as trace context data attributes, allowing you to query these attributes in the [Trace Explorer](https://docs.sentry.io/product/explore/traces/). ## 0.31.1 The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.31.1. ### Bug Fixes - Correct wrong module name for `sentry-go/logrus` ([#950](https://github.com/getsentry/sentry-go/pull/950)) ## 0.31.0 The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.31.0. ### Breaking Changes - Remove support for metrics. Read more about the end of the Metrics beta [here](https://sentry.zendesk.com/hc/en-us/articles/26369339769883-Metrics-Beta-Ended-on-October-7th). ([#914](https://github.com/getsentry/sentry-go/pull/914)) - Remove support for profiling. ([#915](https://github.com/getsentry/sentry-go/pull/915)) - Remove `Segment` field from the `User` struct. This field is no longer used in the Sentry product. ([#928](https://github.com/getsentry/sentry-go/pull/928)) - Every integration is now a separate module, reducing the binary size and number of dependencies. Once you update `sentry-go` to latest version, you'll need to `go get` the integration you want to use. For example, if you want to use the `echo` integration, you'll need to run `go get github.com/getsentry/sentry-go/echo` ([#919](github.com/getsentry/sentry-go/pull/919)). ### Features Add the ability to override `hub` in `context` for integrations that use custom context. ([#931](https://github.com/getsentry/sentry-go/pull/931)) - Add `HubProvider` Hook for `sentrylogrus`, enabling dynamic Sentry hub allocation for each log entry or goroutine. ([#936](https://github.com/getsentry/sentry-go/pull/936)) This change enhances compatibility with Sentry's recommendation of using separate hubs per goroutine. To ensure a separate Sentry hub for each goroutine, configure the `HubProvider` like this: ```go hook, err := sentrylogrus.New(nil, sentry.ClientOptions{}) if err != nil { log.Fatalf("Failed to initialize Sentry hook: %v", err) } // Set a custom HubProvider to generate a new hub for each goroutine or log entry hook.SetHubProvider(func() *sentry.Hub { client, _ := sentry.NewClient(sentry.ClientOptions{}) return sentry.NewHub(client, sentry.NewScope()) }) logrus.AddHook(hook) ``` ### Bug Fixes - Add support for closing worker goroutines started by the `HTTPTranport` to prevent goroutine leaks. ([#894](https://github.com/getsentry/sentry-go/pull/894)) ```go client, _ := sentry.NewClient() defer client.Close() ``` Worker can be also closed by calling `Close()` method on the `HTTPTransport` instance. `Close` should be called after `Flush` and before terminating the program otherwise some events may be lost. ```go transport := sentry.NewHTTPTransport() defer transport.Close() ``` ### Misc - Bump [gin-gonic/gin](https://github.com/gin-gonic/gin) to v1.9.1. ([#946](https://github.com/getsentry/sentry-go/pull/946)) ## 0.30.0 The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.30.0. ### Features - Add `sentryzerolog` integration ([#857](https://github.com/getsentry/sentry-go/pull/857)) - Add `sentryslog` integration ([#865](https://github.com/getsentry/sentry-go/pull/865)) - Always set Mechanism Type to generic ([#896](https://github.com/getsentry/sentry-go/pull/897)) ### Bug Fixes - Prevent panic in `fasthttp` and `fiber` integration in case a malformed URL has to be parsed ([#912](https://github.com/getsentry/sentry-go/pull/912)) ### Misc Drop support for Go 1.18, 1.19 and 1.20. The currently supported Go versions are the last 3 stable releases: 1.23, 1.22 and 1.21. ## 0.29.1 The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.29.1. ### Bug Fixes - Correlate errors to the current trace ([#886](https://github.com/getsentry/sentry-go/pull/886)) - Set the trace context when the transaction finishes ([#888](https://github.com/getsentry/sentry-go/pull/888)) ### Misc - Update the `sentrynegroni` integration to use the latest (v3.1.1) version of Negroni ([#885](https://github.com/getsentry/sentry-go/pull/885)) ## 0.29.0 The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.29.0. ### Breaking Changes - Remove the `sentrymartini` integration ([#861](https://github.com/getsentry/sentry-go/pull/861)) - The `WrapResponseWriter` has been moved from the `sentryhttp` package to the `internal/httputils` package. If you've imported it previosuly, you'll need to copy the implementation in your project. ([#871](https://github.com/getsentry/sentry-go/pull/871)) ### Features - Add new convenience methods to continue a trace and propagate tracing headers for error-only use cases. ([#862](https://github.com/getsentry/sentry-go/pull/862)) If you are not using one of our integrations, you can manually continue an incoming trace by using `sentry.ContinueTrace()` by providing the `sentry-trace` and `baggage` header received from a downstream SDK. ```go hub := sentry.CurrentHub() sentry.ContinueTrace(hub, r.Header.Get(sentry.SentryTraceHeader), r.Header.Get(sentry.SentryBaggageHeader)), ``` You can use `hub.GetTraceparent()` and `hub.GetBaggage()` to fetch the necessary header values for outgoing HTTP requests. ```go hub := sentry.GetHubFromContext(ctx) req, _ := http.NewRequest("GET", "http://localhost:3000", nil) req.Header.Add(sentry.SentryTraceHeader, hub.GetTraceparent()) req.Header.Add(sentry.SentryBaggageHeader, hub.GetBaggage()) ``` ### Bug Fixes - Initialize `HTTPTransport.limit` if `nil` ([#844](https://github.com/getsentry/sentry-go/pull/844)) - Fix `sentry.StartTransaction()` returning a transaction with an outdated context on existing transactions ([#854](https://github.com/getsentry/sentry-go/pull/854)) - Treat `Proxy-Authorization` as a sensitive header ([#859](https://github.com/getsentry/sentry-go/pull/859)) - Add support for the `http.Hijacker` interface to the `sentrynegroni` package ([#871](https://github.com/getsentry/sentry-go/pull/871)) - Go version >= 1.23: Use value from `http.Request.Pattern` for HTTP transaction names when using `sentryhttp` & `sentrynegroni` ([#875](https://github.com/getsentry/sentry-go/pull/875)) - Go version >= 1.21: Fix closure functions name grouping ([#877](https://github.com/getsentry/sentry-go/pull/877)) ### Misc - Collect `span` origins ([#849](https://github.com/getsentry/sentry-go/pull/849)) ## 0.28.1 The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.28.1. ### Bug Fixes - Implement `http.ResponseWriter` to hook into various parts of the response process ([#837](https://github.com/getsentry/sentry-go/pull/837)) ## 0.28.0 The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.28.0. ### Features - Add a `Fiber` performance tracing & error reporting integration ([#795](https://github.com/getsentry/sentry-go/pull/795)) - Add performance tracing to the `Echo` integration ([#722](https://github.com/getsentry/sentry-go/pull/722)) - Add performance tracing to the `FastHTTP` integration ([#732](https://github.com/getsentry/sentry-go/pull/723)) - Add performance tracing to the `Iris` integration ([#809](https://github.com/getsentry/sentry-go/pull/809)) - Add performance tracing to the `Negroni` integration ([#808](https://github.com/getsentry/sentry-go/pull/808)) - Add `FailureIssueThreshold` & `RecoveryThreshold` to `MonitorConfig` ([#775](https://github.com/getsentry/sentry-go/pull/775)) - Use `errors.Unwrap()` to create exception groups ([#792](https://github.com/getsentry/sentry-go/pull/792)) - Add support for matching on strings for `ClientOptions.IgnoreErrors` & `ClientOptions.IgnoreTransactions` ([#819](https://github.com/getsentry/sentry-go/pull/819)) - Add `http.request.method` attribute for performance span data ([#786](https://github.com/getsentry/sentry-go/pull/786)) - Accept `interface{}` for span data values ([#784](https://github.com/getsentry/sentry-go/pull/784)) ### Bug Fixes - Fix missing stack trace for parsing error in `logrusentry` ([#689](https://github.com/getsentry/sentry-go/pull/689)) ## 0.27.0 The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.27.0. ### Breaking Changes - `Exception.ThreadId` is now typed as `uint64`. It was wrongly typed as `string` before. ([#770](https://github.com/getsentry/sentry-go/pull/770)) ### Misc - Export `Event.Attachments` ([#771](https://github.com/getsentry/sentry-go/pull/771)) ## 0.26.0 The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.26.0. ### Breaking Changes As previously announced, this release removes some methods from the SDK. - `sentry.TransactionName()` use `sentry.WithTransactionName()` instead. - `sentry.OpName()` use `sentry.WithOpName()` instead. - `sentry.TransctionSource()` use `sentry.WithTransactionSource()` instead. - `sentry.SpanSampled()` use `sentry.WithSpanSampled()` instead. ### Features - Add `WithDescription` span option ([#751](https://github.com/getsentry/sentry-go/pull/751)) ```go span := sentry.StartSpan(ctx, "http.client", WithDescription("GET /api/users")) ``` - Add support for package name parsing in Go 1.20 and higher ([#730](https://github.com/getsentry/sentry-go/pull/730)) ### Bug Fixes - Apply `ClientOptions.SampleRate` only to errors & messages ([#754](https://github.com/getsentry/sentry-go/pull/754)) - Check if git is available before executing any git commands ([#737](https://github.com/getsentry/sentry-go/pull/737)) ## 0.25.0 The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.25.0. ### Breaking Changes As previously announced, this release removes two global constants from the SDK. - `sentry.Version` was removed. Use `sentry.SDKVersion` instead ([#727](https://github.com/getsentry/sentry-go/pull/727)) - `sentry.SDKIdentifier` was removed. Use `Client.GetSDKIdentifier()` instead ([#727](https://github.com/getsentry/sentry-go/pull/727)) ### Features - Add `ClientOptions.IgnoreTransactions`, which allows you to ignore specific transactions based on their name ([#717](https://github.com/getsentry/sentry-go/pull/717)) - Add `ClientOptions.Tags`, which allows you to set global tags that are applied to all events. You can also define tags by setting `SENTRY_TAGS_` environment variables ([#718](https://github.com/getsentry/sentry-go/pull/718)) ### Bug fixes - Fix an issue in the profiler that would cause an infinite loop if the duration of a transaction is longer than 30 seconds ([#724](https://github.com/getsentry/sentry-go/issues/724)) ### Misc - `dsn.RequestHeaders()` is not to be removed, though it is still considered deprecated and should only be used when using a custom transport that sends events to the `/store` endpoint ([#720](https://github.com/getsentry/sentry-go/pull/720)) ## 0.24.1 The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.24.1. ### Bug fixes - Prevent a panic in `sentryotel.flushSpanProcessor()` ([(#711)](https://github.com/getsentry/sentry-go/pull/711)) - Prevent a panic when setting the SDK identifier ([#715](https://github.com/getsentry/sentry-go/pull/715)) ## 0.24.0 The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.24.0. ### Deprecations - `sentry.Version` to be removed in 0.25.0. Use `sentry.SDKVersion` instead. - `sentry.SDKIdentifier` to be removed in 0.25.0. Use `Client.GetSDKIdentifier()` instead. - `dsn.RequestHeaders()` to be removed after 0.25.0, but no earlier than December 1, 2023. Requests to the `/envelope` endpoint are authenticated using the DSN in the envelope header. ### Features - Run a single instance of the profiler instead of multiple ones for each Go routine ([#655](https://github.com/getsentry/sentry-go/pull/655)) - Use the route path as the transaction names when using the Gin integration ([#675](https://github.com/getsentry/sentry-go/pull/675)) - Set the SDK name accordingly when a framework integration is used ([#694](https://github.com/getsentry/sentry-go/pull/694)) - Read release information (VCS revision) from `debug.ReadBuildInfo` ([#704](https://github.com/getsentry/sentry-go/pull/704)) ### Bug fixes - [otel] Fix incorrect usage of `attributes.Value.AsString` ([#684](https://github.com/getsentry/sentry-go/pull/684)) - Fix trace function name parsing in profiler on go1.21+ ([#695](https://github.com/getsentry/sentry-go/pull/695)) ### Misc - Test against Go 1.21 ([#695](https://github.com/getsentry/sentry-go/pull/695)) - Make tests more robust ([#698](https://github.com/getsentry/sentry-go/pull/698), [#699](https://github.com/getsentry/sentry-go/pull/699), [#700](https://github.com/getsentry/sentry-go/pull/700), [#702](https://github.com/getsentry/sentry-go/pull/702)) ## 0.23.0 The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.23.0. ### Features - Initial support for [Cron Monitoring](https://docs.sentry.io/product/crons/) ([#661](https://github.com/getsentry/sentry-go/pull/661)) This is how the basic usage of the feature looks like: ```go // 🟡 Notify Sentry your job is running: checkinId := sentry.CaptureCheckIn( &sentry.CheckIn{ MonitorSlug: "", Status: sentry.CheckInStatusInProgress, }, nil, ) // Execute your scheduled task here... // 🟢 Notify Sentry your job has completed successfully: sentry.CaptureCheckIn( &sentry.CheckIn{ ID: *checkinId, MonitorSlug: "", Status: sentry.CheckInStatusOK, }, nil, ) ``` A full example of using Crons Monitoring is available [here](https://github.com/getsentry/sentry-go/blob/dde4d360660838f3c2e0ced8205bc8f7a8d312d9/_examples/crons/main.go). More documentation on configuring and using Crons [can be found here](https://docs.sentry.io/platforms/go/crons/). - Add support for [Event Attachments](https://docs.sentry.io/platforms/go/enriching-events/attachments/) ([#670](https://github.com/getsentry/sentry-go/pull/670)) It's now possible to add file/binary payloads to Sentry events: ```go sentry.ConfigureScope(func(scope *sentry.Scope) { scope.AddAttachment(&Attachment{ Filename: "report.html", ContentType: "text/html", Payload: []byte("

Look, HTML

"), }) }) ``` The attachment will then be accessible on the Issue Details page. - Add sampling decision to trace envelope header ([#666](https://github.com/getsentry/sentry-go/pull/666)) - Expose SpanFromContext function ([#672](https://github.com/getsentry/sentry-go/pull/672)) ### Bug fixes - Make `Span.Finish` a no-op when the span is already finished ([#660](https://github.com/getsentry/sentry-go/pull/660)) ## 0.22.0 The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.22.0. This release contains initial [profiling](https://docs.sentry.io/product/profiling/) support, as well as a few bug fixes and improvements. ### Features - Initial (alpha) support for [profiling](https://docs.sentry.io/product/profiling/) ([#626](https://github.com/getsentry/sentry-go/pull/626)) Profiling is disabled by default. To enable it, configure both `TracesSampleRate` and `ProfilesSampleRate` when initializing the SDK: ```go err := sentry.Init(sentry.ClientOptions{ Dsn: "__DSN__", EnableTracing: true, TracesSampleRate: 1.0, // The sampling rate for profiling is relative to TracesSampleRate. In this case, we'll capture profiles for 100% of transactions. ProfilesSampleRate: 1.0, }) ``` More documentation on profiling and current limitations [can be found here](https://docs.sentry.io/platforms/go/profiling/). - Add transactions/tracing support go the Gin integration ([#644](https://github.com/getsentry/sentry-go/pull/644)) ### Bug fixes - Always set a valid source on transactions ([#637](https://github.com/getsentry/sentry-go/pull/637)) - Clone scope.Context in more places to avoid panics on concurrent reads and writes ([#638](https://github.com/getsentry/sentry-go/pull/638)) - Fixes [#570](https://github.com/getsentry/sentry-go/issues/570) - Fix frames recognized as not being in-app still showing as in-app ([#647](https://github.com/getsentry/sentry-go/pull/647)) ## 0.21.0 The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.21.0. Note: this release includes one **breaking change** and some **deprecations**, which are listed below. ### Breaking Changes **This change does not apply if you use [https://sentry.io](https://sentry.io)** - Remove support for the `/store` endpoint ([#631](https://github.com/getsentry/sentry-go/pull/631)) - This change requires a self-hosted version of Sentry 20.6.0 or higher. If you are using a version of [self-hosted Sentry](https://develop.sentry.dev/self-hosted/) (aka *on-premise*) older than 20.6.0, then you will need to [upgrade](https://develop.sentry.dev/self-hosted/releases/) your instance. ### Features - Rename four span option functions ([#611](https://github.com/getsentry/sentry-go/pull/611), [#624](https://github.com/getsentry/sentry-go/pull/624)) - `TransctionSource` -> `WithTransactionSource` - `SpanSampled` -> `WithSpanSampled` - `OpName` -> `WithOpName` - `TransactionName` -> `WithTransactionName` - Old functions `TransctionSource`, `SpanSampled`, `OpName`, and `TransactionName` are still available but are now **deprecated** and will be removed in a future release. - Make `client.EventFromMessage` and `client.EventFromException` methods public ([#607](https://github.com/getsentry/sentry-go/pull/607)) - Add `client.SetException` method ([#607](https://github.com/getsentry/sentry-go/pull/607)) - This allows to set or add errors to an existing `Event`. ### Bug Fixes - Protect from panics while doing concurrent reads/writes to Span data fields ([#609](https://github.com/getsentry/sentry-go/pull/609)) - [otel] Improve detection of Sentry-related spans ([#632](https://github.com/getsentry/sentry-go/pull/632), [#636](https://github.com/getsentry/sentry-go/pull/636)) - Fixes cases when HTTP spans containing requests to Sentry were captured by Sentry ([#627](https://github.com/getsentry/sentry-go/issues/627)) ### Misc - Drop testing in (legacy) GOPATH mode ([#618](https://github.com/getsentry/sentry-go/pull/618)) - Remove outdated documentation from https://pkg.go.dev/github.com/getsentry/sentry-go ([#623](https://github.com/getsentry/sentry-go/pull/623)) ## 0.20.0 The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.20.0. Note: this release has some **breaking changes**, which are listed below. ### Breaking Changes - Remove the following methods: `Scope.SetTransaction()`, `Scope.Transaction()` ([#605](https://github.com/getsentry/sentry-go/pull/605)) Span.Name should be used instead to access the transaction's name. For example, the following [`TracesSampler`](https://docs.sentry.io/platforms/go/configuration/sampling/#setting-a-sampling-function) function should be now written as follows: **Before:** ```go TracesSampler: func(ctx sentry.SamplingContext) float64 { hub := sentry.GetHubFromContext(ctx.Span.Context()) if hub.Scope().Transaction() == "GET /health" { return 0 } return 1 }, ``` **After:** ```go TracesSampler: func(ctx sentry.SamplingContext) float64 { if ctx.Span.Name == "GET /health" { return 0 } return 1 }, ``` ### Features - Add `Span.SetContext()` method ([#599](https://github.com/getsentry/sentry-go/pull/599/)) - It is recommended to use it instead of `hub.Scope().SetContext` when setting or updating context on transactions. - Add `DebugMeta` interface to `Event` and extend `Frame` structure with more fields ([#606](https://github.com/getsentry/sentry-go/pull/606)) - More about DebugMeta interface [here](https://develop.sentry.dev/sdk/event-payloads/debugmeta/). ### Bug Fixes - [otel] Fix missing OpenTelemetry context on some events ([#599](https://github.com/getsentry/sentry-go/pull/599), [#605](https://github.com/getsentry/sentry-go/pull/605)) - Fixes ([#596](https://github.com/getsentry/sentry-go/issues/596)). - [otel] Better handling for HTTP span attributes ([#610](https://github.com/getsentry/sentry-go/pull/610)) ### Misc - Bump minimum versions: `github.com/kataras/iris/v12` to 12.2.0, `github.com/labstack/echo/v4` to v4.10.0 ([#595](https://github.com/getsentry/sentry-go/pull/595)) - Resolves [GO-2022-1144 / CVE-2022-41717](https://deps.dev/advisory/osv/GO-2022-1144), [GO-2023-1495 / CVE-2022-41721](https://deps.dev/advisory/osv/GO-2023-1495), [GO-2022-1059 / CVE-2022-32149](https://deps.dev/advisory/osv/GO-2022-1059). - Bump `google.golang.org/protobuf` minimum required version to 1.29.1 ([#604](https://github.com/getsentry/sentry-go/pull/604)) - This fixes a potential denial of service issue ([CVE-2023-24535](https://github.com/advisories/GHSA-hw7c-3rfg-p46j)). - Exclude the `otel` module when building in GOPATH mode ([#615](https://github.com/getsentry/sentry-go/pull/615)) ## 0.19.0 The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.19.0. ### Features - Add support for exception mechanism metadata ([#564](https://github.com/getsentry/sentry-go/pull/564/)) - More about exception mechanisms [here](https://develop.sentry.dev/sdk/event-payloads/exception/#exception-mechanism). ### Bug Fixes - [otel] Use the correct "trace" context when sending a Sentry error ([#580](https://github.com/getsentry/sentry-go/pull/580/)) ### Misc - Drop support for Go 1.17, add support for Go 1.20 ([#563](https://github.com/getsentry/sentry-go/pull/563/)) - According to our policy, we're officially supporting the last three minor releases of Go. - Switch repository license to MIT ([#583](https://github.com/getsentry/sentry-go/pull/583/)) - More about Sentry licensing [here](https://open.sentry.io/licensing/). - Bump `golang.org/x/text` minimum required version to 0.3.8 ([#586](https://github.com/getsentry/sentry-go/pull/586)) - This fixes [CVE-2022-32149](https://github.com/advisories/GHSA-69ch-w2m2-3vjp) vulnerability. ## 0.18.0 The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.18.0. This release contains initial support for [OpenTelemetry](https://opentelemetry.io/) and various other bug fixes and improvements. **Note**: This is the last release supporting Go 1.17. ### Features - Initial support for [OpenTelemetry](https://opentelemetry.io/). You can now send all your OpenTelemetry spans to Sentry. Install the `otel` module ```bash go get github.com/getsentry/sentry-go \ github.com/getsentry/sentry-go/otel ``` Configure the Sentry and OpenTelemetry SDKs ```go import ( "go.opentelemetry.io/otel" sdktrace "go.opentelemetry.io/otel/sdk/trace" "github.com/getsentry/sentry-go" "github.com/getsentry/sentry-go/otel" // ... ) // Initlaize the Sentry SDK sentry.Init(sentry.ClientOptions{ Dsn: "__DSN__", EnableTracing: true, TracesSampleRate: 1.0, }) // Set up the Sentry span processor tp := sdktrace.NewTracerProvider( sdktrace.WithSpanProcessor(sentryotel.NewSentrySpanProcessor()), // ... ) otel.SetTracerProvider(tp) // Set up the Sentry propagator otel.SetTextMapPropagator(sentryotel.NewSentryPropagator()) ``` You can read more about using OpenTelemetry with Sentry in our [docs](https://docs.sentry.io/platforms/go/performance/instrumentation/opentelemetry/). ### Bug Fixes - Do not freeze the Dynamic Sampling Context when no Sentry values are present in the baggage header ([#532](https://github.com/getsentry/sentry-go/pull/532)) - Create a frozen Dynamic Sampling Context when calling `span.ToBaggage()` ([#566](https://github.com/getsentry/sentry-go/pull/566)) - Fix baggage parsing and encoding in vendored otel package ([#568](https://github.com/getsentry/sentry-go/pull/568)) ### Misc - Add `Span.SetDynamicSamplingContext()` ([#539](https://github.com/getsentry/sentry-go/pull/539/)) - Add various getters for `Dsn` ([#540](https://github.com/getsentry/sentry-go/pull/540)) - Add `SpanOption::SpanSampled` ([#546](https://github.com/getsentry/sentry-go/pull/546)) - Add `Span.SetData()` ([#542](https://github.com/getsentry/sentry-go/pull/542)) - Add `Span.IsTransaction()` ([#543](https://github.com/getsentry/sentry-go/pull/543)) - Add `Span.GetTransaction()` method ([#558](https://github.com/getsentry/sentry-go/pull/558)) ## 0.17.0 The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.17.0. This release contains a new `BeforeSendTransaction` hook option and corrects two regressions introduced in `0.16.0`. ### Features - Add `BeforeSendTransaction` hook to `ClientOptions` ([#517](https://github.com/getsentry/sentry-go/pull/517)) - Here's [an example](https://github.com/getsentry/sentry-go/blob/master/_examples/http/main.go#L56-L66) of how BeforeSendTransaction can be used to modify or drop transaction events. ### Bug Fixes - Do not crash in Span.Finish() when the Client is empty [#520](https://github.com/getsentry/sentry-go/pull/520) - Fixes [#518](https://github.com/getsentry/sentry-go/issues/518) - Attach non-PII/non-sensitive request headers to events when `ClientOptions.SendDefaultPii` is set to `false` ([#524](https://github.com/getsentry/sentry-go/pull/524)) - Fixes [#523](https://github.com/getsentry/sentry-go/issues/523) ### Misc - Clarify how to handle logrus.Fatalf events ([#501](https://github.com/getsentry/sentry-go/pull/501/)) - Rename the `examples` directory to `_examples` ([#521](https://github.com/getsentry/sentry-go/pull/521)) - This removes an indirect dependency to `github.com/golang-jwt/jwt` ## 0.16.0 The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.16.0. Due to ongoing work towards a stable API for `v1.0.0`, we sadly had to include **two breaking changes** in this release. ### Breaking Changes - Add `EnableTracing`, a boolean option flag to enable performance monitoring (`false` by default). - If you're using `TracesSampleRate` or `TracesSampler`, this option is **required** to enable performance monitoring. ```go sentry.Init(sentry.ClientOptions{ EnableTracing: true, TracesSampleRate: 1.0, }) ``` - Unify TracesSampler [#498](https://github.com/getsentry/sentry-go/pull/498) - `TracesSampler` was changed to a callback that must return a `float64` between `0.0` and `1.0`. For example, you can apply a sample rate of `1.0` (100%) to all `/api` transactions, and a sample rate of `0.5` (50%) to all other transactions. You can read more about this in our [SDK docs](https://docs.sentry.io/platforms/go/configuration/filtering/#using-sampling-to-filter-transaction-events). ```go sentry.Init(sentry.ClientOptions{ TracesSampler: sentry.TracesSampler(func(ctx sentry.SamplingContext) float64 { hub := sentry.GetHubFromContext(ctx.Span.Context()) name := hub.Scope().Transaction() if strings.HasPrefix(name, "GET /api") { return 1.0 } return 0.5 }), } ``` ### Features - Send errors logged with [Logrus](https://github.com/sirupsen/logrus) to Sentry. - Have a look at our [logrus examples](https://github.com/getsentry/sentry-go/blob/master/_examples/logrus/main.go) on how to use the integration. - Add support for Dynamic Sampling [#491](https://github.com/getsentry/sentry-go/pull/491) - You can read more about Dynamic Sampling in our [product docs](https://docs.sentry.io/product/data-management-settings/dynamic-sampling/). - Add detailed logging about the reason transactions are being dropped. - You can enable SDK logging via `sentry.ClientOptions.Debug: true`. ### Bug Fixes - Do not clone the hub when calling `StartTransaction` [#505](https://github.com/getsentry/sentry-go/pull/505) - Fixes [#502](https://github.com/getsentry/sentry-go/issues/502) ## 0.15.0 - fix: Scope values should not override Event values (#446) - feat: Make maximum amount of spans configurable (#460) - feat: Add a method to start a transaction (#482) - feat: Extend User interface by adding Data, Name and Segment (#483) - feat: Add ClientOptions.SendDefaultPII (#485) ## 0.14.0 - feat: Add function to continue from trace string (#434) - feat: Add `max-depth` options (#428) - *[breaking]* ref: Use a `Context` type mapping to a `map[string]interface{}` for all event contexts (#444) - *[breaking]* ref: Replace deprecated `ioutil` pkg with `os` & `io` (#454) - ref: Optimize `stacktrace.go` from size and speed (#467) - ci: Test against `go1.19` and `go1.18`, drop `go1.16` and `go1.15` support (#432, #477) - deps: Dependency update to fix CVEs (#462, #464, #477) _NOTE:_ This version drops support for Go 1.16 and Go 1.15. The currently supported Go versions are the last 3 stable releases: 1.19, 1.18 and 1.17. ## v0.13.0 - ref: Change DSN ProjectID to be a string (#420) - fix: When extracting PCs from stack frames, try the `PC` field (#393) - build: Bump gin-gonic/gin from v1.4.0 to v1.7.7 (#412) - build: Bump Go version in go.mod (#410) - ci: Bump golangci-lint version in GH workflow (#419) - ci: Update GraphQL config with appropriate permissions (#417) - ci: ci: Add craft release automation (#422) ## v0.12.0 - feat: Automatic Release detection (#363, #369, #386, #400) - fix: Do not change Hub.lastEventID for transactions (#379) - fix: Do not clear LastEventID when events are dropped (#382) - Updates to documentation (#366, #385) _NOTE:_ This version drops support for Go 1.14, however no changes have been made that would make the SDK not work with Go 1.14. The currently supported Go versions are the last 3 stable releases: 1.15, 1.16 and 1.17. There are two behavior changes related to `LastEventID`, both of which were intended to align the behavior of the Sentry Go SDK with other Sentry SDKs. The new [automatic release detection feature](https://github.com/getsentry/sentry-go/issues/335) makes it easier to use Sentry and separate events per release without requiring extra work from users. We intend to improve this functionality in a future release by utilizing information that will be available in runtime starting with Go 1.18. The tracking issue is [#401](https://github.com/getsentry/sentry-go/issues/401). ## v0.11.0 - feat(transports): Category-based Rate Limiting ([#354](https://github.com/getsentry/sentry-go/pull/354)) - feat(transports): Report User-Agent identifying SDK ([#357](https://github.com/getsentry/sentry-go/pull/357)) - fix(scope): Include event processors in clone ([#349](https://github.com/getsentry/sentry-go/pull/349)) - Improvements to `go doc` documentation ([#344](https://github.com/getsentry/sentry-go/pull/344), [#350](https://github.com/getsentry/sentry-go/pull/350), [#351](https://github.com/getsentry/sentry-go/pull/351)) - Miscellaneous changes to our testing infrastructure with GitHub Actions ([57123a40](https://github.com/getsentry/sentry-go/commit/57123a409be55f61b1d5a6da93c176c55a399ad0), [#128](https://github.com/getsentry/sentry-go/pull/128), [#338](https://github.com/getsentry/sentry-go/pull/338), [#345](https://github.com/getsentry/sentry-go/pull/345), [#346](https://github.com/getsentry/sentry-go/pull/346), [#352](https://github.com/getsentry/sentry-go/pull/352), [#353](https://github.com/getsentry/sentry-go/pull/353), [#355](https://github.com/getsentry/sentry-go/pull/355)) _NOTE:_ This version drops support for Go 1.13. The currently supported Go versions are the last 3 stable releases: 1.14, 1.15 and 1.16. Users of the tracing functionality (`StartSpan`, etc) should upgrade to this version to benefit from separate rate limits for errors and transactions. There are no breaking changes and upgrading should be a smooth experience for all users. ## v0.10.0 - feat: Debug connection reuse (#323) - fix: Send root span data as `Event.Extra` (#329) - fix: Do not double sample transactions (#328) - fix: Do not override trace context of transactions (#327) - fix: Drain and close API response bodies (#322) - ci: Run tests against Go tip (#319) - ci: Move away from Travis in favor of GitHub Actions (#314) (#321) ## v0.9.0 - feat: Initial tracing and performance monitoring support (#285) - doc: Revamp sentryhttp documentation (#304) - fix: Hub.PopScope never empties the scope stack (#300) - ref: Report Event.Timestamp in local time (#299) - ref: Report Breadcrumb.Timestamp in local time (#299) _NOTE:_ This version introduces support for [Sentry's Performance Monitoring](https://docs.sentry.io/platforms/go/performance/). The new tracing capabilities are beta, and we plan to expand them on future versions. Feedback is welcome, please open new issues on GitHub. The `sentryhttp` package got better API docs, an [updated usage example](https://github.com/getsentry/sentry-go/tree/master/_examples/http) and support for creating automatic transactions as part of Performance Monitoring. ## v0.8.0 - build: Bump required version of Iris (#296) - fix: avoid unnecessary allocation in Client.processEvent (#293) - doc: Remove deprecation of sentryhttp.HandleFunc (#284) - ref: Update sentryhttp example (#283) - doc: Improve documentation of sentryhttp package (#282) - doc: Clarify SampleRate documentation (#279) - fix: Remove RawStacktrace (#278) - docs: Add example of custom HTTP transport - ci: Test against go1.15, drop go1.12 support (#271) _NOTE:_ This version comes with a few updates. Some examples and documentation have been improved. We've bumped the supported version of the Iris framework to avoid LGPL-licensed modules in the module dependency graph. The `Exception.RawStacktrace` and `Thread.RawStacktrace` fields have been removed to conform to Sentry's ingestion protocol, only `Exception.Stacktrace` and `Thread.Stacktrace` should appear in user code. ## v0.7.0 - feat: Include original error when event cannot be encoded as JSON (#258) - feat: Use Hub from request context when available (#217, #259) - feat: Extract stack frames from golang.org/x/xerrors (#262) - feat: Make Environment Integration preserve existing context data (#261) - feat: Recover and RecoverWithContext with arbitrary types (#268) - feat: Report bad usage of CaptureMessage and CaptureEvent (#269) - feat: Send debug logging to stderr by default (#266) - feat: Several improvements to documentation (#223, #245, #250, #265) - feat: Example of Recover followed by panic (#241, #247) - feat: Add Transactions and Spans (to support OpenTelemetry Sentry Exporter) (#235, #243, #254) - fix: Set either Frame.Filename or Frame.AbsPath (#233) - fix: Clone requestBody to new Scope (#244) - fix: Synchronize access and mutation of Hub.lastEventID (#264) - fix: Avoid repeated syscalls in prepareEvent (#256) - fix: Do not allocate new RNG for every event (#256) - fix: Remove stale replace directive in go.mod (#255) - fix(http): Deprecate HandleFunc, remove duplication (#260) _NOTE:_ This version comes packed with several fixes and improvements and no breaking changes. Notably, there is a change in how the SDK reports file names in stack traces that should resolve any ambiguity when looking at stack traces and using the Suspect Commits feature. We recommend all users to upgrade. ## v0.6.1 - fix: Use NewEvent to init Event struct (#220) _NOTE:_ A change introduced in v0.6.0 with the intent of avoiding allocations made a pattern used in official examples break in certain circumstances (attempting to write to a nil map). This release reverts the change such that maps in the Event struct are always allocated. ## v0.6.0 - feat: Read module dependencies from runtime/debug (#199) - feat: Support chained errors using Unwrap (#206) - feat: Report chain of errors when available (#185) - **[breaking]** fix: Accept http.RoundTripper to customize transport (#205) Before the SDK accepted a concrete value of type `*http.Transport` in `ClientOptions`, now it accepts any value implementing the `http.RoundTripper` interface. Note that `*http.Transport` implements `http.RoundTripper`, so most code bases will continue to work unchanged. Users of custom transport gain the ability to pass in other implementations of `http.RoundTripper` and may be able to simplify their code bases. - fix: Do not panic when scope event processor drops event (#192) - **[breaking]** fix: Use time.Time for timestamps (#191) Users of sentry-go typically do not need to manipulate timestamps manually. For those who do, the field type changed from `int64` to `time.Time`, which should be more convenient to use. The recommended way to get the current time is `time.Now().UTC()`. - fix: Report usage error including stack trace (#189) - feat: Add Exception.ThreadID field (#183) - ci: Test against Go 1.14, drop 1.11 (#170) - feat: Limit reading bytes from request bodies (#168) - **[breaking]** fix: Rename fasthttp integration package sentryhttp => sentryfasthttp The current recommendation is to use a named import, in which case existing code should not require any change: ```go package main import ( "fmt" "github.com/getsentry/sentry-go" sentryfasthttp "github.com/getsentry/sentry-go/fasthttp" "github.com/valyala/fasthttp" ) ``` _NOTE:_ This version includes some new features and a few breaking changes, none of which should pose troubles with upgrading. Most code bases should be able to upgrade without any changes. ## v0.5.1 - fix: Ignore err.Cause() when it is nil (#160) ## v0.5.0 - fix: Synchronize access to HTTPTransport.disabledUntil (#158) - docs: Update Flush documentation (#153) - fix: HTTPTransport.Flush panic and data race (#140) _NOTE:_ This version changes the implementation of the default transport, modifying the behavior of `sentry.Flush`. The previous behavior was to wait until there were no buffered events; new concurrent events kept `Flush` from returning. The new behavior is to wait until the last event prior to the call to `Flush` has been sent or the timeout; new concurrent events have no effect. The new behavior is inline with the [Unified API Guidelines](https://docs.sentry.io/development/sdk-dev/unified-api/). We have updated the documentation and examples to clarify that `Flush` is meant to be called typically only once before program termination, to wait for in-flight events to be sent to Sentry. Calling `Flush` after every event is not recommended, as it introduces unnecessary latency to the surrounding function. Please verify the usage of `sentry.Flush` in your code base. ## v0.4.0 - fix(stacktrace): Correctly report package names (#127) - fix(stacktrace): Do not rely on AbsPath of files (#123) - build: Require github.com/ugorji/go@v1.1.7 (#110) - fix: Correctly store last event id (#99) - fix: Include request body in event payload (#94) - build: Reset go.mod version to 1.11 (#109) - fix: Eliminate data race in modules integration (#105) - feat: Add support for path prefixes in the DSN (#102) - feat: Add HTTPClient option (#86) - feat: Extract correct type and value from top-most error (#85) - feat: Check for broken pipe errors in Gin integration (#82) - fix: Client.CaptureMessage accept nil EventModifier (#72) ## v0.3.1 - feat: Send extra information exposed by the Go runtime (#76) - fix: Handle new lines in module integration (#65) - fix: Make sure that cache is locked when updating for contextifyFramesIntegration - ref: Update Iris integration and example to version 12 - misc: Remove indirect dependencies in order to move them to separate go.mod files ## v0.3.0 - feat: Retry event marshaling without contextual data if the first pass fails - fix: Include `url.Parse` error in `DsnParseError` - fix: Make more `Scope` methods safe for concurrency - fix: Synchronize concurrent access to `Hub.client` - ref: Remove mutex from `Scope` exported API - ref: Remove mutex from `Hub` exported API - ref: Compile regexps for `filterFrames` only once - ref: Change `SampleRate` type to `float64` - doc: `Scope.Clear` not safe for concurrent use - ci: Test sentry-go with `go1.13`, drop `go1.10` _NOTE:_ This version removes some of the internal APIs that landed publicly (namely `Hub/Scope` mutex structs) and may require (but shouldn't) some changes to your code. It's not done through major version update, as we are still in `0.x` stage. ## v0.2.1 - fix: Run `Contextify` integration on `Threads` as well ## v0.2.0 - feat: Add `SetTransaction()` method on the `Scope` - feat: `fasthttp` framework support with `sentryfasthttp` package - fix: Add `RWMutex` locks to internal `Hub` and `Scope` changes ## v0.1.3 - feat: Move frames context reading into `contextifyFramesIntegration` (#28) _NOTE:_ In case of any performance issues due to source contexts IO, you can let us know and turn off the integration in the meantime with: ```go sentry.Init(sentry.ClientOptions{ Integrations: func(integrations []sentry.Integration) []sentry.Integration { var filteredIntegrations []sentry.Integration for _, integration := range integrations { if integration.Name() == "ContextifyFrames" { continue } filteredIntegrations = append(filteredIntegrations, integration) } return filteredIntegrations }, }) ``` ## v0.1.2 - feat: Better source code location resolution and more useful inapp frames (#26) - feat: Use `noopTransport` when no `Dsn` provided (#27) - ref: Allow empty `Dsn` instead of returning an error (#22) - fix: Use `NewScope` instead of literal struct inside a `scope.Clear` call (#24) - fix: Add to `WaitGroup` before the request is put inside a buffer (#25) ## v0.1.1 - fix: Check for initialized `Client` in `AddBreadcrumbs` (#20) - build: Bump version when releasing with Craft (#19) ## v0.1.0 - First stable release! \o/ ## v0.0.1-beta.5 - feat: **[breaking]** Add `NewHTTPTransport` and `NewHTTPSyncTransport` which accepts all transport options - feat: New `HTTPSyncTransport` that blocks after each call - feat: New `Echo` integration - ref: **[breaking]** Remove `BufferSize` option from `ClientOptions` and move it to `HTTPTransport` instead - ref: Export default `HTTPTransport` - ref: Export `net/http` integration handler - ref: Set `Request` instantly in the package handlers, not in `recoverWithSentry` so it can be accessed later on - ci: Add craft config ## v0.0.1-beta.4 - feat: `IgnoreErrors` client option and corresponding integration - ref: Reworked `net/http` integration, wrote better example and complete readme - ref: Reworked `Gin` integration, wrote better example and complete readme - ref: Reworked `Iris` integration, wrote better example and complete readme - ref: Reworked `Negroni` integration, wrote better example and complete readme - ref: Reworked `Martini` integration, wrote better example and complete readme - ref: Remove `Handle()` from frameworks handlers and return it directly from New ## v0.0.1-beta.3 - feat: `Iris` framework support with `sentryiris` package - feat: `Gin` framework support with `sentrygin` package - feat: `Martini` framework support with `sentrymartini` package - feat: `Negroni` framework support with `sentrynegroni` package - feat: Add `Hub.Clone()` for easier frameworks integration - feat: Return `EventID` from `Recovery` methods - feat: Add `NewScope` and `NewEvent` functions and use them in the whole codebase - feat: Add `AddEventProcessor` to the `Client` - fix: Operate on requests body copy instead of the original - ref: Try to read source files from the root directory, based on the filename as well, to make it work on AWS Lambda - ref: Remove `gocertifi` dependence and document how to provide your own certificates - ref: **[breaking]** Remove `Decorate` and `DecorateFunc` methods in favor of `sentryhttp` package - ref: **[breaking]** Allow for integrations to live on the client, by passing client instance in `SetupOnce` method - ref: **[breaking]** Remove `GetIntegration` from the `Hub` - ref: **[breaking]** Remove `GlobalEventProcessors` getter from the public API ## v0.0.1-beta.2 - feat: Add `AttachStacktrace` client option to include stacktrace for messages - feat: Add `BufferSize` client option to configure transport buffer size - feat: Add `SetRequest` method on a `Scope` to control `Request` context data - feat: Add `FromHTTPRequest` for `Request` type for easier extraction - ref: Extract `Request` information more accurately - fix: Attach `ServerName`, `Release`, `Dist`, `Environment` options to the event - fix: Don't log events dropped due to full transport buffer as sent - fix: Don't panic and create an appropriate event when called `CaptureException` or `Recover` with `nil` value ## v0.0.1-beta - Initial release ================================================ FILE: vendor/github.com/getsentry/sentry-go/CONTRIBUTING.md ================================================ # Contributing to sentry-go Hey, thank you if you're reading this, we welcome your contribution! ## Sending a Pull Request Please help us save time when reviewing your PR by following this simple process: 1. Is your PR a simple typo fix? Read no further, **click that green "Create pull request" button**! 2. For more complex PRs that involve behavior changes or new APIs, please consider [opening an **issue**][new-issue] describing the problem you're trying to solve if there's not one already. A PR is often one specific solution to a problem and sometimes talking about the problem unfolds new possible solutions. Remember we will be responsible for maintaining the changes later. 3. Fixing a bug and changing a behavior? Please add automated tests to prevent future regression. 4. Practice writing good commit messages. We have [commit guidelines][commit-guide]. 5. We have [guidelines for PR submitters][pr-guide]. A short summary: - Good PR descriptions are very helpful and most of the time they include **why** something is done and why done in this particular way. Also list other possible solutions that were considered and discarded. - Be your own first reviewer. Make sure your code compiles and passes the existing tests. [new-issue]: https://github.com/getsentry/sentry-go/issues/new/choose [commit-guide]: https://develop.sentry.dev/code-review/#commit-guidelines [pr-guide]: https://develop.sentry.dev/code-review/#guidelines-for-submitters Please also read through our [SDK Development docs](https://develop.sentry.dev/sdk/). It contains information about SDK features, expected payloads and best practices for contributing to Sentry SDKs. ## Community The public-facing channels for support and development of Sentry SDKs can be found on [Discord](https://discord.gg/Ww9hbqr). ## Testing ```console $ go test ``` ### Watch mode Use: https://github.com/cespare/reflex ```console $ reflex -g '*.go' -d "none" -- sh -c 'printf "\n"; go test' ``` ### With data race detection ```console $ go test -race ``` ### Coverage ```console $ go test -race -coverprofile=coverage.txt -covermode=atomic && go tool cover -html coverage.txt ``` ## Linting Lint with [`golangci-lint`](https://github.com/golangci/golangci-lint): ```console $ golangci-lint run ``` ## Release 1. Update `CHANGELOG.md` with new version in `vX.X.X` format title and list of changes. The command below can be used to get a list of changes since the last tag, with the format used in `CHANGELOG.md`: ```console $ git log --no-merges --format=%s $(git describe --abbrev=0).. | sed 's/^/- /' ``` 2. Commit with `misc: vX.X.X changelog` commit message and push to `master`. 3. Let [`craft`](https://github.com/getsentry/craft) do the rest: ```console $ craft prepare X.X.X $ craft publish X.X.X ``` ================================================ FILE: vendor/github.com/getsentry/sentry-go/LICENSE ================================================ MIT License Copyright (c) 2019 Functional Software, Inc. dba Sentry Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: vendor/github.com/getsentry/sentry-go/MIGRATION.md ================================================ # `raven-go` to `sentry-go` Migration Guide A [`raven-go` to `sentry-go` migration guide](https://docs.sentry.io/platforms/go/migration/) is available at the official Sentry documentation site. ================================================ FILE: vendor/github.com/getsentry/sentry-go/Makefile ================================================ .DEFAULT_GOAL := help MKFILE_PATH := $(abspath $(lastword $(MAKEFILE_LIST))) MKFILE_DIR := $(dir $(MKFILE_PATH)) ALL_GO_MOD_DIRS := $(shell find . -type f -name 'go.mod' -exec dirname {} \; | sort) GO = go TIMEOUT = 300 # Parse Makefile and display the help help: ## Show help @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' .PHONY: help build: ## Build everything for dir in $(ALL_GO_MOD_DIRS); do \ cd "$${dir}"; \ echo ">>> Running 'go build' for module: $${dir}"; \ go build ./...; \ done; .PHONY: build ### Tests (inspired by https://github.com/open-telemetry/opentelemetry-go/blob/main/Makefile) TEST_TARGETS := test-short test-verbose test-race test-race: ARGS=-race test-short: ARGS=-short test-verbose: ARGS=-v -race $(TEST_TARGETS): test test: $(ALL_GO_MOD_DIRS:%=test/%) ## Run tests test/%: DIR=$* test/%: @echo ">>> Running tests for module: $(DIR)" @# We use '-count=1' to disable test caching. (cd $(DIR) && $(GO) test -count=1 -timeout $(TIMEOUT)s $(ARGS) ./...) .PHONY: $(TEST_TARGETS) test # Coverage COVERAGE_MODE = atomic COVERAGE_PROFILE = coverage.out COVERAGE_REPORT_DIR = .coverage COVERAGE_REPORT_DIR_ABS = "$(MKFILE_DIR)/$(COVERAGE_REPORT_DIR)" $(COVERAGE_REPORT_DIR): mkdir -p $(COVERAGE_REPORT_DIR) clean-report-dir: $(COVERAGE_REPORT_DIR) test $(COVERAGE_REPORT_DIR) && rm -f $(COVERAGE_REPORT_DIR)/* test-coverage: $(COVERAGE_REPORT_DIR) clean-report-dir ## Test with coverage enabled set -e ; \ for dir in $(ALL_GO_MOD_DIRS); do \ echo ">>> Running tests with coverage for module: $${dir}"; \ DIR_ABS=$$(python -c 'import os, sys; print(os.path.realpath(sys.argv[1]))' $${dir}) ; \ REPORT_NAME=$$(basename $${DIR_ABS}); \ (cd "$${dir}" && \ $(GO) test -count=1 -timeout $(TIMEOUT)s -coverpkg=./... -covermode=$(COVERAGE_MODE) -coverprofile="$(COVERAGE_PROFILE)" ./... && \ cp $(COVERAGE_PROFILE) "$(COVERAGE_REPORT_DIR_ABS)/$${REPORT_NAME}_$(COVERAGE_PROFILE)" && \ $(GO) tool cover -html=$(COVERAGE_PROFILE) -o coverage.html); \ done; .PHONY: test-coverage clean-report-dir test-race-coverage: $(COVERAGE_REPORT_DIR) clean-report-dir ## Run tests with race detection and coverage set -e ; \ for dir in $(ALL_GO_MOD_DIRS); do \ echo ">>> Running tests with race detection and coverage for module: $${dir}"; \ DIR_ABS=$$(python -c 'import os, sys; print(os.path.realpath(sys.argv[1]))' $${dir}) ; \ REPORT_NAME=$$(basename $${DIR_ABS}); \ (cd "$${dir}" && \ $(GO) test -count=1 -timeout $(TIMEOUT)s -race -coverpkg=./... -covermode=$(COVERAGE_MODE) -coverprofile="$(COVERAGE_PROFILE)" ./... && \ cp $(COVERAGE_PROFILE) "$(COVERAGE_REPORT_DIR_ABS)/$${REPORT_NAME}_$(COVERAGE_PROFILE)" && \ $(GO) tool cover -html=$(COVERAGE_PROFILE) -o coverage.html); \ done; .PHONY: test-race-coverage mod-tidy: ## Check go.mod tidiness set -e ; \ for dir in $(ALL_GO_MOD_DIRS); do \ echo ">>> Running 'go mod tidy' for module: $${dir}"; \ (cd "$${dir}" && GOTOOLCHAIN=local go mod tidy -go=1.24.0 -compat=1.24.0); \ done; \ git diff --exit-code; .PHONY: mod-tidy vet: ## Run "go vet" set -e ; \ for dir in $(ALL_GO_MOD_DIRS); do \ echo ">>> Running 'go vet' for module: $${dir}"; \ (cd "$${dir}" && go vet ./...); \ done; .PHONY: vet lint: ## Lint (using "golangci-lint") golangci-lint run .PHONY: lint fmt: ## Format all Go files gofmt -l -w -s . .PHONY: fmt ================================================ FILE: vendor/github.com/getsentry/sentry-go/README.md ================================================

Sentry

# Official Sentry SDK for Go [![Build Status](https://github.com/getsentry/sentry-go/actions/workflows/test.yml/badge.svg)](https://github.com/getsentry/sentry-go/actions/workflows/test.yml) [![Go Report Card](https://goreportcard.com/badge/github.com/getsentry/sentry-go)](https://goreportcard.com/report/github.com/getsentry/sentry-go) [![Discord](https://img.shields.io/discord/621778831602221064)](https://discord.gg/Ww9hbqr) [![X Follow](https://img.shields.io/twitter/follow/sentry?label=sentry&style=social)](https://x.com/intent/follow?screen_name=sentry) [![go.dev](https://img.shields.io/badge/go.dev-pkg-007d9c.svg?style=flat)](https://pkg.go.dev/github.com/getsentry/sentry-go) `sentry-go` provides a Sentry client implementation for the Go programming language. This is the next generation of the Go SDK for [Sentry](https://sentry.io/), intended to replace the `raven-go` package. > Looking for the old `raven-go` SDK documentation? See the Legacy client section [here](https://docs.sentry.io/clients/go/). > If you want to start using `sentry-go` instead, check out the [migration guide](https://docs.sentry.io/platforms/go/migration/). ## Requirements The only requirement is a Go compiler. We verify this package against the 3 most recent releases of Go. Those are the supported versions. The exact versions are defined in [`GitHub workflow`](.github/workflows/test.yml). In addition, we run tests against the current master branch of the Go toolchain, though support for this configuration is best-effort. ## Installation `sentry-go` can be installed like any other Go library through `go get`: ```console $ go get github.com/getsentry/sentry-go@latest ``` Check out the [list of released versions](https://github.com/getsentry/sentry-go/releases). ## Configuration To use `sentry-go`, you’ll need to import the `sentry-go` package and initialize it with your DSN and other [options](https://pkg.go.dev/github.com/getsentry/sentry-go#ClientOptions). If not specified in the SDK initialization, the [DSN](https://docs.sentry.io/product/sentry-basics/dsn-explainer/), [Release](https://docs.sentry.io/product/releases/) and [Environment](https://docs.sentry.io/product/sentry-basics/environments/) are read from the environment variables `SENTRY_DSN`, `SENTRY_RELEASE` and `SENTRY_ENVIRONMENT`, respectively. More on this in the [Configuration section of the official Sentry Go SDK documentation](https://docs.sentry.io/platforms/go/configuration/). ## Usage The SDK supports reporting errors and tracking application performance. To get started, have a look at one of our [examples](_examples/): - [Basic error instrumentation](_examples/basic/main.go) - [Error and tracing for HTTP servers](_examples/http/main.go) We also provide a [complete API reference](https://pkg.go.dev/github.com/getsentry/sentry-go). For more detailed information about how to get the most out of `sentry-go`, check out the official documentation: - [Sentry Go SDK documentation](https://docs.sentry.io/platforms/go/) - Guides: - [net/http](https://docs.sentry.io/platforms/go/guides/http/) - [echo](https://docs.sentry.io/platforms/go/guides/echo/) - [fasthttp](https://docs.sentry.io/platforms/go/guides/fasthttp/) - [fiber](https://docs.sentry.io/platforms/go/guides/fiber/) - [gin](https://docs.sentry.io/platforms/go/guides/gin/) - [iris](https://docs.sentry.io/platforms/go/guides/iris/) - [logrus](https://docs.sentry.io/platforms/go/guides/logrus/) - [negroni](https://docs.sentry.io/platforms/go/guides/negroni/) - [slog](https://docs.sentry.io/platforms/go/guides/slog/) - [zerolog](https://docs.sentry.io/platforms/go/guides/zerolog/) ## Resources - [Bug Tracker](https://github.com/getsentry/sentry-go/issues) - [GitHub Project](https://github.com/getsentry/sentry-go) - [![go.dev](https://img.shields.io/badge/go.dev-pkg-007d9c.svg?style=flat)](https://pkg.go.dev/github.com/getsentry/sentry-go) - [![Documentation](https://img.shields.io/badge/documentation-sentry.io-green.svg)](https://docs.sentry.io/platforms/go/) - [![Discussions](https://img.shields.io/github/discussions/getsentry/sentry-go.svg)](https://github.com/getsentry/sentry-go/discussions) - [![Discord](https://img.shields.io/discord/621778831602221064)](https://discord.gg/Ww9hbqr) - [![Stack Overflow](https://img.shields.io/badge/stack%20overflow-sentry-green.svg)](http://stackoverflow.com/questions/tagged/sentry) - [![Twitter Follow](https://img.shields.io/twitter/follow/getsentry?label=getsentry&style=social)](https://twitter.com/intent/follow?screen_name=getsentry) ## License Licensed under [The MIT License](https://opensource.org/licenses/mit/), see [`LICENSE`](LICENSE). ## Community Join Sentry's [`#go` channel on Discord](https://discord.gg/Ww9hbqr) to get involved and help us improve the SDK! ================================================ FILE: vendor/github.com/getsentry/sentry-go/attribute/builder.go ================================================ package attribute type Builder struct { Key string Value Value } // String returns a Builder for a string value. func String(key, value string) Builder { return Builder{key, StringValue(value)} } // Int64 returns a Builder for an int64. func Int64(key string, value int64) Builder { return Builder{key, Int64Value(value)} } // Int returns a Builder for an int64. func Int(key string, value int) Builder { return Builder{key, IntValue(value)} } // Float64 returns a Builder for a float64. func Float64(key string, v float64) Builder { return Builder{key, Float64Value(v)} } // Bool returns a Builder for a boolean. func Bool(key string, v bool) Builder { return Builder{key, BoolValue(v)} } // Valid checks for valid key and type. func (b *Builder) Valid() bool { return len(b.Key) > 0 && b.Value.Type() != INVALID } ================================================ FILE: vendor/github.com/getsentry/sentry-go/attribute/rawhelpers.go ================================================ // Copied from https://github.com/open-telemetry/opentelemetry-go/blob/cc43e01c27892252aac9a8f20da28cdde957a289/attribute/rawhelpers.go // Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package attribute import ( "math" ) func boolToRaw(b bool) uint64 { // b is not a control flag. if b { return 1 } return 0 } func rawToBool(r uint64) bool { return r != 0 } func int64ToRaw(i int64) uint64 { // Assumes original was a valid int64 (overflow not checked). return uint64(i) // nolint: gosec } func rawToInt64(r uint64) int64 { // Assumes original was a valid int64 (overflow not checked). return int64(r) // nolint: gosec } func float64ToRaw(f float64) uint64 { return math.Float64bits(f) } func rawToFloat64(r uint64) float64 { return math.Float64frombits(r) } ================================================ FILE: vendor/github.com/getsentry/sentry-go/attribute/value.go ================================================ // Adapted from https://github.com/open-telemetry/opentelemetry-go/blob/cc43e01c27892252aac9a8f20da28cdde957a289/attribute/value.go // // Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package attribute import ( "encoding/json" "fmt" "strconv" ) // Type describes the type of the data Value holds. type Type int // redefines builtin Type. // Value represents the value part in key-value pairs. type Value struct { vtype Type numeric uint64 stringly string } const ( // INVALID is used for a Value with no value set. INVALID Type = iota // BOOL is a boolean Type Value. BOOL // INT64 is a 64-bit signed integral Type Value. INT64 // FLOAT64 is a 64-bit floating point Type Value. FLOAT64 // STRING is a string Type Value. STRING // UINT64 is a 64-bit unsigned integral Type Value. // // This type is intentionally not exposed through the Builder API. UINT64 ) // BoolValue creates a BOOL Value. func BoolValue(v bool) Value { return Value{ vtype: BOOL, numeric: boolToRaw(v), } } // IntValue creates an INT64 Value. func IntValue(v int) Value { return Int64Value(int64(v)) } // Int64Value creates an INT64 Value. func Int64Value(v int64) Value { return Value{ vtype: INT64, numeric: int64ToRaw(v), } } // Float64Value creates a FLOAT64 Value. func Float64Value(v float64) Value { return Value{ vtype: FLOAT64, numeric: float64ToRaw(v), } } // StringValue creates a STRING Value. func StringValue(v string) Value { return Value{ vtype: STRING, stringly: v, } } // Uint64Value creates a UINT64 Value. // // This constructor is intentionally not exposed through the Builder API. func Uint64Value(v uint64) Value { return Value{ vtype: UINT64, numeric: v, } } // Type returns a type of the Value. func (v Value) Type() Type { return v.vtype } // AsBool returns the bool value. Make sure that the Value's type is // BOOL. func (v Value) AsBool() bool { return rawToBool(v.numeric) } // AsInt64 returns the int64 value. Make sure that the Value's type is // INT64. func (v Value) AsInt64() int64 { return rawToInt64(v.numeric) } // AsFloat64 returns the float64 value. Make sure that the Value's // type is FLOAT64. func (v Value) AsFloat64() float64 { return rawToFloat64(v.numeric) } // AsString returns the string value. Make sure that the Value's type // is STRING. func (v Value) AsString() string { return v.stringly } // AsUint64 returns the uint64 value. Make sure that the Value's type is // UINT64. func (v Value) AsUint64() uint64 { return v.numeric } type unknownValueType struct{} // AsInterface returns Value's data as interface{}. func (v Value) AsInterface() interface{} { switch v.Type() { case BOOL: return v.AsBool() case INT64: return v.AsInt64() case FLOAT64: return v.AsFloat64() case STRING: return v.stringly case UINT64: return v.numeric } return unknownValueType{} } // String returns a string representation of Value's data. func (v Value) String() string { switch v.Type() { case BOOL: return strconv.FormatBool(v.AsBool()) case INT64: return strconv.FormatInt(v.AsInt64(), 10) case FLOAT64: return fmt.Sprint(v.AsFloat64()) case STRING: return v.stringly case UINT64: return strconv.FormatUint(v.numeric, 10) default: return "unknown" } } // MarshalJSON returns the JSON encoding of the Value. func (v Value) MarshalJSON() ([]byte, error) { var jsonVal struct { Value any `json:"value"` Type string `json:"type"` } jsonVal.Type = mapTypesToStr[v.Type()] jsonVal.Value = v.AsInterface() return json.Marshal(jsonVal) } func (t Type) String() string { switch t { case BOOL: return "bool" case INT64: return "int64" case FLOAT64: return "float64" case STRING: return "string" case UINT64: return "uint64" } return "invalid" } // mapTypesToStr is a map from attribute.Type to the primitive types the server understands. // https://develop.sentry.dev/sdk/foundations/data-model/attributes/#primitive-types var mapTypesToStr = map[Type]string{ INVALID: "", BOOL: "boolean", INT64: "integer", FLOAT64: "double", STRING: "string", UINT64: "integer", // wire format: same "integer" type } ================================================ FILE: vendor/github.com/getsentry/sentry-go/batch_processor.go ================================================ package sentry import ( "context" "sync" "time" ) const ( batchSize = 100 defaultBatchTimeout = 5 * time.Second ) type batchProcessor[T any] struct { sendBatch func([]T) itemCh chan T flushCh chan chan struct{} cancel context.CancelFunc wg sync.WaitGroup startOnce sync.Once shutdownOnce sync.Once batchTimeout time.Duration } func newBatchProcessor[T any](sendBatch func([]T)) *batchProcessor[T] { return &batchProcessor[T]{ itemCh: make(chan T, batchSize), flushCh: make(chan chan struct{}), sendBatch: sendBatch, batchTimeout: defaultBatchTimeout, } } // WithBatchTimeout sets a custom batch timeout for the processor. // This is useful for testing or when different timing behavior is needed. func (p *batchProcessor[T]) WithBatchTimeout(timeout time.Duration) *batchProcessor[T] { p.batchTimeout = timeout return p } func (p *batchProcessor[T]) Send(item T) bool { select { case p.itemCh <- item: return true default: return false } } func (p *batchProcessor[T]) Start() { p.startOnce.Do(func() { ctx, cancel := context.WithCancel(context.Background()) p.cancel = cancel p.wg.Add(1) go p.run(ctx) }) } func (p *batchProcessor[T]) Flush(timeout <-chan struct{}) { done := make(chan struct{}) select { case p.flushCh <- done: select { case <-done: case <-timeout: } case <-timeout: } } func (p *batchProcessor[T]) Shutdown() { p.shutdownOnce.Do(func() { if p.cancel != nil { p.cancel() p.wg.Wait() } }) } func (p *batchProcessor[T]) run(ctx context.Context) { defer p.wg.Done() var items []T timer := time.NewTimer(0) timer.Stop() defer timer.Stop() for { select { case item := <-p.itemCh: if len(items) == 0 { timer.Reset(p.batchTimeout) } items = append(items, item) if len(items) >= batchSize { p.sendBatch(items) items = nil } case <-timer.C: if len(items) > 0 { p.sendBatch(items) items = nil } case done := <-p.flushCh: flushDrain: for { select { case item := <-p.itemCh: items = append(items, item) default: break flushDrain } } if len(items) > 0 { p.sendBatch(items) items = nil } close(done) case <-ctx.Done(): drain: for { select { case item := <-p.itemCh: items = append(items, item) default: break drain } } if len(items) > 0 { p.sendBatch(items) } return } } } ================================================ FILE: vendor/github.com/getsentry/sentry-go/check_in.go ================================================ package sentry import "time" type CheckInStatus string const ( CheckInStatusInProgress CheckInStatus = "in_progress" CheckInStatusOK CheckInStatus = "ok" CheckInStatusError CheckInStatus = "error" ) type checkInScheduleType string const ( checkInScheduleTypeCrontab checkInScheduleType = "crontab" checkInScheduleTypeInterval checkInScheduleType = "interval" ) type MonitorSchedule interface { // scheduleType is a private method that must be implemented for monitor schedule // implementation. It should never be called. This method is made for having // specific private implementation of MonitorSchedule interface. scheduleType() checkInScheduleType } type crontabSchedule struct { Type string `json:"type"` Value string `json:"value"` } func (c crontabSchedule) scheduleType() checkInScheduleType { return checkInScheduleTypeCrontab } // CrontabSchedule defines the MonitorSchedule with a cron format. // Example: "8 * * * *". func CrontabSchedule(scheduleString string) MonitorSchedule { return crontabSchedule{ Type: string(checkInScheduleTypeCrontab), Value: scheduleString, } } type intervalSchedule struct { Type string `json:"type"` Value int64 `json:"value"` Unit string `json:"unit"` } func (i intervalSchedule) scheduleType() checkInScheduleType { return checkInScheduleTypeInterval } type MonitorScheduleUnit string const ( MonitorScheduleUnitMinute MonitorScheduleUnit = "minute" MonitorScheduleUnitHour MonitorScheduleUnit = "hour" MonitorScheduleUnitDay MonitorScheduleUnit = "day" MonitorScheduleUnitWeek MonitorScheduleUnit = "week" MonitorScheduleUnitMonth MonitorScheduleUnit = "month" MonitorScheduleUnitYear MonitorScheduleUnit = "year" ) // IntervalSchedule defines the MonitorSchedule with an interval format. // // Example: // // IntervalSchedule(1, sentry.MonitorScheduleUnitDay) func IntervalSchedule(value int64, unit MonitorScheduleUnit) MonitorSchedule { return intervalSchedule{ Type: string(checkInScheduleTypeInterval), Value: value, Unit: string(unit), } } type MonitorConfig struct { //nolint: maligned // prefer readability over optimal memory layout Schedule MonitorSchedule `json:"schedule,omitempty"` // The allowed margin of minutes after the expected check-in time that // the monitor will not be considered missed for. CheckInMargin int64 `json:"checkin_margin,omitempty"` // The allowed duration in minutes that the monitor may be `in_progress` // for before being considered failed due to timeout. MaxRuntime int64 `json:"max_runtime,omitempty"` // A tz database string representing the timezone which the monitor's execution schedule is in. // See: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones Timezone string `json:"timezone,omitempty"` // The number of consecutive failed check-ins it takes before an issue is created. FailureIssueThreshold int64 `json:"failure_issue_threshold,omitempty"` // The number of consecutive OK check-ins it takes before an issue is resolved. RecoveryThreshold int64 `json:"recovery_threshold,omitempty"` } type CheckIn struct { //nolint: maligned // prefer readability over optimal memory layout // Check-In ID (unique and client generated) ID EventID `json:"check_in_id"` // The distinct slug of the monitor. MonitorSlug string `json:"monitor_slug"` // The status of the check-in. Status CheckInStatus `json:"status"` // The duration of the check-in. Will only take effect if the status is ok or error. Duration time.Duration `json:"duration,omitempty"` } // serializedCheckIn is used by checkInMarshalJSON method on Event struct. // See https://develop.sentry.dev/sdk/check-ins/ type serializedCheckIn struct { //nolint: maligned // Check-In ID (unique and client generated). CheckInID string `json:"check_in_id"` // The distinct slug of the monitor. MonitorSlug string `json:"monitor_slug"` // The status of the check-in. Status CheckInStatus `json:"status"` // The duration of the check-in in seconds. Will only take effect if the status is ok or error. Duration float64 `json:"duration,omitempty"` Release string `json:"release,omitempty"` Environment string `json:"environment,omitempty"` MonitorConfig *MonitorConfig `json:"monitor_config,omitempty"` } ================================================ FILE: vendor/github.com/getsentry/sentry-go/client.go ================================================ package sentry import ( "context" "crypto/x509" "fmt" "io" "math/rand" "net/http" "os" "sort" "strings" "sync" "time" "github.com/getsentry/sentry-go/internal/debug" "github.com/getsentry/sentry-go/internal/debuglog" httpInternal "github.com/getsentry/sentry-go/internal/http" "github.com/getsentry/sentry-go/internal/protocol" "github.com/getsentry/sentry-go/internal/ratelimit" "github.com/getsentry/sentry-go/internal/telemetry" ) // The identifier of the SDK. const sdkIdentifier = "sentry.go" const ( // maxErrorDepth is the maximum number of errors reported in a chain of errors. // This protects the SDK from an arbitrarily long chain of wrapped errors. // // An additional consideration is that arguably reporting a long chain of errors // is of little use when debugging production errors with Sentry. The Sentry UI // is not optimized for long chains either. The top-level error together with a // stack trace is often the most useful information. maxErrorDepth = 100 // defaultMaxSpans limits the default number of recorded spans per transaction. The limit is // meant to bound memory usage and prevent too large transaction events that // would be rejected by Sentry. defaultMaxSpans = 1000 // defaultMaxBreadcrumbs is the default maximum number of breadcrumbs added to // an event. Can be overwritten with the MaxBreadcrumbs option. defaultMaxBreadcrumbs = 100 ) // hostname is the host name reported by the kernel. It is precomputed once to // avoid syscalls when capturing events. // // The error is ignored because retrieving the host name is best-effort. If the // error is non-nil, there is nothing to do other than retrying. We choose not // to retry for now. var hostname, _ = os.Hostname() // lockedRand is a random number generator safe for concurrent use. Its API is // intentionally limited and it is not meant as a full replacement for a // rand.Rand. type lockedRand struct { mu sync.Mutex r *rand.Rand } // Float64 returns a pseudo-random number in [0.0,1.0). func (r *lockedRand) Float64() float64 { r.mu.Lock() defer r.mu.Unlock() return r.r.Float64() } // rng is the internal random number generator. // // We do not use the global functions from math/rand because, while they are // safe for concurrent use, any package in a build could change the seed and // affect the generated numbers, for instance making them deterministic. On the // other hand, the source returned from rand.NewSource is not safe for // concurrent use, so we need to couple its use with a sync.Mutex. var rng = &lockedRand{ // #nosec G404 -- We are fine using transparent, non-secure value here. r: rand.New(rand.NewSource(time.Now().UnixNano())), } // usageError is used to report to Sentry an SDK usage error. // // It is not exported because it is never returned by any function or method in // the exported API. type usageError struct { error } // DebugLogger is an instance of log.Logger that is used to provide debug information about running Sentry Client // can be enabled by either using debuglog.SetOutput directly or with Debug client option. var DebugLogger = debuglog.GetLogger() // EventProcessor is a function that processes an event. // Event processors are used to change an event before it is sent to Sentry. type EventProcessor func(event *Event, hint *EventHint) *Event // EventModifier is the interface that wraps the ApplyToEvent method. // // ApplyToEvent changes an event based on external data and/or // an event hint. type EventModifier interface { ApplyToEvent(event *Event, hint *EventHint, client *Client) *Event } var globalEventProcessors []EventProcessor // AddGlobalEventProcessor adds processor to the global list of event // processors. Global event processors apply to all events. // // AddGlobalEventProcessor is deprecated. Most users will prefer to initialize // the SDK with Init and provide a ClientOptions.BeforeSend function or use // Scope.AddEventProcessor instead. func AddGlobalEventProcessor(processor EventProcessor) { globalEventProcessors = append(globalEventProcessors, processor) } // Integration allows for registering a functions that modify or discard captured events. type Integration interface { Name() string SetupOnce(client *Client) } // ClientOptions that configures a SDK Client. type ClientOptions struct { // The DSN to use. If the DSN is not set, the client is effectively // disabled. Dsn string // In debug mode, the debug information is printed to stdout to help you // understand what sentry is doing. Debug bool // Configures whether SDK should generate and attach stacktraces to pure // capture message calls. AttachStacktrace bool // The sample rate for event submission in the range [0.0, 1.0]. By default, // all events are sent. Thus, as a historical special case, the sample rate // 0.0 is treated as if it was 1.0. To drop all events, set the DSN to the // empty string. SampleRate float64 // Enable performance tracing. EnableTracing bool // The sample rate for sampling traces in the range [0.0, 1.0]. TracesSampleRate float64 // Used to customize the sampling of traces, overrides TracesSampleRate. TracesSampler TracesSampler // Control with URLs trace propagation should be enabled. Does not support regex patterns. TracePropagationTargets []string // PropagateTraceparent is used to control whether the W3C Trace Context HTTP traceparent header // is propagated on outgoing http requests. PropagateTraceparent bool // List of regexp strings that will be used to match against event's message // and if applicable, caught errors type and value. // If the match is found, then a whole event will be dropped. IgnoreErrors []string // List of regexp strings that will be used to match against a transaction's // name. If a match is found, then the transaction will be dropped. IgnoreTransactions []string // If this flag is enabled, certain personally identifiable information (PII) is added by active integrations. // By default, no such data is sent. SendDefaultPII bool // BeforeSend is called before error events are sent to Sentry. // You can use it to mutate the event or return nil to discard it. BeforeSend func(event *Event, hint *EventHint) *Event // BeforeSendLong is called before log events are sent to Sentry. // You can use it to mutate the log event or return nil to discard it. BeforeSendLog func(event *Log) *Log // BeforeSendTransaction is called before transaction events are sent to Sentry. // Use it to mutate the transaction or return nil to discard the transaction. BeforeSendTransaction func(event *Event, hint *EventHint) *Event // Before breadcrumb add callback. BeforeBreadcrumb func(breadcrumb *Breadcrumb, hint *BreadcrumbHint) *Breadcrumb // BeforeSendMetric is called before metric events are sent to Sentry. // You can use it to mutate the metric or return nil to discard it. BeforeSendMetric func(metric *Metric) *Metric // Integrations to be installed on the current Client, receives default // integrations. Integrations func([]Integration) []Integration // io.Writer implementation that should be used with the Debug mode. DebugWriter io.Writer // The transport to use. Defaults to HTTPTransport. Transport Transport // The server name to be reported. ServerName string // The release to be sent with events. // // Some Sentry features are built around releases, and, thus, reporting // events with a non-empty release improves the product experience. See // https://docs.sentry.io/product/releases/. // // If Release is not set, the SDK will try to derive a default value // from environment variables or the Git repository in the working // directory. // // If you distribute a compiled binary, it is recommended to set the // Release value explicitly at build time. As an example, you can use: // // go build -ldflags='-X main.release=VALUE' // // That will set the value of a predeclared variable 'release' in the // 'main' package to 'VALUE'. Then, use that variable when initializing // the SDK: // // sentry.Init(ClientOptions{Release: release}) // // See https://golang.org/cmd/go/ and https://golang.org/cmd/link/ for // the official documentation of -ldflags and -X, respectively. Release string // The dist to be sent with events. Dist string // The environment to be sent with events. Environment string // Maximum number of breadcrumbs // when MaxBreadcrumbs is negative then ignore breadcrumbs. MaxBreadcrumbs int // Maximum number of spans. // // See https://develop.sentry.dev/sdk/envelopes/#size-limits for size limits // applied during event ingestion. Events that exceed these limits might get dropped. MaxSpans int // An optional pointer to http.Client that will be used with a default // HTTPTransport. Using your own client will make HTTPTransport, HTTPProxy, // HTTPSProxy and CaCerts options ignored. HTTPClient *http.Client // An optional pointer to http.Transport that will be used with a default // HTTPTransport. Using your own transport will make HTTPProxy, HTTPSProxy // and CaCerts options ignored. HTTPTransport http.RoundTripper // An optional HTTP proxy to use. // This will default to the HTTP_PROXY environment variable. HTTPProxy string // An optional HTTPS proxy to use. // This will default to the HTTPS_PROXY environment variable. // HTTPS_PROXY takes precedence over HTTP_PROXY for https requests. HTTPSProxy string // An optional set of SSL certificates to use. CaCerts *x509.CertPool // MaxErrorDepth is the maximum number of errors reported in a chain of errors. // This protects the SDK from an arbitrarily long chain of wrapped errors. // // An additional consideration is that arguably reporting a long chain of errors // is of little use when debugging production errors with Sentry. The Sentry UI // is not optimized for long chains either. The top-level error together with a // stack trace is often the most useful information. MaxErrorDepth int // Default event tags. These are overridden by tags set on a scope. Tags map[string]string // EnableLogs controls when logs should be emitted. EnableLogs bool // DisableMetrics controls when metrics should be emitted. DisableMetrics bool // TraceIgnoreStatusCodes is a list of HTTP status codes that should not be traced. // Each element can be either: // - A single-element slice [code] for a specific status code // - A two-element slice [min, max] for a range of status codes (inclusive) // When an HTTP request results in a status code that matches any of these codes or ranges, // the transaction will not be sent to Sentry. // // Examples: // [][]int{{404}} // ignore only status code 404 // [][]int{{400, 405}} // ignore status codes 400-405 // [][]int{{404}, {500}} // ignore status codes 404 and 500 // [][]int{{404}, {400, 405}, {500, 599}} // ignore 404, range 400-405, and range 500-599 // // By default, this ignores 404 status codes. // // IMPORTANT: to not ignore any status codes, the option should be an empty slice and not nil. The nil option is // used for defaulting to 404 ignores. TraceIgnoreStatusCodes [][]int // DisableTelemetryBuffer disables the telemetry buffer layer for prioritizing events and uses the old transport layer. DisableTelemetryBuffer bool } // Client is the underlying processor that is used by the main API and Hub // instances. It must be created with NewClient. type Client struct { mu sync.RWMutex options ClientOptions dsn *Dsn eventProcessors []EventProcessor integrations []Integration sdkIdentifier string sdkVersion string // Transport is read-only. Replacing the transport of an existing client is // not supported, create a new client instead. Transport Transport batchLogger *logBatchProcessor batchMeter *metricBatchProcessor telemetryProcessor *telemetry.Processor } // NewClient creates and returns an instance of Client configured using // ClientOptions. // // Most users will not create clients directly. Instead, initialize the SDK with // Init and use the package-level functions (for simple programs that run on a // single goroutine) or hub methods (for concurrent programs, for example web // servers). func NewClient(options ClientOptions) (*Client, error) { // The default error event sample rate for all SDKs is 1.0 (send all). // // In Go, the zero value (default) for float64 is 0.0, which means that // constructing a client with NewClient(ClientOptions{}), or, equivalently, // initializing the SDK with Init(ClientOptions{}) without an explicit // SampleRate would drop all events. // // To retain the desired default behavior, we exceptionally flip SampleRate // from 0.0 to 1.0 here. Setting the sample rate to 0.0 is not very useful // anyway, and the same end result can be achieved in many other ways like // not initializing the SDK, setting the DSN to the empty string or using an // event processor that always returns nil. // // An alternative API could be such that default options don't need to be // the same as Go's zero values, for example using the Functional Options // pattern. That would either require a breaking change if we want to reuse // the obvious NewClient name, or a new function as an alternative // constructor. if options.SampleRate == 0.0 { options.SampleRate = 1.0 } if options.Debug { debugWriter := options.DebugWriter if debugWriter == nil { debugWriter = os.Stderr } debuglog.SetOutput(debugWriter) } if options.Dsn == "" { options.Dsn = os.Getenv("SENTRY_DSN") } if options.Release == "" { options.Release = defaultRelease() } if options.Environment == "" { options.Environment = os.Getenv("SENTRY_ENVIRONMENT") } if options.MaxErrorDepth == 0 { options.MaxErrorDepth = maxErrorDepth } if options.MaxSpans == 0 { options.MaxSpans = defaultMaxSpans } if options.TraceIgnoreStatusCodes == nil { options.TraceIgnoreStatusCodes = [][]int{{404}} } // SENTRYGODEBUG is a comma-separated list of key=value pairs (similar // to GODEBUG). It is not a supported feature: recognized debug options // may change any time. // // The intended public is SDK developers. It is orthogonal to // options.Debug, which is also available for SDK users. dbg := strings.Split(os.Getenv("SENTRYGODEBUG"), ",") sort.Strings(dbg) // dbgOpt returns true when the given debug option is enabled, for // example SENTRYGODEBUG=someopt=1. dbgOpt := func(opt string) bool { s := opt + "=1" return dbg[sort.SearchStrings(dbg, s)%len(dbg)] == s } if dbgOpt("httpdump") || dbgOpt("httptrace") { options.HTTPTransport = &debug.Transport{ RoundTripper: http.DefaultTransport, Output: os.Stderr, Dump: dbgOpt("httpdump"), Trace: dbgOpt("httptrace"), } } var dsn *Dsn if options.Dsn != "" { var err error dsn, err = NewDsn(options.Dsn) if err != nil { return nil, err } } client := Client{ options: options, dsn: dsn, sdkIdentifier: sdkIdentifier, sdkVersion: SDKVersion, } client.setupTransport() // noop Telemetry Buffers and Processor fow now // if !options.DisableTelemetryBuffer { // client.setupTelemetryProcessor() // } else if options.EnableLogs { client.batchLogger = newLogBatchProcessor(&client) client.batchLogger.Start() } if !options.DisableMetrics { client.batchMeter = newMetricBatchProcessor(&client) client.batchMeter.Start() } client.setupIntegrations() return &client, nil } func (client *Client) setupTransport() { opts := client.options transport := opts.Transport if transport == nil { if opts.Dsn == "" { transport = new(noopTransport) } else { transport = NewHTTPTransport() } } transport.Configure(opts) client.Transport = transport } func (client *Client) setupTelemetryProcessor() { // nolint: unused if client.options.DisableTelemetryBuffer { return } if client.dsn == nil { debuglog.Println("Telemetry buffer disabled: no DSN configured") return } // We currently disallow using custom Transport with the new Telemetry Processor, due to the difference in transport signatures. // The option should be enabled when the new Transport interface signature changes. if client.options.Transport != nil { debuglog.Println("Cannot enable Telemetry Processor/Buffers with custom Transport: fallback to old transport") if client.options.EnableLogs { client.batchLogger = newLogBatchProcessor(client) client.batchLogger.Start() } if !client.options.DisableMetrics { client.batchMeter = newMetricBatchProcessor(client) client.batchMeter.Start() } return } transport := httpInternal.NewAsyncTransport(httpInternal.TransportOptions{ Dsn: client.options.Dsn, HTTPClient: client.options.HTTPClient, HTTPTransport: client.options.HTTPTransport, HTTPProxy: client.options.HTTPProxy, HTTPSProxy: client.options.HTTPSProxy, CaCerts: client.options.CaCerts, }) client.Transport = &internalAsyncTransportAdapter{transport: transport} buffers := map[ratelimit.Category]telemetry.Buffer[protocol.TelemetryItem]{ ratelimit.CategoryError: telemetry.NewRingBuffer[protocol.TelemetryItem](ratelimit.CategoryError, 100, telemetry.OverflowPolicyDropOldest, 1, 0), ratelimit.CategoryTransaction: telemetry.NewRingBuffer[protocol.TelemetryItem](ratelimit.CategoryTransaction, 1000, telemetry.OverflowPolicyDropOldest, 1, 0), ratelimit.CategoryLog: telemetry.NewRingBuffer[protocol.TelemetryItem](ratelimit.CategoryLog, 10*100, telemetry.OverflowPolicyDropOldest, 100, 5*time.Second), ratelimit.CategoryMonitor: telemetry.NewRingBuffer[protocol.TelemetryItem](ratelimit.CategoryMonitor, 100, telemetry.OverflowPolicyDropOldest, 1, 0), ratelimit.CategoryTraceMetric: telemetry.NewRingBuffer[protocol.TelemetryItem](ratelimit.CategoryTraceMetric, 10*100, telemetry.OverflowPolicyDropOldest, 100, 5*time.Second), } sdkInfo := &protocol.SdkInfo{ Name: client.sdkIdentifier, Version: client.sdkVersion, } client.telemetryProcessor = telemetry.NewProcessor(buffers, transport, &client.dsn.Dsn, sdkInfo) } func (client *Client) setupIntegrations() { integrations := []Integration{ new(contextifyFramesIntegration), new(environmentIntegration), new(modulesIntegration), new(ignoreErrorsIntegration), new(ignoreTransactionsIntegration), new(globalTagsIntegration), } if client.options.Integrations != nil { integrations = client.options.Integrations(integrations) } for _, integration := range integrations { if client.integrationAlreadyInstalled(integration.Name()) { debuglog.Printf("Integration %s is already installed\n", integration.Name()) continue } client.integrations = append(client.integrations, integration) integration.SetupOnce(client) debuglog.Printf("Integration installed: %s\n", integration.Name()) } sort.Slice(client.integrations, func(i, j int) bool { return client.integrations[i].Name() < client.integrations[j].Name() }) } // AddEventProcessor adds an event processor to the client. It must not be // called from concurrent goroutines. Most users will prefer to use // ClientOptions.BeforeSend or Scope.AddEventProcessor instead. // // Note that typical programs have only a single client created by Init and the // client is shared among multiple hubs, one per goroutine, such that adding an // event processor to the client affects all hubs that share the client. func (client *Client) AddEventProcessor(processor EventProcessor) { client.eventProcessors = append(client.eventProcessors, processor) } // Options return ClientOptions for the current Client. func (client *Client) Options() ClientOptions { // Note: internally, consider using `client.options` instead of `client.Options()` to avoid copying the object each time. return client.options } // CaptureMessage captures an arbitrary message. func (client *Client) CaptureMessage(message string, hint *EventHint, scope EventModifier) *EventID { event := client.EventFromMessage(message, LevelInfo) return client.CaptureEvent(event, hint, scope) } // CaptureException captures an error. func (client *Client) CaptureException(exception error, hint *EventHint, scope EventModifier) *EventID { event := client.EventFromException(exception, LevelError) return client.CaptureEvent(event, hint, scope) } // CaptureCheckIn captures a check in. func (client *Client) CaptureCheckIn(checkIn *CheckIn, monitorConfig *MonitorConfig, scope EventModifier) *EventID { event := client.EventFromCheckIn(checkIn, monitorConfig) if event != nil && event.CheckIn != nil { client.CaptureEvent(event, nil, scope) return &event.CheckIn.ID } return nil } // CaptureEvent captures an event on the currently active client if any. // // The event must already be assembled. Typically, code would instead use // the utility methods like CaptureException. The return value is the // event ID. In case Sentry is disabled or event was dropped, the return value will be nil. func (client *Client) CaptureEvent(event *Event, hint *EventHint, scope EventModifier) *EventID { return client.processEvent(event, hint, scope) } func (client *Client) captureLog(log *Log, _ *Scope) bool { if log == nil { return false } if client.options.BeforeSendLog != nil { log = client.options.BeforeSendLog(log) if log == nil { debuglog.Println("Log dropped due to BeforeSendLog callback.") return false } } if client.telemetryProcessor != nil { if !client.telemetryProcessor.Add(log) { debuglog.Print("Dropping log: telemetry buffer full or category missing") return false } } else if client.batchLogger != nil { if !client.batchLogger.Send(log) { debuglog.Printf("Dropping log [%s]: buffer full", log.Level) return false } } return true } func (client *Client) captureMetric(metric *Metric, _ *Scope) bool { if metric == nil { return false } if client.options.BeforeSendMetric != nil { metric = client.options.BeforeSendMetric(metric) if metric == nil { debuglog.Println("Metric dropped due to BeforeSendMetric callback.") return false } } if client.telemetryProcessor != nil { if !client.telemetryProcessor.Add(metric) { debuglog.Printf("Dropping metric: telemetry buffer full or category missing") return false } } else if client.batchMeter != nil { if !client.batchMeter.Send(metric) { debuglog.Printf("Dropping metric %q: buffer full", metric.Name) return false } } return true } // Recover captures a panic. // Returns EventID if successfully, or nil if there's no error to recover from. func (client *Client) Recover(err interface{}, hint *EventHint, scope EventModifier) *EventID { if err == nil { err = recover() } // Normally we would not pass a nil Context, but RecoverWithContext doesn't // use the Context for communicating deadline nor cancelation. All it does // is store the Context in the EventHint and there nil means the Context is // not available. // nolint: staticcheck return client.RecoverWithContext(nil, err, hint, scope) } // RecoverWithContext captures a panic and passes relevant context object. // Returns EventID if successfully, or nil if there's no error to recover from. func (client *Client) RecoverWithContext( ctx context.Context, err interface{}, hint *EventHint, scope EventModifier, ) *EventID { if err == nil { err = recover() } if err == nil { return nil } if ctx != nil { if hint == nil { hint = &EventHint{} } if hint.Context == nil { hint.Context = ctx } } var event *Event switch err := err.(type) { case error: event = client.EventFromException(err, LevelFatal) case string: event = client.EventFromMessage(err, LevelFatal) default: event = client.EventFromMessage(fmt.Sprintf("%#v", err), LevelFatal) } return client.CaptureEvent(event, hint, scope) } // Flush waits until the underlying Transport sends any buffered events to the // Sentry server, blocking for at most the given timeout. It returns false if // the timeout was reached. In that case, some events may not have been sent. // // Flush should be called before terminating the program to avoid // unintentionally dropping events. // // Do not call Flush indiscriminately after every call to CaptureEvent, // CaptureException or CaptureMessage. Instead, to have the SDK send events over // the network synchronously, configure it to use the HTTPSyncTransport in the // call to Init. func (client *Client) Flush(timeout time.Duration) bool { if client.batchLogger != nil || client.batchMeter != nil || client.telemetryProcessor != nil { ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() return client.FlushWithContext(ctx) } return client.Transport.Flush(timeout) } // FlushWithContext waits until the underlying Transport sends any buffered events // to the Sentry server, blocking for at most the duration specified by the context. // It returns false if the context is canceled before the events are sent. In such a case, // some events may not be delivered. // // FlushWithContext should be called before terminating the program to ensure no // events are unintentionally dropped. // // Avoid calling FlushWithContext indiscriminately after each call to CaptureEvent, // CaptureException, or CaptureMessage. To send events synchronously over the network, // configure the SDK to use HTTPSyncTransport during initialization with Init. func (client *Client) FlushWithContext(ctx context.Context) bool { if client.batchLogger != nil { client.batchLogger.Flush(ctx.Done()) } if client.batchMeter != nil { client.batchMeter.Flush(ctx.Done()) } if client.telemetryProcessor != nil { return client.telemetryProcessor.FlushWithContext(ctx) } return client.Transport.FlushWithContext(ctx) } // Close clean up underlying Transport resources. // // Close should be called after Flush and before terminating the program // otherwise some events may be lost. func (client *Client) Close() { if client.telemetryProcessor != nil { client.telemetryProcessor.Close(5 * time.Second) } if client.batchLogger != nil { client.batchLogger.Shutdown() } if client.batchMeter != nil { client.batchMeter.Shutdown() } client.Transport.Close() } // EventFromMessage creates an event from the given message string. func (client *Client) EventFromMessage(message string, level Level) *Event { if message == "" { err := usageError{fmt.Errorf("%s called with empty message", callerFunctionName())} return client.EventFromException(err, level) } event := NewEvent() event.Level = level event.Message = message if client.options.AttachStacktrace { event.Threads = []Thread{{ Stacktrace: NewStacktrace(), Crashed: false, Current: true, }} } return event } // EventFromException creates a new Sentry event from the given `error` instance. func (client *Client) EventFromException(exception error, level Level) *Event { event := NewEvent() event.Level = level err := exception if err == nil { err = usageError{fmt.Errorf("%s called with nil error", callerFunctionName())} } event.SetException(err, client.options.MaxErrorDepth) return event } // EventFromCheckIn creates a new Sentry event from the given `check_in` instance. func (client *Client) EventFromCheckIn(checkIn *CheckIn, monitorConfig *MonitorConfig) *Event { if checkIn == nil { return nil } event := NewEvent() event.Type = checkInType var checkInID EventID if checkIn.ID == "" { checkInID = EventID(uuid()) } else { checkInID = checkIn.ID } event.CheckIn = &CheckIn{ ID: checkInID, MonitorSlug: checkIn.MonitorSlug, Status: checkIn.Status, Duration: checkIn.Duration, } event.MonitorConfig = monitorConfig return event } func (client *Client) SetSDKIdentifier(identifier string) { client.mu.Lock() defer client.mu.Unlock() client.sdkIdentifier = identifier } func (client *Client) GetSDKIdentifier() string { client.mu.RLock() defer client.mu.RUnlock() return client.sdkIdentifier } func (client *Client) processEvent(event *Event, hint *EventHint, scope EventModifier) *EventID { if event == nil { err := usageError{fmt.Errorf("%s called with nil event", callerFunctionName())} return client.CaptureException(err, hint, scope) } // Transactions are sampled by options.TracesSampleRate or // options.TracesSampler when they are started. Other events // (errors, messages) are sampled here. Does not apply to check-ins. if event.Type != transactionType && event.Type != checkInType && !sample(client.options.SampleRate) { debuglog.Println("Event dropped due to SampleRate hit.") return nil } if event = client.prepareEvent(event, hint, scope); event == nil { return nil } // Apply beforeSend* processors if hint == nil { hint = &EventHint{} } switch event.Type { case transactionType: if client.options.BeforeSendTransaction != nil { if event = client.options.BeforeSendTransaction(event, hint); event == nil { debuglog.Println("Transaction dropped due to BeforeSendTransaction callback.") return nil } } case checkInType: // not a default case, since we shouldn't apply BeforeSend on check-in events default: if client.options.BeforeSend != nil { if event = client.options.BeforeSend(event, hint); event == nil { debuglog.Println("Event dropped due to BeforeSend callback.") return nil } } } if client.telemetryProcessor != nil { if !client.telemetryProcessor.Add(event) { debuglog.Println("Event dropped: telemetry buffer full or unavailable") } } else { client.Transport.SendEvent(event) } return &event.EventID } func (client *Client) prepareEvent(event *Event, hint *EventHint, scope EventModifier) *Event { if event.EventID == "" { // TODO set EventID when the event is created, same as in other SDKs. It's necessary for profileTransaction.ID. event.EventID = EventID(uuid()) } if event.Timestamp.IsZero() { event.Timestamp = time.Now() } if event.Level == "" { event.Level = LevelInfo } if event.ServerName == "" { event.ServerName = client.options.ServerName if event.ServerName == "" { event.ServerName = hostname } } if event.Release == "" { event.Release = client.options.Release } if event.Dist == "" { event.Dist = client.options.Dist } if event.Environment == "" { event.Environment = client.options.Environment } event.Platform = "go" event.Sdk = SdkInfo{ Name: client.GetSDKIdentifier(), Version: SDKVersion, Integrations: client.listIntegrations(), Packages: []SdkPackage{{ Name: "sentry-go", Version: SDKVersion, }}, } if scope != nil { event = scope.ApplyToEvent(event, hint, client) if event == nil { return nil } } for _, processor := range client.eventProcessors { id := event.EventID event = processor(event, hint) if event == nil { debuglog.Printf("Event dropped by one of the Client EventProcessors: %s\n", id) return nil } } for _, processor := range globalEventProcessors { id := event.EventID event = processor(event, hint) if event == nil { debuglog.Printf("Event dropped by one of the Global EventProcessors: %s\n", id) return nil } } return event } func (client *Client) listIntegrations() []string { integrations := make([]string, len(client.integrations)) for i, integration := range client.integrations { integrations[i] = integration.Name() } return integrations } func (client *Client) integrationAlreadyInstalled(name string) bool { for _, integration := range client.integrations { if integration.Name() == name { return true } } return false } // sample returns true with the given probability, which must be in the range // [0.0, 1.0]. func sample(probability float64) bool { return rng.Float64() < probability } ================================================ FILE: vendor/github.com/getsentry/sentry-go/doc.go ================================================ /* Package repository: https://github.com/getsentry/sentry-go/ For more information about Sentry and SDK features, please have a look at the official documentation site: https://docs.sentry.io/platforms/go/ */ package sentry ================================================ FILE: vendor/github.com/getsentry/sentry-go/dsn.go ================================================ package sentry import ( "github.com/getsentry/sentry-go/internal/protocol" ) // Re-export protocol types to maintain public API compatibility // Dsn is used as the remote address source to client transport. type Dsn struct { protocol.Dsn } // DsnParseError represents an error that occurs if a Sentry // DSN cannot be parsed. type DsnParseError = protocol.DsnParseError // NewDsn creates a Dsn by parsing rawURL. Most users will never call this // function directly. It is provided for use in custom Transport // implementations. func NewDsn(rawURL string) (*Dsn, error) { protocolDsn, err := protocol.NewDsn(rawURL) if err != nil { return nil, err } return &Dsn{Dsn: *protocolDsn}, nil } // RequestHeaders returns all the necessary headers that have to be used in the transport when sending events // to the /store endpoint. // // Deprecated: This method shall only be used if you want to implement your own transport that sends events to // the /store endpoint. If you're using the transport provided by the SDK, all necessary headers to authenticate // against the /envelope endpoint are added automatically. func (dsn Dsn) RequestHeaders() map[string]string { return dsn.Dsn.RequestHeaders(SDKVersion) } ================================================ FILE: vendor/github.com/getsentry/sentry-go/dynamic_sampling_context.go ================================================ package sentry import ( "strconv" "strings" "github.com/getsentry/sentry-go/internal/otel/baggage" ) const ( sentryPrefix = "sentry-" ) // DynamicSamplingContext holds information about the current event that can be used to make dynamic sampling decisions. type DynamicSamplingContext struct { Entries map[string]string Frozen bool } func DynamicSamplingContextFromHeader(header []byte) (DynamicSamplingContext, error) { bag, err := baggage.Parse(string(header)) if err != nil { return DynamicSamplingContext{}, err } entries := map[string]string{} for _, member := range bag.Members() { // We only store baggage members if their key starts with "sentry-". if k, v := member.Key(), member.Value(); strings.HasPrefix(k, sentryPrefix) { entries[strings.TrimPrefix(k, sentryPrefix)] = v } } return DynamicSamplingContext{ Entries: entries, // If there's at least one Sentry value, we consider the DSC frozen Frozen: len(entries) > 0, }, nil } func DynamicSamplingContextFromTransaction(span *Span) DynamicSamplingContext { hub := hubFromContext(span.Context()) scope := hub.Scope() client := hub.Client() if client == nil || scope == nil { return DynamicSamplingContext{ Entries: map[string]string{}, Frozen: false, } } entries := make(map[string]string) if traceID := span.TraceID.String(); traceID != "" { entries["trace_id"] = traceID } if sampleRate := span.sampleRate; sampleRate != 0 { entries["sample_rate"] = strconv.FormatFloat(sampleRate, 'f', -1, 64) } if dsn := client.dsn; dsn != nil { if publicKey := dsn.GetPublicKey(); publicKey != "" { entries["public_key"] = publicKey } } if release := client.options.Release; release != "" { entries["release"] = release } if environment := client.options.Environment; environment != "" { entries["environment"] = environment } // Only include the transaction name if it's of good quality (not empty and not SourceURL) if span.Source != "" && span.Source != SourceURL { if span.IsTransaction() { entries["transaction"] = span.Name } } entries["sampled"] = strconv.FormatBool(span.Sampled.Bool()) return DynamicSamplingContext{Entries: entries, Frozen: true} } func (d DynamicSamplingContext) HasEntries() bool { return len(d.Entries) > 0 } func (d DynamicSamplingContext) IsFrozen() bool { return d.Frozen } func (d DynamicSamplingContext) String() string { members := []baggage.Member{} for k, entry := range d.Entries { member, err := baggage.NewMember(sentryPrefix+k, entry) if err != nil { continue } members = append(members, member) } if len(members) == 0 { return "" } baggage, err := baggage.New(members...) if err != nil { return "" } return baggage.String() } // Constructs a new DynamicSamplingContext using a scope and client. Accessing // fields on the scope are not thread safe, and this function should only be // called within scope methods. func DynamicSamplingContextFromScope(scope *Scope, client *Client) DynamicSamplingContext { entries := map[string]string{} if client == nil || scope == nil { return DynamicSamplingContext{ Entries: entries, Frozen: false, } } propagationContext := scope.propagationContext if traceID := propagationContext.TraceID.String(); traceID != "" { entries["trace_id"] = traceID } if sampleRate := client.options.TracesSampleRate; sampleRate != 0 { entries["sample_rate"] = strconv.FormatFloat(sampleRate, 'f', -1, 64) } if dsn := client.dsn; dsn != nil { if publicKey := dsn.GetPublicKey(); publicKey != "" { entries["public_key"] = publicKey } } if release := client.options.Release; release != "" { entries["release"] = release } if environment := client.options.Environment; environment != "" { entries["environment"] = environment } return DynamicSamplingContext{ Entries: entries, Frozen: true, } } ================================================ FILE: vendor/github.com/getsentry/sentry-go/exception.go ================================================ package sentry import ( "fmt" "reflect" "slices" ) const ( MechanismTypeGeneric string = "generic" MechanismTypeChained string = "chained" MechanismTypeUnwrap string = "unwrap" MechanismSourceCause string = "cause" ) type visited struct { ptrs map[uintptr]struct{} msgs map[string]struct{} } func (v *visited) seenError(err error) bool { t := reflect.ValueOf(err) if t.Kind() == reflect.Ptr && !t.IsNil() { ptr := t.Pointer() if _, ok := v.ptrs[ptr]; ok { return true } v.ptrs[ptr] = struct{}{} return false } key := t.String() + err.Error() if _, ok := v.msgs[key]; ok { return true } v.msgs[key] = struct{}{} return false } func convertErrorToExceptions(err error, maxErrorDepth int) []Exception { var exceptions []Exception vis := &visited{ ptrs: make(map[uintptr]struct{}), msgs: make(map[string]struct{}), } convertErrorDFS(err, &exceptions, nil, "", vis, maxErrorDepth, 0) // mechanism type is used for debugging purposes, but since we can't really distinguish the origin of who invoked // captureException, we set it to nil if the error is not chained. if len(exceptions) == 1 { exceptions[0].Mechanism = nil } slices.Reverse(exceptions) // Add a trace of the current stack to the top level(outermost) error in a chain if // it doesn't have a stack trace yet. // We only add to the most recent error to avoid duplication and because the // current stack is most likely unrelated to errors deeper in the chain. if len(exceptions) > 0 && exceptions[len(exceptions)-1].Stacktrace == nil { exceptions[len(exceptions)-1].Stacktrace = NewStacktrace() } return exceptions } func convertErrorDFS(err error, exceptions *[]Exception, parentID *int, source string, visited *visited, maxErrorDepth int, currentDepth int) { if err == nil { return } if visited.seenError(err) { return } _, isExceptionGroup := err.(interface{ Unwrap() []error }) exception := Exception{ Value: err.Error(), Type: reflect.TypeOf(err).String(), Stacktrace: ExtractStacktrace(err), } currentID := len(*exceptions) var mechanismType string if parentID == nil { mechanismType = MechanismTypeGeneric source = "" } else { mechanismType = MechanismTypeChained } exception.Mechanism = &Mechanism{ Type: mechanismType, ExceptionID: currentID, ParentID: parentID, Source: source, IsExceptionGroup: isExceptionGroup, } *exceptions = append(*exceptions, exception) if maxErrorDepth >= 0 && currentDepth >= maxErrorDepth { return } switch v := err.(type) { case interface{ Unwrap() []error }: unwrapped := v.Unwrap() for i := range unwrapped { if unwrapped[i] != nil { childSource := fmt.Sprintf("errors[%d]", i) convertErrorDFS(unwrapped[i], exceptions, ¤tID, childSource, visited, maxErrorDepth, currentDepth+1) } } case interface{ Unwrap() error }: unwrapped := v.Unwrap() if unwrapped != nil { convertErrorDFS(unwrapped, exceptions, ¤tID, MechanismTypeUnwrap, visited, maxErrorDepth, currentDepth+1) } case interface{ Cause() error }: cause := v.Cause() if cause != nil { convertErrorDFS(cause, exceptions, ¤tID, MechanismSourceCause, visited, maxErrorDepth, currentDepth+1) } } } ================================================ FILE: vendor/github.com/getsentry/sentry-go/hub.go ================================================ package sentry import ( "context" "fmt" "sync" "time" "github.com/getsentry/sentry-go/internal/debuglog" ) type contextKey int // Keys used to store values in a Context. Use with Context.Value to access // values stored by the SDK. const ( // HubContextKey is the key used to store the current Hub. HubContextKey = contextKey(1) // RequestContextKey is the key used to store the current http.Request. RequestContextKey = contextKey(2) ) // currentHub is the initial Hub with no Client bound and an empty Scope. var currentHub = NewHub(nil, NewScope()) // Hub is the central object that manages scopes and clients. // // This can be used to capture events and manage the scope. // The default hub that is available automatically. // // In most situations developers do not need to interface the hub. Instead // toplevel convenience functions are exposed that will automatically dispatch // to global (CurrentHub) hub. In some situations this might not be // possible in which case it might become necessary to manually work with the // hub. This is for instance the case when working with async code. type Hub struct { mu sync.RWMutex stack *stack lastEventID EventID } type layer struct { // mu protects concurrent reads and writes to client. mu sync.RWMutex client *Client // scope is read-only, not protected by mu. scope *Scope } // Client returns the layer's client. Safe for concurrent use. func (l *layer) Client() *Client { l.mu.RLock() defer l.mu.RUnlock() return l.client } // SetClient sets the layer's client. Safe for concurrent use. func (l *layer) SetClient(c *Client) { l.mu.Lock() defer l.mu.Unlock() l.client = c } type stack []*layer // NewHub returns an instance of a Hub with provided Client and Scope bound. func NewHub(client *Client, scope *Scope) *Hub { hub := Hub{ stack: &stack{{ client: client, scope: scope, }}, } return &hub } // CurrentHub returns an instance of previously initialized Hub stored in the global namespace. func CurrentHub() *Hub { return currentHub } // LastEventID returns the ID of the last event (error or message) captured // through the hub and sent to the underlying transport. // // Transactions and events dropped by sampling or event processors do not change // the last event ID. // // LastEventID is a convenience method to cover use cases in which errors are // captured indirectly and the ID is needed. For example, it can be used as part // of an HTTP middleware to log the ID of the last error, if any. // // For more flexibility, consider instead using the ClientOptions.BeforeSend // function or event processors. func (hub *Hub) LastEventID() EventID { hub.mu.RLock() defer hub.mu.RUnlock() return hub.lastEventID } // stackTop returns the top layer of the hub stack. Valid hubs always have at // least one layer, therefore stackTop always return a non-nil pointer. func (hub *Hub) stackTop() *layer { hub.mu.RLock() defer hub.mu.RUnlock() stack := hub.stack stackLen := len(*stack) top := (*stack)[stackLen-1] return top } // Clone returns a copy of the current Hub with top-most scope and client copied over. func (hub *Hub) Clone() *Hub { top := hub.stackTop() scope := top.scope if scope != nil { scope = scope.Clone() } return NewHub(top.Client(), scope) } // Scope returns top-level Scope of the current Hub or nil if no Scope is bound. func (hub *Hub) Scope() *Scope { top := hub.stackTop() return top.scope } // Client returns top-level Client of the current Hub or nil if no Client is bound. func (hub *Hub) Client() *Client { top := hub.stackTop() return top.Client() } // PushScope pushes a new scope for the current Hub and reuses previously bound Client. func (hub *Hub) PushScope() *Scope { top := hub.stackTop() var scope *Scope if top.scope != nil { scope = top.scope.Clone() } else { scope = NewScope() } hub.mu.Lock() defer hub.mu.Unlock() *hub.stack = append(*hub.stack, &layer{ client: top.Client(), scope: scope, }) return scope } // PopScope drops the most recent scope. // // Calls to PopScope must be coordinated with PushScope. For most cases, using // WithScope should be more convenient. // // Calls to PopScope that do not match previous calls to PushScope are silently // ignored. func (hub *Hub) PopScope() { hub.mu.Lock() defer hub.mu.Unlock() stack := *hub.stack stackLen := len(stack) if stackLen > 1 { // Never pop the last item off the stack, the stack should always have // at least one item. *hub.stack = stack[0 : stackLen-1] } } // BindClient binds a new Client for the current Hub. func (hub *Hub) BindClient(client *Client) { top := hub.stackTop() top.SetClient(client) } // WithScope runs f in an isolated temporary scope. // // It is useful when extra data should be sent with a single capture call, for // instance a different level or tags. // // The scope passed to f starts as a clone of the current scope and can be // freely modified without affecting the current scope. // // It is a shorthand for PushScope followed by PopScope. func (hub *Hub) WithScope(f func(scope *Scope)) { scope := hub.PushScope() defer hub.PopScope() f(scope) } // ConfigureScope runs f in the current scope. // // It is useful to set data that applies to all events that share the current // scope. // // Modifying the scope affects all references to the current scope. // // See also WithScope for making isolated temporary changes. func (hub *Hub) ConfigureScope(f func(scope *Scope)) { scope := hub.Scope() f(scope) } // CaptureEvent calls the method of a same name on currently bound Client instance // passing it a top-level Scope. // Returns EventID if successfully, or nil if there's no Scope or Client available. func (hub *Hub) CaptureEvent(event *Event) *EventID { client, scope := hub.Client(), hub.Scope() if client == nil || scope == nil { return nil } eventID := client.CaptureEvent(event, nil, scope) if event.Type != transactionType && eventID != nil { hub.mu.Lock() hub.lastEventID = *eventID hub.mu.Unlock() } return eventID } // CaptureMessage calls the method of a same name on currently bound Client instance // passing it a top-level Scope. // Returns EventID if successfully, or nil if there's no Scope or Client available. func (hub *Hub) CaptureMessage(message string) *EventID { client, scope := hub.Client(), hub.Scope() if client == nil || scope == nil { return nil } eventID := client.CaptureMessage(message, nil, scope) if eventID != nil { hub.mu.Lock() hub.lastEventID = *eventID hub.mu.Unlock() } return eventID } // CaptureException calls the method of a same name on currently bound Client instance // passing it a top-level Scope. // Returns EventID if successfully, or nil if there's no Scope or Client available. func (hub *Hub) CaptureException(exception error) *EventID { client, scope := hub.Client(), hub.Scope() if client == nil || scope == nil { return nil } eventID := client.CaptureException(exception, &EventHint{OriginalException: exception}, scope) if eventID != nil { hub.mu.Lock() hub.lastEventID = *eventID hub.mu.Unlock() } return eventID } // CaptureCheckIn calls the method of the same name on currently bound Client instance // passing it a top-level Scope. // Returns CheckInID if the check-in was captured successfully, or nil otherwise. func (hub *Hub) CaptureCheckIn(checkIn *CheckIn, monitorConfig *MonitorConfig) *EventID { client, scope := hub.Client(), hub.Scope() if client == nil { return nil } return client.CaptureCheckIn(checkIn, monitorConfig, scope) } // AddBreadcrumb records a new breadcrumb. // // The total number of breadcrumbs that can be recorded are limited by the // configuration on the client. func (hub *Hub) AddBreadcrumb(breadcrumb *Breadcrumb, hint *BreadcrumbHint) { client := hub.Client() // If there's no client, just store it on the scope straight away if client == nil { hub.Scope().AddBreadcrumb(breadcrumb, defaultMaxBreadcrumbs) return } limit := client.options.MaxBreadcrumbs switch { case limit < 0: return case limit == 0: limit = defaultMaxBreadcrumbs } if client.options.BeforeBreadcrumb != nil { if hint == nil { hint = &BreadcrumbHint{} } if breadcrumb = client.options.BeforeBreadcrumb(breadcrumb, hint); breadcrumb == nil { debuglog.Println("breadcrumb dropped due to BeforeBreadcrumb callback.") return } } hub.Scope().AddBreadcrumb(breadcrumb, limit) } // Recover calls the method of a same name on currently bound Client instance // passing it a top-level Scope. // Returns EventID if successfully, or nil if there's no Scope or Client available. func (hub *Hub) Recover(err interface{}) *EventID { if err == nil { err = recover() } client, scope := hub.Client(), hub.Scope() if client == nil || scope == nil { return nil } return client.Recover(err, &EventHint{RecoveredException: err}, scope) } // RecoverWithContext calls the method of a same name on currently bound Client instance // passing it a top-level Scope. // Returns EventID if successfully, or nil if there's no Scope or Client available. func (hub *Hub) RecoverWithContext(ctx context.Context, err interface{}) *EventID { if err == nil { err = recover() } client, scope := hub.Client(), hub.Scope() if client == nil || scope == nil { return nil } return client.RecoverWithContext(ctx, err, &EventHint{RecoveredException: err}, scope) } // Flush waits until the underlying Transport sends any buffered events to the // Sentry server, blocking for at most the given timeout. It returns false if // the timeout was reached. In that case, some events may not have been sent. // // Flush should be called before terminating the program to avoid // unintentionally dropping events. // // Do not call Flush indiscriminately after every call to CaptureEvent, // CaptureException or CaptureMessage. Instead, to have the SDK send events over // the network synchronously, configure it to use the HTTPSyncTransport in the // call to Init. func (hub *Hub) Flush(timeout time.Duration) bool { client := hub.Client() if client == nil { return false } return client.Flush(timeout) } // FlushWithContext waits until the underlying Transport sends any buffered events // to the Sentry server, blocking for at most the duration specified by the context. // It returns false if the context is canceled before the events are sent. In such a case, // some events may not be delivered. // // FlushWithContext should be called before terminating the program to ensure no // events are unintentionally dropped. // // Avoid calling FlushWithContext indiscriminately after each call to CaptureEvent, // CaptureException, or CaptureMessage. To send events synchronously over the network, // configure the SDK to use HTTPSyncTransport during initialization with Init. func (hub *Hub) FlushWithContext(ctx context.Context) bool { client := hub.Client() if client == nil { return false } return client.FlushWithContext(ctx) } // GetTraceparent returns the current Sentry traceparent string, to be used as a HTTP header value // or HTML meta tag value. // This function is context aware, as in it either returns the traceparent based // on the current span, or the scope's propagation context. func (hub *Hub) GetTraceparent() string { scope := hub.Scope() if scope.span != nil { return scope.span.ToSentryTrace() } return fmt.Sprintf("%s-%s", scope.propagationContext.TraceID, scope.propagationContext.SpanID) } // GetTraceparentW3C returns the current traceparent string in W3C format. // This is intended for propagation to downstream services that expect the W3C header. func (hub *Hub) GetTraceparentW3C() string { scope := hub.Scope() if scope.span != nil { return scope.span.ToTraceparent() } return fmt.Sprintf("00-%s-%s-00", scope.propagationContext.TraceID, scope.propagationContext.SpanID) } // GetBaggage returns the current Sentry baggage string, to be used as a HTTP header value // or HTML meta tag value. // This function is context aware, as in it either returns the baggage based // on the current span or the scope's propagation context. func (hub *Hub) GetBaggage() string { scope := hub.Scope() if scope.span != nil { return scope.span.ToBaggage() } return scope.propagationContext.DynamicSamplingContext.String() } // HasHubOnContext checks whether Hub instance is bound to a given Context struct. func HasHubOnContext(ctx context.Context) bool { _, ok := ctx.Value(HubContextKey).(*Hub) return ok } // GetHubFromContext tries to retrieve Hub instance from the given Context struct // or return nil if one is not found. func GetHubFromContext(ctx context.Context) *Hub { if hub, ok := ctx.Value(HubContextKey).(*Hub); ok { return hub } return nil } // hubFromContext returns either a hub stored in the context or the current hub. // The return value is guaranteed to be non-nil, unlike GetHubFromContext. func hubFromContext(ctx context.Context) *Hub { if hub, ok := ctx.Value(HubContextKey).(*Hub); ok { return hub } return currentHub } // SetHubOnContext stores given Hub instance on the Context struct and returns a new Context. func SetHubOnContext(ctx context.Context, hub *Hub) context.Context { return context.WithValue(ctx, HubContextKey, hub) } ================================================ FILE: vendor/github.com/getsentry/sentry-go/integrations.go ================================================ package sentry import ( "fmt" "os" "regexp" "runtime" "runtime/debug" "strings" "sync" "github.com/getsentry/sentry-go/internal/debuglog" ) // ================================ // Modules Integration // ================================ type modulesIntegration struct { once sync.Once modules map[string]string } func (mi *modulesIntegration) Name() string { return "Modules" } func (mi *modulesIntegration) SetupOnce(client *Client) { client.AddEventProcessor(mi.processor) } func (mi *modulesIntegration) processor(event *Event, _ *EventHint) *Event { if len(event.Modules) == 0 { mi.once.Do(func() { info, ok := debug.ReadBuildInfo() if !ok { debuglog.Print("The Modules integration is not available in binaries built without module support.") return } mi.modules = extractModules(info) }) } event.Modules = mi.modules return event } func extractModules(info *debug.BuildInfo) map[string]string { modules := map[string]string{ info.Main.Path: info.Main.Version, } for _, dep := range info.Deps { ver := dep.Version if dep.Replace != nil { ver += fmt.Sprintf(" => %s %s", dep.Replace.Path, dep.Replace.Version) } modules[dep.Path] = strings.TrimSuffix(ver, " ") } return modules } // ================================ // Environment Integration // ================================ type environmentIntegration struct{} func (ei *environmentIntegration) Name() string { return "Environment" } func (ei *environmentIntegration) SetupOnce(client *Client) { client.AddEventProcessor(ei.processor) } func (ei *environmentIntegration) processor(event *Event, _ *EventHint) *Event { // Initialize maps as necessary. contextNames := []string{"device", "os", "runtime"} if event.Contexts == nil { event.Contexts = make(map[string]Context, len(contextNames)) } for _, name := range contextNames { if event.Contexts[name] == nil { event.Contexts[name] = make(Context) } } // Set contextual information preserving existing data. For each context, if // the existing value is not of type map[string]interface{}, then no // additional information is added. if deviceContext, ok := event.Contexts["device"]; ok { if _, ok := deviceContext["arch"]; !ok { deviceContext["arch"] = runtime.GOARCH } if _, ok := deviceContext["num_cpu"]; !ok { deviceContext["num_cpu"] = runtime.NumCPU() } } if osContext, ok := event.Contexts["os"]; ok { if _, ok := osContext["name"]; !ok { osContext["name"] = runtime.GOOS } } if runtimeContext, ok := event.Contexts["runtime"]; ok { if _, ok := runtimeContext["name"]; !ok { runtimeContext["name"] = "go" } if _, ok := runtimeContext["version"]; !ok { runtimeContext["version"] = runtime.Version() } if _, ok := runtimeContext["go_numroutines"]; !ok { runtimeContext["go_numroutines"] = runtime.NumGoroutine() } if _, ok := runtimeContext["go_maxprocs"]; !ok { runtimeContext["go_maxprocs"] = runtime.GOMAXPROCS(0) } if _, ok := runtimeContext["go_numcgocalls"]; !ok { runtimeContext["go_numcgocalls"] = runtime.NumCgoCall() } } return event } // ================================ // Ignore Errors Integration // ================================ type ignoreErrorsIntegration struct { ignoreErrors []*regexp.Regexp } func (iei *ignoreErrorsIntegration) Name() string { return "IgnoreErrors" } func (iei *ignoreErrorsIntegration) SetupOnce(client *Client) { iei.ignoreErrors = transformStringsIntoRegexps(client.options.IgnoreErrors) client.AddEventProcessor(iei.processor) } func (iei *ignoreErrorsIntegration) processor(event *Event, _ *EventHint) *Event { suspects := getIgnoreErrorsSuspects(event) for _, suspect := range suspects { for _, pattern := range iei.ignoreErrors { if pattern.Match([]byte(suspect)) || strings.Contains(suspect, pattern.String()) { debuglog.Printf("Event dropped due to being matched by `IgnoreErrors` option."+ "| Value matched: %s | Filter used: %s", suspect, pattern) return nil } } } return event } func transformStringsIntoRegexps(strings []string) []*regexp.Regexp { var exprs []*regexp.Regexp for _, s := range strings { r, err := regexp.Compile(s) if err == nil { exprs = append(exprs, r) } } return exprs } func getIgnoreErrorsSuspects(event *Event) []string { suspects := []string{} if event.Message != "" { suspects = append(suspects, event.Message) } for _, ex := range event.Exception { suspects = append(suspects, ex.Type, ex.Value) } return suspects } // ================================ // Ignore Transactions Integration // ================================ type ignoreTransactionsIntegration struct { ignoreTransactions []*regexp.Regexp } func (iei *ignoreTransactionsIntegration) Name() string { return "IgnoreTransactions" } func (iei *ignoreTransactionsIntegration) SetupOnce(client *Client) { iei.ignoreTransactions = transformStringsIntoRegexps(client.options.IgnoreTransactions) client.AddEventProcessor(iei.processor) } func (iei *ignoreTransactionsIntegration) processor(event *Event, _ *EventHint) *Event { suspect := event.Transaction if suspect == "" { return event } for _, pattern := range iei.ignoreTransactions { if pattern.Match([]byte(suspect)) || strings.Contains(suspect, pattern.String()) { debuglog.Printf("Transaction dropped due to being matched by `IgnoreTransactions` option."+ "| Value matched: %s | Filter used: %s", suspect, pattern) return nil } } return event } // ================================ // Contextify Frames Integration // ================================ type contextifyFramesIntegration struct { sr sourceReader contextLines int cachedLocations sync.Map } func (cfi *contextifyFramesIntegration) Name() string { return "ContextifyFrames" } func (cfi *contextifyFramesIntegration) SetupOnce(client *Client) { cfi.sr = newSourceReader() cfi.contextLines = 5 client.AddEventProcessor(cfi.processor) } func (cfi *contextifyFramesIntegration) processor(event *Event, _ *EventHint) *Event { // Range over all exceptions for _, ex := range event.Exception { // If it has no stacktrace, just bail out if ex.Stacktrace == nil { continue } // If it does, it should have frames, so try to contextify them ex.Stacktrace.Frames = cfi.contextify(ex.Stacktrace.Frames) } // Range over all threads for _, th := range event.Threads { // If it has no stacktrace, just bail out if th.Stacktrace == nil { continue } // If it does, it should have frames, so try to contextify them th.Stacktrace.Frames = cfi.contextify(th.Stacktrace.Frames) } return event } func (cfi *contextifyFramesIntegration) contextify(frames []Frame) []Frame { contextifiedFrames := make([]Frame, 0, len(frames)) for _, frame := range frames { if !frame.InApp { contextifiedFrames = append(contextifiedFrames, frame) continue } var path string if cachedPath, ok := cfi.cachedLocations.Load(frame.AbsPath); ok { if p, ok := cachedPath.(string); ok { path = p } } else { // Optimize for happy path here if fileExists(frame.AbsPath) { path = frame.AbsPath } else { path = cfi.findNearbySourceCodeLocation(frame.AbsPath) } } if path == "" { contextifiedFrames = append(contextifiedFrames, frame) continue } lines, contextLine := cfi.sr.readContextLines(path, frame.Lineno, cfi.contextLines) contextifiedFrames = append(contextifiedFrames, cfi.addContextLinesToFrame(frame, lines, contextLine)) } return contextifiedFrames } func (cfi *contextifyFramesIntegration) findNearbySourceCodeLocation(originalPath string) string { trimmedPath := strings.TrimPrefix(originalPath, "/") components := strings.Split(trimmedPath, "/") for len(components) > 0 { components = components[1:] possibleLocation := strings.Join(components, "/") if fileExists(possibleLocation) { cfi.cachedLocations.Store(originalPath, possibleLocation) return possibleLocation } } cfi.cachedLocations.Store(originalPath, "") return "" } func (cfi *contextifyFramesIntegration) addContextLinesToFrame(frame Frame, lines [][]byte, contextLine int) Frame { for i, line := range lines { switch { case i < contextLine: frame.PreContext = append(frame.PreContext, string(line)) case i == contextLine: frame.ContextLine = string(line) default: frame.PostContext = append(frame.PostContext, string(line)) } } return frame } // ================================ // Global Tags Integration // ================================ const envTagsPrefix = "SENTRY_TAGS_" type globalTagsIntegration struct { tags map[string]string envTags map[string]string } func (ti *globalTagsIntegration) Name() string { return "GlobalTags" } func (ti *globalTagsIntegration) SetupOnce(client *Client) { ti.tags = make(map[string]string, len(client.options.Tags)) for k, v := range client.options.Tags { ti.tags[k] = v } ti.envTags = loadEnvTags() client.AddEventProcessor(ti.processor) } func (ti *globalTagsIntegration) processor(event *Event, _ *EventHint) *Event { if len(ti.tags) == 0 && len(ti.envTags) == 0 { return event } if event.Tags == nil { event.Tags = make(map[string]string, len(ti.tags)+len(ti.envTags)) } for k, v := range ti.tags { if _, ok := event.Tags[k]; !ok { event.Tags[k] = v } } for k, v := range ti.envTags { if _, ok := event.Tags[k]; !ok { event.Tags[k] = v } } return event } func loadEnvTags() map[string]string { tags := map[string]string{} for _, pair := range os.Environ() { parts := strings.Split(pair, "=") if !strings.HasPrefix(parts[0], envTagsPrefix) { continue } tag := strings.TrimPrefix(parts[0], envTagsPrefix) tags[tag] = parts[1] } return tags } ================================================ FILE: vendor/github.com/getsentry/sentry-go/interfaces.go ================================================ package sentry import ( "context" "encoding/json" "fmt" "net" "net/http" "strings" "time" "github.com/getsentry/sentry-go/attribute" "github.com/getsentry/sentry-go/internal/debuglog" "github.com/getsentry/sentry-go/internal/protocol" "github.com/getsentry/sentry-go/internal/ratelimit" ) const errorType = "" const eventType = "event" const transactionType = "transaction" const checkInType = "check_in" var logEvent = struct { Type string ContentType string }{ "log", "application/vnd.sentry.items.log+json", } var traceMetricEvent = struct { Type string ContentType string }{ "trace_metric", "application/vnd.sentry.items.trace-metric+json", } // Level marks the severity of the event. type Level string // Describes the severity of the event. const ( LevelDebug Level = "debug" LevelInfo Level = "info" LevelWarning Level = "warning" LevelError Level = "error" LevelFatal Level = "fatal" ) // SdkInfo contains all metadata about the SDK. type SdkInfo = protocol.SdkInfo type SdkPackage = protocol.SdkPackage // TODO: This type could be more useful, as map of interface{} is too generic // and requires a lot of type assertions in beforeBreadcrumb calls // plus it could just be map[string]interface{} then. // BreadcrumbHint contains information that can be associated with a Breadcrumb. type BreadcrumbHint map[string]interface{} // Breadcrumb specifies an application event that occurred before a Sentry event. // An event may contain one or more breadcrumbs. type Breadcrumb struct { Type string `json:"type,omitempty"` Category string `json:"category,omitempty"` Message string `json:"message,omitempty"` Data map[string]interface{} `json:"data,omitempty"` Level Level `json:"level,omitempty"` Timestamp time.Time `json:"timestamp,omitzero"` } // TODO: provide constants for known breadcrumb types. // See https://develop.sentry.dev/sdk/event-payloads/breadcrumbs/#breadcrumb-types. // Logger provides a chaining API for structured logging to Sentry. type Logger interface { // Write implements the io.Writer interface. Currently, the [sentry.Hub] is // context aware, in order to get the correct trace correlation. Using this // might result in incorrect span association on logs. If you need to use // Write it is recommended to create a NewLogger so that the associated context // is passed correctly. Write(p []byte) (n int, err error) // SetAttributes allows attaching parameters to the logger using the attribute API. // These attributes will be included in all subsequent log entries. SetAttributes(...attribute.Builder) // Trace defines the [sentry.LogLevel] for the log entry. Trace() LogEntry // Debug defines the [sentry.LogLevel] for the log entry. Debug() LogEntry // Info defines the [sentry.LogLevel] for the log entry. Info() LogEntry // Warn defines the [sentry.LogLevel] for the log entry. Warn() LogEntry // Error defines the [sentry.LogLevel] for the log entry. Error() LogEntry // Fatal defines the [sentry.LogLevel] for the log entry. Fatal() LogEntry // Panic defines the [sentry.LogLevel] for the log entry. Panic() LogEntry // LFatal defines the [sentry.LogLevel] for the log entry. This only sets // the level to fatal, but does not panic or exit. LFatal() LogEntry // GetCtx returns the [context.Context] set on the logger. GetCtx() context.Context } // LogEntry defines the interface for a log entry that supports chaining attributes. type LogEntry interface { // WithCtx creates a new LogEntry with the specified context without overwriting the previous one. WithCtx(ctx context.Context) LogEntry // String adds a string attribute to the LogEntry. String(key, value string) LogEntry // Int adds an int attribute to the LogEntry. Int(key string, value int) LogEntry // Int64 adds an int64 attribute to the LogEntry. Int64(key string, value int64) LogEntry // Float64 adds a float64 attribute to the LogEntry. Float64(key string, value float64) LogEntry // Bool adds a bool attribute to the LogEntry. Bool(key string, value bool) LogEntry // Emit emits the LogEntry with the provided arguments. Emit(args ...interface{}) // Emitf emits the LogEntry using a format string and arguments. Emitf(format string, args ...interface{}) } // Meter provides an interface for recording metrics. type Meter interface { // WithCtx returns a new Meter that uses the given context for trace/span association. WithCtx(ctx context.Context) Meter // SetAttributes allows attaching parameters to the meter using the attribute API. // These attributes will be included in all subsequent metrics. SetAttributes(attrs ...attribute.Builder) // Count records a count metric. Count(name string, count int64, opts ...MeterOption) // Gauge records a gauge metric. Gauge(name string, value float64, opts ...MeterOption) // Distribution records a distribution metric. Distribution(name string, sample float64, opts ...MeterOption) } // MeterOption configures a metric recording call. type MeterOption func(*meterOptions) type meterOptions struct { unit string scope *Scope attributes map[string]attribute.Value } // WithUnit sets the unit for the metric (e.g., "millisecond", "byte"). func WithUnit(unit string) MeterOption { return func(o *meterOptions) { o.unit = unit } } // WithScopeOverride sets a custom scope for the metric, overriding the default scope from the hub. func WithScopeOverride(scope *Scope) MeterOption { return func(o *meterOptions) { o.scope = scope } } // WithAttributes sets attributes for the metric. func WithAttributes(attrs ...attribute.Builder) MeterOption { return func(o *meterOptions) { if o.attributes == nil { o.attributes = make(map[string]attribute.Value, len(attrs)) } for _, a := range attrs { if a.Value.Type() == attribute.INVALID { debuglog.Printf("invalid attribute: %v", a) continue } o.attributes[a.Key] = a.Value } } } // Attachment allows associating files with your events to aid in investigation. // An event may contain one or more attachments. type Attachment struct { Filename string ContentType string Payload []byte } // User describes the user associated with an Event. If this is used, at least // an ID or an IP address should be provided. type User struct { ID string `json:"id,omitempty"` Email string `json:"email,omitempty"` IPAddress string `json:"ip_address,omitempty"` Username string `json:"username,omitempty"` Name string `json:"name,omitempty"` Data map[string]string `json:"data,omitempty"` } func (u User) IsEmpty() bool { if u.ID != "" { return false } if u.Email != "" { return false } if u.IPAddress != "" { return false } if u.Username != "" { return false } if u.Name != "" { return false } if len(u.Data) > 0 { return false } return true } // Request contains information on a HTTP request related to the event. type Request struct { URL string `json:"url,omitempty"` Method string `json:"method,omitempty"` Data string `json:"data,omitempty"` QueryString string `json:"query_string,omitempty"` Cookies string `json:"cookies,omitempty"` Headers map[string]string `json:"headers,omitempty"` Env map[string]string `json:"env,omitempty"` } var sensitiveHeaders = map[string]struct{}{ "_csrf": {}, "_csrf_token": {}, "_session": {}, "_xsrf": {}, "Api-Key": {}, "Apikey": {}, "Auth": {}, "Authorization": {}, "Cookie": {}, "Credentials": {}, "Csrf": {}, "Csrf-Token": {}, "Csrftoken": {}, "Ip-Address": {}, "Passwd": {}, "Password": {}, "Private-Key": {}, "Privatekey": {}, "Proxy-Authorization": {}, "Remote-Addr": {}, "Secret": {}, "Session": {}, "Sessionid": {}, "Token": {}, "User-Session": {}, "X-Api-Key": {}, "X-Csrftoken": {}, "X-Forwarded-For": {}, "X-Real-Ip": {}, "XSRF-TOKEN": {}, } // NewRequest returns a new Sentry Request from the given http.Request. // // NewRequest avoids operations that depend on network access. In particular, it // does not read r.Body. func NewRequest(r *http.Request) *Request { prot := protocol.SchemeHTTP if r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" { prot = protocol.SchemeHTTPS } url := fmt.Sprintf("%s://%s%s", prot, r.Host, r.URL.Path) var cookies string var env map[string]string headers := map[string]string{} if client := CurrentHub().Client(); client != nil && client.options.SendDefaultPII { // We read only the first Cookie header because of the specification: // https://tools.ietf.org/html/rfc6265#section-5.4 // When the user agent generates an HTTP request, the user agent MUST NOT // attach more than one Cookie header field. cookies = r.Header.Get("Cookie") headers = make(map[string]string, len(r.Header)) for k, v := range r.Header { headers[k] = strings.Join(v, ",") } if addr, port, err := net.SplitHostPort(r.RemoteAddr); err == nil { env = map[string]string{"REMOTE_ADDR": addr, "REMOTE_PORT": port} } } else { for k, v := range r.Header { if _, ok := sensitiveHeaders[k]; !ok { headers[k] = strings.Join(v, ",") } } } headers["Host"] = r.Host return &Request{ URL: url, Method: r.Method, QueryString: r.URL.RawQuery, Cookies: cookies, Headers: headers, Env: env, } } // Mechanism is the mechanism by which an exception was generated and handled. type Mechanism struct { Type string `json:"type"` Description string `json:"description,omitempty"` HelpLink string `json:"help_link,omitempty"` Source string `json:"source,omitempty"` Handled *bool `json:"handled,omitempty"` ParentID *int `json:"parent_id,omitempty"` ExceptionID int `json:"exception_id"` IsExceptionGroup bool `json:"is_exception_group,omitempty"` Data map[string]any `json:"data,omitempty"` } // SetUnhandled indicates that the exception is an unhandled exception, i.e. // from a panic. func (m *Mechanism) SetUnhandled() { m.Handled = Pointer(false) } // Exception specifies an error that occurred. type Exception struct { Type string `json:"type,omitempty"` // used as the main issue title Value string `json:"value,omitempty"` // used as the main issue subtitle Module string `json:"module,omitempty"` ThreadID uint64 `json:"thread_id,omitempty"` Stacktrace *Stacktrace `json:"stacktrace,omitempty"` Mechanism *Mechanism `json:"mechanism,omitempty"` } // SDKMetaData is a struct to stash data which is needed at some point in the SDK's event processing pipeline // but which shouldn't get send to Sentry. type SDKMetaData struct { dsc DynamicSamplingContext } // Contains information about how the name of the transaction was determined. type TransactionInfo struct { Source TransactionSource `json:"source,omitempty"` } // The DebugMeta interface is not used in Golang apps, but may be populated // when proxying Events from other platforms, like iOS, Android, and the // Web. (See: https://develop.sentry.dev/sdk/event-payloads/debugmeta/ ). type DebugMeta struct { SdkInfo *DebugMetaSdkInfo `json:"sdk_info,omitempty"` Images []DebugMetaImage `json:"images,omitempty"` } type DebugMetaSdkInfo struct { SdkName string `json:"sdk_name,omitempty"` VersionMajor int `json:"version_major,omitempty"` VersionMinor int `json:"version_minor,omitempty"` VersionPatchlevel int `json:"version_patchlevel,omitempty"` } type DebugMetaImage struct { Type string `json:"type,omitempty"` // all ImageAddr string `json:"image_addr,omitempty"` // macho,elf,pe ImageSize int `json:"image_size,omitempty"` // macho,elf,pe DebugID string `json:"debug_id,omitempty"` // macho,elf,pe,wasm,sourcemap DebugFile string `json:"debug_file,omitempty"` // macho,elf,pe,wasm CodeID string `json:"code_id,omitempty"` // macho,elf,pe,wasm CodeFile string `json:"code_file,omitempty"` // macho,elf,pe,wasm,sourcemap ImageVmaddr string `json:"image_vmaddr,omitempty"` // macho,elf,pe Arch string `json:"arch,omitempty"` // macho,elf,pe UUID string `json:"uuid,omitempty"` // proguard } // EventID is a hexadecimal string representing a unique uuid4 for an Event. // An EventID must be 32 characters long, lowercase and not have any dashes. type EventID string type Context = map[string]interface{} // Event is the fundamental data structure that is sent to Sentry. type Event struct { Breadcrumbs []*Breadcrumb `json:"breadcrumbs,omitempty"` Contexts map[string]Context `json:"contexts,omitempty"` Dist string `json:"dist,omitempty"` Environment string `json:"environment,omitempty"` EventID EventID `json:"event_id,omitempty"` Extra map[string]interface{} `json:"extra,omitempty"` Fingerprint []string `json:"fingerprint,omitempty"` Level Level `json:"level,omitempty"` Message string `json:"message,omitempty"` Platform string `json:"platform,omitempty"` Release string `json:"release,omitempty"` Sdk SdkInfo `json:"sdk,omitempty"` ServerName string `json:"server_name,omitempty"` Threads []Thread `json:"threads,omitempty"` Tags map[string]string `json:"tags,omitempty"` Timestamp time.Time `json:"timestamp,omitzero"` Transaction string `json:"transaction,omitempty"` User User `json:"user,omitempty"` Logger string `json:"logger,omitempty"` Modules map[string]string `json:"modules,omitempty"` Request *Request `json:"request,omitempty"` Exception []Exception `json:"exception,omitempty"` DebugMeta *DebugMeta `json:"debug_meta,omitempty"` Attachments []*Attachment `json:"-"` // The fields below are only relevant for transactions. Type string `json:"type,omitempty"` StartTime time.Time `json:"start_timestamp,omitzero"` Spans []*Span `json:"spans,omitempty"` TransactionInfo *TransactionInfo `json:"transaction_info,omitempty"` // The fields below are only relevant for crons/check ins CheckIn *CheckIn `json:"check_in,omitempty"` MonitorConfig *MonitorConfig `json:"monitor_config,omitempty"` // The fields below are only relevant for logs Logs []Log `json:"-"` // The fields below are only relevant for metrics Metrics []Metric `json:"-"` // The fields below are not part of the final JSON payload. sdkMetaData SDKMetaData } // SetException appends the unwrapped errors to the event's exception list. // // maxErrorDepth is the maximum depth of the error chain we will look // into while unwrapping the errors. If maxErrorDepth is -1, we will // unwrap all errors in the chain. func (e *Event) SetException(exception error, maxErrorDepth int) { if exception == nil { return } exceptions := convertErrorToExceptions(exception, maxErrorDepth) if len(exceptions) == 0 { return } e.Exception = exceptions } // ToEnvelopeItem converts the Event to a Sentry envelope item. func (e *Event) ToEnvelopeItem() (*protocol.EnvelopeItem, error) { eventBody, err := json.Marshal(e) if err != nil { // Try fallback: remove problematic fields and retry e.Breadcrumbs = nil e.Contexts = nil e.Extra = map[string]interface{}{ "info": fmt.Sprintf("Could not encode original event as JSON. "+ "Succeeded by removing Breadcrumbs, Contexts and Extra. "+ "Please verify the data you attach to the scope. "+ "Error: %s", err), } eventBody, err = json.Marshal(e) if err != nil { return nil, fmt.Errorf("event could not be marshaled even with fallback: %w", err) } DebugLogger.Printf("Event marshaling succeeded with fallback after removing problematic fields") } // TODO: all event types should be abstracted to implement EnvelopeItemConvertible and convert themselves. var item *protocol.EnvelopeItem switch e.Type { case transactionType: item = protocol.NewEnvelopeItem(protocol.EnvelopeItemTypeTransaction, eventBody) case checkInType: item = protocol.NewEnvelopeItem(protocol.EnvelopeItemTypeCheckIn, eventBody) case logEvent.Type: item = protocol.NewLogItem(len(e.Logs), eventBody) case traceMetricEvent.Type: item = protocol.NewTraceMetricItem(len(e.Metrics), eventBody) default: item = protocol.NewEnvelopeItem(protocol.EnvelopeItemTypeEvent, eventBody) } return item, nil } // GetCategory returns the rate limit category for this event. func (e *Event) GetCategory() ratelimit.Category { return e.toCategory() } // GetEventID returns the event ID. func (e *Event) GetEventID() string { return string(e.EventID) } // GetSdkInfo returns SDK information for the envelope header. func (e *Event) GetSdkInfo() *protocol.SdkInfo { return &e.Sdk } // GetDynamicSamplingContext returns trace context for the envelope header. func (e *Event) GetDynamicSamplingContext() map[string]string { trace := make(map[string]string) if dsc := e.sdkMetaData.dsc; dsc.HasEntries() { for k, v := range dsc.Entries { trace[k] = v } } return trace } // TODO: Event.Contexts map[string]interface{} => map[string]EventContext, // to prevent accidentally storing T when we mean *T. // For example, the TraceContext must be stored as *TraceContext to pick up the // MarshalJSON method (and avoid copying). // type EventContext interface{ EventContext() } // MarshalJSON converts the Event struct to JSON. func (e *Event) MarshalJSON() ([]byte, error) { if e.Type == checkInType { return e.checkInMarshalJSON() } return e.defaultMarshalJSON() } func (e *Event) defaultMarshalJSON() ([]byte, error) { // event aliases Event to allow calling json.Marshal without an infinite // loop. It preserves all fields while none of the attached methods. type event Event if e.Type == transactionType { return json.Marshal(struct{ *event }{(*event)(e)}) } // metrics and logs should be serialized under the same `items` json field. if e.Type == logEvent.Type { type logEvent struct { *event Items []Log `json:"items,omitempty"` Type json.RawMessage `json:"type,omitempty"` } return json.Marshal(logEvent{event: (*event)(e), Items: e.Logs}) } if e.Type == traceMetricEvent.Type { type metricEvent struct { *event Items []Metric `json:"items,omitempty"` Type json.RawMessage `json:"type,omitempty"` } return json.Marshal(metricEvent{event: (*event)(e), Items: e.Metrics}) } // errorEvent is like Event with shadowed fields for customizing JSON // marshaling. type errorEvent struct { *event // The fields below are not part of error events and only make sense to // be sent for transactions. They shadow the respective fields in Event // and are meant to remain nil, triggering the omitempty behavior. Type json.RawMessage `json:"type,omitempty"` StartTime json.RawMessage `json:"start_timestamp,omitempty"` Spans json.RawMessage `json:"spans,omitempty"` TransactionInfo json.RawMessage `json:"transaction_info,omitempty"` } x := errorEvent{event: (*event)(e)} return json.Marshal(x) } func (e *Event) checkInMarshalJSON() ([]byte, error) { checkIn := serializedCheckIn{ CheckInID: string(e.CheckIn.ID), MonitorSlug: e.CheckIn.MonitorSlug, Status: e.CheckIn.Status, Duration: e.CheckIn.Duration.Seconds(), Release: e.Release, Environment: e.Environment, MonitorConfig: nil, } if e.MonitorConfig != nil { checkIn.MonitorConfig = &MonitorConfig{ Schedule: e.MonitorConfig.Schedule, CheckInMargin: e.MonitorConfig.CheckInMargin, MaxRuntime: e.MonitorConfig.MaxRuntime, Timezone: e.MonitorConfig.Timezone, FailureIssueThreshold: e.MonitorConfig.FailureIssueThreshold, RecoveryThreshold: e.MonitorConfig.RecoveryThreshold, } } return json.Marshal(checkIn) } func (e *Event) toCategory() ratelimit.Category { switch e.Type { case errorType: return ratelimit.CategoryError case transactionType: return ratelimit.CategoryTransaction case logEvent.Type: return ratelimit.CategoryLog case checkInType: return ratelimit.CategoryMonitor case traceMetricEvent.Type: return ratelimit.CategoryTraceMetric default: return ratelimit.CategoryUnknown } } // NewEvent creates a new Event. func NewEvent() *Event { return &Event{ Contexts: make(map[string]Context), Extra: make(map[string]interface{}), Tags: make(map[string]string), Modules: make(map[string]string), } } // Thread specifies threads that were running at the time of an event. type Thread struct { ID string `json:"id,omitempty"` Name string `json:"name,omitempty"` Stacktrace *Stacktrace `json:"stacktrace,omitempty"` Crashed bool `json:"crashed,omitempty"` Current bool `json:"current,omitempty"` } // EventHint contains information that can be associated with an Event. type EventHint struct { Data interface{} EventID string OriginalException error RecoveredException interface{} Context context.Context Request *http.Request Response *http.Response } type Log struct { Timestamp time.Time `json:"timestamp,omitzero"` TraceID TraceID `json:"trace_id"` SpanID SpanID `json:"span_id,omitzero"` Level LogLevel `json:"level"` Severity int `json:"severity_number,omitempty"` Body string `json:"body"` Attributes map[string]attribute.Value `json:"attributes,omitempty"` } // GetCategory returns the rate limit category for logs. func (l *Log) GetCategory() ratelimit.Category { return ratelimit.CategoryLog } // GetEventID returns empty string (event ID set when batching). func (l *Log) GetEventID() string { return "" } // GetSdkInfo returns nil (SDK info set when batching). func (l *Log) GetSdkInfo() *protocol.SdkInfo { return nil } // GetDynamicSamplingContext returns nil (trace context set when batching). func (l *Log) GetDynamicSamplingContext() map[string]string { return nil } type MetricType string const ( MetricTypeInvalid MetricType = "" MetricTypeCounter MetricType = "counter" MetricTypeGauge MetricType = "gauge" MetricTypeDistribution MetricType = "distribution" ) type Metric struct { Timestamp time.Time `json:"timestamp,omitzero"` TraceID TraceID `json:"trace_id"` SpanID SpanID `json:"span_id,omitzero"` Type MetricType `json:"type"` Name string `json:"name"` Value MetricValue `json:"value"` Unit string `json:"unit,omitempty"` Attributes map[string]attribute.Value `json:"attributes,omitempty"` } // GetCategory returns the rate limit category for metrics. func (m *Metric) GetCategory() ratelimit.Category { return ratelimit.CategoryTraceMetric } // GetEventID returns empty string (event ID set when batching). func (m *Metric) GetEventID() string { return "" } // GetSdkInfo returns nil (SDK info set when batching). func (m *Metric) GetSdkInfo() *protocol.SdkInfo { return nil } // GetDynamicSamplingContext returns nil (trace context set when batching). func (m *Metric) GetDynamicSamplingContext() map[string]string { return nil } // MetricValue stores metric values with full precision. // It supports int64 (for counters) and float64 (for gauges and distributions). type MetricValue struct { value attribute.Value } // Int64MetricValue creates a MetricValue from an int64. // Used for counter metrics to preserve full int64 precision. func Int64MetricValue(v int64) MetricValue { return MetricValue{value: attribute.Int64Value(v)} } // Float64MetricValue creates a MetricValue from a float64. // Used for gauge and distribution metrics. func Float64MetricValue(v float64) MetricValue { return MetricValue{value: attribute.Float64Value(v)} } // Type returns the type of the stored value (attribute.INT64 or attribute.FLOAT64). func (v MetricValue) Type() attribute.Type { return v.value.Type() } // Int64 returns the value as int64 if it holds an int64. // The second return value indicates whether the type matched. func (v MetricValue) Int64() (int64, bool) { if v.value.Type() == attribute.INT64 { return v.value.AsInt64(), true } return 0, false } // Float64 returns the value as float64 if it holds a float64. // The second return value indicates whether the type matched. func (v MetricValue) Float64() (float64, bool) { if v.value.Type() == attribute.FLOAT64 { return v.value.AsFloat64(), true } return 0, false } // AsInterface returns the value as int64 or float64. // Use type assertion or type switch to handle the result. func (v MetricValue) AsInterface() any { return v.value.AsInterface() } // MarshalJSON serializes the value as a bare number. func (v MetricValue) MarshalJSON() ([]byte, error) { return json.Marshal(v.value.AsInterface()) } ================================================ FILE: vendor/github.com/getsentry/sentry-go/internal/debug/transport.go ================================================ package debug import ( "bytes" "fmt" "io" "net/http" "net/http/httptrace" "net/http/httputil" ) // Transport implements http.RoundTripper and can be used to wrap other HTTP // transports for debugging, normally http.DefaultTransport. type Transport struct { http.RoundTripper Output io.Writer // Dump controls whether to dump HTTP request and responses. Dump bool // Trace enables usage of net/http/httptrace. Trace bool } func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { var buf bytes.Buffer if t.Dump { b, err := httputil.DumpRequestOut(req, true) if err != nil { panic(err) } _, err = buf.Write(ensureTrailingNewline(b)) if err != nil { panic(err) } } if t.Trace { trace := &httptrace.ClientTrace{ DNSDone: func(di httptrace.DNSDoneInfo) { fmt.Fprintf(&buf, "* DNS %v → %v\n", req.Host, di.Addrs) }, GotConn: func(ci httptrace.GotConnInfo) { fmt.Fprintf(&buf, "* Connection local=%v remote=%v", ci.Conn.LocalAddr(), ci.Conn.RemoteAddr()) if ci.Reused { fmt.Fprint(&buf, " (reused)") } if ci.WasIdle { fmt.Fprintf(&buf, " (idle %v)", ci.IdleTime) } fmt.Fprintln(&buf) }, } req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace)) } resp, err := t.RoundTripper.RoundTrip(req) if err != nil { return nil, err } if t.Dump { b, err := httputil.DumpResponse(resp, true) if err != nil { panic(err) } _, err = buf.Write(ensureTrailingNewline(b)) if err != nil { panic(err) } } _, err = io.Copy(t.Output, &buf) if err != nil { panic(err) } return resp, nil } func ensureTrailingNewline(b []byte) []byte { if len(b) > 0 && b[len(b)-1] != '\n' { b = append(b, '\n') } return b } ================================================ FILE: vendor/github.com/getsentry/sentry-go/internal/debuglog/log.go ================================================ package debuglog import ( "io" "log" ) // logger is the global debug logger instance. var logger = log.New(io.Discard, "[Sentry] ", log.LstdFlags) // SetOutput changes the output destination of the logger. func SetOutput(w io.Writer) { logger.SetOutput(w) } // GetLogger returns the current logger instance. // This function is thread-safe and can be called concurrently. func GetLogger() *log.Logger { return logger } // Printf calls Printf on the underlying logger. func Printf(format string, args ...interface{}) { logger.Printf(format, args...) } // Println calls Println on the underlying logger. func Println(args ...interface{}) { logger.Println(args...) } // Print calls Print on the underlying logger. func Print(args ...interface{}) { logger.Print(args...) } ================================================ FILE: vendor/github.com/getsentry/sentry-go/internal/http/transport.go ================================================ package http import ( "bytes" "context" "crypto/tls" "crypto/x509" "errors" "fmt" "io" "net/http" "net/url" "sync" "sync/atomic" "time" "github.com/getsentry/sentry-go/internal/debuglog" "github.com/getsentry/sentry-go/internal/protocol" "github.com/getsentry/sentry-go/internal/ratelimit" "github.com/getsentry/sentry-go/internal/util" ) const ( apiVersion = 7 defaultTimeout = time.Second * 30 defaultQueueSize = 1000 ) var ( ErrTransportQueueFull = errors.New("transport queue full") ErrTransportClosed = errors.New("transport is closed") ErrEmptyEnvelope = errors.New("empty envelope provided") ) type TransportOptions struct { Dsn string HTTPClient *http.Client HTTPTransport http.RoundTripper HTTPProxy string HTTPSProxy string CaCerts *x509.CertPool } func getProxyConfig(options TransportOptions) func(*http.Request) (*url.URL, error) { if len(options.HTTPSProxy) > 0 { return func(*http.Request) (*url.URL, error) { return url.Parse(options.HTTPSProxy) } } if len(options.HTTPProxy) > 0 { return func(*http.Request) (*url.URL, error) { return url.Parse(options.HTTPProxy) } } return http.ProxyFromEnvironment } func getTLSConfig(options TransportOptions) *tls.Config { if options.CaCerts != nil { return &tls.Config{ RootCAs: options.CaCerts, MinVersion: tls.VersionTLS12, } } return nil } func getSentryRequestFromEnvelope(ctx context.Context, dsn *protocol.Dsn, envelope *protocol.Envelope) (r *http.Request, err error) { defer func() { if r != nil { sdkName := envelope.Header.Sdk.Name sdkVersion := envelope.Header.Sdk.Version r.Header.Set("User-Agent", fmt.Sprintf("%s/%s", sdkName, sdkVersion)) r.Header.Set("Content-Type", "application/x-sentry-envelope") auth := fmt.Sprintf("Sentry sentry_version=%d, "+ "sentry_client=%s/%s, sentry_key=%s", apiVersion, sdkName, sdkVersion, dsn.GetPublicKey()) if dsn.GetSecretKey() != "" { auth = fmt.Sprintf("%s, sentry_secret=%s", auth, dsn.GetSecretKey()) } r.Header.Set("X-Sentry-Auth", auth) } }() var buf bytes.Buffer _, err = envelope.WriteTo(&buf) if err != nil { return nil, err } return http.NewRequestWithContext( ctx, http.MethodPost, dsn.GetAPIURL().String(), &buf, ) } func categoryFromEnvelope(envelope *protocol.Envelope) ratelimit.Category { if envelope == nil || len(envelope.Items) == 0 { return ratelimit.CategoryAll } for _, item := range envelope.Items { if item == nil || item.Header == nil { continue } switch item.Header.Type { case protocol.EnvelopeItemTypeEvent: return ratelimit.CategoryError case protocol.EnvelopeItemTypeTransaction: return ratelimit.CategoryTransaction case protocol.EnvelopeItemTypeCheckIn: return ratelimit.CategoryMonitor case protocol.EnvelopeItemTypeLog: return ratelimit.CategoryLog case protocol.EnvelopeItemTypeAttachment: continue default: return ratelimit.CategoryAll } } return ratelimit.CategoryAll } // SyncTransport is a blocking implementation of Transport. // // Clients using this transport will send requests to Sentry sequentially and // block until a response is returned. // // The blocking behavior is useful in a limited set of use cases. For example, // use it when deploying code to a Function as a Service ("Serverless") // platform, where any work happening in a background goroutine is not // guaranteed to execute. // // For most cases, prefer AsyncTransport. type SyncTransport struct { dsn *protocol.Dsn client *http.Client transport http.RoundTripper mu sync.Mutex limits ratelimit.Map Timeout time.Duration } func NewSyncTransport(options TransportOptions) protocol.TelemetryTransport { dsn, err := protocol.NewDsn(options.Dsn) if err != nil || dsn == nil { debuglog.Printf("Transport is disabled: invalid dsn: %v\n", err) return NewNoopTransport() } transport := &SyncTransport{ Timeout: defaultTimeout, limits: make(ratelimit.Map), dsn: dsn, } if options.HTTPTransport != nil { transport.transport = options.HTTPTransport } else { transport.transport = &http.Transport{ Proxy: getProxyConfig(options), TLSClientConfig: getTLSConfig(options), } } if options.HTTPClient != nil { transport.client = options.HTTPClient } else { transport.client = &http.Client{ Transport: transport.transport, Timeout: transport.Timeout, } } return transport } func (t *SyncTransport) SendEnvelope(envelope *protocol.Envelope) error { return t.SendEnvelopeWithContext(context.Background(), envelope) } func (t *SyncTransport) Close() {} func (t *SyncTransport) IsRateLimited(category ratelimit.Category) bool { return t.disabled(category) } func (t *SyncTransport) HasCapacity() bool { return true } func (t *SyncTransport) SendEnvelopeWithContext(ctx context.Context, envelope *protocol.Envelope) error { if envelope == nil || len(envelope.Items) == 0 { return ErrEmptyEnvelope } category := categoryFromEnvelope(envelope) if t.disabled(category) { return nil } request, err := getSentryRequestFromEnvelope(ctx, t.dsn, envelope) if err != nil { debuglog.Printf("There was an issue creating the request: %v", err) return err } identifier := util.EnvelopeIdentifier(envelope) debuglog.Printf( "Sending %s to %s project: %s", identifier, t.dsn.GetHost(), t.dsn.GetProjectID(), ) response, err := t.client.Do(request) if err != nil { debuglog.Printf("There was an issue with sending an event: %v", err) return err } util.HandleHTTPResponse(response, identifier) t.mu.Lock() if t.limits == nil { t.limits = make(ratelimit.Map) } t.limits.Merge(ratelimit.FromResponse(response)) t.mu.Unlock() _, _ = io.CopyN(io.Discard, response.Body, util.MaxDrainResponseBytes) return response.Body.Close() } func (t *SyncTransport) Flush(_ time.Duration) bool { return true } func (t *SyncTransport) FlushWithContext(_ context.Context) bool { return true } func (t *SyncTransport) disabled(c ratelimit.Category) bool { t.mu.Lock() defer t.mu.Unlock() disabled := t.limits.IsRateLimited(c) if disabled { debuglog.Printf("Too many requests for %q, backing off till: %v", c, t.limits.Deadline(c)) } return disabled } // AsyncTransport is the default, non-blocking, implementation of Transport. // // Clients using this transport will enqueue requests in a queue and return to // the caller before any network communication has happened. Requests are sent // to Sentry sequentially from a background goroutine. type AsyncTransport struct { dsn *protocol.Dsn client *http.Client transport http.RoundTripper queue chan *protocol.Envelope mu sync.RWMutex limits ratelimit.Map done chan struct{} wg sync.WaitGroup flushRequest chan chan struct{} sentCount int64 droppedCount int64 errorCount int64 QueueSize int Timeout time.Duration startOnce sync.Once closeOnce sync.Once } func NewAsyncTransport(options TransportOptions) protocol.TelemetryTransport { dsn, err := protocol.NewDsn(options.Dsn) if err != nil || dsn == nil { debuglog.Printf("Transport is disabled: invalid dsn: %v", err) return NewNoopTransport() } transport := &AsyncTransport{ QueueSize: defaultQueueSize, Timeout: defaultTimeout, done: make(chan struct{}), limits: make(ratelimit.Map), dsn: dsn, } transport.queue = make(chan *protocol.Envelope, transport.QueueSize) transport.flushRequest = make(chan chan struct{}) if options.HTTPTransport != nil { transport.transport = options.HTTPTransport } else { transport.transport = &http.Transport{ Proxy: getProxyConfig(options), TLSClientConfig: getTLSConfig(options), } } if options.HTTPClient != nil { transport.client = options.HTTPClient } else { transport.client = &http.Client{ Transport: transport.transport, Timeout: transport.Timeout, } } transport.start() return transport } func (t *AsyncTransport) start() { t.startOnce.Do(func() { t.wg.Add(1) go t.worker() }) } // HasCapacity reports whether the async transport queue appears to have space // for at least one more envelope. This is a best-effort, non-blocking check. func (t *AsyncTransport) HasCapacity() bool { t.mu.RLock() defer t.mu.RUnlock() select { case <-t.done: return false default: } return len(t.queue) < cap(t.queue) } func (t *AsyncTransport) SendEnvelope(envelope *protocol.Envelope) error { select { case <-t.done: return ErrTransportClosed default: } if envelope == nil || len(envelope.Items) == 0 { return ErrEmptyEnvelope } category := categoryFromEnvelope(envelope) if t.isRateLimited(category) { return nil } select { case t.queue <- envelope: identifier := util.EnvelopeIdentifier(envelope) debuglog.Printf( "Sending %s to %s project: %s", identifier, t.dsn.GetHost(), t.dsn.GetProjectID(), ) return nil default: atomic.AddInt64(&t.droppedCount, 1) return ErrTransportQueueFull } } func (t *AsyncTransport) Flush(timeout time.Duration) bool { ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() return t.FlushWithContext(ctx) } func (t *AsyncTransport) FlushWithContext(ctx context.Context) bool { flushResponse := make(chan struct{}) select { case t.flushRequest <- flushResponse: select { case <-flushResponse: debuglog.Println("Buffer flushed successfully.") return true case <-ctx.Done(): debuglog.Println("Failed to flush, buffer timed out.") return false } case <-ctx.Done(): debuglog.Println("Failed to flush, buffer timed out.") return false } } func (t *AsyncTransport) Close() { t.closeOnce.Do(func() { close(t.done) close(t.queue) close(t.flushRequest) t.wg.Wait() }) } func (t *AsyncTransport) IsRateLimited(category ratelimit.Category) bool { return t.isRateLimited(category) } func (t *AsyncTransport) worker() { defer t.wg.Done() for { select { case <-t.done: return case envelope, open := <-t.queue: if !open { return } t.processEnvelope(envelope) case flushResponse, open := <-t.flushRequest: if !open { return } t.drainQueue() close(flushResponse) } } } func (t *AsyncTransport) drainQueue() { for { select { case envelope, open := <-t.queue: if !open { return } t.processEnvelope(envelope) default: return } } } func (t *AsyncTransport) processEnvelope(envelope *protocol.Envelope) { if t.sendEnvelopeHTTP(envelope) { atomic.AddInt64(&t.sentCount, 1) } else { atomic.AddInt64(&t.errorCount, 1) } } func (t *AsyncTransport) sendEnvelopeHTTP(envelope *protocol.Envelope) bool { category := categoryFromEnvelope(envelope) if t.isRateLimited(category) { return false } ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout) defer cancel() request, err := getSentryRequestFromEnvelope(ctx, t.dsn, envelope) if err != nil { debuglog.Printf("Failed to create request from envelope: %v", err) return false } response, err := t.client.Do(request) if err != nil { debuglog.Printf("HTTP request failed: %v", err) return false } defer response.Body.Close() identifier := util.EnvelopeIdentifier(envelope) success := util.HandleHTTPResponse(response, identifier) t.mu.Lock() if t.limits == nil { t.limits = make(ratelimit.Map) } t.limits.Merge(ratelimit.FromResponse(response)) t.mu.Unlock() _, _ = io.CopyN(io.Discard, response.Body, util.MaxDrainResponseBytes) return success } func (t *AsyncTransport) isRateLimited(category ratelimit.Category) bool { t.mu.RLock() defer t.mu.RUnlock() limited := t.limits.IsRateLimited(category) if limited { debuglog.Printf("Rate limited for category %q until %v", category, t.limits.Deadline(category)) } return limited } // NoopTransport is a transport implementation that drops all events. // Used internally when an empty or invalid DSN is provided. type NoopTransport struct{} func NewNoopTransport() *NoopTransport { debuglog.Println("Transport initialized with invalid DSN. Using NoopTransport. No events will be delivered.") return &NoopTransport{} } func (t *NoopTransport) SendEnvelope(_ *protocol.Envelope) error { debuglog.Println("Envelope dropped due to NoopTransport usage.") return nil } func (t *NoopTransport) IsRateLimited(_ ratelimit.Category) bool { return false } func (t *NoopTransport) Flush(_ time.Duration) bool { return true } func (t *NoopTransport) FlushWithContext(_ context.Context) bool { return true } func (t *NoopTransport) Close() { // Nothing to close } func (t *NoopTransport) HasCapacity() bool { return true } ================================================ FILE: vendor/github.com/getsentry/sentry-go/internal/otel/baggage/README.md ================================================ ## Why do we have this "otel/baggage" folder? The root sentry-go SDK (namely, the Dynamic Sampling functionality) needs an implementation of the [baggage spec](https://www.w3.org/TR/baggage/). For that reason, we've taken the existing baggage implementation from the [opentelemetry-go](https://github.com/open-telemetry/opentelemetry-go/) repository, and fixed a few things that in our opinion were violating the specification. These issues are: 1. Baggage string value `one%20two` should be properly parsed as "one two" 1. Baggage string value `one+two` should be parsed as "one+two" 1. Go string value "one two" should be encoded as `one%20two` (percent encoding), and NOT as `one+two` (URL query encoding). 1. Go string value "1=1" might be encoded as `1=1`, because the spec says: "Note, value MAY contain any number of the equal sign (=) characters. Parsers MUST NOT assume that the equal sign is only used to separate key and value.". `1%3D1` is also valid, but to simplify the implementation we're not doing it. Changes were made in this PR: https://github.com/getsentry/sentry-go/pull/568 ================================================ FILE: vendor/github.com/getsentry/sentry-go/internal/otel/baggage/baggage.go ================================================ // Adapted from https://github.com/open-telemetry/opentelemetry-go/blob/c21b6b6bb31a2f74edd06e262f1690f3f6ea3d5c/baggage/baggage.go // // Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package baggage import ( "errors" "fmt" "net/url" "regexp" "strings" "unicode/utf8" "github.com/getsentry/sentry-go/internal/otel/baggage/internal/baggage" ) const ( maxMembers = 180 maxBytesPerMembers = 4096 maxBytesPerBaggageString = 8192 listDelimiter = "," keyValueDelimiter = "=" propertyDelimiter = ";" keyDef = `([\x21\x23-\x27\x2A\x2B\x2D\x2E\x30-\x39\x41-\x5a\x5e-\x7a\x7c\x7e]+)` valueDef = `([\x21\x23-\x2b\x2d-\x3a\x3c-\x5B\x5D-\x7e]*)` keyValueDef = `\s*` + keyDef + `\s*` + keyValueDelimiter + `\s*` + valueDef + `\s*` ) var ( keyRe = regexp.MustCompile(`^` + keyDef + `$`) valueRe = regexp.MustCompile(`^` + valueDef + `$`) propertyRe = regexp.MustCompile(`^(?:\s*` + keyDef + `\s*|` + keyValueDef + `)$`) ) var ( errInvalidKey = errors.New("invalid key") errInvalidValue = errors.New("invalid value") errInvalidProperty = errors.New("invalid baggage list-member property") errInvalidMember = errors.New("invalid baggage list-member") errMemberNumber = errors.New("too many list-members in baggage-string") errMemberBytes = errors.New("list-member too large") errBaggageBytes = errors.New("baggage-string too large") ) // Property is an additional metadata entry for a baggage list-member. type Property struct { key, value string // hasValue indicates if a zero-value value means the property does not // have a value or if it was the zero-value. hasValue bool // hasData indicates whether the created property contains data or not. // Properties that do not contain data are invalid with no other check // required. hasData bool } // NewKeyProperty returns a new Property for key. // // If key is invalid, an error will be returned. func NewKeyProperty(key string) (Property, error) { if !keyRe.MatchString(key) { return newInvalidProperty(), fmt.Errorf("%w: %q", errInvalidKey, key) } p := Property{key: key, hasData: true} return p, nil } // NewKeyValueProperty returns a new Property for key with value. // // If key or value are invalid, an error will be returned. func NewKeyValueProperty(key, value string) (Property, error) { if !keyRe.MatchString(key) { return newInvalidProperty(), fmt.Errorf("%w: %q", errInvalidKey, key) } if !valueRe.MatchString(value) { return newInvalidProperty(), fmt.Errorf("%w: %q", errInvalidValue, value) } p := Property{ key: key, value: value, hasValue: true, hasData: true, } return p, nil } func newInvalidProperty() Property { return Property{} } // parseProperty attempts to decode a Property from the passed string. It // returns an error if the input is invalid according to the W3C Baggage // specification. func parseProperty(property string) (Property, error) { if property == "" { return newInvalidProperty(), nil } match := propertyRe.FindStringSubmatch(property) if len(match) != 4 { return newInvalidProperty(), fmt.Errorf("%w: %q", errInvalidProperty, property) } p := Property{hasData: true} if match[1] != "" { p.key = match[1] } else { p.key = match[2] p.value = match[3] p.hasValue = true } return p, nil } // validate ensures p conforms to the W3C Baggage specification, returning an // error otherwise. func (p Property) validate() error { errFunc := func(err error) error { return fmt.Errorf("invalid property: %w", err) } if !p.hasData { return errFunc(fmt.Errorf("%w: %q", errInvalidProperty, p)) } if !keyRe.MatchString(p.key) { return errFunc(fmt.Errorf("%w: %q", errInvalidKey, p.key)) } if p.hasValue && !valueRe.MatchString(p.value) { return errFunc(fmt.Errorf("%w: %q", errInvalidValue, p.value)) } if !p.hasValue && p.value != "" { return errFunc(errors.New("inconsistent value")) } return nil } // Key returns the Property key. func (p Property) Key() string { return p.key } // Value returns the Property value. Additionally, a boolean value is returned // indicating if the returned value is the empty if the Property has a value // that is empty or if the value is not set. func (p Property) Value() (string, bool) { return p.value, p.hasValue } // String encodes Property into a string compliant with the W3C Baggage // specification. func (p Property) String() string { if p.hasValue { return fmt.Sprintf("%s%s%v", p.key, keyValueDelimiter, p.value) } return p.key } type properties []Property func fromInternalProperties(iProps []baggage.Property) properties { if len(iProps) == 0 { return nil } props := make(properties, len(iProps)) for i, p := range iProps { props[i] = Property{ key: p.Key, value: p.Value, hasValue: p.HasValue, } } return props } func (p properties) asInternal() []baggage.Property { if len(p) == 0 { return nil } iProps := make([]baggage.Property, len(p)) for i, prop := range p { iProps[i] = baggage.Property{ Key: prop.key, Value: prop.value, HasValue: prop.hasValue, } } return iProps } func (p properties) Copy() properties { if len(p) == 0 { return nil } props := make(properties, len(p)) copy(props, p) return props } // validate ensures each Property in p conforms to the W3C Baggage // specification, returning an error otherwise. func (p properties) validate() error { for _, prop := range p { if err := prop.validate(); err != nil { return err } } return nil } // String encodes properties into a string compliant with the W3C Baggage // specification. func (p properties) String() string { props := make([]string, len(p)) for i, prop := range p { props[i] = prop.String() } return strings.Join(props, propertyDelimiter) } // Member is a list-member of a baggage-string as defined by the W3C Baggage // specification. type Member struct { key, value string properties properties // hasData indicates whether the created property contains data or not. // Properties that do not contain data are invalid with no other check // required. hasData bool } // NewMember returns a new Member from the passed arguments. The key will be // used directly while the value will be url decoded after validation. An error // is returned if the created Member would be invalid according to the W3C // Baggage specification. func NewMember(key, value string, props ...Property) (Member, error) { m := Member{ key: key, value: value, properties: properties(props).Copy(), hasData: true, } if err := m.validate(); err != nil { return newInvalidMember(), err } //// NOTE(anton): I don't think we need to unescape here // decodedValue, err := url.PathUnescape(value) // if err != nil { // return newInvalidMember(), fmt.Errorf("%w: %q", errInvalidValue, value) // } // m.value = decodedValue return m, nil } func newInvalidMember() Member { return Member{} } // parseMember attempts to decode a Member from the passed string. It returns // an error if the input is invalid according to the W3C Baggage // specification. func parseMember(member string) (Member, error) { if n := len(member); n > maxBytesPerMembers { return newInvalidMember(), fmt.Errorf("%w: %d", errMemberBytes, n) } var ( key, value string props properties ) parts := strings.SplitN(member, propertyDelimiter, 2) switch len(parts) { case 2: // Parse the member properties. for _, pStr := range strings.Split(parts[1], propertyDelimiter) { p, err := parseProperty(pStr) if err != nil { return newInvalidMember(), err } props = append(props, p) } fallthrough case 1: // Parse the member key/value pair. // Take into account a value can contain equal signs (=). kv := strings.SplitN(parts[0], keyValueDelimiter, 2) if len(kv) != 2 { return newInvalidMember(), fmt.Errorf("%w: %q", errInvalidMember, member) } // "Leading and trailing whitespaces are allowed but MUST be trimmed // when converting the header into a data structure." key = strings.TrimSpace(kv[0]) value = strings.TrimSpace(kv[1]) var err error if !keyRe.MatchString(key) { return newInvalidMember(), fmt.Errorf("%w: %q", errInvalidKey, key) } if !valueRe.MatchString(value) { return newInvalidMember(), fmt.Errorf("%w: %q", errInvalidValue, value) } decodedValue, err := url.PathUnescape(value) if err != nil { return newInvalidMember(), fmt.Errorf("%w: %q", err, value) } value = decodedValue default: // This should never happen unless a developer has changed the string // splitting somehow. Panic instead of failing silently and allowing // the bug to slip past the CI checks. panic("failed to parse baggage member") } return Member{key: key, value: value, properties: props, hasData: true}, nil } // validate ensures m conforms to the W3C Baggage specification. // A key is just an ASCII string, but a value must be URL encoded UTF-8, // returning an error otherwise. func (m Member) validate() error { if !m.hasData { return fmt.Errorf("%w: %q", errInvalidMember, m) } if !keyRe.MatchString(m.key) { return fmt.Errorf("%w: %q", errInvalidKey, m.key) } //// NOTE(anton): IMO it's too early to validate the value here. // if !valueRe.MatchString(m.value) { // return fmt.Errorf("%w: %q", errInvalidValue, m.value) // } return m.properties.validate() } // Key returns the Member key. func (m Member) Key() string { return m.key } // Value returns the Member value. func (m Member) Value() string { return m.value } // Properties returns a copy of the Member properties. func (m Member) Properties() []Property { return m.properties.Copy() } // String encodes Member into a string compliant with the W3C Baggage // specification. func (m Member) String() string { // A key is just an ASCII string, but a value is URL encoded UTF-8. s := fmt.Sprintf("%s%s%s", m.key, keyValueDelimiter, percentEncodeValue(m.value)) if len(m.properties) > 0 { s = fmt.Sprintf("%s%s%s", s, propertyDelimiter, m.properties.String()) } return s } // percentEncodeValue encodes the baggage value, using percent-encoding for // disallowed octets. func percentEncodeValue(s string) string { const upperhex = "0123456789ABCDEF" var sb strings.Builder for byteIndex, width := 0, 0; byteIndex < len(s); byteIndex += width { runeValue, w := utf8.DecodeRuneInString(s[byteIndex:]) width = w char := string(runeValue) if valueRe.MatchString(char) && char != "%" { // The character is returned as is, no need to percent-encode sb.WriteString(char) } else { // We need to percent-encode each byte of the multi-octet character for j := 0; j < width; j++ { b := s[byteIndex+j] sb.WriteByte('%') // Bitwise operations are inspired by "net/url" sb.WriteByte(upperhex[b>>4]) sb.WriteByte(upperhex[b&15]) } } } return sb.String() } // Baggage is a list of baggage members representing the baggage-string as // defined by the W3C Baggage specification. type Baggage struct { //nolint:golint list baggage.List } // New returns a new valid Baggage. It returns an error if it results in a // Baggage exceeding limits set in that specification. // // It expects all the provided members to have already been validated. func New(members ...Member) (Baggage, error) { if len(members) == 0 { return Baggage{}, nil } b := make(baggage.List) for _, m := range members { if !m.hasData { return Baggage{}, errInvalidMember } // OpenTelemetry resolves duplicates by last-one-wins. b[m.key] = baggage.Item{ Value: m.value, Properties: m.properties.asInternal(), } } // Check member numbers after deduplication. if len(b) > maxMembers { return Baggage{}, errMemberNumber } bag := Baggage{b} if n := len(bag.String()); n > maxBytesPerBaggageString { return Baggage{}, fmt.Errorf("%w: %d", errBaggageBytes, n) } return bag, nil } // Parse attempts to decode a baggage-string from the passed string. It // returns an error if the input is invalid according to the W3C Baggage // specification. // // If there are duplicate list-members contained in baggage, the last one // defined (reading left-to-right) will be the only one kept. This diverges // from the W3C Baggage specification which allows duplicate list-members, but // conforms to the OpenTelemetry Baggage specification. func Parse(bStr string) (Baggage, error) { if bStr == "" { return Baggage{}, nil } if n := len(bStr); n > maxBytesPerBaggageString { return Baggage{}, fmt.Errorf("%w: %d", errBaggageBytes, n) } b := make(baggage.List) for _, memberStr := range strings.Split(bStr, listDelimiter) { m, err := parseMember(memberStr) if err != nil { return Baggage{}, err } // OpenTelemetry resolves duplicates by last-one-wins. b[m.key] = baggage.Item{ Value: m.value, Properties: m.properties.asInternal(), } } // OpenTelemetry does not allow for duplicate list-members, but the W3C // specification does. Now that we have deduplicated, ensure the baggage // does not exceed list-member limits. if len(b) > maxMembers { return Baggage{}, errMemberNumber } return Baggage{b}, nil } // Member returns the baggage list-member identified by key. // // If there is no list-member matching the passed key the returned Member will // be a zero-value Member. // The returned member is not validated, as we assume the validation happened // when it was added to the Baggage. func (b Baggage) Member(key string) Member { v, ok := b.list[key] if !ok { // We do not need to worry about distinguishing between the situation // where a zero-valued Member is included in the Baggage because a // zero-valued Member is invalid according to the W3C Baggage // specification (it has an empty key). return newInvalidMember() } return Member{ key: key, value: v.Value, properties: fromInternalProperties(v.Properties), hasData: true, } } // Members returns all the baggage list-members. // The order of the returned list-members does not have significance. // // The returned members are not validated, as we assume the validation happened // when they were added to the Baggage. func (b Baggage) Members() []Member { if len(b.list) == 0 { return nil } members := make([]Member, 0, len(b.list)) for k, v := range b.list { members = append(members, Member{ key: k, value: v.Value, properties: fromInternalProperties(v.Properties), hasData: true, }) } return members } // SetMember returns a copy the Baggage with the member included. If the // baggage contains a Member with the same key the existing Member is // replaced. // // If member is invalid according to the W3C Baggage specification, an error // is returned with the original Baggage. func (b Baggage) SetMember(member Member) (Baggage, error) { if !member.hasData { return b, errInvalidMember } n := len(b.list) if _, ok := b.list[member.key]; !ok { n++ } list := make(baggage.List, n) for k, v := range b.list { // Do not copy if we are just going to overwrite. if k == member.key { continue } list[k] = v } list[member.key] = baggage.Item{ Value: member.value, Properties: member.properties.asInternal(), } return Baggage{list: list}, nil } // DeleteMember returns a copy of the Baggage with the list-member identified // by key removed. func (b Baggage) DeleteMember(key string) Baggage { n := len(b.list) if _, ok := b.list[key]; ok { n-- } list := make(baggage.List, n) for k, v := range b.list { if k == key { continue } list[k] = v } return Baggage{list: list} } // Len returns the number of list-members in the Baggage. func (b Baggage) Len() int { return len(b.list) } // String encodes Baggage into a string compliant with the W3C Baggage // specification. The returned string will be invalid if the Baggage contains // any invalid list-members. func (b Baggage) String() string { members := make([]string, 0, len(b.list)) for k, v := range b.list { members = append(members, Member{ key: k, value: v.Value, properties: fromInternalProperties(v.Properties), }.String()) } return strings.Join(members, listDelimiter) } ================================================ FILE: vendor/github.com/getsentry/sentry-go/internal/otel/baggage/internal/baggage/baggage.go ================================================ // Adapted from https://github.com/open-telemetry/opentelemetry-go/blob/c21b6b6bb31a2f74edd06e262f1690f3f6ea3d5c/internal/baggage/baggage.go // // Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. /* Package baggage provides base types and functionality to store and retrieve baggage in Go context. This package exists because the OpenTracing bridge to OpenTelemetry needs to synchronize state whenever baggage for a context is modified and that context contains an OpenTracing span. If it were not for this need this package would not need to exist and the `go.opentelemetry.io/otel/baggage` package would be the singular place where W3C baggage is handled. */ package baggage // List is the collection of baggage members. The W3C allows for duplicates, // but OpenTelemetry does not, therefore, this is represented as a map. type List map[string]Item // Item is the value and metadata properties part of a list-member. type Item struct { Value string Properties []Property } // Property is a metadata entry for a list-member. type Property struct { Key, Value string // HasValue indicates if a zero-value value means the property does not // have a value or if it was the zero-value. HasValue bool } ================================================ FILE: vendor/github.com/getsentry/sentry-go/internal/protocol/dsn.go ================================================ package protocol import ( "encoding/json" "fmt" "net/url" "strconv" "strings" "time" ) // apiVersion is the version of the Sentry API. const apiVersion = "7" type scheme string const ( SchemeHTTP scheme = "http" SchemeHTTPS scheme = "https" ) func (scheme scheme) defaultPort() int { switch scheme { case SchemeHTTPS: return 443 case SchemeHTTP: return 80 default: return 80 } } // DsnParseError represents an error that occurs if a Sentry // DSN cannot be parsed. type DsnParseError struct { Message string } func (e DsnParseError) Error() string { return "[Sentry] DsnParseError: " + e.Message } // Dsn is used as the remote address source to client transport. type Dsn struct { scheme scheme publicKey string secretKey string host string port int path string projectID string } // NewDsn creates a Dsn by parsing rawURL. Most users will never call this // function directly. It is provided for use in custom Transport // implementations. func NewDsn(rawURL string) (*Dsn, error) { // Parse parsedURL, err := url.Parse(rawURL) if err != nil { return nil, &DsnParseError{fmt.Sprintf("invalid url: %v", err)} } // Scheme var scheme scheme switch parsedURL.Scheme { case "http": scheme = SchemeHTTP case "https": scheme = SchemeHTTPS default: return nil, &DsnParseError{"invalid scheme"} } // PublicKey publicKey := parsedURL.User.Username() if publicKey == "" { return nil, &DsnParseError{"empty username"} } // SecretKey var secretKey string if parsedSecretKey, ok := parsedURL.User.Password(); ok { secretKey = parsedSecretKey } // Host host := parsedURL.Hostname() if host == "" { return nil, &DsnParseError{"empty host"} } // Port var port int if p := parsedURL.Port(); p != "" { port, err = strconv.Atoi(p) if err != nil { return nil, &DsnParseError{"invalid port"} } } else { port = scheme.defaultPort() } // ProjectID if parsedURL.Path == "" || parsedURL.Path == "/" { return nil, &DsnParseError{"empty project id"} } pathSegments := strings.Split(parsedURL.Path[1:], "/") projectID := pathSegments[len(pathSegments)-1] if projectID == "" { return nil, &DsnParseError{"empty project id"} } // Path var path string if len(pathSegments) > 1 { path = "/" + strings.Join(pathSegments[0:len(pathSegments)-1], "/") } return &Dsn{ scheme: scheme, publicKey: publicKey, secretKey: secretKey, host: host, port: port, path: path, projectID: projectID, }, nil } // String formats Dsn struct into a valid string url. func (dsn Dsn) String() string { var url string url += fmt.Sprintf("%s://%s", dsn.scheme, dsn.publicKey) if dsn.secretKey != "" { url += fmt.Sprintf(":%s", dsn.secretKey) } url += fmt.Sprintf("@%s", dsn.host) if dsn.port != dsn.scheme.defaultPort() { url += fmt.Sprintf(":%d", dsn.port) } if dsn.path != "" { url += dsn.path } url += fmt.Sprintf("/%s", dsn.projectID) return url } // Get the scheme of the DSN. func (dsn Dsn) GetScheme() string { return string(dsn.scheme) } // Get the public key of the DSN. func (dsn Dsn) GetPublicKey() string { return dsn.publicKey } // Get the secret key of the DSN. func (dsn Dsn) GetSecretKey() string { return dsn.secretKey } // Get the host of the DSN. func (dsn Dsn) GetHost() string { return dsn.host } // Get the port of the DSN. func (dsn Dsn) GetPort() int { return dsn.port } // Get the path of the DSN. func (dsn Dsn) GetPath() string { return dsn.path } // Get the project ID of the DSN. func (dsn Dsn) GetProjectID() string { return dsn.projectID } // GetAPIURL returns the URL of the envelope endpoint of the project // associated with the DSN. func (dsn Dsn) GetAPIURL() *url.URL { var rawURL string rawURL += fmt.Sprintf("%s://%s", dsn.scheme, dsn.host) if dsn.port != dsn.scheme.defaultPort() { rawURL += fmt.Sprintf(":%d", dsn.port) } if dsn.path != "" { rawURL += dsn.path } rawURL += fmt.Sprintf("/api/%s/%s/", dsn.projectID, "envelope") parsedURL, _ := url.Parse(rawURL) return parsedURL } // RequestHeaders returns all the necessary headers that have to be used in the transport when sending events // to the /store endpoint. // // Deprecated: This method shall only be used if you want to implement your own transport that sends events to // the /store endpoint. If you're using the transport provided by the SDK, all necessary headers to authenticate // against the /envelope endpoint are added automatically. func (dsn Dsn) RequestHeaders(sdkVersion string) map[string]string { auth := fmt.Sprintf("Sentry sentry_version=%s, sentry_timestamp=%d, "+ "sentry_client=sentry.go/%s, sentry_key=%s", apiVersion, time.Now().Unix(), sdkVersion, dsn.publicKey) if dsn.secretKey != "" { auth = fmt.Sprintf("%s, sentry_secret=%s", auth, dsn.secretKey) } return map[string]string{ "Content-Type": "application/json", "X-Sentry-Auth": auth, } } // MarshalJSON converts the Dsn struct to JSON. func (dsn Dsn) MarshalJSON() ([]byte, error) { return json.Marshal(dsn.String()) } // UnmarshalJSON converts JSON data to the Dsn struct. func (dsn *Dsn) UnmarshalJSON(data []byte) error { var str string _ = json.Unmarshal(data, &str) newDsn, err := NewDsn(str) if err != nil { return err } *dsn = *newDsn return nil } ================================================ FILE: vendor/github.com/getsentry/sentry-go/internal/protocol/envelope.go ================================================ package protocol import ( "bytes" "encoding/json" "fmt" "io" "time" ) // Envelope represents a Sentry envelope containing headers and items. type Envelope struct { Header *EnvelopeHeader `json:"-"` Items []*EnvelopeItem `json:"-"` } // EnvelopeHeader represents the header of a Sentry envelope. type EnvelopeHeader struct { // EventID is the unique identifier for this event EventID string `json:"event_id"` // SentAt is the timestamp when the event was sent from the SDK as string in RFC 3339 format. // Used for clock drift correction of the event timestamp. The time zone must be UTC. SentAt time.Time `json:"sent_at,omitzero"` // Dsn can be used for self-authenticated envelopes. // This means that the envelope has all the information necessary to be sent to sentry. // In this case the full DSN must be stored in this key. Dsn string `json:"dsn,omitempty"` // Sdk carries the same payload as the sdk interface in the event payload but can be carried for all events. // This means that SDK information can be carried for minidumps, session data and other submissions. Sdk *SdkInfo `json:"sdk,omitempty"` // Trace contains the [Dynamic Sampling Context](https://develop.sentry.dev/sdk/telemetry/traces/dynamic-sampling-context/) Trace map[string]string `json:"trace,omitempty"` } // EnvelopeItemType represents the type of envelope item. type EnvelopeItemType string // Constants for envelope item types as defined in the Sentry documentation. const ( EnvelopeItemTypeEvent EnvelopeItemType = "event" EnvelopeItemTypeTransaction EnvelopeItemType = "transaction" EnvelopeItemTypeCheckIn EnvelopeItemType = "check_in" EnvelopeItemTypeAttachment EnvelopeItemType = "attachment" EnvelopeItemTypeLog EnvelopeItemType = "log" EnvelopeItemTypeTraceMetric EnvelopeItemType = "trace_metric" ) // EnvelopeItemHeader represents the header of an envelope item. type EnvelopeItemHeader struct { // Type specifies the type of this Item and its contents. // Based on the Item type, more headers may be required. Type EnvelopeItemType `json:"type"` // Length is the length of the payload in bytes. // If no length is specified, the payload implicitly goes to the next newline. // For payloads containing newline characters, the length must be specified. Length *int `json:"length,omitempty"` // Filename is the name of the attachment file (used for attachments) Filename string `json:"filename,omitempty"` // ContentType is the MIME type of the item payload (used for attachments and some other item types) ContentType string `json:"content_type,omitempty"` // ItemCount is the number of items in a batch (used for logs) ItemCount *int `json:"item_count,omitempty"` } // EnvelopeItem represents a single item or batch within an envelope. type EnvelopeItem struct { Header *EnvelopeItemHeader `json:"-"` Payload []byte `json:"-"` } // NewEnvelope creates a new envelope with the given header. func NewEnvelope(header *EnvelopeHeader) *Envelope { return &Envelope{ Header: header, Items: make([]*EnvelopeItem, 0), } } // AddItem adds an item to the envelope. func (e *Envelope) AddItem(item *EnvelopeItem) { if item == nil { return } e.Items = append(e.Items, item) } // Serialize serializes the envelope to the Sentry envelope format. // // Format: Headers "\n" { Item } [ "\n" ] // Item: Headers "\n" Payload "\n". func (e *Envelope) Serialize() ([]byte, error) { var buf bytes.Buffer headerBytes, err := json.Marshal(e.Header) if err != nil { return nil, fmt.Errorf("failed to marshal envelope header: %w", err) } if _, err := buf.Write(headerBytes); err != nil { return nil, fmt.Errorf("failed to write envelope header: %w", err) } if _, err := buf.WriteString("\n"); err != nil { return nil, fmt.Errorf("failed to write newline after envelope header: %w", err) } for _, item := range e.Items { if err := e.writeItem(&buf, item); err != nil { return nil, fmt.Errorf("failed to write envelope item: %w", err) } } return buf.Bytes(), nil } // WriteTo writes the envelope to the given writer in the Sentry envelope format. func (e *Envelope) WriteTo(w io.Writer) (int64, error) { data, err := e.Serialize() if err != nil { return 0, err } n, err := w.Write(data) return int64(n), err } // writeItem writes a single envelope item to the buffer. func (e *Envelope) writeItem(buf *bytes.Buffer, item *EnvelopeItem) error { headerBytes, err := json.Marshal(item.Header) if err != nil { return fmt.Errorf("failed to marshal item header: %w", err) } if _, err := buf.Write(headerBytes); err != nil { return fmt.Errorf("failed to write item header: %w", err) } if _, err := buf.WriteString("\n"); err != nil { return fmt.Errorf("failed to write newline after item header: %w", err) } if len(item.Payload) > 0 { if _, err := buf.Write(item.Payload); err != nil { return fmt.Errorf("failed to write item payload: %w", err) } } if _, err := buf.WriteString("\n"); err != nil { return fmt.Errorf("failed to write newline after item payload: %w", err) } return nil } // Size returns the total size of the envelope when serialized. func (e *Envelope) Size() (int, error) { data, err := e.Serialize() if err != nil { return 0, err } return len(data), nil } // NewEnvelopeItem creates a new envelope item with the specified type and payload. func NewEnvelopeItem(itemType EnvelopeItemType, payload []byte) *EnvelopeItem { length := len(payload) return &EnvelopeItem{ Header: &EnvelopeItemHeader{ Type: itemType, Length: &length, }, Payload: payload, } } // NewAttachmentItem creates a new envelope item for an attachment. // Parameters: filename, contentType, payload. func NewAttachmentItem(filename, contentType string, payload []byte) *EnvelopeItem { length := len(payload) return &EnvelopeItem{ Header: &EnvelopeItemHeader{ Type: EnvelopeItemTypeAttachment, Length: &length, ContentType: contentType, Filename: filename, }, Payload: payload, } } // NewLogItem creates a new envelope item for logs. func NewLogItem(itemCount int, payload []byte) *EnvelopeItem { length := len(payload) return &EnvelopeItem{ Header: &EnvelopeItemHeader{ Type: EnvelopeItemTypeLog, Length: &length, ItemCount: &itemCount, ContentType: "application/vnd.sentry.items.log+json", }, Payload: payload, } } // NewTraceMetricItem creates a new envelope item for trace metrics. func NewTraceMetricItem(itemCount int, payload []byte) *EnvelopeItem { length := len(payload) return &EnvelopeItem{ Header: &EnvelopeItemHeader{ Type: EnvelopeItemTypeTraceMetric, Length: &length, ItemCount: &itemCount, ContentType: "application/vnd.sentry.items.trace-metric+json", }, Payload: payload, } } ================================================ FILE: vendor/github.com/getsentry/sentry-go/internal/protocol/interfaces.go ================================================ package protocol import ( "context" "time" "github.com/getsentry/sentry-go/internal/ratelimit" ) // TelemetryItem represents any telemetry data that can be stored in buffers and sent to Sentry. // This is the base interface that all telemetry items must implement. type TelemetryItem interface { // GetCategory returns the rate limit category for this item. GetCategory() ratelimit.Category // GetEventID returns the event ID for this item. GetEventID() string // GetSdkInfo returns SDK information for the envelope header. GetSdkInfo() *SdkInfo // GetDynamicSamplingContext returns trace context for the envelope header. GetDynamicSamplingContext() map[string]string } // EnvelopeItemConvertible represents items that can be converted directly to envelope items. type EnvelopeItemConvertible interface { TelemetryItem // ToEnvelopeItem converts the item to a Sentry envelope item. ToEnvelopeItem() (*EnvelopeItem, error) } // TelemetryTransport represents the envelope-first transport interface. // This interface is designed for the telemetry buffer system and provides // non-blocking sends with backpressure signals. type TelemetryTransport interface { // SendEnvelope sends an envelope to Sentry. Returns immediately with // backpressure error if the queue is full. SendEnvelope(envelope *Envelope) error // HasCapacity reports whether the transport has capacity to accept at least one more envelope. HasCapacity() bool // IsRateLimited checks if a specific category is currently rate limited IsRateLimited(category ratelimit.Category) bool // Flush waits for all pending envelopes to be sent, with timeout Flush(timeout time.Duration) bool // FlushWithContext waits for all pending envelopes to be sent FlushWithContext(ctx context.Context) bool // Close shuts down the transport gracefully Close() } ================================================ FILE: vendor/github.com/getsentry/sentry-go/internal/protocol/log_batch.go ================================================ package protocol import ( "encoding/json" "github.com/getsentry/sentry-go/internal/ratelimit" ) // LogAttribute is the JSON representation for a single log attribute value. type LogAttribute struct { Value any `json:"value"` Type string `json:"type"` } // Logs is a container for multiple log items which knows how to convert // itself into a single batched log envelope item. type Logs []TelemetryItem func (ls Logs) ToEnvelopeItem() (*EnvelopeItem, error) { // Convert each log to its JSON representation items := make([]json.RawMessage, 0, len(ls)) for _, log := range ls { logPayload, err := json.Marshal(log) if err != nil { continue } items = append(items, logPayload) } if len(items) == 0 { return nil, nil } wrapper := struct { Items []json.RawMessage `json:"items"` }{Items: items} payload, err := json.Marshal(wrapper) if err != nil { return nil, err } return NewLogItem(len(ls), payload), nil } func (Logs) GetCategory() ratelimit.Category { return ratelimit.CategoryLog } func (Logs) GetEventID() string { return "" } func (Logs) GetSdkInfo() *SdkInfo { return nil } func (Logs) GetDynamicSamplingContext() map[string]string { return nil } ================================================ FILE: vendor/github.com/getsentry/sentry-go/internal/protocol/metric_batch.go ================================================ package protocol import ( "encoding/json" "github.com/getsentry/sentry-go/internal/ratelimit" ) type Metrics []TelemetryItem func (ms Metrics) ToEnvelopeItem() (*EnvelopeItem, error) { // Convert each metric to its JSON representation items := make([]json.RawMessage, 0, len(ms)) for _, metric := range ms { metricPayload, err := json.Marshal(metric) if err != nil { continue } items = append(items, metricPayload) } if len(items) == 0 { return nil, nil } wrapper := struct { Items []json.RawMessage `json:"items"` }{Items: items} payload, err := json.Marshal(wrapper) if err != nil { return nil, err } return NewTraceMetricItem(len(items), payload), nil } func (Metrics) GetCategory() ratelimit.Category { return ratelimit.CategoryTraceMetric } func (Metrics) GetEventID() string { return "" } func (Metrics) GetSdkInfo() *SdkInfo { return nil } func (Metrics) GetDynamicSamplingContext() map[string]string { return nil } ================================================ FILE: vendor/github.com/getsentry/sentry-go/internal/protocol/types.go ================================================ package protocol // SdkInfo contains SDK metadata. type SdkInfo struct { Name string `json:"name,omitempty"` Version string `json:"version,omitempty"` Integrations []string `json:"integrations,omitempty"` Packages []SdkPackage `json:"packages,omitempty"` } // SdkPackage describes a package that was installed. type SdkPackage struct { Name string `json:"name,omitempty"` Version string `json:"version,omitempty"` } ================================================ FILE: vendor/github.com/getsentry/sentry-go/internal/protocol/uuid.go ================================================ package protocol import ( "crypto/rand" "encoding/hex" ) // GenerateEventID generates a random UUID v4 for use as a Sentry event ID. func GenerateEventID() string { id := make([]byte, 16) // Prefer rand.Read over rand.Reader, see https://go-review.googlesource.com/c/go/+/272326/. _, _ = rand.Read(id) id[6] &= 0x0F // clear version id[6] |= 0x40 // set version to 4 (random uuid) id[8] &= 0x3F // clear variant id[8] |= 0x80 // set to IETF variant return hex.EncodeToString(id) } ================================================ FILE: vendor/github.com/getsentry/sentry-go/internal/ratelimit/category.go ================================================ package ratelimit import ( "strings" "golang.org/x/text/cases" "golang.org/x/text/language" ) // Reference: // https://github.com/getsentry/relay/blob/46dfaa850b8717a6e22c3e9a275ba17fe673b9da/relay-base-schema/src/data_category.rs#L231-L271 // Category classifies supported payload types that can be ingested by Sentry // and, therefore, rate limited. type Category string // Known rate limit categories that are specified in rate limit headers. const ( CategoryUnknown Category = "unknown" // Unknown category should not get rate limited CategoryAll Category = "" // Special category for empty categories (applies to all) CategoryError Category = "error" CategoryTransaction Category = "transaction" CategoryLog Category = "log_item" CategoryMonitor Category = "monitor" CategoryTraceMetric Category = "trace_metric" ) // knownCategories is the set of currently known categories. Other categories // are ignored for the purpose of rate-limiting. var knownCategories = map[Category]struct{}{ CategoryAll: {}, CategoryError: {}, CategoryTransaction: {}, CategoryLog: {}, CategoryMonitor: {}, CategoryTraceMetric: {}, } // String returns the category formatted for debugging. func (c Category) String() string { switch c { case CategoryAll: return "CategoryAll" case CategoryError: return "CategoryError" case CategoryTransaction: return "CategoryTransaction" case CategoryLog: return "CategoryLog" case CategoryMonitor: return "CategoryMonitor" case CategoryTraceMetric: return "CategoryTraceMetric" default: // For unknown categories, use the original formatting logic caser := cases.Title(language.English) rv := "Category" for _, w := range strings.Fields(string(c)) { rv += caser.String(w) } return rv } } // Priority represents the importance level of a category for buffer management. type Priority int const ( PriorityCritical Priority = iota + 1 PriorityHigh PriorityMedium PriorityLow PriorityLowest ) func (p Priority) String() string { switch p { case PriorityCritical: return "critical" case PriorityHigh: return "high" case PriorityMedium: return "medium" case PriorityLow: return "low" case PriorityLowest: return "lowest" default: return "unknown" } } // GetPriority returns the priority level for this category. func (c Category) GetPriority() Priority { switch c { case CategoryError: return PriorityCritical case CategoryMonitor: return PriorityHigh case CategoryLog: return PriorityLow case CategoryTransaction: return PriorityMedium case CategoryTraceMetric: return PriorityLow default: return PriorityMedium } } ================================================ FILE: vendor/github.com/getsentry/sentry-go/internal/ratelimit/deadline.go ================================================ package ratelimit import "time" // A Deadline is a time instant when a rate limit expires. type Deadline time.Time // After reports whether the deadline d is after other. func (d Deadline) After(other Deadline) bool { return time.Time(d).After(time.Time(other)) } // Equal reports whether d and e represent the same deadline. func (d Deadline) Equal(e Deadline) bool { return time.Time(d).Equal(time.Time(e)) } // String returns the deadline formatted for debugging. func (d Deadline) String() string { // Like time.Time.String, but without the monotonic clock reading. return time.Time(d).Round(0).String() } ================================================ FILE: vendor/github.com/getsentry/sentry-go/internal/ratelimit/doc.go ================================================ // Package ratelimit provides tools to work with rate limits imposed by Sentry's // data ingestion pipeline. package ratelimit ================================================ FILE: vendor/github.com/getsentry/sentry-go/internal/ratelimit/map.go ================================================ package ratelimit import ( "net/http" "time" ) // Map maps categories to rate limit deadlines. // // A rate limit is in effect for a given category if either the category's // deadline or the deadline for the special CategoryAll has not yet expired. // // Use IsRateLimited to check whether a category is rate-limited. type Map map[Category]Deadline // IsRateLimited returns true if the category is currently rate limited. func (m Map) IsRateLimited(c Category) bool { return m.isRateLimited(c, time.Now()) } func (m Map) isRateLimited(c Category, now time.Time) bool { return m.Deadline(c).After(Deadline(now)) } // Deadline returns the deadline when the rate limit for the given category or // the special CategoryAll expire, whichever is furthest into the future. func (m Map) Deadline(c Category) Deadline { categoryDeadline := m[c] allDeadline := m[CategoryAll] if categoryDeadline.After(allDeadline) { return categoryDeadline } return allDeadline } // Merge merges the other map into m. // // If a category appears in both maps, the deadline that is furthest into the // future is preserved. func (m Map) Merge(other Map) { for c, d := range other { if d.After(m[c]) { m[c] = d } } } // FromResponse returns a rate limit map from an HTTP response. func FromResponse(r *http.Response) Map { return fromResponse(r, time.Now()) } func fromResponse(r *http.Response, now time.Time) Map { s := r.Header.Get("X-Sentry-Rate-Limits") if s != "" { return parseXSentryRateLimits(s, now) } if r.StatusCode == http.StatusTooManyRequests { s := r.Header.Get("Retry-After") deadline, _ := parseRetryAfter(s, now) return Map{CategoryAll: deadline} } return Map{} } ================================================ FILE: vendor/github.com/getsentry/sentry-go/internal/ratelimit/rate_limits.go ================================================ package ratelimit import ( "errors" "math" "strconv" "strings" "time" ) var errInvalidXSRLRetryAfter = errors.New("invalid retry-after value") // parseXSentryRateLimits returns a RateLimits map by parsing an input string in // the format of the X-Sentry-Rate-Limits header. // // Example // // X-Sentry-Rate-Limits: 60:transaction, 2700:default;error;security // // This will rate limit transactions for the next 60 seconds and errors for the // next 2700 seconds. // // Limits for unknown categories are ignored. func parseXSentryRateLimits(s string, now time.Time) Map { // https://github.com/getsentry/relay/blob/0424a2e017d193a93918053c90cdae9472d164bf/relay-server/src/utils/rate_limits.rs#L44-L82 m := make(Map, len(knownCategories)) for _, limit := range strings.Split(s, ",") { limit = strings.TrimSpace(limit) if limit == "" { continue } components := strings.Split(limit, ":") if len(components) == 0 { continue } retryAfter, err := parseXSRLRetryAfter(strings.TrimSpace(components[0]), now) if err != nil { continue } categories := "" if len(components) > 1 { categories = components[1] } for _, category := range strings.Split(categories, ";") { c := Category(strings.ToLower(strings.TrimSpace(category))) if _, ok := knownCategories[c]; !ok { // skip unknown categories, keep m small continue } // always keep the deadline furthest into the future if retryAfter.After(m[c]) { m[c] = retryAfter } } } return m } // parseXSRLRetryAfter parses a string into a retry-after rate limit deadline. // // Valid input is a number, possibly signed and possibly floating-point, // indicating the number of seconds to wait before sending another request. // Negative values are treated as zero. Fractional values are rounded to the // next integer. func parseXSRLRetryAfter(s string, now time.Time) (Deadline, error) { // https://github.com/getsentry/relay/blob/0424a2e017d193a93918053c90cdae9472d164bf/relay-quotas/src/rate_limit.rs#L88-L96 f, err := strconv.ParseFloat(s, 64) if err != nil { return Deadline{}, errInvalidXSRLRetryAfter } d := time.Duration(math.Ceil(math.Max(f, 0.0))) * time.Second if d < 0 { d = 0 } return Deadline(now.Add(d)), nil } ================================================ FILE: vendor/github.com/getsentry/sentry-go/internal/ratelimit/retry_after.go ================================================ package ratelimit import ( "errors" "strconv" "time" ) const defaultRetryAfter = 1 * time.Minute var errInvalidRetryAfter = errors.New("invalid input") // parseRetryAfter parses a string s as in the standard Retry-After HTTP header // and returns a deadline until when requests are rate limited and therefore new // requests should not be sent. The input may be either a date or a non-negative // integer number of seconds. // // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After // // parseRetryAfter always returns a usable deadline, even in case of an error. // // This is the original rate limiting mechanism used by Sentry, superseeded by // the X-Sentry-Rate-Limits response header. func parseRetryAfter(s string, now time.Time) (Deadline, error) { if s == "" { goto invalid } if n, err := strconv.Atoi(s); err == nil { if n < 0 { goto invalid } d := time.Duration(n) * time.Second return Deadline(now.Add(d)), nil } if date, err := time.Parse(time.RFC1123, s); err == nil { return Deadline(date), nil } invalid: return Deadline(now.Add(defaultRetryAfter)), errInvalidRetryAfter } ================================================ FILE: vendor/github.com/getsentry/sentry-go/internal/telemetry/bucketed_buffer.go ================================================ package telemetry import ( "sync" "sync/atomic" "time" "github.com/getsentry/sentry-go/internal/ratelimit" ) const ( defaultBucketedCapacity = 100 perBucketItemLimit = 100 ) type Bucket[T any] struct { traceID string items []T createdAt time.Time lastUpdatedAt time.Time } // BucketedBuffer groups items by trace id, flushing per bucket. type BucketedBuffer[T any] struct { mu sync.RWMutex buckets []*Bucket[T] traceIndex map[string]int head int tail int itemCapacity int bucketCapacity int totalItems int bucketCount int category ratelimit.Category priority ratelimit.Priority overflowPolicy OverflowPolicy batchSize int timeout time.Duration lastFlushTime time.Time offered int64 dropped int64 onDropped func(item T, reason string) } func NewBucketedBuffer[T any]( category ratelimit.Category, capacity int, overflowPolicy OverflowPolicy, batchSize int, timeout time.Duration, ) *BucketedBuffer[T] { if capacity <= 0 { capacity = defaultBucketedCapacity } if batchSize <= 0 { batchSize = 1 } if timeout < 0 { timeout = 0 } bucketCapacity := capacity / 10 if bucketCapacity < 10 { bucketCapacity = 10 } return &BucketedBuffer[T]{ buckets: make([]*Bucket[T], bucketCapacity), traceIndex: make(map[string]int), itemCapacity: capacity, bucketCapacity: bucketCapacity, category: category, priority: category.GetPriority(), overflowPolicy: overflowPolicy, batchSize: batchSize, timeout: timeout, lastFlushTime: time.Now(), } } func (b *BucketedBuffer[T]) Offer(item T) bool { atomic.AddInt64(&b.offered, 1) traceID := "" if ta, ok := any(item).(TraceAware); ok { if tid, hasTrace := ta.GetTraceID(); hasTrace { traceID = tid } } b.mu.Lock() defer b.mu.Unlock() return b.offerToBucket(item, traceID) } func (b *BucketedBuffer[T]) offerToBucket(item T, traceID string) bool { if traceID != "" { if idx, exists := b.traceIndex[traceID]; exists { bucket := b.buckets[idx] if len(bucket.items) >= perBucketItemLimit { delete(b.traceIndex, traceID) } else { bucket.items = append(bucket.items, item) bucket.lastUpdatedAt = time.Now() b.totalItems++ return true } } } if b.totalItems >= b.itemCapacity { return b.handleOverflow(item, traceID) } if b.bucketCount >= b.bucketCapacity { return b.handleOverflow(item, traceID) } bucket := &Bucket[T]{ traceID: traceID, items: []T{item}, createdAt: time.Now(), lastUpdatedAt: time.Now(), } b.buckets[b.tail] = bucket if traceID != "" { b.traceIndex[traceID] = b.tail } b.tail = (b.tail + 1) % b.bucketCapacity b.bucketCount++ b.totalItems++ return true } func (b *BucketedBuffer[T]) handleOverflow(item T, traceID string) bool { switch b.overflowPolicy { case OverflowPolicyDropOldest: oldestBucket := b.buckets[b.head] if oldestBucket == nil { atomic.AddInt64(&b.dropped, 1) if b.onDropped != nil { b.onDropped(item, "buffer_full_invalid_state") } return false } if oldestBucket.traceID != "" { delete(b.traceIndex, oldestBucket.traceID) } droppedCount := len(oldestBucket.items) atomic.AddInt64(&b.dropped, int64(droppedCount)) if b.onDropped != nil { for _, di := range oldestBucket.items { b.onDropped(di, "buffer_full_drop_oldest_bucket") } } b.totalItems -= droppedCount b.bucketCount-- b.head = (b.head + 1) % b.bucketCapacity // add new bucket bucket := &Bucket[T]{traceID: traceID, items: []T{item}, createdAt: time.Now(), lastUpdatedAt: time.Now()} b.buckets[b.tail] = bucket if traceID != "" { b.traceIndex[traceID] = b.tail } b.tail = (b.tail + 1) % b.bucketCapacity b.bucketCount++ b.totalItems++ return true case OverflowPolicyDropNewest: atomic.AddInt64(&b.dropped, 1) if b.onDropped != nil { b.onDropped(item, "buffer_full_drop_newest") } return false default: atomic.AddInt64(&b.dropped, 1) if b.onDropped != nil { b.onDropped(item, "unknown_overflow_policy") } return false } } func (b *BucketedBuffer[T]) Poll() (T, bool) { b.mu.Lock() defer b.mu.Unlock() var zero T if b.bucketCount == 0 { return zero, false } bucket := b.buckets[b.head] if bucket == nil || len(bucket.items) == 0 { return zero, false } item := bucket.items[0] bucket.items = bucket.items[1:] b.totalItems-- if len(bucket.items) == 0 { if bucket.traceID != "" { delete(b.traceIndex, bucket.traceID) } b.buckets[b.head] = nil b.head = (b.head + 1) % b.bucketCapacity b.bucketCount-- } return item, true } func (b *BucketedBuffer[T]) PollBatch(maxItems int) []T { if maxItems <= 0 { return nil } b.mu.Lock() defer b.mu.Unlock() if b.bucketCount == 0 { return nil } res := make([]T, 0, maxItems) for len(res) < maxItems && b.bucketCount > 0 { bucket := b.buckets[b.head] if bucket == nil { break } n := maxItems - len(res) if n > len(bucket.items) { n = len(bucket.items) } res = append(res, bucket.items[:n]...) bucket.items = bucket.items[n:] b.totalItems -= n if len(bucket.items) == 0 { if bucket.traceID != "" { delete(b.traceIndex, bucket.traceID) } b.buckets[b.head] = nil b.head = (b.head + 1) % b.bucketCapacity b.bucketCount-- } } return res } func (b *BucketedBuffer[T]) PollIfReady() []T { b.mu.Lock() defer b.mu.Unlock() if b.bucketCount == 0 { return nil } ready := b.totalItems >= b.batchSize || (b.timeout > 0 && time.Since(b.lastFlushTime) >= b.timeout) if !ready { return nil } oldest := b.buckets[b.head] if oldest == nil { return nil } items := oldest.items if oldest.traceID != "" { delete(b.traceIndex, oldest.traceID) } b.buckets[b.head] = nil b.head = (b.head + 1) % b.bucketCapacity b.totalItems -= len(items) b.bucketCount-- b.lastFlushTime = time.Now() return items } func (b *BucketedBuffer[T]) Drain() []T { b.mu.Lock() defer b.mu.Unlock() if b.bucketCount == 0 { return nil } res := make([]T, 0, b.totalItems) for i := 0; i < b.bucketCount; i++ { idx := (b.head + i) % b.bucketCapacity bucket := b.buckets[idx] if bucket != nil { res = append(res, bucket.items...) b.buckets[idx] = nil } } b.traceIndex = make(map[string]int) b.head = 0 b.tail = 0 b.totalItems = 0 b.bucketCount = 0 return res } func (b *BucketedBuffer[T]) Peek() (T, bool) { b.mu.RLock() defer b.mu.RUnlock() var zero T if b.bucketCount == 0 { return zero, false } bucket := b.buckets[b.head] if bucket == nil || len(bucket.items) == 0 { return zero, false } return bucket.items[0], true } func (b *BucketedBuffer[T]) Size() int { b.mu.RLock(); defer b.mu.RUnlock(); return b.totalItems } func (b *BucketedBuffer[T]) Capacity() int { b.mu.RLock(); defer b.mu.RUnlock(); return b.itemCapacity } func (b *BucketedBuffer[T]) Category() ratelimit.Category { b.mu.RLock() defer b.mu.RUnlock() return b.category } func (b *BucketedBuffer[T]) Priority() ratelimit.Priority { b.mu.RLock() defer b.mu.RUnlock() return b.priority } func (b *BucketedBuffer[T]) IsEmpty() bool { b.mu.RLock() defer b.mu.RUnlock() return b.bucketCount == 0 } func (b *BucketedBuffer[T]) IsFull() bool { b.mu.RLock() defer b.mu.RUnlock() return b.totalItems >= b.itemCapacity } func (b *BucketedBuffer[T]) Utilization() float64 { b.mu.RLock() defer b.mu.RUnlock() if b.itemCapacity == 0 { return 0 } return float64(b.totalItems) / float64(b.itemCapacity) } func (b *BucketedBuffer[T]) OfferedCount() int64 { return atomic.LoadInt64(&b.offered) } func (b *BucketedBuffer[T]) DroppedCount() int64 { return atomic.LoadInt64(&b.dropped) } func (b *BucketedBuffer[T]) AcceptedCount() int64 { return b.OfferedCount() - b.DroppedCount() } func (b *BucketedBuffer[T]) DropRate() float64 { off := b.OfferedCount() if off == 0 { return 0 } return float64(b.DroppedCount()) / float64(off) } func (b *BucketedBuffer[T]) GetMetrics() BufferMetrics { b.mu.RLock() size := b.totalItems util := 0.0 if b.itemCapacity > 0 { util = float64(b.totalItems) / float64(b.itemCapacity) } b.mu.RUnlock() return BufferMetrics{Category: b.category, Priority: b.priority, Capacity: b.itemCapacity, Size: size, Utilization: util, OfferedCount: b.OfferedCount(), DroppedCount: b.DroppedCount(), AcceptedCount: b.AcceptedCount(), DropRate: b.DropRate(), LastUpdated: time.Now()} } func (b *BucketedBuffer[T]) SetDroppedCallback(callback func(item T, reason string)) { b.mu.Lock() defer b.mu.Unlock() b.onDropped = callback } func (b *BucketedBuffer[T]) Clear() { b.mu.Lock() defer b.mu.Unlock() for i := 0; i < b.bucketCapacity; i++ { b.buckets[i] = nil } b.traceIndex = make(map[string]int) b.head = 0 b.tail = 0 b.totalItems = 0 b.bucketCount = 0 } func (b *BucketedBuffer[T]) IsReadyToFlush() bool { b.mu.RLock() defer b.mu.RUnlock() if b.bucketCount == 0 { return false } if b.totalItems >= b.batchSize { return true } if b.timeout > 0 && time.Since(b.lastFlushTime) >= b.timeout { return true } return false } func (b *BucketedBuffer[T]) MarkFlushed() { b.mu.Lock() defer b.mu.Unlock() b.lastFlushTime = time.Now() } ================================================ FILE: vendor/github.com/getsentry/sentry-go/internal/telemetry/buffer.go ================================================ package telemetry import ( "github.com/getsentry/sentry-go/internal/ratelimit" ) // Buffer defines the common interface for all buffer implementations. type Buffer[T any] interface { // Core operations Offer(item T) bool Poll() (T, bool) PollBatch(maxItems int) []T PollIfReady() []T Drain() []T Peek() (T, bool) // State queries Size() int Capacity() int IsEmpty() bool IsFull() bool Utilization() float64 // Flush management IsReadyToFlush() bool MarkFlushed() // Category/Priority Category() ratelimit.Category Priority() ratelimit.Priority // Metrics OfferedCount() int64 DroppedCount() int64 AcceptedCount() int64 DropRate() float64 GetMetrics() BufferMetrics // Configuration SetDroppedCallback(callback func(item T, reason string)) Clear() } ================================================ FILE: vendor/github.com/getsentry/sentry-go/internal/telemetry/processor.go ================================================ package telemetry import ( "context" "time" "github.com/getsentry/sentry-go/internal/protocol" "github.com/getsentry/sentry-go/internal/ratelimit" ) // Processor is the top-level object that wraps the scheduler and buffers. type Processor struct { scheduler *Scheduler } // NewProcessor creates a new Processor with the given configuration. func NewProcessor( buffers map[ratelimit.Category]Buffer[protocol.TelemetryItem], transport protocol.TelemetryTransport, dsn *protocol.Dsn, sdkInfo *protocol.SdkInfo, ) *Processor { scheduler := NewScheduler(buffers, transport, dsn, sdkInfo) scheduler.Start() return &Processor{ scheduler: scheduler, } } // Add adds a TelemetryItem to the appropriate buffer based on its category. func (b *Processor) Add(item protocol.TelemetryItem) bool { return b.scheduler.Add(item) } // Flush forces all buffers to flush within the given timeout. func (b *Processor) Flush(timeout time.Duration) bool { return b.scheduler.Flush(timeout) } // FlushWithContext flushes with a custom context for cancellation. func (b *Processor) FlushWithContext(ctx context.Context) bool { return b.scheduler.FlushWithContext(ctx) } // Close stops the buffer, flushes remaining data, and releases resources. func (b *Processor) Close(timeout time.Duration) { b.scheduler.Stop(timeout) } ================================================ FILE: vendor/github.com/getsentry/sentry-go/internal/telemetry/ring_buffer.go ================================================ package telemetry import ( "sync" "sync/atomic" "time" "github.com/getsentry/sentry-go/internal/ratelimit" ) const defaultCapacity = 100 // RingBuffer is a thread-safe ring buffer with overflow policies. type RingBuffer[T any] struct { mu sync.RWMutex items []T head int tail int size int capacity int category ratelimit.Category priority ratelimit.Priority overflowPolicy OverflowPolicy batchSize int timeout time.Duration lastFlushTime time.Time offered int64 dropped int64 onDropped func(item T, reason string) } func NewRingBuffer[T any](category ratelimit.Category, capacity int, overflowPolicy OverflowPolicy, batchSize int, timeout time.Duration) *RingBuffer[T] { if capacity <= 0 { capacity = defaultCapacity } if batchSize <= 0 { batchSize = 1 } if timeout < 0 { timeout = 0 } return &RingBuffer[T]{ items: make([]T, capacity), capacity: capacity, category: category, priority: category.GetPriority(), overflowPolicy: overflowPolicy, batchSize: batchSize, timeout: timeout, lastFlushTime: time.Now(), } } func (b *RingBuffer[T]) SetDroppedCallback(callback func(item T, reason string)) { b.mu.Lock() defer b.mu.Unlock() b.onDropped = callback } func (b *RingBuffer[T]) Offer(item T) bool { atomic.AddInt64(&b.offered, 1) b.mu.Lock() defer b.mu.Unlock() if b.size < b.capacity { b.items[b.tail] = item b.tail = (b.tail + 1) % b.capacity b.size++ return true } switch b.overflowPolicy { case OverflowPolicyDropOldest: oldItem := b.items[b.head] b.items[b.head] = item b.head = (b.head + 1) % b.capacity b.tail = (b.tail + 1) % b.capacity atomic.AddInt64(&b.dropped, 1) if b.onDropped != nil { b.onDropped(oldItem, "buffer_full_drop_oldest") } return true case OverflowPolicyDropNewest: atomic.AddInt64(&b.dropped, 1) if b.onDropped != nil { b.onDropped(item, "buffer_full_drop_newest") } return false default: atomic.AddInt64(&b.dropped, 1) if b.onDropped != nil { b.onDropped(item, "unknown_overflow_policy") } return false } } func (b *RingBuffer[T]) Poll() (T, bool) { b.mu.Lock() defer b.mu.Unlock() var zero T if b.size == 0 { return zero, false } item := b.items[b.head] b.items[b.head] = zero b.head = (b.head + 1) % b.capacity b.size-- return item, true } func (b *RingBuffer[T]) PollBatch(maxItems int) []T { if maxItems <= 0 { return nil } b.mu.Lock() defer b.mu.Unlock() if b.size == 0 { return nil } itemCount := maxItems if itemCount > b.size { itemCount = b.size } result := make([]T, itemCount) var zero T for i := 0; i < itemCount; i++ { result[i] = b.items[b.head] b.items[b.head] = zero b.head = (b.head + 1) % b.capacity b.size-- } return result } func (b *RingBuffer[T]) Drain() []T { b.mu.Lock() defer b.mu.Unlock() if b.size == 0 { return nil } result := make([]T, b.size) index := 0 var zero T for i := 0; i < b.size; i++ { pos := (b.head + i) % b.capacity result[index] = b.items[pos] b.items[pos] = zero index++ } b.head = 0 b.tail = 0 b.size = 0 return result } func (b *RingBuffer[T]) Peek() (T, bool) { b.mu.RLock() defer b.mu.RUnlock() var zero T if b.size == 0 { return zero, false } return b.items[b.head], true } func (b *RingBuffer[T]) Size() int { b.mu.RLock() defer b.mu.RUnlock() return b.size } func (b *RingBuffer[T]) Capacity() int { b.mu.RLock() defer b.mu.RUnlock() return b.capacity } func (b *RingBuffer[T]) Category() ratelimit.Category { b.mu.RLock() defer b.mu.RUnlock() return b.category } func (b *RingBuffer[T]) Priority() ratelimit.Priority { b.mu.RLock() defer b.mu.RUnlock() return b.priority } func (b *RingBuffer[T]) IsEmpty() bool { b.mu.RLock() defer b.mu.RUnlock() return b.size == 0 } func (b *RingBuffer[T]) IsFull() bool { b.mu.RLock() defer b.mu.RUnlock() return b.size == b.capacity } func (b *RingBuffer[T]) Utilization() float64 { b.mu.RLock() defer b.mu.RUnlock() return float64(b.size) / float64(b.capacity) } func (b *RingBuffer[T]) OfferedCount() int64 { return atomic.LoadInt64(&b.offered) } func (b *RingBuffer[T]) DroppedCount() int64 { return atomic.LoadInt64(&b.dropped) } func (b *RingBuffer[T]) AcceptedCount() int64 { return b.OfferedCount() - b.DroppedCount() } func (b *RingBuffer[T]) DropRate() float64 { offered := b.OfferedCount() if offered == 0 { return 0.0 } return float64(b.DroppedCount()) / float64(offered) } func (b *RingBuffer[T]) Clear() { b.mu.Lock() defer b.mu.Unlock() var zero T for i := 0; i < b.capacity; i++ { b.items[i] = zero } b.head = 0 b.tail = 0 b.size = 0 } func (b *RingBuffer[T]) GetMetrics() BufferMetrics { b.mu.RLock() size := b.size util := float64(b.size) / float64(b.capacity) b.mu.RUnlock() return BufferMetrics{ Category: b.category, Priority: b.priority, Capacity: b.capacity, Size: size, Utilization: util, OfferedCount: b.OfferedCount(), DroppedCount: b.DroppedCount(), AcceptedCount: b.AcceptedCount(), DropRate: b.DropRate(), LastUpdated: time.Now(), } } func (b *RingBuffer[T]) IsReadyToFlush() bool { b.mu.RLock() defer b.mu.RUnlock() if b.size == 0 { return false } if b.size >= b.batchSize { return true } if b.timeout > 0 && time.Since(b.lastFlushTime) >= b.timeout { return true } return false } func (b *RingBuffer[T]) MarkFlushed() { b.mu.Lock() defer b.mu.Unlock() b.lastFlushTime = time.Now() } func (b *RingBuffer[T]) PollIfReady() []T { b.mu.Lock() defer b.mu.Unlock() if b.size == 0 { return nil } ready := b.size >= b.batchSize || (b.timeout > 0 && time.Since(b.lastFlushTime) >= b.timeout) if !ready { return nil } itemCount := b.batchSize if itemCount > b.size { itemCount = b.size } result := make([]T, itemCount) var zero T for i := 0; i < itemCount; i++ { result[i] = b.items[b.head] b.items[b.head] = zero b.head = (b.head + 1) % b.capacity b.size-- } b.lastFlushTime = time.Now() return result } type BufferMetrics struct { Category ratelimit.Category `json:"category"` Priority ratelimit.Priority `json:"priority"` Capacity int `json:"capacity"` Size int `json:"size"` Utilization float64 `json:"utilization"` OfferedCount int64 `json:"offered_count"` DroppedCount int64 `json:"dropped_count"` AcceptedCount int64 `json:"accepted_count"` DropRate float64 `json:"drop_rate"` LastUpdated time.Time `json:"last_updated"` } // OverflowPolicy defines how the ring buffer handles overflow. type OverflowPolicy int const ( OverflowPolicyDropOldest OverflowPolicy = iota OverflowPolicyDropNewest ) func (op OverflowPolicy) String() string { switch op { case OverflowPolicyDropOldest: return "drop_oldest" case OverflowPolicyDropNewest: return "drop_newest" default: return "unknown" } } ================================================ FILE: vendor/github.com/getsentry/sentry-go/internal/telemetry/scheduler.go ================================================ package telemetry import ( "context" "sync" "time" "github.com/getsentry/sentry-go/internal/debuglog" "github.com/getsentry/sentry-go/internal/protocol" "github.com/getsentry/sentry-go/internal/ratelimit" ) // Scheduler implements a weighted round-robin scheduler for processing buffered events. type Scheduler struct { buffers map[ratelimit.Category]Buffer[protocol.TelemetryItem] transport protocol.TelemetryTransport dsn *protocol.Dsn sdkInfo *protocol.SdkInfo currentCycle []ratelimit.Priority cyclePos int ctx context.Context cancel context.CancelFunc processingWg sync.WaitGroup mu sync.Mutex cond *sync.Cond startOnce sync.Once finishOnce sync.Once } func NewScheduler( buffers map[ratelimit.Category]Buffer[protocol.TelemetryItem], transport protocol.TelemetryTransport, dsn *protocol.Dsn, sdkInfo *protocol.SdkInfo, ) *Scheduler { ctx, cancel := context.WithCancel(context.Background()) priorityWeights := map[ratelimit.Priority]int{ ratelimit.PriorityCritical: 5, ratelimit.PriorityHigh: 4, ratelimit.PriorityMedium: 3, ratelimit.PriorityLow: 2, ratelimit.PriorityLowest: 1, } var currentCycle []ratelimit.Priority for priority, weight := range priorityWeights { hasBuffers := false for _, buffer := range buffers { if buffer.Priority() == priority { hasBuffers = true break } } if hasBuffers { for i := 0; i < weight; i++ { currentCycle = append(currentCycle, priority) } } } s := &Scheduler{ buffers: buffers, transport: transport, dsn: dsn, sdkInfo: sdkInfo, currentCycle: currentCycle, ctx: ctx, cancel: cancel, } s.cond = sync.NewCond(&s.mu) return s } func (s *Scheduler) Start() { s.startOnce.Do(func() { s.processingWg.Add(1) go s.run() }) } func (s *Scheduler) Stop(timeout time.Duration) { s.finishOnce.Do(func() { s.Flush(timeout) s.cancel() s.cond.Broadcast() done := make(chan struct{}) go func() { defer close(done) s.processingWg.Wait() }() select { case <-done: case <-time.After(timeout): debuglog.Printf("scheduler stop timed out after %v", timeout) } }) } func (s *Scheduler) Signal() { s.cond.Signal() } func (s *Scheduler) Add(item protocol.TelemetryItem) bool { category := item.GetCategory() buffer, exists := s.buffers[category] if !exists { return false } accepted := buffer.Offer(item) if accepted { s.Signal() } return accepted } func (s *Scheduler) Flush(timeout time.Duration) bool { ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() return s.FlushWithContext(ctx) } func (s *Scheduler) FlushWithContext(ctx context.Context) bool { s.flushBuffers() return s.transport.FlushWithContext(ctx) } func (s *Scheduler) run() { defer s.processingWg.Done() go func() { ticker := time.NewTicker(100 * time.Millisecond) defer ticker.Stop() for { select { case <-ticker.C: s.cond.Broadcast() case <-s.ctx.Done(): return } } }() for { s.mu.Lock() for !s.hasWork() && s.ctx.Err() == nil { s.cond.Wait() } if s.ctx.Err() != nil { s.mu.Unlock() return } s.mu.Unlock() s.processNextBatch() } } func (s *Scheduler) hasWork() bool { for _, buffer := range s.buffers { if buffer.IsReadyToFlush() { return true } } return false } func (s *Scheduler) processNextBatch() { if len(s.currentCycle) == 0 { return } priority := s.currentCycle[s.cyclePos] s.cyclePos = (s.cyclePos + 1) % len(s.currentCycle) var bufferToProcess Buffer[protocol.TelemetryItem] var categoryToProcess ratelimit.Category for category, buffer := range s.buffers { if buffer.Priority() == priority && buffer.IsReadyToFlush() { bufferToProcess = buffer categoryToProcess = category break } } if bufferToProcess != nil { s.processItems(bufferToProcess, categoryToProcess, false) } } func (s *Scheduler) processItems(buffer Buffer[protocol.TelemetryItem], category ratelimit.Category, force bool) { var items []protocol.TelemetryItem if force { items = buffer.Drain() } else { items = buffer.PollIfReady() } // drop the current batch if rate-limited or if transport is full if len(items) == 0 || s.isRateLimited(category) || !s.transport.HasCapacity() { return } switch category { case ratelimit.CategoryLog: logs := protocol.Logs(items) header := &protocol.EnvelopeHeader{EventID: protocol.GenerateEventID(), SentAt: time.Now(), Sdk: s.sdkInfo} if s.dsn != nil { header.Dsn = s.dsn.String() } envelope := protocol.NewEnvelope(header) item, err := logs.ToEnvelopeItem() if err != nil { debuglog.Printf("error creating log batch envelope item: %v", err) return } envelope.AddItem(item) if err := s.transport.SendEnvelope(envelope); err != nil { debuglog.Printf("error sending envelope: %v", err) } return case ratelimit.CategoryTraceMetric: metrics := protocol.Metrics(items) header := &protocol.EnvelopeHeader{EventID: protocol.GenerateEventID(), SentAt: time.Now(), Sdk: s.sdkInfo} if s.dsn != nil { header.Dsn = s.dsn.String() } envelope := protocol.NewEnvelope(header) item, err := metrics.ToEnvelopeItem() if err != nil { debuglog.Printf("error creating trace metric batch envelope item: %v", err) return } envelope.AddItem(item) if err := s.transport.SendEnvelope(envelope); err != nil { debuglog.Printf("error sending envelope: %v", err) } return default: // if the buffers are properly configured, buffer.PollIfReady should return a single item for every category // other than logs. We still iterate over the items just in case, because we don't want to send broken envelopes. for _, it := range items { convertible, ok := it.(protocol.EnvelopeItemConvertible) if !ok { debuglog.Printf("item does not implement EnvelopeItemConvertible: %T", it) continue } s.sendItem(convertible) } } } func (s *Scheduler) sendItem(item protocol.EnvelopeItemConvertible) { header := &protocol.EnvelopeHeader{ EventID: item.GetEventID(), SentAt: time.Now(), Trace: item.GetDynamicSamplingContext(), Sdk: s.sdkInfo, } if header.EventID == "" { header.EventID = protocol.GenerateEventID() } if s.dsn != nil { header.Dsn = s.dsn.String() } envelope := protocol.NewEnvelope(header) envItem, err := item.ToEnvelopeItem() if err != nil { debuglog.Printf("error while converting to envelope item: %v", err) return } envelope.AddItem(envItem) if err := s.transport.SendEnvelope(envelope); err != nil { debuglog.Printf("error sending envelope: %v", err) } } func (s *Scheduler) flushBuffers() { for category, buffer := range s.buffers { if !buffer.IsEmpty() { s.processItems(buffer, category, true) } } } func (s *Scheduler) isRateLimited(category ratelimit.Category) bool { return s.transport.IsRateLimited(category) } ================================================ FILE: vendor/github.com/getsentry/sentry-go/internal/telemetry/trace_aware.go ================================================ package telemetry // TraceAware is implemented by items that can expose a trace ID. // BucketedBuffer uses this to group items by trace. type TraceAware interface { GetTraceID() (string, bool) } ================================================ FILE: vendor/github.com/getsentry/sentry-go/internal/util/map.go ================================================ package util import "sync" type SyncMap[K comparable, V any] struct { m sync.Map } func (s *SyncMap[K, V]) Store(key K, value V) { s.m.Store(key, value) } func (s *SyncMap[K, V]) CompareAndDelete(key K, value V) { s.m.CompareAndDelete(key, value) } func (s *SyncMap[K, V]) Load(key K) (V, bool) { v, ok := s.m.Load(key) if !ok { var zero V return zero, false } return v.(V), true } func (s *SyncMap[K, V]) Delete(key K) { s.m.Delete(key) } func (s *SyncMap[K, V]) LoadOrStore(key K, value V) (V, bool) { actual, loaded := s.m.LoadOrStore(key, value) return actual.(V), loaded } func (s *SyncMap[K, V]) Clear() { s.m.Clear() } func (s *SyncMap[K, V]) Range(f func(key K, value V) bool) { s.m.Range(func(key, value any) bool { return f(key.(K), value.(V)) }) } ================================================ FILE: vendor/github.com/getsentry/sentry-go/internal/util/util.go ================================================ package util import ( "fmt" "io" "net/http" "github.com/getsentry/sentry-go/internal/debuglog" "github.com/getsentry/sentry-go/internal/protocol" ) // MaxDrainResponseBytes is the maximum number of bytes that transport // implementations will read from response bodies when draining them. const MaxDrainResponseBytes = 16 << 10 // HandleHTTPResponse is a helper method that reads the HTTP response and handles debug output. func HandleHTTPResponse(response *http.Response, identifier string) bool { if response.StatusCode >= 200 && response.StatusCode < 300 { return true } if response.StatusCode >= 400 && response.StatusCode <= 599 { body, err := io.ReadAll(io.LimitReader(response.Body, MaxDrainResponseBytes)) if err != nil { debuglog.Printf("Error while reading response body: %v", err) return false } switch { case response.StatusCode == http.StatusRequestEntityTooLarge: debuglog.Printf("Sending %s failed because the request was too large: %s", identifier, string(body)) case response.StatusCode >= 500: debuglog.Printf("Sending %s failed with server error %d: %s", identifier, response.StatusCode, string(body)) default: debuglog.Printf("Sending %s failed with client error %d: %s", identifier, response.StatusCode, string(body)) } return false } debuglog.Printf("Unexpected status code %d for event %s", response.StatusCode, identifier) return false } // EnvelopeIdentifier returns a human-readable identifier for the event to be used in log messages. // Format: " []". func EnvelopeIdentifier(envelope *protocol.Envelope) string { if envelope == nil || len(envelope.Items) == 0 { return "empty envelope" } var description string // we don't currently support mixed envelope types, so all event types would have the same type. itemType := envelope.Items[0].Header.Type switch itemType { case protocol.EnvelopeItemTypeEvent: description = "error" case protocol.EnvelopeItemTypeTransaction: description = "transaction" case protocol.EnvelopeItemTypeCheckIn: description = "check-in" case protocol.EnvelopeItemTypeLog: logCount := 0 for _, item := range envelope.Items { if item != nil && item.Header != nil && item.Header.Type == protocol.EnvelopeItemTypeLog && item.Header.ItemCount != nil { logCount += *item.Header.ItemCount } } description = fmt.Sprintf("%d log events", logCount) case protocol.EnvelopeItemTypeTraceMetric: metricCount := 0 for _, item := range envelope.Items { if item != nil && item.Header != nil && item.Header.Type == protocol.EnvelopeItemTypeTraceMetric && item.Header.ItemCount != nil { metricCount += *item.Header.ItemCount } } description = fmt.Sprintf("%d metric events", metricCount) default: description = fmt.Sprintf("%s event", itemType) } return fmt.Sprintf("%s [%s]", description, envelope.Header.EventID) } ================================================ FILE: vendor/github.com/getsentry/sentry-go/log.go ================================================ package sentry import ( "context" "fmt" "maps" "os" "strings" "sync" "time" "github.com/getsentry/sentry-go/attribute" "github.com/getsentry/sentry-go/internal/debuglog" ) type LogLevel string const ( LogLevelTrace LogLevel = "trace" LogLevelDebug LogLevel = "debug" LogLevelInfo LogLevel = "info" LogLevelWarn LogLevel = "warn" LogLevelError LogLevel = "error" LogLevelFatal LogLevel = "fatal" ) const ( LogSeverityTrace int = 1 LogSeverityDebug int = 5 LogSeverityInfo int = 9 LogSeverityWarning int = 13 LogSeverityError int = 17 LogSeverityFatal int = 21 ) type sentryLogger struct { ctx context.Context hub *Hub attributes map[string]attribute.Value defaultAttributes map[string]attribute.Value mu sync.RWMutex } type logEntry struct { logger *sentryLogger ctx context.Context level LogLevel severity int attributes map[string]attribute.Value shouldPanic bool shouldFatal bool } // NewLogger returns a Logger that emits logs to Sentry. If logging is turned off, all logs get discarded. func NewLogger(ctx context.Context) Logger { // nolint: dupl var hub *Hub hub = GetHubFromContext(ctx) if hub == nil { hub = CurrentHub() } client := hub.Client() if client != nil && client.options.EnableLogs { // Build default attrs serverAddr := client.options.ServerName if serverAddr == "" { serverAddr, _ = os.Hostname() } defaults := map[string]string{ "sentry.release": client.options.Release, "sentry.environment": client.options.Environment, "sentry.server.address": serverAddr, "sentry.sdk.name": client.sdkIdentifier, "sentry.sdk.version": client.sdkVersion, } defaultAttrs := make(map[string]attribute.Value, len(defaults)) for k, v := range defaults { if v != "" { defaultAttrs[k] = attribute.StringValue(v) } } return &sentryLogger{ ctx: ctx, hub: hub, attributes: make(map[string]attribute.Value), defaultAttributes: defaultAttrs, mu: sync.RWMutex{}, } } debuglog.Println("fallback to noopLogger: enableLogs disabled") return &noopLogger{} } func (l *sentryLogger) Write(p []byte) (int, error) { msg := strings.TrimRight(string(p), "\n") l.Info().Emit(msg) return len(p), nil } func (l *sentryLogger) log(ctx context.Context, level LogLevel, severity int, message string, entryAttrs map[string]attribute.Value, args ...interface{}) { if message == "" { return } hub := hubFromContexts(ctx, l.ctx) if hub == nil { hub = l.hub } client := hub.Client() if client == nil { return } scope := hub.Scope() traceID, spanID := resolveTrace(scope, ctx, l.ctx) // Pre-allocate with capacity hint to avoid map growth reallocations estimatedCap := len(l.defaultAttributes) + len(entryAttrs) + len(args) + 8 // scope ~3 + instance ~5 attrs := make(map[string]attribute.Value, estimatedCap) // attribute precedence: default -> scope -> instance (from SetAttrs) -> entry-specific for k, v := range l.defaultAttributes { attrs[k] = v } scope.populateAttrs(attrs) l.mu.RLock() for k, v := range l.attributes { attrs[k] = v } l.mu.RUnlock() for k, v := range entryAttrs { attrs[k] = v } if len(args) > 0 { attrs["sentry.message.template"] = attribute.StringValue(message) for i, p := range args { attrs[fmt.Sprintf("sentry.message.parameters.%d", i)] = attribute.StringValue(fmt.Sprintf("%+v", p)) } } log := &Log{ Timestamp: time.Now(), TraceID: traceID, SpanID: spanID, Level: level, Severity: severity, Body: fmt.Sprintf(message, args...), Attributes: attrs, } client.captureLog(log, scope) if client.options.Debug { debuglog.Printf(message, args...) } } func (l *sentryLogger) SetAttributes(attrs ...attribute.Builder) { l.mu.Lock() defer l.mu.Unlock() for _, a := range attrs { if a.Value.Type() == attribute.INVALID { debuglog.Printf("invalid attribute: %v", a) continue } l.attributes[a.Key] = a.Value } } func (l *sentryLogger) Trace() LogEntry { return &logEntry{ logger: l, ctx: l.ctx, level: LogLevelTrace, severity: LogSeverityTrace, attributes: make(map[string]attribute.Value), } } func (l *sentryLogger) Debug() LogEntry { return &logEntry{ logger: l, ctx: l.ctx, level: LogLevelDebug, severity: LogSeverityDebug, attributes: make(map[string]attribute.Value), } } func (l *sentryLogger) Info() LogEntry { return &logEntry{ logger: l, ctx: l.ctx, level: LogLevelInfo, severity: LogSeverityInfo, attributes: make(map[string]attribute.Value), } } func (l *sentryLogger) Warn() LogEntry { return &logEntry{ logger: l, ctx: l.ctx, level: LogLevelWarn, severity: LogSeverityWarning, attributes: make(map[string]attribute.Value), } } func (l *sentryLogger) Error() LogEntry { return &logEntry{ logger: l, ctx: l.ctx, level: LogLevelError, severity: LogSeverityError, attributes: make(map[string]attribute.Value), } } func (l *sentryLogger) Fatal() LogEntry { return &logEntry{ logger: l, ctx: l.ctx, level: LogLevelFatal, severity: LogSeverityFatal, attributes: make(map[string]attribute.Value), shouldFatal: true, } } func (l *sentryLogger) Panic() LogEntry { return &logEntry{ logger: l, ctx: l.ctx, level: LogLevelFatal, severity: LogSeverityFatal, attributes: make(map[string]attribute.Value), shouldPanic: true, } } func (l *sentryLogger) LFatal() LogEntry { return &logEntry{ logger: l, ctx: l.ctx, level: LogLevelFatal, severity: LogSeverityFatal, attributes: make(map[string]attribute.Value), } } func (l *sentryLogger) GetCtx() context.Context { return l.ctx } func (e *logEntry) WithCtx(ctx context.Context) LogEntry { return &logEntry{ logger: e.logger, ctx: ctx, level: e.level, severity: e.severity, attributes: maps.Clone(e.attributes), shouldPanic: e.shouldPanic, shouldFatal: e.shouldFatal, } } func (e *logEntry) String(key, value string) LogEntry { e.attributes[key] = attribute.StringValue(value) return e } func (e *logEntry) Int(key string, value int) LogEntry { e.attributes[key] = attribute.Int64Value(int64(value)) return e } func (e *logEntry) Int64(key string, value int64) LogEntry { e.attributes[key] = attribute.Int64Value(value) return e } func (e *logEntry) Float64(key string, value float64) LogEntry { e.attributes[key] = attribute.Float64Value(value) return e } func (e *logEntry) Bool(key string, value bool) LogEntry { e.attributes[key] = attribute.BoolValue(value) return e } // Uint64 adds uint64 attributes to the log entry. // // This method is intentionally not part of the LogEntry interface to avoid exposing uint64 in the public API. func (e *logEntry) Uint64(key string, value uint64) LogEntry { e.attributes[key] = attribute.Uint64Value(value) return e } func (e *logEntry) Emit(args ...interface{}) { e.logger.log(e.ctx, e.level, e.severity, fmt.Sprint(args...), e.attributes) if e.level == LogLevelFatal { if e.shouldPanic { panic(fmt.Sprint(args...)) } if e.shouldFatal { os.Exit(1) } } } func (e *logEntry) Emitf(format string, args ...interface{}) { e.logger.log(e.ctx, e.level, e.severity, format, e.attributes, args...) if e.level == LogLevelFatal { if e.shouldPanic { formattedMessage := fmt.Sprintf(format, args...) panic(formattedMessage) } if e.shouldFatal { os.Exit(1) } } } ================================================ FILE: vendor/github.com/getsentry/sentry-go/log_batch_processor.go ================================================ package sentry import ( "time" ) // logBatchProcessor batches logs and sends them to Sentry. type logBatchProcessor struct { *batchProcessor[Log] } func newLogBatchProcessor(client *Client) *logBatchProcessor { return &logBatchProcessor{ batchProcessor: newBatchProcessor(func(items []Log) { if len(items) == 0 { return } event := NewEvent() event.Timestamp = time.Now() event.EventID = EventID(uuid()) event.Type = logEvent.Type event.Logs = items client.Transport.SendEvent(event) }), } } func (p *logBatchProcessor) Send(log *Log) bool { return p.batchProcessor.Send(*log) } ================================================ FILE: vendor/github.com/getsentry/sentry-go/log_fallback.go ================================================ package sentry import ( "context" "fmt" "os" "github.com/getsentry/sentry-go/attribute" "github.com/getsentry/sentry-go/internal/debuglog" ) // Fallback, no-op logger if logging is disabled. type noopLogger struct{} // noopLogEntry implements LogEntry for the no-op logger. type noopLogEntry struct { level LogLevel shouldPanic bool shouldFatal bool } func (n *noopLogEntry) WithCtx(_ context.Context) LogEntry { return n } func (n *noopLogEntry) String(_, _ string) LogEntry { return n } func (n *noopLogEntry) Int(_ string, _ int) LogEntry { return n } func (n *noopLogEntry) Int64(_ string, _ int64) LogEntry { return n } func (n *noopLogEntry) Float64(_ string, _ float64) LogEntry { return n } func (n *noopLogEntry) Bool(_ string, _ bool) LogEntry { return n } func (n *noopLogEntry) Attributes(_ ...attribute.Builder) LogEntry { return n } func (n *noopLogEntry) Emit(args ...interface{}) { debuglog.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", n.level) if n.level == LogLevelFatal { if n.shouldPanic { panic(args) } if n.shouldFatal { os.Exit(1) } } } func (n *noopLogEntry) Emitf(message string, args ...interface{}) { debuglog.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", n.level) if n.level == LogLevelFatal { if n.shouldPanic { panic(fmt.Sprintf(message, args...)) } if n.shouldFatal { os.Exit(1) } } } func (n *noopLogger) GetCtx() context.Context { return context.Background() } func (*noopLogger) Trace() LogEntry { return &noopLogEntry{level: LogLevelTrace} } func (*noopLogger) Debug() LogEntry { return &noopLogEntry{level: LogLevelDebug} } func (*noopLogger) Info() LogEntry { return &noopLogEntry{level: LogLevelInfo} } func (*noopLogger) Warn() LogEntry { return &noopLogEntry{level: LogLevelWarn} } func (*noopLogger) Error() LogEntry { return &noopLogEntry{level: LogLevelError} } func (*noopLogger) Fatal() LogEntry { return &noopLogEntry{level: LogLevelFatal, shouldFatal: true} } func (*noopLogger) Panic() LogEntry { return &noopLogEntry{level: LogLevelFatal, shouldPanic: true} } func (*noopLogger) LFatal() LogEntry { return &noopLogEntry{level: LogLevelFatal} } func (*noopLogger) SetAttributes(...attribute.Builder) { debuglog.Printf("No attributes attached. Turn on logging via EnableLogs") } func (*noopLogger) Write(_ []byte) (n int, err error) { return 0, fmt.Errorf("log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelInfo) } ================================================ FILE: vendor/github.com/getsentry/sentry-go/metric_batch_processor.go ================================================ package sentry import ( "time" ) // metricBatchProcessor batches metrics and sends them to Sentry. type metricBatchProcessor struct { *batchProcessor[Metric] } func newMetricBatchProcessor(client *Client) *metricBatchProcessor { return &metricBatchProcessor{ batchProcessor: newBatchProcessor(func(items []Metric) { if len(items) == 0 { return } event := NewEvent() event.Timestamp = time.Now() event.EventID = EventID(uuid()) event.Type = traceMetricEvent.Type event.Metrics = items client.Transport.SendEvent(event) }), } } func (p *metricBatchProcessor) Send(metric *Metric) bool { return p.batchProcessor.Send(*metric) } ================================================ FILE: vendor/github.com/getsentry/sentry-go/metrics.go ================================================ package sentry import ( "context" "maps" "os" "sync" "time" "github.com/getsentry/sentry-go/attribute" "github.com/getsentry/sentry-go/internal/debuglog" ) // Duration Units. const ( UnitNanosecond = "nanosecond" UnitMicrosecond = "microsecond" UnitMillisecond = "millisecond" UnitSecond = "second" UnitMinute = "minute" UnitHour = "hour" UnitDay = "day" UnitWeek = "week" ) // Information Units. const ( UnitBit = "bit" UnitByte = "byte" UnitKilobyte = "kilobyte" UnitKibibyte = "kibibyte" UnitMegabyte = "megabyte" UnitMebibyte = "mebibyte" UnitGigabyte = "gigabyte" UnitGibibyte = "gibibyte" UnitTerabyte = "terabyte" UnitTebibyte = "tebibyte" UnitPetabyte = "petabyte" UnitPebibyte = "pebibyte" UnitExabyte = "exabyte" UnitExbibyte = "exbibyte" ) // Fraction Units. const ( UnitRatio = "ratio" UnitPercent = "percent" ) // NewMeter returns a new Meter. If there is no Client bound to the current hub, or if metrics are disabled, // it returns a no-op Meter that discards all metrics. func NewMeter(ctx context.Context) Meter { hub := GetHubFromContext(ctx) if hub == nil { hub = CurrentHub() } client := hub.Client() if client != nil && !client.options.DisableMetrics { // build default attrs serverAddr := client.options.ServerName if serverAddr == "" { serverAddr, _ = os.Hostname() } defaults := map[string]string{ "sentry.release": client.options.Release, "sentry.environment": client.options.Environment, "sentry.server.address": serverAddr, "sentry.sdk.name": client.sdkIdentifier, "sentry.sdk.version": client.sdkVersion, } defaultAttrs := make(map[string]attribute.Value) for k, v := range defaults { if v != "" { defaultAttrs[k] = attribute.StringValue(v) } } return &sentryMeter{ ctx: ctx, hub: hub, attributes: make(map[string]attribute.Value), defaultAttributes: defaultAttrs, mu: sync.RWMutex{}, } } debuglog.Printf("fallback to noopMeter: metrics disabled") return &noopMeter{} } type sentryMeter struct { ctx context.Context hub *Hub attributes map[string]attribute.Value defaultAttributes map[string]attribute.Value mu sync.RWMutex } func (m *sentryMeter) emit(ctx context.Context, metricType MetricType, name string, value MetricValue, unit string, attributes map[string]attribute.Value, customScope *Scope) { if name == "" { debuglog.Println("empty name provided, dropping metric") return } hub := hubFromContexts(ctx, m.ctx) if hub == nil { hub = m.hub } client := hub.Client() if client == nil { return } scope := hub.Scope() if customScope != nil { scope = customScope } traceID, spanID := resolveTrace(scope, ctx, m.ctx) // Pre-allocate with capacity hint to avoid map growth reallocations estimatedCap := len(m.defaultAttributes) + len(attributes) + 8 // scope ~3 + call-specific ~5 attrs := make(map[string]attribute.Value, estimatedCap) // attribute precedence: default -> scope -> instance (from SetAttrs) -> entry-specific for k, v := range m.defaultAttributes { attrs[k] = v } scope.populateAttrs(attrs) m.mu.RLock() for k, v := range m.attributes { attrs[k] = v } m.mu.RUnlock() for k, v := range attributes { attrs[k] = v } metric := &Metric{ Timestamp: time.Now(), TraceID: traceID, SpanID: spanID, Type: metricType, Name: name, Value: value, Unit: unit, Attributes: attrs, } if client.captureMetric(metric, scope) && client.options.Debug { debuglog.Printf("Metric %s [%s]: %v %s", metricType, name, value.AsInterface(), unit) } } // WithCtx returns a new Meter that uses the given context for trace/span association. func (m *sentryMeter) WithCtx(ctx context.Context) Meter { m.mu.RLock() attrsCopy := maps.Clone(m.attributes) m.mu.RUnlock() return &sentryMeter{ ctx: ctx, hub: m.hub, attributes: attrsCopy, defaultAttributes: m.defaultAttributes, mu: sync.RWMutex{}, } } func (m *sentryMeter) applyOptions(opts []MeterOption) *meterOptions { o := &meterOptions{} for _, opt := range opts { opt(o) } return o } // Count implements Meter. func (m *sentryMeter) Count(name string, count int64, opts ...MeterOption) { o := m.applyOptions(opts) m.emit(m.ctx, MetricTypeCounter, name, Int64MetricValue(count), o.unit, o.attributes, o.scope) } // Distribution implements Meter. func (m *sentryMeter) Distribution(name string, sample float64, opts ...MeterOption) { o := m.applyOptions(opts) m.emit(m.ctx, MetricTypeDistribution, name, Float64MetricValue(sample), o.unit, o.attributes, o.scope) } // Gauge implements Meter. func (m *sentryMeter) Gauge(name string, value float64, opts ...MeterOption) { o := m.applyOptions(opts) m.emit(m.ctx, MetricTypeGauge, name, Float64MetricValue(value), o.unit, o.attributes, o.scope) } // SetAttributes implements Meter. func (m *sentryMeter) SetAttributes(attrs ...attribute.Builder) { m.mu.Lock() defer m.mu.Unlock() for _, a := range attrs { if a.Value.Type() == attribute.INVALID { debuglog.Printf("invalid attribute: %v", a) continue } m.attributes[a.Key] = a.Value } } // noopMeter is a no-operation implementation of Meter. // This is used when there is no client available in the context or when metrics are disabled. type noopMeter struct{} // WithCtx implements Meter. func (n *noopMeter) WithCtx(_ context.Context) Meter { return n } // Count implements Meter. func (n *noopMeter) Count(name string, _ int64, _ ...MeterOption) { debuglog.Printf("Metric %q is being dropped. Turn on metrics by setting DisableMetrics to false", name) } // Distribution implements Meter. func (n *noopMeter) Distribution(name string, _ float64, _ ...MeterOption) { debuglog.Printf("Metric %q is being dropped. Turn on metrics by setting DisableMetrics to false", name) } // Gauge implements Meter. func (n *noopMeter) Gauge(name string, _ float64, _ ...MeterOption) { debuglog.Printf("Metric %q is being dropped. Turn on metrics by setting DisableMetrics to false", name) } // SetAttributes implements Meter. func (n *noopMeter) SetAttributes(_ ...attribute.Builder) { debuglog.Printf("No attributes attached. Turn on metrics by setting DisableMetrics to false") } ================================================ FILE: vendor/github.com/getsentry/sentry-go/mocks.go ================================================ package sentry import ( "context" "sync" "time" ) // MockScope implements [Scope] for use in tests. type MockScope struct { breadcrumb *Breadcrumb shouldDropEvent bool } func (scope *MockScope) AddBreadcrumb(breadcrumb *Breadcrumb, _ int) { scope.breadcrumb = breadcrumb } func (scope *MockScope) ApplyToEvent(event *Event, _ *EventHint, _ *Client) *Event { if scope.shouldDropEvent { return nil } return event } // MockTransport implements [Transport] for use in tests. type MockTransport struct { mu sync.Mutex events []*Event lastEvent *Event } func (t *MockTransport) Configure(_ ClientOptions) {} func (t *MockTransport) SendEvent(event *Event) { t.mu.Lock() defer t.mu.Unlock() t.events = append(t.events, event) t.lastEvent = event } func (t *MockTransport) Flush(_ time.Duration) bool { return true } func (t *MockTransport) FlushWithContext(_ context.Context) bool { return true } func (t *MockTransport) Events() []*Event { t.mu.Lock() defer t.mu.Unlock() return t.events } func (t *MockTransport) Close() {} // MockLogEntry implements [sentry.LogEntry] for use in tests. type MockLogEntry struct { Attributes map[string]any } func NewMockLogEntry() *MockLogEntry { return &MockLogEntry{Attributes: make(map[string]any)} } func (m *MockLogEntry) WithCtx(_ context.Context) LogEntry { return m } func (m *MockLogEntry) String(key, value string) LogEntry { m.Attributes[key] = value; return m } func (m *MockLogEntry) Int(key string, value int) LogEntry { m.Attributes[key] = int64(value) return m } func (m *MockLogEntry) Int64(key string, value int64) LogEntry { m.Attributes[key] = value return m } func (m *MockLogEntry) Float64(key string, value float64) LogEntry { m.Attributes[key] = value return m } func (m *MockLogEntry) Bool(key string, value bool) LogEntry { m.Attributes[key] = value return m } func (m *MockLogEntry) Emit(...any) {} func (m *MockLogEntry) Emitf(string, ...any) {} ================================================ FILE: vendor/github.com/getsentry/sentry-go/propagation_context.go ================================================ package sentry import ( "crypto/rand" ) type PropagationContext struct { TraceID TraceID `json:"trace_id"` SpanID SpanID `json:"span_id"` ParentSpanID SpanID `json:"parent_span_id,omitzero"` DynamicSamplingContext DynamicSamplingContext `json:"-"` } func (p PropagationContext) Map() map[string]interface{} { m := map[string]interface{}{ "trace_id": p.TraceID, "span_id": p.SpanID, } if p.ParentSpanID != zeroSpanID { m["parent_span_id"] = p.ParentSpanID } return m } func NewPropagationContext() PropagationContext { p := PropagationContext{} if _, err := rand.Read(p.TraceID[:]); err != nil { panic(err) } if _, err := rand.Read(p.SpanID[:]); err != nil { panic(err) } return p } func PropagationContextFromHeaders(trace, baggage string) (PropagationContext, error) { p := NewPropagationContext() if _, err := rand.Read(p.SpanID[:]); err != nil { panic(err) } hasTrace := false if trace != "" { if tpc, valid := ParseTraceParentContext([]byte(trace)); valid { hasTrace = true p.TraceID = tpc.TraceID p.ParentSpanID = tpc.ParentSpanID } } if baggage != "" { dsc, err := DynamicSamplingContextFromHeader([]byte(baggage)) if err != nil { return PropagationContext{}, err } p.DynamicSamplingContext = dsc } // In case a sentry-trace header is present but there are no sentry-related // values in the baggage, create an empty, frozen DynamicSamplingContext. if hasTrace && !p.DynamicSamplingContext.HasEntries() { p.DynamicSamplingContext = DynamicSamplingContext{ Frozen: true, } } return p, nil } ================================================ FILE: vendor/github.com/getsentry/sentry-go/scope.go ================================================ package sentry import ( "bytes" "context" "io" "net/http" "sync" "time" "github.com/getsentry/sentry-go/attribute" "github.com/getsentry/sentry-go/internal/debuglog" ) // Scope holds contextual data for the current scope. // // The scope is an object that can cloned efficiently and stores data that is // locally relevant to an event. For instance the scope will hold recorded // breadcrumbs and similar information. // // The scope can be interacted with in two ways. First, the scope is routinely // updated with information by functions such as AddBreadcrumb which will modify // the current scope. Second, the current scope can be configured through the // ConfigureScope function or Hub method of the same name. // // The scope is meant to be modified but not inspected directly. When preparing // an event for reporting, the current client adds information from the current // scope into the event. type Scope struct { mu sync.RWMutex breadcrumbs []*Breadcrumb attachments []*Attachment user User tags map[string]string contexts map[string]Context extra map[string]interface{} fingerprint []string level Level request *http.Request // requestBody holds a reference to the original request.Body. requestBody interface { // Bytes returns bytes from the original body, lazily buffered as the // original body is read. Bytes() []byte // Overflow returns true if the body is larger than the maximum buffer // size. Overflow() bool } eventProcessors []EventProcessor propagationContext PropagationContext span *Span } // NewScope creates a new Scope. func NewScope() *Scope { return &Scope{ breadcrumbs: make([]*Breadcrumb, 0), attachments: make([]*Attachment, 0), tags: make(map[string]string), contexts: make(map[string]Context), extra: make(map[string]interface{}), fingerprint: make([]string, 0), propagationContext: NewPropagationContext(), } } // AddBreadcrumb adds new breadcrumb to the current scope // and optionally throws the old one if limit is reached. func (scope *Scope) AddBreadcrumb(breadcrumb *Breadcrumb, limit int) { if breadcrumb.Timestamp.IsZero() { breadcrumb.Timestamp = time.Now() } scope.mu.Lock() defer scope.mu.Unlock() scope.breadcrumbs = append(scope.breadcrumbs, breadcrumb) if len(scope.breadcrumbs) > limit { scope.breadcrumbs = scope.breadcrumbs[1 : limit+1] } } // ClearBreadcrumbs clears all breadcrumbs from the current scope. func (scope *Scope) ClearBreadcrumbs() { scope.mu.Lock() defer scope.mu.Unlock() scope.breadcrumbs = []*Breadcrumb{} } // AddAttachment adds new attachment to the current scope. func (scope *Scope) AddAttachment(attachment *Attachment) { scope.mu.Lock() defer scope.mu.Unlock() scope.attachments = append(scope.attachments, attachment) } // ClearAttachments clears all attachments from the current scope. func (scope *Scope) ClearAttachments() { scope.mu.Lock() defer scope.mu.Unlock() scope.attachments = []*Attachment{} } // SetUser sets the user for the current scope. func (scope *Scope) SetUser(user User) { scope.mu.Lock() defer scope.mu.Unlock() scope.user = user } // SetRequest sets the request for the current scope. func (scope *Scope) SetRequest(r *http.Request) { scope.mu.Lock() defer scope.mu.Unlock() scope.request = r if r == nil { return } // Don't buffer request body if we know it is oversized. if r.ContentLength > maxRequestBodyBytes { return } // Don't buffer if there is no body. if r.Body == nil || r.Body == http.NoBody { return } buf := &limitedBuffer{Capacity: maxRequestBodyBytes} r.Body = readCloser{ Reader: io.TeeReader(r.Body, buf), Closer: r.Body, } scope.requestBody = buf } // SetRequestBody sets the request body for the current scope. // // This method should only be called when the body bytes are already available // in memory. Typically, the request body is buffered lazily from the // Request.Body from SetRequest. func (scope *Scope) SetRequestBody(b []byte) { scope.mu.Lock() defer scope.mu.Unlock() capacity := maxRequestBodyBytes overflow := false if len(b) > capacity { overflow = true b = b[:capacity] } scope.requestBody = &limitedBuffer{ Capacity: capacity, Buffer: *bytes.NewBuffer(b), overflow: overflow, } } // maxRequestBodyBytes is the default maximum request body size to send to // Sentry. const maxRequestBodyBytes = 10 * 1024 // A limitedBuffer is like a bytes.Buffer, but limited to store at most Capacity // bytes. Any writes past the capacity are silently discarded, similar to // io.Discard. type limitedBuffer struct { Capacity int bytes.Buffer overflow bool } // Write implements io.Writer. func (b *limitedBuffer) Write(p []byte) (n int, err error) { // Silently ignore writes after overflow. if b.overflow { return len(p), nil } left := b.Capacity - b.Len() if left < 0 { left = 0 } if len(p) > left { b.overflow = true p = p[:left] } return b.Buffer.Write(p) } // Overflow returns true if the limitedBuffer discarded bytes written to it. func (b *limitedBuffer) Overflow() bool { return b.overflow } // readCloser combines an io.Reader and an io.Closer to implement io.ReadCloser. type readCloser struct { io.Reader io.Closer } // SetTag adds a tag to the current scope. func (scope *Scope) SetTag(key, value string) { scope.mu.Lock() defer scope.mu.Unlock() scope.tags[key] = value } // SetTags assigns multiple tags to the current scope. func (scope *Scope) SetTags(tags map[string]string) { scope.mu.Lock() defer scope.mu.Unlock() for k, v := range tags { scope.tags[k] = v } } // RemoveTag removes a tag from the current scope. func (scope *Scope) RemoveTag(key string) { scope.mu.Lock() defer scope.mu.Unlock() delete(scope.tags, key) } // SetContext adds a context to the current scope. func (scope *Scope) SetContext(key string, value Context) { scope.mu.Lock() defer scope.mu.Unlock() scope.contexts[key] = value } // SetContexts assigns multiple contexts to the current scope. func (scope *Scope) SetContexts(contexts map[string]Context) { scope.mu.Lock() defer scope.mu.Unlock() for k, v := range contexts { scope.contexts[k] = v } } // RemoveContext removes a context from the current scope. func (scope *Scope) RemoveContext(key string) { scope.mu.Lock() defer scope.mu.Unlock() delete(scope.contexts, key) } // SetExtra adds an extra to the current scope. func (scope *Scope) SetExtra(key string, value interface{}) { scope.mu.Lock() defer scope.mu.Unlock() scope.extra[key] = value } // SetExtras assigns multiple extras to the current scope. func (scope *Scope) SetExtras(extra map[string]interface{}) { scope.mu.Lock() defer scope.mu.Unlock() for k, v := range extra { scope.extra[k] = v } } // RemoveExtra removes a extra from the current scope. func (scope *Scope) RemoveExtra(key string) { scope.mu.Lock() defer scope.mu.Unlock() delete(scope.extra, key) } // SetFingerprint sets new fingerprint for the current scope. func (scope *Scope) SetFingerprint(fingerprint []string) { scope.mu.Lock() defer scope.mu.Unlock() scope.fingerprint = fingerprint } // SetLevel sets new level for the current scope. func (scope *Scope) SetLevel(level Level) { scope.mu.Lock() defer scope.mu.Unlock() scope.level = level } // SetPropagationContext sets the propagation context for the current scope. func (scope *Scope) SetPropagationContext(propagationContext PropagationContext) { scope.mu.Lock() defer scope.mu.Unlock() scope.propagationContext = propagationContext } // GetSpan returns the span from the current scope. func (scope *Scope) GetSpan() *Span { scope.mu.RLock() defer scope.mu.RUnlock() return scope.span } // SetSpan sets a span for the current scope. func (scope *Scope) SetSpan(span *Span) { scope.mu.Lock() defer scope.mu.Unlock() scope.span = span } // Clone returns a copy of the current scope with all data copied over. func (scope *Scope) Clone() *Scope { scope.mu.RLock() defer scope.mu.RUnlock() clone := NewScope() clone.user = scope.user clone.breadcrumbs = make([]*Breadcrumb, len(scope.breadcrumbs)) copy(clone.breadcrumbs, scope.breadcrumbs) clone.attachments = make([]*Attachment, len(scope.attachments)) copy(clone.attachments, scope.attachments) for key, value := range scope.tags { clone.tags[key] = value } for key, value := range scope.contexts { clone.contexts[key] = cloneContext(value) } for key, value := range scope.extra { clone.extra[key] = value } clone.fingerprint = make([]string, len(scope.fingerprint)) copy(clone.fingerprint, scope.fingerprint) clone.level = scope.level clone.request = scope.request clone.requestBody = scope.requestBody clone.eventProcessors = scope.eventProcessors clone.propagationContext = scope.propagationContext clone.span = scope.span return clone } // Clear removes the data from the current scope. Not safe for concurrent use. func (scope *Scope) Clear() { *scope = *NewScope() } // AddEventProcessor adds an event processor to the current scope. func (scope *Scope) AddEventProcessor(processor EventProcessor) { scope.mu.Lock() defer scope.mu.Unlock() scope.eventProcessors = append(scope.eventProcessors, processor) } // ApplyToEvent takes the data from the current scope and attaches it to the event. func (scope *Scope) ApplyToEvent(event *Event, hint *EventHint, client *Client) *Event { scope.mu.RLock() defer scope.mu.RUnlock() if len(scope.breadcrumbs) > 0 { event.Breadcrumbs = append(event.Breadcrumbs, scope.breadcrumbs...) } if len(scope.attachments) > 0 { event.Attachments = append(event.Attachments, scope.attachments...) } if len(scope.tags) > 0 { if event.Tags == nil { event.Tags = make(map[string]string, len(scope.tags)) } for key, value := range scope.tags { event.Tags[key] = value } } if len(scope.contexts) > 0 { if event.Contexts == nil { event.Contexts = make(map[string]Context) } for key, value := range scope.contexts { if key == "trace" && event.Type == transactionType { // Do not override trace context of // transactions, otherwise it breaks the // transaction event representation. // For error events, the trace context is used // to link errors and traces/spans in Sentry. continue } // Ensure we are not overwriting event fields if _, ok := event.Contexts[key]; !ok { event.Contexts[key] = cloneContext(value) } } } if event.Contexts == nil { event.Contexts = make(map[string]Context) } if scope.span != nil { if _, ok := event.Contexts["trace"]; !ok { event.Contexts["trace"] = scope.span.traceContext().Map() } transaction := scope.span.GetTransaction() if transaction != nil { event.sdkMetaData.dsc = DynamicSamplingContextFromTransaction(transaction) } } else { event.Contexts["trace"] = scope.propagationContext.Map() dsc := scope.propagationContext.DynamicSamplingContext if !dsc.HasEntries() && client != nil { dsc = DynamicSamplingContextFromScope(scope, client) } event.sdkMetaData.dsc = dsc } if len(scope.extra) > 0 { if event.Extra == nil { event.Extra = make(map[string]interface{}, len(scope.extra)) } for key, value := range scope.extra { event.Extra[key] = value } } if event.User.IsEmpty() { event.User = scope.user } if len(event.Fingerprint) == 0 { event.Fingerprint = append(event.Fingerprint, scope.fingerprint...) } if scope.level != "" { event.Level = scope.level } if event.Request == nil && scope.request != nil { event.Request = NewRequest(scope.request) // NOTE: The SDK does not attempt to send partial request body data. // // The reason being that Sentry's ingest pipeline and UI are optimized // to show structured data. Additionally, tooling around PII scrubbing // relies on structured data; truncated request bodies would create // invalid payloads that are more prone to leaking PII data. // // Users can still send more data along their events if they want to, // for example using Event.Extra. if scope.requestBody != nil && !scope.requestBody.Overflow() { event.Request.Data = string(scope.requestBody.Bytes()) } } for _, processor := range scope.eventProcessors { id := event.EventID event = processor(event, hint) if event == nil { debuglog.Printf("Event dropped by one of the Scope EventProcessors: %s\n", id) return nil } } return event } // cloneContext returns a new context with keys and values copied from the passed one. // // Note: a new Context (map) is returned, but the function does NOT do // a proper deep copy: if some context values are pointer types (e.g. maps), // they won't be properly copied. func cloneContext(c Context) Context { res := make(Context, len(c)) for k, v := range c { res[k] = v } return res } func (scope *Scope) populateAttrs(attrs map[string]attribute.Value) { if scope == nil { return } scope.mu.RLock() defer scope.mu.RUnlock() // Add user-related attributes if !scope.user.IsEmpty() { if scope.user.ID != "" { attrs["user.id"] = attribute.StringValue(scope.user.ID) } if scope.user.Name != "" { attrs["user.name"] = attribute.StringValue(scope.user.Name) } if scope.user.Email != "" { attrs["user.email"] = attribute.StringValue(scope.user.Email) } } // In the future, add scope.attributes here // for k, v := range scope.attributes { // attrs[k] = v // } } // hubFromContexts is a helper to return the first hub found in the given contexts. func hubFromContexts(ctxs ...context.Context) *Hub { for _, ctx := range ctxs { if ctx == nil { continue } if hub := GetHubFromContext(ctx); hub != nil { return hub } } return nil } // resolveTrace resolves trace ID and span ID from the given scope and contexts. // // The resolution order follows a most-specific-to-least-specific pattern: // 1. Check for span directly in contexts (SpanFromContext) - this is the most specific // source as it represents a span explicitly attached to the current operation's context // 2. Check scope's span - provides access to span set on the hub's scope // 3. Fall back to scope's propagation context trace ID // // This ordering ensures we always use the most contextually relevant tracing information. // For example, if a specific span is active for an operation, we use that span's trace/span IDs // rather than accidentally using a different span that might be set on the hub's scope. func resolveTrace(scope *Scope, ctxs ...context.Context) (traceID TraceID, spanID SpanID) { var span *Span for _, ctx := range ctxs { if ctx == nil { continue } if span = SpanFromContext(ctx); span != nil { break } } if scope != nil { scope.mu.RLock() if span == nil { span = scope.span } if span != nil { traceID = span.TraceID spanID = span.SpanID } else { traceID = scope.propagationContext.TraceID } scope.mu.RUnlock() } return traceID, spanID } ================================================ FILE: vendor/github.com/getsentry/sentry-go/sentry.go ================================================ package sentry import ( "context" "time" ) // The version of the SDK. const SDKVersion = "0.43.0" // apiVersion is the minimum version of the Sentry API compatible with the // sentry-go SDK. const apiVersion = "7" // Init initializes the SDK with options. The returned error is non-nil if // options is invalid, for instance if a malformed DSN is provided. func Init(options ClientOptions) error { hub := CurrentHub() client, err := NewClient(options) if err != nil { return err } hub.BindClient(client) return nil } // AddBreadcrumb records a new breadcrumb. // // The total number of breadcrumbs that can be recorded are limited by the // configuration on the client. func AddBreadcrumb(breadcrumb *Breadcrumb) { hub := CurrentHub() hub.AddBreadcrumb(breadcrumb, nil) } // CaptureMessage captures an arbitrary message. func CaptureMessage(message string) *EventID { hub := CurrentHub() return hub.CaptureMessage(message) } // CaptureException captures an error. func CaptureException(exception error) *EventID { hub := CurrentHub() return hub.CaptureException(exception) } // CaptureCheckIn captures a (cron) monitor check-in. func CaptureCheckIn(checkIn *CheckIn, monitorConfig *MonitorConfig) *EventID { hub := CurrentHub() return hub.CaptureCheckIn(checkIn, monitorConfig) } // CaptureEvent captures an event on the currently active client if any. // // The event must already be assembled. Typically code would instead use // the utility methods like CaptureException. The return value is the // event ID. In case Sentry is disabled or event was dropped, the return value will be nil. func CaptureEvent(event *Event) *EventID { hub := CurrentHub() return hub.CaptureEvent(event) } // Recover captures a panic. func Recover() *EventID { if err := recover(); err != nil { hub := CurrentHub() return hub.Recover(err) } return nil } // RecoverWithContext captures a panic and passes relevant context object. func RecoverWithContext(ctx context.Context) *EventID { err := recover() if err == nil { return nil } hub := GetHubFromContext(ctx) if hub == nil { hub = CurrentHub() } return hub.RecoverWithContext(ctx, err) } // WithScope is a shorthand for CurrentHub().WithScope. func WithScope(f func(scope *Scope)) { hub := CurrentHub() hub.WithScope(f) } // ConfigureScope is a shorthand for CurrentHub().ConfigureScope. func ConfigureScope(f func(scope *Scope)) { hub := CurrentHub() hub.ConfigureScope(f) } // PushScope is a shorthand for CurrentHub().PushScope. func PushScope() { hub := CurrentHub() hub.PushScope() } // PopScope is a shorthand for CurrentHub().PopScope. func PopScope() { hub := CurrentHub() hub.PopScope() } // Flush waits until the underlying Transport sends any buffered events to the // Sentry server, blocking for at most the given timeout. It returns false if // the timeout was reached. In that case, some events may not have been sent. // // Flush should be called before terminating the program to avoid // unintentionally dropping events. // // Do not call Flush indiscriminately after every call to CaptureEvent, // CaptureException or CaptureMessage. Instead, to have the SDK send events over // the network synchronously, configure it to use the HTTPSyncTransport in the // call to Init. func Flush(timeout time.Duration) bool { hub := CurrentHub() return hub.Flush(timeout) } // FlushWithContext waits until the underlying Transport sends any buffered events // to the Sentry server, blocking for at most the duration specified by the context. // It returns false if the context is canceled before the events are sent. In such a case, // some events may not be delivered. // // FlushWithContext should be called before terminating the program to ensure no // events are unintentionally dropped. // // Avoid calling FlushWithContext indiscriminately after each call to CaptureEvent, // CaptureException, or CaptureMessage. To send events synchronously over the network, // configure the SDK to use HTTPSyncTransport during initialization with Init. func FlushWithContext(ctx context.Context) bool { hub := CurrentHub() return hub.FlushWithContext(ctx) } // LastEventID returns an ID of last captured event. func LastEventID() EventID { hub := CurrentHub() return hub.LastEventID() } ================================================ FILE: vendor/github.com/getsentry/sentry-go/sourcereader.go ================================================ package sentry import ( "bytes" "os" "sync" ) type sourceReader struct { mu sync.Mutex cache map[string][][]byte } func newSourceReader() sourceReader { return sourceReader{ cache: make(map[string][][]byte), } } func (sr *sourceReader) readContextLines(filename string, line, context int) ([][]byte, int) { sr.mu.Lock() defer sr.mu.Unlock() lines, ok := sr.cache[filename] if !ok { data, err := os.ReadFile(filename) if err != nil { sr.cache[filename] = nil return nil, 0 } lines = bytes.Split(data, []byte{'\n'}) sr.cache[filename] = lines } return sr.calculateContextLines(lines, line, context) } func (sr *sourceReader) calculateContextLines(lines [][]byte, line, context int) ([][]byte, int) { // Stacktrace lines are 1-indexed, slices are 0-indexed line-- // contextLine points to a line that caused an issue itself, in relation to // returned slice. contextLine := context if lines == nil || line >= len(lines) || line < 0 { return nil, 0 } if context < 0 { context = 0 contextLine = 0 } start := line - context if start < 0 { contextLine += start start = 0 } end := line + context + 1 if end > len(lines) { end = len(lines) } return lines[start:end], contextLine } ================================================ FILE: vendor/github.com/getsentry/sentry-go/span_recorder.go ================================================ package sentry import ( "sync" "github.com/getsentry/sentry-go/internal/debuglog" ) // A spanRecorder stores a span tree that makes up a transaction. Safe for // concurrent use. It is okay to add child spans from multiple goroutines. type spanRecorder struct { mu sync.Mutex spans []*Span overflowOnce sync.Once } // record stores a span. The first stored span is assumed to be the root of a // span tree. func (r *spanRecorder) record(s *Span) { maxSpans := defaultMaxSpans if client := CurrentHub().Client(); client != nil { maxSpans = client.options.MaxSpans } r.mu.Lock() defer r.mu.Unlock() if len(r.spans) >= maxSpans { r.overflowOnce.Do(func() { root := r.spans[0] debuglog.Printf("Too many spans: dropping spans from transaction with TraceID=%s SpanID=%s limit=%d", root.TraceID, root.SpanID, maxSpans) }) // TODO(tracing): mark the transaction event in some way to // communicate that spans were dropped. return } r.spans = append(r.spans, s) } // root returns the first recorded span. Returns nil if none have been recorded. func (r *spanRecorder) root() *Span { r.mu.Lock() defer r.mu.Unlock() if len(r.spans) == 0 { return nil } return r.spans[0] } // children returns a list of all recorded spans, except the root. Returns nil // if there are no children. func (r *spanRecorder) children() []*Span { r.mu.Lock() defer r.mu.Unlock() if len(r.spans) < 2 { return nil } return r.spans[1:] } ================================================ FILE: vendor/github.com/getsentry/sentry-go/stacktrace.go ================================================ package sentry import ( "go/build" "reflect" "runtime" "slices" "strings" ) const unknown string = "unknown" // The module download is split into two parts: downloading the go.mod and downloading the actual code. // If you have dependencies only needed for tests, then they will show up in your go.mod, // and go get will download their go.mods, but it will not download their code. // The test-only dependencies get downloaded only when you need it, such as the first time you run go test. // // https://github.com/golang/go/issues/26913#issuecomment-411976222 // Stacktrace holds information about the frames of the stack. type Stacktrace struct { Frames []Frame `json:"frames,omitempty"` FramesOmitted []uint `json:"frames_omitted,omitempty"` } // NewStacktrace creates a stacktrace using runtime.Callers. func NewStacktrace() *Stacktrace { pcs := make([]uintptr, 100) n := runtime.Callers(1, pcs) if n == 0 { return nil } runtimeFrames := extractFrames(pcs[:n]) frames := createFrames(runtimeFrames) stacktrace := Stacktrace{ Frames: frames, } return &stacktrace } // TODO: Make it configurable so that anyone can provide their own implementation? // Use of reflection allows us to not have a hard dependency on any given // package, so we don't have to import it. // ExtractStacktrace creates a new Stacktrace based on the given error. func ExtractStacktrace(err error) *Stacktrace { method := extractReflectedStacktraceMethod(err) var pcs []uintptr if method.IsValid() { pcs = extractPcs(method) } else { pcs = extractXErrorsPC(err) } if len(pcs) == 0 { return nil } runtimeFrames := extractFrames(pcs) frames := createFrames(runtimeFrames) stacktrace := Stacktrace{ Frames: frames, } return &stacktrace } func extractReflectedStacktraceMethod(err error) reflect.Value { errValue := reflect.ValueOf(err) // https://github.com/go-errors/errors methodStackFrames := errValue.MethodByName("StackFrames") if methodStackFrames.IsValid() { return methodStackFrames } // https://github.com/pkg/errors methodStackTrace := errValue.MethodByName("StackTrace") if methodStackTrace.IsValid() { return methodStackTrace } // https://github.com/pingcap/errors methodGetStackTracer := errValue.MethodByName("GetStackTracer") if methodGetStackTracer.IsValid() { stacktracer := methodGetStackTracer.Call(nil)[0] stacktracerStackTrace := reflect.ValueOf(stacktracer).MethodByName("StackTrace") if stacktracerStackTrace.IsValid() { return stacktracerStackTrace } } return reflect.Value{} } func extractPcs(method reflect.Value) []uintptr { var pcs []uintptr stacktrace := method.Call(nil)[0] if stacktrace.Kind() != reflect.Slice { return nil } for i := 0; i < stacktrace.Len(); i++ { pc := stacktrace.Index(i) switch pc.Kind() { case reflect.Uintptr: pcs = append(pcs, uintptr(pc.Uint())) case reflect.Struct: for _, fieldName := range []string{"ProgramCounter", "PC"} { field := pc.FieldByName(fieldName) if !field.IsValid() { continue } if field.Kind() == reflect.Uintptr { pcs = append(pcs, uintptr(field.Uint())) break } } } } return pcs } // extractXErrorsPC extracts program counters from error values compatible with // the error types from golang.org/x/xerrors. // // It returns nil if err is not compatible with errors from that package or if // no program counters are stored in err. func extractXErrorsPC(err error) []uintptr { // This implementation uses the reflect package to avoid a hard dependency // on third-party packages. // We don't know if err matches the expected type. For simplicity, instead // of trying to account for all possible ways things can go wrong, some // assumptions are made and if they are violated the code will panic. We // recover from any panic and ignore it, returning nil. //nolint: errcheck defer func() { recover() }() field := reflect.ValueOf(err).Elem().FieldByName("frame") // type Frame struct{ frames [3]uintptr } field = field.FieldByName("frames") field = field.Slice(1, field.Len()) // drop first pc pointing to xerrors.New pc := make([]uintptr, field.Len()) for i := 0; i < field.Len(); i++ { pc[i] = uintptr(field.Index(i).Uint()) } return pc } // Frame represents a function call and it's metadata. Frames are associated // with a Stacktrace. type Frame struct { Function string `json:"function,omitempty"` Symbol string `json:"symbol,omitempty"` // Module is, despite the name, the Sentry protocol equivalent of a Go // package's import path. Module string `json:"module,omitempty"` Filename string `json:"filename,omitempty"` AbsPath string `json:"abs_path,omitempty"` Lineno int `json:"lineno,omitempty"` Colno int `json:"colno,omitempty"` PreContext []string `json:"pre_context,omitempty"` ContextLine string `json:"context_line,omitempty"` PostContext []string `json:"post_context,omitempty"` InApp bool `json:"in_app"` Vars map[string]interface{} `json:"vars,omitempty"` // Package and the below are not used for Go stack trace frames. In // other platforms it refers to a container where the Module can be // found. For example, a Java JAR, a .NET Assembly, or a native // dynamic library. They exists for completeness, allowing the // construction and reporting of custom event payloads. Package string `json:"package,omitempty"` InstructionAddr string `json:"instruction_addr,omitempty"` AddrMode string `json:"addr_mode,omitempty"` SymbolAddr string `json:"symbol_addr,omitempty"` ImageAddr string `json:"image_addr,omitempty"` Platform string `json:"platform,omitempty"` StackStart bool `json:"stack_start,omitempty"` } // NewFrame assembles a stacktrace frame out of runtime.Frame. func NewFrame(f runtime.Frame) Frame { function := f.Function var pkg string if function != "" { pkg, function = splitQualifiedFunctionName(function) } return newFrame(pkg, function, f.File, f.Line) } // Like filepath.IsAbs() but doesn't care what platform you run this on. // I.e. it also recognizies `/path/to/file` when run on Windows. func isAbsPath(path string) bool { if len(path) == 0 { return false } // If the volume name starts with a double slash, this is an absolute path. if len(path) >= 1 && (path[0] == '/' || path[0] == '\\') { return true } // Windows absolute path, see https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats if len(path) >= 3 && path[1] == ':' && (path[2] == '/' || path[2] == '\\') { return true } return false } func newFrame(module string, function string, file string, line int) Frame { frame := Frame{ Lineno: line, Module: module, Function: function, } switch { case len(file) == 0: frame.Filename = unknown // Leave abspath as the empty string to be omitted when serializing event as JSON. case isAbsPath(file): frame.AbsPath = file // TODO: in the general case, it is not trivial to come up with a // "project relative" path with the data we have in run time. // We shall not use filepath.Base because it creates ambiguous paths and // affects the "Suspect Commits" feature. // For now, leave relpath empty to be omitted when serializing the event // as JSON. Improve this later. default: // f.File is a relative path. This may happen when the binary is built // with the -trimpath flag. frame.Filename = file // Omit abspath when serializing the event as JSON. } setInAppFrame(&frame) return frame } // splitQualifiedFunctionName splits a package path-qualified function name into // package name and function name. Such qualified names are found in // runtime.Frame.Function values. func splitQualifiedFunctionName(name string) (pkg string, fun string) { pkg = packageName(name) if len(pkg) > 0 { fun = name[len(pkg)+1:] } return } func extractFrames(pcs []uintptr) []runtime.Frame { var frames = make([]runtime.Frame, 0, len(pcs)) callersFrames := runtime.CallersFrames(pcs) for { callerFrame, more := callersFrames.Next() frames = append(frames, callerFrame) if !more { break } } slices.Reverse(frames) return frames } // createFrames creates Frame objects while filtering out frames that are not // meant to be reported to Sentry, those are frames internal to the SDK or Go. func createFrames(frames []runtime.Frame) []Frame { if len(frames) == 0 { return nil } result := make([]Frame, 0, len(frames)) for _, frame := range frames { function := frame.Function var pkg string if function != "" { pkg, function = splitQualifiedFunctionName(function) } if !shouldSkipFrame(pkg) { result = append(result, newFrame(pkg, function, frame.File, frame.Line)) } } // Fix issues grouping errors with the new fully qualified function names // introduced from Go 1.21 result = cleanupFunctionNamePrefix(result) return result } // TODO ID: why do we want to do this? // I'm not aware of other SDKs skipping all Sentry frames, regardless of their position in the stactrace. // For example, in the .NET SDK, only the first frames are skipped until the call to the SDK. // As is, this will also hide any intermediate frames in the stack and make debugging issues harder. func shouldSkipFrame(module string) bool { // Skip Go internal frames. if module == "runtime" || module == "testing" { return true } // Skip Sentry internal frames, except for frames in _test packages (for testing). if strings.HasPrefix(module, "github.com/getsentry/sentry-go") && !strings.HasSuffix(module, "_test") { return true } return false } // On Windows, GOROOT has backslashes, but we want forward slashes. var goRoot = strings.ReplaceAll(build.Default.GOROOT, "\\", "/") func setInAppFrame(frame *Frame) { frame.InApp = true if strings.HasPrefix(frame.AbsPath, goRoot) || strings.Contains(frame.Module, "vendor") || strings.Contains(frame.Module, "third_party") { frame.InApp = false } } func callerFunctionName() string { pcs := make([]uintptr, 1) runtime.Callers(3, pcs) callersFrames := runtime.CallersFrames(pcs) callerFrame, _ := callersFrames.Next() return baseName(callerFrame.Function) } // packageName returns the package part of the symbol name, or the empty string // if there is none. // It replicates https://golang.org/pkg/debug/gosym/#Sym.PackageName, avoiding a // dependency on debug/gosym. func packageName(name string) string { if isCompilerGeneratedSymbol(name) { return "" } pathend := strings.LastIndex(name, "/") if pathend < 0 { pathend = 0 } if i := strings.Index(name[pathend:], "."); i != -1 { return name[:pathend+i] } return "" } // baseName returns the symbol name without the package or receiver name. // It replicates https://golang.org/pkg/debug/gosym/#Sym.BaseName, avoiding a // dependency on debug/gosym. func baseName(name string) string { if i := strings.LastIndex(name, "."); i != -1 { return name[i+1:] } return name } func isCompilerGeneratedSymbol(name string) bool { // In versions of Go 1.20 and above a prefix of "type:" and "go:" is a // compiler-generated symbol that doesn't belong to any package. // See variable reservedimports in cmd/compile/internal/gc/subr.go if strings.HasPrefix(name, "go:") || strings.HasPrefix(name, "type:") { return true } return false } // Walk backwards through the results and for the current function name // remove it's parent function's prefix, leaving only it's actual name. This // fixes issues grouping errors with the new fully qualified function names // introduced from Go 1.21. func cleanupFunctionNamePrefix(f []Frame) []Frame { for i := len(f) - 1; i > 0; i-- { name := f[i].Function parentName := f[i-1].Function + "." if !strings.HasPrefix(name, parentName) { continue } f[i].Function = name[len(parentName):] } return f } ================================================ FILE: vendor/github.com/getsentry/sentry-go/traces_sampler.go ================================================ package sentry // A SamplingContext is passed to a TracesSampler to determine a sampling // decision. // // TODO(tracing): possibly expand SamplingContext to include custom / // user-provided data. type SamplingContext struct { Span *Span // The current span, always non-nil. Parent *Span // The parent span, may be nil. } // The TracesSample type is an adapter to allow the use of ordinary // functions as a TracesSampler. type TracesSampler func(ctx SamplingContext) float64 func (f TracesSampler) Sample(ctx SamplingContext) float64 { return f(ctx) } ================================================ FILE: vendor/github.com/getsentry/sentry-go/tracing.go ================================================ package sentry import ( "context" "crypto/rand" "encoding/hex" "encoding/json" "fmt" "net/http" "regexp" "strconv" "strings" "sync" "time" "github.com/getsentry/sentry-go/internal/debuglog" ) const ( SentryTraceHeader = "sentry-trace" SentryBaggageHeader = "baggage" TraceparentHeader = "traceparent" ) // SpanOrigin indicates what created a trace or a span. See: https://develop.sentry.dev/sdk/performance/trace-origin/ type SpanOrigin string const ( SpanOriginManual = "manual" SpanOriginEcho = "auto.http.echo" SpanOriginFastHTTP = "auto.http.fasthttp" SpanOriginFiber = "auto.http.fiber" SpanOriginGin = "auto.http.gin" SpanOriginStdLib = "auto.http.stdlib" SpanOriginIris = "auto.http.iris" SpanOriginNegroni = "auto.http.negroni" ) // A Span is the building block of a Sentry transaction. Spans build up a tree // structure of timed operations. The span tree makes up a transaction event // that is sent to Sentry when the root span is finished. // // Spans must be started with either StartSpan or Span.StartChild. type Span struct { //nolint: maligned // prefer readability over optimal memory layout (see note below *) TraceID TraceID `json:"trace_id"` SpanID SpanID `json:"span_id"` ParentSpanID SpanID `json:"parent_span_id,omitzero"` Name string `json:"name,omitempty"` Op string `json:"op,omitempty"` Description string `json:"description,omitempty"` Status SpanStatus `json:"status,omitempty"` Tags map[string]string `json:"tags,omitempty"` StartTime time.Time `json:"start_timestamp,omitzero"` EndTime time.Time `json:"timestamp,omitzero"` // Deprecated: use Data instead. To be removed in 0.33.0 Extra map[string]interface{} `json:"-"` Data map[string]interface{} `json:"data,omitempty"` Sampled Sampled `json:"-"` Source TransactionSource `json:"-"` Origin SpanOrigin `json:"origin,omitempty"` // mu protects concurrent writes to map fields mu sync.RWMutex // sample rate the span was sampled with. sampleRate float64 // ctx is the context where the span was started. Always non-nil. ctx context.Context // Dynamic Sampling context dynamicSamplingContext DynamicSamplingContext // parent refers to the immediate local parent span. A remote parent span is // only referenced by setting ParentSpanID. parent *Span // recorder stores all spans in a transaction. Guaranteed to be non-nil. recorder *spanRecorder // span context, can only be set on transactions contexts map[string]Context // a Once instance to make sure that Finish() is only called once. finishOnce sync.Once // explicitSampled is a flag for configuring sampling by using `WithSpanSampled` option. explicitSampled Sampled } // TraceParentContext describes the context of a (remote) parent span. // // The context is normally extracted from a received "sentry-trace" header and // used to initialize a new transaction. // // Note: the name might be not the best one. It was taken mostly to stay aligned // with other SDKs, and it alludes to W3C "traceparent" header (https://www.w3.org/TR/trace-context/), // which serves a similar purpose to "sentry-trace". We should eventually consider // making this type internal-only and give it a better name. type TraceParentContext struct { TraceID TraceID ParentSpanID SpanID Sampled Sampled } // (*) Note on maligned: // // We prefer readability over optimal memory layout. If we ever decide to // reorder fields, we can use a tool: // // go run honnef.co/go/tools/cmd/structlayout -json . Span | go run honnef.co/go/tools/cmd/structlayout-optimize // // Other structs would deserve reordering as well, for example Event. // TODO: make Span.Tags and Span.Data opaque types (struct{unexported []slice}). // An opaque type allows us to add methods and make it more convenient to use // than maps, because maps require careful nil checks to use properly or rely on // explicit initialization for every span, even when there might be no // tags/data. For Span.Data, must gracefully handle values that cannot be // marshaled into JSON (see transport.go:getRequestBodyFromEvent). // StartSpan starts a new span to describe an operation. The new span will be a // child of the last span stored in ctx, if any. // // One or more options can be used to modify the span properties. Typically one // option as a function literal is enough. Combining multiple options can be // useful to define and reuse specific properties with named functions. // // Caller should call the Finish method on the span to mark its end. Finishing a // root span sends the span and all of its children, recursively, as a // transaction to Sentry. func StartSpan(ctx context.Context, operation string, options ...SpanOption) *Span { parent, hasParent := ctx.Value(spanContextKey{}).(*Span) var span Span span = Span{ // defaults Op: operation, StartTime: time.Now(), Sampled: SampledUndefined, ctx: context.WithValue(ctx, spanContextKey{}, &span), parent: parent, } _, err := rand.Read(span.SpanID[:]) if err != nil { panic(err) } if hasParent { span.TraceID = parent.TraceID span.ParentSpanID = parent.SpanID span.Origin = parent.Origin } else { // Only set the Source if this is a transaction span.Source = SourceCustom span.Origin = SpanOriginManual // Implementation note: // // While math/rand is ~2x faster than crypto/rand (exact // difference depends on hardware / OS), crypto/rand is probably // fast enough and a safer choice. // // For reference, OpenTelemetry [1] uses crypto/rand to seed // math/rand. AFAICT this approach does not preserve the // properties from crypto/rand that make it suitable for // cryptography. While it might be debatable whether those // properties are important for us here, again, we're taking the // safer path. // // See [2a] & [2b] for a discussion of some of the properties we // obtain by using crypto/rand and [3a] & [3b] for why we avoid // math/rand. // // Because the math/rand seed has only 64 bits (int64), if the // first thing we do after seeding an RNG is to read in a random // TraceID, there are only 2^64 possible values. Compared to // UUID v4 that have 122 random bits, there is a much greater // chance of collision [4a] & [4b]. // // [1]: https://github.com/open-telemetry/opentelemetry-go/blob/958041ddf619a128/sdk/trace/trace.go#L25-L31 // [2a]: https://security.stackexchange.com/q/120352/246345 // [2b]: https://security.stackexchange.com/a/120365/246345 // [3a]: https://github.com/golang/go/issues/11871#issuecomment-126333686 // [3b]: https://github.com/golang/go/issues/11871#issuecomment-126357889 // [4a]: https://en.wikipedia.org/wiki/Universally_unique_identifier#Collisions // [4b]: https://www.wolframalpha.com/input/?i=sqrt%282*2%5E64*ln%281%2F%281-0.5%29%29%29 _, err := rand.Read(span.TraceID[:]) if err != nil { panic(err) } } // Apply options to override defaults. for _, option := range options { option(&span) } span.Sampled = span.sample() span.recorder = &spanRecorder{} if hasParent { span.recorder = parent.spanRecorder() } span.recorder.record(&span) clientOptions := span.clientOptions() if clientOptions.EnableTracing { hub := hubFromContext(ctx) hub.Scope().SetSpan(&span) } return &span } // Finish sets the span's end time, unless already set. If the span is the root // of a span tree, Finish sends the span tree to Sentry as a transaction. // // The logic is executed at most once per span, so that (incorrectly) calling it twice // never double sends to Sentry. func (s *Span) Finish() { s.finishOnce.Do(s.doFinish) } // Context returns the context containing the span. func (s *Span) Context() context.Context { return s.ctx } // StartChild starts a new child span. // // The call span.StartChild(operation, options...) is a shortcut for // StartSpan(span.Context(), operation, options...). func (s *Span) StartChild(operation string, options ...SpanOption) *Span { return StartSpan(s.Context(), operation, options...) } // SetTag sets a tag on the span. It is recommended to use SetTag instead of // accessing the tags map directly as SetTag takes care of initializing the map // when necessary. func (s *Span) SetTag(name, value string) { s.mu.Lock() defer s.mu.Unlock() if s.Tags == nil { s.Tags = make(map[string]string) } s.Tags[name] = value } // SetData sets a data on the span. It is recommended to use SetData instead of // accessing the data map directly as SetData takes care of initializing the map // when necessary. func (s *Span) SetData(name string, value interface{}) { if value == nil { return } s.mu.Lock() defer s.mu.Unlock() if s.Data == nil { s.Data = make(map[string]interface{}) } s.Data[name] = value } // SetContext sets a context on the span. It is recommended to use SetContext instead of // accessing the contexts map directly as SetContext takes care of initializing the map // when necessary. func (s *Span) SetContext(key string, value Context) { s.mu.Lock() defer s.mu.Unlock() if s.contexts == nil { s.contexts = make(map[string]Context) } s.contexts[key] = value } // IsTransaction checks if the given span is a transaction. func (s *Span) IsTransaction() bool { return s.parent == nil } // GetTransaction returns the transaction that contains this span. // // For transaction spans it returns itself. For spans that were created manually // the method returns "nil". func (s *Span) GetTransaction() *Span { spanRecorder := s.spanRecorder() if spanRecorder == nil { // This probably means that the Span was created manually (not via // StartTransaction/StartSpan or StartChild). // Return "nil" to indicate that it's not a normal situation. return nil } recorderRoot := spanRecorder.root() if recorderRoot == nil { // Same as above: manually created Span. return nil } return recorderRoot } // TODO(tracing): maybe add shortcuts to get/set transaction name. Right now the // transaction name is in the Scope, as it has existed there historically, prior // to tracing. // // See Scope.Transaction() and Scope.SetTransaction(). // // func (s *Span) TransactionName() string // func (s *Span) SetTransactionName(name string) // ToSentryTrace returns the serialized TraceParentContext from a transaction/span. // Use this function to propagate the TraceParentContext to a downstream SDK, // either as the value of the "sentry-trace" HTTP header, or as an html "sentry-trace" meta tag. func (s *Span) ToSentryTrace() string { // TODO(tracing): add instrumentation for outgoing HTTP requests using // ToSentryTrace. var b strings.Builder fmt.Fprintf(&b, "%s-%s", s.TraceID.Hex(), s.SpanID.Hex()) switch s.Sampled { case SampledTrue: b.WriteString("-1") case SampledFalse: b.WriteString("-0") } return b.String() } // ToTraceparent returns the W3C traceparent header value for the span. func (s *Span) ToTraceparent() string { traceFlags := "00" if s.Sampled == SampledTrue { traceFlags = "01" } return fmt.Sprintf("00-%s-%s-%s", s.TraceID.String(), s.SpanID.String(), traceFlags) } // ToBaggage returns the serialized DynamicSamplingContext from a transaction. // Use this function to propagate the DynamicSamplingContext to a downstream SDK, // either as the value of the "baggage" HTTP header, or as an html "baggage" meta tag. func (s *Span) ToBaggage() string { t := s.GetTransaction() if t == nil { return "" } // In case there is currently no frozen DynamicSamplingContext attached to the transaction, // create one from the properties of the transaction. if !s.dynamicSamplingContext.IsFrozen() { // This will return a frozen DynamicSamplingContext. if dsc := DynamicSamplingContextFromTransaction(t); dsc.HasEntries() { t.dynamicSamplingContext = dsc } } return t.dynamicSamplingContext.String() } // SetDynamicSamplingContext sets the given dynamic sampling context on the // current transaction. func (s *Span) SetDynamicSamplingContext(dsc DynamicSamplingContext) { if s.IsTransaction() { s.dynamicSamplingContext = dsc } } // shouldIgnoreStatusCode checks if the transaction should be ignored based on HTTP status code. func (s *Span) shouldIgnoreStatusCode() bool { if !s.IsTransaction() { return false } ignoreStatusCodes := s.clientOptions().TraceIgnoreStatusCodes if len(ignoreStatusCodes) == 0 { return false } s.mu.Lock() statusCodeData, exists := s.Data["http.response.status_code"] s.mu.Unlock() if !exists { return false } statusCode, ok := statusCodeData.(int) if !ok { return false } for _, ignoredRange := range ignoreStatusCodes { switch len(ignoredRange) { case 1: // Single status code if statusCode == ignoredRange[0] { s.mu.Lock() s.Sampled = SampledFalse s.mu.Unlock() debuglog.Printf("dropping transaction with status code %v: found in TraceIgnoreStatusCodes", statusCode) return true } case 2: // Range of status codes [min, max] if ignoredRange[0] <= statusCode && statusCode <= ignoredRange[1] { s.mu.Lock() s.Sampled = SampledFalse s.mu.Unlock() debuglog.Printf("dropping transaction with status code %v: found in TraceIgnoreStatusCodes range [%d, %d]", statusCode, ignoredRange[0], ignoredRange[1]) return true } default: debuglog.Printf("incorrect TraceIgnoreStatusCodes format: %v", ignoredRange) } } return false } // doFinish runs the actual Span.Finish() logic. func (s *Span) doFinish() { if s.EndTime.IsZero() { s.EndTime = monotonicTimeSince(s.StartTime) } hub := hubFromContext(s.ctx) if !s.IsTransaction() { if s.parent != nil { hub.Scope().SetSpan(s.parent) } } if s.shouldIgnoreStatusCode() { return } if !s.Sampled.Bool() { return } event := s.toEvent() if event == nil { return } // TODO(tracing): add breadcrumbs // (see https://github.com/getsentry/sentry-python/blob/f6f3525f8812f609/sentry_sdk/tracing.py#L372) hub.CaptureEvent(event) } // sentryTracePattern matches either // // TRACE_ID - SPAN_ID // [[:xdigit:]]{32}-[[:xdigit:]]{16} // // or // // TRACE_ID - SPAN_ID - SAMPLED // [[:xdigit:]]{32}-[[:xdigit:]]{16}-[01] var sentryTracePattern = regexp.MustCompile(`^([[:xdigit:]]{32})-([[:xdigit:]]{16})(?:-([01]))?$`) // updateFromSentryTrace parses a sentry-trace HTTP header (as returned by // ToSentryTrace) and updates fields of the span. If the header cannot be // recognized as valid, the span is left unchanged. The returned value indicates // whether the span was updated. func (s *Span) updateFromSentryTrace(header []byte) (updated bool) { m := sentryTracePattern.FindSubmatch(header) if m == nil { // no match return false } _, _ = hex.Decode(s.TraceID[:], m[1]) _, _ = hex.Decode(s.ParentSpanID[:], m[2]) if len(m[3]) != 0 { switch m[3][0] { case '0': s.Sampled = SampledFalse case '1': s.Sampled = SampledTrue } } return true } func (s *Span) updateFromBaggage(header []byte) { if s.IsTransaction() { dsc, err := DynamicSamplingContextFromHeader(header) if err != nil { return } s.dynamicSamplingContext = dsc } } func (s *Span) clientOptions() *ClientOptions { client := hubFromContext(s.ctx).Client() if client != nil { return &client.options } return &ClientOptions{} } func (s *Span) sample() Sampled { clientOptions := s.clientOptions() // https://develop.sentry.dev/sdk/performance/#sampling // #1 tracing is not enabled. if !clientOptions.EnableTracing { debuglog.Printf("Dropping transaction: EnableTracing is set to %t", clientOptions.EnableTracing) s.sampleRate = 0.0 return SampledFalse } // #2 explicit sampling decision via StartSpan/StartTransaction options. if s.explicitSampled != SampledUndefined { debuglog.Printf("Using explicit sampling decision from StartSpan/StartTransaction: %v", s.explicitSampled) switch s.explicitSampled { case SampledTrue: s.sampleRate = 1.0 case SampledFalse: s.sampleRate = 0.0 } return s.explicitSampled } // Variant for non-transaction spans: they inherit the parent decision. // Note: non-transaction should always have a parent, but we check both // conditions anyway -- the first for semantic meaning, the second to // avoid a nil pointer dereference. if !s.IsTransaction() && s.parent != nil { return s.parent.Sampled } // #3 use TracesSampler from ClientOptions. sampler := clientOptions.TracesSampler samplingContext := SamplingContext{ Span: s, Parent: s.parent, } if sampler != nil { tracesSamplerSampleRate := sampler.Sample(samplingContext) s.sampleRate = tracesSamplerSampleRate // tracesSampler can update the sample_rate on frozen DSC if s.dynamicSamplingContext.HasEntries() { s.dynamicSamplingContext.Entries["sample_rate"] = strconv.FormatFloat(tracesSamplerSampleRate, 'f', -1, 64) } if tracesSamplerSampleRate < 0.0 || tracesSamplerSampleRate > 1.0 { debuglog.Printf("Dropping transaction: Returned TracesSampler rate is out of range [0.0, 1.0]: %f", tracesSamplerSampleRate) return SampledFalse } if tracesSamplerSampleRate == 0.0 { debuglog.Printf("Dropping transaction: Returned TracesSampler rate is: %f", tracesSamplerSampleRate) return SampledFalse } if rng.Float64() < tracesSamplerSampleRate { return SampledTrue } debuglog.Printf("Dropping transaction: TracesSampler returned rate: %f", tracesSamplerSampleRate) return SampledFalse } // #4 inherit parent decision. if s.Sampled != SampledUndefined { debuglog.Printf("Using sampling decision from parent: %v", s.Sampled) switch s.Sampled { case SampledTrue: s.sampleRate = 1.0 case SampledFalse: s.sampleRate = 0.0 } return s.Sampled } // #5 use TracesSampleRate from ClientOptions. sampleRate := clientOptions.TracesSampleRate s.sampleRate = sampleRate // tracesSampleRate can update the sample_rate on frozen DSC if s.dynamicSamplingContext.HasEntries() { s.dynamicSamplingContext.Entries["sample_rate"] = strconv.FormatFloat(sampleRate, 'f', -1, 64) } if sampleRate < 0.0 || sampleRate > 1.0 { debuglog.Printf("Dropping transaction: TracesSampleRate out of range [0.0, 1.0]: %f", sampleRate) return SampledFalse } if sampleRate == 0.0 { debuglog.Printf("Dropping transaction: TracesSampleRate rate is: %f", sampleRate) return SampledFalse } if rng.Float64() < sampleRate { return SampledTrue } return SampledFalse } func (s *Span) toEvent() *Event { s.mu.Lock() defer s.mu.Unlock() if !s.IsTransaction() { return nil // only transactions can be transformed into events } children := s.recorder.children() finished := make([]*Span, 0, len(children)) for _, child := range children { if child.EndTime.IsZero() { debuglog.Printf("Dropped unfinished span: Op=%q TraceID=%s SpanID=%s", child.Op, child.TraceID, child.SpanID) continue } finished = append(finished, child) } // Create and attach a DynamicSamplingContext to the transaction. // If the DynamicSamplingContext is not frozen at this point, we can assume being head of trace. if !s.dynamicSamplingContext.IsFrozen() { s.dynamicSamplingContext = DynamicSamplingContextFromTransaction(s) } contexts := make(map[string]Context, len(s.contexts)+1) for k, v := range s.contexts { contexts[k] = cloneContext(v) } contexts["trace"] = s.traceContext().Map() // Make sure that the transaction source is valid transactionSource := s.Source if !transactionSource.isValid() { transactionSource = SourceCustom } return &Event{ Type: transactionType, Transaction: s.Name, Contexts: contexts, Tags: s.Tags, Timestamp: s.EndTime, StartTime: s.StartTime, Spans: finished, TransactionInfo: &TransactionInfo{ Source: transactionSource, }, sdkMetaData: SDKMetaData{ dsc: s.dynamicSamplingContext, }, } } func (s *Span) traceContext() *TraceContext { return &TraceContext{ TraceID: s.TraceID, SpanID: s.SpanID, ParentSpanID: s.ParentSpanID, Op: s.Op, Data: s.Data, Description: s.Description, Status: s.Status, } } // spanRecorder stores the span tree. Guaranteed to be non-nil. func (s *Span) spanRecorder() *spanRecorder { return s.recorder } // ParseTraceParentContext parses a sentry-trace header and builds a TraceParentContext from the // parsed values. If the header was parsed correctly, the second returned argument // ("valid") will be set to true, otherwise (e.g., empty or malformed header) it will // be false. func ParseTraceParentContext(header []byte) (traceParentContext TraceParentContext, valid bool) { s := Span{} updated := s.updateFromSentryTrace(header) if !updated { return TraceParentContext{}, false } return TraceParentContext{ TraceID: s.TraceID, ParentSpanID: s.ParentSpanID, Sampled: s.Sampled, }, true } // TraceID identifies a trace. type TraceID [16]byte func (id TraceID) Hex() []byte { b := make([]byte, hex.EncodedLen(len(id))) hex.Encode(b, id[:]) return b } func (id TraceID) String() string { return string(id.Hex()) } func (id TraceID) MarshalText() ([]byte, error) { return id.Hex(), nil } // SpanID identifies a span. type SpanID [8]byte func (id SpanID) Hex() []byte { b := make([]byte, hex.EncodedLen(len(id))) hex.Encode(b, id[:]) return b } func (id SpanID) String() string { return string(id.Hex()) } func (id SpanID) MarshalText() ([]byte, error) { return id.Hex(), nil } // Zero values of TraceID and SpanID used for comparisons. var ( zeroTraceID TraceID zeroSpanID SpanID ) // Contains information about how the name of the transaction was determined. type TransactionSource string const ( SourceCustom TransactionSource = "custom" SourceURL TransactionSource = "url" SourceRoute TransactionSource = "route" SourceView TransactionSource = "view" SourceComponent TransactionSource = "component" SourceTask TransactionSource = "task" ) // A set of all valid transaction sources. var allTransactionSources = map[TransactionSource]struct{}{ SourceCustom: {}, SourceURL: {}, SourceRoute: {}, SourceView: {}, SourceComponent: {}, SourceTask: {}, } // isValid returns 'true' if the given transaction source is a valid // source as recognized by the envelope protocol: // https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations func (ts TransactionSource) isValid() bool { _, found := allTransactionSources[ts] return found } // SpanStatus is the status of a span. type SpanStatus uint8 // Implementation note: // // In Relay (ingestion), the SpanStatus type is an enum used as // Annotated when embedded in structs, making it effectively // Option. It means the status is either null or one of the known // string values. // // In Snuba (search), the SpanStatus is stored as an uint8 and defaulted to 2 // ("unknown") when not set. It means that Discover searches for // `transaction.status:unknown` return both transactions/spans with status // `null` or `"unknown"`. Searches for `transaction.status:""` return nothing. // // With that in mind, the Go SDK default is SpanStatusUndefined, which is // null/omitted when serializing to JSON, but integrations may update the status // automatically based on contextual information. const ( SpanStatusUndefined SpanStatus = iota SpanStatusOK SpanStatusCanceled SpanStatusUnknown SpanStatusInvalidArgument SpanStatusDeadlineExceeded SpanStatusNotFound SpanStatusAlreadyExists SpanStatusPermissionDenied SpanStatusResourceExhausted SpanStatusFailedPrecondition SpanStatusAborted SpanStatusOutOfRange SpanStatusUnimplemented SpanStatusInternalError SpanStatusUnavailable SpanStatusDataLoss SpanStatusUnauthenticated maxSpanStatus ) var spanStatuses = [maxSpanStatus]string{ "", "ok", "cancelled", // [sic] "unknown", "invalid_argument", "deadline_exceeded", "not_found", "already_exists", "permission_denied", "resource_exhausted", "failed_precondition", "aborted", "out_of_range", "unimplemented", "internal_error", "unavailable", "data_loss", "unauthenticated", } func (ss SpanStatus) String() string { if ss >= maxSpanStatus { return "" } return spanStatuses[ss] } func (ss SpanStatus) MarshalJSON() ([]byte, error) { s := ss.String() if s == "" { return []byte("null"), nil } return json.Marshal(s) } // A TraceContext carries information about an ongoing trace and is meant to be // stored in Event.Contexts (as *TraceContext). type TraceContext struct { TraceID TraceID `json:"trace_id"` SpanID SpanID `json:"span_id"` ParentSpanID SpanID `json:"parent_span_id,omitzero"` Op string `json:"op,omitempty"` Description string `json:"description,omitempty"` Status SpanStatus `json:"status,omitempty"` Data map[string]interface{} `json:"data,omitempty"` } func (tc TraceContext) Map() map[string]interface{} { m := map[string]interface{}{ "trace_id": tc.TraceID, "span_id": tc.SpanID, } if tc.ParentSpanID != [8]byte{} { m["parent_span_id"] = tc.ParentSpanID } if tc.Op != "" { m["op"] = tc.Op } if tc.Description != "" { m["description"] = tc.Description } if tc.Status > 0 && tc.Status < maxSpanStatus { m["status"] = tc.Status } if len(tc.Data) > 0 { m["data"] = tc.Data } return m } // Sampled signifies a sampling decision. type Sampled int8 // The possible trace sampling decisions are: SampledFalse, SampledUndefined // (default) and SampledTrue. const ( SampledFalse Sampled = -1 SampledUndefined Sampled = 0 SampledTrue Sampled = 1 ) func (s Sampled) String() string { switch s { case SampledFalse: return "SampledFalse" case SampledUndefined: return "SampledUndefined" case SampledTrue: return "SampledTrue" default: return fmt.Sprintf("SampledInvalid(%d)", s) } } // Bool returns true if the sample decision is SampledTrue, false otherwise. func (s Sampled) Bool() bool { return s == SampledTrue } // A SpanOption is a function that can modify the properties of a span. type SpanOption func(s *Span) // WithTransactionName option sets the name of the current transaction. // // A span tree has a single transaction name, therefore using this option when // starting a span affects the span tree as a whole, potentially overwriting a // name set previously. func WithTransactionName(name string) SpanOption { return func(s *Span) { s.Name = name } } // WithDescription sets the description of a span. func WithDescription(description string) SpanOption { return func(s *Span) { s.Description = description } } // WithOpName sets the operation name for a given span. func WithOpName(name string) SpanOption { return func(s *Span) { s.Op = name } } // WithTransactionSource sets the source of the transaction name. // // Note: if the transaction source is not a valid source (as described // by the spec https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations), // it will be corrected to "custom" eventually, before the transaction is sent. func WithTransactionSource(source TransactionSource) SpanOption { return func(s *Span) { s.Source = source } } // WithSpanSampled updates the sampling flag for a given span. func WithSpanSampled(sampled Sampled) SpanOption { return func(s *Span) { s.explicitSampled = sampled } } // WithSpanOrigin sets the origin of the span. func WithSpanOrigin(origin SpanOrigin) SpanOption { return func(s *Span) { s.Origin = origin } } // ContinueTrace continues a trace based on traceparent and baggage values. // If the SDK is configured with tracing enabled, // this function returns populated SpanOption. // In any other cases, it populates the propagation context on the scope. func ContinueTrace(hub *Hub, traceparent, baggage string) SpanOption { scope := hub.Scope() propagationContext, _ := PropagationContextFromHeaders(traceparent, baggage) scope.SetPropagationContext(propagationContext) return ContinueFromHeaders(traceparent, baggage) } // ContinueFromRequest returns a span option that updates the span to continue // an existing trace. If it cannot detect an existing trace in the request, the // span will be left unchanged. // // ContinueFromRequest is an alias for: // // ContinueFromHeaders(r.Header.Get(SentryTraceHeader), r.Header.Get(SentryBaggageHeader)). func ContinueFromRequest(r *http.Request) SpanOption { return ContinueFromHeaders(r.Header.Get(SentryTraceHeader), r.Header.Get(SentryBaggageHeader)) } // ContinueFromHeaders returns a span option that updates the span to continue // an existing TraceID and propagates the Dynamic Sampling context. func ContinueFromHeaders(trace, baggage string) SpanOption { return func(s *Span) { if trace != "" { s.updateFromSentryTrace([]byte(trace)) if baggage != "" { s.updateFromBaggage([]byte(baggage)) } // In case a sentry-trace header is present but there are no sentry-related // values in the baggage, create an empty, frozen DynamicSamplingContext. if !s.dynamicSamplingContext.HasEntries() { s.dynamicSamplingContext = DynamicSamplingContext{ Frozen: true, } } } } } // ContinueFromTrace returns a span option that updates the span to continue // an existing TraceID. func ContinueFromTrace(trace string) SpanOption { return func(s *Span) { if trace == "" { return } s.updateFromSentryTrace([]byte(trace)) } } // spanContextKey is used to store span values in contexts. type spanContextKey struct{} // TransactionFromContext returns the root span of the current transaction. It // returns nil if no transaction is tracked in the context. func TransactionFromContext(ctx context.Context) *Span { if span, ok := ctx.Value(spanContextKey{}).(*Span); ok { return span.recorder.root() } return nil } // SpanFromContext returns the last span stored in the context, or nil if no span // is set on the context. func SpanFromContext(ctx context.Context) *Span { if span, ok := ctx.Value(spanContextKey{}).(*Span); ok { return span } return nil } // StartTransaction will create a transaction (root span) if there's no existing // transaction in the context otherwise, it will return the existing transaction. func StartTransaction(ctx context.Context, name string, options ...SpanOption) *Span { currentTransaction, exists := ctx.Value(spanContextKey{}).(*Span) if exists { currentTransaction.ctx = ctx return currentTransaction } options = append(options, WithTransactionName(name)) return StartSpan( ctx, "", options..., ) } // HTTPtoSpanStatus converts an HTTP status code to a SpanStatus. func HTTPtoSpanStatus(code int) SpanStatus { if code < http.StatusBadRequest { return SpanStatusOK } if http.StatusBadRequest <= code && code < http.StatusInternalServerError { switch code { case http.StatusForbidden: return SpanStatusPermissionDenied case http.StatusNotFound: return SpanStatusNotFound case http.StatusTooManyRequests: return SpanStatusResourceExhausted case http.StatusRequestEntityTooLarge: return SpanStatusFailedPrecondition case http.StatusUnauthorized: return SpanStatusUnauthenticated case http.StatusConflict: return SpanStatusAlreadyExists default: return SpanStatusInvalidArgument } } if http.StatusInternalServerError <= code && code < 600 { switch code { case http.StatusGatewayTimeout: return SpanStatusDeadlineExceeded case http.StatusNotImplemented: return SpanStatusUnimplemented case http.StatusServiceUnavailable: return SpanStatusUnavailable default: return SpanStatusInternalError } } return SpanStatusUnknown } ================================================ FILE: vendor/github.com/getsentry/sentry-go/transport.go ================================================ package sentry import ( "bytes" "context" "crypto/tls" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "sync" "time" "github.com/getsentry/sentry-go/internal/debuglog" httpinternal "github.com/getsentry/sentry-go/internal/http" "github.com/getsentry/sentry-go/internal/protocol" "github.com/getsentry/sentry-go/internal/ratelimit" "github.com/getsentry/sentry-go/internal/util" ) const ( defaultBufferSize = 1000 defaultTimeout = time.Second * 30 ) // Transport is used by the Client to deliver events to remote server. type Transport interface { Flush(timeout time.Duration) bool FlushWithContext(ctx context.Context) bool Configure(options ClientOptions) SendEvent(event *Event) Close() } func getProxyConfig(options ClientOptions) func(*http.Request) (*url.URL, error) { if options.HTTPSProxy != "" { return func(*http.Request) (*url.URL, error) { return url.Parse(options.HTTPSProxy) } } if options.HTTPProxy != "" { return func(*http.Request) (*url.URL, error) { return url.Parse(options.HTTPProxy) } } return http.ProxyFromEnvironment } func getTLSConfig(options ClientOptions) *tls.Config { if options.CaCerts != nil { // #nosec G402 -- We should be using `MinVersion: tls.VersionTLS12`, // but we don't want to break peoples code without the major bump. return &tls.Config{ RootCAs: options.CaCerts, } } return nil } func getRequestBodyFromEvent(event *Event) []byte { body, err := json.Marshal(event) if err == nil { return body } msg := fmt.Sprintf("Could not encode original event as JSON. "+ "Succeeded by removing Breadcrumbs, Contexts and Extra. "+ "Please verify the data you attach to the scope. "+ "Error: %s", err) // Try to serialize the event, with all the contextual data that allows for interface{} stripped. event.Breadcrumbs = nil event.Contexts = nil event.Extra = map[string]interface{}{ "info": msg, } body, err = json.Marshal(event) if err == nil { debuglog.Println(msg) return body } // This should _only_ happen when Event.Exception[0].Stacktrace.Frames[0].Vars is unserializable // Which won't ever happen, as we don't use it now (although it's the part of public interface accepted by Sentry) // Juuust in case something, somehow goes utterly wrong. debuglog.Println("Event couldn't be marshaled, even with stripped contextual data. Skipping delivery. " + "Please notify the SDK owners with possibly broken payload.") return nil } func encodeAttachment(enc *json.Encoder, b io.Writer, attachment *Attachment) error { // Attachment header err := enc.Encode(struct { Type string `json:"type"` Length int `json:"length"` Filename string `json:"filename"` ContentType string `json:"content_type,omitempty"` }{ Type: "attachment", Length: len(attachment.Payload), Filename: attachment.Filename, ContentType: attachment.ContentType, }) if err != nil { return err } // Attachment payload if _, err = b.Write(attachment.Payload); err != nil { return err } // "Envelopes should be terminated with a trailing newline." // // [1]: https://develop.sentry.dev/sdk/envelopes/#envelopes if _, err := b.Write([]byte("\n")); err != nil { return err } return nil } func encodeEnvelopeItem(enc *json.Encoder, itemType string, body json.RawMessage) error { // Item header err := enc.Encode(struct { Type string `json:"type"` Length int `json:"length"` }{ Type: itemType, Length: len(body), }) if err == nil { // payload err = enc.Encode(body) } return err } func encodeEnvelopeLogs(enc *json.Encoder, count int, body json.RawMessage) error { err := enc.Encode( struct { Type string `json:"type"` ItemCount int `json:"item_count"` ContentType string `json:"content_type"` }{ Type: logEvent.Type, ItemCount: count, ContentType: logEvent.ContentType, }) if err == nil { err = enc.Encode(body) } return err } func encodeEnvelopeMetrics(enc *json.Encoder, count int, body json.RawMessage) error { err := enc.Encode( struct { Type string `json:"type"` ItemCount int `json:"item_count"` ContentType string `json:"content_type"` }{ Type: traceMetricEvent.Type, ItemCount: count, ContentType: traceMetricEvent.ContentType, }) if err == nil { err = enc.Encode(body) } return err } func envelopeFromBody(event *Event, dsn *Dsn, sentAt time.Time, body json.RawMessage) (*bytes.Buffer, error) { var b bytes.Buffer enc := json.NewEncoder(&b) // Construct the trace envelope header var trace = map[string]string{} if dsc := event.sdkMetaData.dsc; dsc.HasEntries() { for k, v := range dsc.Entries { trace[k] = v } } // Envelope header err := enc.Encode(struct { EventID EventID `json:"event_id"` SentAt time.Time `json:"sent_at"` Dsn string `json:"dsn"` Sdk map[string]string `json:"sdk"` Trace map[string]string `json:"trace,omitempty"` }{ EventID: event.EventID, SentAt: sentAt, Trace: trace, Dsn: dsn.String(), Sdk: map[string]string{ "name": event.Sdk.Name, "version": event.Sdk.Version, }, }) if err != nil { return nil, err } switch event.Type { case transactionType, checkInType: err = encodeEnvelopeItem(enc, event.Type, body) case logEvent.Type: err = encodeEnvelopeLogs(enc, len(event.Logs), body) case traceMetricEvent.Type: err = encodeEnvelopeMetrics(enc, len(event.Metrics), body) default: err = encodeEnvelopeItem(enc, eventType, body) } if err != nil { return nil, err } // Attachments for _, attachment := range event.Attachments { if err := encodeAttachment(enc, &b, attachment); err != nil { return nil, err } } return &b, nil } func getRequestFromEvent(ctx context.Context, event *Event, dsn *Dsn) (r *http.Request, err error) { defer func() { if r != nil { r.Header.Set("User-Agent", fmt.Sprintf("%s/%s", event.Sdk.Name, event.Sdk.Version)) r.Header.Set("Content-Type", "application/x-sentry-envelope") auth := fmt.Sprintf("Sentry sentry_version=%s, "+ "sentry_client=%s/%s, sentry_key=%s", apiVersion, event.Sdk.Name, event.Sdk.Version, dsn.GetPublicKey()) // The key sentry_secret is effectively deprecated and no longer needs to be set. // However, since it was required in older self-hosted versions, // it should still passed through to Sentry if set. if dsn.GetSecretKey() != "" { auth = fmt.Sprintf("%s, sentry_secret=%s", auth, dsn.GetSecretKey()) } r.Header.Set("X-Sentry-Auth", auth) } }() body := getRequestBodyFromEvent(event) if body == nil { return nil, errors.New("event could not be marshaled") } envelope, err := envelopeFromBody(event, dsn, time.Now(), body) if err != nil { return nil, err } if ctx == nil { ctx = context.Background() } return http.NewRequestWithContext( ctx, http.MethodPost, dsn.GetAPIURL().String(), envelope, ) } // ================================ // HTTPTransport // ================================ // A batch groups items that are processed sequentially. type batch struct { items chan batchItem started chan struct{} // closed to signal items started to be worked on done chan struct{} // closed to signal completion of all items } type batchItem struct { request *http.Request category ratelimit.Category eventIdentifier string } // HTTPTransport is the default, non-blocking, implementation of Transport. // // Clients using this transport will enqueue requests in a buffer and return to // the caller before any network communication has happened. Requests are sent // to Sentry sequentially from a background goroutine. type HTTPTransport struct { dsn *Dsn client *http.Client transport http.RoundTripper // buffer is a channel of batches. Calling Flush terminates work on the // current in-flight items and starts a new batch for subsequent events. buffer chan batch startOnce sync.Once closeOnce sync.Once // Size of the transport buffer. Defaults to 30. BufferSize int // HTTP Client request timeout. Defaults to 30 seconds. Timeout time.Duration mu sync.RWMutex limits ratelimit.Map // receiving signal will terminate worker. done chan struct{} } // NewHTTPTransport returns a new pre-configured instance of HTTPTransport. func NewHTTPTransport() *HTTPTransport { transport := HTTPTransport{ BufferSize: defaultBufferSize, Timeout: defaultTimeout, done: make(chan struct{}), } return &transport } // Configure is called by the Client itself, providing it it's own ClientOptions. func (t *HTTPTransport) Configure(options ClientOptions) { dsn, err := NewDsn(options.Dsn) if err != nil { debuglog.Printf("%v\n", err) return } t.dsn = dsn // A buffered channel with capacity 1 works like a mutex, ensuring only one // goroutine can access the current batch at a given time. Access is // synchronized by reading from and writing to the channel. t.buffer = make(chan batch, 1) t.buffer <- batch{ items: make(chan batchItem, t.BufferSize), started: make(chan struct{}), done: make(chan struct{}), } if options.HTTPTransport != nil { t.transport = options.HTTPTransport } else { t.transport = &http.Transport{ Proxy: getProxyConfig(options), TLSClientConfig: getTLSConfig(options), } } if options.HTTPClient != nil { t.client = options.HTTPClient } else { t.client = &http.Client{ Transport: t.transport, Timeout: t.Timeout, } } t.startOnce.Do(func() { go t.worker() }) } // SendEvent assembles a new packet out of Event and sends it to the remote server. func (t *HTTPTransport) SendEvent(event *Event) { t.SendEventWithContext(context.Background(), event) } // SendEventWithContext assembles a new packet out of Event and sends it to the remote server. func (t *HTTPTransport) SendEventWithContext(ctx context.Context, event *Event) { if t.dsn == nil { return } category := event.toCategory() if t.disabled(category) { return } request, err := getRequestFromEvent(ctx, event, t.dsn) if err != nil { return } // <-t.buffer is equivalent to acquiring a lock to access the current batch. // A few lines below, t.buffer <- b releases the lock. // // The lock must be held during the select block below to guarantee that // b.items is not closed while trying to send to it. Remember that sending // on a closed channel panics. // // Note that the select block takes a bounded amount of CPU time because of // the default case that is executed if sending on b.items would block. That // is, the event is dropped if it cannot be sent immediately to the b.items // channel (used as a queue). b := <-t.buffer identifier := eventIdentifier(event) select { case b.items <- batchItem{ request: request, category: category, eventIdentifier: identifier, }: debuglog.Printf( "Sending %s to %s project: %s", identifier, t.dsn.GetHost(), t.dsn.GetProjectID(), ) default: debuglog.Println("Event dropped due to transport buffer being full.") } t.buffer <- b } // Flush waits until any buffered events are sent to the Sentry server, blocking // for at most the given timeout. It returns false if the timeout was reached. // In that case, some events may not have been sent. // // Flush should be called before terminating the program to avoid // unintentionally dropping events. // // Do not call Flush indiscriminately after every call to SendEvent. Instead, to // have the SDK send events over the network synchronously, configure it to use // the HTTPSyncTransport in the call to Init. func (t *HTTPTransport) Flush(timeout time.Duration) bool { ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() return t.FlushWithContext(ctx) } // FlushWithContext works like Flush, but it accepts a context.Context instead of a timeout. func (t *HTTPTransport) FlushWithContext(ctx context.Context) bool { return t.flushInternal(ctx.Done()) } func (t *HTTPTransport) flushInternal(timeout <-chan struct{}) bool { // Wait until processing the current batch has started or the timeout. // // We must wait until the worker has seen the current batch, because it is // the only way b.done will be closed. If we do not wait, there is a // possible execution flow in which b.done is never closed, and the only way // out of Flush would be waiting for the timeout, which is undesired. var b batch for { select { case b = <-t.buffer: select { case <-b.started: goto started default: t.buffer <- b } case <-timeout: goto fail } } started: // Signal that there won't be any more items in this batch, so that the // worker inner loop can end. close(b.items) // Start a new batch for subsequent events. t.buffer <- batch{ items: make(chan batchItem, t.BufferSize), started: make(chan struct{}), done: make(chan struct{}), } // Wait until the current batch is done or the timeout. select { case <-b.done: debuglog.Println("Buffer flushed successfully.") return true case <-timeout: goto fail } fail: debuglog.Println("Buffer flushing was canceled or timed out.") return false } // Close will terminate events sending loop. // It useful to prevent goroutines leak in case of multiple HTTPTransport instances initiated. // // Close should be called after Flush and before terminating the program // otherwise some events may be lost. func (t *HTTPTransport) Close() { t.closeOnce.Do(func() { close(t.done) }) } func (t *HTTPTransport) worker() { for b := range t.buffer { // Signal that processing of the current batch has started. close(b.started) // Return the batch to the buffer so that other goroutines can use it. // Equivalent to releasing a lock. t.buffer <- b // Process all batch items. loop: for { select { case <-t.done: return case item, open := <-b.items: if !open { break loop } if t.disabled(item.category) { continue } response, err := t.client.Do(item.request) if err != nil { debuglog.Printf("There was an issue with sending an event: %v", err) continue } util.HandleHTTPResponse(response, item.eventIdentifier) t.mu.Lock() if t.limits == nil { t.limits = make(ratelimit.Map) } t.limits.Merge(ratelimit.FromResponse(response)) t.mu.Unlock() // Drain body up to a limit and close it, allowing the // transport to reuse TCP connections. _, _ = io.CopyN(io.Discard, response.Body, util.MaxDrainResponseBytes) response.Body.Close() } } // Signal that processing of the batch is done. close(b.done) } } func (t *HTTPTransport) disabled(c ratelimit.Category) bool { t.mu.RLock() defer t.mu.RUnlock() disabled := t.limits.IsRateLimited(c) if disabled { debuglog.Printf("Too many requests for %q, backing off till: %v", c, t.limits.Deadline(c)) } return disabled } // ================================ // HTTPSyncTransport // ================================ // HTTPSyncTransport is a blocking implementation of Transport. // // Clients using this transport will send requests to Sentry sequentially and // block until a response is returned. // // The blocking behavior is useful in a limited set of use cases. For example, // use it when deploying code to a Function as a Service ("Serverless") // platform, where any work happening in a background goroutine is not // guaranteed to execute. // // For most cases, prefer HTTPTransport. type HTTPSyncTransport struct { dsn *Dsn client *http.Client transport http.RoundTripper mu sync.Mutex limits ratelimit.Map // HTTP Client request timeout. Defaults to 30 seconds. Timeout time.Duration } // NewHTTPSyncTransport returns a new pre-configured instance of HTTPSyncTransport. func NewHTTPSyncTransport() *HTTPSyncTransport { transport := HTTPSyncTransport{ Timeout: defaultTimeout, limits: make(ratelimit.Map), } return &transport } // Configure is called by the Client itself, providing it it's own ClientOptions. func (t *HTTPSyncTransport) Configure(options ClientOptions) { dsn, err := NewDsn(options.Dsn) if err != nil { debuglog.Printf("%v\n", err) return } t.dsn = dsn if options.HTTPTransport != nil { t.transport = options.HTTPTransport } else { t.transport = &http.Transport{ Proxy: getProxyConfig(options), TLSClientConfig: getTLSConfig(options), } } if options.HTTPClient != nil { t.client = options.HTTPClient } else { t.client = &http.Client{ Transport: t.transport, Timeout: t.Timeout, } } } // SendEvent assembles a new packet out of Event and sends it to the remote server. func (t *HTTPSyncTransport) SendEvent(event *Event) { t.SendEventWithContext(context.Background(), event) } func (t *HTTPSyncTransport) Close() {} // SendEventWithContext assembles a new packet out of Event and sends it to the remote server. func (t *HTTPSyncTransport) SendEventWithContext(ctx context.Context, event *Event) { if t.dsn == nil { return } if t.disabled(event.toCategory()) { return } request, err := getRequestFromEvent(ctx, event, t.dsn) if err != nil { return } identifier := eventIdentifier(event) debuglog.Printf( "Sending %s to %s project: %s", identifier, t.dsn.GetHost(), t.dsn.GetProjectID(), ) response, err := t.client.Do(request) if err != nil { debuglog.Printf("There was an issue with sending an event: %v", err) return } util.HandleHTTPResponse(response, identifier) t.mu.Lock() if t.limits == nil { t.limits = make(ratelimit.Map) } t.limits.Merge(ratelimit.FromResponse(response)) t.mu.Unlock() // Drain body up to a limit and close it, allowing the // transport to reuse TCP connections. _, _ = io.CopyN(io.Discard, response.Body, util.MaxDrainResponseBytes) response.Body.Close() } // Flush is a no-op for HTTPSyncTransport. It always returns true immediately. func (t *HTTPSyncTransport) Flush(_ time.Duration) bool { return true } // FlushWithContext is a no-op for HTTPSyncTransport. It always returns true immediately. func (t *HTTPSyncTransport) FlushWithContext(_ context.Context) bool { return true } func (t *HTTPSyncTransport) disabled(c ratelimit.Category) bool { t.mu.Lock() defer t.mu.Unlock() disabled := t.limits.IsRateLimited(c) if disabled { debuglog.Printf("Too many requests for %q, backing off till: %v", c, t.limits.Deadline(c)) } return disabled } // ================================ // noopTransport // ================================ // noopTransport is an implementation of Transport interface which drops all the events. // Only used internally when an empty DSN is provided, which effectively disables the SDK. type noopTransport struct{} var _ Transport = noopTransport{} func (noopTransport) Configure(ClientOptions) { debuglog.Println("Sentry client initialized with an empty DSN. Using noopTransport. No events will be delivered.") } func (noopTransport) SendEvent(*Event) { debuglog.Println("Event dropped due to noopTransport usage.") } func (noopTransport) Flush(time.Duration) bool { return true } func (noopTransport) FlushWithContext(context.Context) bool { return true } func (noopTransport) Close() {} // ================================ // Internal Transport Adapters // ================================ // newInternalAsyncTransport creates a new AsyncTransport from internal/http // wrapped to satisfy the Transport interface. // // This is not yet exposed in the public API and is for internal experimentation. func newInternalAsyncTransport() Transport { return &internalAsyncTransportAdapter{} } // internalAsyncTransportAdapter wraps the internal AsyncTransport to implement // the root-level Transport interface. type internalAsyncTransportAdapter struct { transport protocol.TelemetryTransport dsn *protocol.Dsn } func (a *internalAsyncTransportAdapter) Configure(options ClientOptions) { transportOptions := httpinternal.TransportOptions{ Dsn: options.Dsn, HTTPClient: options.HTTPClient, HTTPTransport: options.HTTPTransport, HTTPProxy: options.HTTPProxy, HTTPSProxy: options.HTTPSProxy, CaCerts: options.CaCerts, } a.transport = httpinternal.NewAsyncTransport(transportOptions) if options.Dsn != "" { dsn, err := protocol.NewDsn(options.Dsn) if err != nil { debuglog.Printf("Failed to parse DSN in adapter: %v\n", err) } else { a.dsn = dsn } } } func (a *internalAsyncTransportAdapter) SendEvent(event *Event) { header := &protocol.EnvelopeHeader{EventID: string(event.EventID), SentAt: time.Now(), Sdk: &protocol.SdkInfo{Name: event.Sdk.Name, Version: event.Sdk.Version}} if a.dsn != nil { header.Dsn = a.dsn.String() } if header.EventID == "" { header.EventID = protocol.GenerateEventID() } envelope := protocol.NewEnvelope(header) item, err := event.ToEnvelopeItem() if err != nil { debuglog.Printf("Failed to convert event to envelope item: %v", err) return } envelope.AddItem(item) for _, attachment := range event.Attachments { attachmentItem := protocol.NewAttachmentItem(attachment.Filename, attachment.ContentType, attachment.Payload) envelope.AddItem(attachmentItem) } if err := a.transport.SendEnvelope(envelope); err != nil { debuglog.Printf("Error sending envelope: %v", err) } } func (a *internalAsyncTransportAdapter) Flush(timeout time.Duration) bool { return a.transport.Flush(timeout) } func (a *internalAsyncTransportAdapter) FlushWithContext(ctx context.Context) bool { return a.transport.FlushWithContext(ctx) } func (a *internalAsyncTransportAdapter) Close() { a.transport.Close() } ================================================ FILE: vendor/github.com/getsentry/sentry-go/util.go ================================================ package sentry import ( "encoding/json" "fmt" "os" "runtime/debug" "strings" "time" "github.com/getsentry/sentry-go/internal/debuglog" "github.com/getsentry/sentry-go/internal/protocol" exec "golang.org/x/sys/execabs" ) func uuid() string { return protocol.GenerateEventID() } func fileExists(fileName string) bool { _, err := os.Stat(fileName) return err == nil } // monotonicTimeSince replaces uses of time.Now() to take into account the // monotonic clock reading stored in start, such that duration = end - start is // unaffected by changes in the system wall clock. func monotonicTimeSince(start time.Time) (end time.Time) { return start.Add(time.Since(start)) } // nolint: unused func prettyPrint(data interface{}) { dbg, _ := json.MarshalIndent(data, "", " ") fmt.Println(string(dbg)) } // defaultRelease attempts to guess a default release for the currently running // program. func defaultRelease() (release string) { // Return first non-empty environment variable known to hold release info, if any. envs := []string{ "SENTRY_RELEASE", "HEROKU_SLUG_COMMIT", "SOURCE_VERSION", "CODEBUILD_RESOLVED_SOURCE_VERSION", "CIRCLE_SHA1", "GAE_DEPLOYMENT_ID", "GITHUB_SHA", // GitHub Actions - https://help.github.com/en/actions "COMMIT_REF", // Netlify - https://docs.netlify.com/ "VERCEL_GIT_COMMIT_SHA", // Vercel - https://vercel.com/ "ZEIT_GITHUB_COMMIT_SHA", // Zeit (now known as Vercel) "ZEIT_GITLAB_COMMIT_SHA", "ZEIT_BITBUCKET_COMMIT_SHA", } for _, e := range envs { if release = os.Getenv(e); release != "" { debuglog.Printf("Using release from environment variable %s: %s", e, release) return release } } if info, ok := debug.ReadBuildInfo(); ok { buildInfoVcsRevision := revisionFromBuildInfo(info) if len(buildInfoVcsRevision) > 0 { return buildInfoVcsRevision } } // Derive a version string from Git. Example outputs: // v1.0.1-0-g9de4 // v2.0-8-g77df-dirty // 4f72d7 if _, err := exec.LookPath("git"); err == nil { cmd := exec.Command("git", "describe", "--long", "--always", "--dirty") b, err := cmd.Output() if err != nil { // Either Git is not available or the current directory is not a // Git repository. var s strings.Builder fmt.Fprintf(&s, "Release detection failed: %v", err) if err, ok := err.(*exec.ExitError); ok && len(err.Stderr) > 0 { fmt.Fprintf(&s, ": %s", err.Stderr) } debuglog.Print(s.String()) } else { release = strings.TrimSpace(string(b)) debuglog.Printf("Using release from Git: %s", release) return release } } debuglog.Print("Some Sentry features will not be available. See https://docs.sentry.io/product/releases/.") debuglog.Print("To stop seeing this message, pass a Release to sentry.Init or set the SENTRY_RELEASE environment variable.") return "" } func revisionFromBuildInfo(info *debug.BuildInfo) string { for _, setting := range info.Settings { if setting.Key == "vcs.revision" && setting.Value != "" { debuglog.Printf("Using release from debug info: %s", setting.Value) return setting.Value } } return "" } func Pointer[T any](v T) *T { return &v } // eventIdentifier returns a human-readable identifier for the event to be used in log messages. // Format: " []". func eventIdentifier(event *Event) string { var description string switch event.Type { case errorType: description = "error" case transactionType: description = "transaction" case checkInType: description = "check-in" case logEvent.Type: description = fmt.Sprintf("%d log events", len(event.Logs)) case traceMetricEvent.Type: description = fmt.Sprintf("%d metric events", len(event.Metrics)) default: description = fmt.Sprintf("%s event", event.Type) } return fmt.Sprintf("%s [%s]", description, event.EventID) } ================================================ FILE: vendor/github.com/go-chi/chi/v5/.gitignore ================================================ .idea *.sw? .vscode ================================================ FILE: vendor/github.com/go-chi/chi/v5/CHANGELOG.md ================================================ # Changelog ## v5.0.12 (2024-02-16) - History of changes: see https://github.com/go-chi/chi/compare/v5.0.11...v5.0.12 ## v5.0.11 (2023-12-19) - History of changes: see https://github.com/go-chi/chi/compare/v5.0.10...v5.0.11 ## v5.0.10 (2023-07-13) - Fixed small edge case in tests of v5.0.9 for older Go versions - History of changes: see https://github.com/go-chi/chi/compare/v5.0.9...v5.0.10 ## v5.0.9 (2023-07-13) - History of changes: see https://github.com/go-chi/chi/compare/v5.0.8...v5.0.9 ## v5.0.8 (2022-12-07) - History of changes: see https://github.com/go-chi/chi/compare/v5.0.7...v5.0.8 ## v5.0.7 (2021-11-18) - History of changes: see https://github.com/go-chi/chi/compare/v5.0.6...v5.0.7 ## v5.0.6 (2021-11-15) - History of changes: see https://github.com/go-chi/chi/compare/v5.0.5...v5.0.6 ## v5.0.5 (2021-10-27) - History of changes: see https://github.com/go-chi/chi/compare/v5.0.4...v5.0.5 ## v5.0.4 (2021-08-29) - History of changes: see https://github.com/go-chi/chi/compare/v5.0.3...v5.0.4 ## v5.0.3 (2021-04-29) - History of changes: see https://github.com/go-chi/chi/compare/v5.0.2...v5.0.3 ## v5.0.2 (2021-03-25) - History of changes: see https://github.com/go-chi/chi/compare/v5.0.1...v5.0.2 ## v5.0.1 (2021-03-10) - Small improvements - History of changes: see https://github.com/go-chi/chi/compare/v5.0.0...v5.0.1 ## v5.0.0 (2021-02-27) - chi v5, `github.com/go-chi/chi/v5` introduces the adoption of Go's SIV to adhere to the current state-of-the-tools in Go. - chi v1.5.x did not work out as planned, as the Go tooling is too powerful and chi's adoption is too wide. The most responsible thing to do for everyone's benefit is to just release v5 with SIV, so I present to you all, chi v5 at `github.com/go-chi/chi/v5`. I hope someday the developer experience and ergonomics I've been seeking will still come to fruition in some form, see https://github.com/golang/go/issues/44550 - History of changes: see https://github.com/go-chi/chi/compare/v1.5.4...v5.0.0 ## v1.5.4 (2021-02-27) - Undo prior retraction in v1.5.3 as we prepare for v5.0.0 release - History of changes: see https://github.com/go-chi/chi/compare/v1.5.3...v1.5.4 ## v1.5.3 (2021-02-21) - Update go.mod to go 1.16 with new retract directive marking all versions without prior go.mod support - History of changes: see https://github.com/go-chi/chi/compare/v1.5.2...v1.5.3 ## v1.5.2 (2021-02-10) - Reverting allocation optimization as a precaution as go test -race fails. - Minor improvements, see history below - History of changes: see https://github.com/go-chi/chi/compare/v1.5.1...v1.5.2 ## v1.5.1 (2020-12-06) - Performance improvement: removing 1 allocation by foregoing context.WithValue, thank you @bouk for your contribution (https://github.com/go-chi/chi/pull/555). Note: new benchmarks posted in README. - `middleware.CleanPath`: new middleware that clean's request path of double slashes - deprecate & remove `chi.ServerBaseContext` in favour of stdlib `http.Server#BaseContext` - plus other tiny improvements, see full commit history below - History of changes: see https://github.com/go-chi/chi/compare/v4.1.2...v1.5.1 ## v1.5.0 (2020-11-12) - now with go.mod support `chi` dates back to 2016 with it's original implementation as one of the first routers to adopt the newly introduced context.Context api to the stdlib -- set out to design a router that is faster, more modular and simpler than anything else out there -- while not introducing any custom handler types or dependencies. Today, `chi` still has zero dependencies, and in many ways is future proofed from changes, given it's minimal nature. Between versions, chi's iterations have been very incremental, with the architecture and api being the same today as it was originally designed in 2016. For this reason it makes chi a pretty easy project to maintain, as well thanks to the many amazing community contributions over the years to who all help make chi better (total of 86 contributors to date -- thanks all!). Chi has been a labour of love, art and engineering, with the goals to offer beautiful ergonomics, flexibility, performance and simplicity when building HTTP services with Go. I've strived to keep the router very minimal in surface area / code size, and always improving the code wherever possible -- and as of today the `chi` package is just 1082 lines of code (not counting middlewares, which are all optional). As well, I don't have the exact metrics, but from my analysis and email exchanges from companies and developers, chi is used by thousands of projects around the world -- thank you all as there is no better form of joy for me than to have art I had started be helpful and enjoyed by others. And of course I use chi in all of my own projects too :) For me, the aesthetics of chi's code and usage are very important. With the introduction of Go's module support (which I'm a big fan of), chi's past versioning scheme choice to v2, v3 and v4 would mean I'd require the import path of "github.com/go-chi/chi/v4", leading to the lengthy discussion at https://github.com/go-chi/chi/issues/462. Haha, to some, you may be scratching your head why I've spent > 1 year stalling to adopt "/vXX" convention in the import path -- which isn't horrible in general -- but for chi, I'm unable to accept it as I strive for perfection in it's API design, aesthetics and simplicity. It just doesn't feel good to me given chi's simple nature -- I do not foresee a "v5" or "v6", and upgrading between versions in the future will also be just incremental. I do understand versioning is a part of the API design as well, which is why the solution for a while has been to "do nothing", as Go supports both old and new import paths with/out go.mod. However, now that Go module support has had time to iron out kinks and is adopted everywhere, it's time for chi to get with the times. Luckily, I've discovered a path forward that will make me happy, while also not breaking anyone's app who adopted a prior versioning from tags in v2/v3/v4. I've made an experimental release of v1.5.0 with go.mod silently, and tested it with new and old projects, to ensure the developer experience is preserved, and it's largely unnoticed. Fortunately, Go's toolchain will check the tags of a repo and consider the "latest" tag the one with go.mod. However, you can still request a specific older tag such as v4.1.2, and everything will "just work". But new users can just `go get github.com/go-chi/chi` or `go get github.com/go-chi/chi@latest` and they will get the latest version which contains go.mod support, which is v1.5.0+. `chi` will not change very much over the years, just like it hasn't changed much from 4 years ago. Therefore, we will stay on v1.x from here on, starting from v1.5.0. Any breaking changes will bump a "minor" release and backwards-compatible improvements/fixes will bump a "tiny" release. For existing projects who want to upgrade to the latest go.mod version, run: `go get -u github.com/go-chi/chi@v1.5.0`, which will get you on the go.mod version line (as Go's mod cache may still remember v4.x). Brand new systems can run `go get -u github.com/go-chi/chi` or `go get -u github.com/go-chi/chi@latest` to install chi, which will install v1.5.0+ built with go.mod support. My apologies to the developers who will disagree with the decisions above, but, hope you'll try it and see it's a very minor request which is backwards compatible and won't break your existing installations. Cheers all, happy coding! --- ## v4.1.2 (2020-06-02) - fix that handles MethodNotAllowed with path variables, thank you @caseyhadden for your contribution - fix to replace nested wildcards correctly in RoutePattern, thank you @@unmultimedio for your contribution - History of changes: see https://github.com/go-chi/chi/compare/v4.1.1...v4.1.2 ## v4.1.1 (2020-04-16) - fix for issue https://github.com/go-chi/chi/issues/411 which allows for overlapping regexp route to the correct handler through a recursive tree search, thanks to @Jahaja for the PR/fix! - new middleware.RouteHeaders as a simple router for request headers with wildcard support - History of changes: see https://github.com/go-chi/chi/compare/v4.1.0...v4.1.1 ## v4.1.0 (2020-04-1) - middleware.LogEntry: Write method on interface now passes the response header and an extra interface type useful for custom logger implementations. - middleware.WrapResponseWriter: minor fix - middleware.Recoverer: a bit prettier - History of changes: see https://github.com/go-chi/chi/compare/v4.0.4...v4.1.0 ## v4.0.4 (2020-03-24) - middleware.Recoverer: new pretty stack trace printing (https://github.com/go-chi/chi/pull/496) - a few minor improvements and fixes - History of changes: see https://github.com/go-chi/chi/compare/v4.0.3...v4.0.4 ## v4.0.3 (2020-01-09) - core: fix regexp routing to include default value when param is not matched - middleware: rewrite of middleware.Compress - middleware: suppress http.ErrAbortHandler in middleware.Recoverer - History of changes: see https://github.com/go-chi/chi/compare/v4.0.2...v4.0.3 ## v4.0.2 (2019-02-26) - Minor fixes - History of changes: see https://github.com/go-chi/chi/compare/v4.0.1...v4.0.2 ## v4.0.1 (2019-01-21) - Fixes issue with compress middleware: #382 #385 - History of changes: see https://github.com/go-chi/chi/compare/v4.0.0...v4.0.1 ## v4.0.0 (2019-01-10) - chi v4 requires Go 1.10.3+ (or Go 1.9.7+) - we have deprecated support for Go 1.7 and 1.8 - router: respond with 404 on router with no routes (#362) - router: additional check to ensure wildcard is at the end of a url pattern (#333) - middleware: deprecate use of http.CloseNotifier (#347) - middleware: fix RedirectSlashes to include query params on redirect (#334) - History of changes: see https://github.com/go-chi/chi/compare/v3.3.4...v4.0.0 ## v3.3.4 (2019-01-07) - Minor middleware improvements. No changes to core library/router. Moving v3 into its - own branch as a version of chi for Go 1.7, 1.8, 1.9, 1.10, 1.11 - History of changes: see https://github.com/go-chi/chi/compare/v3.3.3...v3.3.4 ## v3.3.3 (2018-08-27) - Minor release - See https://github.com/go-chi/chi/compare/v3.3.2...v3.3.3 ## v3.3.2 (2017-12-22) - Support to route trailing slashes on mounted sub-routers (#281) - middleware: new `ContentCharset` to check matching charsets. Thank you @csucu for your community contribution! ## v3.3.1 (2017-11-20) - middleware: new `AllowContentType` handler for explicit whitelist of accepted request Content-Types - middleware: new `SetHeader` handler for short-hand middleware to set a response header key/value - Minor bug fixes ## v3.3.0 (2017-10-10) - New chi.RegisterMethod(method) to add support for custom HTTP methods, see _examples/custom-method for usage - Deprecated LINK and UNLINK methods from the default list, please use `chi.RegisterMethod("LINK")` and `chi.RegisterMethod("UNLINK")` in an `init()` function ## v3.2.1 (2017-08-31) - Add new `Match(rctx *Context, method, path string) bool` method to `Routes` interface and `Mux`. Match searches the mux's routing tree for a handler that matches the method/path - Add new `RouteMethod` to `*Context` - Add new `Routes` pointer to `*Context` - Add new `middleware.GetHead` to route missing HEAD requests to GET handler - Updated benchmarks (see README) ## v3.1.5 (2017-08-02) - Setup golint and go vet for the project - As per golint, we've redefined `func ServerBaseContext(h http.Handler, baseCtx context.Context) http.Handler` to `func ServerBaseContext(baseCtx context.Context, h http.Handler) http.Handler` ## v3.1.0 (2017-07-10) - Fix a few minor issues after v3 release - Move `docgen` sub-pkg to https://github.com/go-chi/docgen - Move `render` sub-pkg to https://github.com/go-chi/render - Add new `URLFormat` handler to chi/middleware sub-pkg to make working with url mime suffixes easier, ie. parsing `/articles/1.json` and `/articles/1.xml`. See comments in https://github.com/go-chi/chi/blob/master/middleware/url_format.go for example usage. ## v3.0.0 (2017-06-21) - Major update to chi library with many exciting updates, but also some *breaking changes* - URL parameter syntax changed from `/:id` to `/{id}` for even more flexible routing, such as `/articles/{month}-{day}-{year}-{slug}`, `/articles/{id}`, and `/articles/{id}.{ext}` on the same router - Support for regexp for routing patterns, in the form of `/{paramKey:regExp}` for example: `r.Get("/articles/{name:[a-z]+}", h)` and `chi.URLParam(r, "name")` - Add `Method` and `MethodFunc` to `chi.Router` to allow routing definitions such as `r.Method("GET", "/", h)` which provides a cleaner interface for custom handlers like in `_examples/custom-handler` - Deprecating `mux#FileServer` helper function. Instead, we encourage users to create their own using file handler with the stdlib, see `_examples/fileserver` for an example - Add support for LINK/UNLINK http methods via `r.Method()` and `r.MethodFunc()` - Moved the chi project to its own organization, to allow chi-related community packages to be easily discovered and supported, at: https://github.com/go-chi - *NOTE:* please update your import paths to `"github.com/go-chi/chi"` - *NOTE:* chi v2 is still available at https://github.com/go-chi/chi/tree/v2 ## v2.1.0 (2017-03-30) - Minor improvements and update to the chi core library - Introduced a brand new `chi/render` sub-package to complete the story of building APIs to offer a pattern for managing well-defined request / response payloads. Please check out the updated `_examples/rest` example for how it works. - Added `MethodNotAllowed(h http.HandlerFunc)` to chi.Router interface ## v2.0.0 (2017-01-06) - After many months of v2 being in an RC state with many companies and users running it in production, the inclusion of some improvements to the middlewares, we are very pleased to announce v2.0.0 of chi. ## v2.0.0-rc1 (2016-07-26) - Huge update! chi v2 is a large refactor targeting Go 1.7+. As of Go 1.7, the popular community `"net/context"` package has been included in the standard library as `"context"` and utilized by `"net/http"` and `http.Request` to managing deadlines, cancelation signals and other request-scoped values. We're very excited about the new context addition and are proud to introduce chi v2, a minimal and powerful routing package for building large HTTP services, with zero external dependencies. Chi focuses on idiomatic design and encourages the use of stdlib HTTP handlers and middlewares. - chi v2 deprecates its `chi.Handler` interface and requires `http.Handler` or `http.HandlerFunc` - chi v2 stores URL routing parameters and patterns in the standard request context: `r.Context()` - chi v2 lower-level routing context is accessible by `chi.RouteContext(r.Context()) *chi.Context`, which provides direct access to URL routing parameters, the routing path and the matching routing patterns. - Users upgrading from chi v1 to v2, need to: 1. Update the old chi.Handler signature, `func(ctx context.Context, w http.ResponseWriter, r *http.Request)` to the standard http.Handler: `func(w http.ResponseWriter, r *http.Request)` 2. Use `chi.URLParam(r *http.Request, paramKey string) string` or `URLParamFromCtx(ctx context.Context, paramKey string) string` to access a url parameter value ## v1.0.0 (2016-07-01) - Released chi v1 stable https://github.com/go-chi/chi/tree/v1.0.0 for Go 1.6 and older. ## v0.9.0 (2016-03-31) - Reuse context objects via sync.Pool for zero-allocation routing [#33](https://github.com/go-chi/chi/pull/33) - BREAKING NOTE: due to subtle API changes, previously `chi.URLParams(ctx)["id"]` used to access url parameters has changed to: `chi.URLParam(ctx, "id")` ================================================ FILE: vendor/github.com/go-chi/chi/v5/CONTRIBUTING.md ================================================ # Contributing ## Prerequisites 1. [Install Go][go-install]. 2. Download the sources and switch the working directory: ```bash go get -u -d github.com/go-chi/chi cd $GOPATH/src/github.com/go-chi/chi ``` ## Submitting a Pull Request A typical workflow is: 1. [Fork the repository.][fork] 2. [Create a topic branch.][branch] 3. Add tests for your change. 4. Run `go test`. If your tests pass, return to the step 3. 5. Implement the change and ensure the steps from the previous step pass. 6. Run `goimports -w .`, to ensure the new code conforms to Go formatting guideline. 7. [Add, commit and push your changes.][git-help] 8. [Submit a pull request.][pull-req] [go-install]: https://golang.org/doc/install [fork]: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo [branch]: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-branches [git-help]: https://docs.github.com/en [pull-req]: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests ================================================ FILE: vendor/github.com/go-chi/chi/v5/LICENSE ================================================ Copyright (c) 2015-present Peter Kieltyka (https://github.com/pkieltyka), Google Inc. MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: vendor/github.com/go-chi/chi/v5/Makefile ================================================ .PHONY: all all: @echo "**********************************************************" @echo "** chi build tool **" @echo "**********************************************************" .PHONY: test test: go clean -testcache && $(MAKE) test-router && $(MAKE) test-middleware .PHONY: test-router test-router: go test -race -v . .PHONY: test-middleware test-middleware: go test -race -v ./middleware .PHONY: docs docs: npx docsify-cli serve ./docs ================================================ FILE: vendor/github.com/go-chi/chi/v5/README.md ================================================ # chi [![GoDoc Widget]][GoDoc] `chi` is a lightweight, idiomatic and composable router for building Go HTTP services. It's especially good at helping you write large REST API services that are kept maintainable as your project grows and changes. `chi` is built on the new `context` package introduced in Go 1.7 to handle signaling, cancelation and request-scoped values across a handler chain. The focus of the project has been to seek out an elegant and comfortable design for writing REST API servers, written during the development of the Pressly API service that powers our public API service, which in turn powers all of our client-side applications. The key considerations of chi's design are: project structure, maintainability, standard http handlers (stdlib-only), developer productivity, and deconstructing a large system into many small parts. The core router `github.com/go-chi/chi` is quite small (less than 1000 LOC), but we've also included some useful/optional subpackages: [middleware](/middleware), [render](https://github.com/go-chi/render) and [docgen](https://github.com/go-chi/docgen). We hope you enjoy it too! ## Install ```sh go get -u github.com/go-chi/chi/v5 ``` ## Features * **Lightweight** - cloc'd in ~1000 LOC for the chi router * **Fast** - yes, see [benchmarks](#benchmarks) * **100% compatible with net/http** - use any http or middleware pkg in the ecosystem that is also compatible with `net/http` * **Designed for modular/composable APIs** - middlewares, inline middlewares, route groups and sub-router mounting * **Context control** - built on new `context` package, providing value chaining, cancellations and timeouts * **Robust** - in production at Pressly, Cloudflare, Heroku, 99Designs, and many others (see [discussion](https://github.com/go-chi/chi/issues/91)) * **Doc generation** - `docgen` auto-generates routing documentation from your source to JSON or Markdown * **Go.mod support** - as of v5, go.mod support (see [CHANGELOG](https://github.com/go-chi/chi/blob/master/CHANGELOG.md)) * **No external dependencies** - plain ol' Go stdlib + net/http ## Examples See [_examples/](https://github.com/go-chi/chi/blob/master/_examples/) for a variety of examples. **As easy as:** ```go package main import ( "net/http" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" ) func main() { r := chi.NewRouter() r.Use(middleware.Logger) r.Get("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("welcome")) }) http.ListenAndServe(":3000", r) } ``` **REST Preview:** Here is a little preview of what routing looks like with chi. Also take a look at the generated routing docs in JSON ([routes.json](https://github.com/go-chi/chi/blob/master/_examples/rest/routes.json)) and in Markdown ([routes.md](https://github.com/go-chi/chi/blob/master/_examples/rest/routes.md)). I highly recommend reading the source of the [examples](https://github.com/go-chi/chi/blob/master/_examples/) listed above, they will show you all the features of chi and serve as a good form of documentation. ```go import ( //... "context" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" ) func main() { r := chi.NewRouter() // A good base middleware stack r.Use(middleware.RequestID) r.Use(middleware.RealIP) r.Use(middleware.Logger) r.Use(middleware.Recoverer) // Set a timeout value on the request context (ctx), that will signal // through ctx.Done() that the request has timed out and further // processing should be stopped. r.Use(middleware.Timeout(60 * time.Second)) r.Get("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("hi")) }) // RESTy routes for "articles" resource r.Route("/articles", func(r chi.Router) { r.With(paginate).Get("/", listArticles) // GET /articles r.With(paginate).Get("/{month}-{day}-{year}", listArticlesByDate) // GET /articles/01-16-2017 r.Post("/", createArticle) // POST /articles r.Get("/search", searchArticles) // GET /articles/search // Regexp url parameters: r.Get("/{articleSlug:[a-z-]+}", getArticleBySlug) // GET /articles/home-is-toronto // Subrouters: r.Route("/{articleID}", func(r chi.Router) { r.Use(ArticleCtx) r.Get("/", getArticle) // GET /articles/123 r.Put("/", updateArticle) // PUT /articles/123 r.Delete("/", deleteArticle) // DELETE /articles/123 }) }) // Mount the admin sub-router r.Mount("/admin", adminRouter()) http.ListenAndServe(":3333", r) } func ArticleCtx(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { articleID := chi.URLParam(r, "articleID") article, err := dbGetArticle(articleID) if err != nil { http.Error(w, http.StatusText(404), 404) return } ctx := context.WithValue(r.Context(), "article", article) next.ServeHTTP(w, r.WithContext(ctx)) }) } func getArticle(w http.ResponseWriter, r *http.Request) { ctx := r.Context() article, ok := ctx.Value("article").(*Article) if !ok { http.Error(w, http.StatusText(422), 422) return } w.Write([]byte(fmt.Sprintf("title:%s", article.Title))) } // A completely separate router for administrator routes func adminRouter() http.Handler { r := chi.NewRouter() r.Use(AdminOnly) r.Get("/", adminIndex) r.Get("/accounts", adminListAccounts) return r } func AdminOnly(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() perm, ok := ctx.Value("acl.permission").(YourPermissionType) if !ok || !perm.IsAdmin() { http.Error(w, http.StatusText(403), 403) return } next.ServeHTTP(w, r) }) } ``` ## Router interface chi's router is based on a kind of [Patricia Radix trie](https://en.wikipedia.org/wiki/Radix_tree). The router is fully compatible with `net/http`. Built on top of the tree is the `Router` interface: ```go // Router consisting of the core routing methods used by chi's Mux, // using only the standard net/http. type Router interface { http.Handler Routes // Use appends one or more middlewares onto the Router stack. Use(middlewares ...func(http.Handler) http.Handler) // With adds inline middlewares for an endpoint handler. With(middlewares ...func(http.Handler) http.Handler) Router // Group adds a new inline-Router along the current routing // path, with a fresh middleware stack for the inline-Router. Group(fn func(r Router)) Router // Route mounts a sub-Router along a `pattern` string. Route(pattern string, fn func(r Router)) Router // Mount attaches another http.Handler along ./pattern/* Mount(pattern string, h http.Handler) // Handle and HandleFunc adds routes for `pattern` that matches // all HTTP methods. Handle(pattern string, h http.Handler) HandleFunc(pattern string, h http.HandlerFunc) // Method and MethodFunc adds routes for `pattern` that matches // the `method` HTTP method. Method(method, pattern string, h http.Handler) MethodFunc(method, pattern string, h http.HandlerFunc) // HTTP-method routing along `pattern` Connect(pattern string, h http.HandlerFunc) Delete(pattern string, h http.HandlerFunc) Get(pattern string, h http.HandlerFunc) Head(pattern string, h http.HandlerFunc) Options(pattern string, h http.HandlerFunc) Patch(pattern string, h http.HandlerFunc) Post(pattern string, h http.HandlerFunc) Put(pattern string, h http.HandlerFunc) Trace(pattern string, h http.HandlerFunc) // NotFound defines a handler to respond whenever a route could // not be found. NotFound(h http.HandlerFunc) // MethodNotAllowed defines a handler to respond whenever a method is // not allowed. MethodNotAllowed(h http.HandlerFunc) } // Routes interface adds two methods for router traversal, which is also // used by the github.com/go-chi/docgen package to generate documentation for Routers. type Routes interface { // Routes returns the routing tree in an easily traversable structure. Routes() []Route // Middlewares returns the list of middlewares in use by the router. Middlewares() Middlewares // Match searches the routing tree for a handler that matches // the method/path - similar to routing a http request, but without // executing the handler thereafter. Match(rctx *Context, method, path string) bool } ``` Each routing method accepts a URL `pattern` and chain of `handlers`. The URL pattern supports named params (ie. `/users/{userID}`) and wildcards (ie. `/admin/*`). URL parameters can be fetched at runtime by calling `chi.URLParam(r, "userID")` for named parameters and `chi.URLParam(r, "*")` for a wildcard parameter. ### Middleware handlers chi's middlewares are just stdlib net/http middleware handlers. There is nothing special about them, which means the router and all the tooling is designed to be compatible and friendly with any middleware in the community. This offers much better extensibility and reuse of packages and is at the heart of chi's purpose. Here is an example of a standard net/http middleware where we assign a context key `"user"` the value of `"123"`. This middleware sets a hypothetical user identifier on the request context and calls the next handler in the chain. ```go // HTTP middleware setting a value on the request context func MyMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // create new context from `r` request context, and assign key `"user"` // to value of `"123"` ctx := context.WithValue(r.Context(), "user", "123") // call the next handler in the chain, passing the response writer and // the updated request object with the new context value. // // note: context.Context values are nested, so any previously set // values will be accessible as well, and the new `"user"` key // will be accessible from this point forward. next.ServeHTTP(w, r.WithContext(ctx)) }) } ``` ### Request handlers chi uses standard net/http request handlers. This little snippet is an example of a http.Handler func that reads a user identifier from the request context - hypothetically, identifying the user sending an authenticated request, validated+set by a previous middleware handler. ```go // HTTP handler accessing data from the request context. func MyRequestHandler(w http.ResponseWriter, r *http.Request) { // here we read from the request context and fetch out `"user"` key set in // the MyMiddleware example above. user := r.Context().Value("user").(string) // respond to the client w.Write([]byte(fmt.Sprintf("hi %s", user))) } ``` ### URL parameters chi's router parses and stores URL parameters right onto the request context. Here is an example of how to access URL params in your net/http handlers. And of course, middlewares are able to access the same information. ```go // HTTP handler accessing the url routing parameters. func MyRequestHandler(w http.ResponseWriter, r *http.Request) { // fetch the url parameter `"userID"` from the request of a matching // routing pattern. An example routing pattern could be: /users/{userID} userID := chi.URLParam(r, "userID") // fetch `"key"` from the request context ctx := r.Context() key := ctx.Value("key").(string) // respond to the client w.Write([]byte(fmt.Sprintf("hi %v, %v", userID, key))) } ``` ## Middlewares chi comes equipped with an optional `middleware` package, providing a suite of standard `net/http` middlewares. Please note, any middleware in the ecosystem that is also compatible with `net/http` can be used with chi's mux. ### Core middlewares ---------------------------------------------------------------------------------------------------- | chi/middleware Handler | description | | :--------------------- | :---------------------------------------------------------------------- | | [AllowContentEncoding] | Enforces a whitelist of request Content-Encoding headers | | [AllowContentType] | Explicit whitelist of accepted request Content-Types | | [BasicAuth] | Basic HTTP authentication | | [Compress] | Gzip compression for clients that accept compressed responses | | [ContentCharset] | Ensure charset for Content-Type request headers | | [CleanPath] | Clean double slashes from request path | | [GetHead] | Automatically route undefined HEAD requests to GET handlers | | [Heartbeat] | Monitoring endpoint to check the servers pulse | | [Logger] | Logs the start and end of each request with the elapsed processing time | | [NoCache] | Sets response headers to prevent clients from caching | | [Profiler] | Easily attach net/http/pprof to your routers | | [RealIP] | Sets a http.Request's RemoteAddr to either X-Real-IP or X-Forwarded-For | | [Recoverer] | Gracefully absorb panics and prints the stack trace | | [RequestID] | Injects a request ID into the context of each request | | [RedirectSlashes] | Redirect slashes on routing paths | | [RouteHeaders] | Route handling for request headers | | [SetHeader] | Short-hand middleware to set a response header key/value | | [StripSlashes] | Strip slashes on routing paths | | [Sunset] | Sunset set Deprecation/Sunset header to response | | [Throttle] | Puts a ceiling on the number of concurrent requests | | [Timeout] | Signals to the request context when the timeout deadline is reached | | [URLFormat] | Parse extension from url and put it on request context | | [WithValue] | Short-hand middleware to set a key/value on the request context | ---------------------------------------------------------------------------------------------------- [AllowContentEncoding]: https://pkg.go.dev/github.com/go-chi/chi/middleware#AllowContentEncoding [AllowContentType]: https://pkg.go.dev/github.com/go-chi/chi/middleware#AllowContentType [BasicAuth]: https://pkg.go.dev/github.com/go-chi/chi/middleware#BasicAuth [Compress]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Compress [ContentCharset]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ContentCharset [CleanPath]: https://pkg.go.dev/github.com/go-chi/chi/middleware#CleanPath [GetHead]: https://pkg.go.dev/github.com/go-chi/chi/middleware#GetHead [GetReqID]: https://pkg.go.dev/github.com/go-chi/chi/middleware#GetReqID [Heartbeat]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Heartbeat [Logger]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Logger [NoCache]: https://pkg.go.dev/github.com/go-chi/chi/middleware#NoCache [Profiler]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Profiler [RealIP]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RealIP [Recoverer]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Recoverer [RedirectSlashes]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RedirectSlashes [RequestLogger]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RequestLogger [RequestID]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RequestID [RouteHeaders]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RouteHeaders [SetHeader]: https://pkg.go.dev/github.com/go-chi/chi/middleware#SetHeader [StripSlashes]: https://pkg.go.dev/github.com/go-chi/chi/middleware#StripSlashes [Sunset]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#Sunset [Throttle]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Throttle [ThrottleBacklog]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleBacklog [ThrottleWithOpts]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleWithOpts [Timeout]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Timeout [URLFormat]: https://pkg.go.dev/github.com/go-chi/chi/middleware#URLFormat [WithLogEntry]: https://pkg.go.dev/github.com/go-chi/chi/middleware#WithLogEntry [WithValue]: https://pkg.go.dev/github.com/go-chi/chi/middleware#WithValue [Compressor]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Compressor [DefaultLogFormatter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#DefaultLogFormatter [EncoderFunc]: https://pkg.go.dev/github.com/go-chi/chi/middleware#EncoderFunc [HeaderRoute]: https://pkg.go.dev/github.com/go-chi/chi/middleware#HeaderRoute [HeaderRouter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#HeaderRouter [LogEntry]: https://pkg.go.dev/github.com/go-chi/chi/middleware#LogEntry [LogFormatter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#LogFormatter [LoggerInterface]: https://pkg.go.dev/github.com/go-chi/chi/middleware#LoggerInterface [ThrottleOpts]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleOpts [WrapResponseWriter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#WrapResponseWriter ### Extra middlewares & packages Please see https://github.com/go-chi for additional packages. -------------------------------------------------------------------------------------------------------------------- | package | description | |:---------------------------------------------------|:------------------------------------------------------------- | [cors](https://github.com/go-chi/cors) | Cross-origin resource sharing (CORS) | | [docgen](https://github.com/go-chi/docgen) | Print chi.Router routes at runtime | | [jwtauth](https://github.com/go-chi/jwtauth) | JWT authentication | | [hostrouter](https://github.com/go-chi/hostrouter) | Domain/host based request routing | | [httplog](https://github.com/go-chi/httplog) | Small but powerful structured HTTP request logging | | [httprate](https://github.com/go-chi/httprate) | HTTP request rate limiter | | [httptracer](https://github.com/go-chi/httptracer) | HTTP request performance tracing library | | [httpvcr](https://github.com/go-chi/httpvcr) | Write deterministic tests for external sources | | [stampede](https://github.com/go-chi/stampede) | HTTP request coalescer | -------------------------------------------------------------------------------------------------------------------- ## context? `context` is a tiny pkg that provides simple interface to signal context across call stacks and goroutines. It was originally written by [Sameer Ajmani](https://github.com/Sajmani) and is available in stdlib since go1.7. Learn more at https://blog.golang.org/context and.. * Docs: https://golang.org/pkg/context * Source: https://github.com/golang/go/tree/master/src/context ## Benchmarks The benchmark suite: https://github.com/pkieltyka/go-http-routing-benchmark Results as of Nov 29, 2020 with Go 1.15.5 on Linux AMD 3950x ```shell BenchmarkChi_Param 3075895 384 ns/op 400 B/op 2 allocs/op BenchmarkChi_Param5 2116603 566 ns/op 400 B/op 2 allocs/op BenchmarkChi_Param20 964117 1227 ns/op 400 B/op 2 allocs/op BenchmarkChi_ParamWrite 2863413 420 ns/op 400 B/op 2 allocs/op BenchmarkChi_GithubStatic 3045488 395 ns/op 400 B/op 2 allocs/op BenchmarkChi_GithubParam 2204115 540 ns/op 400 B/op 2 allocs/op BenchmarkChi_GithubAll 10000 113811 ns/op 81203 B/op 406 allocs/op BenchmarkChi_GPlusStatic 3337485 359 ns/op 400 B/op 2 allocs/op BenchmarkChi_GPlusParam 2825853 423 ns/op 400 B/op 2 allocs/op BenchmarkChi_GPlus2Params 2471697 483 ns/op 400 B/op 2 allocs/op BenchmarkChi_GPlusAll 194220 5950 ns/op 5200 B/op 26 allocs/op BenchmarkChi_ParseStatic 3365324 356 ns/op 400 B/op 2 allocs/op BenchmarkChi_ParseParam 2976614 404 ns/op 400 B/op 2 allocs/op BenchmarkChi_Parse2Params 2638084 439 ns/op 400 B/op 2 allocs/op BenchmarkChi_ParseAll 109567 11295 ns/op 10400 B/op 52 allocs/op BenchmarkChi_StaticAll 16846 71308 ns/op 62802 B/op 314 allocs/op ``` Comparison with other routers: https://gist.github.com/pkieltyka/123032f12052520aaccab752bd3e78cc NOTE: the allocs in the benchmark above are from the calls to http.Request's `WithContext(context.Context)` method that clones the http.Request, sets the `Context()` on the duplicated (alloc'd) request and returns it the new request object. This is just how setting context on a request in Go works. ## Credits * Carl Jackson for https://github.com/zenazn/goji * Parts of chi's thinking comes from goji, and chi's middleware package sources from [goji](https://github.com/zenazn/goji/tree/master/web/middleware). * Please see goji's [LICENSE](https://github.com/zenazn/goji/blob/master/LICENSE) (MIT) * Armon Dadgar for https://github.com/armon/go-radix * Contributions: [@VojtechVitek](https://github.com/VojtechVitek) We'll be more than happy to see [your contributions](./CONTRIBUTING.md)! ## Beyond REST chi is just a http router that lets you decompose request handling into many smaller layers. Many companies use chi to write REST services for their public APIs. But, REST is just a convention for managing state via HTTP, and there's a lot of other pieces required to write a complete client-server system or network of microservices. Looking beyond REST, I also recommend some newer works in the field: * [webrpc](https://github.com/webrpc/webrpc) - Web-focused RPC client+server framework with code-gen * [gRPC](https://github.com/grpc/grpc-go) - Google's RPC framework via protobufs * [graphql](https://github.com/99designs/gqlgen) - Declarative query language * [NATS](https://nats.io) - lightweight pub-sub ## License Copyright (c) 2015-present [Peter Kieltyka](https://github.com/pkieltyka) Licensed under [MIT License](./LICENSE) [GoDoc]: https://pkg.go.dev/github.com/go-chi/chi/v5 [GoDoc Widget]: https://godoc.org/github.com/go-chi/chi?status.svg [Travis]: https://travis-ci.org/go-chi/chi [Travis Widget]: https://travis-ci.org/go-chi/chi.svg?branch=master ================================================ FILE: vendor/github.com/go-chi/chi/v5/SECURITY.md ================================================ # Reporting Security Issues We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions. To report a security issue, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/go-chi/chi/security/advisories/new) tab. ================================================ FILE: vendor/github.com/go-chi/chi/v5/chain.go ================================================ package chi import "net/http" // Chain returns a Middlewares type from a slice of middleware handlers. func Chain(middlewares ...func(http.Handler) http.Handler) Middlewares { return Middlewares(middlewares) } // Handler builds and returns a http.Handler from the chain of middlewares, // with `h http.Handler` as the final handler. func (mws Middlewares) Handler(h http.Handler) http.Handler { return &ChainHandler{h, chain(mws, h), mws} } // HandlerFunc builds and returns a http.Handler from the chain of middlewares, // with `h http.Handler` as the final handler. func (mws Middlewares) HandlerFunc(h http.HandlerFunc) http.Handler { return &ChainHandler{h, chain(mws, h), mws} } // ChainHandler is a http.Handler with support for handler composition and // execution. type ChainHandler struct { Endpoint http.Handler chain http.Handler Middlewares Middlewares } func (c *ChainHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { c.chain.ServeHTTP(w, r) } // chain builds a http.Handler composed of an inline middleware stack and endpoint // handler in the order they are passed. func chain(middlewares []func(http.Handler) http.Handler, endpoint http.Handler) http.Handler { // Return ahead of time if there aren't any middlewares for the chain if len(middlewares) == 0 { return endpoint } // Wrap the end handler with the middleware chain h := middlewares[len(middlewares)-1](endpoint) for i := len(middlewares) - 2; i >= 0; i-- { h = middlewares[i](h) } return h } ================================================ FILE: vendor/github.com/go-chi/chi/v5/chi.go ================================================ // Package chi is a small, idiomatic and composable router for building HTTP services. // // chi requires Go 1.14 or newer. // // Example: // // package main // // import ( // "net/http" // // "github.com/go-chi/chi/v5" // "github.com/go-chi/chi/v5/middleware" // ) // // func main() { // r := chi.NewRouter() // r.Use(middleware.Logger) // r.Use(middleware.Recoverer) // // r.Get("/", func(w http.ResponseWriter, r *http.Request) { // w.Write([]byte("root.")) // }) // // http.ListenAndServe(":3333", r) // } // // See github.com/go-chi/chi/_examples/ for more in-depth examples. // // URL patterns allow for easy matching of path components in HTTP // requests. The matching components can then be accessed using // chi.URLParam(). All patterns must begin with a slash. // // A simple named placeholder {name} matches any sequence of characters // up to the next / or the end of the URL. Trailing slashes on paths must // be handled explicitly. // // A placeholder with a name followed by a colon allows a regular // expression match, for example {number:\\d+}. The regular expression // syntax is Go's normal regexp RE2 syntax, except that / will never be // matched. An anonymous regexp pattern is allowed, using an empty string // before the colon in the placeholder, such as {:\\d+} // // The special placeholder of asterisk matches the rest of the requested // URL. Any trailing characters in the pattern are ignored. This is the only // placeholder which will match / characters. // // Examples: // // "/user/{name}" matches "/user/jsmith" but not "/user/jsmith/info" or "/user/jsmith/" // "/user/{name}/info" matches "/user/jsmith/info" // "/page/*" matches "/page/intro/latest" // "/page/{other}/latest" also matches "/page/intro/latest" // "/date/{yyyy:\\d\\d\\d\\d}/{mm:\\d\\d}/{dd:\\d\\d}" matches "/date/2017/04/01" package chi import "net/http" // NewRouter returns a new Mux object that implements the Router interface. func NewRouter() *Mux { return NewMux() } // Router consisting of the core routing methods used by chi's Mux, // using only the standard net/http. type Router interface { http.Handler Routes // Use appends one or more middlewares onto the Router stack. Use(middlewares ...func(http.Handler) http.Handler) // With adds inline middlewares for an endpoint handler. With(middlewares ...func(http.Handler) http.Handler) Router // Group adds a new inline-Router along the current routing // path, with a fresh middleware stack for the inline-Router. Group(fn func(r Router)) Router // Route mounts a sub-Router along a `pattern`` string. Route(pattern string, fn func(r Router)) Router // Mount attaches another http.Handler along ./pattern/* Mount(pattern string, h http.Handler) // Handle and HandleFunc adds routes for `pattern` that matches // all HTTP methods. Handle(pattern string, h http.Handler) HandleFunc(pattern string, h http.HandlerFunc) // Method and MethodFunc adds routes for `pattern` that matches // the `method` HTTP method. Method(method, pattern string, h http.Handler) MethodFunc(method, pattern string, h http.HandlerFunc) // HTTP-method routing along `pattern` Connect(pattern string, h http.HandlerFunc) Delete(pattern string, h http.HandlerFunc) Get(pattern string, h http.HandlerFunc) Head(pattern string, h http.HandlerFunc) Options(pattern string, h http.HandlerFunc) Patch(pattern string, h http.HandlerFunc) Post(pattern string, h http.HandlerFunc) Put(pattern string, h http.HandlerFunc) Trace(pattern string, h http.HandlerFunc) // NotFound defines a handler to respond whenever a route could // not be found. NotFound(h http.HandlerFunc) // MethodNotAllowed defines a handler to respond whenever a method is // not allowed. MethodNotAllowed(h http.HandlerFunc) } // Routes interface adds two methods for router traversal, which is also // used by the `docgen` subpackage to generation documentation for Routers. type Routes interface { // Routes returns the routing tree in an easily traversable structure. Routes() []Route // Middlewares returns the list of middlewares in use by the router. Middlewares() Middlewares // Match searches the routing tree for a handler that matches // the method/path - similar to routing a http request, but without // executing the handler thereafter. Match(rctx *Context, method, path string) bool // Find searches the routing tree for the pattern that matches // the method/path. Find(rctx *Context, method, path string) string } // Middlewares type is a slice of standard middleware handlers with methods // to compose middleware chains and http.Handler's. type Middlewares []func(http.Handler) http.Handler ================================================ FILE: vendor/github.com/go-chi/chi/v5/context.go ================================================ package chi import ( "context" "net/http" "strings" ) // URLParam returns the url parameter from a http.Request object. func URLParam(r *http.Request, key string) string { if rctx := RouteContext(r.Context()); rctx != nil { return rctx.URLParam(key) } return "" } // URLParamFromCtx returns the url parameter from a http.Request Context. func URLParamFromCtx(ctx context.Context, key string) string { if rctx := RouteContext(ctx); rctx != nil { return rctx.URLParam(key) } return "" } // RouteContext returns chi's routing Context object from a // http.Request Context. func RouteContext(ctx context.Context) *Context { val, _ := ctx.Value(RouteCtxKey).(*Context) return val } // NewRouteContext returns a new routing Context object. func NewRouteContext() *Context { return &Context{} } var ( // RouteCtxKey is the context.Context key to store the request context. RouteCtxKey = &contextKey{"RouteContext"} ) // Context is the default routing context set on the root node of a // request context to track route patterns, URL parameters and // an optional routing path. type Context struct { Routes Routes // parentCtx is the parent of this one, for using Context as a // context.Context directly. This is an optimization that saves // 1 allocation. parentCtx context.Context // Routing path/method override used during the route search. // See Mux#routeHTTP method. RoutePath string RouteMethod string // URLParams are the stack of routeParams captured during the // routing lifecycle across a stack of sub-routers. URLParams RouteParams // Route parameters matched for the current sub-router. It is // intentionally unexported so it can't be tampered. routeParams RouteParams // The endpoint routing pattern that matched the request URI path // or `RoutePath` of the current sub-router. This value will update // during the lifecycle of a request passing through a stack of // sub-routers. routePattern string // Routing pattern stack throughout the lifecycle of the request, // across all connected routers. It is a record of all matching // patterns across a stack of sub-routers. RoutePatterns []string methodsAllowed []methodTyp // allowed methods in case of a 405 methodNotAllowed bool } // Reset a routing context to its initial state. func (x *Context) Reset() { x.Routes = nil x.RoutePath = "" x.RouteMethod = "" x.RoutePatterns = x.RoutePatterns[:0] x.URLParams.Keys = x.URLParams.Keys[:0] x.URLParams.Values = x.URLParams.Values[:0] x.routePattern = "" x.routeParams.Keys = x.routeParams.Keys[:0] x.routeParams.Values = x.routeParams.Values[:0] x.methodNotAllowed = false x.methodsAllowed = x.methodsAllowed[:0] x.parentCtx = nil } // URLParam returns the corresponding URL parameter value from the request // routing context. func (x *Context) URLParam(key string) string { for k := len(x.URLParams.Keys) - 1; k >= 0; k-- { if x.URLParams.Keys[k] == key { return x.URLParams.Values[k] } } return "" } // RoutePattern builds the routing pattern string for the particular // request, at the particular point during routing. This means, the value // will change throughout the execution of a request in a router. That is // why it's advised to only use this value after calling the next handler. // // For example, // // func Instrument(next http.Handler) http.Handler { // return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // next.ServeHTTP(w, r) // routePattern := chi.RouteContext(r.Context()).RoutePattern() // measure(w, r, routePattern) // }) // } func (x *Context) RoutePattern() string { if x == nil { return "" } routePattern := strings.Join(x.RoutePatterns, "") routePattern = replaceWildcards(routePattern) if routePattern != "/" { routePattern = strings.TrimSuffix(routePattern, "//") routePattern = strings.TrimSuffix(routePattern, "/") } return routePattern } // replaceWildcards takes a route pattern and recursively replaces all // occurrences of "/*/" to "/". func replaceWildcards(p string) string { if strings.Contains(p, "/*/") { return replaceWildcards(strings.Replace(p, "/*/", "/", -1)) } return p } // RouteParams is a structure to track URL routing parameters efficiently. type RouteParams struct { Keys, Values []string } // Add will append a URL parameter to the end of the route param func (s *RouteParams) Add(key, value string) { s.Keys = append(s.Keys, key) s.Values = append(s.Values, value) } // contextKey is a value for use with context.WithValue. It's used as // a pointer so it fits in an interface{} without allocation. This technique // for defining context keys was copied from Go 1.7's new use of context in net/http. type contextKey struct { name string } func (k *contextKey) String() string { return "chi context value " + k.name } ================================================ FILE: vendor/github.com/go-chi/chi/v5/mux.go ================================================ package chi import ( "context" "fmt" "net/http" "strings" "sync" ) var _ Router = &Mux{} // Mux is a simple HTTP route multiplexer that parses a request path, // records any URL params, and executes an end handler. It implements // the http.Handler interface and is friendly with the standard library. // // Mux is designed to be fast, minimal and offer a powerful API for building // modular and composable HTTP services with a large set of handlers. It's // particularly useful for writing large REST API services that break a handler // into many smaller parts composed of middlewares and end handlers. type Mux struct { // The computed mux handler made of the chained middleware stack and // the tree router handler http.Handler // The radix trie router tree *node // Custom method not allowed handler methodNotAllowedHandler http.HandlerFunc // A reference to the parent mux used by subrouters when mounting // to a parent mux parent *Mux // Routing context pool pool *sync.Pool // Custom route not found handler notFoundHandler http.HandlerFunc // The middleware stack middlewares []func(http.Handler) http.Handler // Controls the behaviour of middleware chain generation when a mux // is registered as an inline group inside another mux. inline bool } // NewMux returns a newly initialized Mux object that implements the Router // interface. func NewMux() *Mux { mux := &Mux{tree: &node{}, pool: &sync.Pool{}} mux.pool.New = func() interface{} { return NewRouteContext() } return mux } // ServeHTTP is the single method of the http.Handler interface that makes // Mux interoperable with the standard library. It uses a sync.Pool to get and // reuse routing contexts for each request. func (mx *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Ensure the mux has some routes defined on the mux if mx.handler == nil { mx.NotFoundHandler().ServeHTTP(w, r) return } // Check if a routing context already exists from a parent router. rctx, _ := r.Context().Value(RouteCtxKey).(*Context) if rctx != nil { mx.handler.ServeHTTP(w, r) return } // Fetch a RouteContext object from the sync pool, and call the computed // mx.handler that is comprised of mx.middlewares + mx.routeHTTP. // Once the request is finished, reset the routing context and put it back // into the pool for reuse from another request. rctx = mx.pool.Get().(*Context) rctx.Reset() rctx.Routes = mx rctx.parentCtx = r.Context() // NOTE: r.WithContext() causes 2 allocations and context.WithValue() causes 1 allocation r = r.WithContext(context.WithValue(r.Context(), RouteCtxKey, rctx)) // Serve the request and once its done, put the request context back in the sync pool mx.handler.ServeHTTP(w, r) mx.pool.Put(rctx) } // Use appends a middleware handler to the Mux middleware stack. // // The middleware stack for any Mux will execute before searching for a matching // route to a specific handler, which provides opportunity to respond early, // change the course of the request execution, or set request-scoped values for // the next http.Handler. func (mx *Mux) Use(middlewares ...func(http.Handler) http.Handler) { if mx.handler != nil { panic("chi: all middlewares must be defined before routes on a mux") } mx.middlewares = append(mx.middlewares, middlewares...) } // Handle adds the route `pattern` that matches any http method to // execute the `handler` http.Handler. func (mx *Mux) Handle(pattern string, handler http.Handler) { if method, rest, found := strings.Cut(pattern, " "); found { mx.Method(method, rest, handler) return } mx.handle(mALL, pattern, handler) } // HandleFunc adds the route `pattern` that matches any http method to // execute the `handlerFn` http.HandlerFunc. func (mx *Mux) HandleFunc(pattern string, handlerFn http.HandlerFunc) { if method, rest, found := strings.Cut(pattern, " "); found { mx.Method(method, rest, handlerFn) return } mx.handle(mALL, pattern, handlerFn) } // Method adds the route `pattern` that matches `method` http method to // execute the `handler` http.Handler. func (mx *Mux) Method(method, pattern string, handler http.Handler) { m, ok := methodMap[strings.ToUpper(method)] if !ok { panic(fmt.Sprintf("chi: '%s' http method is not supported.", method)) } mx.handle(m, pattern, handler) } // MethodFunc adds the route `pattern` that matches `method` http method to // execute the `handlerFn` http.HandlerFunc. func (mx *Mux) MethodFunc(method, pattern string, handlerFn http.HandlerFunc) { mx.Method(method, pattern, handlerFn) } // Connect adds the route `pattern` that matches a CONNECT http method to // execute the `handlerFn` http.HandlerFunc. func (mx *Mux) Connect(pattern string, handlerFn http.HandlerFunc) { mx.handle(mCONNECT, pattern, handlerFn) } // Delete adds the route `pattern` that matches a DELETE http method to // execute the `handlerFn` http.HandlerFunc. func (mx *Mux) Delete(pattern string, handlerFn http.HandlerFunc) { mx.handle(mDELETE, pattern, handlerFn) } // Get adds the route `pattern` that matches a GET http method to // execute the `handlerFn` http.HandlerFunc. func (mx *Mux) Get(pattern string, handlerFn http.HandlerFunc) { mx.handle(mGET, pattern, handlerFn) } // Head adds the route `pattern` that matches a HEAD http method to // execute the `handlerFn` http.HandlerFunc. func (mx *Mux) Head(pattern string, handlerFn http.HandlerFunc) { mx.handle(mHEAD, pattern, handlerFn) } // Options adds the route `pattern` that matches an OPTIONS http method to // execute the `handlerFn` http.HandlerFunc. func (mx *Mux) Options(pattern string, handlerFn http.HandlerFunc) { mx.handle(mOPTIONS, pattern, handlerFn) } // Patch adds the route `pattern` that matches a PATCH http method to // execute the `handlerFn` http.HandlerFunc. func (mx *Mux) Patch(pattern string, handlerFn http.HandlerFunc) { mx.handle(mPATCH, pattern, handlerFn) } // Post adds the route `pattern` that matches a POST http method to // execute the `handlerFn` http.HandlerFunc. func (mx *Mux) Post(pattern string, handlerFn http.HandlerFunc) { mx.handle(mPOST, pattern, handlerFn) } // Put adds the route `pattern` that matches a PUT http method to // execute the `handlerFn` http.HandlerFunc. func (mx *Mux) Put(pattern string, handlerFn http.HandlerFunc) { mx.handle(mPUT, pattern, handlerFn) } // Trace adds the route `pattern` that matches a TRACE http method to // execute the `handlerFn` http.HandlerFunc. func (mx *Mux) Trace(pattern string, handlerFn http.HandlerFunc) { mx.handle(mTRACE, pattern, handlerFn) } // NotFound sets a custom http.HandlerFunc for routing paths that could // not be found. The default 404 handler is `http.NotFound`. func (mx *Mux) NotFound(handlerFn http.HandlerFunc) { // Build NotFound handler chain m := mx hFn := handlerFn if mx.inline && mx.parent != nil { m = mx.parent hFn = Chain(mx.middlewares...).HandlerFunc(hFn).ServeHTTP } // Update the notFoundHandler from this point forward m.notFoundHandler = hFn m.updateSubRoutes(func(subMux *Mux) { if subMux.notFoundHandler == nil { subMux.NotFound(hFn) } }) } // MethodNotAllowed sets a custom http.HandlerFunc for routing paths where the // method is unresolved. The default handler returns a 405 with an empty body. func (mx *Mux) MethodNotAllowed(handlerFn http.HandlerFunc) { // Build MethodNotAllowed handler chain m := mx hFn := handlerFn if mx.inline && mx.parent != nil { m = mx.parent hFn = Chain(mx.middlewares...).HandlerFunc(hFn).ServeHTTP } // Update the methodNotAllowedHandler from this point forward m.methodNotAllowedHandler = hFn m.updateSubRoutes(func(subMux *Mux) { if subMux.methodNotAllowedHandler == nil { subMux.MethodNotAllowed(hFn) } }) } // With adds inline middlewares for an endpoint handler. func (mx *Mux) With(middlewares ...func(http.Handler) http.Handler) Router { // Similarly as in handle(), we must build the mux handler once additional // middleware registration isn't allowed for this stack, like now. if !mx.inline && mx.handler == nil { mx.updateRouteHandler() } // Copy middlewares from parent inline muxs var mws Middlewares if mx.inline { mws = make(Middlewares, len(mx.middlewares)) copy(mws, mx.middlewares) } mws = append(mws, middlewares...) im := &Mux{ pool: mx.pool, inline: true, parent: mx, tree: mx.tree, middlewares: mws, notFoundHandler: mx.notFoundHandler, methodNotAllowedHandler: mx.methodNotAllowedHandler, } return im } // Group creates a new inline-Mux with a copy of middleware stack. It's useful // for a group of handlers along the same routing path that use an additional // set of middlewares. See _examples/. func (mx *Mux) Group(fn func(r Router)) Router { im := mx.With() if fn != nil { fn(im) } return im } // Route creates a new Mux and mounts it along the `pattern` as a subrouter. // Effectively, this is a short-hand call to Mount. See _examples/. func (mx *Mux) Route(pattern string, fn func(r Router)) Router { if fn == nil { panic(fmt.Sprintf("chi: attempting to Route() a nil subrouter on '%s'", pattern)) } subRouter := NewRouter() fn(subRouter) mx.Mount(pattern, subRouter) return subRouter } // Mount attaches another http.Handler or chi Router as a subrouter along a routing // path. It's very useful to split up a large API as many independent routers and // compose them as a single service using Mount. See _examples/. // // Note that Mount() simply sets a wildcard along the `pattern` that will continue // routing at the `handler`, which in most cases is another chi.Router. As a result, // if you define two Mount() routes on the exact same pattern the mount will panic. func (mx *Mux) Mount(pattern string, handler http.Handler) { if handler == nil { panic(fmt.Sprintf("chi: attempting to Mount() a nil handler on '%s'", pattern)) } // Provide runtime safety for ensuring a pattern isn't mounted on an existing // routing pattern. if mx.tree.findPattern(pattern+"*") || mx.tree.findPattern(pattern+"/*") { panic(fmt.Sprintf("chi: attempting to Mount() a handler on an existing path, '%s'", pattern)) } // Assign sub-Router's with the parent not found & method not allowed handler if not specified. subr, ok := handler.(*Mux) if ok && subr.notFoundHandler == nil && mx.notFoundHandler != nil { subr.NotFound(mx.notFoundHandler) } if ok && subr.methodNotAllowedHandler == nil && mx.methodNotAllowedHandler != nil { subr.MethodNotAllowed(mx.methodNotAllowedHandler) } mountHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { rctx := RouteContext(r.Context()) // shift the url path past the previous subrouter rctx.RoutePath = mx.nextRoutePath(rctx) // reset the wildcard URLParam which connects the subrouter n := len(rctx.URLParams.Keys) - 1 if n >= 0 && rctx.URLParams.Keys[n] == "*" && len(rctx.URLParams.Values) > n { rctx.URLParams.Values[n] = "" } handler.ServeHTTP(w, r) }) if pattern == "" || pattern[len(pattern)-1] != '/' { mx.handle(mALL|mSTUB, pattern, mountHandler) mx.handle(mALL|mSTUB, pattern+"/", mountHandler) pattern += "/" } method := mALL subroutes, _ := handler.(Routes) if subroutes != nil { method |= mSTUB } n := mx.handle(method, pattern+"*", mountHandler) if subroutes != nil { n.subroutes = subroutes } } // Routes returns a slice of routing information from the tree, // useful for traversing available routes of a router. func (mx *Mux) Routes() []Route { return mx.tree.routes() } // Middlewares returns a slice of middleware handler functions. func (mx *Mux) Middlewares() Middlewares { return mx.middlewares } // Match searches the routing tree for a handler that matches the method/path. // It's similar to routing a http request, but without executing the handler // thereafter. // // Note: the *Context state is updated during execution, so manage // the state carefully or make a NewRouteContext(). func (mx *Mux) Match(rctx *Context, method, path string) bool { return mx.Find(rctx, method, path) != "" } // Find searches the routing tree for the pattern that matches // the method/path. // // Note: the *Context state is updated during execution, so manage // the state carefully or make a NewRouteContext(). func (mx *Mux) Find(rctx *Context, method, path string) string { m, ok := methodMap[method] if !ok { return "" } node, _, _ := mx.tree.FindRoute(rctx, m, path) pattern := rctx.routePattern if node != nil { if node.subroutes == nil { e := node.endpoints[m] return e.pattern } rctx.RoutePath = mx.nextRoutePath(rctx) subPattern := node.subroutes.Find(rctx, method, rctx.RoutePath) if subPattern == "" { return "" } pattern = strings.TrimSuffix(pattern, "/*") pattern += subPattern } return pattern } // NotFoundHandler returns the default Mux 404 responder whenever a route // cannot be found. func (mx *Mux) NotFoundHandler() http.HandlerFunc { if mx.notFoundHandler != nil { return mx.notFoundHandler } return http.NotFound } // MethodNotAllowedHandler returns the default Mux 405 responder whenever // a method cannot be resolved for a route. func (mx *Mux) MethodNotAllowedHandler(methodsAllowed ...methodTyp) http.HandlerFunc { if mx.methodNotAllowedHandler != nil { return mx.methodNotAllowedHandler } return methodNotAllowedHandler(methodsAllowed...) } // handle registers a http.Handler in the routing tree for a particular http method // and routing pattern. func (mx *Mux) handle(method methodTyp, pattern string, handler http.Handler) *node { if len(pattern) == 0 || pattern[0] != '/' { panic(fmt.Sprintf("chi: routing pattern must begin with '/' in '%s'", pattern)) } // Build the computed routing handler for this routing pattern. if !mx.inline && mx.handler == nil { mx.updateRouteHandler() } // Build endpoint handler with inline middlewares for the route var h http.Handler if mx.inline { mx.handler = http.HandlerFunc(mx.routeHTTP) h = Chain(mx.middlewares...).Handler(handler) } else { h = handler } // Add the endpoint to the tree and return the node return mx.tree.InsertRoute(method, pattern, h) } // routeHTTP routes a http.Request through the Mux routing tree to serve // the matching handler for a particular http method. func (mx *Mux) routeHTTP(w http.ResponseWriter, r *http.Request) { // Grab the route context object rctx := r.Context().Value(RouteCtxKey).(*Context) // The request routing path routePath := rctx.RoutePath if routePath == "" { if r.URL.RawPath != "" { routePath = r.URL.RawPath } else { routePath = r.URL.Path } if routePath == "" { routePath = "/" } } // Check if method is supported by chi if rctx.RouteMethod == "" { rctx.RouteMethod = r.Method } method, ok := methodMap[rctx.RouteMethod] if !ok { mx.MethodNotAllowedHandler().ServeHTTP(w, r) return } // Find the route if _, _, h := mx.tree.FindRoute(rctx, method, routePath); h != nil { if supportsPathValue { setPathValue(rctx, r) } h.ServeHTTP(w, r) return } if rctx.methodNotAllowed { mx.MethodNotAllowedHandler(rctx.methodsAllowed...).ServeHTTP(w, r) } else { mx.NotFoundHandler().ServeHTTP(w, r) } } func (mx *Mux) nextRoutePath(rctx *Context) string { routePath := "/" nx := len(rctx.routeParams.Keys) - 1 // index of last param in list if nx >= 0 && rctx.routeParams.Keys[nx] == "*" && len(rctx.routeParams.Values) > nx { routePath = "/" + rctx.routeParams.Values[nx] } return routePath } // Recursively update data on child routers. func (mx *Mux) updateSubRoutes(fn func(subMux *Mux)) { for _, r := range mx.tree.routes() { subMux, ok := r.SubRoutes.(*Mux) if !ok { continue } fn(subMux) } } // updateRouteHandler builds the single mux handler that is a chain of the middleware // stack, as defined by calls to Use(), and the tree router (Mux) itself. After this // point, no other middlewares can be registered on this Mux's stack. But you can still // compose additional middlewares via Group()'s or using a chained middleware handler. func (mx *Mux) updateRouteHandler() { mx.handler = chain(mx.middlewares, http.HandlerFunc(mx.routeHTTP)) } // methodNotAllowedHandler is a helper function to respond with a 405, // method not allowed. It sets the Allow header with the list of allowed // methods for the route. func methodNotAllowedHandler(methodsAllowed ...methodTyp) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { for _, m := range methodsAllowed { w.Header().Add("Allow", reverseMethodMap[m]) } w.WriteHeader(405) w.Write(nil) } } ================================================ FILE: vendor/github.com/go-chi/chi/v5/path_value.go ================================================ //go:build go1.22 && !tinygo // +build go1.22,!tinygo package chi import "net/http" // supportsPathValue is true if the Go version is 1.22 and above. // // If this is true, `net/http.Request` has methods `SetPathValue` and `PathValue`. const supportsPathValue = true // setPathValue sets the path values in the Request value // based on the provided request context. func setPathValue(rctx *Context, r *http.Request) { for i, key := range rctx.URLParams.Keys { value := rctx.URLParams.Values[i] r.SetPathValue(key, value) } } ================================================ FILE: vendor/github.com/go-chi/chi/v5/path_value_fallback.go ================================================ //go:build !go1.22 || tinygo // +build !go1.22 tinygo package chi import "net/http" // supportsPathValue is true if the Go version is 1.22 and above. // // If this is true, `net/http.Request` has methods `SetPathValue` and `PathValue`. const supportsPathValue = false // setPathValue sets the path values in the Request value // based on the provided request context. // // setPathValue is only supported in Go 1.22 and above so // this is just a blank function so that it compiles. func setPathValue(rctx *Context, r *http.Request) { } ================================================ FILE: vendor/github.com/go-chi/chi/v5/tree.go ================================================ package chi // Radix tree implementation below is a based on the original work by // Armon Dadgar in https://github.com/armon/go-radix/blob/master/radix.go // (MIT licensed). It's been heavily modified for use as a HTTP routing tree. import ( "fmt" "net/http" "regexp" "sort" "strconv" "strings" ) type methodTyp uint const ( mSTUB methodTyp = 1 << iota mCONNECT mDELETE mGET mHEAD mOPTIONS mPATCH mPOST mPUT mTRACE ) var mALL = mCONNECT | mDELETE | mGET | mHEAD | mOPTIONS | mPATCH | mPOST | mPUT | mTRACE var methodMap = map[string]methodTyp{ http.MethodConnect: mCONNECT, http.MethodDelete: mDELETE, http.MethodGet: mGET, http.MethodHead: mHEAD, http.MethodOptions: mOPTIONS, http.MethodPatch: mPATCH, http.MethodPost: mPOST, http.MethodPut: mPUT, http.MethodTrace: mTRACE, } var reverseMethodMap = map[methodTyp]string{ mCONNECT: http.MethodConnect, mDELETE: http.MethodDelete, mGET: http.MethodGet, mHEAD: http.MethodHead, mOPTIONS: http.MethodOptions, mPATCH: http.MethodPatch, mPOST: http.MethodPost, mPUT: http.MethodPut, mTRACE: http.MethodTrace, } // RegisterMethod adds support for custom HTTP method handlers, available // via Router#Method and Router#MethodFunc func RegisterMethod(method string) { if method == "" { return } method = strings.ToUpper(method) if _, ok := methodMap[method]; ok { return } n := len(methodMap) if n > strconv.IntSize-2 { panic(fmt.Sprintf("chi: max number of methods reached (%d)", strconv.IntSize)) } mt := methodTyp(2 << n) methodMap[method] = mt mALL |= mt } type nodeTyp uint8 const ( ntStatic nodeTyp = iota // /home ntRegexp // /{id:[0-9]+} ntParam // /{user} ntCatchAll // /api/v1/* ) type node struct { // subroutes on the leaf node subroutes Routes // regexp matcher for regexp nodes rex *regexp.Regexp // HTTP handler endpoints on the leaf node endpoints endpoints // prefix is the common prefix we ignore prefix string // child nodes should be stored in-order for iteration, // in groups of the node type. children [ntCatchAll + 1]nodes // first byte of the child prefix tail byte // node type: static, regexp, param, catchAll typ nodeTyp // first byte of the prefix label byte } // endpoints is a mapping of http method constants to handlers // for a given route. type endpoints map[methodTyp]*endpoint type endpoint struct { // endpoint handler handler http.Handler // pattern is the routing pattern for handler nodes pattern string // parameter keys recorded on handler nodes paramKeys []string } func (s endpoints) Value(method methodTyp) *endpoint { mh, ok := s[method] if !ok { mh = &endpoint{} s[method] = mh } return mh } func (n *node) InsertRoute(method methodTyp, pattern string, handler http.Handler) *node { var parent *node search := pattern for { // Handle key exhaustion if len(search) == 0 { // Insert or update the node's leaf handler n.setEndpoint(method, handler, pattern) return n } // We're going to be searching for a wild node next, // in this case, we need to get the tail var label = search[0] var segTail byte var segEndIdx int var segTyp nodeTyp var segRexpat string if label == '{' || label == '*' { segTyp, _, segRexpat, segTail, _, segEndIdx = patNextSegment(search) } var prefix string if segTyp == ntRegexp { prefix = segRexpat } // Look for the edge to attach to parent = n n = n.getEdge(segTyp, label, segTail, prefix) // No edge, create one if n == nil { child := &node{label: label, tail: segTail, prefix: search} hn := parent.addChild(child, search) hn.setEndpoint(method, handler, pattern) return hn } // Found an edge to match the pattern if n.typ > ntStatic { // We found a param node, trim the param from the search path and continue. // This param/wild pattern segment would already be on the tree from a previous // call to addChild when creating a new node. search = search[segEndIdx:] continue } // Static nodes fall below here. // Determine longest prefix of the search key on match. commonPrefix := longestPrefix(search, n.prefix) if commonPrefix == len(n.prefix) { // the common prefix is as long as the current node's prefix we're attempting to insert. // keep the search going. search = search[commonPrefix:] continue } // Split the node child := &node{ typ: ntStatic, prefix: search[:commonPrefix], } parent.replaceChild(search[0], segTail, child) // Restore the existing node n.label = n.prefix[commonPrefix] n.prefix = n.prefix[commonPrefix:] child.addChild(n, n.prefix) // If the new key is a subset, set the method/handler on this node and finish. search = search[commonPrefix:] if len(search) == 0 { child.setEndpoint(method, handler, pattern) return child } // Create a new edge for the node subchild := &node{ typ: ntStatic, label: search[0], prefix: search, } hn := child.addChild(subchild, search) hn.setEndpoint(method, handler, pattern) return hn } } // addChild appends the new `child` node to the tree using the `pattern` as the trie key. // For a URL router like chi's, we split the static, param, regexp and wildcard segments // into different nodes. In addition, addChild will recursively call itself until every // pattern segment is added to the url pattern tree as individual nodes, depending on type. func (n *node) addChild(child *node, prefix string) *node { search := prefix // handler leaf node added to the tree is the child. // this may be overridden later down the flow hn := child // Parse next segment segTyp, _, segRexpat, segTail, segStartIdx, segEndIdx := patNextSegment(search) // Add child depending on next up segment switch segTyp { case ntStatic: // Search prefix is all static (that is, has no params in path) // noop default: // Search prefix contains a param, regexp or wildcard if segTyp == ntRegexp { rex, err := regexp.Compile(segRexpat) if err != nil { panic(fmt.Sprintf("chi: invalid regexp pattern '%s' in route param", segRexpat)) } child.prefix = segRexpat child.rex = rex } if segStartIdx == 0 { // Route starts with a param child.typ = segTyp if segTyp == ntCatchAll { segStartIdx = -1 } else { segStartIdx = segEndIdx } if segStartIdx < 0 { segStartIdx = len(search) } child.tail = segTail // for params, we set the tail if segStartIdx != len(search) { // add static edge for the remaining part, split the end. // its not possible to have adjacent param nodes, so its certainly // going to be a static node next. search = search[segStartIdx:] // advance search position nn := &node{ typ: ntStatic, label: search[0], prefix: search, } hn = child.addChild(nn, search) } } else if segStartIdx > 0 { // Route has some param // starts with a static segment child.typ = ntStatic child.prefix = search[:segStartIdx] child.rex = nil // add the param edge node search = search[segStartIdx:] nn := &node{ typ: segTyp, label: search[0], tail: segTail, } hn = child.addChild(nn, search) } } n.children[child.typ] = append(n.children[child.typ], child) n.children[child.typ].Sort() return hn } func (n *node) replaceChild(label, tail byte, child *node) { for i := 0; i < len(n.children[child.typ]); i++ { if n.children[child.typ][i].label == label && n.children[child.typ][i].tail == tail { n.children[child.typ][i] = child n.children[child.typ][i].label = label n.children[child.typ][i].tail = tail return } } panic("chi: replacing missing child") } func (n *node) getEdge(ntyp nodeTyp, label, tail byte, prefix string) *node { nds := n.children[ntyp] for i := 0; i < len(nds); i++ { if nds[i].label == label && nds[i].tail == tail { if ntyp == ntRegexp && nds[i].prefix != prefix { continue } return nds[i] } } return nil } func (n *node) setEndpoint(method methodTyp, handler http.Handler, pattern string) { // Set the handler for the method type on the node if n.endpoints == nil { n.endpoints = make(endpoints) } paramKeys := patParamKeys(pattern) if method&mSTUB == mSTUB { n.endpoints.Value(mSTUB).handler = handler } if method&mALL == mALL { h := n.endpoints.Value(mALL) h.handler = handler h.pattern = pattern h.paramKeys = paramKeys for _, m := range methodMap { h := n.endpoints.Value(m) h.handler = handler h.pattern = pattern h.paramKeys = paramKeys } } else { h := n.endpoints.Value(method) h.handler = handler h.pattern = pattern h.paramKeys = paramKeys } } func (n *node) FindRoute(rctx *Context, method methodTyp, path string) (*node, endpoints, http.Handler) { // Reset the context routing pattern and params rctx.routePattern = "" rctx.routeParams.Keys = rctx.routeParams.Keys[:0] rctx.routeParams.Values = rctx.routeParams.Values[:0] // Find the routing handlers for the path rn := n.findRoute(rctx, method, path) if rn == nil { return nil, nil, nil } // Record the routing params in the request lifecycle rctx.URLParams.Keys = append(rctx.URLParams.Keys, rctx.routeParams.Keys...) rctx.URLParams.Values = append(rctx.URLParams.Values, rctx.routeParams.Values...) // Record the routing pattern in the request lifecycle if rn.endpoints[method].pattern != "" { rctx.routePattern = rn.endpoints[method].pattern rctx.RoutePatterns = append(rctx.RoutePatterns, rctx.routePattern) } return rn, rn.endpoints, rn.endpoints[method].handler } // Recursive edge traversal by checking all nodeTyp groups along the way. // It's like searching through a multi-dimensional radix trie. func (n *node) findRoute(rctx *Context, method methodTyp, path string) *node { nn := n search := path for t, nds := range nn.children { ntyp := nodeTyp(t) if len(nds) == 0 { continue } var xn *node xsearch := search var label byte if search != "" { label = search[0] } switch ntyp { case ntStatic: xn = nds.findEdge(label) if xn == nil || !strings.HasPrefix(xsearch, xn.prefix) { continue } xsearch = xsearch[len(xn.prefix):] case ntParam, ntRegexp: // short-circuit and return no matching route for empty param values if xsearch == "" { continue } // serially loop through each node grouped by the tail delimiter for idx := 0; idx < len(nds); idx++ { xn = nds[idx] // label for param nodes is the delimiter byte p := strings.IndexByte(xsearch, xn.tail) if p < 0 { if xn.tail == '/' { p = len(xsearch) } else { continue } } else if ntyp == ntRegexp && p == 0 { continue } if ntyp == ntRegexp && xn.rex != nil { if !xn.rex.MatchString(xsearch[:p]) { continue } } else if strings.IndexByte(xsearch[:p], '/') != -1 { // avoid a match across path segments continue } prevlen := len(rctx.routeParams.Values) rctx.routeParams.Values = append(rctx.routeParams.Values, xsearch[:p]) xsearch = xsearch[p:] if len(xsearch) == 0 { if xn.isLeaf() { h := xn.endpoints[method] if h != nil && h.handler != nil { rctx.routeParams.Keys = append(rctx.routeParams.Keys, h.paramKeys...) return xn } for endpoints := range xn.endpoints { if endpoints == mALL || endpoints == mSTUB { continue } rctx.methodsAllowed = append(rctx.methodsAllowed, endpoints) } // flag that the routing context found a route, but not a corresponding // supported method rctx.methodNotAllowed = true } } // recursively find the next node on this branch fin := xn.findRoute(rctx, method, xsearch) if fin != nil { return fin } // not found on this branch, reset vars rctx.routeParams.Values = rctx.routeParams.Values[:prevlen] xsearch = search } rctx.routeParams.Values = append(rctx.routeParams.Values, "") default: // catch-all nodes rctx.routeParams.Values = append(rctx.routeParams.Values, search) xn = nds[0] xsearch = "" } if xn == nil { continue } // did we find it yet? if len(xsearch) == 0 { if xn.isLeaf() { h := xn.endpoints[method] if h != nil && h.handler != nil { rctx.routeParams.Keys = append(rctx.routeParams.Keys, h.paramKeys...) return xn } for endpoints := range xn.endpoints { if endpoints == mALL || endpoints == mSTUB { continue } rctx.methodsAllowed = append(rctx.methodsAllowed, endpoints) } // flag that the routing context found a route, but not a corresponding // supported method rctx.methodNotAllowed = true } } // recursively find the next node.. fin := xn.findRoute(rctx, method, xsearch) if fin != nil { return fin } // Did not find final handler, let's remove the param here if it was set if xn.typ > ntStatic { if len(rctx.routeParams.Values) > 0 { rctx.routeParams.Values = rctx.routeParams.Values[:len(rctx.routeParams.Values)-1] } } } return nil } func (n *node) findEdge(ntyp nodeTyp, label byte) *node { nds := n.children[ntyp] num := len(nds) idx := 0 switch ntyp { case ntStatic, ntParam, ntRegexp: i, j := 0, num-1 for i <= j { idx = i + (j-i)/2 if label > nds[idx].label { i = idx + 1 } else if label < nds[idx].label { j = idx - 1 } else { i = num // breaks cond } } if nds[idx].label != label { return nil } return nds[idx] default: // catch all return nds[idx] } } func (n *node) isLeaf() bool { return n.endpoints != nil } func (n *node) findPattern(pattern string) bool { nn := n for _, nds := range nn.children { if len(nds) == 0 { continue } n = nn.findEdge(nds[0].typ, pattern[0]) if n == nil { continue } var idx int var xpattern string switch n.typ { case ntStatic: idx = longestPrefix(pattern, n.prefix) if idx < len(n.prefix) { continue } case ntParam, ntRegexp: idx = strings.IndexByte(pattern, '}') + 1 case ntCatchAll: idx = longestPrefix(pattern, "*") default: panic("chi: unknown node type") } xpattern = pattern[idx:] if len(xpattern) == 0 { return true } return n.findPattern(xpattern) } return false } func (n *node) routes() []Route { rts := []Route{} n.walk(func(eps endpoints, subroutes Routes) bool { if eps[mSTUB] != nil && eps[mSTUB].handler != nil && subroutes == nil { return false } // Group methodHandlers by unique patterns pats := make(map[string]endpoints) for mt, h := range eps { if h.pattern == "" { continue } p, ok := pats[h.pattern] if !ok { p = endpoints{} pats[h.pattern] = p } p[mt] = h } for p, mh := range pats { hs := make(map[string]http.Handler) if mh[mALL] != nil && mh[mALL].handler != nil { hs["*"] = mh[mALL].handler } for mt, h := range mh { if h.handler == nil { continue } m := methodTypString(mt) if m == "" { continue } hs[m] = h.handler } rt := Route{subroutes, hs, p} rts = append(rts, rt) } return false }) return rts } func (n *node) walk(fn func(eps endpoints, subroutes Routes) bool) bool { // Visit the leaf values if any if (n.endpoints != nil || n.subroutes != nil) && fn(n.endpoints, n.subroutes) { return true } // Recurse on the children for _, ns := range n.children { for _, cn := range ns { if cn.walk(fn) { return true } } } return false } // patNextSegment returns the next segment details from a pattern: // node type, param key, regexp string, param tail byte, param starting index, param ending index func patNextSegment(pattern string) (nodeTyp, string, string, byte, int, int) { ps := strings.Index(pattern, "{") ws := strings.Index(pattern, "*") if ps < 0 && ws < 0 { return ntStatic, "", "", 0, 0, len(pattern) // we return the entire thing } // Sanity check if ps >= 0 && ws >= 0 && ws < ps { panic("chi: wildcard '*' must be the last pattern in a route, otherwise use a '{param}'") } var tail byte = '/' // Default endpoint tail to / byte if ps >= 0 { // Param/Regexp pattern is next nt := ntParam // Read to closing } taking into account opens and closes in curl count (cc) cc := 0 pe := ps for i, c := range pattern[ps:] { if c == '{' { cc++ } else if c == '}' { cc-- if cc == 0 { pe = ps + i break } } } if pe == ps { panic("chi: route param closing delimiter '}' is missing") } key := pattern[ps+1 : pe] pe++ // set end to next position if pe < len(pattern) { tail = pattern[pe] } key, rexpat, isRegexp := strings.Cut(key, ":") if isRegexp { nt = ntRegexp } if len(rexpat) > 0 { if rexpat[0] != '^' { rexpat = "^" + rexpat } if rexpat[len(rexpat)-1] != '$' { rexpat += "$" } } return nt, key, rexpat, tail, ps, pe } // Wildcard pattern as finale if ws < len(pattern)-1 { panic("chi: wildcard '*' must be the last value in a route. trim trailing text or use a '{param}' instead") } return ntCatchAll, "*", "", 0, ws, len(pattern) } func patParamKeys(pattern string) []string { pat := pattern paramKeys := []string{} for { ptyp, paramKey, _, _, _, e := patNextSegment(pat) if ptyp == ntStatic { return paramKeys } for i := 0; i < len(paramKeys); i++ { if paramKeys[i] == paramKey { panic(fmt.Sprintf("chi: routing pattern '%s' contains duplicate param key, '%s'", pattern, paramKey)) } } paramKeys = append(paramKeys, paramKey) pat = pat[e:] } } // longestPrefix finds the length of the shared prefix // of two strings func longestPrefix(k1, k2 string) int { max := len(k1) if l := len(k2); l < max { max = l } var i int for i = 0; i < max; i++ { if k1[i] != k2[i] { break } } return i } func methodTypString(method methodTyp) string { for s, t := range methodMap { if method == t { return s } } return "" } type nodes []*node // Sort the list of nodes by label func (ns nodes) Sort() { sort.Sort(ns); ns.tailSort() } func (ns nodes) Len() int { return len(ns) } func (ns nodes) Swap(i, j int) { ns[i], ns[j] = ns[j], ns[i] } func (ns nodes) Less(i, j int) bool { return ns[i].label < ns[j].label } // tailSort pushes nodes with '/' as the tail to the end of the list for param nodes. // The list order determines the traversal order. func (ns nodes) tailSort() { for i := len(ns) - 1; i >= 0; i-- { if ns[i].typ > ntStatic && ns[i].tail == '/' { ns.Swap(i, len(ns)-1) return } } } func (ns nodes) findEdge(label byte) *node { num := len(ns) idx := 0 i, j := 0, num-1 for i <= j { idx = i + (j-i)/2 if label > ns[idx].label { i = idx + 1 } else if label < ns[idx].label { j = idx - 1 } else { i = num // breaks cond } } if ns[idx].label != label { return nil } return ns[idx] } // Route describes the details of a routing handler. // Handlers map key is an HTTP method type Route struct { SubRoutes Routes Handlers map[string]http.Handler Pattern string } // WalkFunc is the type of the function called for each method and route visited by Walk. type WalkFunc func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error // Walk walks any router tree that implements Routes interface. func Walk(r Routes, walkFn WalkFunc) error { return walk(r, walkFn, "") } func walk(r Routes, walkFn WalkFunc, parentRoute string, parentMw ...func(http.Handler) http.Handler) error { for _, route := range r.Routes() { mws := make([]func(http.Handler) http.Handler, len(parentMw)) copy(mws, parentMw) mws = append(mws, r.Middlewares()...) if route.SubRoutes != nil { if err := walk(route.SubRoutes, walkFn, parentRoute+route.Pattern, mws...); err != nil { return err } continue } for method, handler := range route.Handlers { if method == "*" { // Ignore a "catchAll" method, since we pass down all the specific methods for each route. continue } fullRoute := parentRoute + route.Pattern fullRoute = strings.Replace(fullRoute, "/*/", "/", -1) if chain, ok := handler.(*ChainHandler); ok { if err := walkFn(method, fullRoute, chain.Endpoint, append(mws, chain.Middlewares...)...); err != nil { return err } } else { if err := walkFn(method, fullRoute, handler, mws...); err != nil { return err } } } } return nil } ================================================ FILE: vendor/github.com/go-chi/cors/LICENSE ================================================ Copyright (c) 2014 Olivier Poitrey Copyright (c) 2016-Present https://github.com/go-chi authors MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: vendor/github.com/go-chi/cors/README.md ================================================ # CORS net/http middleware [go-chi/cors](https://github.com/go-chi/cors) is a fork of [github.com/rs/cors](https://github.com/rs/cors) that provides a `net/http` compatible middleware for performing preflight CORS checks on the server side. These headers are required for using the browser native [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). This middleware is designed to be used as a top-level middleware on the [chi](https://github.com/go-chi/chi) router. Applying with within a `r.Group()` or using `With()` will not work without routes matching `OPTIONS` added. ## Usage ```go func main() { r := chi.NewRouter() // Basic CORS // for more ideas, see: https://developer.github.com/v3/#cross-origin-resource-sharing r.Use(cors.Handler(cors.Options{ // AllowedOrigins: []string{"https://foo.com"}, // Use this to allow specific origin hosts AllowedOrigins: []string{"https://*", "http://*"}, // AllowOriginFunc: func(r *http.Request, origin string) bool { return true }, AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, ExposedHeaders: []string{"Link"}, AllowCredentials: false, MaxAge: 300, // Maximum value not ignored by any of major browsers })) r.Get("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("welcome")) }) http.ListenAndServe(":3000", r) } ``` ## Credits All credit for the original work of this middleware goes out to [github.com/rs](github.com/rs). ================================================ FILE: vendor/github.com/go-chi/cors/cors.go ================================================ // cors package is net/http handler to handle CORS related requests // as defined by http://www.w3.org/TR/cors/ // // You can configure it by passing an option struct to cors.New: // // c := cors.New(cors.Options{ // AllowedOrigins: []string{"foo.com"}, // AllowedMethods: []string{"GET", "POST", "DELETE"}, // AllowCredentials: true, // }) // // Then insert the handler in the chain: // // handler = c.Handler(handler) // // See Options documentation for more options. // // The resulting handler is a standard net/http handler. package cors import ( "log" "net/http" "os" "strconv" "strings" ) // Options is a configuration container to setup the CORS middleware. type Options struct { // AllowedOrigins is a list of origins a cross-domain request can be executed from. // If the special "*" value is present in the list, all origins will be allowed. // An origin may contain a wildcard (*) to replace 0 or more characters // (i.e.: http://*.domain.com). Usage of wildcards implies a small performance penalty. // Only one wildcard can be used per origin. // Default value is ["*"] AllowedOrigins []string // AllowOriginFunc is a custom function to validate the origin. It takes the origin // as argument and returns true if allowed or false otherwise. If this option is // set, the content of AllowedOrigins is ignored. AllowOriginFunc func(r *http.Request, origin string) bool // AllowedMethods is a list of methods the client is allowed to use with // cross-domain requests. Default value is simple methods (HEAD, GET and POST). AllowedMethods []string // AllowedHeaders is list of non simple headers the client is allowed to use with // cross-domain requests. // If the special "*" value is present in the list, all headers will be allowed. // Default value is [] but "Origin" is always appended to the list. AllowedHeaders []string // ExposedHeaders indicates which headers are safe to expose to the API of a CORS // API specification ExposedHeaders []string // AllowCredentials indicates whether the request can include user credentials like // cookies, HTTP authentication or client side SSL certificates. AllowCredentials bool // MaxAge indicates how long (in seconds) the results of a preflight request // can be cached MaxAge int // OptionsPassthrough instructs preflight to let other potential next handlers to // process the OPTIONS method. Turn this on if your application handles OPTIONS. OptionsPassthrough bool // Debugging flag adds additional output to debug server side CORS issues Debug bool } // Logger generic interface for logger type Logger interface { Printf(string, ...interface{}) } // Cors http handler type Cors struct { // Debug logger Log Logger // Normalized list of plain allowed origins allowedOrigins []string // List of allowed origins containing wildcards allowedWOrigins []wildcard // Optional origin validator function allowOriginFunc func(r *http.Request, origin string) bool // Normalized list of allowed headers allowedHeaders []string // Normalized list of allowed methods allowedMethods []string // Normalized list of exposed headers exposedHeaders []string maxAge int // Set to true when allowed origins contains a "*" allowedOriginsAll bool // Set to true when allowed headers contains a "*" allowedHeadersAll bool allowCredentials bool optionPassthrough bool } // New creates a new Cors handler with the provided options. func New(options Options) *Cors { c := &Cors{ exposedHeaders: convert(options.ExposedHeaders, http.CanonicalHeaderKey), allowOriginFunc: options.AllowOriginFunc, allowCredentials: options.AllowCredentials, maxAge: options.MaxAge, optionPassthrough: options.OptionsPassthrough, } if options.Debug && c.Log == nil { c.Log = log.New(os.Stdout, "[cors] ", log.LstdFlags) } // Normalize options // Note: for origins and methods matching, the spec requires a case-sensitive matching. // As it may error prone, we chose to ignore the spec here. // Allowed Origins if len(options.AllowedOrigins) == 0 { if options.AllowOriginFunc == nil { // Default is all origins c.allowedOriginsAll = true } } else { c.allowedOrigins = []string{} c.allowedWOrigins = []wildcard{} for _, origin := range options.AllowedOrigins { // Normalize origin = strings.ToLower(origin) if origin == "*" { // If "*" is present in the list, turn the whole list into a match all c.allowedOriginsAll = true c.allowedOrigins = nil c.allowedWOrigins = nil break } else if i := strings.IndexByte(origin, '*'); i >= 0 { // Split the origin in two: start and end string without the * w := wildcard{origin[0:i], origin[i+1:]} c.allowedWOrigins = append(c.allowedWOrigins, w) } else { c.allowedOrigins = append(c.allowedOrigins, origin) } } } // Allowed Headers if len(options.AllowedHeaders) == 0 { // Use sensible defaults c.allowedHeaders = []string{"Origin", "Accept", "Content-Type"} } else { // Origin is always appended as some browsers will always request for this header at preflight c.allowedHeaders = convert(append(options.AllowedHeaders, "Origin"), http.CanonicalHeaderKey) for _, h := range options.AllowedHeaders { if h == "*" { c.allowedHeadersAll = true c.allowedHeaders = nil break } } } // Allowed Methods if len(options.AllowedMethods) == 0 { // Default is spec's "simple" methods c.allowedMethods = []string{http.MethodGet, http.MethodPost, http.MethodHead} } else { c.allowedMethods = convert(options.AllowedMethods, strings.ToUpper) } return c } // Handler creates a new Cors handler with passed options. func Handler(options Options) func(next http.Handler) http.Handler { c := New(options) return c.Handler } // AllowAll create a new Cors handler with permissive configuration allowing all // origins with all standard methods with any header and credentials. func AllowAll() *Cors { return New(Options{ AllowedOrigins: []string{"*"}, AllowedMethods: []string{ http.MethodHead, http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete, }, AllowedHeaders: []string{"*"}, AllowCredentials: false, }) } // Handler apply the CORS specification on the request, and add relevant CORS headers // as necessary. func (c *Cors) Handler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodOptions && r.Header.Get("Access-Control-Request-Method") != "" { c.logf("Handler: Preflight request") c.handlePreflight(w, r) // Preflight requests are standalone and should stop the chain as some other // middleware may not handle OPTIONS requests correctly. One typical example // is authentication middleware ; OPTIONS requests won't carry authentication // headers (see #1) if c.optionPassthrough { next.ServeHTTP(w, r) } else { w.WriteHeader(http.StatusOK) } } else { c.logf("Handler: Actual request") c.handleActualRequest(w, r) next.ServeHTTP(w, r) } }) } // handlePreflight handles pre-flight CORS requests func (c *Cors) handlePreflight(w http.ResponseWriter, r *http.Request) { headers := w.Header() origin := r.Header.Get("Origin") if r.Method != http.MethodOptions { c.logf("Preflight aborted: %s!=OPTIONS", r.Method) return } // Always set Vary headers // see https://github.com/rs/cors/issues/10, // https://github.com/rs/cors/commit/dbdca4d95feaa7511a46e6f1efb3b3aa505bc43f#commitcomment-12352001 headers.Add("Vary", "Origin") headers.Add("Vary", "Access-Control-Request-Method") headers.Add("Vary", "Access-Control-Request-Headers") if origin == "" { c.logf("Preflight aborted: empty origin") return } if !c.isOriginAllowed(r, origin) { c.logf("Preflight aborted: origin '%s' not allowed", origin) return } reqMethod := r.Header.Get("Access-Control-Request-Method") if !c.isMethodAllowed(reqMethod) { c.logf("Preflight aborted: method '%s' not allowed", reqMethod) return } reqHeaders := parseHeaderList(r.Header.Get("Access-Control-Request-Headers")) if !c.areHeadersAllowed(reqHeaders) { c.logf("Preflight aborted: headers '%v' not allowed", reqHeaders) return } if c.allowedOriginsAll { headers.Set("Access-Control-Allow-Origin", "*") } else { headers.Set("Access-Control-Allow-Origin", origin) } // Spec says: Since the list of methods can be unbounded, simply returning the method indicated // by Access-Control-Request-Method (if supported) can be enough headers.Set("Access-Control-Allow-Methods", strings.ToUpper(reqMethod)) if len(reqHeaders) > 0 { // Spec says: Since the list of headers can be unbounded, simply returning supported headers // from Access-Control-Request-Headers can be enough headers.Set("Access-Control-Allow-Headers", strings.Join(reqHeaders, ", ")) } if c.allowCredentials { headers.Set("Access-Control-Allow-Credentials", "true") } if c.maxAge > 0 { headers.Set("Access-Control-Max-Age", strconv.Itoa(c.maxAge)) } c.logf("Preflight response headers: %v", headers) } // handleActualRequest handles simple cross-origin requests, actual request or redirects func (c *Cors) handleActualRequest(w http.ResponseWriter, r *http.Request) { headers := w.Header() origin := r.Header.Get("Origin") // Always set Vary, see https://github.com/rs/cors/issues/10 headers.Add("Vary", "Origin") if origin == "" { c.logf("Actual request no headers added: missing origin") return } if !c.isOriginAllowed(r, origin) { c.logf("Actual request no headers added: origin '%s' not allowed", origin) return } // Note that spec does define a way to specifically disallow a simple method like GET or // POST. Access-Control-Allow-Methods is only used for pre-flight requests and the // spec doesn't instruct to check the allowed methods for simple cross-origin requests. // We think it's a nice feature to be able to have control on those methods though. if !c.isMethodAllowed(r.Method) { c.logf("Actual request no headers added: method '%s' not allowed", r.Method) return } if c.allowedOriginsAll { headers.Set("Access-Control-Allow-Origin", "*") } else { headers.Set("Access-Control-Allow-Origin", origin) } if len(c.exposedHeaders) > 0 { headers.Set("Access-Control-Expose-Headers", strings.Join(c.exposedHeaders, ", ")) } if c.allowCredentials { headers.Set("Access-Control-Allow-Credentials", "true") } c.logf("Actual response added headers: %v", headers) } // convenience method. checks if a logger is set. func (c *Cors) logf(format string, a ...interface{}) { if c.Log != nil { c.Log.Printf(format, a...) } } // isOriginAllowed checks if a given origin is allowed to perform cross-domain requests // on the endpoint func (c *Cors) isOriginAllowed(r *http.Request, origin string) bool { if c.allowOriginFunc != nil { return c.allowOriginFunc(r, origin) } if c.allowedOriginsAll { return true } origin = strings.ToLower(origin) for _, o := range c.allowedOrigins { if o == origin { return true } } for _, w := range c.allowedWOrigins { if w.match(origin) { return true } } return false } // isMethodAllowed checks if a given method can be used as part of a cross-domain request // on the endpoint func (c *Cors) isMethodAllowed(method string) bool { if len(c.allowedMethods) == 0 { // If no method allowed, always return false, even for preflight request return false } method = strings.ToUpper(method) if method == http.MethodOptions { // Always allow preflight requests return true } for _, m := range c.allowedMethods { if m == method { return true } } return false } // areHeadersAllowed checks if a given list of headers are allowed to used within // a cross-domain request. func (c *Cors) areHeadersAllowed(requestedHeaders []string) bool { if c.allowedHeadersAll || len(requestedHeaders) == 0 { return true } for _, header := range requestedHeaders { header = http.CanonicalHeaderKey(header) found := false for _, h := range c.allowedHeaders { if h == header { found = true break } } if !found { return false } } return true } ================================================ FILE: vendor/github.com/go-chi/cors/utils.go ================================================ package cors import "strings" const toLower = 'a' - 'A' type converter func(string) string type wildcard struct { prefix string suffix string } func (w wildcard) match(s string) bool { return len(s) >= len(w.prefix+w.suffix) && strings.HasPrefix(s, w.prefix) && strings.HasSuffix(s, w.suffix) } // convert converts a list of string using the passed converter function func convert(s []string, c converter) []string { out := []string{} for _, i := range s { out = append(out, c(i)) } return out } // parseHeaderList tokenize + normalize a string containing a list of headers func parseHeaderList(headerList string) []string { l := len(headerList) h := make([]byte, 0, l) upper := true // Estimate the number headers in order to allocate the right splice size t := 0 for i := 0; i < l; i++ { if headerList[i] == ',' { t++ } } headers := make([]string, 0, t) for i := 0; i < l; i++ { b := headerList[i] if b >= 'a' && b <= 'z' { if upper { h = append(h, b-toLower) } else { h = append(h, b) } } else if b >= 'A' && b <= 'Z' { if !upper { h = append(h, b+toLower) } else { h = append(h, b) } } else if b == '-' || b == '_' || b == '.' || (b >= '0' && b <= '9') { h = append(h, b) } if b == ' ' || b == ',' || i == l-1 { if len(h) > 0 { // Flush the found header headers = append(headers, string(h)) h = h[:0] upper = true } } else { upper = b == '-' } } return headers } ================================================ FILE: vendor/github.com/go-jose/go-jose/v4/.gitignore ================================================ jose-util/jose-util jose-util.t.err ================================================ FILE: vendor/github.com/go-jose/go-jose/v4/.golangci.yml ================================================ # https://github.com/golangci/golangci-lint run: skip-files: - doc_test.go modules-download-mode: readonly linters: enable-all: true disable: - gochecknoglobals - goconst - lll - maligned - nakedret - scopelint - unparam - funlen # added in 1.18 (requires go-jose changes before it can be enabled) linters-settings: gocyclo: min-complexity: 35 issues: exclude-rules: - text: "don't use ALL_CAPS in Go names" linters: - golint - text: "hardcoded credentials" linters: - gosec - text: "weak cryptographic primitive" linters: - gosec - path: json/ linters: - dupl - errcheck - gocritic - gocyclo - golint - govet - ineffassign - staticcheck - structcheck - stylecheck - unused - path: _test\.go linters: - scopelint - path: jwk.go linters: - gocyclo ================================================ FILE: vendor/github.com/go-jose/go-jose/v4/.travis.yml ================================================ language: go matrix: fast_finish: true allow_failures: - go: tip go: - "1.13.x" - "1.14.x" - tip before_script: - export PATH=$HOME/.local/bin:$PATH before_install: - go get -u github.com/mattn/goveralls github.com/wadey/gocovmerge - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.18.0 - pip install cram --user script: - go test -v -covermode=count -coverprofile=profile.cov . - go test -v -covermode=count -coverprofile=cryptosigner/profile.cov ./cryptosigner - go test -v -covermode=count -coverprofile=cipher/profile.cov ./cipher - go test -v -covermode=count -coverprofile=jwt/profile.cov ./jwt - go test -v ./json # no coverage for forked encoding/json package - golangci-lint run - cd jose-util && go build && PATH=$PWD:$PATH cram -v jose-util.t # cram tests jose-util - cd .. after_success: - gocovmerge *.cov */*.cov > merged.coverprofile - goveralls -coverprofile merged.coverprofile -service=travis-ci ================================================ FILE: vendor/github.com/go-jose/go-jose/v4/CONTRIBUTING.md ================================================ # Contributing If you would like to contribute code to go-jose you can do so through GitHub by forking the repository and sending a pull request. When submitting code, please make every effort to follow existing conventions and style in order to keep the code as readable as possible. Please also make sure all tests pass by running `go test`, and format your code with `go fmt`. We also recommend using `golint` and `errcheck`. ================================================ FILE: vendor/github.com/go-jose/go-jose/v4/LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: vendor/github.com/go-jose/go-jose/v4/README.md ================================================ # Go JOSE [![godoc](https://pkg.go.dev/badge/github.com/go-jose/go-jose/v4.svg)](https://pkg.go.dev/github.com/go-jose/go-jose/v4) [![godoc](https://pkg.go.dev/badge/github.com/go-jose/go-jose/v4/jwt.svg)](https://pkg.go.dev/github.com/go-jose/go-jose/v4/jwt) [![license](https://img.shields.io/badge/license-apache_2.0-blue.svg?style=flat)](https://raw.githubusercontent.com/go-jose/go-jose/master/LICENSE) Package jose aims to provide an implementation of the Javascript Object Signing and Encryption set of standards. This includes support for JSON Web Encryption, JSON Web Signature, and JSON Web Token standards. ## Overview The implementation follows the [JSON Web Encryption](https://dx.doi.org/10.17487/RFC7516) (RFC 7516), [JSON Web Signature](https://dx.doi.org/10.17487/RFC7515) (RFC 7515), and [JSON Web Token](https://dx.doi.org/10.17487/RFC7519) (RFC 7519) specifications. Tables of supported algorithms are shown below. The library supports both the compact and JWS/JWE JSON Serialization formats, and has optional support for multiple recipients. It also comes with a small command-line utility ([`jose-util`](https://pkg.go.dev/github.com/go-jose/go-jose/jose-util)) for dealing with JOSE messages in a shell. **Note**: We use a forked version of the `encoding/json` package from the Go standard library which uses case-sensitive matching for member names (instead of [case-insensitive matching](https://www.ietf.org/mail-archive/web/json/current/msg03763.html)). This is to avoid differences in interpretation of messages between go-jose and libraries in other languages. ### Versions The forthcoming Version 5 will be released with several breaking API changes, and will require Golang's `encoding/json/v2`, which is currently requires Go 1.25 built with GOEXPERIMENT=jsonv2. Version 4 is the current stable version: import "github.com/go-jose/go-jose/v4" It supports at least the current and previous Golang release. Currently it requires Golang 1.24. Version 3 is only receiving critical security updates. Migration to Version 4 is recommended. Versions 1 and 2 are obsolete, but can be found in the old repository, [square/go-jose](https://github.com/square/go-jose). ### Supported algorithms See below for a table of supported algorithms. Algorithm identifiers match the names in the [JSON Web Algorithms](https://dx.doi.org/10.17487/RFC7518) standard where possible. The Godoc reference has a list of constants. | Key encryption | Algorithm identifier(s) | |:-----------------------|:-----------------------------------------------| | RSA-PKCS#1v1.5 | RSA1_5 | | RSA-OAEP | RSA-OAEP, RSA-OAEP-256 | | AES key wrap | A128KW, A192KW, A256KW | | AES-GCM key wrap | A128GCMKW, A192GCMKW, A256GCMKW | | ECDH-ES + AES key wrap | ECDH-ES+A128KW, ECDH-ES+A192KW, ECDH-ES+A256KW | | ECDH-ES (direct) | ECDH-ES1 | | Direct encryption | dir1 | 1. Not supported in multi-recipient mode | Signing / MAC | Algorithm identifier(s) | |:------------------|:------------------------| | RSASSA-PKCS#1v1.5 | RS256, RS384, RS512 | | RSASSA-PSS | PS256, PS384, PS512 | | HMAC | HS256, HS384, HS512 | | ECDSA | ES256, ES384, ES512 | | Ed25519 | EdDSA2 | 2. Only available in version 2 of the package | Content encryption | Algorithm identifier(s) | |:-------------------|:--------------------------------------------| | AES-CBC+HMAC | A128CBC-HS256, A192CBC-HS384, A256CBC-HS512 | | AES-GCM | A128GCM, A192GCM, A256GCM | | Compression | Algorithm identifiers(s) | |:-------------------|--------------------------| | DEFLATE (RFC 1951) | DEF | ### Supported key types See below for a table of supported key types. These are understood by the library, and can be passed to corresponding functions such as `NewEncrypter` or `NewSigner`. Each of these keys can also be wrapped in a JWK if desired, which allows attaching a key id. | Algorithm(s) | Corresponding types | |:------------------|--------------------------------------------------------------------------------------------------------------------------------------| | RSA | *[rsa.PublicKey](https://pkg.go.dev/crypto/rsa/#PublicKey), *[rsa.PrivateKey](https://pkg.go.dev/crypto/rsa/#PrivateKey) | | ECDH, ECDSA | *[ecdsa.PublicKey](https://pkg.go.dev/crypto/ecdsa/#PublicKey), *[ecdsa.PrivateKey](https://pkg.go.dev/crypto/ecdsa/#PrivateKey) | | EdDSA1 | [ed25519.PublicKey](https://pkg.go.dev/crypto/ed25519#PublicKey), [ed25519.PrivateKey](https://pkg.go.dev/crypto/ed25519#PrivateKey) | | AES, HMAC | []byte | 1. Only available in version 2 or later of the package ## Examples [![godoc](https://pkg.go.dev/badge/github.com/go-jose/go-jose/v4.svg)](https://pkg.go.dev/github.com/go-jose/go-jose/v4) [![godoc](https://pkg.go.dev/badge/github.com/go-jose/go-jose/v4/jwt.svg)](https://pkg.go.dev/github.com/go-jose/go-jose/v4/jwt) Examples can be found in the Godoc reference for this package. The [`jose-util`](https://github.com/go-jose/go-jose/tree/main/jose-util) subdirectory also contains a small command-line utility which might be useful as an example as well. ================================================ FILE: vendor/github.com/go-jose/go-jose/v4/SECURITY.md ================================================ # Security Policy This document explains how to contact the Let's Encrypt security team to report security vulnerabilities. ## Supported Versions | Version | Supported | | ------- | ----------| | >= v3 | ✓ | | v2 | ✗ | | v1 | ✗ | ## Reporting a vulnerability Please see [https://letsencrypt.org/contact/#security](https://letsencrypt.org/contact/#security) for the email address to report a vulnerability. Ensure that the subject line for your report contains the word `vulnerability` and is descriptive. Your email should be acknowledged within 24 hours. If you do not receive a response within 24 hours, please follow-up again with another email. ================================================ FILE: vendor/github.com/go-jose/go-jose/v4/asymmetric.go ================================================ /*- * Copyright 2014 Square Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jose import ( "crypto" "crypto/aes" "crypto/ecdsa" "crypto/ed25519" "crypto/rand" "crypto/rsa" "crypto/sha1" "crypto/sha256" "errors" "fmt" "math/big" josecipher "github.com/go-jose/go-jose/v4/cipher" "github.com/go-jose/go-jose/v4/json" ) // A generic RSA-based encrypter/verifier type rsaEncrypterVerifier struct { publicKey *rsa.PublicKey } // A generic RSA-based decrypter/signer type rsaDecrypterSigner struct { privateKey *rsa.PrivateKey } // A generic EC-based encrypter/verifier type ecEncrypterVerifier struct { publicKey *ecdsa.PublicKey } type edEncrypterVerifier struct { publicKey ed25519.PublicKey } // A key generator for ECDH-ES type ecKeyGenerator struct { size int algID string publicKey *ecdsa.PublicKey } // A generic EC-based decrypter/signer type ecDecrypterSigner struct { privateKey *ecdsa.PrivateKey } type edDecrypterSigner struct { privateKey ed25519.PrivateKey } // newRSARecipient creates recipientKeyInfo based on the given key. func newRSARecipient(keyAlg KeyAlgorithm, publicKey *rsa.PublicKey) (recipientKeyInfo, error) { // Verify that key management algorithm is supported by this encrypter switch keyAlg { case RSA1_5, RSA_OAEP, RSA_OAEP_256: default: return recipientKeyInfo{}, ErrUnsupportedAlgorithm } if publicKey == nil { return recipientKeyInfo{}, errors.New("invalid public key") } return recipientKeyInfo{ keyAlg: keyAlg, keyEncrypter: &rsaEncrypterVerifier{ publicKey: publicKey, }, }, nil } // newRSASigner creates a recipientSigInfo based on the given key. func newRSASigner(sigAlg SignatureAlgorithm, privateKey *rsa.PrivateKey) (recipientSigInfo, error) { // Verify that key management algorithm is supported by this encrypter switch sigAlg { case RS256, RS384, RS512, PS256, PS384, PS512: default: return recipientSigInfo{}, ErrUnsupportedAlgorithm } if privateKey == nil { return recipientSigInfo{}, errors.New("invalid private key") } return recipientSigInfo{ sigAlg: sigAlg, publicKey: staticPublicKey(&JSONWebKey{ Key: privateKey.Public(), }), signer: &rsaDecrypterSigner{ privateKey: privateKey, }, }, nil } func newEd25519Signer(sigAlg SignatureAlgorithm, privateKey ed25519.PrivateKey) (recipientSigInfo, error) { if sigAlg != EdDSA { return recipientSigInfo{}, ErrUnsupportedAlgorithm } if privateKey == nil { return recipientSigInfo{}, errors.New("invalid private key") } return recipientSigInfo{ sigAlg: sigAlg, publicKey: staticPublicKey(&JSONWebKey{ Key: privateKey.Public(), }), signer: &edDecrypterSigner{ privateKey: privateKey, }, }, nil } // newECDHRecipient creates recipientKeyInfo based on the given key. func newECDHRecipient(keyAlg KeyAlgorithm, publicKey *ecdsa.PublicKey) (recipientKeyInfo, error) { // Verify that key management algorithm is supported by this encrypter switch keyAlg { case ECDH_ES, ECDH_ES_A128KW, ECDH_ES_A192KW, ECDH_ES_A256KW: default: return recipientKeyInfo{}, ErrUnsupportedAlgorithm } if publicKey == nil || !publicKey.Curve.IsOnCurve(publicKey.X, publicKey.Y) { return recipientKeyInfo{}, errors.New("invalid public key") } return recipientKeyInfo{ keyAlg: keyAlg, keyEncrypter: &ecEncrypterVerifier{ publicKey: publicKey, }, }, nil } // newECDSASigner creates a recipientSigInfo based on the given key. func newECDSASigner(sigAlg SignatureAlgorithm, privateKey *ecdsa.PrivateKey) (recipientSigInfo, error) { // Verify that key management algorithm is supported by this encrypter switch sigAlg { case ES256, ES384, ES512: default: return recipientSigInfo{}, ErrUnsupportedAlgorithm } if privateKey == nil { return recipientSigInfo{}, errors.New("invalid private key") } return recipientSigInfo{ sigAlg: sigAlg, publicKey: staticPublicKey(&JSONWebKey{ Key: privateKey.Public(), }), signer: &ecDecrypterSigner{ privateKey: privateKey, }, }, nil } // Encrypt the given payload and update the object. func (ctx rsaEncrypterVerifier) encryptKey(cek []byte, alg KeyAlgorithm) (recipientInfo, error) { encryptedKey, err := ctx.encrypt(cek, alg) if err != nil { return recipientInfo{}, err } return recipientInfo{ encryptedKey: encryptedKey, header: &rawHeader{}, }, nil } // Encrypt the given payload. Based on the key encryption algorithm, // this will either use RSA-PKCS1v1.5 or RSA-OAEP (with SHA-1 or SHA-256). func (ctx rsaEncrypterVerifier) encrypt(cek []byte, alg KeyAlgorithm) ([]byte, error) { switch alg { case RSA1_5: return rsa.EncryptPKCS1v15(RandReader, ctx.publicKey, cek) case RSA_OAEP: return rsa.EncryptOAEP(sha1.New(), RandReader, ctx.publicKey, cek, []byte{}) case RSA_OAEP_256: return rsa.EncryptOAEP(sha256.New(), RandReader, ctx.publicKey, cek, []byte{}) } return nil, ErrUnsupportedAlgorithm } // Decrypt the given payload and return the content encryption key. func (ctx rsaDecrypterSigner) decryptKey(headers rawHeader, recipient *recipientInfo, generator keyGenerator) ([]byte, error) { return ctx.decrypt(recipient.encryptedKey, headers.getAlgorithm(), generator) } // Decrypt the given payload. Based on the key encryption algorithm, // this will either use RSA-PKCS1v1.5 or RSA-OAEP (with SHA-1 or SHA-256). func (ctx rsaDecrypterSigner) decrypt(jek []byte, alg KeyAlgorithm, generator keyGenerator) ([]byte, error) { // Note: The random reader on decrypt operations is only used for blinding, // so stubbing is meanlingless (hence the direct use of rand.Reader). switch alg { case RSA1_5: defer func() { // DecryptPKCS1v15SessionKey sometimes panics on an invalid payload // because of an index out of bounds error, which we want to ignore. // This has been fixed in Go 1.3.1 (released 2014/08/13), the recover() // only exists for preventing crashes with unpatched versions. // See: https://groups.google.com/forum/#!topic/golang-dev/7ihX6Y6kx9k // See: https://code.google.com/p/go/source/detail?r=58ee390ff31602edb66af41ed10901ec95904d33 _ = recover() }() // Perform some input validation. keyBytes := ctx.privateKey.PublicKey.N.BitLen() / 8 if keyBytes != len(jek) { // Input size is incorrect, the encrypted payload should always match // the size of the public modulus (e.g. using a 2048 bit key will // produce 256 bytes of output). Reject this since it's invalid input. return nil, ErrCryptoFailure } cek, _, err := generator.genKey() if err != nil { return nil, ErrCryptoFailure } // When decrypting an RSA-PKCS1v1.5 payload, we must take precautions to // prevent chosen-ciphertext attacks as described in RFC 3218, "Preventing // the Million Message Attack on Cryptographic Message Syntax". We are // therefore deliberately ignoring errors here. _ = rsa.DecryptPKCS1v15SessionKey(rand.Reader, ctx.privateKey, jek, cek) return cek, nil case RSA_OAEP: // Use rand.Reader for RSA blinding return rsa.DecryptOAEP(sha1.New(), rand.Reader, ctx.privateKey, jek, []byte{}) case RSA_OAEP_256: // Use rand.Reader for RSA blinding return rsa.DecryptOAEP(sha256.New(), rand.Reader, ctx.privateKey, jek, []byte{}) } return nil, ErrUnsupportedAlgorithm } // Sign the given payload func (ctx rsaDecrypterSigner) signPayload(payload []byte, alg SignatureAlgorithm) (Signature, error) { var hash crypto.Hash switch alg { case RS256, PS256: hash = crypto.SHA256 case RS384, PS384: hash = crypto.SHA384 case RS512, PS512: hash = crypto.SHA512 default: return Signature{}, ErrUnsupportedAlgorithm } hasher := hash.New() // According to documentation, Write() on hash never fails _, _ = hasher.Write(payload) hashed := hasher.Sum(nil) var out []byte var err error switch alg { case RS256, RS384, RS512: // TODO(https://github.com/go-jose/go-jose/issues/40): As of go1.20, the // random parameter is legacy and ignored, and it can be nil. // https://cs.opensource.google/go/go/+/refs/tags/go1.20:src/crypto/rsa/pkcs1v15.go;l=263;bpv=0;bpt=1 out, err = rsa.SignPKCS1v15(RandReader, ctx.privateKey, hash, hashed) case PS256, PS384, PS512: out, err = rsa.SignPSS(RandReader, ctx.privateKey, hash, hashed, &rsa.PSSOptions{ SaltLength: rsa.PSSSaltLengthEqualsHash, }) } if err != nil { return Signature{}, err } return Signature{ Signature: out, protected: &rawHeader{}, }, nil } // Verify the given payload func (ctx rsaEncrypterVerifier) verifyPayload(payload []byte, signature []byte, alg SignatureAlgorithm) error { var hash crypto.Hash switch alg { case RS256, PS256: hash = crypto.SHA256 case RS384, PS384: hash = crypto.SHA384 case RS512, PS512: hash = crypto.SHA512 default: return ErrUnsupportedAlgorithm } hasher := hash.New() // According to documentation, Write() on hash never fails _, _ = hasher.Write(payload) hashed := hasher.Sum(nil) switch alg { case RS256, RS384, RS512: return rsa.VerifyPKCS1v15(ctx.publicKey, hash, hashed, signature) case PS256, PS384, PS512: return rsa.VerifyPSS(ctx.publicKey, hash, hashed, signature, nil) } return ErrUnsupportedAlgorithm } // Encrypt the given payload and update the object. func (ctx ecEncrypterVerifier) encryptKey(cek []byte, alg KeyAlgorithm) (recipientInfo, error) { switch alg { case ECDH_ES: // ECDH-ES mode doesn't wrap a key, the shared secret is used directly as the key. return recipientInfo{ header: &rawHeader{}, }, nil case ECDH_ES_A128KW, ECDH_ES_A192KW, ECDH_ES_A256KW: default: return recipientInfo{}, ErrUnsupportedAlgorithm } generator := ecKeyGenerator{ algID: string(alg), publicKey: ctx.publicKey, } switch alg { case ECDH_ES_A128KW: generator.size = 16 case ECDH_ES_A192KW: generator.size = 24 case ECDH_ES_A256KW: generator.size = 32 } kek, header, err := generator.genKey() if err != nil { return recipientInfo{}, err } block, err := aes.NewCipher(kek) if err != nil { return recipientInfo{}, err } jek, err := josecipher.KeyWrap(block, cek) if err != nil { return recipientInfo{}, err } return recipientInfo{ encryptedKey: jek, header: &header, }, nil } // Get key size for EC key generator func (ctx ecKeyGenerator) keySize() int { return ctx.size } // Get a content encryption key for ECDH-ES func (ctx ecKeyGenerator) genKey() ([]byte, rawHeader, error) { priv, err := ecdsa.GenerateKey(ctx.publicKey.Curve, RandReader) if err != nil { return nil, rawHeader{}, err } out := josecipher.DeriveECDHES(ctx.algID, []byte{}, []byte{}, priv, ctx.publicKey, ctx.size) b, err := json.Marshal(&JSONWebKey{ Key: &priv.PublicKey, }) if err != nil { return nil, nil, err } headers := rawHeader{ headerEPK: makeRawMessage(b), } return out, headers, nil } // Decrypt the given payload and return the content encryption key. func (ctx ecDecrypterSigner) decryptKey(headers rawHeader, recipient *recipientInfo, generator keyGenerator) ([]byte, error) { epk, err := headers.getEPK() if err != nil { return nil, errors.New("go-jose/go-jose: invalid epk header") } if epk == nil { return nil, errors.New("go-jose/go-jose: missing epk header") } publicKey, ok := epk.Key.(*ecdsa.PublicKey) if publicKey == nil || !ok { return nil, errors.New("go-jose/go-jose: invalid epk header") } if !ctx.privateKey.Curve.IsOnCurve(publicKey.X, publicKey.Y) { return nil, errors.New("go-jose/go-jose: invalid public key in epk header") } apuData, err := headers.getAPU() if err != nil { return nil, errors.New("go-jose/go-jose: invalid apu header") } apvData, err := headers.getAPV() if err != nil { return nil, errors.New("go-jose/go-jose: invalid apv header") } deriveKey := func(algID string, size int) []byte { return josecipher.DeriveECDHES(algID, apuData.bytes(), apvData.bytes(), ctx.privateKey, publicKey, size) } var keySize int algorithm := headers.getAlgorithm() switch algorithm { case ECDH_ES: // ECDH-ES uses direct key agreement, no key unwrapping necessary. return deriveKey(string(headers.getEncryption()), generator.keySize()), nil case ECDH_ES_A128KW: keySize = 16 case ECDH_ES_A192KW: keySize = 24 case ECDH_ES_A256KW: keySize = 32 default: return nil, ErrUnsupportedAlgorithm } key := deriveKey(string(algorithm), keySize) block, err := aes.NewCipher(key) if err != nil { return nil, err } return josecipher.KeyUnwrap(block, recipient.encryptedKey) } func (ctx edDecrypterSigner) signPayload(payload []byte, alg SignatureAlgorithm) (Signature, error) { if alg != EdDSA { return Signature{}, ErrUnsupportedAlgorithm } sig, err := ctx.privateKey.Sign(RandReader, payload, crypto.Hash(0)) if err != nil { return Signature{}, err } return Signature{ Signature: sig, protected: &rawHeader{}, }, nil } func (ctx edEncrypterVerifier) verifyPayload(payload []byte, signature []byte, alg SignatureAlgorithm) error { if alg != EdDSA { return ErrUnsupportedAlgorithm } ok := ed25519.Verify(ctx.publicKey, payload, signature) if !ok { return errors.New("go-jose/go-jose: ed25519 signature failed to verify") } return nil } // Sign the given payload func (ctx ecDecrypterSigner) signPayload(payload []byte, alg SignatureAlgorithm) (Signature, error) { var expectedBitSize int var hash crypto.Hash switch alg { case ES256: expectedBitSize = 256 hash = crypto.SHA256 case ES384: expectedBitSize = 384 hash = crypto.SHA384 case ES512: expectedBitSize = 521 hash = crypto.SHA512 } curveBits := ctx.privateKey.Curve.Params().BitSize if expectedBitSize != curveBits { return Signature{}, fmt.Errorf("go-jose/go-jose: expected %d bit key, got %d bits instead", expectedBitSize, curveBits) } hasher := hash.New() // According to documentation, Write() on hash never fails _, _ = hasher.Write(payload) hashed := hasher.Sum(nil) r, s, err := ecdsa.Sign(RandReader, ctx.privateKey, hashed) if err != nil { return Signature{}, err } keyBytes := curveBits / 8 if curveBits%8 > 0 { keyBytes++ } // We serialize the outputs (r and s) into big-endian byte arrays and pad // them with zeros on the left to make sure the sizes work out. Both arrays // must be keyBytes long, and the output must be 2*keyBytes long. rBytes := r.Bytes() rBytesPadded := make([]byte, keyBytes) copy(rBytesPadded[keyBytes-len(rBytes):], rBytes) sBytes := s.Bytes() sBytesPadded := make([]byte, keyBytes) copy(sBytesPadded[keyBytes-len(sBytes):], sBytes) out := append(rBytesPadded, sBytesPadded...) return Signature{ Signature: out, protected: &rawHeader{}, }, nil } // Verify the given payload func (ctx ecEncrypterVerifier) verifyPayload(payload []byte, signature []byte, alg SignatureAlgorithm) error { var keySize int var hash crypto.Hash switch alg { case ES256: keySize = 32 hash = crypto.SHA256 case ES384: keySize = 48 hash = crypto.SHA384 case ES512: keySize = 66 hash = crypto.SHA512 default: return ErrUnsupportedAlgorithm } if len(signature) != 2*keySize { return fmt.Errorf("go-jose/go-jose: invalid signature size, have %d bytes, wanted %d", len(signature), 2*keySize) } hasher := hash.New() // According to documentation, Write() on hash never fails _, _ = hasher.Write(payload) hashed := hasher.Sum(nil) r := big.NewInt(0).SetBytes(signature[:keySize]) s := big.NewInt(0).SetBytes(signature[keySize:]) match := ecdsa.Verify(ctx.publicKey, hashed, r, s) if !match { return errors.New("go-jose/go-jose: ecdsa signature failed to verify") } return nil } ================================================ FILE: vendor/github.com/go-jose/go-jose/v4/cipher/cbc_hmac.go ================================================ /*- * Copyright 2014 Square Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package josecipher import ( "bytes" "crypto/cipher" "crypto/hmac" "crypto/sha256" "crypto/sha512" "crypto/subtle" "encoding/binary" "errors" "hash" ) const ( nonceBytes = 16 ) // NewCBCHMAC instantiates a new AEAD based on CBC+HMAC. func NewCBCHMAC(key []byte, newBlockCipher func([]byte) (cipher.Block, error)) (cipher.AEAD, error) { keySize := len(key) / 2 integrityKey := key[:keySize] encryptionKey := key[keySize:] blockCipher, err := newBlockCipher(encryptionKey) if err != nil { return nil, err } var hash func() hash.Hash switch keySize { case 16: hash = sha256.New case 24: hash = sha512.New384 case 32: hash = sha512.New } return &cbcAEAD{ hash: hash, blockCipher: blockCipher, authtagBytes: keySize, integrityKey: integrityKey, }, nil } // An AEAD based on CBC+HMAC type cbcAEAD struct { hash func() hash.Hash authtagBytes int integrityKey []byte blockCipher cipher.Block } func (ctx *cbcAEAD) NonceSize() int { return nonceBytes } func (ctx *cbcAEAD) Overhead() int { // Maximum overhead is block size (for padding) plus auth tag length, where // the length of the auth tag is equivalent to the key size. return ctx.blockCipher.BlockSize() + ctx.authtagBytes } // Seal encrypts and authenticates the plaintext. func (ctx *cbcAEAD) Seal(dst, nonce, plaintext, data []byte) []byte { // Output buffer -- must take care not to mangle plaintext input. ciphertext := make([]byte, uint64(len(plaintext))+uint64(ctx.Overhead()))[:len(plaintext)] copy(ciphertext, plaintext) ciphertext = padBuffer(ciphertext, ctx.blockCipher.BlockSize()) cbc := cipher.NewCBCEncrypter(ctx.blockCipher, nonce) cbc.CryptBlocks(ciphertext, ciphertext) authtag := ctx.computeAuthTag(data, nonce, ciphertext) ret, out := resize(dst, uint64(len(dst))+uint64(len(ciphertext))+uint64(len(authtag))) copy(out, ciphertext) copy(out[len(ciphertext):], authtag) return ret } // Open decrypts and authenticates the ciphertext. func (ctx *cbcAEAD) Open(dst, nonce, ciphertext, data []byte) ([]byte, error) { if len(ciphertext) < ctx.authtagBytes { return nil, errors.New("go-jose/go-jose: invalid ciphertext (too short)") } offset := len(ciphertext) - ctx.authtagBytes expectedTag := ctx.computeAuthTag(data, nonce, ciphertext[:offset]) match := subtle.ConstantTimeCompare(expectedTag, ciphertext[offset:]) if match != 1 { return nil, errors.New("go-jose/go-jose: invalid ciphertext (auth tag mismatch)") } cbc := cipher.NewCBCDecrypter(ctx.blockCipher, nonce) // Make copy of ciphertext buffer, don't want to modify in place buffer := append([]byte{}, ciphertext[:offset]...) if len(buffer)%ctx.blockCipher.BlockSize() > 0 { return nil, errors.New("go-jose/go-jose: invalid ciphertext (invalid length)") } cbc.CryptBlocks(buffer, buffer) // Remove padding plaintext, err := unpadBuffer(buffer, ctx.blockCipher.BlockSize()) if err != nil { return nil, err } ret, out := resize(dst, uint64(len(dst))+uint64(len(plaintext))) copy(out, plaintext) return ret, nil } // Compute an authentication tag func (ctx *cbcAEAD) computeAuthTag(aad, nonce, ciphertext []byte) []byte { buffer := make([]byte, uint64(len(aad))+uint64(len(nonce))+uint64(len(ciphertext))+8) n := 0 n += copy(buffer, aad) n += copy(buffer[n:], nonce) n += copy(buffer[n:], ciphertext) binary.BigEndian.PutUint64(buffer[n:], uint64(len(aad))*8) // According to documentation, Write() on hash.Hash never fails. hmac := hmac.New(ctx.hash, ctx.integrityKey) _, _ = hmac.Write(buffer) return hmac.Sum(nil)[:ctx.authtagBytes] } // resize ensures that the given slice has a capacity of at least n bytes. // If the capacity of the slice is less than n, a new slice is allocated // and the existing data will be copied. func resize(in []byte, n uint64) (head, tail []byte) { if uint64(cap(in)) >= n { head = in[:n] } else { head = make([]byte, n) copy(head, in) } tail = head[len(in):] return } // Apply padding func padBuffer(buffer []byte, blockSize int) []byte { missing := blockSize - (len(buffer) % blockSize) ret, out := resize(buffer, uint64(len(buffer))+uint64(missing)) padding := bytes.Repeat([]byte{byte(missing)}, missing) copy(out, padding) return ret } // Remove padding func unpadBuffer(buffer []byte, blockSize int) ([]byte, error) { if len(buffer)%blockSize != 0 { return nil, errors.New("go-jose/go-jose: invalid padding") } last := buffer[len(buffer)-1] count := int(last) if count == 0 || count > blockSize || count > len(buffer) { return nil, errors.New("go-jose/go-jose: invalid padding") } padding := bytes.Repeat([]byte{last}, count) if !bytes.HasSuffix(buffer, padding) { return nil, errors.New("go-jose/go-jose: invalid padding") } return buffer[:len(buffer)-count], nil } ================================================ FILE: vendor/github.com/go-jose/go-jose/v4/cipher/concat_kdf.go ================================================ /*- * Copyright 2014 Square Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package josecipher import ( "crypto" "encoding/binary" "hash" "io" ) type concatKDF struct { z, info []byte i uint32 cache []byte hasher hash.Hash } // NewConcatKDF builds a KDF reader based on the given inputs. func NewConcatKDF(hash crypto.Hash, z, algID, ptyUInfo, ptyVInfo, supPubInfo, supPrivInfo []byte) io.Reader { buffer := make([]byte, uint64(len(algID))+uint64(len(ptyUInfo))+uint64(len(ptyVInfo))+uint64(len(supPubInfo))+uint64(len(supPrivInfo))) n := 0 n += copy(buffer, algID) n += copy(buffer[n:], ptyUInfo) n += copy(buffer[n:], ptyVInfo) n += copy(buffer[n:], supPubInfo) copy(buffer[n:], supPrivInfo) hasher := hash.New() return &concatKDF{ z: z, info: buffer, hasher: hasher, cache: []byte{}, i: 1, } } func (ctx *concatKDF) Read(out []byte) (int, error) { copied := copy(out, ctx.cache) ctx.cache = ctx.cache[copied:] for copied < len(out) { ctx.hasher.Reset() // Write on a hash.Hash never fails _ = binary.Write(ctx.hasher, binary.BigEndian, ctx.i) _, _ = ctx.hasher.Write(ctx.z) _, _ = ctx.hasher.Write(ctx.info) hash := ctx.hasher.Sum(nil) chunkCopied := copy(out[copied:], hash) copied += chunkCopied ctx.cache = hash[chunkCopied:] ctx.i++ } return copied, nil } ================================================ FILE: vendor/github.com/go-jose/go-jose/v4/cipher/ecdh_es.go ================================================ /*- * Copyright 2014 Square Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package josecipher import ( "bytes" "crypto" "crypto/ecdsa" "crypto/elliptic" "encoding/binary" ) // DeriveECDHES derives a shared encryption key using ECDH/ConcatKDF as described in JWE/JWA. // It is an error to call this function with a private/public key that are not on the same // curve. Callers must ensure that the keys are valid before calling this function. Output // size may be at most 1<<16 bytes (64 KiB). func DeriveECDHES(alg string, apuData, apvData []byte, priv *ecdsa.PrivateKey, pub *ecdsa.PublicKey, size int) []byte { if size > 1<<16 { panic("ECDH-ES output size too large, must be less than or equal to 1<<16") } // algId, partyUInfo, partyVInfo inputs must be prefixed with the length algID := lengthPrefixed([]byte(alg)) ptyUInfo := lengthPrefixed(apuData) ptyVInfo := lengthPrefixed(apvData) // suppPubInfo is the encoded length of the output size in bits supPubInfo := make([]byte, 4) binary.BigEndian.PutUint32(supPubInfo, uint32(size)*8) if !priv.PublicKey.Curve.IsOnCurve(pub.X, pub.Y) { panic("public key not on same curve as private key") } z, _ := priv.Curve.ScalarMult(pub.X, pub.Y, priv.D.Bytes()) zBytes := z.Bytes() // Note that calling z.Bytes() on a big.Int may strip leading zero bytes from // the returned byte array. This can lead to a problem where zBytes will be // shorter than expected which breaks the key derivation. Therefore we must pad // to the full length of the expected coordinate here before calling the KDF. octSize := dSize(priv.Curve) if len(zBytes) != octSize { zBytes = append(bytes.Repeat([]byte{0}, octSize-len(zBytes)), zBytes...) } reader := NewConcatKDF(crypto.SHA256, zBytes, algID, ptyUInfo, ptyVInfo, supPubInfo, []byte{}) key := make([]byte, size) // Read on the KDF will never fail _, _ = reader.Read(key) return key } // dSize returns the size in octets for a coordinate on a elliptic curve. func dSize(curve elliptic.Curve) int { order := curve.Params().P bitLen := order.BitLen() size := bitLen / 8 if bitLen%8 != 0 { size++ } return size } func lengthPrefixed(data []byte) []byte { out := make([]byte, len(data)+4) binary.BigEndian.PutUint32(out, uint32(len(data))) copy(out[4:], data) return out } ================================================ FILE: vendor/github.com/go-jose/go-jose/v4/cipher/key_wrap.go ================================================ /*- * Copyright 2014 Square Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package josecipher import ( "crypto/cipher" "crypto/subtle" "encoding/binary" "errors" ) var defaultIV = []byte{0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6} // KeyWrap implements NIST key wrapping; it wraps a content encryption key (cek) with the given block cipher. func KeyWrap(block cipher.Block, cek []byte) ([]byte, error) { if len(cek)%8 != 0 { return nil, errors.New("go-jose/go-jose: key wrap input must be 8 byte blocks") } n := len(cek) / 8 r := make([][]byte, n) for i := range r { r[i] = make([]byte, 8) copy(r[i], cek[i*8:]) } buffer := make([]byte, 16) tBytes := make([]byte, 8) copy(buffer, defaultIV) for t := 0; t < 6*n; t++ { copy(buffer[8:], r[t%n]) block.Encrypt(buffer, buffer) binary.BigEndian.PutUint64(tBytes, uint64(t+1)) for i := 0; i < 8; i++ { buffer[i] ^= tBytes[i] } copy(r[t%n], buffer[8:]) } out := make([]byte, (n+1)*8) copy(out, buffer[:8]) for i := range r { copy(out[(i+1)*8:], r[i]) } return out, nil } // KeyUnwrap implements NIST key unwrapping; it unwraps a content encryption key (cek) with the given block cipher. func KeyUnwrap(block cipher.Block, ciphertext []byte) ([]byte, error) { if len(ciphertext)%8 != 0 { return nil, errors.New("go-jose/go-jose: key wrap input must be 8 byte blocks") } n := (len(ciphertext) / 8) - 1 r := make([][]byte, n) for i := range r { r[i] = make([]byte, 8) copy(r[i], ciphertext[(i+1)*8:]) } buffer := make([]byte, 16) tBytes := make([]byte, 8) copy(buffer[:8], ciphertext[:8]) for t := 6*n - 1; t >= 0; t-- { binary.BigEndian.PutUint64(tBytes, uint64(t+1)) for i := 0; i < 8; i++ { buffer[i] ^= tBytes[i] } copy(buffer[8:], r[t%n]) block.Decrypt(buffer, buffer) copy(r[t%n], buffer[8:]) } if subtle.ConstantTimeCompare(buffer[:8], defaultIV) == 0 { return nil, errors.New("go-jose/go-jose: failed to unwrap key") } out := make([]byte, n*8) for i := range r { copy(out[i*8:], r[i]) } return out, nil } ================================================ FILE: vendor/github.com/go-jose/go-jose/v4/crypter.go ================================================ /*- * Copyright 2014 Square Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jose import ( "crypto/ecdsa" "crypto/rsa" "errors" "fmt" "github.com/go-jose/go-jose/v4/json" ) // Encrypter represents an encrypter which produces an encrypted JWE object. type Encrypter interface { Encrypt(plaintext []byte) (*JSONWebEncryption, error) EncryptWithAuthData(plaintext []byte, aad []byte) (*JSONWebEncryption, error) Options() EncrypterOptions } // A generic content cipher type contentCipher interface { keySize() int encrypt(cek []byte, aad, plaintext []byte) (*aeadParts, error) decrypt(cek []byte, aad []byte, parts *aeadParts) ([]byte, error) } // A key generator (for generating/getting a CEK) type keyGenerator interface { keySize() int genKey() ([]byte, rawHeader, error) } // A generic key encrypter type keyEncrypter interface { encryptKey(cek []byte, alg KeyAlgorithm) (recipientInfo, error) // Encrypt a key } // A generic key decrypter type keyDecrypter interface { decryptKey(headers rawHeader, recipient *recipientInfo, generator keyGenerator) ([]byte, error) // Decrypt a key } // A generic encrypter based on the given key encrypter and content cipher. type genericEncrypter struct { contentAlg ContentEncryption compressionAlg CompressionAlgorithm cipher contentCipher recipients []recipientKeyInfo keyGenerator keyGenerator extraHeaders map[HeaderKey]interface{} } type recipientKeyInfo struct { keyID string keyAlg KeyAlgorithm keyEncrypter keyEncrypter } // EncrypterOptions represents options that can be set on new encrypters. type EncrypterOptions struct { Compression CompressionAlgorithm // Optional map of name/value pairs to be inserted into the protected // header of a JWS object. Some specifications which make use of // JWS require additional values here. // // Values will be serialized by [json.Marshal] and must be valid inputs to // that function. // // [json.Marshal]: https://pkg.go.dev/encoding/json#Marshal ExtraHeaders map[HeaderKey]interface{} } // WithHeader adds an arbitrary value to the ExtraHeaders map, initializing it // if necessary, and returns the updated EncrypterOptions. // // The v parameter will be serialized by [json.Marshal] and must be a valid // input to that function. // // [json.Marshal]: https://pkg.go.dev/encoding/json#Marshal func (eo *EncrypterOptions) WithHeader(k HeaderKey, v interface{}) *EncrypterOptions { if eo.ExtraHeaders == nil { eo.ExtraHeaders = map[HeaderKey]interface{}{} } eo.ExtraHeaders[k] = v return eo } // WithContentType adds a content type ("cty") header and returns the updated // EncrypterOptions. func (eo *EncrypterOptions) WithContentType(contentType ContentType) *EncrypterOptions { return eo.WithHeader(HeaderContentType, contentType) } // WithType adds a type ("typ") header and returns the updated EncrypterOptions. func (eo *EncrypterOptions) WithType(typ ContentType) *EncrypterOptions { return eo.WithHeader(HeaderType, typ) } // Recipient represents an algorithm/key to encrypt messages to. // // PBES2Count and PBES2Salt correspond with the "p2c" and "p2s" headers used // on the password-based encryption algorithms PBES2-HS256+A128KW, // PBES2-HS384+A192KW, and PBES2-HS512+A256KW. If they are not provided a safe // default of 100000 will be used for the count and a 128-bit random salt will // be generated. type Recipient struct { Algorithm KeyAlgorithm // Key must have one of these types: // - ed25519.PublicKey // - *ecdsa.PublicKey // - *rsa.PublicKey // - *JSONWebKey // - JSONWebKey // - []byte (a symmetric key) // - Any type that satisfies the OpaqueKeyEncrypter interface // // The type of Key must match the value of Algorithm. Key interface{} KeyID string PBES2Count int PBES2Salt []byte } // NewEncrypter creates an appropriate encrypter based on the key type func NewEncrypter(enc ContentEncryption, rcpt Recipient, opts *EncrypterOptions) (Encrypter, error) { encrypter := &genericEncrypter{ contentAlg: enc, recipients: []recipientKeyInfo{}, cipher: getContentCipher(enc), } if opts != nil { encrypter.compressionAlg = opts.Compression encrypter.extraHeaders = opts.ExtraHeaders } if encrypter.cipher == nil { return nil, ErrUnsupportedAlgorithm } var keyID string var rawKey interface{} switch encryptionKey := rcpt.Key.(type) { case JSONWebKey: keyID, rawKey = encryptionKey.KeyID, encryptionKey.Key case *JSONWebKey: keyID, rawKey = encryptionKey.KeyID, encryptionKey.Key case OpaqueKeyEncrypter: keyID, rawKey = encryptionKey.KeyID(), encryptionKey default: rawKey = encryptionKey } switch rcpt.Algorithm { case DIRECT: // Direct encryption mode must be treated differently keyBytes, ok := rawKey.([]byte) if !ok { return nil, ErrUnsupportedKeyType } if encrypter.cipher.keySize() != len(keyBytes) { return nil, ErrInvalidKeySize } encrypter.keyGenerator = staticKeyGenerator{ key: keyBytes, } recipientInfo, _ := newSymmetricRecipient(rcpt.Algorithm, keyBytes) recipientInfo.keyID = keyID if rcpt.KeyID != "" { recipientInfo.keyID = rcpt.KeyID } encrypter.recipients = []recipientKeyInfo{recipientInfo} return encrypter, nil case ECDH_ES: // ECDH-ES (w/o key wrapping) is similar to DIRECT mode keyDSA, ok := rawKey.(*ecdsa.PublicKey) if !ok { return nil, ErrUnsupportedKeyType } encrypter.keyGenerator = ecKeyGenerator{ size: encrypter.cipher.keySize(), algID: string(enc), publicKey: keyDSA, } recipientInfo, _ := newECDHRecipient(rcpt.Algorithm, keyDSA) recipientInfo.keyID = keyID if rcpt.KeyID != "" { recipientInfo.keyID = rcpt.KeyID } encrypter.recipients = []recipientKeyInfo{recipientInfo} return encrypter, nil default: // Can just add a standard recipient encrypter.keyGenerator = randomKeyGenerator{ size: encrypter.cipher.keySize(), } err := encrypter.addRecipient(rcpt) return encrypter, err } } // NewMultiEncrypter creates a multi-encrypter based on the given parameters func NewMultiEncrypter(enc ContentEncryption, rcpts []Recipient, opts *EncrypterOptions) (Encrypter, error) { cipher := getContentCipher(enc) if cipher == nil { return nil, ErrUnsupportedAlgorithm } if len(rcpts) == 0 { return nil, fmt.Errorf("go-jose/go-jose: recipients is nil or empty") } encrypter := &genericEncrypter{ contentAlg: enc, recipients: []recipientKeyInfo{}, cipher: cipher, keyGenerator: randomKeyGenerator{ size: cipher.keySize(), }, } if opts != nil { encrypter.compressionAlg = opts.Compression encrypter.extraHeaders = opts.ExtraHeaders } for _, recipient := range rcpts { err := encrypter.addRecipient(recipient) if err != nil { return nil, err } } return encrypter, nil } func (ctx *genericEncrypter) addRecipient(recipient Recipient) (err error) { var recipientInfo recipientKeyInfo switch recipient.Algorithm { case DIRECT, ECDH_ES: return fmt.Errorf("go-jose/go-jose: key algorithm '%s' not supported in multi-recipient mode", recipient.Algorithm) } recipientInfo, err = makeJWERecipient(recipient.Algorithm, recipient.Key) if recipient.KeyID != "" { recipientInfo.keyID = recipient.KeyID } switch recipient.Algorithm { case PBES2_HS256_A128KW, PBES2_HS384_A192KW, PBES2_HS512_A256KW: if sr, ok := recipientInfo.keyEncrypter.(*symmetricKeyCipher); ok { sr.p2c = recipient.PBES2Count sr.p2s = recipient.PBES2Salt } } if err == nil { ctx.recipients = append(ctx.recipients, recipientInfo) } return err } func makeJWERecipient(alg KeyAlgorithm, encryptionKey interface{}) (recipientKeyInfo, error) { switch encryptionKey := encryptionKey.(type) { case *rsa.PublicKey: return newRSARecipient(alg, encryptionKey) case *ecdsa.PublicKey: return newECDHRecipient(alg, encryptionKey) case []byte: return newSymmetricRecipient(alg, encryptionKey) case string: return newSymmetricRecipient(alg, []byte(encryptionKey)) case JSONWebKey: recipient, err := makeJWERecipient(alg, encryptionKey.Key) recipient.keyID = encryptionKey.KeyID return recipient, err case *JSONWebKey: recipient, err := makeJWERecipient(alg, encryptionKey.Key) recipient.keyID = encryptionKey.KeyID return recipient, err case OpaqueKeyEncrypter: return newOpaqueKeyEncrypter(alg, encryptionKey) } return recipientKeyInfo{}, ErrUnsupportedKeyType } // newDecrypter creates an appropriate decrypter based on the key type func newDecrypter(decryptionKey interface{}) (keyDecrypter, error) { switch decryptionKey := decryptionKey.(type) { case *rsa.PrivateKey: return &rsaDecrypterSigner{ privateKey: decryptionKey, }, nil case *ecdsa.PrivateKey: return &ecDecrypterSigner{ privateKey: decryptionKey, }, nil case []byte: return &symmetricKeyCipher{ key: decryptionKey, }, nil case string: return &symmetricKeyCipher{ key: []byte(decryptionKey), }, nil case JSONWebKey: return newDecrypter(decryptionKey.Key) case *JSONWebKey: return newDecrypter(decryptionKey.Key) case OpaqueKeyDecrypter: return &opaqueKeyDecrypter{decrypter: decryptionKey}, nil default: return nil, ErrUnsupportedKeyType } } // Implementation of encrypt method producing a JWE object. func (ctx *genericEncrypter) Encrypt(plaintext []byte) (*JSONWebEncryption, error) { return ctx.EncryptWithAuthData(plaintext, nil) } // Implementation of encrypt method producing a JWE object. func (ctx *genericEncrypter) EncryptWithAuthData(plaintext, aad []byte) (*JSONWebEncryption, error) { obj := &JSONWebEncryption{} obj.aad = aad obj.protected = &rawHeader{} err := obj.protected.set(headerEncryption, ctx.contentAlg) if err != nil { return nil, err } obj.recipients = make([]recipientInfo, len(ctx.recipients)) if len(ctx.recipients) == 0 { return nil, fmt.Errorf("go-jose/go-jose: no recipients to encrypt to") } cek, headers, err := ctx.keyGenerator.genKey() if err != nil { return nil, err } obj.protected.merge(&headers) for i, info := range ctx.recipients { recipient, err := info.keyEncrypter.encryptKey(cek, info.keyAlg) if err != nil { return nil, err } err = recipient.header.set(headerAlgorithm, info.keyAlg) if err != nil { return nil, err } if info.keyID != "" { err = recipient.header.set(headerKeyID, info.keyID) if err != nil { return nil, err } } obj.recipients[i] = recipient } if len(ctx.recipients) == 1 { // Move per-recipient headers into main protected header if there's // only a single recipient. obj.protected.merge(obj.recipients[0].header) obj.recipients[0].header = nil } if ctx.compressionAlg != NONE { plaintext, err = compress(ctx.compressionAlg, plaintext) if err != nil { return nil, err } err = obj.protected.set(headerCompression, ctx.compressionAlg) if err != nil { return nil, err } } for k, v := range ctx.extraHeaders { b, err := json.Marshal(v) if err != nil { return nil, err } (*obj.protected)[k] = makeRawMessage(b) } authData := obj.computeAuthData() parts, err := ctx.cipher.encrypt(cek, authData, plaintext) if err != nil { return nil, err } obj.iv = parts.iv obj.ciphertext = parts.ciphertext obj.tag = parts.tag return obj, nil } func (ctx *genericEncrypter) Options() EncrypterOptions { return EncrypterOptions{ Compression: ctx.compressionAlg, ExtraHeaders: ctx.extraHeaders, } } // Decrypt and validate the object and return the plaintext. This // function does not support multi-recipient. If you desire multi-recipient // decryption use DecryptMulti instead. // // The decryptionKey argument must contain a private or symmetric key // and must have one of these types: // - *ecdsa.PrivateKey // - *rsa.PrivateKey // - *JSONWebKey // - JSONWebKey // - *JSONWebKeySet // - JSONWebKeySet // - []byte (a symmetric key) // - string (a symmetric key) // - Any type that satisfies the OpaqueKeyDecrypter interface. // // Note that ed25519 is only available for signatures, not encryption, so is // not an option here. // // Automatically decompresses plaintext, but returns an error if the decompressed // data would be >250kB or >10x the size of the compressed data, whichever is larger. func (obj JSONWebEncryption) Decrypt(decryptionKey interface{}) ([]byte, error) { headers := obj.mergedHeaders(nil) if len(obj.recipients) > 1 { return nil, errors.New("go-jose/go-jose: too many recipients in payload; expecting only one") } err := headers.checkNoCritical() if err != nil { return nil, err } key, err := tryJWKS(decryptionKey, obj.Header) if err != nil { return nil, err } decrypter, err := newDecrypter(key) if err != nil { return nil, err } cipher := getContentCipher(headers.getEncryption()) if cipher == nil { return nil, fmt.Errorf("go-jose/go-jose: unsupported enc value '%s'", string(headers.getEncryption())) } generator := randomKeyGenerator{ size: cipher.keySize(), } parts := &aeadParts{ iv: obj.iv, ciphertext: obj.ciphertext, tag: obj.tag, } authData := obj.computeAuthData() var plaintext []byte recipient := obj.recipients[0] recipientHeaders := obj.mergedHeaders(&recipient) cek, err := decrypter.decryptKey(recipientHeaders, &recipient, generator) if err == nil { // Found a valid CEK -- let's try to decrypt. plaintext, err = cipher.decrypt(cek, authData, parts) } if plaintext == nil { return nil, ErrCryptoFailure } // The "zip" header parameter may only be present in the protected header. if comp := obj.protected.getCompression(); comp != "" { plaintext, err = decompress(comp, plaintext) if err != nil { return nil, fmt.Errorf("go-jose/go-jose: failed to decompress plaintext: %v", err) } } return plaintext, nil } // DecryptMulti decrypts and validates the object and returns the plaintexts, // with support for multiple recipients. It returns the index of the recipient // for which the decryption was successful, the merged headers for that recipient, // and the plaintext. // // The decryptionKey argument must have one of the types allowed for the // decryptionKey argument of Decrypt(). // // Automatically decompresses plaintext, but returns an error if the decompressed // data would be >250kB or >3x the size of the compressed data, whichever is larger. func (obj JSONWebEncryption) DecryptMulti(decryptionKey interface{}) (int, Header, []byte, error) { globalHeaders := obj.mergedHeaders(nil) err := globalHeaders.checkNoCritical() if err != nil { return -1, Header{}, nil, err } key, err := tryJWKS(decryptionKey, obj.Header) if err != nil { return -1, Header{}, nil, err } decrypter, err := newDecrypter(key) if err != nil { return -1, Header{}, nil, err } encryption := globalHeaders.getEncryption() cipher := getContentCipher(encryption) if cipher == nil { return -1, Header{}, nil, fmt.Errorf("go-jose/go-jose: unsupported enc value '%s'", string(encryption)) } generator := randomKeyGenerator{ size: cipher.keySize(), } parts := &aeadParts{ iv: obj.iv, ciphertext: obj.ciphertext, tag: obj.tag, } authData := obj.computeAuthData() index := -1 var plaintext []byte var headers rawHeader for i, recipient := range obj.recipients { recipientHeaders := obj.mergedHeaders(&recipient) cek, err := decrypter.decryptKey(recipientHeaders, &recipient, generator) if err == nil { // Found a valid CEK -- let's try to decrypt. plaintext, err = cipher.decrypt(cek, authData, parts) if err == nil { index = i headers = recipientHeaders break } } } if plaintext == nil { return -1, Header{}, nil, ErrCryptoFailure } // The "zip" header parameter may only be present in the protected header. if comp := obj.protected.getCompression(); comp != "" { plaintext, err = decompress(comp, plaintext) if err != nil { return -1, Header{}, nil, fmt.Errorf("go-jose/go-jose: failed to decompress plaintext: %v", err) } } sanitized, err := headers.sanitized() if err != nil { return -1, Header{}, nil, fmt.Errorf("go-jose/go-jose: failed to sanitize header: %v", err) } return index, sanitized, plaintext, err } ================================================ FILE: vendor/github.com/go-jose/go-jose/v4/doc.go ================================================ /*- * Copyright 2014 Square Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* Package jose aims to provide an implementation of the Javascript Object Signing and Encryption set of standards. It implements encryption and signing based on the JSON Web Encryption and JSON Web Signature standards, with optional JSON Web Token support available in a sub-package. The library supports both the compact and JWS/JWE JSON Serialization formats, and has optional support for multiple recipients. */ package jose ================================================ FILE: vendor/github.com/go-jose/go-jose/v4/encoding.go ================================================ /*- * Copyright 2014 Square Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jose import ( "bytes" "compress/flate" "encoding/base64" "encoding/binary" "fmt" "io" "math/big" "strings" "unicode" "github.com/go-jose/go-jose/v4/json" ) // Helper function to serialize known-good objects. // Precondition: value is not a nil pointer. func mustSerializeJSON(value interface{}) []byte { out, err := json.Marshal(value) if err != nil { panic(err) } // We never want to serialize the top-level value "null," since it's not a // valid JOSE message. But if a caller passes in a nil pointer to this method, // MarshalJSON will happily serialize it as the top-level value "null". If // that value is then embedded in another operation, for instance by being // base64-encoded and fed as input to a signing algorithm // (https://github.com/go-jose/go-jose/issues/22), the result will be // incorrect. Because this method is intended for known-good objects, and a nil // pointer is not a known-good object, we are free to panic in this case. // Note: It's not possible to directly check whether the data pointed at by an // interface is a nil pointer, so we do this hacky workaround. // https://groups.google.com/forum/#!topic/golang-nuts/wnH302gBa4I if string(out) == "null" { panic("Tried to serialize a nil pointer.") } return out } // Strip all newlines and whitespace func stripWhitespace(data string) string { buf := strings.Builder{} buf.Grow(len(data)) for _, r := range data { if !unicode.IsSpace(r) { buf.WriteRune(r) } } return buf.String() } // Perform compression based on algorithm func compress(algorithm CompressionAlgorithm, input []byte) ([]byte, error) { switch algorithm { case DEFLATE: return deflate(input) default: return nil, ErrUnsupportedAlgorithm } } // Perform decompression based on algorithm func decompress(algorithm CompressionAlgorithm, input []byte) ([]byte, error) { switch algorithm { case DEFLATE: return inflate(input) default: return nil, ErrUnsupportedAlgorithm } } // deflate compresses the input. func deflate(input []byte) ([]byte, error) { output := new(bytes.Buffer) // Writing to byte buffer, err is always nil writer, _ := flate.NewWriter(output, 1) _, _ = io.Copy(writer, bytes.NewBuffer(input)) err := writer.Close() return output.Bytes(), err } // inflate decompresses the input. // // Errors if the decompressed data would be >250kB or >10x the size of the // compressed data, whichever is larger. func inflate(input []byte) ([]byte, error) { output := new(bytes.Buffer) reader := flate.NewReader(bytes.NewBuffer(input)) maxCompressedSize := max(250_000, 10*int64(len(input))) limit := maxCompressedSize + 1 n, err := io.CopyN(output, reader, limit) if err != nil && err != io.EOF { return nil, err } if n == limit { return nil, fmt.Errorf("uncompressed data would be too large (>%d bytes)", maxCompressedSize) } err = reader.Close() return output.Bytes(), err } // byteBuffer represents a slice of bytes that can be serialized to url-safe base64. type byteBuffer struct { data []byte } func newBuffer(data []byte) *byteBuffer { if data == nil { return nil } return &byteBuffer{ data: data, } } func newFixedSizeBuffer(data []byte, length int) *byteBuffer { if len(data) > length { panic("go-jose/go-jose: invalid call to newFixedSizeBuffer (len(data) > length)") } pad := make([]byte, length-len(data)) return newBuffer(append(pad, data...)) } func newBufferFromInt(num uint64) *byteBuffer { data := make([]byte, 8) binary.BigEndian.PutUint64(data, num) return newBuffer(bytes.TrimLeft(data, "\x00")) } func (b *byteBuffer) MarshalJSON() ([]byte, error) { return json.Marshal(b.base64()) } func (b *byteBuffer) UnmarshalJSON(data []byte) error { var encoded string err := json.Unmarshal(data, &encoded) if err != nil { return err } if encoded == "" { return nil } decoded, err := base64.RawURLEncoding.DecodeString(encoded) if err != nil { return err } *b = *newBuffer(decoded) return nil } func (b *byteBuffer) base64() string { return base64.RawURLEncoding.EncodeToString(b.data) } func (b *byteBuffer) bytes() []byte { // Handling nil here allows us to transparently handle nil slices when serializing. if b == nil { return nil } return b.data } func (b byteBuffer) bigInt() *big.Int { return new(big.Int).SetBytes(b.data) } func (b byteBuffer) toInt() int { return int(b.bigInt().Int64()) } func base64EncodeLen(sl []byte) int { return base64.RawURLEncoding.EncodedLen(len(sl)) } func base64JoinWithDots(inputs ...[]byte) string { if len(inputs) == 0 { return "" } // Count of dots. totalCount := len(inputs) - 1 for _, input := range inputs { totalCount += base64EncodeLen(input) } out := make([]byte, totalCount) startEncode := 0 for i, input := range inputs { base64.RawURLEncoding.Encode(out[startEncode:], input) if i == len(inputs)-1 { continue } startEncode += base64EncodeLen(input) out[startEncode] = '.' startEncode++ } return string(out) } ================================================ FILE: vendor/github.com/go-jose/go-jose/v4/json/LICENSE ================================================ Copyright (c) 2012 The Go Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: vendor/github.com/go-jose/go-jose/v4/json/README.md ================================================ # Safe JSON This repository contains a fork of the `encoding/json` package from Go 1.6. The following changes were made: * Object deserialization uses case-sensitive member name matching instead of [case-insensitive matching](https://www.ietf.org/mail-archive/web/json/current/msg03763.html). This is to avoid differences in the interpretation of JOSE messages between go-jose and libraries written in other languages. * When deserializing a JSON object, we check for duplicate keys and reject the input whenever we detect a duplicate. Rather than trying to work with malformed data, we prefer to reject it right away. ================================================ FILE: vendor/github.com/go-jose/go-jose/v4/json/decode.go ================================================ // Copyright 2010 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Represents JSON data structure using native Go types: booleans, floats, // strings, arrays, and maps. package json import ( "bytes" "encoding" "encoding/base64" "errors" "fmt" "math" "reflect" "runtime" "strconv" "unicode" "unicode/utf16" "unicode/utf8" ) // Unmarshal parses the JSON-encoded data and stores the result // in the value pointed to by v. // // Unmarshal uses the inverse of the encodings that // Marshal uses, allocating maps, slices, and pointers as necessary, // with the following additional rules: // // To unmarshal JSON into a pointer, Unmarshal first handles the case of // the JSON being the JSON literal null. In that case, Unmarshal sets // the pointer to nil. Otherwise, Unmarshal unmarshals the JSON into // the value pointed at by the pointer. If the pointer is nil, Unmarshal // allocates a new value for it to point to. // // To unmarshal JSON into a struct, Unmarshal matches incoming object // keys to the keys used by Marshal (either the struct field name or its tag), // preferring an exact match but also accepting a case-insensitive match. // Unmarshal will only set exported fields of the struct. // // To unmarshal JSON into an interface value, // Unmarshal stores one of these in the interface value: // // bool, for JSON booleans // float64, for JSON numbers // string, for JSON strings // []interface{}, for JSON arrays // map[string]interface{}, for JSON objects // nil for JSON null // // To unmarshal a JSON array into a slice, Unmarshal resets the slice length // to zero and then appends each element to the slice. // As a special case, to unmarshal an empty JSON array into a slice, // Unmarshal replaces the slice with a new empty slice. // // To unmarshal a JSON array into a Go array, Unmarshal decodes // JSON array elements into corresponding Go array elements. // If the Go array is smaller than the JSON array, // the additional JSON array elements are discarded. // If the JSON array is smaller than the Go array, // the additional Go array elements are set to zero values. // // To unmarshal a JSON object into a string-keyed map, Unmarshal first // establishes a map to use, If the map is nil, Unmarshal allocates a new map. // Otherwise Unmarshal reuses the existing map, keeping existing entries. // Unmarshal then stores key-value pairs from the JSON object into the map. // // If a JSON value is not appropriate for a given target type, // or if a JSON number overflows the target type, Unmarshal // skips that field and completes the unmarshaling as best it can. // If no more serious errors are encountered, Unmarshal returns // an UnmarshalTypeError describing the earliest such error. // // The JSON null value unmarshals into an interface, map, pointer, or slice // by setting that Go value to nil. Because null is often used in JSON to mean // “not present,” unmarshaling a JSON null into any other Go type has no effect // on the value and produces no error. // // When unmarshaling quoted strings, invalid UTF-8 or // invalid UTF-16 surrogate pairs are not treated as an error. // Instead, they are replaced by the Unicode replacement // character U+FFFD. func Unmarshal(data []byte, v interface{}) error { // Check for well-formedness. // Avoids filling out half a data structure // before discovering a JSON syntax error. var d decodeState err := checkValid(data, &d.scan) if err != nil { return err } d.init(data) return d.unmarshal(v) } // Unmarshaler is the interface implemented by objects // that can unmarshal a JSON description of themselves. // The input can be assumed to be a valid encoding of // a JSON value. UnmarshalJSON must copy the JSON data // if it wishes to retain the data after returning. type Unmarshaler interface { UnmarshalJSON([]byte) error } // An UnmarshalTypeError describes a JSON value that was // not appropriate for a value of a specific Go type. type UnmarshalTypeError struct { Value string // description of JSON value - "bool", "array", "number -5" Type reflect.Type // type of Go value it could not be assigned to Offset int64 // error occurred after reading Offset bytes } func (e *UnmarshalTypeError) Error() string { return "json: cannot unmarshal " + e.Value + " into Go value of type " + e.Type.String() } // An UnmarshalFieldError describes a JSON object key that // led to an unexported (and therefore unwritable) struct field. // (No longer used; kept for compatibility.) type UnmarshalFieldError struct { Key string Type reflect.Type Field reflect.StructField } func (e *UnmarshalFieldError) Error() string { return "json: cannot unmarshal object key " + strconv.Quote(e.Key) + " into unexported field " + e.Field.Name + " of type " + e.Type.String() } // An InvalidUnmarshalError describes an invalid argument passed to Unmarshal. // (The argument to Unmarshal must be a non-nil pointer.) type InvalidUnmarshalError struct { Type reflect.Type } func (e *InvalidUnmarshalError) Error() string { if e.Type == nil { return "json: Unmarshal(nil)" } if e.Type.Kind() != reflect.Ptr { return "json: Unmarshal(non-pointer " + e.Type.String() + ")" } return "json: Unmarshal(nil " + e.Type.String() + ")" } func (d *decodeState) unmarshal(v interface{}) (err error) { defer func() { if r := recover(); r != nil { if _, ok := r.(runtime.Error); ok { panic(r) } err = r.(error) } }() rv := reflect.ValueOf(v) if rv.Kind() != reflect.Ptr || rv.IsNil() { return &InvalidUnmarshalError{reflect.TypeOf(v)} } d.scan.reset() // We decode rv not rv.Elem because the Unmarshaler interface // test must be applied at the top level of the value. d.value(rv) return d.savedError } // A Number represents a JSON number literal. type Number string // String returns the literal text of the number. func (n Number) String() string { return string(n) } // Float64 returns the number as a float64. func (n Number) Float64() (float64, error) { return strconv.ParseFloat(string(n), 64) } // Int64 returns the number as an int64. func (n Number) Int64() (int64, error) { return strconv.ParseInt(string(n), 10, 64) } // isValidNumber reports whether s is a valid JSON number literal. func isValidNumber(s string) bool { // This function implements the JSON numbers grammar. // See https://tools.ietf.org/html/rfc7159#section-6 // and http://json.org/number.gif if s == "" { return false } // Optional - if s[0] == '-' { s = s[1:] if s == "" { return false } } // Digits switch { default: return false case s[0] == '0': s = s[1:] case '1' <= s[0] && s[0] <= '9': s = s[1:] for len(s) > 0 && '0' <= s[0] && s[0] <= '9' { s = s[1:] } } // . followed by 1 or more digits. if len(s) >= 2 && s[0] == '.' && '0' <= s[1] && s[1] <= '9' { s = s[2:] for len(s) > 0 && '0' <= s[0] && s[0] <= '9' { s = s[1:] } } // e or E followed by an optional - or + and // 1 or more digits. if len(s) >= 2 && (s[0] == 'e' || s[0] == 'E') { s = s[1:] if s[0] == '+' || s[0] == '-' { s = s[1:] if s == "" { return false } } for len(s) > 0 && '0' <= s[0] && s[0] <= '9' { s = s[1:] } } // Make sure we are at the end. return s == "" } type NumberUnmarshalType int const ( // unmarshal a JSON number into an interface{} as a float64 UnmarshalFloat NumberUnmarshalType = iota // unmarshal a JSON number into an interface{} as a `json.Number` UnmarshalJSONNumber // unmarshal a JSON number into an interface{} as a int64 // if value is an integer otherwise float64 UnmarshalIntOrFloat ) // decodeState represents the state while decoding a JSON value. type decodeState struct { data []byte off int // read offset in data scan scanner nextscan scanner // for calls to nextValue savedError error numberType NumberUnmarshalType } // errPhase is used for errors that should not happen unless // there is a bug in the JSON decoder or something is editing // the data slice while the decoder executes. var errPhase = errors.New("JSON decoder out of sync - data changing underfoot?") func (d *decodeState) init(data []byte) *decodeState { d.data = data d.off = 0 d.savedError = nil return d } // error aborts the decoding by panicking with err. func (d *decodeState) error(err error) { panic(err) } // saveError saves the first err it is called with, // for reporting at the end of the unmarshal. func (d *decodeState) saveError(err error) { if d.savedError == nil { d.savedError = err } } // next cuts off and returns the next full JSON value in d.data[d.off:]. // The next value is known to be an object or array, not a literal. func (d *decodeState) next() []byte { c := d.data[d.off] item, rest, err := nextValue(d.data[d.off:], &d.nextscan) if err != nil { d.error(err) } d.off = len(d.data) - len(rest) // Our scanner has seen the opening brace/bracket // and thinks we're still in the middle of the object. // invent a closing brace/bracket to get it out. if c == '{' { d.scan.step(&d.scan, '}') } else { d.scan.step(&d.scan, ']') } return item } // scanWhile processes bytes in d.data[d.off:] until it // receives a scan code not equal to op. // It updates d.off and returns the new scan code. func (d *decodeState) scanWhile(op int) int { var newOp int for { if d.off >= len(d.data) { newOp = d.scan.eof() d.off = len(d.data) + 1 // mark processed EOF with len+1 } else { c := d.data[d.off] d.off++ newOp = d.scan.step(&d.scan, c) } if newOp != op { break } } return newOp } // value decodes a JSON value from d.data[d.off:] into the value. // it updates d.off to point past the decoded value. func (d *decodeState) value(v reflect.Value) { if !v.IsValid() { _, rest, err := nextValue(d.data[d.off:], &d.nextscan) if err != nil { d.error(err) } d.off = len(d.data) - len(rest) // d.scan thinks we're still at the beginning of the item. // Feed in an empty string - the shortest, simplest value - // so that it knows we got to the end of the value. if d.scan.redo { // rewind. d.scan.redo = false d.scan.step = stateBeginValue } d.scan.step(&d.scan, '"') d.scan.step(&d.scan, '"') n := len(d.scan.parseState) if n > 0 && d.scan.parseState[n-1] == parseObjectKey { // d.scan thinks we just read an object key; finish the object d.scan.step(&d.scan, ':') d.scan.step(&d.scan, '"') d.scan.step(&d.scan, '"') d.scan.step(&d.scan, '}') } return } switch op := d.scanWhile(scanSkipSpace); op { default: d.error(errPhase) case scanBeginArray: d.array(v) case scanBeginObject: d.object(v) case scanBeginLiteral: d.literal(v) } } type unquotedValue struct{} // valueQuoted is like value but decodes a // quoted string literal or literal null into an interface value. // If it finds anything other than a quoted string literal or null, // valueQuoted returns unquotedValue{}. func (d *decodeState) valueQuoted() interface{} { switch op := d.scanWhile(scanSkipSpace); op { default: d.error(errPhase) case scanBeginArray: d.array(reflect.Value{}) case scanBeginObject: d.object(reflect.Value{}) case scanBeginLiteral: switch v := d.literalInterface().(type) { case nil, string: return v } } return unquotedValue{} } // indirect walks down v allocating pointers as needed, // until it gets to a non-pointer. // if it encounters an Unmarshaler, indirect stops and returns that. // if decodingNull is true, indirect stops at the last pointer so it can be set to nil. func (d *decodeState) indirect(v reflect.Value, decodingNull bool) (Unmarshaler, encoding.TextUnmarshaler, reflect.Value) { // If v is a named type and is addressable, // start with its address, so that if the type has pointer methods, // we find them. if v.Kind() != reflect.Ptr && v.Type().Name() != "" && v.CanAddr() { v = v.Addr() } for { // Load value from interface, but only if the result will be // usefully addressable. if v.Kind() == reflect.Interface && !v.IsNil() { e := v.Elem() if e.Kind() == reflect.Ptr && !e.IsNil() && (!decodingNull || e.Elem().Kind() == reflect.Ptr) { v = e continue } } if v.Kind() != reflect.Ptr { break } if v.Elem().Kind() != reflect.Ptr && decodingNull && v.CanSet() { break } if v.IsNil() { v.Set(reflect.New(v.Type().Elem())) } if v.Type().NumMethod() > 0 { if u, ok := v.Interface().(Unmarshaler); ok { return u, nil, reflect.Value{} } if u, ok := v.Interface().(encoding.TextUnmarshaler); ok { return nil, u, reflect.Value{} } } v = v.Elem() } return nil, nil, v } // array consumes an array from d.data[d.off-1:], decoding into the value v. // the first byte of the array ('[') has been read already. func (d *decodeState) array(v reflect.Value) { // Check for unmarshaler. u, ut, pv := d.indirect(v, false) if u != nil { d.off-- err := u.UnmarshalJSON(d.next()) if err != nil { d.error(err) } return } if ut != nil { d.saveError(&UnmarshalTypeError{"array", v.Type(), int64(d.off)}) d.off-- d.next() return } v = pv // Check type of target. switch v.Kind() { case reflect.Interface: if v.NumMethod() == 0 { // Decoding into nil interface? Switch to non-reflect code. v.Set(reflect.ValueOf(d.arrayInterface())) return } // Otherwise it's invalid. fallthrough default: d.saveError(&UnmarshalTypeError{"array", v.Type(), int64(d.off)}) d.off-- d.next() return case reflect.Array: case reflect.Slice: break } i := 0 for { // Look ahead for ] - can only happen on first iteration. op := d.scanWhile(scanSkipSpace) if op == scanEndArray { break } // Back up so d.value can have the byte we just read. d.off-- d.scan.undo(op) // Get element of array, growing if necessary. if v.Kind() == reflect.Slice { // Grow slice if necessary if i >= v.Cap() { newcap := v.Cap() + v.Cap()/2 if newcap < 4 { newcap = 4 } newv := reflect.MakeSlice(v.Type(), v.Len(), newcap) reflect.Copy(newv, v) v.Set(newv) } if i >= v.Len() { v.SetLen(i + 1) } } if i < v.Len() { // Decode into element. d.value(v.Index(i)) } else { // Ran out of fixed array: skip. d.value(reflect.Value{}) } i++ // Next token must be , or ]. op = d.scanWhile(scanSkipSpace) if op == scanEndArray { break } if op != scanArrayValue { d.error(errPhase) } } if i < v.Len() { if v.Kind() == reflect.Array { // Array. Zero the rest. z := reflect.Zero(v.Type().Elem()) for ; i < v.Len(); i++ { v.Index(i).Set(z) } } else { v.SetLen(i) } } if i == 0 && v.Kind() == reflect.Slice { v.Set(reflect.MakeSlice(v.Type(), 0, 0)) } } var nullLiteral = []byte("null") // object consumes an object from d.data[d.off-1:], decoding into the value v. // the first byte ('{') of the object has been read already. func (d *decodeState) object(v reflect.Value) { // Check for unmarshaler. u, ut, pv := d.indirect(v, false) if u != nil { d.off-- err := u.UnmarshalJSON(d.next()) if err != nil { d.error(err) } return } if ut != nil { d.saveError(&UnmarshalTypeError{"object", v.Type(), int64(d.off)}) d.off-- d.next() // skip over { } in input return } v = pv // Decoding into nil interface? Switch to non-reflect code. if v.Kind() == reflect.Interface && v.NumMethod() == 0 { v.Set(reflect.ValueOf(d.objectInterface())) return } // Check type of target: struct or map[string]T switch v.Kind() { case reflect.Map: // map must have string kind t := v.Type() if t.Key().Kind() != reflect.String { d.saveError(&UnmarshalTypeError{"object", v.Type(), int64(d.off)}) d.off-- d.next() // skip over { } in input return } if v.IsNil() { v.Set(reflect.MakeMap(t)) } case reflect.Struct: default: d.saveError(&UnmarshalTypeError{"object", v.Type(), int64(d.off)}) d.off-- d.next() // skip over { } in input return } var mapElem reflect.Value keys := map[string]bool{} for { // Read opening " of string key or closing }. op := d.scanWhile(scanSkipSpace) if op == scanEndObject { // closing } - can only happen on first iteration. break } if op != scanBeginLiteral { d.error(errPhase) } // Read key. start := d.off - 1 op = d.scanWhile(scanContinue) item := d.data[start : d.off-1] key, ok := unquote(item) if !ok { d.error(errPhase) } // Check for duplicate keys. _, ok = keys[key] if !ok { keys[key] = true } else { d.error(fmt.Errorf("json: duplicate key '%s' in object", key)) } // Figure out field corresponding to key. var subv reflect.Value destring := false // whether the value is wrapped in a string to be decoded first if v.Kind() == reflect.Map { elemType := v.Type().Elem() if !mapElem.IsValid() { mapElem = reflect.New(elemType).Elem() } else { mapElem.Set(reflect.Zero(elemType)) } subv = mapElem } else { var f *field fields := cachedTypeFields(v.Type()) for i := range fields { ff := &fields[i] if bytes.Equal(ff.nameBytes, []byte(key)) { f = ff break } } if f != nil { subv = v destring = f.quoted for _, i := range f.index { if subv.Kind() == reflect.Ptr { if subv.IsNil() { subv.Set(reflect.New(subv.Type().Elem())) } subv = subv.Elem() } subv = subv.Field(i) } } } // Read : before value. if op == scanSkipSpace { op = d.scanWhile(scanSkipSpace) } if op != scanObjectKey { d.error(errPhase) } // Read value. if destring { switch qv := d.valueQuoted().(type) { case nil: d.literalStore(nullLiteral, subv, false) case string: d.literalStore([]byte(qv), subv, true) default: d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal unquoted value into %v", subv.Type())) } } else { d.value(subv) } // Write value back to map; // if using struct, subv points into struct already. if v.Kind() == reflect.Map { kv := reflect.ValueOf(key).Convert(v.Type().Key()) v.SetMapIndex(kv, subv) } // Next token must be , or }. op = d.scanWhile(scanSkipSpace) if op == scanEndObject { break } if op != scanObjectValue { d.error(errPhase) } } } // literal consumes a literal from d.data[d.off-1:], decoding into the value v. // The first byte of the literal has been read already // (that's how the caller knows it's a literal). func (d *decodeState) literal(v reflect.Value) { // All bytes inside literal return scanContinue op code. start := d.off - 1 op := d.scanWhile(scanContinue) // Scan read one byte too far; back up. d.off-- d.scan.undo(op) d.literalStore(d.data[start:d.off], v, false) } // convertNumber converts the number literal s to a float64, int64 or a Number // depending on d.numberDecodeType. func (d *decodeState) convertNumber(s string) (interface{}, error) { switch d.numberType { case UnmarshalJSONNumber: return Number(s), nil case UnmarshalIntOrFloat: v, err := strconv.ParseInt(s, 10, 64) if err == nil { return v, nil } // tries to parse integer number in scientific notation f, err := strconv.ParseFloat(s, 64) if err != nil { return nil, &UnmarshalTypeError{"number " + s, reflect.TypeOf(0.0), int64(d.off)} } // if it has no decimal value use int64 if fi, fd := math.Modf(f); fd == 0.0 { return int64(fi), nil } return f, nil default: f, err := strconv.ParseFloat(s, 64) if err != nil { return nil, &UnmarshalTypeError{"number " + s, reflect.TypeOf(0.0), int64(d.off)} } return f, nil } } var numberType = reflect.TypeOf(Number("")) // literalStore decodes a literal stored in item into v. // // fromQuoted indicates whether this literal came from unwrapping a // string from the ",string" struct tag option. this is used only to // produce more helpful error messages. func (d *decodeState) literalStore(item []byte, v reflect.Value, fromQuoted bool) { // Check for unmarshaler. if len(item) == 0 { //Empty string given d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) return } wantptr := item[0] == 'n' // null u, ut, pv := d.indirect(v, wantptr) if u != nil { err := u.UnmarshalJSON(item) if err != nil { d.error(err) } return } if ut != nil { if item[0] != '"' { if fromQuoted { d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) } else { d.saveError(&UnmarshalTypeError{"string", v.Type(), int64(d.off)}) } return } s, ok := unquoteBytes(item) if !ok { if fromQuoted { d.error(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) } else { d.error(errPhase) } } err := ut.UnmarshalText(s) if err != nil { d.error(err) } return } v = pv switch c := item[0]; c { case 'n': // null switch v.Kind() { case reflect.Interface, reflect.Ptr, reflect.Map, reflect.Slice: v.Set(reflect.Zero(v.Type())) // otherwise, ignore null for primitives/string } case 't', 'f': // true, false value := c == 't' switch v.Kind() { default: if fromQuoted { d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) } else { d.saveError(&UnmarshalTypeError{"bool", v.Type(), int64(d.off)}) } case reflect.Bool: v.SetBool(value) case reflect.Interface: if v.NumMethod() == 0 { v.Set(reflect.ValueOf(value)) } else { d.saveError(&UnmarshalTypeError{"bool", v.Type(), int64(d.off)}) } } case '"': // string s, ok := unquoteBytes(item) if !ok { if fromQuoted { d.error(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) } else { d.error(errPhase) } } switch v.Kind() { default: d.saveError(&UnmarshalTypeError{"string", v.Type(), int64(d.off)}) case reflect.Slice: if v.Type().Elem().Kind() != reflect.Uint8 { d.saveError(&UnmarshalTypeError{"string", v.Type(), int64(d.off)}) break } b := make([]byte, base64.StdEncoding.DecodedLen(len(s))) n, err := base64.StdEncoding.Decode(b, s) if err != nil { d.saveError(err) break } v.SetBytes(b[:n]) case reflect.String: v.SetString(string(s)) case reflect.Interface: if v.NumMethod() == 0 { v.Set(reflect.ValueOf(string(s))) } else { d.saveError(&UnmarshalTypeError{"string", v.Type(), int64(d.off)}) } } default: // number if c != '-' && (c < '0' || c > '9') { if fromQuoted { d.error(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) } else { d.error(errPhase) } } s := string(item) switch v.Kind() { default: if v.Kind() == reflect.String && v.Type() == numberType { v.SetString(s) if !isValidNumber(s) { d.error(fmt.Errorf("json: invalid number literal, trying to unmarshal %q into Number", item)) } break } if fromQuoted { d.error(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) } else { d.error(&UnmarshalTypeError{"number", v.Type(), int64(d.off)}) } case reflect.Interface: n, err := d.convertNumber(s) if err != nil { d.saveError(err) break } if v.NumMethod() != 0 { d.saveError(&UnmarshalTypeError{"number", v.Type(), int64(d.off)}) break } v.Set(reflect.ValueOf(n)) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: n, err := strconv.ParseInt(s, 10, 64) if err != nil || v.OverflowInt(n) { d.saveError(&UnmarshalTypeError{"number " + s, v.Type(), int64(d.off)}) break } v.SetInt(n) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: n, err := strconv.ParseUint(s, 10, 64) if err != nil || v.OverflowUint(n) { d.saveError(&UnmarshalTypeError{"number " + s, v.Type(), int64(d.off)}) break } v.SetUint(n) case reflect.Float32, reflect.Float64: n, err := strconv.ParseFloat(s, v.Type().Bits()) if err != nil || v.OverflowFloat(n) { d.saveError(&UnmarshalTypeError{"number " + s, v.Type(), int64(d.off)}) break } v.SetFloat(n) } } } // The xxxInterface routines build up a value to be stored // in an empty interface. They are not strictly necessary, // but they avoid the weight of reflection in this common case. // valueInterface is like value but returns interface{} func (d *decodeState) valueInterface() interface{} { switch d.scanWhile(scanSkipSpace) { default: d.error(errPhase) panic("unreachable") case scanBeginArray: return d.arrayInterface() case scanBeginObject: return d.objectInterface() case scanBeginLiteral: return d.literalInterface() } } // arrayInterface is like array but returns []interface{}. func (d *decodeState) arrayInterface() []interface{} { var v = make([]interface{}, 0) for { // Look ahead for ] - can only happen on first iteration. op := d.scanWhile(scanSkipSpace) if op == scanEndArray { break } // Back up so d.value can have the byte we just read. d.off-- d.scan.undo(op) v = append(v, d.valueInterface()) // Next token must be , or ]. op = d.scanWhile(scanSkipSpace) if op == scanEndArray { break } if op != scanArrayValue { d.error(errPhase) } } return v } // objectInterface is like object but returns map[string]interface{}. func (d *decodeState) objectInterface() map[string]interface{} { m := make(map[string]interface{}) keys := map[string]bool{} for { // Read opening " of string key or closing }. op := d.scanWhile(scanSkipSpace) if op == scanEndObject { // closing } - can only happen on first iteration. break } if op != scanBeginLiteral { d.error(errPhase) } // Read string key. start := d.off - 1 op = d.scanWhile(scanContinue) item := d.data[start : d.off-1] key, ok := unquote(item) if !ok { d.error(errPhase) } // Check for duplicate keys. _, ok = keys[key] if !ok { keys[key] = true } else { d.error(fmt.Errorf("json: duplicate key '%s' in object", key)) } // Read : before value. if op == scanSkipSpace { op = d.scanWhile(scanSkipSpace) } if op != scanObjectKey { d.error(errPhase) } // Read value. m[key] = d.valueInterface() // Next token must be , or }. op = d.scanWhile(scanSkipSpace) if op == scanEndObject { break } if op != scanObjectValue { d.error(errPhase) } } return m } // literalInterface is like literal but returns an interface value. func (d *decodeState) literalInterface() interface{} { // All bytes inside literal return scanContinue op code. start := d.off - 1 op := d.scanWhile(scanContinue) // Scan read one byte too far; back up. d.off-- d.scan.undo(op) item := d.data[start:d.off] switch c := item[0]; c { case 'n': // null return nil case 't', 'f': // true, false return c == 't' case '"': // string s, ok := unquote(item) if !ok { d.error(errPhase) } return s default: // number if c != '-' && (c < '0' || c > '9') { d.error(errPhase) } n, err := d.convertNumber(string(item)) if err != nil { d.saveError(err) } return n } } // getu4 decodes \uXXXX from the beginning of s, returning the hex value, // or it returns -1. func getu4(s []byte) rune { if len(s) < 6 || s[0] != '\\' || s[1] != 'u' { return -1 } r, err := strconv.ParseUint(string(s[2:6]), 16, 64) if err != nil { return -1 } return rune(r) } // unquote converts a quoted JSON string literal s into an actual string t. // The rules are different than for Go, so cannot use strconv.Unquote. func unquote(s []byte) (t string, ok bool) { s, ok = unquoteBytes(s) t = string(s) return } func unquoteBytes(s []byte) (t []byte, ok bool) { if len(s) < 2 || s[0] != '"' || s[len(s)-1] != '"' { return } s = s[1 : len(s)-1] // Check for unusual characters. If there are none, // then no unquoting is needed, so return a slice of the // original bytes. r := 0 for r < len(s) { c := s[r] if c == '\\' || c == '"' || c < ' ' { break } if c < utf8.RuneSelf { r++ continue } rr, size := utf8.DecodeRune(s[r:]) if rr == utf8.RuneError && size == 1 { break } r += size } if r == len(s) { return s, true } b := make([]byte, len(s)+2*utf8.UTFMax) w := copy(b, s[0:r]) for r < len(s) { // Out of room? Can only happen if s is full of // malformed UTF-8 and we're replacing each // byte with RuneError. if w >= len(b)-2*utf8.UTFMax { nb := make([]byte, (len(b)+utf8.UTFMax)*2) copy(nb, b[0:w]) b = nb } switch c := s[r]; { case c == '\\': r++ if r >= len(s) { return } switch s[r] { default: return case '"', '\\', '/', '\'': b[w] = s[r] r++ w++ case 'b': b[w] = '\b' r++ w++ case 'f': b[w] = '\f' r++ w++ case 'n': b[w] = '\n' r++ w++ case 'r': b[w] = '\r' r++ w++ case 't': b[w] = '\t' r++ w++ case 'u': r-- rr := getu4(s[r:]) if rr < 0 { return } r += 6 if utf16.IsSurrogate(rr) { rr1 := getu4(s[r:]) if dec := utf16.DecodeRune(rr, rr1); dec != unicode.ReplacementChar { // A valid pair; consume. r += 6 w += utf8.EncodeRune(b[w:], dec) break } // Invalid surrogate; fall back to replacement rune. rr = unicode.ReplacementChar } w += utf8.EncodeRune(b[w:], rr) } // Quote, control characters are invalid. case c == '"', c < ' ': return // ASCII case c < utf8.RuneSelf: b[w] = c r++ w++ // Coerce to well-formed UTF-8. default: rr, size := utf8.DecodeRune(s[r:]) r += size w += utf8.EncodeRune(b[w:], rr) } } return b[0:w], true } ================================================ FILE: vendor/github.com/go-jose/go-jose/v4/json/encode.go ================================================ // Copyright 2010 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Package json implements encoding and decoding of JSON objects as defined in // RFC 4627. The mapping between JSON objects and Go values is described // in the documentation for the Marshal and Unmarshal functions. // // See "JSON and Go" for an introduction to this package: // https://golang.org/doc/articles/json_and_go.html package json import ( "bytes" "encoding" "encoding/base64" "fmt" "math" "reflect" "runtime" "sort" "strconv" "strings" "sync" "unicode" "unicode/utf8" ) // Marshal returns the JSON encoding of v. // // Marshal traverses the value v recursively. // If an encountered value implements the Marshaler interface // and is not a nil pointer, Marshal calls its MarshalJSON method // to produce JSON. If no MarshalJSON method is present but the // value implements encoding.TextMarshaler instead, Marshal calls // its MarshalText method. // The nil pointer exception is not strictly necessary // but mimics a similar, necessary exception in the behavior of // UnmarshalJSON. // // Otherwise, Marshal uses the following type-dependent default encodings: // // Boolean values encode as JSON booleans. // // Floating point, integer, and Number values encode as JSON numbers. // // String values encode as JSON strings coerced to valid UTF-8, // replacing invalid bytes with the Unicode replacement rune. // The angle brackets "<" and ">" are escaped to "\u003c" and "\u003e" // to keep some browsers from misinterpreting JSON output as HTML. // Ampersand "&" is also escaped to "\u0026" for the same reason. // // Array and slice values encode as JSON arrays, except that // []byte encodes as a base64-encoded string, and a nil slice // encodes as the null JSON object. // // Struct values encode as JSON objects. Each exported struct field // becomes a member of the object unless // - the field's tag is "-", or // - the field is empty and its tag specifies the "omitempty" option. // // The empty values are false, 0, any // nil pointer or interface value, and any array, slice, map, or string of // length zero. The object's default key string is the struct field name // but can be specified in the struct field's tag value. The "json" key in // the struct field's tag value is the key name, followed by an optional comma // and options. Examples: // // // Field is ignored by this package. // Field int `json:"-"` // // // Field appears in JSON as key "myName". // Field int `json:"myName"` // // // Field appears in JSON as key "myName" and // // the field is omitted from the object if its value is empty, // // as defined above. // Field int `json:"myName,omitempty"` // // // Field appears in JSON as key "Field" (the default), but // // the field is skipped if empty. // // Note the leading comma. // Field int `json:",omitempty"` // // The "string" option signals that a field is stored as JSON inside a // JSON-encoded string. It applies only to fields of string, floating point, // integer, or boolean types. This extra level of encoding is sometimes used // when communicating with JavaScript programs: // // Int64String int64 `json:",string"` // // The key name will be used if it's a non-empty string consisting of // only Unicode letters, digits, dollar signs, percent signs, hyphens, // underscores and slashes. // // Anonymous struct fields are usually marshaled as if their inner exported fields // were fields in the outer struct, subject to the usual Go visibility rules amended // as described in the next paragraph. // An anonymous struct field with a name given in its JSON tag is treated as // having that name, rather than being anonymous. // An anonymous struct field of interface type is treated the same as having // that type as its name, rather than being anonymous. // // The Go visibility rules for struct fields are amended for JSON when // deciding which field to marshal or unmarshal. If there are // multiple fields at the same level, and that level is the least // nested (and would therefore be the nesting level selected by the // usual Go rules), the following extra rules apply: // // 1) Of those fields, if any are JSON-tagged, only tagged fields are considered, // even if there are multiple untagged fields that would otherwise conflict. // 2) If there is exactly one field (tagged or not according to the first rule), that is selected. // 3) Otherwise there are multiple fields, and all are ignored; no error occurs. // // Handling of anonymous struct fields is new in Go 1.1. // Prior to Go 1.1, anonymous struct fields were ignored. To force ignoring of // an anonymous struct field in both current and earlier versions, give the field // a JSON tag of "-". // // Map values encode as JSON objects. // The map's key type must be string; the map keys are used as JSON object // keys, subject to the UTF-8 coercion described for string values above. // // Pointer values encode as the value pointed to. // A nil pointer encodes as the null JSON object. // // Interface values encode as the value contained in the interface. // A nil interface value encodes as the null JSON object. // // Channel, complex, and function values cannot be encoded in JSON. // Attempting to encode such a value causes Marshal to return // an UnsupportedTypeError. // // JSON cannot represent cyclic data structures and Marshal does not // handle them. Passing cyclic structures to Marshal will result in // an infinite recursion. func Marshal(v interface{}) ([]byte, error) { e := &encodeState{} err := e.marshal(v) if err != nil { return nil, err } return e.Bytes(), nil } // MarshalIndent is like Marshal but applies Indent to format the output. func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error) { b, err := Marshal(v) if err != nil { return nil, err } var buf bytes.Buffer err = Indent(&buf, b, prefix, indent) if err != nil { return nil, err } return buf.Bytes(), nil } // HTMLEscape appends to dst the JSON-encoded src with <, >, &, U+2028 and U+2029 // characters inside string literals changed to \u003c, \u003e, \u0026, \u2028, \u2029 // so that the JSON will be safe to embed inside HTML