Compare commits

..

157 Commits

Author SHA1 Message Date
Cuong Manh Le 5553490b27 docs: add default value to configs
While at it, also correct some configs to match the latest version.

Fixes #44
2023-06-08 21:54:06 +07:00
Yegor S eaf39f48a0 Update README.md 2023-06-08 01:48:37 -04:00
Yegor S a5ddbdcb42 Update README.md 2023-06-08 01:40:13 -04:00
Yegor S 0c99d27be5 Merge pull request #51 from Control-D-Inc/release-branch-v1.2.1
Release branch v1.2.1
2023-06-08 00:19:07 -04:00
Cuong Manh Le b9eb89c02e internal/router: fix missing Run() call 2023-06-08 02:27:20 +07:00
Cuong Manh Le 53f8d006f0 all: support older version of Openwrt 2023-06-08 02:07:32 +07:00
Cuong Manh Le 929de49c7b cmd/ctrld: only spawn DNS server for ntpd if necessary
On some platforms, like pfsense, ntpd is not problem, so do not spawn
the DNS server for it, which may conflict with default DNS server.

While at it, also make sure that ctrld will be run at last on startup.
2023-06-08 02:07:10 +07:00
Cuong Manh Le 542c4f7daf all: adding more function/type documentation 2023-06-06 00:07:15 +07:00
Cuong Manh Le c941f9c621 all: add flag to use dev domain for testing 2023-06-06 00:07:05 +07:00
Cuong Manh Le 25eae187db internal/router: do not exit when stopping successfully on freshtomato
Otherwise, "restart" will be broken because "start" won't never be called.
2023-06-03 10:31:08 +07:00
Cuong Manh Le 726a25a7ea internal/router: emit error if dnsfilter is enabled on Ubios/EdgeOS 2023-06-02 22:45:39 +07:00
Cuong Manh Le a46bb152af cmd/ctrld: do not mutual net.Addr when spoofing client source IP
Otherwise, the original address will be overwritten, causing the
connection between the listener and dnsmasq broken.
2023-06-02 22:43:00 +07:00
Cuong Manh Le bbfa7c6c22 internal/router: relax dnsmasq lease file parsing condition
On DD-WRT v3.0-r52189, dnsmasq version 2.89 lease format looks like:

1685794060 <mac> <ip> <hostname> 00:00:00:00:00:04 9

It has 6 fields, while the current parser only looks for line with exact
5 fields, which is too restricted. In fact, the parser shold just skip
line with less than 4 fields, because the 4th field is the hostname,
which is the last client info that ctrld needs.
2023-06-02 22:42:47 +07:00
Cuong Manh Le 1cd54a48e9 all: rework routers ntp waiting mechanism
Currently, on routers that require NTP waiting, ctrld makes the cleanup
process, and restart dnsmasq for restoring default DNS config, so ntpd
can query the NTP servers. It did work, but the code will depends on
router platforms.

Instead, we can spawn a plain DNS listener before PreRun on routers,
this listener will serve NTP dns queries and once ntp is configured, the
listener is terminated and ctrld will start serving using its configured
upstreams.

While at it, also fix the userHomeDir function on freshtomato, which
must return the binary directory for routers that requires JFFS.
2023-06-02 20:25:11 +07:00
Cuong Manh Le 2d950eecdf cmd/ctrld: spoofing client IP on routers 2023-06-02 20:24:59 +07:00
Cuong Manh Le b143e46eb0 all: add support for pfsense 2023-06-02 20:24:42 +07:00
Cuong Manh Le 8fda856e24 all: add UpstreamConfig.VerifyDomain
So the self-check process is only done for ControlD upstream, and can be
distinguished between .com and .dev resolvers.
2023-06-02 20:24:25 +07:00
Cuong Manh Le 54e63ccf9b all: add support for EdgeOS 2023-06-02 20:23:37 +07:00
Cuong Manh Le ee53db1e35 all: add support for freshtomato 2023-06-02 20:21:17 +07:00
Cuong Manh Le fc502b920b internal/router: add Synology client info file 2023-06-02 20:21:02 +07:00
Cuong Manh Le 20eae82f11 cmd/ctrld: ensure error passed to backoff is wrapped in self-check
In commit 670879d1, the backoff is changed to be passed a real error,
instead of a place holder. However, the test query may return a failed
response with a nil error, causing the backoff never fire.

Fixing this by ensuring the error is wrapped, so the backoff always see
a non-nil error.
2023-06-02 20:20:47 +07:00
Cuong Manh Le d2fc530316 all: add support for Synology router 2023-06-02 20:20:31 +07:00
Cuong Manh Le 7ac5555a84 internal/router: fix wrong platform check in PreStart
The NTP workaround is intended to be run on Merlin only.
2023-06-02 20:20:12 +07:00
Cuong Manh Le 15d397d8a6 cmd/ctrld: fix problem with default iface name on WSL 1
On WSL 1, the routing table do not contain default route, causing ctrld
failed to get the default iface for setting DNS. However, WSL 1 only use
/etc/resolv.conf for setting up DNS, so the interface does not matter,
because the setting is applied global anyway.

To fix it, just return "lo" as the default interface name on WSL 1.
While at it, also removing the useless service.Logger call, which is not
unified with the current logger, and may cause false positive on system
where syslog is not configured properly (like WSL 1).

Also passing the real error when doing sel-check to backoff, so we don't
have to use a place holder error.
2023-06-02 20:19:57 +07:00
Cuong Manh Le b471adfb09 Fix split mode for all protocols but DoH
In split mode, the code must check for ipv6 availability to return the
correct network stack. Otherwise, we may end up using "tcp6-tls" even
though the upstream IP is an ipv4.
2023-06-02 20:19:25 +07:00
Yegor S d7a38363e6 Merge pull request #42 from Control-D-Inc/update-readme
Update README.md
2023-05-16 15:17:05 -04:00
Yegor Sak 90def8f9b5 Update README.md 2023-05-17 01:59:11 +07:00
Yegor S b126db453b Update README.md 2023-05-15 21:49:44 -04:00
Yegor S 601d357456 Merge pull request #41 from Control-D-Inc/release-branch-v1.2.0
Release branch v1.2.0
2023-05-15 21:48:05 -04:00
Yegor Sak 3a2024ebd7 Update README.md 2023-05-16 08:39:47 +07:00
Yegor Sak 6cd451acec Update README.md 2023-05-16 00:17:48 +07:00
Cuong Manh Le 3b6c12abd4 all: support GL.iNET router 2023-05-16 00:17:13 +07:00
Cuong Manh Le d9dfc584e7 internal/router: disable DNSSEC on ddwrt/merlin 2023-05-16 00:16:17 +07:00
Cuong Manh Le 57fa68970a internal/router: fix lint ignore comment 2023-05-15 22:51:33 +07:00
Cuong Manh Le fa14f1dadf Fix wrong timeout in lookupIP
The assignment is changed wrongly in process of refactoring parallel
dialer for resolving bootstrap IP.

While at it, also satisfy staticheck for jffs not enabled error.
2023-05-15 22:37:47 +07:00
Cuong Manh Le 9689607409 all: wait NTP synced on Merlin
On some Merlin routers, the time is broken when system reboot, and need
to wait for NTP synced to get the correct time. For fetching API in cd
mode successfully, ctrld need to wait until NTP set the time correctly,
otherwise, the certificate validation would complain.
2023-05-15 21:13:23 +07:00
Cuong Manh Le d75f871541 internal/router: workaround problem with ntp bug on some Merlin routers
On some Merlin routers, due to ntp bug, after rebooing, dnsmasq config
was restored to default without ctrld changes, causing ctrld stop
working. Workaround this problem by catching restart diskmon event,
which is triggered by ntpd_synced, then restart dnsmasq.
2023-05-15 21:13:23 +07:00
Cuong Manh Le 45895067c6 cmd/ctrld: only ignore listener.0 setup when setup router 2023-05-15 21:13:23 +07:00
Cuong Manh Le 521f06dcc1 cmd/ctrld: force 127.0.0.1:53 for listener.0 only 2023-05-15 21:13:23 +07:00
Cuong Manh Le 5b6a3a4c6f internal/router: disable native dot on merlin
While at it, also ensure custom config is ignored when running on
router, because we need to point to 127.0.0.1:53 (dnsmasq listener).
2023-05-15 21:13:23 +07:00
Cuong Manh Le be497a68de internal/router: skip bad entry in leases file
Seen in UDM Dream Machine.
2023-05-15 21:13:21 +07:00
Cuong Manh Le c872a3b3f6 cmd/ctrld: add "--silent" to disable log output 2023-05-15 20:54:01 +07:00
Cuong Manh Le e0ae0f8e7b cmd/ctrld: set default value for ip/port from custom config if missing 2023-05-15 20:54:01 +07:00
Cuong Manh Le ad4ca32873 cmd/ctrld: factor out code to read config file
So start/run command will use the same code path, prevent mismatch from
reading/searching/writing config file.
2023-05-15 20:54:01 +07:00
Cuong Manh Le 24100c4cbe cmd/ctrld: use Windscribe fork of zerolog
For supporting default log level notice. While at it, also fix a missing
os.Exit call when setup router on non-supported platforms.
2023-05-15 20:54:01 +07:00
Cuong Manh Le e3a792d50d cmd/ctrld: start listener with no default upstream
We can have more listeners than upstreams.
2023-05-15 20:54:01 +07:00
Cuong Manh Le 440d085c6d cmd/ctrld: unified logging
By using a separate console logging and use it in all places before
reading in logging config.
2023-05-15 20:54:01 +07:00
Cuong Manh Le 270ea9f6ca Do not block when ping upstream
Because the network may not be available at the time ping upstream
happens, so ctrld will stuck there waiting for pinging upstream.
2023-05-15 20:54:01 +07:00
Cuong Manh Le 7a156d7d15 Wait until bootstrap IPs resolved
When bootstrapping, if the network changed, for example, firewall rules
changed during VPN connection, the bootstrap IPs may not be resolved, so
ctrld won't work. Since bootstrap IPs is necessary for ctrld to work
properly, we should wait until we can resolve upstream IP before we can
start serving requests.
2023-05-15 20:54:01 +07:00
Cuong Manh Le 4c45e6cf3d Lock while getting doh/doh3 transport 2023-05-15 20:54:01 +07:00
Cuong Manh Le 704bc27dba Check msg is not nil before access Question field 2023-05-15 20:54:01 +07:00
Cuong Manh Le b267572b38 all: implement split upstreams
This commit introduces split upstreams feature, allowing to configure
what ip stack that ctrld will use to connect to upstream.
2023-05-15 20:53:59 +07:00
Cuong Manh Le 5cad0d6be1 all: watch link state on Linux using netlink
So we can detect changed to link and trigger re-bootstrap.
2023-05-13 12:24:16 +07:00
Cuong Manh Le 56d8dc865f Use different failover mechanism on Linux
Instead of always doubling the request, first we wrap the request with a
failover timeout, 500ms, which is an average time for a normal request.
If this request failed, trigger re-bootstrapping and retry the request.
2023-05-13 12:18:26 +07:00
Cuong Manh Le d57c1d6d44 Workaround for DOH broken transport when network changes
When network changes, for example: connect/disconnect VPN, the old
connection will become broken, but still can be re-used for new
requests. That would cause un-necessary delay for ctrld clients:

 - Time 0   - do request with broken transport, 5s timeout.
 - Time 0.5 - network stack become usable.
 - Time 5   - timeout reached.
 - Time 5.1 - do request with new transport -> success.

Instead, we can do two requests in parallel, with the failover one using
a fresh new transport. So if the main one is broken, we still can get
the result from the failover one.
2023-05-13 12:18:01 +07:00
Cuong Manh Le 02fa7fbe2e Workaround issue with weird DNS server when bootstraping
We see in practice on fresh new VM test, there's a DNS server that
return the answer with record not for the query domain.

To workaround this, filter out the answers not for the query domain.
2023-05-13 12:17:49 +07:00
Cuong Manh Le 07689954bf cmd/ctrld: change default log level to warn 2023-05-13 12:17:02 +07:00
Cuong Manh Le a7ea20b117 cmd/ctrld: ensure runDNSServer returns when error happens 2023-05-13 12:07:52 +07:00
Cuong Manh Le 43fecdf60f all: log when client info included in the request 2023-05-13 12:07:32 +07:00
Cuong Manh Le 31239684c7 Revert "cmd/ctrld: add "start --no-cd" flag to disable cd mode"
This reverts commit 00fe7f59d13774f2ea6c325bdbb8165be58a1edd.

The purpose is disable cd mode for already installed service, which is
a hard problem than we thought. So leave it out of v1.2 cycle.
2023-05-13 12:07:20 +07:00
Cuong Manh Le 5528ac8bf1 internal/router: log invalid ip address entry 2023-05-13 12:06:26 +07:00
Cuong Manh Le 411e23ecfe cmd/ctrld: fix missing content for default config
When writing default config file, the content must be marshalled to the
config object first before writing to disk.

While at it, also use full path for default config file to make it clear
to the user where the config is written.
2023-05-13 12:06:11 +07:00
Cuong Manh Le 7bf231643b internal/router: normalize ip address from dnsmasq lease file
dnsmasq may put an ip address with the interface index in lease file,
causing bad data sent to the Control-D backend.
2023-05-13 12:05:49 +07:00
Cuong Manh Le 2326160f2f Do not rely on unspecified assignment order of return statement
See: https://github.com/golang/go/issues/58233
2023-05-13 12:05:33 +07:00
Cuong Manh Le 68fe7e8406 cmd/ctrld: add "start --no-cd" flag to disable cd mode 2023-05-13 12:05:18 +07:00
Cuong Manh Le c7bad63869 all: allow chosing random address and port for listener 2023-05-13 12:04:58 +07:00
Cuong Manh Le 69319c6b41 all: support custom config from Control-D resolver 2023-05-13 12:04:39 +07:00
Cuong Manh Le 9df381d3d1 all: add "version" query param when fetching config 2023-05-13 12:04:21 +07:00
Cuong Manh Le 0af7f64bca all: use parallel dialer for bootstrapping ip
So we don't have to depend on network probing for checking ipv4/ipv6
enabled, making ctrld working more stably.
2023-05-13 12:04:06 +07:00
Cuong Manh Le f73cbde7a5 Update HTTP request headers 2023-05-13 12:03:51 +07:00
Cuong Manh Le 0645a738ad all: add router client info detection
This commit add the ability for ctrld to gather client information,
including mac/ip/hostname, and send to Control-D server through a
config per upstream.

 - Add send_client_info upstream config.
 - Read/Watch dnsmasq leases files on supported platforms.
 - Add corresponding client info to DoH query header

All of these only apply for Control-D upstream, though.
2023-05-13 12:03:24 +07:00
Cuong Manh Le d52cd11322 all: use parallel dialer for connecting upstream/api
So we don't have to depend on network stack probing to decide whether
ipv4 or ipv6 will be used.

While at it, also prevent a race report when doing the same parallel
resolving for os resolver, even though this race is harmless.
2023-05-13 12:02:18 +07:00
Cuong Manh Le d3d08022cc cmd/ctrld: restoring DNS on darwin before stop
Otherwise, we experiment with ctrld slow start after rebooting, because
the network check continuously report failed status even the network
state is up. Restoring the DNS before stopping, we leave the network
state as default, as long as ctrld starts, the DNS is configured again.
2023-05-13 12:00:33 +07:00
Cuong Manh Le 21c8b9f8e7 Revert ignoring SIGCHLD
Using signal.Ignore causes exec.Command failed with no child process
error.
2023-05-13 12:00:13 +07:00
Cuong Manh Le 6c55d8f139 internal/router: remove ctrld-boot service when uninstall 2023-05-13 11:59:55 +07:00
Cuong Manh Le ccdb2a3f70 Tweak log message for policy logging 2023-05-13 11:59:33 +07:00
Cuong Manh Le f5ef9b917e all: implement router setup for ubios 2023-05-13 11:59:14 +07:00
Cuong Manh Le a5443d5ca4 all: implement router setup for merlin 2023-05-13 11:58:56 +07:00
Cuong Manh Le 2c7d95bba2 Support query param in upstream value 2023-05-13 11:58:31 +07:00
Cuong Manh Le 8a2cdbfaa3 all: implement router setup for ddwrt 2023-05-13 11:58:02 +07:00
Cuong Manh Le c94be0df35 all: implement router setup for openwrt 2023-05-13 11:53:48 +07:00
Cuong Manh Le 4b6a976747 all: initial support for setup linux router
Wiring code to configure router when running ctrld. Future commits will
add implementation for each supported platforms.
2023-05-13 11:51:29 +07:00
alexelisenko 0043fdf859 enable compression 2023-05-13 11:18:57 +07:00
Cuong Manh Le 24e62e18fa Use errors.Join instead of copied version 2023-05-13 11:13:00 +07:00
Yegor S 663dbbb476 Merge pull request #39 from Control-D-Inc/timeout-no-config-mode
cmd/ctrld: add default timeout when generating config in no config mode
2023-04-05 16:17:03 -04:00
Cuong Manh Le 471427a439 cmd/ctrld: add default timeout when generating config in no config mode 2023-04-06 00:57:07 +07:00
Yegor S a777c4b00f Merge pull request #38 from Control-D-Inc/issue-33
Add support for mipsle
2023-04-04 11:15:55 -04:00
Cuong Manh Le dcc4cdd316 Add support for mipsle
While at it, also add 386 and arm to quic free build

Fixes #33
2023-04-04 21:55:04 +07:00
Yegor S 9c22701940 Merge pull request #37 from Control-D-Inc/release-branch-v1.1.4
Release branch v1.1.4
2023-04-03 12:44:02 -04:00
Cuong Manh Le a77a924320 Require go1.20 for building ctrld 2023-03-31 23:31:38 +07:00
Cuong Manh Le 95dbf71939 Upgrage tailscale.com for fixing security issue 2023-03-31 23:31:38 +07:00
Cuong Manh Le 8869e33a20 Inject version and commit during goreleaser build 2023-03-31 23:31:38 +07:00
Cuong Manh Le c94e1b02d2 all: supports multiple protocols for no config mode
Updates #78
2023-03-31 23:31:38 +07:00
Cuong Manh Le 42d29b626b Adding more source for getting available DNS
On some platforms, the gateway may not be a usable DNS. So extending the
current approach to allow retrieving DNS from many sources.
2023-03-31 12:37:37 +07:00
Cuong Manh Le b65a5ac283 all: fix bug that causes ctrld stop working if bootstrap failed
The bootstrap process has two issues that can make ctrld stop resolving
after restarting machine host.

ctrld uses bootstrap DNS and os nameservers for resolving upstream. On
unix, /etc/resolv.conf content is used to get available nameservers.
This works well when installing ctrld. However, after being installed,
ctrld may modify the content of /etc/resolv.conf itself, to make other
apps use its listener as DNS resolver. So when ctrld starts after OS
restart, it ends up using [bootstrap DNS + ctrld's listener], for
resolving upstream. At this moment, if ctrld could not contact bootstrap
DNS for any reason, upstream domain will not be resolved.

For above reason, an upstream may not have bootstrap IPs after ctrld
starts. When re-bootstrapping, if there's no bootstrap IPs, ctrld should
call the setup bootstrap process again. Currently, it does not, causing
all queries failed.

This commit fixes above issue by adding mechanism for retrieving OS
nameservers properly, by querying routing table information:

 - Parsing /proc/net subsystem on Linux.
 - For BSD variants, just fetching routing information base from OS.
 - On Windows, just include the gateway information when reading iface.

The fixing for second issue is trivial, just kickoff a bootstrap process
if there's no bootstrap IPs when re-boostrapping.

While at it, also ensure that fetching resolver information from
ControlD API is also used the same approach.

Fixes #34
2023-03-31 10:23:05 +07:00
Cuong Manh Le ba48ff5965 all: fix os resolver hangs when all server failed
For os resolver, ctrld queries against all servers concurrently, and get
the first success result back. However, if all server failed, the result
channel is not closed, causing ctrld hang.

Fixing this by closing the result channel once getting back all response
from servers.

While at it, also shorten the backoff time when waiting for network up,
ctrld should serve as fast as possible after network is available.

Updates #34
2023-03-31 10:18:14 +07:00
Cuong Manh Le b3a342bc44 all: some improvements for better troubleshooting
- Include version/OS information when logging
 - Make time field human readable in log file
 - Force root privilege when running status command on darwin

Updates #34
2023-03-31 10:17:42 +07:00
Cuong Manh Le 9927803497 cmd/ctrld: response to OS service manager earlier
When startup, ctrld waits for network up before calling s.Run to starts
its logic. However, if network is down on startup, ctrld will hang on
waiting for network up. That causes OS service manager unhappy, as ctrld
do not response to it, marking ctrld as failure service and never start
ctrld again.

To fix this, we should call s.Run as soon as possible, and use a channel
for waiting a signal that we can actual do our logic after network up.

Update #34
2023-03-31 10:14:46 +07:00
Cuong Manh Le f0c604a9f1 cmd/ctrld: only watch config when doing self-check
Avoiding reading/writing global config, causing a data race. While at
it, also guarding read/write access to cfg.Service.AllocateIP field,
since when it is read/write by multiple goroutines.
2023-03-31 10:12:01 +07:00
Cuong Manh Le 8a56389396 cmd/ctrld: ensure both udp/tcp listener aborted
So either one of them return an error, the other will be terminated.
2023-03-31 10:11:12 +07:00
Yegor S 9f7bfc76db Merge pull request #31 from Control-D-Inc/release-branch-v1.1.3
Release branch v1.1.3
2023-03-17 12:33:32 -04:00
Cuong Manh Le a7a5501ea5 Bump version to v1.1.3 2023-03-17 22:22:54 +07:00
Cuong Manh Le c401c4ef87 cmd/ctrld: do not set default iface value for uninstall command
Fixed #30
2023-03-17 22:21:57 +07:00
Cuong Manh Le 8ffb42962a Use rcode string in error message
So it's clearer what went wrong.
2023-03-17 22:21:39 +07:00
Cuong Manh Le aad04200cb Merge pull request #28 from Control-D-Inc/release-branch-v1.1.2
Release branch v1.1.2
2023-03-16 22:35:09 +07:00
Cuong Manh Le 4bfcacaf3c cmd/ctrld: bump version to v1.1.2 2023-03-16 10:53:33 +07:00
Cuong Manh Le 5b362412be Add quic free version to goreleaser 2023-03-16 10:40:17 +07:00
Cuong Manh Le ccf07a7d1c cmd/ctrld: log that ctrld is starting 2023-03-16 09:53:08 +07:00
Cuong Manh Le e4eb3b2ded Do not query ipv6 eagerly when setup bootstrap IP
We only need on demand information when re-bootstrapping. On Bootsrap,
this is already checked by ctrldnet.Up, so on demand query will cause
un-necessary slow down if external ipv6 is slow to response.
2023-03-16 09:52:57 +07:00
Cuong Manh Le 77b62f8734 cmd/ctrld: add default timeout for os resolver
So it can fail fast if internet broken suddenly. While at it, also
filtering out ipv6 nameservers if ipv6 not available.
2023-03-16 09:52:39 +07:00
Cuong Manh Le 096e7ea429 internal/net: enforce timeout for probing stack
On Windows host with StarLink network, ctrld hangs on startup for ~30s
before continue running. This dues to IPv6 is configured but no external
IPv6 can be reached. When probing stack, ctrld is dialing using ipv6
without any timeout set, so the dialing timeout is enforced by OS.

This commit adds a timeout for probing dialer, so we ensure the probing
process will fail fast.
2023-03-16 09:52:22 +07:00
Cuong Manh Le 3e6f6cc721 cmd/ctrld: add TCP listener
Fixes #25
2023-03-16 09:51:33 +07:00
Yegor S 7dab688252 Merge pull request #26 from Control-D-Inc/release-branch-v1.1.1
Release branch v1.1.1
2023-03-10 13:04:08 -05:00
Cuong Manh Le 7cd1f7adda cmd/ctrld: bump version to v1.1.1 2023-03-10 23:20:31 +07:00
Cuong Manh Le 9a249c3029 .github/workflows: use go 1.20 2023-03-10 23:20:31 +07:00
Cuong Manh Le 0dfa377e08 Add freebsd to goreleaser config
While at it, fixed the hook upx script to run per file, and ignore
binaries which are not supported.
2023-03-10 23:20:31 +07:00
Cuong Manh Le 14bc29751f Use both os and bootstrap DNS to resolve bootstrap IP 2023-03-10 23:20:22 +07:00
Cuong Manh Le e6800fbc82 Query all possible nameservers for os resolver
So we don't have to worry about network stack changes causes an upstream
to be broken. Just send requests to all nameservers concurrently, and
get the first success response.
2023-03-10 09:25:48 +07:00
Cuong Manh Le 4f6c2032a1 cmd/ctrld: log reason if first query failed 2023-03-10 09:25:42 +07:00
Cuong Manh Le d1589bd9d6 Use separate context when querying upstream ips
While at it, also include query type in log, and only honor upstream
timeout when it greater than zero.
2023-03-10 09:25:35 +07:00
Cuong Manh Le 85c95a6a3a all: set timeout for re-bootstrapping 2023-03-10 09:25:29 +07:00
Cuong Manh Le fa50cd4df4 all: another rework on discovering bootstrap IPs
Instead of re-query DNS record for upstream when re-bootstrapping, just
query all records on startup, then selecting the next bootstrap ip
depends on the current network stack.
2023-03-10 09:25:17 +07:00
Cuong Manh Le 018f6651c1 Fix wrong time precision in bootstrapping timeout
The timeout is in millisecond, not second.
2023-03-08 10:19:49 +07:00
Cuong Manh Le 1a40767cb7 Use upstream timeout when querying bootstrap IP 2023-03-08 10:16:56 +07:00
Cuong Manh Le 12512a60da Always use first record from DNS response 2023-03-07 10:45:29 +07:00
Cuong Manh Le b0114dfaeb cmd/ctrld: make staticcheck happy 2023-03-07 10:28:49 +07:00
Cuong Manh Le fb20d443c1 all: retry the request more agressively
For better recovery and dealing with network stack changes, this commit
change the request flow to:

failure of any kind -> recreate transport/re-bootstrap -> retry once

That would make ctrld recover from all scenarios in theory.
2023-03-07 10:25:48 +07:00
Cuong Manh Le 262dcb1dff cmd/ctrld: check for ipv6 listen local
Since when the machine may not have external ipv6 capability, but still
can do ipv6 network on local network.
2023-03-07 10:25:48 +07:00
Cuong Manh Le 8b08cc8a6e all: rework bootstrap IP discovering
At startup, ctrld gathers bootstrap IP information and use this
bootstrap IP for connecting to upstream. However, in case the network
stack changed, for example, dues to VPN connection, ctrld will still use
this old (maybe invalid) bootstrap IP for the current network stack.

This commit rework the discovering process, and re-initializing the
bootstrap IP if connecting to upstream failed.
2023-03-07 10:25:48 +07:00
Cuong Manh Le 930a5ad439 cmd/ctrld: only set ::1 as DNS server on Windows if ipv6 available 2023-03-07 10:25:48 +07:00
Cuong Manh Le 8852f60ccb Add idle conn timeout for HTTP transport
Allowing the connection to be re-new once it becomes un-usable.
2023-03-07 10:25:48 +07:00
Cuong Manh Le 2e1b3f9d07 Upgrade golang.org/x/net to v0.7.0
For pulling CVE-2022-41723 fix.
2023-03-07 10:25:48 +07:00
Cuong Manh Le 6d3c82d38d internal/dns: add debian/openresolv to linux manager 2023-02-27 21:50:06 +07:00
Cuong Manh Le cad71997aa cmd/ctrld: allocate new ip instead of port
So the alternative listener address can still be used as system
resolver.
2023-02-27 20:50:01 +07:00
Cuong Manh Le 82900eeca6 cmd/ctrld: move log file if existed on app start
Updates #59
2023-02-27 20:43:56 +07:00
Cuong Manh Le 84fca06c62 cmd/ctrld: implement allocate/deallocate ip on freebsd
Updates #56
2023-02-27 20:43:56 +07:00
Cuong Manh Le 64f2dcb25b Fix parsing network service name on darwin
The network service name appears on the previous line, not the same line
with "Device" name.

Updates #57
2023-02-27 20:43:56 +07:00
Cuong Manh Le 4c2d21a8f8 all: add freebsd supports
This commit add support for ctrld to run on freebsd, supported platforms
are amd64/arm64/armv6/armv7,386.

Supporting freebsd also requires adding debian and openresolv resolvconf.

Updates #47
2023-02-27 20:43:56 +07:00
Cuong Manh Le 4172fc09d0 cmd/ctrld: add self check for better error message reported
After telling service manager to start ctrld, performing self check
status by sending DNS query to ctrld listener. So if ctrld could not
start for any reason, an error message will be reported to user instead
of simply telling service started.

Updates #56
2023-02-27 20:43:55 +07:00
Cuong Manh Le d9b699501d cmd/ctrld: merge proxy log to main log
There's no reason to separate those two loggers anymore, and making them
separated may lead to inconsistent logging behavior.

Updates #54
2023-02-27 20:13:44 +07:00
Cuong Manh Le 71b1b324db cmd/ctrld: honor configPath when writing config file
Updates #58
2023-02-27 20:13:44 +07:00
Cuong Manh Le 35c890048b cmd/ctrld: remove prefix main field
While at it, also make init logging with empty log path when running
start command.

Updates #55
2023-02-27 20:13:44 +07:00
Cuong Manh Le bac6810956 cmd/ctrld: fix missing unmarshalling config without --cd
Otherwise, DNS won't be set in non-Linux systems.

Updates #54
2023-02-27 20:13:44 +07:00
Cuong Manh Le 997ec342e0 cmd/ctrld,internal/dns: support systemd-networkd dbus
For interface managed by systemd-networkd, systemd-resolved can not
reset DNS. To fix this, attempting to check before the run loop and set
the suitable manager for the system.

Updates #55
2023-02-27 20:13:44 +07:00
Cuong Manh Le e385547461 internal/net: fix wrong address when testing network up 2023-02-27 20:13:44 +07:00
Cuong Manh Le 83b551fb2d internal/controld: check if ipv4 is available before connect to API
Updates #53
2023-02-27 20:13:42 +07:00
Cuong Manh Le 45f827a2c5 internal/controld: connect to API using ipv4 only
Connecting to API using ipv6 sometimes hang at TLS handshake, using ipv4
only so we can fetch the config more reliably.

Fixed #53
2023-02-27 19:54:52 +07:00
Cuong Manh Le 3218b5fac1 Add quic-free binaries in build pipeline
Updates #51
2023-02-27 19:54:18 +07:00
Cuong Manh Le df514d15a5 Update quic-go to v0.32.0
Updates #51
2023-02-27 19:51:39 +07:00
Cuong Manh Le 50b0e5a4b0 cmd/ctrld: use proper exit codes for status command
While at it, disable sort commands, so help output will be in order.

Updates #48
2023-02-27 19:50:28 +07:00
Yegor S 6428ac23a0 Merge pull request #20 from Control-D-Inc/upx
Add upx to goreleaser builds
2023-02-14 14:09:13 -05:00
Yegor S 790cb773e2 Merge pull request #17 from Control-D-Inc/readme-badge
Add some badges to README.md
2023-02-13 11:52:36 -05:00
Yegor S 9dab097268 Merge pull request #10 from GiddyGoatGaming/patch-1
ci.yml: bump checkout -> v3 and setup-go-faster -> 1.8.0
2023-02-13 11:51:06 -05:00
Cuong Manh Le f13f61592c Add upx to goreleaser builds
Reducing the size of the final binaries, except for darwin, where the
packed binaries failed to run.
2023-02-13 21:58:45 +07:00
Cuong Manh Le 2f42fc055d Add some badges to README.md 2023-02-08 00:53:16 +07:00
Spencer Comfort 1cdce73070 Update ci.yml 2023-01-16 17:23:59 -05:00
Spencer Comfort 5f9ac5889b ci.yml: bump checkout -> 3.3.0 and setup-go-faster -> 1.8.0 2023-01-15 21:56:56 -05:00
95 changed files with 9985 additions and 933 deletions
+4 -4
View File
@@ -9,18 +9,18 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
os: ["windows-latest", "ubuntu-latest", "macOS-latest"] os: ["windows-latest", "ubuntu-latest", "macOS-latest"]
go: ["1.19.x"] go: ["1.20.x"]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v3
with: with:
fetch-depth: 1 fetch-depth: 1
- uses: WillAbides/setup-go-faster@v1.7.0 - uses: WillAbides/setup-go-faster@v1.8.0
with: with:
go-version: ${{ matrix.go }} go-version: ${{ matrix.go }}
- run: "go test -race ./..." - run: "go test -race ./..."
- uses: dominikh/staticcheck-action@v1.2.0 - uses: dominikh/staticcheck-action@v1.2.0
with: with:
version: "2022.1.1" version: "2023.1.2"
install-go: false install-go: false
cache-key: ${{ matrix.go }} cache-key: ${{ matrix.go }}
+3 -1
View File
@@ -1,3 +1,5 @@
dist/ dist/
gon.hcl gon.hcl
/Build
.DS_Store
+2
View File
@@ -9,6 +9,8 @@ builds:
- -trimpath - -trimpath
ldflags: ldflags:
- -s -w - -s -w
- -X main.version={{.Version}}
- -X main.commit={{.Commit}}
goos: goos:
- darwin - darwin
goarch: goarch:
+44
View File
@@ -0,0 +1,44 @@
before:
hooks:
- go mod tidy
builds:
- id: ctrld
env:
- CGO_ENABLED=0
flags:
- -trimpath
ldflags:
- -s -w
- -X main.version={{.Version}}
- -X main.commit={{.Commit}}
goos:
- darwin
- linux
- windows
goarch:
- 386
- arm
- amd64
- arm64
tags:
- qf
main: ./cmd/ctrld
hooks:
post: /bin/sh ./scripts/upx.sh {{ .Path }}
archives:
- format_overrides:
- goos: windows
format: zip
strip_parent_binary_folder: true
wrap_in_directory: true
files:
- README.md
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ incpatch .Version }}-next"
changelog:
sort: asc
filters:
exclude:
- '^docs:'
+10
View File
@@ -9,13 +9,17 @@ builds:
- -trimpath - -trimpath
ldflags: ldflags:
- -s -w - -s -w
- -X main.version={{.Version}}
- -X main.commit={{.Commit}}
goos: goos:
- linux - linux
- freebsd
- windows - windows
goarch: goarch:
- 386 - 386
- arm - arm
- mips - mips
- mipsle
- amd64 - amd64
- arm64 - arm64
goarm: goarm:
@@ -25,6 +29,12 @@ builds:
gomips: gomips:
- softfloat - softfloat
main: ./cmd/ctrld main: ./cmd/ctrld
hooks:
post: /bin/sh ./scripts/upx.sh {{ .Path }}
ignore:
- goos: freebsd
goarch: arm
goarm: 5
archives: archives:
- format_overrides: - format_overrides:
- goos: windows - goos: windows
+81 -20
View File
@@ -1,9 +1,18 @@
# ctrld # ctrld
![Test](https://github.com/Control-D-Inc/ctrld/actions/workflows/ci.yml/badge.svg)
[![Go Reference](https://pkg.go.dev/badge/github.com/Control-D-Inc/ctrld.svg)](https://pkg.go.dev/github.com/Control-D-Inc/ctrld)
[![Go Report Card](https://goreportcard.com/badge/github.com/Control-D-Inc/ctrld)](https://goreportcard.com/report/github.com/Control-D-Inc/ctrld)
A highly configurable DNS forwarding proxy with support for: A highly configurable DNS forwarding proxy with support for:
- Multiple listeners for incoming queries - Multiple listeners for incoming queries
- Multiple upstreams with fallbacks - Multiple upstreams with fallbacks
- Multiple network policy driven DNS query steering - Multiple network policy driven DNS query steering
- Policy driven domain based "split horizon" DNS with wildcard support - Policy driven domain based "split horizon" DNS with wildcard support
- Integrations with common router vendors and firmware
## TLDR
Proxy legacy DNS traffic to secure DNS upstreams in highly configurable ways.
All DNS protocols are supported, including: All DNS protocols are supported, including:
- `UDP 53` - `UDP 53`
@@ -12,23 +21,41 @@ All DNS protocols are supported, including:
- `DNS-over-HTTP/3` (DOH3) - `DNS-over-HTTP/3` (DOH3)
- `DNS-over-QUIC` - `DNS-over-QUIC`
## Use Cases # Use Cases
1. Use secure DNS protocols on networks and devices that don't natively support them (legacy routers, legacy OSes, TVs, smart toasters). 1. Use secure DNS protocols on networks and devices that don't natively support them (legacy routers, legacy OSes, TVs, smart toasters).
2. Create source IP based DNS routing policies with variable secure DNS upstreams. Subnet 1 (admin) uses upstream resolver A, while Subnet 2 (employee) uses upstream resolver B. 2. Create source IP based DNS routing policies with variable secure DNS upstreams. Subnet 1 (admin) uses upstream resolver A, while Subnet 2 (employee) uses upstream resolver B.
3. Create destination IP based DNS routing policies with variable secure DNS upstreams. Listener 1 uses upstream resolver C, while Listener 2 uses upstream resolver D. 3. Create destination IP based DNS routing policies with variable secure DNS upstreams. Listener 1 uses upstream resolver C, while Listener 2 uses upstream resolver D.
4. Create domain level "split horizon" DNS routing policies to send internal domains (*.company.int) to a local DNS server, while everything else goes to another upstream. 4. Create domain level "split horizon" DNS routing policies to send internal domains (*.company.int) to a local DNS server, while everything else goes to another upstream.
5. Deploy on a router and create LAN client specific DNS routing policies from a web GUI (When using ControlD.com).
## OS Support ## OS Support
- Windows (386, amd64, arm) - Windows (386, amd64, arm)
- Mac (amd64, arm64) - Mac (amd64, arm64)
- Linux (386, amd64, arm, mips) - Linux (386, amd64, arm, mips)
- FreeBSD
- Common routers (See Router Mode below)
## Download # Install
Download pre-compiled binaries from the [Releases](https://github.com/Control-D-Inc/ctrld/releases) section. There are several ways to download and install `ctrld.
## Quick Install
The simplest way to download and install `ctrld` is to use the following installer command on any UNIX-like platform:
```shell
sh -c 'sh -c "$(curl -sL https://api.controld.com/dl)"'
```
Windows user and prefer Powershell (who doesn't)? No problem, execute this command instead in administrative cmd:
```shell
powershell -Command "(Invoke-WebRequest -Uri 'https://api.controld.com/dl' -UseBasicParsing).Content | Set-Content 'ctrld_install.bat'" && ctrld_install.bat
```
## Download Manually
Alternatively, if you know what you're doing you can download pre-compiled binaries from the [Releases](https://github.com/Control-D-Inc/ctrld/releases) section for the appropriate platform.
## Build ## Build
`ctrld` requires `go1.19+`: Lastly, you can build `ctrld` from source which requires `go1.19+`:
```shell ```shell
$ go build ./cmd/ctrld $ go build ./cmd/ctrld
@@ -40,6 +67,10 @@ or
$ go install github.com/Control-D-Inc/ctrld/cmd/ctrld@latest $ go install github.com/Control-D-Inc/ctrld/cmd/ctrld@latest
``` ```
# Usage
The cli is self documenting, so free free to run `--help` on any sub-command to get specific usages.
## Arguments ## Arguments
``` ```
__ .__ .___ __ .__ .___
@@ -55,18 +86,29 @@ Usage:
Available Commands: Available Commands:
run Run the DNS proxy server run Run the DNS proxy server
service Manage ctrld service service Manage ctrld service
start Quick start service and configure DNS on default interface start Quick start service and configure DNS on interface
stop Quick stop service and remove DNS from default interface stop Quick stop service and remove DNS from interface
setup Auto-setup Control D on a router.
Supported platforms:
ₒ ddwrt
ₒ merlin
ₒ openwrt
ₒ ubios
ₒ auto - detect the platform you are running on
Flags: Flags:
-h, --help help for ctrld -h, --help help for ctrld
-s, --silent do not write any log output
-v, --verbose count verbose log output, "-v" basic logging, "-vv" debug level logging -v, --verbose count verbose log output, "-v" basic logging, "-vv" debug level logging
--version version for ctrld --version version for ctrld
Use "ctrld [command] --help" for more information about a command. Use "ctrld [command] --help" for more information about a command.
``` ```
## Usage ## Basic Run Mode
To start the server with default configuration, simply run: `./ctrld run`. This will create a generic `ctrld.toml` file in the **working directory** and start the application in foreground. To start the server with default configuration, simply run: `./ctrld run`. This will create a generic `ctrld.toml` file in the **working directory** and start the application in foreground.
1. Start the server 1. Start the server
``` ```
@@ -80,12 +122,12 @@ To start the server with default configuration, simply run: `./ctrld run`. This
147.185.34.1 147.185.34.1
``` ```
If `verify.controld.com` resolves, you're successfully using the default Control D upstream. If `verify.controld.com` resolves, you're successfully using the default Control D upstream. From here, you can start editing the config file and go nuts with it. To enforce a new config, restart the server.
### Service Mode ## Service Mode
To run the application in service mode, simply run: `./ctrld start` as system/root user. This will create a generic `ctrld.toml` file in the **user home** directory, start the system service, and configure the listener on the default interface. Service will start on OS boot. To run the application in service mode on any Windows, MacOS or Linux distibution, simply run: `./ctrld start` as system/root user. This will create a generic `ctrld.toml` file in the **user home** directory (on Windows) or `/etc/controld/` (everywhere else), start the system service, and configure the listener on the default network interface. Service will start on OS boot.
In order to stop the service, and restore your DNS to original state, simply run `./ctrld stop`. In order to stop the service, and restore your DNS to original state, simply run `./ctrld stop`. If you wish to uninstall the service permanently, run `./ctrld service uninstall`.
For granular control of the service, run the `service` command. Each sub-command has its own help section so you can see what arguments you can supply. For granular control of the service, run the `service` command. Each sub-command has its own help section so you can see what arguments you can supply.
@@ -112,8 +154,23 @@ For granular control of the service, run the `service` command. Each sub-command
Use "ctrld service [command] --help" for more information about a command. Use "ctrld service [command] --help" for more information about a command.
``` ```
## Router Mode
You can run `ctrld` on any supported router, which will function similarly to the Service Mode mentioned above. The list of supported routers and firmware includes:
- Asus Merlin
- DD-WRT
- FreshTomato
- GL.iNet
- OpenWRT
- pfSense
- Synology
- Ubiquiti (UniFi, EdgeOS)
In order to start `ctrld` as a DNS provider, simply run `./ctrld setup auto` command.
In this mode, and when Control D upstreams are used, the router will [relay your network topology](https://docs.controld.com/docs/device-clients) to Control D (LAN IPs, MAC addresses, and hostnames), and you will be able to see your LAN devices in the web panel, view analytics and apply unique profiles to them.
### Control D Auto Configuration ### Control D Auto Configuration
Application can be started with a specific resolver config, instead of the default one. Simply supply your resolver ID with a `--cd` flag, when using the `run` (foreground) or `start` (service) modes. Application can be started with a specific resolver config, instead of the default one. Simply supply your Resolver ID with a `--cd` flag, when using the `run` (foreground) or `start` (service) or `setup` (router) modes.
The following command will start the application in foreground mode, using the free "p2" resolver, which blocks Ads & Trackers. The following command will start the application in foreground mode, using the free "p2" resolver, which blocks Ads & Trackers.
@@ -121,22 +178,27 @@ The following command will start the application in foreground mode, using the f
./ctrld run --cd p2 ./ctrld run --cd p2
``` ```
Alternatively, you can use your own personal Control D Device resolver, and start the application in service mode. Your resolver ID is the part after the slash of your DNS-over-HTTPS resolver. ie. https://dns.controld.com/abcd1234 Alternatively, you can use your own personal Control D Device resolver, and start the application in service mode. Your resolver ID is displayed on the "Show Resolvers" screen for the relevant Control D Device.
```shell ```shell
./ctrld start --cd abcd1234 ./ctrld start --cd abcd1234
``` ```
Once you run the above command, the following things will happen: You can do the same while starting in router mode:
```shell
./ctrld setup auto --cd abcd1234
```
Once you run the above commands (in service or router modes only), the following things will happen:
- You resolver configuration will be fetched from the API, and config file templated with the resolver data - You resolver configuration will be fetched from the API, and config file templated with the resolver data
- Application will start as a service, and keep running (even after reboot) until you run the `stop` or `service uninstall` sub-commands - Application will start as a service, and keep running (even after reboot) until you run the `stop` or `service uninstall` sub-commands
- Your default network interface will be updated to use the listener started by the service - Your default network interface will be updated to use the listener started by the service
- All OS DNS queries will be sent to the listener - All OS DNS queries will be sent to the listener
## Configuration # Configuration
See [Configuration Docs](docs/config.md). See [Configuration Docs](docs/config.md).
### Example ## Example
- Start `listener.0` on 127.0.0.1:53 - Start `listener.0` on 127.0.0.1:53
- Accept queries from any source address - Accept queries from any source address
- Send all queries to `upstream.0` via DoH protocol - Send all queries to `upstream.0` via DoH protocol
@@ -178,17 +240,16 @@ See [Configuration Docs](docs/config.md).
``` ```
### Advanced ## Advanced Configuration
The above is the most basic example, which will work out of the box. If you're looking to do advanced configurations using policies, see [Configuration Docs](docs/config.md) for complete documentation of the config file. The above is the most basic example, which will work out of the box. If you're looking to do advanced configurations using policies, see [Configuration Docs](docs/config.md) for complete documentation of the config file.
You can also supply configuration via launch argeuments, in [Ephemeral Mode](docs/ephemeral_mode.md). You can also supply configuration via launch argeuments, in [Ephemeral Mode](docs/ephemeral_mode.md).
## Contributing ## Contributing
See [Contribution Guideline](./docs/contributing.md) See [Contribution Guideline](./docs/contributing.md)
## Roadmap ## Roadmap
The following functionality is on the roadmap and will be available in future releases. The following functionality is on the roadmap and will be available in future releases.
- Router self-installation
- Client hostname/MAC passthrough
- Prometheus metrics exporter - Prometheus metrics exporter
- DNS intercept mode
- Support for more routers (let us know which ones)
+11
View File
@@ -0,0 +1,11 @@
package ctrld
// ClientInfoCtxKey is the context key to store client info.
type ClientInfoCtxKey struct{}
// ClientInfo represents ctrld's clients information.
type ClientInfo struct {
Mac string
IP string
Hostname string
}
+438 -147
View File
@@ -2,10 +2,11 @@ package main
import ( import (
"bytes" "bytes"
"context"
"crypto/x509"
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt" "fmt"
"log"
"net" "net"
"net/netip" "net/netip"
"os" "os"
@@ -14,22 +15,37 @@ import (
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
"sync"
"time"
"github.com/cuonglm/osinfo"
"github.com/fsnotify/fsnotify"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/kardianos/service" "github.com/kardianos/service"
"github.com/miekg/dns"
"github.com/pelletier/go-toml/v2" "github.com/pelletier/go-toml/v2"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
"tailscale.com/logtail/backoff"
"tailscale.com/net/interfaces" "tailscale.com/net/interfaces"
"github.com/Control-D-Inc/ctrld" "github.com/Control-D-Inc/ctrld"
"github.com/Control-D-Inc/ctrld/internal/certs"
"github.com/Control-D-Inc/ctrld/internal/controld" "github.com/Control-D-Inc/ctrld/internal/controld"
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
"github.com/Control-D-Inc/ctrld/internal/router"
)
var (
version = "dev"
commit = "none"
) )
var ( var (
v = viper.NewWithOptions(viper.KeyDelimiter("::")) v = viper.NewWithOptions(viper.KeyDelimiter("::"))
defaultConfigWritten = false defaultConfigWritten = false
defaultConfigFile = "ctrld.toml" defaultConfigFile = "ctrld.toml"
rootCertPool *x509.CertPool
) )
var basicModeFlags = []string{"listen", "primary_upstream", "secondary_upstream", "domains"} var basicModeFlags = []string{"listen", "primary_upstream", "secondary_upstream", "domains"}
@@ -52,22 +68,44 @@ _/ ___\ __\_ __ \ | / __ |
\/ dns forwarding proxy \/ \/ dns forwarding proxy \/
` `
var rootCmd = &cobra.Command{
Use: "ctrld",
Short: strings.TrimLeft(rootShortDesc, "\n"),
Version: curVersion(),
PreRun: func(cmd *cobra.Command, args []string) {
initConsoleLogging()
},
}
func curVersion() string {
if version != "dev" && !strings.HasPrefix(version, "v") {
version = "v" + version
}
if len(commit) > 7 {
commit = commit[:7]
}
return fmt.Sprintf("%s-%s", version, commit)
}
func initCLI() { func initCLI() {
// Enable opening via explorer.exe on Windows. // Enable opening via explorer.exe on Windows.
// See: https://github.com/spf13/cobra/issues/844. // See: https://github.com/spf13/cobra/issues/844.
cobra.MousetrapHelpText = "" cobra.MousetrapHelpText = ""
cobra.EnableCommandSorting = false
rootCmd := &cobra.Command{
Use: "ctrld",
Short: strings.TrimLeft(rootShortDesc, "\n"),
Version: "1.1.0",
}
rootCmd.PersistentFlags().CountVarP( rootCmd.PersistentFlags().CountVarP(
&verbose, &verbose,
"verbose", "verbose",
"v", "v",
`verbose log output, "-v" basic logging, "-vv" debug level logging`, `verbose log output, "-v" basic logging, "-vv" debug level logging`,
) )
rootCmd.PersistentFlags().BoolVarP(
&silent,
"silent",
"s",
false,
`do not write any log output`,
)
rootCmd.SetHelpCommand(&cobra.Command{Hidden: true}) rootCmd.SetHelpCommand(&cobra.Command{Hidden: true})
rootCmd.CompletionOptions.HiddenDefaultCmd = true rootCmd.CompletionOptions.HiddenDefaultCmd = true
@@ -75,45 +113,70 @@ func initCLI() {
Use: "run", Use: "run",
Short: "Run the DNS proxy server", Short: "Run the DNS proxy server",
Args: cobra.NoArgs, Args: cobra.NoArgs,
PreRun: func(cmd *cobra.Command, args []string) {
initConsoleLogging()
},
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
if daemon && runtime.GOOS == "windows" { if daemon && runtime.GOOS == "windows" {
log.Fatal("Cannot run in daemon mode. Please install a Windows service.") mainLog.Fatal().Msg("Cannot run in daemon mode. Please install a Windows service.")
} }
waitCh := make(chan struct{})
stopCh := make(chan struct{})
if !daemon {
// We need to call s.Run() as soon as possible to response to the OS manager, so it
// can see ctrld is running and don't mark ctrld as failed service.
go func() {
p := &prog{
waitCh: waitCh,
stopCh: stopCh,
}
s, err := service.New(p, svcConfig)
if err != nil {
mainLog.Fatal().Err(err).Msg("failed create new service")
}
s = newService(s)
if err := s.Run(); err != nil {
mainLog.Error().Err(err).Msg("failed to start service")
}
}()
}
noConfigStart := isNoConfigStart(cmd) noConfigStart := isNoConfigStart(cmd)
writeDefaultConfig := !noConfigStart && configBase64 == "" writeDefaultConfig := !noConfigStart && configBase64 == ""
configs := []struct { tryReadingConfig(writeDefaultConfig)
name string
written bool
}{
// For compatibility, we check for config.toml first, but only read it if exists.
{"config", false},
{"ctrld", writeDefaultConfig},
}
for _, config := range configs {
ctrld.SetConfigName(v, config.name)
v.SetConfigFile(configPath)
if readConfigFile(config.written) {
break
}
}
readBase64Config() readBase64Config(configBase64)
processNoConfigFlags(noConfigStart) processNoConfigFlags(noConfigStart)
if err := v.Unmarshal(&cfg); err != nil { if err := v.Unmarshal(&cfg); err != nil {
log.Fatalf("failed to unmarshal config: %v", err) mainLog.Fatal().Msgf("failed to unmarshal config: %v", err)
} }
mainLog.Info().Msgf("starting ctrld %s", curVersion())
oi := osinfo.New()
mainLog.Info().Msgf("os: %s", oi.String())
// Wait for network up. // Wait for network up.
if !netUp() { if !ctrldnet.Up() {
log.Fatal("network is not up yet") mainLog.Fatal().Msg("network is not up yet")
} }
processLogAndCacheFlags() processLogAndCacheFlags()
// Log config do not have thing to validate, so it's safe to init log here, // Log config do not have thing to validate, so it's safe to init log here,
// so it's able to log information in processCDFlags. // so it's able to log information in processCDFlags.
initLogging() initLogging()
if setupRouter {
s, errCh := runDNSServerForNTPD(router.ListenAddress())
if err := router.PreRun(); err != nil {
mainLog.Fatal().Err(err).Msg("failed to perform router pre-start check")
}
if err := s.Shutdown(); err != nil && errCh != nil {
mainLog.Fatal().Err(err).Msg("failed to shutdown dns server for ntpd")
}
}
processCDFlags() processCDFlags()
if err := ctrld.ValidateConfig(validator.New(), &cfg); err != nil { if err := ctrld.ValidateConfig(validator.New(), &cfg); err != nil {
log.Fatalf("invalid config: %v", err) mainLog.Fatal().Msgf("invalid config: %v", err)
} }
initCache() initCache()
@@ -139,22 +202,26 @@ func initCLI() {
os.Exit(0) os.Exit(0)
} }
s, err := service.New(&prog{}, svcConfig) if setupRouter {
if err != nil { switch platform := router.Name(); {
mainLog.Fatal().Err(err).Msg("failed create new service") case platform == router.DDWrt:
} rootCertPool = certs.CACertPool()
serviceLogger, err := s.Logger(nil) fallthrough
if err != nil { case platform != "":
mainLog.Error().Err(err).Msg("failed to get service logger") mainLog.Debug().Msg("Router setup")
return err := router.Configure(&cfg)
if errors.Is(err, router.ErrNotSupported) {
unsupportedPlatformHelp(cmd)
os.Exit(1)
}
if err != nil {
mainLog.Fatal().Err(err).Msg("failed to configure router")
}
}
} }
if err := s.Run(); err != nil { close(waitCh)
if sErr := serviceLogger.Error(err); sErr != nil { <-stopCh
mainLog.Error().Err(sErr).Msg("failed to write service log")
}
mainLog.Error().Err(err).Msg("failed to start service")
}
}, },
} }
runCmd.Flags().BoolVarP(&daemon, "daemon", "d", false, "Run as daemon") runCmd.Flags().BoolVarP(&daemon, "daemon", "d", false, "Run as daemon")
@@ -167,18 +234,25 @@ func initCLI() {
runCmd.Flags().StringVarP(&logPath, "log", "", "", "Path to log file") runCmd.Flags().StringVarP(&logPath, "log", "", "", "Path to log file")
runCmd.Flags().IntVarP(&cacheSize, "cache_size", "", 0, "Enable cache with size items") runCmd.Flags().IntVarP(&cacheSize, "cache_size", "", 0, "Enable cache with size items")
runCmd.Flags().StringVarP(&cdUID, "cd", "", "", "Control D resolver uid") runCmd.Flags().StringVarP(&cdUID, "cd", "", "", "Control D resolver uid")
runCmd.Flags().BoolVarP(&cdDev, "dev", "", false, "Use Control D dev resolver/domain")
_ = runCmd.Flags().MarkHidden("dev")
runCmd.Flags().StringVarP(&homedir, "homedir", "", "", "") runCmd.Flags().StringVarP(&homedir, "homedir", "", "", "")
_ = runCmd.Flags().MarkHidden("homedir") _ = runCmd.Flags().MarkHidden("homedir")
runCmd.Flags().StringVarP(&iface, "iface", "", "", `Update DNS setting for iface, "auto" means the default interface gateway`) runCmd.Flags().StringVarP(&iface, "iface", "", "", `Update DNS setting for iface, "auto" means the default interface gateway`)
_ = runCmd.Flags().MarkHidden("iface") _ = runCmd.Flags().MarkHidden("iface")
runCmd.Flags().BoolVarP(&setupRouter, "router", "", false, `setup for running on router platforms`)
_ = runCmd.Flags().MarkHidden("router")
rootCmd.AddCommand(runCmd) rootCmd.AddCommand(runCmd)
startCmd := &cobra.Command{ startCmd := &cobra.Command{
PreRun: checkHasElevatedPrivilege, PreRun: func(cmd *cobra.Command, args []string) {
Use: "start", initConsoleLogging()
Short: "Start the ctrld service", checkHasElevatedPrivilege()
Args: cobra.NoArgs, },
Use: "start",
Short: "Install and start the ctrld service",
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
sc := &service.Config{} sc := &service.Config{}
*sc = *svcConfig *sc = *svcConfig
@@ -188,35 +262,55 @@ func initCLI() {
} }
setDependencies(sc) setDependencies(sc)
sc.Arguments = append([]string{"run"}, osArgs...) sc.Arguments = append([]string{"run"}, osArgs...)
if dir, err := os.UserHomeDir(); err == nil { if err := router.ConfigureService(sc); err != nil {
// WorkingDirectory is not supported on Windows. mainLog.Fatal().Err(err).Msg("failed to configure service on router")
sc.WorkingDirectory = dir }
// No config path, generating config in HOME directory.
noConfigStart := isNoConfigStart(cmd) // No config path, generating config in HOME directory.
writeDefaultConfig := !noConfigStart && configBase64 == "" noConfigStart := isNoConfigStart(cmd)
writeDefaultConfig := !noConfigStart && configBase64 == ""
if configPath != "" {
v.SetConfigFile(configPath)
}
if dir, err := userHomeDir(); err == nil {
setWorkingDirectory(sc, dir)
if configPath == "" && writeDefaultConfig { if configPath == "" && writeDefaultConfig {
defaultConfigFile = filepath.Join(dir, defaultConfigFile) defaultConfigFile = filepath.Join(dir, defaultConfigFile)
readConfigFile(writeDefaultConfig && cdUID == "")
} }
sc.Arguments = append(sc.Arguments, "--homedir="+dir) sc.Arguments = append(sc.Arguments, "--homedir="+dir)
} }
tryReadingConfig(writeDefaultConfig)
if err := v.Unmarshal(&cfg); err != nil {
mainLog.Fatal().Msgf("failed to unmarshal config: %v", err)
}
logPath := cfg.Service.LogPath
cfg.Service.LogPath = ""
initLogging() initLogging()
cfg.Service.LogPath = logPath
processCDFlags() processCDFlags()
// On Windows, the service will be run as SYSTEM, so if ctrld start as Admin,
// the user home dir is different, so pass specific arguments that relevant here. if err := ctrld.ValidateConfig(validator.New(), &cfg); err != nil {
if runtime.GOOS == "windows" { mainLog.Fatal().Msgf("invalid config: %v", err)
if configPath == "" { }
sc.Arguments = append(sc.Arguments, "--config="+defaultConfigFile)
} // Explicitly passing config, so on system where home directory could not be obtained,
// or sub-process env is different with the parent, we still behave correctly and use
// the expected config file.
if configPath == "" {
sc.Arguments = append(sc.Arguments, "--config="+defaultConfigFile)
} }
prog := &prog{} prog := &prog{}
s, err := service.New(prog, sc) s, err := service.New(prog, sc)
if err != nil { if err != nil {
stderrMsg(err.Error()) mainLog.Error().Msg(err.Error())
return return
} }
s = newService(s)
tasks := []task{ tasks := []task{
{s.Stop, false}, {s.Stop, false},
{s.Uninstall, false}, {s.Uninstall, false},
@@ -224,8 +318,29 @@ func initCLI() {
{s.Start, true}, {s.Start, true},
} }
if doTasks(tasks) { if doTasks(tasks) {
if err := router.PostInstall(svcConfig); err != nil {
mainLog.Warn().Err(err).Msg("post installation failed, please check system/service log for details error")
return
}
status, err := serviceStatus(s)
if err != nil {
mainLog.Warn().Err(err).Msg("could not get service status")
return
}
domain := cfg.Upstream["0"].VerifyDomain()
status = selfCheckStatus(status, domain)
switch status {
case service.StatusRunning:
mainLog.Notice().Msg("Service started")
default:
mainLog.Error().Msg("Service did not start, please check system/service log for details error")
if runtime.GOOS == "linux" {
prog.resetDNS()
}
os.Exit(1)
}
prog.setDNS() prog.setDNS()
mainLog.Info().Msg("Service started")
} }
}, },
} }
@@ -239,43 +354,55 @@ func initCLI() {
startCmd.Flags().StringVarP(&logPath, "log", "", "", "Path to log file") startCmd.Flags().StringVarP(&logPath, "log", "", "", "Path to log file")
startCmd.Flags().IntVarP(&cacheSize, "cache_size", "", 0, "Enable cache with size items") startCmd.Flags().IntVarP(&cacheSize, "cache_size", "", 0, "Enable cache with size items")
startCmd.Flags().StringVarP(&cdUID, "cd", "", "", "Control D resolver uid") startCmd.Flags().StringVarP(&cdUID, "cd", "", "", "Control D resolver uid")
startCmd.Flags().BoolVarP(&cdDev, "dev", "", false, "Use Control D dev resolver/domain")
_ = startCmd.Flags().MarkHidden("dev")
startCmd.Flags().StringVarP(&iface, "iface", "", "", `Update DNS setting for iface, "auto" means the default interface gateway`) startCmd.Flags().StringVarP(&iface, "iface", "", "", `Update DNS setting for iface, "auto" means the default interface gateway`)
startCmd.Flags().BoolVarP(&setupRouter, "router", "", false, `setup for running on router platforms`)
_ = startCmd.Flags().MarkHidden("router")
stopCmd := &cobra.Command{ stopCmd := &cobra.Command{
PreRun: checkHasElevatedPrivilege, PreRun: func(cmd *cobra.Command, args []string) {
Use: "stop", initConsoleLogging()
Short: "Stop the ctrld service", checkHasElevatedPrivilege()
Args: cobra.NoArgs, },
Use: "stop",
Short: "Stop the ctrld service",
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
prog := &prog{} prog := &prog{}
s, err := service.New(prog, svcConfig) s, err := service.New(prog, svcConfig)
if err != nil { if err != nil {
stderrMsg(err.Error()) mainLog.Error().Msg(err.Error())
return return
} }
s = newService(s)
initLogging() initLogging()
if doTasks([]task{{s.Stop, true}}) { if doTasks([]task{{s.Stop, true}}) {
prog.resetDNS() prog.resetDNS()
mainLog.Info().Msg("Service stopped") mainLog.Notice().Msg("Service stopped")
} }
}, },
} }
stopCmd.Flags().StringVarP(&iface, "iface", "", "", `Reset DNS setting for iface, "auto" means the default interface gateway`) stopCmd.Flags().StringVarP(&iface, "iface", "", "", `Reset DNS setting for iface, "auto" means the default interface gateway`)
restartCmd := &cobra.Command{ restartCmd := &cobra.Command{
PreRun: checkHasElevatedPrivilege, PreRun: func(cmd *cobra.Command, args []string) {
Use: "restart", initConsoleLogging()
Short: "Restart the ctrld service", checkHasElevatedPrivilege()
Args: cobra.NoArgs, },
Use: "restart",
Short: "Restart the ctrld service",
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
s, err := service.New(&prog{}, svcConfig) s, err := service.New(&prog{}, svcConfig)
if err != nil { if err != nil {
stderrMsg(err.Error()) mainLog.Error().Msg(err.Error())
return return
} }
s = newService(s)
initLogging() initLogging()
if doTasks([]task{{s.Restart, true}}) { if doTasks([]task{{s.Restart, true}}) {
stdoutMsg("Service restarted") mainLog.Notice().Msg("Service restarted")
} }
}, },
} }
@@ -284,38 +411,58 @@ func initCLI() {
Use: "status", Use: "status",
Short: "Show status of the ctrld service", Short: "Show status of the ctrld service",
Args: cobra.NoArgs, Args: cobra.NoArgs,
PreRun: func(cmd *cobra.Command, args []string) {
initConsoleLogging()
},
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
s, err := service.New(&prog{}, svcConfig) s, err := service.New(&prog{}, svcConfig)
if err != nil { if err != nil {
stderrMsg(err.Error()) mainLog.Error().Msg(err.Error())
return return
} }
status, err := s.Status() s = newService(s)
status, err := serviceStatus(s)
if err != nil { if err != nil {
stderrMsg(err.Error()) mainLog.Error().Msg(err.Error())
return os.Exit(1)
} }
switch status { switch status {
case service.StatusUnknown: case service.StatusUnknown:
stdoutMsg("Unknown status") mainLog.Notice().Msg("Unknown status")
os.Exit(2)
case service.StatusRunning: case service.StatusRunning:
stdoutMsg("Service is running") mainLog.Notice().Msg("Service is running")
os.Exit(0)
case service.StatusStopped: case service.StatusStopped:
stdoutMsg("Service is stopped") mainLog.Notice().Msg("Service is stopped")
os.Exit(1)
} }
}, },
} }
if runtime.GOOS == "darwin" {
// On darwin, running status command without privileges may return wrong information.
statusCmd.PreRun = func(cmd *cobra.Command, args []string) {
initConsoleLogging()
checkHasElevatedPrivilege()
}
}
uninstallCmd := &cobra.Command{ uninstallCmd := &cobra.Command{
PreRun: checkHasElevatedPrivilege, PreRun: func(cmd *cobra.Command, args []string) {
Use: "uninstall", initConsoleLogging()
Short: "Uninstall the ctrld service", checkHasElevatedPrivilege()
Args: cobra.NoArgs, },
Use: "uninstall",
Short: "Stop and uninstall the ctrld service",
Long: `Stop and uninstall the ctrld service.
NOTE: Uninstalling will set DNS to values provided by DHCP.`,
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
prog := &prog{} prog := &prog{}
s, err := service.New(prog, svcConfig) s, err := service.New(prog, svcConfig)
if err != nil { if err != nil {
stderrMsg(err.Error()) mainLog.Error().Msg(err.Error())
return return
} }
tasks := []task{ tasks := []task{
@@ -324,18 +471,28 @@ func initCLI() {
} }
initLogging() initLogging()
if doTasks(tasks) { if doTasks(tasks) {
if iface == "" {
iface = "auto"
}
prog.resetDNS() prog.resetDNS()
mainLog.Info().Msg("Service uninstalled") mainLog.Debug().Msg("Router cleanup")
if err := router.Cleanup(svcConfig); err != nil {
mainLog.Warn().Err(err).Msg("could not cleanup router")
}
mainLog.Notice().Msg("Service uninstalled")
return return
} }
}, },
} }
uninstallCmd.Flags().StringVarP(&iface, "iface", "", "auto", `Reset DNS setting for iface, "auto" means the default interface gateway`) uninstallCmd.Flags().StringVarP(&iface, "iface", "", "", `Reset DNS setting for iface, use "auto" for the default gateway interface`)
listIfacesCmd := &cobra.Command{ listIfacesCmd := &cobra.Command{
Use: "list", Use: "list",
Short: "List network interfaces of the host", Short: "List network interfaces of the host",
Args: cobra.NoArgs, Args: cobra.NoArgs,
PreRun: func(cmd *cobra.Command, args []string) {
initConsoleLogging()
},
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
err := interfaces.ForeachInterface(func(i interfaces.Interface, prefixes []netip.Prefix) { err := interfaces.ForeachInterface(func(i interfaces.Interface, prefixes []netip.Prefix) {
fmt.Printf("Index : %d\n", i.Index) fmt.Printf("Index : %d\n", i.Index)
@@ -358,7 +515,7 @@ func initCLI() {
println() println()
}) })
if err != nil { if err != nil {
stderrMsg(err.Error()) mainLog.Error().Msg(err.Error())
} }
}, },
} }
@@ -393,9 +550,12 @@ func initCLI() {
serviceCmd.AddCommand(interfacesCmd) serviceCmd.AddCommand(interfacesCmd)
rootCmd.AddCommand(serviceCmd) rootCmd.AddCommand(serviceCmd)
startCmdAlias := &cobra.Command{ startCmdAlias := &cobra.Command{
PreRun: checkHasElevatedPrivilege, PreRun: func(cmd *cobra.Command, args []string) {
Use: "start", initConsoleLogging()
Short: "Quick start service and configure DNS on interface", checkHasElevatedPrivilege()
},
Use: "start",
Short: "Quick start service and configure DNS on interface",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
if !cmd.Flags().Changed("iface") { if !cmd.Flags().Changed("iface") {
os.Args = append(os.Args, "--iface="+ifaceStartStop) os.Args = append(os.Args, "--iface="+ifaceStartStop)
@@ -408,9 +568,12 @@ func initCLI() {
startCmdAlias.Flags().AddFlagSet(startCmd.Flags()) startCmdAlias.Flags().AddFlagSet(startCmd.Flags())
rootCmd.AddCommand(startCmdAlias) rootCmd.AddCommand(startCmdAlias)
stopCmdAlias := &cobra.Command{ stopCmdAlias := &cobra.Command{
PreRun: checkHasElevatedPrivilege, PreRun: func(cmd *cobra.Command, args []string) {
Use: "stop", initConsoleLogging()
Short: "Quick stop service and remove DNS from interface", checkHasElevatedPrivilege()
},
Use: "stop",
Short: "Quick stop service and remove DNS from interface",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
if !cmd.Flags().Changed("iface") { if !cmd.Flags().Changed("iface") {
os.Args = append(os.Args, "--iface="+ifaceStartStop) os.Args = append(os.Args, "--iface="+ifaceStartStop)
@@ -422,16 +585,13 @@ func initCLI() {
stopCmdAlias.Flags().StringVarP(&ifaceStartStop, "iface", "", "auto", `Reset DNS setting for iface, "auto" means the default interface gateway`) stopCmdAlias.Flags().StringVarP(&ifaceStartStop, "iface", "", "auto", `Reset DNS setting for iface, "auto" means the default interface gateway`)
stopCmdAlias.Flags().AddFlagSet(stopCmd.Flags()) stopCmdAlias.Flags().AddFlagSet(stopCmd.Flags())
rootCmd.AddCommand(stopCmdAlias) rootCmd.AddCommand(stopCmdAlias)
if err := rootCmd.Execute(); err != nil {
stderrMsg(err.Error())
os.Exit(1)
}
} }
func writeConfigFile() error { func writeConfigFile() error {
if cfu := v.ConfigFileUsed(); cfu != "" { if cfu := v.ConfigFileUsed(); cfu != "" {
defaultConfigFile = cfu defaultConfigFile = cfu
} else if configPath != "" {
defaultConfigFile = configPath
} }
f, err := os.OpenFile(defaultConfigFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, os.FileMode(0o644)) f, err := os.OpenFile(defaultConfigFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, os.FileMode(0o644))
if err != nil { if err != nil {
@@ -444,7 +604,7 @@ func writeConfigFile() error {
} }
} }
enc := toml.NewEncoder(f).SetIndentTables(true) enc := toml.NewEncoder(f).SetIndentTables(true)
if err := enc.Encode(v.AllSettings()); err != nil { if err := enc.Encode(&cfg); err != nil {
return err return err
} }
if err := f.Close(); err != nil { if err := f.Close(); err != nil {
@@ -457,7 +617,7 @@ func readConfigFile(writeDefaultConfig bool) bool {
// If err == nil, there's a config supplied via `--config`, no default config written. // If err == nil, there's a config supplied via `--config`, no default config written.
err := v.ReadInConfig() err := v.ReadInConfig()
if err == nil { if err == nil {
fmt.Println("loading config file from:", v.ConfigFileUsed()) mainLog.Info().Msg("loading config file from: " + v.ConfigFileUsed())
defaultConfigFile = v.ConfigFileUsed() defaultConfigFile = v.ConfigFileUsed()
return true return true
} }
@@ -468,29 +628,36 @@ func readConfigFile(writeDefaultConfig bool) bool {
// If error is viper.ConfigFileNotFoundError, write default config. // If error is viper.ConfigFileNotFoundError, write default config.
if _, ok := err.(viper.ConfigFileNotFoundError); ok { if _, ok := err.(viper.ConfigFileNotFoundError); ok {
if err := v.Unmarshal(&cfg); err != nil {
mainLog.Fatal().Msgf("failed to unmarshal default config: %v", err)
}
if err := writeConfigFile(); err != nil { if err := writeConfigFile(); err != nil {
log.Fatalf("failed to write default config file: %v", err) mainLog.Fatal().Msgf("failed to write default config file: %v", err)
} else { } else {
fmt.Println("writing default config file to: " + defaultConfigFile) fp, err := filepath.Abs(defaultConfigFile)
if err != nil {
mainLog.Fatal().Msgf("failed to get default config file path: %v", err)
}
mainLog.Info().Msg("writing default config file to: " + fp)
} }
defaultConfigWritten = true defaultConfigWritten = true
return false return false
} }
// Otherwise, report fatal error and exit. // Otherwise, report fatal error and exit.
log.Fatalf("failed to decode config file: %v", err) mainLog.Fatal().Msgf("failed to decode config file: %v", err)
return false return false
} }
func readBase64Config() { func readBase64Config(configBase64 string) {
if configBase64 == "" { if configBase64 == "" {
return return
} }
configStr, err := base64.StdEncoding.DecodeString(configBase64) configStr, err := base64.StdEncoding.DecodeString(configBase64)
if err != nil { if err != nil {
log.Fatalf("invalid base64 config: %v", err) mainLog.Fatal().Msgf("invalid base64 config: %v", err)
} }
if err := v.ReadConfig(bytes.NewReader(configStr)); err != nil { if err := v.ReadConfig(bytes.NewReader(configStr)); err != nil {
log.Fatalf("failed to read base64 config: %v", err) mainLog.Fatal().Msgf("failed to read base64 config: %v", err)
} }
} }
@@ -499,22 +666,30 @@ func processNoConfigFlags(noConfigStart bool) {
return return
} }
if listenAddress == "" || primaryUpstream == "" { if listenAddress == "" || primaryUpstream == "" {
log.Fatal(`"listen" and "primary_upstream" flags must be set in no config mode`) mainLog.Fatal().Msg(`"listen" and "primary_upstream" flags must be set in no config mode`)
} }
processListenFlag() processListenFlag()
endpointAndTyp := func(endpoint string) (string, string) {
typ := ctrld.ResolverTypeFromEndpoint(endpoint)
return strings.TrimPrefix(endpoint, "quic://"), typ
}
pEndpoint, pType := endpointAndTyp(primaryUpstream)
upstream := map[string]*ctrld.UpstreamConfig{ upstream := map[string]*ctrld.UpstreamConfig{
"0": { "0": {
Name: primaryUpstream, Name: pEndpoint,
Endpoint: primaryUpstream, Endpoint: pEndpoint,
Type: ctrld.ResolverTypeDOH, Type: pType,
Timeout: 5000,
}, },
} }
if secondaryUpstream != "" { if secondaryUpstream != "" {
sEndpoint, sType := endpointAndTyp(secondaryUpstream)
upstream["1"] = &ctrld.UpstreamConfig{ upstream["1"] = &ctrld.UpstreamConfig{
Name: secondaryUpstream, Name: sEndpoint,
Endpoint: secondaryUpstream, Endpoint: sEndpoint,
Type: ctrld.ResolverTypeLegacy, Type: sType,
Timeout: 5000,
} }
rules := make([]ctrld.Rule, 0, len(domains)) rules := make([]ctrld.Rule, 0, len(domains))
for _, domain := range domains { for _, domain := range domains {
@@ -534,8 +709,8 @@ func processCDFlags() {
iface = "auto" iface = "auto"
} }
logger := mainLog.With().Str("mode", "cd").Logger() logger := mainLog.With().Str("mode", "cd").Logger()
logger.Info().Msg("fetching Controld-D configuration") logger.Info().Msgf("fetching Controld D configuration from API: %s", cdUID)
resolverConfig, err := controld.FetchResolverConfig(cdUID) resolverConfig, err := controld.FetchResolverConfig(cdUID, rootCmd.Version, cdDev)
if uer, ok := err.(*controld.UtilityErrorResponse); ok && uer.ErrorField.Code == controld.InvalidConfigCode { if uer, ok := err.(*controld.UtilityErrorResponse); ok && uer.ErrorField.Code == controld.InvalidConfigCode {
s, err := service.New(&prog{}, svcConfig) s, err := service.New(&prog{}, svcConfig)
if err != nil { if err != nil {
@@ -567,38 +742,57 @@ func processCDFlags() {
return return
} }
logger.Info().Msg("generating ctrld config from Controld-D configuration") logger.Info().Msg("generating ctrld config from Control-D configuration")
cfg = ctrld.Config{} if resolverConfig.Ctrld.CustomConfig != "" {
cfg.Network = make(map[string]*ctrld.NetworkConfig) logger.Info().Msg("using defined custom config of Control-D resolver")
cfg.Network["0"] = &ctrld.NetworkConfig{ readBase64Config(resolverConfig.Ctrld.CustomConfig)
Name: "Network 0", if err := v.Unmarshal(&cfg); err != nil {
Cidrs: []string{"0.0.0.0/0"}, mainLog.Fatal().Msgf("failed to unmarshal config: %v", err)
} }
cfg.Upstream = make(map[string]*ctrld.UpstreamConfig) for _, listener := range cfg.Listener {
cfg.Upstream["0"] = &ctrld.UpstreamConfig{ if listener.IP == "" {
Endpoint: resolverConfig.DOH, listener.IP = randomLocalIP()
Type: ctrld.ResolverTypeDOH, }
Timeout: 5000, if listener.Port == 0 {
} listener.Port = 53
rules := make([]ctrld.Rule, 0, len(resolverConfig.Exclude)) }
for _, domain := range resolverConfig.Exclude { }
rules = append(rules, ctrld.Rule{domain: []string{}}) // On router, we want to keep the listener address point to dnsmasq listener, aka 127.0.0.1:53.
} if router.Name() != "" {
cfg.Listener = make(map[string]*ctrld.ListenerConfig) if lc := cfg.Listener["0"]; lc != nil {
cfg.Listener["0"] = &ctrld.ListenerConfig{ lc.IP = "127.0.0.1"
IP: "127.0.0.1", lc.Port = 53
Port: 53, }
Policy: &ctrld.ListenerPolicyConfig{ }
Name: "My Policy", } else {
Rules: rules, cfg = ctrld.Config{}
}, cfg.Network = make(map[string]*ctrld.NetworkConfig)
cfg.Network["0"] = &ctrld.NetworkConfig{
Name: "Network 0",
Cidrs: []string{"0.0.0.0/0"},
}
cfg.Upstream = make(map[string]*ctrld.UpstreamConfig)
cfg.Upstream["0"] = &ctrld.UpstreamConfig{
Endpoint: resolverConfig.DOH,
Type: ctrld.ResolverTypeDOH,
Timeout: 5000,
}
rules := make([]ctrld.Rule, 0, len(resolverConfig.Exclude))
for _, domain := range resolverConfig.Exclude {
rules = append(rules, ctrld.Rule{domain: []string{}})
}
cfg.Listener = make(map[string]*ctrld.ListenerConfig)
cfg.Listener["0"] = &ctrld.ListenerConfig{
IP: "127.0.0.1",
Port: 53,
Policy: &ctrld.ListenerPolicyConfig{
Name: "My Policy",
Rules: rules,
},
}
processLogAndCacheFlags()
} }
v = viper.NewWithOptions(viper.KeyDelimiter("::"))
v.Set("network", cfg.Network)
v.Set("upstream", cfg.Upstream)
v.Set("listener", cfg.Listener)
processLogAndCacheFlags()
if err := writeConfigFile(); err != nil { if err := writeConfigFile(); err != nil {
logger.Fatal().Err(err).Msg("failed to write config file") logger.Fatal().Err(err).Msg("failed to write config file")
} else { } else {
@@ -612,11 +806,11 @@ func processListenFlag() {
} }
host, portStr, err := net.SplitHostPort(listenAddress) host, portStr, err := net.SplitHostPort(listenAddress)
if err != nil { if err != nil {
log.Fatalf("invalid listener address: %v", err) mainLog.Fatal().Msgf("invalid listener address: %v", err)
} }
port, err := strconv.Atoi(portStr) port, err := strconv.Atoi(portStr)
if err != nil { if err != nil {
log.Fatalf("invalid port number: %v", err) mainLog.Fatal().Msgf("invalid port number: %v", err)
} }
lc := &ctrld.ListenerConfig{ lc := &ctrld.ListenerConfig{
IP: host, IP: host,
@@ -662,7 +856,104 @@ func netInterface(ifaceName string) (*net.Interface, error) {
func defaultIfaceName() string { func defaultIfaceName() string {
dri, err := interfaces.DefaultRouteInterface() dri, err := interfaces.DefaultRouteInterface()
if err != nil { if err != nil {
// On WSL 1, the route table does not have any default route. But the fact that
// it only uses /etc/resolv.conf for setup DNS, so we can use "lo" here.
if oi := osinfo.New(); strings.Contains(oi.String(), "Microsoft") {
return "lo"
}
mainLog.Fatal().Err(err).Msg("failed to get default route interface") mainLog.Fatal().Err(err).Msg("failed to get default route interface")
} }
return dri return dri
} }
func selfCheckStatus(status service.Status, domain string) service.Status {
if domain == "" {
// Nothing to do, return the status as-is.
return status
}
c := new(dns.Client)
bo := backoff.NewBackoff("self-check", logf, 10*time.Second)
bo.LogLongerThan = 500 * time.Millisecond
ctx := context.Background()
maxAttempts := 20
mainLog.Debug().Msg("Performing self-check")
var (
lcChanged map[string]*ctrld.ListenerConfig
mu sync.Mutex
)
v.OnConfigChange(func(in fsnotify.Event) {
mu.Lock()
defer mu.Unlock()
if err := v.UnmarshalKey("listener", &lcChanged); err != nil {
mainLog.Error().Msgf("failed to unmarshal listener config: %v", err)
return
}
})
v.WatchConfig()
for i := 0; i < maxAttempts; i++ {
lc := cfg.Listener["0"]
mu.Lock()
if lcChanged != nil {
lc = lcChanged["0"]
}
mu.Unlock()
m := new(dns.Msg)
m.SetQuestion(domain+".", dns.TypeA)
m.RecursionDesired = true
r, _, err := c.ExchangeContext(ctx, m, net.JoinHostPort(lc.IP, strconv.Itoa(lc.Port)))
if r != nil && r.Rcode == dns.RcodeSuccess && len(r.Answer) > 0 {
mainLog.Debug().Msgf("self-check against %q succeeded", domain)
return status
}
bo.BackOff(ctx, fmt.Errorf("ExchangeContext: %w", err))
}
mainLog.Debug().Msgf("self-check against %q failed", domain)
return service.StatusUnknown
}
func unsupportedPlatformHelp(cmd *cobra.Command) {
mainLog.Error().Msg("Unsupported or incorrectly chosen router platform. Please open an issue and provide all relevant information: https://github.com/Control-D-Inc/ctrld/issues/new")
}
func userHomeDir() (string, error) {
switch router.Name() {
case router.DDWrt, router.Merlin, router.Tomato:
exe, err := os.Executable()
if err != nil {
return "", err
}
return filepath.Dir(exe), nil
}
// viper will expand for us.
if runtime.GOOS == "windows" {
return os.UserHomeDir()
}
dir := "/etc/controld"
if err := os.MkdirAll(dir, 0750); err != nil {
return "", err
}
return dir, nil
}
func tryReadingConfig(writeDefaultConfig bool) {
configs := []struct {
name string
written bool
}{
// For compatibility, we check for config.toml first, but only read it if exists.
{"config", false},
{"ctrld", writeDefaultConfig},
}
dir, err := userHomeDir()
if err != nil {
mainLog.Fatal().Msgf("failed to get config dir: %v", err)
}
for _, config := range configs {
ctrld.SetConfigNameWithPath(v, config.name, dir)
v.SetConfigFile(configPath)
if readConfigFile(config.written) {
break
}
}
}
+100
View File
@@ -0,0 +1,100 @@
//go:build linux || freebsd
package main
import (
"os"
"os/exec"
"strings"
"github.com/spf13/cobra"
"github.com/Control-D-Inc/ctrld/internal/router"
)
func initRouterCLI() {
validArgs := append(router.SupportedPlatforms(), "auto")
var b strings.Builder
b.WriteString("Auto-setup Control D on a router.\n\nSupported platforms:\n\n")
for _, arg := range validArgs {
b.WriteString(" ₒ ")
b.WriteString(arg)
if arg == "auto" {
b.WriteString(" - detect the platform you are running on")
}
b.WriteString("\n")
}
routerCmd := &cobra.Command{
Use: "setup",
Short: b.String(),
PreRun: func(cmd *cobra.Command, args []string) {
initConsoleLogging()
},
Run: func(cmd *cobra.Command, args []string) {
if len(args) == 0 {
_ = cmd.Help()
return
}
if len(args) != 1 {
_ = cmd.Help()
return
}
platform := args[0]
if platform == "auto" {
platform = router.Name()
}
if !router.IsSupported(platform) {
unsupportedPlatformHelp(cmd)
os.Exit(1)
}
exe, err := os.Executable()
if err != nil {
mainLog.Fatal().Msgf("could not find executable path: %v", err)
os.Exit(1)
}
cmdArgs := []string{"start"}
cmdArgs = append(cmdArgs, osArgs(platform)...)
cmdArgs = append(cmdArgs, "--router")
command := exec.Command(exe, cmdArgs...)
command.Stdout = os.Stdout
command.Stderr = os.Stderr
command.Stdin = os.Stdin
if err := command.Run(); err != nil {
mainLog.Fatal().Msg(err.Error())
}
},
}
// Keep these flags in sync with startCmd, except for "--router".
routerCmd.Flags().StringVarP(&configPath, "config", "c", "", "Path to config file")
routerCmd.Flags().StringVarP(&configBase64, "base64_config", "", "", "Base64 encoded config")
routerCmd.Flags().StringVarP(&listenAddress, "listen", "", "", "Listener address and port, in format: address:port")
routerCmd.Flags().StringVarP(&primaryUpstream, "primary_upstream", "", "", "Primary upstream endpoint")
routerCmd.Flags().StringVarP(&secondaryUpstream, "secondary_upstream", "", "", "Secondary upstream endpoint")
routerCmd.Flags().StringSliceVarP(&domains, "domains", "", nil, "List of domain to apply in a split DNS policy")
routerCmd.Flags().StringVarP(&logPath, "log", "", "", "Path to log file")
routerCmd.Flags().IntVarP(&cacheSize, "cache_size", "", 0, "Enable cache with size items")
routerCmd.Flags().StringVarP(&cdUID, "cd", "", "", "Control D resolver uid")
routerCmd.Flags().BoolVarP(&cdDev, "dev", "", false, "Use Control D dev resolver/domain")
_ = routerCmd.Flags().MarkHidden("dev")
routerCmd.Flags().StringVarP(&iface, "iface", "", "", `Update DNS setting for iface, "auto" means the default interface gateway`)
tmpl := routerCmd.UsageTemplate()
tmpl = strings.Replace(tmpl, "{{.UseLine}}", "{{.UseLine}} [platform]", 1)
routerCmd.SetUsageTemplate(tmpl)
rootCmd.AddCommand(routerCmd)
}
func osArgs(platform string) []string {
args := os.Args[2:]
n := 0
for _, x := range args {
if x != platform && x != "auto" {
args[n] = x
n++
}
}
return args[:n]
}
+5
View File
@@ -0,0 +1,5 @@
//go:build !linux && !freebsd
package main
func initRouterCLI() {}
+23
View File
@@ -0,0 +1,23 @@
package main
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_writeConfigFile(t *testing.T) {
tmpdir := t.TempDir()
// simulate --config CLI flag by setting configPath manually.
configPath = filepath.Join(tmpdir, "ctrld.toml")
_, err := os.Stat(configPath)
assert.True(t, os.IsNotExist(err))
assert.NoError(t, writeConfigFile())
_, err = os.Stat(configPath)
require.NoError(t, err)
}
+264 -66
View File
@@ -9,17 +9,33 @@ import (
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
"sync"
"time" "time"
"github.com/miekg/dns" "github.com/miekg/dns"
"golang.org/x/sync/errgroup"
"github.com/Control-D-Inc/ctrld" "github.com/Control-D-Inc/ctrld"
"github.com/Control-D-Inc/ctrld/internal/dnscache" "github.com/Control-D-Inc/ctrld/internal/dnscache"
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
"github.com/Control-D-Inc/ctrld/internal/router"
) )
const staleTTL = 60 * time.Second const (
staleTTL = 60 * time.Second
// EDNS0_OPTION_MAC is dnsmasq EDNS0 code for adding mac option.
// https://thekelleys.org.uk/gitweb/?p=dnsmasq.git;a=blob;f=src/dns-protocol.h;h=76ac66a8c28317e9c121a74ab5fd0e20f6237dc8;hb=HEAD#l81
// This is also dns.EDNS0LOCALSTART, but define our own constant here for clarification.
EDNS0_OPTION_MAC = 0xFDE9
)
func (p *prog) serveUDP(listenerNum string) error { var osUpstreamConfig = &ctrld.UpstreamConfig{
Name: "OS resolver",
Type: ctrld.ResolverTypeOS,
Timeout: 2000,
}
func (p *prog) serveDNS(listenerNum string) error {
listenerConfig := p.cfg.Listener[listenerNum] listenerConfig := p.cfg.Listener[listenerNum]
// make sure ip is allocated // make sure ip is allocated
if allocErr := p.allocateIP(listenerConfig.IP); allocErr != nil { if allocErr := p.allocateIP(listenerConfig.IP); allocErr != nil {
@@ -31,13 +47,15 @@ func (p *prog) serveUDP(listenerNum string) error {
failoverRcodes = listenerConfig.Policy.FailoverRcodeNumbers failoverRcodes = listenerConfig.Policy.FailoverRcodeNumbers
} }
handler := dns.HandlerFunc(func(w dns.ResponseWriter, m *dns.Msg) { handler := dns.HandlerFunc(func(w dns.ResponseWriter, m *dns.Msg) {
domain := canonicalName(m.Question[0].Name) q := m.Question[0]
domain := canonicalName(q.Name)
reqId := requestID() reqId := requestID()
fmtSrcToDest := fmtRemoteToLocal(listenerNum, w.RemoteAddr().String(), w.LocalAddr().String()) remoteAddr := spoofRemoteAddr(w.RemoteAddr(), router.GetClientInfoByMac(macFromMsg(m)))
fmtSrcToDest := fmtRemoteToLocal(listenerNum, remoteAddr.String(), w.LocalAddr().String())
t := time.Now() t := time.Now()
ctx := context.WithValue(context.Background(), ctrld.ReqIdCtxKey{}, reqId) ctx := context.WithValue(context.Background(), ctrld.ReqIdCtxKey{}, reqId)
ctrld.Log(ctx, proxyLog.Debug(), "%s received query: %s", fmtSrcToDest, domain) ctrld.Log(ctx, mainLog.Debug(), "%s received query: %s %s", fmtSrcToDest, dns.TypeToString[q.Qtype], domain)
upstreams, matched := p.upstreamFor(ctx, listenerNum, listenerConfig, w.RemoteAddr(), domain) upstreams, matched := p.upstreamFor(ctx, listenerNum, listenerConfig, remoteAddr, domain)
var answer *dns.Msg var answer *dns.Msg
if !matched && listenerConfig.Restricted { if !matched && listenerConfig.Restricted {
answer = new(dns.Msg) answer = new(dns.Msg)
@@ -46,34 +64,59 @@ func (p *prog) serveUDP(listenerNum string) error {
} else { } else {
answer = p.proxy(ctx, upstreams, failoverRcodes, m) answer = p.proxy(ctx, upstreams, failoverRcodes, m)
rtt := time.Since(t) rtt := time.Since(t)
ctrld.Log(ctx, proxyLog.Debug(), "received response of %d bytes in %s", answer.Len(), rtt) ctrld.Log(ctx, mainLog.Debug(), "received response of %d bytes in %s", answer.Len(), rtt)
} }
if err := w.WriteMsg(answer); err != nil { if err := w.WriteMsg(answer); err != nil {
ctrld.Log(ctx, mainLog.Error().Err(err), "serveUDP: failed to send DNS response to client") ctrld.Log(ctx, mainLog.Error().Err(err), "serveUDP: failed to send DNS response to client")
} }
}) })
// On Windows, there's no easy way for disabling/removing IPv6 DNS resolver, so we check whether we can g, ctx := errgroup.WithContext(context.Background())
// listen on ::1, then spawn a listener for receiving DNS requests. for _, proto := range []string{"udp", "tcp"} {
if runtime.GOOS == "windows" && supportsIPv6ListenLocal() { proto := proto
go func() { if needLocalIPv6Listener() {
s := &dns.Server{ g.Go(func() error {
Addr: net.JoinHostPort("::1", strconv.Itoa(listenerConfig.Port)), s, errCh := runDNSServer(net.JoinHostPort("::1", strconv.Itoa(listenerConfig.Port)), proto, handler)
Net: "udp", defer s.Shutdown()
Handler: handler, select {
case <-ctx.Done():
case err := <-errCh:
// Local ipv6 listener should not terminate ctrld.
// It's a workaround for a quirk on Windows.
mainLog.Warn().Err(err).Msg("local ipv6 listener failed")
}
return nil
})
}
g.Go(func() error {
s, errCh := runDNSServer(dnsListenAddress(listenerNum, listenerConfig), proto, handler)
defer s.Shutdown()
if listenerConfig.Port == 0 {
switch s.Net {
case "udp":
mainLog.Info().Msgf("Random port chosen for udp listener.%s: %s", listenerNum, s.PacketConn.LocalAddr())
case "tcp":
mainLog.Info().Msgf("Random port chosen for tcp listener.%s: %s", listenerNum, s.Listener.Addr())
}
} }
_ = s.ListenAndServe() select {
}() case <-ctx.Done():
return nil
case err := <-errCh:
return err
}
})
} }
return g.Wait()
s := &dns.Server{
Addr: net.JoinHostPort(listenerConfig.IP, strconv.Itoa(listenerConfig.Port)),
Net: "udp",
Handler: handler,
}
return s.ListenAndServe()
} }
// upstreamFor returns the list of upstreams for resolving the given domain,
// matching by policies defined in the listener config. The second return value
// reports whether the domain matches the policy.
//
// Though domain policy has higher priority than network policy, it is still
// processed later, because policy logging want to know whether a network rule
// is disregarded in favor of the domain level rule.
func (p *prog) upstreamFor(ctx context.Context, defaultUpstreamNum string, lc *ctrld.ListenerConfig, addr net.Addr, domain string) ([]string, bool) { func (p *prog) upstreamFor(ctx context.Context, defaultUpstreamNum string, lc *ctrld.ListenerConfig, addr net.Addr, domain string) ([]string, bool) {
upstreams := []string{"upstream." + defaultUpstreamNum} upstreams := []string{"upstream." + defaultUpstreamNum}
matchedPolicy := "no policy" matchedPolicy := "no policy"
@@ -83,10 +126,10 @@ func (p *prog) upstreamFor(ctx context.Context, defaultUpstreamNum string, lc *c
defer func() { defer func() {
if !matched && lc.Restricted { if !matched && lc.Restricted {
ctrld.Log(ctx, proxyLog.Info(), "query refused, %s does not match any network policy", addr.String()) ctrld.Log(ctx, mainLog.Info(), "query refused, %s does not match any network policy", addr.String())
return return
} }
ctrld.Log(ctx, proxyLog.Info(), "%s, %s, %s -> %v", matchedPolicy, matchedNetwork, matchedRule, upstreams) ctrld.Log(ctx, mainLog.Info(), "%s, %s, %s -> %v", matchedPolicy, matchedNetwork, matchedRule, upstreams)
}() }()
if lc.Policy == nil { if lc.Policy == nil {
@@ -97,11 +140,43 @@ func (p *prog) upstreamFor(ctx context.Context, defaultUpstreamNum string, lc *c
upstreams = append([]string(nil), policyUpstreams...) upstreams = append([]string(nil), policyUpstreams...)
} }
var networkTargets []string
var sourceIP net.IP
switch addr := addr.(type) {
case *net.UDPAddr:
sourceIP = addr.IP
case *net.TCPAddr:
sourceIP = addr.IP
}
networkRules:
for _, rule := range lc.Policy.Networks {
for source, targets := range rule {
networkNum := strings.TrimPrefix(source, "network.")
nc := p.cfg.Network[networkNum]
if nc == nil {
continue
}
for _, ipNet := range nc.IPNets {
if ipNet.Contains(sourceIP) {
matchedPolicy = lc.Policy.Name
matchedNetwork = source
networkTargets = targets
matched = true
break networkRules
}
}
}
}
for _, rule := range lc.Policy.Rules { for _, rule := range lc.Policy.Rules {
// There's only one entry per rule, config validation ensures this. // There's only one entry per rule, config validation ensures this.
for source, targets := range rule { for source, targets := range rule {
if source == domain || wildcardMatches(source, domain) { if source == domain || wildcardMatches(source, domain) {
matchedPolicy = lc.Policy.Name matchedPolicy = lc.Policy.Name
if len(networkTargets) > 0 {
matchedNetwork += " (unenforced)"
}
matchedRule = source matchedRule = source
do(targets) do(targets)
matched = true matched = true
@@ -110,31 +185,8 @@ func (p *prog) upstreamFor(ctx context.Context, defaultUpstreamNum string, lc *c
} }
} }
var sourceIP net.IP if matched {
switch addr := addr.(type) { do(networkTargets)
case *net.UDPAddr:
sourceIP = addr.IP
case *net.TCPAddr:
sourceIP = addr.IP
}
for _, rule := range lc.Policy.Networks {
for source, targets := range rule {
networkNum := strings.TrimPrefix(source, "network.")
nc := p.cfg.Network[networkNum]
if nc == nil {
continue
}
for _, ipNet := range nc.IPNets {
if ipNet.Contains(sourceIP) {
matchedPolicy = lc.Policy.Name
matchedNetwork = source
do(targets)
matched = true
return upstreams, matched
}
}
}
} }
return upstreams, matched return upstreams, matched
@@ -159,19 +211,19 @@ func (p *prog) proxy(ctx context.Context, upstreams []string, failoverRcodes []i
answer.SetRcode(msg, answer.Rcode) answer.SetRcode(msg, answer.Rcode)
now := time.Now() now := time.Now()
if cachedValue.Expire.After(now) { if cachedValue.Expire.After(now) {
ctrld.Log(ctx, proxyLog.Debug(), "hit cached response") ctrld.Log(ctx, mainLog.Debug(), "hit cached response")
setCachedAnswerTTL(answer, now, cachedValue.Expire) setCachedAnswerTTL(answer, now, cachedValue.Expire)
return answer return answer
} }
staleAnswer = answer staleAnswer = answer
} }
} }
resolve := func(n int, upstreamConfig *ctrld.UpstreamConfig, msg *dns.Msg) *dns.Msg { resolve1 := func(n int, upstreamConfig *ctrld.UpstreamConfig, msg *dns.Msg) (*dns.Msg, error) {
ctrld.Log(ctx, proxyLog.Debug(), "sending query to %s: %s", upstreams[n], upstreamConfig.Name) ctrld.Log(ctx, mainLog.Debug(), "sending query to %s: %s", upstreams[n], upstreamConfig.Name)
dnsResolver, err := ctrld.NewResolver(upstreamConfig) dnsResolver, err := ctrld.NewResolver(upstreamConfig)
if err != nil { if err != nil {
ctrld.Log(ctx, proxyLog.Error().Err(err), "failed to create resolver") ctrld.Log(ctx, mainLog.Error().Err(err), "failed to create resolver")
return nil return nil, err
} }
resolveCtx, cancel := context.WithCancel(ctx) resolveCtx, cancel := context.WithCancel(ctx)
defer cancel() defer cancel()
@@ -180,18 +232,39 @@ func (p *prog) proxy(ctx context.Context, upstreams []string, failoverRcodes []i
defer cancel() defer cancel()
resolveCtx = timeoutCtx resolveCtx = timeoutCtx
} }
answer, err := dnsResolver.Resolve(resolveCtx, msg) return dnsResolver.Resolve(resolveCtx, msg)
if err != nil { }
ctrld.Log(ctx, proxyLog.Error().Err(err), "failed to resolve query") resolve := func(n int, upstreamConfig *ctrld.UpstreamConfig, msg *dns.Msg) *dns.Msg {
if upstreamConfig.UpstreamSendClientInfo() {
ci := router.GetClientInfoByMac(macFromMsg(msg))
if ci != nil {
ctrld.Log(ctx, mainLog.Debug(), "including client info with the request")
ctx = context.WithValue(ctx, ctrld.ClientInfoCtxKey{}, ci)
}
}
answer, err := resolve1(n, upstreamConfig, msg)
// Only do re-bootstrapping if bootstrap ip is not explicitly set by user.
if err != nil && upstreamConfig.BootstrapIP == "" {
ctrld.Log(ctx, mainLog.Debug().Err(err), "could not resolve query on first attempt, retrying...")
// If any error occurred, re-bootstrap transport/ip, retry the request.
upstreamConfig.ReBootstrap()
answer, err = resolve1(n, upstreamConfig, msg)
if err == nil {
return answer
}
ctrld.Log(ctx, mainLog.Error().Err(err), "failed to resolve query")
return nil return nil
} }
return answer return answer
} }
for n, upstreamConfig := range upstreamConfigs { for n, upstreamConfig := range upstreamConfigs {
if upstreamConfig == nil {
continue
}
answer := resolve(n, upstreamConfig, msg) answer := resolve(n, upstreamConfig, msg)
if answer == nil { if answer == nil {
if serveStaleCache && staleAnswer != nil { if serveStaleCache && staleAnswer != nil {
ctrld.Log(ctx, proxyLog.Debug(), "serving stale cached response") ctrld.Log(ctx, mainLog.Debug(), "serving stale cached response")
now := time.Now() now := time.Now()
setCachedAnswerTTL(staleAnswer, now, now.Add(staleTTL)) setCachedAnswerTTL(staleAnswer, now, now.Add(staleTTL))
return staleAnswer return staleAnswer
@@ -199,9 +272,13 @@ func (p *prog) proxy(ctx context.Context, upstreams []string, failoverRcodes []i
continue continue
} }
if answer.Rcode != dns.RcodeSuccess && len(upstreamConfigs) > 1 && containRcode(failoverRcodes, answer.Rcode) { if answer.Rcode != dns.RcodeSuccess && len(upstreamConfigs) > 1 && containRcode(failoverRcodes, answer.Rcode) {
ctrld.Log(ctx, proxyLog.Debug(), "failover rcode matched, process to next upstream") ctrld.Log(ctx, mainLog.Debug(), "failover rcode matched, process to next upstream")
continue continue
} }
// set compression, as it is not set by default when unpacking
answer.Compress = true
if p.cache != nil { if p.cache != nil {
ttl := ttlFromMsg(answer) ttl := ttlFromMsg(answer)
now := time.Now() now := time.Now()
@@ -211,11 +288,11 @@ func (p *prog) proxy(ctx context.Context, upstreams []string, failoverRcodes []i
} }
setCachedAnswerTTL(answer, now, expired) setCachedAnswerTTL(answer, now, expired)
p.cache.Add(dnscache.NewKey(msg, upstreams[n]), dnscache.NewValue(answer, expired)) p.cache.Add(dnscache.NewKey(msg, upstreams[n]), dnscache.NewValue(answer, expired))
ctrld.Log(ctx, proxyLog.Debug(), "add cached response") ctrld.Log(ctx, mainLog.Debug(), "add cached response")
} }
return answer return answer
} }
ctrld.Log(ctx, proxyLog.Error(), "all upstreams failed") ctrld.Log(ctx, mainLog.Error(), "all upstreams failed")
answer := new(dns.Msg) answer := new(dns.Msg)
answer.SetRcode(msg, dns.RcodeServerFailure) answer.SetRcode(msg, dns.RcodeServerFailure)
return answer return answer
@@ -315,7 +392,128 @@ func ttlFromMsg(msg *dns.Msg) uint32 {
return 0 return 0
} }
var osUpstreamConfig = &ctrld.UpstreamConfig{ func needLocalIPv6Listener() bool {
Name: "OS resolver", // On Windows, there's no easy way for disabling/removing IPv6 DNS resolver, so we check whether we can
Type: ctrld.ResolverTypeOS, // listen on ::1, then spawn a listener for receiving DNS requests.
return ctrldnet.SupportsIPv6ListenLocal() && runtime.GOOS == "windows"
}
func dnsListenAddress(lcNum string, lc *ctrld.ListenerConfig) string {
if addr := router.ListenAddress(); setupRouter && addr != "" && lcNum == "0" {
return addr
}
return net.JoinHostPort(lc.IP, strconv.Itoa(lc.Port))
}
func macFromMsg(msg *dns.Msg) string {
if opt := msg.IsEdns0(); opt != nil {
for _, s := range opt.Option {
switch e := s.(type) {
case *dns.EDNS0_LOCAL:
if e.Code == EDNS0_OPTION_MAC {
return net.HardwareAddr(e.Data).String()
}
}
}
}
return ""
}
func spoofRemoteAddr(addr net.Addr, ci *ctrld.ClientInfo) net.Addr {
if ci != nil && ci.IP != "" {
switch addr := addr.(type) {
case *net.UDPAddr:
udpAddr := &net.UDPAddr{
IP: net.ParseIP(ci.IP),
Port: addr.Port,
Zone: addr.Zone,
}
return udpAddr
case *net.TCPAddr:
udpAddr := &net.TCPAddr{
IP: net.ParseIP(ci.IP),
Port: addr.Port,
Zone: addr.Zone,
}
return udpAddr
}
}
return addr
}
// runDNSServer starts a DNS server for given address and network,
// with the given handler. It ensures the server has started listening.
// Any error will be reported to the caller via returned channel.
//
// It's the caller responsibility to call Shutdown to close the server.
func runDNSServer(addr, network string, handler dns.Handler) (*dns.Server, <-chan error) {
s := &dns.Server{
Addr: addr,
Net: network,
Handler: handler,
}
waitLock := sync.Mutex{}
waitLock.Lock()
s.NotifyStartedFunc = waitLock.Unlock
errCh := make(chan error)
go func() {
defer close(errCh)
if err := s.ListenAndServe(); err != nil {
waitLock.Unlock()
mainLog.Error().Err(err).Msgf("could not listen and serve on: %s", s.Addr)
errCh <- err
}
}()
waitLock.Lock()
return s, errCh
}
// runDNSServerForNTPD starts a DNS server listening on router.ListenAddress(). It must only be called when ctrld
// running on router, before router.PreRun() to serve DNS request for NTP synchronization. The caller must call
// s.Shutdown() explicitly when NTP is synced successfully.
func runDNSServerForNTPD(addr string) (*dns.Server, <-chan error) {
if addr == "" {
return &dns.Server{}, nil
}
dnsResolver := ctrld.NewBootstrapResolver()
s := &dns.Server{
Addr: addr,
Net: "udp",
Handler: dns.HandlerFunc(func(w dns.ResponseWriter, m *dns.Msg) {
mainLog.Debug().Msg("Serving query for ntpd")
resolveCtx, cancel := context.WithCancel(context.Background())
defer cancel()
if osUpstreamConfig.Timeout > 0 {
timeoutCtx, cancel := context.WithTimeout(resolveCtx, time.Millisecond*time.Duration(osUpstreamConfig.Timeout))
defer cancel()
resolveCtx = timeoutCtx
}
answer, err := dnsResolver.Resolve(resolveCtx, m)
if err != nil {
mainLog.Error().Err(err).Msgf("could not resolve: %v", m)
return
}
if err := w.WriteMsg(answer); err != nil {
mainLog.Error().Err(err).Msg("runDNSServerForNTPD: failed to send DNS response")
}
}),
}
waitLock := sync.Mutex{}
waitLock.Lock()
s.NotifyStartedFunc = waitLock.Unlock
errCh := make(chan error)
go func() {
defer close(errCh)
if err := s.ListenAndServe(); err != nil {
waitLock.Unlock()
mainLog.Error().Err(err).Msgf("could not listen and serve on: %s", s.Addr)
errCh <- err
}
}()
waitLock.Lock()
return s, errCh
} }
+70 -6
View File
@@ -86,17 +86,17 @@ func Test_prog_upstreamFor(t *testing.T) {
domain string domain string
upstreams []string upstreams []string
matched bool matched bool
testLogMsg string
}{ }{
{"Policy map matches", "192.168.0.1:0", "0", prog.cfg.Listener["0"], "abc.xyz", []string{"upstream.1", "upstream.0"}, true}, {"Policy map matches", "192.168.0.1:0", "0", prog.cfg.Listener["0"], "abc.xyz", []string{"upstream.1", "upstream.0"}, true, ""},
{"Policy split matches", "192.168.0.1:0", "0", prog.cfg.Listener["0"], "abc.ru", []string{"upstream.1"}, true}, {"Policy split matches", "192.168.0.1:0", "0", prog.cfg.Listener["0"], "abc.ru", []string{"upstream.1"}, true, ""},
{"Policy map for other network matches", "192.168.1.2:0", "0", prog.cfg.Listener["0"], "abc.xyz", []string{"upstream.0"}, true}, {"Policy map for other network matches", "192.168.1.2:0", "0", prog.cfg.Listener["0"], "abc.xyz", []string{"upstream.0"}, true, ""},
{"No policy map for listener", "192.168.1.2:0", "1", prog.cfg.Listener["1"], "abc.ru", []string{"upstream.1"}, false}, {"No policy map for listener", "192.168.1.2:0", "1", prog.cfg.Listener["1"], "abc.ru", []string{"upstream.1"}, false, ""},
{"unenforced loging", "192.168.1.2:0", "0", prog.cfg.Listener["0"], "abc.ru", []string{"upstream.1"}, true, "My Policy, network.1 (unenforced), *.ru -> [upstream.1]"},
} }
for _, tc := range tests { for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel()
for _, network := range []string{"udp", "tcp"} { for _, network := range []string{"udp", "tcp"} {
var ( var (
addr net.Addr addr net.Addr
@@ -114,6 +114,9 @@ func Test_prog_upstreamFor(t *testing.T) {
upstreams, matched := prog.upstreamFor(ctx, tc.defaultUpstreamNum, tc.lc, addr, tc.domain) upstreams, matched := prog.upstreamFor(ctx, tc.defaultUpstreamNum, tc.lc, addr, tc.domain)
assert.Equal(t, tc.matched, matched) assert.Equal(t, tc.matched, matched)
assert.Equal(t, tc.upstreams, upstreams) assert.Equal(t, tc.upstreams, upstreams)
if tc.testLogMsg != "" {
assert.Contains(t, logOutput.String(), tc.testLogMsg)
}
} }
}) })
} }
@@ -152,3 +155,64 @@ func TestCache(t *testing.T) {
assert.Equal(t, answer1.Rcode, got1.Rcode) assert.Equal(t, answer1.Rcode, got1.Rcode)
assert.Equal(t, answer2.Rcode, got2.Rcode) assert.Equal(t, answer2.Rcode, got2.Rcode)
} }
func Test_macFromMsg(t *testing.T) {
tests := []struct {
name string
mac string
wantMac bool
}{
{"has mac", "4c:20:b8:ab:87:1b", true},
{"no mac", "4c:20:b8:ab:87:1b", false},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
hw, err := net.ParseMAC(tc.mac)
if err != nil {
t.Fatal(err)
}
m := new(dns.Msg)
m.SetQuestion("example.com.", dns.TypeA)
o := &dns.OPT{Hdr: dns.RR_Header{Name: ".", Rrtype: dns.TypeOPT}}
if tc.wantMac {
ec1 := &dns.EDNS0_LOCAL{Code: EDNS0_OPTION_MAC, Data: hw}
o.Option = append(o.Option, ec1)
}
m.Extra = append(m.Extra, o)
got := macFromMsg(m)
if tc.wantMac && got != tc.mac {
t.Errorf("mismatch, want: %q, got: %q", tc.mac, got)
}
if !tc.wantMac && got != "" {
t.Errorf("unexpected mac: %q", got)
}
})
}
}
func Test_remoteAddrFromMsg(t *testing.T) {
loopbackIP := net.ParseIP("127.0.0.1")
tests := []struct {
name string
addr net.Addr
ci *ctrld.ClientInfo
want string
}{
{"tcp", &net.TCPAddr{IP: loopbackIP, Port: 12345}, &ctrld.ClientInfo{IP: "192.168.1.10"}, "192.168.1.10:12345"},
{"udp", &net.UDPAddr{IP: loopbackIP, Port: 12345}, &ctrld.ClientInfo{IP: "192.168.1.11"}, "192.168.1.11:12345"},
{"nil client info", &net.UDPAddr{IP: loopbackIP, Port: 12345}, nil, "127.0.0.1:12345"},
{"empty ip", &net.UDPAddr{IP: loopbackIP, Port: 12345}, &ctrld.ClientInfo{}, "127.0.0.1:12345"},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
addr := spoofRemoteAddr(tc.addr, tc.ci)
if addr.String() != tc.want {
t.Errorf("unexpected result, want: %q, got: %q", tc.want, addr.String())
}
})
}
}
+50 -28
View File
@@ -1,7 +1,6 @@
package main package main
import ( import (
"fmt"
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
@@ -26,21 +25,25 @@ var (
cacheSize int cacheSize int
cfg ctrld.Config cfg ctrld.Config
verbose int verbose int
silent bool
cdUID string
cdDev bool
iface string
ifaceStartStop string
setupRouter bool
bootstrapDNS = "76.76.2.0" mainLog = zerolog.New(io.Discard)
consoleWriter zerolog.ConsoleWriter
rootLogger = zerolog.New(io.Discard)
mainLog = rootLogger
proxyLog = rootLogger
cdUID string
iface string
ifaceStartStop string
) )
func main() { func main() {
ctrld.InitConfig(v, "ctrld") ctrld.InitConfig(v, "ctrld")
initCLI() initCLI()
initRouterCLI()
if err := rootCmd.Execute(); err != nil {
mainLog.Error().Msg(err.Error())
os.Exit(1)
}
} }
func normalizeLogFilePath(logFilePath string) string { func normalizeLogFilePath(logFilePath string) string {
@@ -50,46 +53,65 @@ func normalizeLogFilePath(logFilePath string) string {
if homedir != "" { if homedir != "" {
return filepath.Join(homedir, logFilePath) return filepath.Join(homedir, logFilePath)
} }
dir, _ := os.UserHomeDir() dir, _ := userHomeDir()
if dir == "" { if dir == "" {
return logFilePath return logFilePath
} }
return filepath.Join(dir, logFilePath) return filepath.Join(dir, logFilePath)
} }
func initConsoleLogging() {
consoleWriter = zerolog.NewConsoleWriter(func(w *zerolog.ConsoleWriter) {
w.TimeFormat = time.StampMilli
})
multi := zerolog.MultiLevelWriter(consoleWriter)
mainLog = mainLog.Output(multi).With().Timestamp().Logger()
switch {
case silent:
zerolog.SetGlobalLevel(zerolog.NoLevel)
case verbose == 1:
zerolog.SetGlobalLevel(zerolog.InfoLevel)
case verbose > 1:
zerolog.SetGlobalLevel(zerolog.DebugLevel)
default:
zerolog.SetGlobalLevel(zerolog.NoticeLevel)
}
}
func initLogging() { func initLogging() {
writers := []io.Writer{io.Discard} writers := []io.Writer{io.Discard}
isLog := cfg.Service.LogLevel != ""
if logFilePath := normalizeLogFilePath(cfg.Service.LogPath); logFilePath != "" { if logFilePath := normalizeLogFilePath(cfg.Service.LogPath); logFilePath != "" {
// Create parent directory if necessary. // Create parent directory if necessary.
if err := os.MkdirAll(filepath.Dir(logFilePath), 0750); err != nil { if err := os.MkdirAll(filepath.Dir(logFilePath), 0750); err != nil {
fmt.Fprintf(os.Stderr, "failed to create log path: %v", err) mainLog.Error().Msgf("failed to create log path: %v", err)
os.Exit(1) os.Exit(1)
} }
logFile, err := os.OpenFile(logFilePath, os.O_APPEND|os.O_CREATE|os.O_RDWR, os.FileMode(0o600)) // Backup old log file with .1 suffix.
if err := os.Rename(logFilePath, logFilePath+".1"); err != nil && !os.IsNotExist(err) {
mainLog.Error().Msgf("could not backup old log file: %v", err)
}
logFile, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_RDWR, os.FileMode(0o600))
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "failed to create log file: %v", err) mainLog.Error().Msgf("failed to create log file: %v", err)
os.Exit(1) os.Exit(1)
} }
isLog = true
writers = append(writers, logFile) writers = append(writers, logFile)
} }
zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs
consoleWriter := zerolog.NewConsoleWriter(func(w *zerolog.ConsoleWriter) {
w.TimeFormat = time.StampMilli
})
writers = append(writers, consoleWriter) writers = append(writers, consoleWriter)
multi := zerolog.MultiLevelWriter(writers...) multi := zerolog.MultiLevelWriter(writers...)
mainLog = mainLog.Output(multi).With().Timestamp().Str("prefix", "main").Logger() mainLog = mainLog.Output(multi).With().Timestamp().Logger()
if verbose > 0 || isLog { // TODO: find a better way.
proxyLog = proxyLog.Output(multi).With().Timestamp().Logger() ctrld.ProxyLog = mainLog
// TODO: find a better way.
ctrld.ProxyLog = proxyLog
}
zerolog.SetGlobalLevel(zerolog.InfoLevel) zerolog.SetGlobalLevel(zerolog.NoticeLevel)
logLevel := cfg.Service.LogLevel logLevel := cfg.Service.LogLevel
if verbose > 1 { switch {
case silent:
zerolog.SetGlobalLevel(zerolog.NoLevel)
return
case verbose == 1:
logLevel = "info"
case verbose > 1:
logLevel = "debug" logLevel = "debug"
} }
if logLevel == "" { if logLevel == "" {
+16
View File
@@ -0,0 +1,16 @@
package main
import (
"os"
"strings"
"testing"
"github.com/rs/zerolog"
)
var logOutput strings.Builder
func TestMain(m *testing.M) {
mainLog = zerolog.New(&logOutput)
os.Exit(m.Run())
}
-65
View File
@@ -1,65 +0,0 @@
package main
import (
"context"
"net"
"sync"
"time"
"tailscale.com/logtail/backoff"
"github.com/Control-D-Inc/ctrld/internal/controld"
)
const (
controldIPv6Test = "ipv6.controld.io"
)
var (
stackOnce sync.Once
ipv6Enabled bool
canListenIPv6Local bool
hasNetworkUp bool
)
func probeStack() {
b := backoff.NewBackoff("probeStack", func(format string, args ...any) {}, time.Minute)
for {
if _, err := controld.Dialer.Dial("udp", net.JoinHostPort(bootstrapDNS, "53")); err == nil {
hasNetworkUp = true
break
} else {
b.BackOff(context.Background(), err)
}
}
if _, err := controld.Dialer.Dial("tcp6", net.JoinHostPort(controldIPv6Test, "80")); err == nil {
ipv6Enabled = true
}
if ln, err := net.Listen("tcp6", "[::1]:53"); err == nil {
ln.Close()
canListenIPv6Local = true
}
}
func netUp() bool {
stackOnce.Do(probeStack)
return hasNetworkUp
}
func supportsIPv6() bool {
stackOnce.Do(probeStack)
return ipv6Enabled
}
func supportsIPv6ListenLocal() bool {
stackOnce.Do(probeStack)
return canListenIPv6Local
}
// isIPv6 checks if the provided IP is v6.
//
//lint:ignore U1000 use in os_windows.go
func isIPv6(ip string) bool {
parsedIP := net.ParseIP(ip)
return parsedIP != nil && parsedIP.To4() == nil && parsedIP.To16() != nil
}
+17 -7
View File
@@ -3,6 +3,7 @@ package main
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"io"
"net" "net"
"os/exec" "os/exec"
"strings" "strings"
@@ -14,21 +15,30 @@ func patchNetIfaceName(iface *net.Interface) error {
return err return err
} }
scanner := bufio.NewScanner(bytes.NewReader(b)) if name := networkServiceName(iface.Name, bytes.NewReader(b)); name != "" {
iface.Name = name
mainLog.Debug().Str("network_service", name).Msg("found network service name for interface")
}
return nil
}
func networkServiceName(ifaceName string, r io.Reader) string {
scanner := bufio.NewScanner(r)
prevLine := ""
for scanner.Scan() { for scanner.Scan() {
line := scanner.Text() line := scanner.Text()
if strings.Contains(line, "*") { if strings.Contains(line, "*") {
// Network services is disabled. // Network services is disabled.
continue continue
} }
if !strings.Contains(line, "Device: "+iface.Name) { if !strings.Contains(line, "Device: "+ifaceName) {
prevLine = line
continue continue
} }
parts := strings.Split(line, ",") parts := strings.SplitN(prevLine, " ", 2)
if _, networkServiceName, ok := strings.Cut(parts[0], "(Hardware Port: "); ok { if len(parts) == 2 {
mainLog.Debug().Str("network_service", networkServiceName).Msg("found network service name for interface") return strings.TrimSpace(parts[1])
iface.Name = networkServiceName
} }
} }
return nil return ""
} }
+59
View File
@@ -0,0 +1,59 @@
package main
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
const listnetworkserviceorderOutput = `
(1) USB 10/100/1000 LAN 2
(Hardware Port: USB 10/100/1000 LAN, Device: en7)
(2) Ethernet
(Hardware Port: Ethernet, Device: en0)
(3) Wi-Fi
(Hardware Port: Wi-Fi, Device: en1)
(4) Bluetooth PAN
(Hardware Port: Bluetooth PAN, Device: en4)
(5) Thunderbolt Bridge
(Hardware Port: Thunderbolt Bridge, Device: bridge0)
(6) kernal
(Hardware Port: com.wireguard.macos, Device: )
(7) WS BT
(Hardware Port: com.wireguard.macos, Device: )
(8) ca-001-stg
(Hardware Port: com.wireguard.macos, Device: )
(9) ca-001-stg-2
(Hardware Port: com.wireguard.macos, Device: )
`
func Test_networkServiceName(t *testing.T) {
tests := []struct {
ifaceName string
networkServiceName string
}{
{"en7", "USB 10/100/1000 LAN 2"},
{"en0", "Ethernet"},
{"en1", "Wi-Fi"},
{"en4", "Bluetooth PAN"},
{"bridge0", "Thunderbolt Bridge"},
}
for _, tc := range tests {
tc := tc
t.Run(tc.ifaceName, func(t *testing.T) {
t.Parallel()
name := networkServiceName(tc.ifaceName, strings.NewReader(listnetworkserviceorderOutput))
assert.Equal(t, tc.networkServiceName, name)
})
}
}
+27
View File
@@ -0,0 +1,27 @@
package main
import (
"github.com/vishvananda/netlink"
"golang.org/x/sys/unix"
)
func (p *prog) watchLinkState() {
ch := make(chan netlink.LinkUpdate)
done := make(chan struct{})
defer close(done)
if err := netlink.LinkSubscribe(ch, done); err != nil {
mainLog.Warn().Err(err).Msg("could not subscribe link")
return
}
for lu := range ch {
if lu.Change == 0xFFFFFFFF {
continue
}
if lu.Change&unix.IFF_UP != 0 {
mainLog.Debug().Msgf("link state changed, re-bootstrapping")
for _, uc := range p.cfg.Upstream {
uc.ReBootstrap()
}
}
}
}
+5
View File
@@ -0,0 +1,5 @@
//go:build !linux
package main
func (p *prog) watchLinkState() {}
@@ -4,7 +4,6 @@ import (
"context" "context"
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"time" "time"
"github.com/coreos/go-systemd/v22/dbus" "github.com/coreos/go-systemd/v22/dbus"
@@ -24,10 +23,6 @@ systemd-resolved=false
var networkManagerCtrldConfFile = filepath.Join(nmConfDir, nmCtrldConfFilename) var networkManagerCtrldConfFile = filepath.Join(nmConfDir, nmCtrldConfFilename)
func setupNetworkManager() error { func setupNetworkManager() error {
if runtime.GOOS != "linux" {
mainLog.Debug().Msg("skipping NetworkManager setup, not on Linux")
return nil
}
if content, _ := os.ReadFile(nmCtrldConfContent); string(content) == nmCtrldConfContent { if content, _ := os.ReadFile(nmCtrldConfContent); string(content) == nmCtrldConfContent {
mainLog.Debug().Msg("NetworkManager already setup, nothing to do") mainLog.Debug().Msg("NetworkManager already setup, nothing to do")
return nil return nil
@@ -48,10 +43,6 @@ func setupNetworkManager() error {
} }
func restoreNetworkManager() error { func restoreNetworkManager() error {
if runtime.GOOS != "linux" {
mainLog.Debug().Msg("skipping NetworkManager restoring, not on Linux")
return nil
}
err := os.Remove(networkManagerCtrldConfFile) err := os.Remove(networkManagerCtrldConfFile)
if os.IsNotExist(err) { if os.IsNotExist(err) {
mainLog.Debug().Msg("NetworkManager is not available") mainLog.Debug().Msg("NetworkManager is not available")
+15
View File
@@ -0,0 +1,15 @@
//go:build !linux
package main
func setupNetworkManager() error {
reloadNetworkManager()
return nil
}
func restoreNetworkManager() error {
reloadNetworkManager()
return nil
}
func reloadNetworkManager() {}
@@ -1,6 +1,3 @@
//go:build darwin
// +build darwin
package main package main
import ( import (
+68
View File
@@ -0,0 +1,68 @@
package main
import (
"net"
"net/netip"
"os/exec"
"github.com/Control-D-Inc/ctrld/internal/dns"
"github.com/Control-D-Inc/ctrld/internal/resolvconffile"
)
// allocate loopback ip
// sudo ifconfig lo0 127.0.0.53 alias
func allocateIP(ip string) error {
cmd := exec.Command("ifconfig", "lo0", ip, "alias")
if err := cmd.Run(); err != nil {
mainLog.Error().Err(err).Msg("allocateIP failed")
return err
}
return nil
}
func deAllocateIP(ip string) error {
cmd := exec.Command("ifconfig", "lo0", ip, "-alias")
if err := cmd.Run(); err != nil {
mainLog.Error().Err(err).Msg("deAllocateIP failed")
return err
}
return nil
}
// set the dns server for the provided network interface
func setDNS(iface *net.Interface, nameservers []string) error {
r, err := dns.NewOSConfigurator(logf, iface.Name)
if err != nil {
mainLog.Error().Err(err).Msg("failed to create DNS OS configurator")
return err
}
ns := make([]netip.Addr, 0, len(nameservers))
for _, nameserver := range nameservers {
ns = append(ns, netip.MustParseAddr(nameserver))
}
if err := r.SetDNS(dns.OSConfig{Nameservers: ns}); err != nil {
mainLog.Error().Err(err).Msg("failed to set DNS")
return err
}
return nil
}
func resetDNS(iface *net.Interface) error {
r, err := dns.NewOSConfigurator(logf, iface.Name)
if err != nil {
mainLog.Error().Err(err).Msg("failed to create DNS OS configurator")
return err
}
if err := r.Close(); err != nil {
mainLog.Error().Err(err).Msg("failed to rollback DNS setting")
return err
}
return nil
}
func currentDNS(_ *net.Interface) []string {
return resolvconffile.NameServers("")
}
+16 -15
View File
@@ -19,19 +19,16 @@ import (
"tailscale.com/util/dnsname" "tailscale.com/util/dnsname"
"github.com/Control-D-Inc/ctrld/internal/dns" "github.com/Control-D-Inc/ctrld/internal/dns"
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
"github.com/Control-D-Inc/ctrld/internal/resolvconffile" "github.com/Control-D-Inc/ctrld/internal/resolvconffile"
) )
var logf = func(format string, args ...any) {
mainLog.Debug().Msgf(format, args...)
}
// allocate loopback ip // allocate loopback ip
// sudo ip a add 127.0.0.2/24 dev lo // sudo ip a add 127.0.0.2/24 dev lo
func allocateIP(ip string) error { func allocateIP(ip string) error {
cmd := exec.Command("ip", "a", "add", ip+"/24", "dev", "lo") cmd := exec.Command("ip", "a", "add", ip+"/24", "dev", "lo")
if err := cmd.Run(); err != nil { if out, err := cmd.CombinedOutput(); err != nil {
mainLog.Error().Err(err).Msg("allocateIP failed") mainLog.Error().Err(err).Msgf("allocateIP failed: %s", string(out))
return err return err
} }
return nil return nil
@@ -79,16 +76,20 @@ func setDNS(iface *net.Interface, nameservers []string) error {
return nil return nil
} }
func resetDNS(iface *net.Interface) error { func resetDNS(iface *net.Interface) (err error) {
if r, err := dns.NewOSConfigurator(logf, iface.Name); err == nil { defer func() {
if err := r.Close(); err != nil { if err == nil {
mainLog.Error().Err(err).Msg("failed to rollback DNS setting") return
return err
} }
if r.Mode() == "direct" { if r, oerr := dns.NewOSConfigurator(logf, iface.Name); oerr == nil {
return nil _ = r.SetDNS(dns.OSConfig{})
if err := r.Close(); err != nil {
mainLog.Error().Err(err).Msg("failed to rollback DNS setting")
return
}
err = nil
} }
} }()
var ns []string var ns []string
c, err := nclient4.New(iface.Name) c, err := nclient4.New(iface.Name)
@@ -111,7 +112,7 @@ func resetDNS(iface *net.Interface) error {
} }
// TODO(cuonglm): handle DHCPv6 properly. // TODO(cuonglm): handle DHCPv6 properly.
if supportsIPv6() { if ctrldnet.IPv6Available(ctx) {
c := client6.NewClient() c := client6.NewClient()
conversation, err := c.Exchange(iface.Name) conversation, err := c.Exchange(iface.Name)
if err != nil { if err != nil {
+13
View File
@@ -0,0 +1,13 @@
//go:build !linux && !darwin && !freebsd
package main
// TODO(cuonglm): implement.
func allocateIP(ip string) error {
return nil
}
// TODO(cuonglm): implement.
func deAllocateIP(ip string) error {
return nil
}
+6 -17
View File
@@ -1,6 +1,3 @@
//go:build windows
// +build windows
package main package main
import ( import (
@@ -10,18 +7,10 @@ import (
"strconv" "strconv"
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg" "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
) )
// TODO(cuonglm): implement.
func allocateIP(ip string) error {
return nil
}
// TODO(cuonglm): implement.
func deAllocateIP(ip string) error {
return nil
}
func setDNS(iface *net.Interface, nameservers []string) error { func setDNS(iface *net.Interface, nameservers []string) error {
if len(nameservers) == 0 { if len(nameservers) == 0 {
return errors.New("empty DNS nameservers") return errors.New("empty DNS nameservers")
@@ -39,7 +28,7 @@ func setDNS(iface *net.Interface, nameservers []string) error {
// TODO(cuonglm): should we use system API? // TODO(cuonglm): should we use system API?
func resetDNS(iface *net.Interface) error { func resetDNS(iface *net.Interface) error {
if supportsIPv6ListenLocal() { if ctrldnet.SupportsIPv6ListenLocal() {
if output, err := netsh("interface", "ipv6", "set", "dnsserver", strconv.Itoa(iface.Index), "dhcp"); err != nil { if output, err := netsh("interface", "ipv6", "set", "dnsserver", strconv.Itoa(iface.Index), "dhcp"); err != nil {
mainLog.Warn().Err(err).Msgf("failed to reset ipv6 DNS: %s", string(output)) mainLog.Warn().Err(err).Msgf("failed to reset ipv6 DNS: %s", string(output))
} }
@@ -54,7 +43,7 @@ func resetDNS(iface *net.Interface) error {
func setPrimaryDNS(iface *net.Interface, dns string) error { func setPrimaryDNS(iface *net.Interface, dns string) error {
ipVer := "ipv4" ipVer := "ipv4"
if isIPv6(dns) { if ctrldnet.IsIPv6(dns) {
ipVer = "ipv6" ipVer = "ipv6"
} }
idx := strconv.Itoa(iface.Index) idx := strconv.Itoa(iface.Index)
@@ -63,7 +52,7 @@ func setPrimaryDNS(iface *net.Interface, dns string) error {
mainLog.Error().Err(err).Msgf("failed to set primary DNS: %s", string(output)) mainLog.Error().Err(err).Msgf("failed to set primary DNS: %s", string(output))
return err return err
} }
if ipVer == "ipv4" { if ipVer == "ipv4" && ctrldnet.SupportsIPv6ListenLocal() {
// Disable IPv6 DNS, so the query will be fallback to IPv4. // Disable IPv6 DNS, so the query will be fallback to IPv4.
_, _ = netsh("interface", "ipv6", "set", "dnsserver", idx, "static", "::1", "primary") _, _ = netsh("interface", "ipv6", "set", "dnsserver", idx, "static", "::1", "primary")
} }
@@ -73,7 +62,7 @@ func setPrimaryDNS(iface *net.Interface, dns string) error {
func addSecondaryDNS(iface *net.Interface, dns string) error { func addSecondaryDNS(iface *net.Interface, dns string) error {
ipVer := "ipv4" ipVer := "ipv4"
if isIPv6(dns) { if ctrldnet.IsIPv6(dns) {
ipVer = "ipv6" ipVer = "ipv6"
} }
output, err := netsh("interface", ipVer, "add", "dns", strconv.Itoa(iface.Index), dns, "index=2") output, err := netsh("interface", ipVer, "add", "dns", strconv.Itoa(iface.Index), dns, "index=2")
+69 -76
View File
@@ -2,6 +2,8 @@ package main
import ( import (
"errors" "errors"
"fmt"
"math/rand"
"net" "net"
"os" "os"
"strconv" "strconv"
@@ -9,20 +11,29 @@ import (
"syscall" "syscall"
"github.com/kardianos/service" "github.com/kardianos/service"
"github.com/miekg/dns"
"github.com/Control-D-Inc/ctrld" "github.com/Control-D-Inc/ctrld"
"github.com/Control-D-Inc/ctrld/internal/dnscache" "github.com/Control-D-Inc/ctrld/internal/dnscache"
"github.com/Control-D-Inc/ctrld/internal/router"
) )
var logf = func(format string, args ...any) {
mainLog.Debug().Msgf(format, args...)
}
var errWindowsAddrInUse = syscall.Errno(0x2740) var errWindowsAddrInUse = syscall.Errno(0x2740)
var svcConfig = &service.Config{ var svcConfig = &service.Config{
Name: "ctrld", Name: "ctrld",
DisplayName: "Control-D Helper Service", DisplayName: "Control-D Helper Service",
Option: service.KeyValue{},
} }
type prog struct { type prog struct {
mu sync.Mutex
waitCh chan struct{}
stopCh chan struct{}
cfg *ctrld.Config cfg *ctrld.Config
cache dnscache.Cacher cache dnscache.Cacher
} }
@@ -30,11 +41,12 @@ type prog struct {
func (p *prog) Start(s service.Service) error { func (p *prog) Start(s service.Service) error {
p.cfg = &cfg p.cfg = &cfg
go p.run() go p.run()
mainLog.Info().Msg("Service started")
return nil return nil
} }
func (p *prog) run() { func (p *prog) run() {
// Wait the caller to signal that we can do our logic.
<-p.waitCh
p.preRun() p.preRun()
if p.cfg.Service.CacheEnable { if p.cfg.Service.CacheEnable {
cacher, err := dnscache.NewLRUCache(p.cfg.Service.CacheSize) cacher, err := dnscache.NewLRUCache(p.cfg.Service.CacheSize)
@@ -51,7 +63,7 @@ func (p *prog) run() {
for _, cidr := range nc.Cidrs { for _, cidr := range nc.Cidrs {
_, ipNet, err := net.ParseCIDR(cidr) _, ipNet, err := net.ParseCIDR(cidr)
if err != nil { if err != nil {
proxyLog.Error().Err(err).Str("network", nc.Name).Str("cidr", cidr).Msg("invalid cidr") mainLog.Error().Err(err).Str("network", nc.Name).Str("cidr", cidr).Msg("invalid cidr")
continue continue
} }
nc.IPNets = append(nc.IPNets, ipNet) nc.IPNets = append(nc.IPNets, ipNet)
@@ -61,47 +73,17 @@ func (p *prog) run() {
uc := p.cfg.Upstream[n] uc := p.cfg.Upstream[n]
uc.Init() uc.Init()
if uc.BootstrapIP == "" { if uc.BootstrapIP == "" {
// resolve it manually and set the bootstrap ip uc.SetupBootstrapIP()
c := new(dns.Client) mainLog.Info().Msgf("Bootstrap IPs for upstream.%s: %q", n, uc.BootstrapIPs())
for _, dnsType := range []uint16{dns.TypeAAAA, dns.TypeA} { } else {
if !supportsIPv6() && dnsType == dns.TypeAAAA { mainLog.Info().Str("bootstrap_ip", uc.BootstrapIP).Msgf("Using bootstrap IP for upstream.%s", n)
continue
}
m := new(dns.Msg)
m.SetQuestion(uc.Domain+".", dnsType)
m.RecursionDesired = true
r, _, err := c.Exchange(m, net.JoinHostPort(bootstrapDNS, "53"))
if err != nil {
proxyLog.Error().Err(err).Msgf("could not resolve domain %s for upstream.%s", uc.Domain, n)
continue
}
if r.Rcode != dns.RcodeSuccess {
proxyLog.Error().Msgf("could not resolve domain return code: %d, upstream.%s", r.Rcode, n)
continue
}
if len(r.Answer) == 0 {
continue
}
for _, a := range r.Answer {
switch ar := a.(type) {
case *dns.A:
uc.BootstrapIP = ar.A.String()
case *dns.AAAA:
uc.BootstrapIP = ar.AAAA.String()
default:
continue
}
mainLog.Info().Str("bootstrap_ip", uc.BootstrapIP).Msgf("Setting bootstrap IP for upstream.%s", n)
// Stop if we reached here, because we got the bootstrap IP from r.Answer.
break
}
// If we reached here, uc.BootstrapIP was set, nothing to do anymore.
break
}
} }
uc.SetCertPool(rootCertPool)
uc.SetupTransport() uc.SetupTransport()
} }
go p.watchLinkState()
for listenerNum := range p.cfg.Listener { for listenerNum := range p.cfg.Listener {
p.cfg.Listener[listenerNum].Init() p.cfg.Listener[listenerNum].Init()
go func(listenerNum string) { go func(listenerNum string) {
@@ -109,60 +91,43 @@ func (p *prog) run() {
listenerConfig := p.cfg.Listener[listenerNum] listenerConfig := p.cfg.Listener[listenerNum]
upstreamConfig := p.cfg.Upstream[listenerNum] upstreamConfig := p.cfg.Upstream[listenerNum]
if upstreamConfig == nil { if upstreamConfig == nil {
proxyLog.Error().Msgf("missing upstream config for: [listener.%s]", listenerNum) mainLog.Warn().Msgf("no default upstream for: [listener.%s]", listenerNum)
return
} }
addr := net.JoinHostPort(listenerConfig.IP, strconv.Itoa(listenerConfig.Port)) addr := net.JoinHostPort(listenerConfig.IP, strconv.Itoa(listenerConfig.Port))
mainLog.Info().Msgf("Starting DNS server on listener.%s: %s", listenerNum, addr) mainLog.Info().Msgf("Starting DNS server on listener.%s: %s", listenerNum, addr)
err := p.serveUDP(listenerNum) err := p.serveDNS(listenerNum)
if err != nil && !defaultConfigWritten { if err != nil && !defaultConfigWritten && cdUID == "" {
proxyLog.Fatal().Err(err).Msgf("Unable to start dns proxy on listener.%s", listenerNum) mainLog.Fatal().Err(err).Msgf("Unable to start dns proxy on listener.%s", listenerNum)
return return
} }
if err == nil { if err == nil {
return return
} }
if opErr, ok := err.(*net.OpError); ok { if opErr, ok := err.(*net.OpError); ok && listenerNum == "0" {
if sErr, ok := opErr.Err.(*os.SyscallError); ok && errors.Is(opErr.Err, syscall.EADDRINUSE) || errors.Is(sErr.Err, errWindowsAddrInUse) { if sErr, ok := opErr.Err.(*os.SyscallError); ok && errors.Is(opErr.Err, syscall.EADDRINUSE) || errors.Is(sErr.Err, errWindowsAddrInUse) {
proxyLog.Warn().Msgf("Address %s already in used, pick a random one", addr) mainLog.Warn().Msgf("Address %s already in used, pick a random one", addr)
pc, err := net.ListenPacket("udp", net.JoinHostPort(listenerConfig.IP, "0")) ip := randomLocalIP()
if err != nil { listenerConfig.IP = ip
proxyLog.Fatal().Err(err).Msg("failed to listen packet") port := listenerConfig.Port
return cfg.Upstream = map[string]*ctrld.UpstreamConfig{"0": cfg.Upstream["0"]}
}
_, portStr, _ := net.SplitHostPort(pc.LocalAddr().String())
port, err := strconv.Atoi(portStr)
if err != nil {
proxyLog.Fatal().Err(err).Msg("malformed port")
return
}
listenerConfig.Port = port
v.Set("listener", map[string]*ctrld.ListenerConfig{
"0": {
IP: "127.0.0.1",
Port: port,
},
})
if err := writeConfigFile(); err != nil { if err := writeConfigFile(); err != nil {
proxyLog.Fatal().Err(err).Msg("failed to write config file") mainLog.Fatal().Err(err).Msg("failed to write config file")
} else { } else {
mainLog.Info().Msg("writing config file to: " + defaultConfigFile) mainLog.Info().Msg("writing config file to: " + defaultConfigFile)
} }
mainLog.Info().Msgf("Starting DNS server on listener.%s: %s", listenerNum, pc.LocalAddr()) p.mu.Lock()
// There can be a race between closing the listener and start our own UDP server, but it's p.cfg.Service.AllocateIP = true
// rare, and we only do this once, so let conservative here. p.mu.Unlock()
if err := pc.Close(); err != nil { p.preRun()
proxyLog.Fatal().Err(err).Msg("failed to close packet conn") mainLog.Info().Msgf("Starting DNS server on listener.%s: %s", listenerNum, net.JoinHostPort(ip, strconv.Itoa(port)))
return if err := p.serveDNS(listenerNum); err != nil {
} mainLog.Fatal().Err(err).Msgf("Unable to start dns proxy on listener.%s", listenerNum)
if err := p.serveUDP(listenerNum); err != nil {
proxyLog.Fatal().Err(err).Msgf("Unable to start dns proxy on listener.%s", listenerNum)
return return
} }
} }
} }
proxyLog.Fatal().Err(err).Msgf("Unable to start dns proxy on listener.%s", listenerNum) mainLog.Fatal().Err(err).Msgf("Unable to start dns proxy on listener.%s", listenerNum)
}(listenerNum) }(listenerNum)
} }
@@ -174,11 +139,18 @@ func (p *prog) Stop(s service.Service) error {
mainLog.Error().Err(err).Msg("de-allocate ip failed") mainLog.Error().Err(err).Msg("de-allocate ip failed")
return err return err
} }
p.preStop()
if err := router.Stop(); err != nil {
mainLog.Warn().Err(err).Msg("problem occurred while stopping router")
}
mainLog.Info().Msg("Service stopped") mainLog.Info().Msg("Service stopped")
close(p.stopCh)
return nil return nil
} }
func (p *prog) allocateIP(ip string) error { func (p *prog) allocateIP(ip string) error {
p.mu.Lock()
defer p.mu.Unlock()
if !p.cfg.Service.AllocateIP { if !p.cfg.Service.AllocateIP {
return nil return nil
} }
@@ -186,6 +158,8 @@ func (p *prog) allocateIP(ip string) error {
} }
func (p *prog) deAllocateIP() error { func (p *prog) deAllocateIP() error {
p.mu.Lock()
defer p.mu.Unlock()
if !p.cfg.Service.AllocateIP { if !p.cfg.Service.AllocateIP {
return nil return nil
} }
@@ -198,6 +172,15 @@ func (p *prog) deAllocateIP() error {
} }
func (p *prog) setDNS() { func (p *prog) setDNS() {
switch router.Name() {
case router.DDWrt, router.OpenWrt, router.Ubios:
// On router, ctrld run as a DNS forwarder, it does not have to change system DNS.
// Except for:
// + EdgeOS, which /etc/resolv.conf could be managed by vyatta_update_resolv.pl script.
// + Merlin/Tomato, which has WAN DNS setup on boot for NTP.
// + Synology, which /etc/resolv.conf is not configured to point to localhost.
return
}
if cfg.Listener == nil || cfg.Listener["0"] == nil { if cfg.Listener == nil || cfg.Listener["0"] == nil {
return return
} }
@@ -226,6 +209,11 @@ func (p *prog) setDNS() {
} }
func (p *prog) resetDNS() { func (p *prog) resetDNS() {
switch router.Name() {
case router.DDWrt, router.OpenWrt, router.Ubios:
// See comment in p.setDNS method.
return
}
if iface == "" { if iface == "" {
return return
} }
@@ -249,3 +237,8 @@ func (p *prog) resetDNS() {
} }
logger.Debug().Msg("Restoring DNS successfully") logger.Debug().Msg("Restoring DNS successfully")
} }
func randomLocalIP() string {
n := rand.Intn(254-2) + 2
return fmt.Sprintf("127.0.0.%d", n)
}
+23
View File
@@ -0,0 +1,23 @@
package main
import (
"github.com/kardianos/service"
)
func (p *prog) preRun() {
if !service.Interactive() {
p.setDNS()
}
}
func setDependencies(svc *service.Config) {}
func setWorkingDirectory(svc *service.Config, dir string) {
svc.WorkingDirectory = dir
}
func (p *prog) preStop() {
if !service.Interactive() {
p.resetDNS()
}
}
+22
View File
@@ -0,0 +1,22 @@
package main
import (
"os"
"github.com/kardianos/service"
)
func (p *prog) preRun() {
if !service.Interactive() {
p.setDNS()
}
}
func setDependencies(svc *service.Config) {
// TODO(cuonglm): remove once https://github.com/kardianos/service/issues/359 fixed.
_ = os.MkdirAll("/usr/local/etc/rc.d", 0755)
}
func setWorkingDirectory(svc *service.Config, dir string) {}
func (p *prog) preStop() {}
+15
View File
@@ -2,6 +2,8 @@ package main
import ( import (
"github.com/kardianos/service" "github.com/kardianos/service"
"github.com/Control-D-Inc/ctrld/internal/router"
) )
func (p *prog) preRun() { func (p *prog) preRun() {
@@ -17,4 +19,17 @@ func setDependencies(svc *service.Config) {
"Wants=NetworkManager-wait-online.service", "Wants=NetworkManager-wait-online.service",
"After=NetworkManager-wait-online.service", "After=NetworkManager-wait-online.service",
} }
// On EdeOS, ctrld needs to start after vyatta-dhcpd, so it can read leases file.
if router.Name() == router.EdgeOS {
svc.Dependencies = append(svc.Dependencies, "Wants=vyatta-dhcpd.service")
svc.Dependencies = append(svc.Dependencies, "After=vyatta-dhcpd.service")
svc.Dependencies = append(svc.Dependencies, "Wants=dnsmasq.service")
svc.Dependencies = append(svc.Dependencies, "After=dnsmasq.service")
}
} }
func setWorkingDirectory(svc *service.Config, dir string) {
svc.WorkingDirectory = dir
}
func (p *prog) preStop() {}
+8 -2
View File
@@ -1,5 +1,4 @@
//go:build !linux //go:build !linux && !freebsd && !darwin
// +build !linux
package main package main
@@ -8,3 +7,10 @@ import "github.com/kardianos/service"
func (p *prog) preRun() {} func (p *prog) preRun() {}
func setDependencies(svc *service.Config) {} func setDependencies(svc *service.Config) {}
func setWorkingDirectory(svc *service.Config, dir string) {
// WorkingDirectory is not supported on Windows.
svc.WorkingDirectory = dir
}
func (p *prog) preStop() {}
+60 -10
View File
@@ -1,18 +1,45 @@
package main package main
import ( import (
"fmt" "bytes"
"errors"
"os" "os"
"os/exec"
"github.com/spf13/cobra" "github.com/kardianos/service"
"github.com/Control-D-Inc/ctrld/internal/router"
) )
func stderrMsg(msg string) { func newService(s service.Service) service.Service {
_, _ = fmt.Fprintln(os.Stderr, msg) // TODO: unify for other SysV system.
switch {
case router.IsGLiNet(), router.IsOldOpenwrt():
return &sysV{s}
}
return s
} }
func stdoutMsg(msg string) { // sysV wraps a service.Service, and provide start/stop/status command
_, _ = fmt.Fprintln(os.Stdout, msg) // base on "/etc/init.d/<service_name>".
//
// Use this on system wherer "service" command is not available, like GL.iNET router.
type sysV struct {
service.Service
}
func (s *sysV) Start() error {
_, err := exec.Command("/etc/init.d/ctrld", "start").CombinedOutput()
return err
}
func (s *sysV) Stop() error {
_, err := exec.Command("/etc/init.d/ctrld", "stop").CombinedOutput()
return err
}
func (s *sysV) Status() (service.Status, error) {
return unixSystemVServiceStatus()
} }
type task struct { type task struct {
@@ -21,25 +48,48 @@ type task struct {
} }
func doTasks(tasks []task) bool { func doTasks(tasks []task) bool {
var prevErr error
for _, task := range tasks { for _, task := range tasks {
if err := task.f(); err != nil { if err := task.f(); err != nil {
if task.abortOnError { if task.abortOnError {
stderrMsg(err.Error()) mainLog.Error().Msg(errors.Join(prevErr, err).Error())
return false return false
} }
prevErr = err
} }
} }
return true return true
} }
func checkHasElevatedPrivilege(cmd *cobra.Command, args []string) { func checkHasElevatedPrivilege() {
ok, err := hasElevatedPrivilege() ok, err := hasElevatedPrivilege()
if err != nil { if err != nil {
fmt.Printf("could not detect user privilege: %v", err) mainLog.Error().Msgf("could not detect user privilege: %v", err)
return return
} }
if !ok { if !ok {
fmt.Println("Please relaunch process with admin/root privilege.") mainLog.Error().Msg("Please relaunch process with admin/root privilege.")
os.Exit(1) os.Exit(1)
} }
} }
func serviceStatus(s service.Service) (service.Status, error) {
status, err := s.Status()
if err != nil && service.Platform() == "unix-systemv" {
return unixSystemVServiceStatus()
}
return status, err
}
func unixSystemVServiceStatus() (service.Status, error) {
out, err := exec.Command("/etc/init.d/ctrld", "status").CombinedOutput()
if err != nil {
return service.StatusUnknown, nil
}
switch string(bytes.TrimSpace(out)) {
case "running":
return service.StatusRunning, nil
default:
return service.StatusStopped, nil
}
}
+376 -67
View File
@@ -3,39 +3,74 @@ package ctrld
import ( import (
"context" "context"
"crypto/tls" "crypto/tls"
"crypto/x509"
"errors"
"math/rand"
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"strings" "strings"
"sync"
"time" "time"
"github.com/Control-D-Inc/ctrld/internal/dnsrcode"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/lucas-clemente/quic-go"
"github.com/lucas-clemente/quic-go/http3"
"github.com/miekg/dns" "github.com/miekg/dns"
"github.com/spf13/viper" "github.com/spf13/viper"
"golang.org/x/sync/singleflight"
"tailscale.com/logtail/backoff"
"github.com/Control-D-Inc/ctrld/internal/dnsrcode"
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
)
// IpStackBoth ...
const (
// IpStackBoth indicates that ctrld will use either ipv4 or ipv6 for connecting to upstream,
// depending on which stack is available when receiving the DNS query.
IpStackBoth = "both"
// IpStackV4 indicates that ctrld will use only ipv4 for connecting to upstream.
IpStackV4 = "v4"
// IpStackV6 indicates that ctrld will use only ipv6 for connecting to upstream.
IpStackV6 = "v6"
// IpStackSplit indicates that ctrld will use either ipv4 or ipv6 for connecting to upstream,
// depending on the record type of the DNS query.
IpStackSplit = "split"
controlDComDomain = "controld.com"
controlDNetDomain = "controld.net"
controlDDevDomain = "controld.dev"
)
var (
controldParentDomains = []string{controlDComDomain, controlDNetDomain, controlDDevDomain}
controldVerifiedDomain = map[string]string{
controlDComDomain: "verify.controld.com",
controlDDevDomain: "verify.controld.dev",
}
) )
// SetConfigName set the config name that ctrld will look for. // SetConfigName set the config name that ctrld will look for.
// DEPRECATED: use SetConfigNameWithPath instead.
func SetConfigName(v *viper.Viper, name string) { func SetConfigName(v *viper.Viper, name string) {
v.SetConfigName(name)
configPath := "$HOME" configPath := "$HOME"
// viper has its own way to get user home directory: https://github.com/spf13/viper/blob/v1.14.0/util.go#L134 // viper has its own way to get user home directory: https://github.com/spf13/viper/blob/v1.14.0/util.go#L134
// To be consistent, we prefer os.UserHomeDir instead. // To be consistent, we prefer os.UserHomeDir instead.
if homeDir, err := os.UserHomeDir(); err == nil { if homeDir, err := os.UserHomeDir(); err == nil {
configPath = homeDir configPath = homeDir
} }
SetConfigNameWithPath(v, name, configPath)
}
// SetConfigNameWithPath set the config path and name that ctrld will look for.
func SetConfigNameWithPath(v *viper.Viper, name, configPath string) {
v.SetConfigName(name)
v.AddConfigPath(configPath) v.AddConfigPath(configPath)
v.AddConfigPath(".") v.AddConfigPath(".")
} }
// InitConfig initializes default config values for given *viper.Viper instance. // InitConfig initializes default config values for given *viper.Viper instance.
func InitConfig(v *viper.Viper, name string) { func InitConfig(v *viper.Viper, name string) {
SetConfigName(v, name)
v.SetDefault("listener", map[string]*ListenerConfig{ v.SetDefault("listener", map[string]*ListenerConfig{
"0": { "0": {
IP: "127.0.0.1", IP: "127.0.0.1",
@@ -69,9 +104,20 @@ func InitConfig(v *viper.Viper, name string) {
// Config represents ctrld supported configuration. // Config represents ctrld supported configuration.
type Config struct { type Config struct {
Service ServiceConfig `mapstructure:"service" toml:"service,omitempty"` Service ServiceConfig `mapstructure:"service" toml:"service,omitempty"`
Listener map[string]*ListenerConfig `mapstructure:"listener" toml:"listener" validate:"min=1,dive"`
Network map[string]*NetworkConfig `mapstructure:"network" toml:"network" validate:"min=1,dive"` Network map[string]*NetworkConfig `mapstructure:"network" toml:"network" validate:"min=1,dive"`
Upstream map[string]*UpstreamConfig `mapstructure:"upstream" toml:"upstream" validate:"min=1,dive"` Upstream map[string]*UpstreamConfig `mapstructure:"upstream" toml:"upstream" validate:"min=1,dive"`
Listener map[string]*ListenerConfig `mapstructure:"listener" toml:"listener" validate:"min=1,dive"` }
// HasUpstreamSendClientInfo reports whether the config has any upstream
// is configured to send client info to Control D DNS server.
func (c *Config) HasUpstreamSendClientInfo() bool {
for _, uc := range c.Upstream {
if uc.UpstreamSendClientInfo() {
return true
}
}
return false
} }
// ServiceConfig specifies the general ctrld config. // ServiceConfig specifies the general ctrld config.
@@ -95,20 +141,36 @@ type NetworkConfig struct {
// UpstreamConfig specifies configuration for upstreams that ctrld will forward requests to. // UpstreamConfig specifies configuration for upstreams that ctrld will forward requests to.
type UpstreamConfig struct { type UpstreamConfig struct {
Name string `mapstructure:"name" toml:"name,omitempty"` Name string `mapstructure:"name" toml:"name,omitempty"`
Type string `mapstructure:"type" toml:"type,omitempty" validate:"oneof=doh doh3 dot doq os legacy"` Type string `mapstructure:"type" toml:"type,omitempty" validate:"oneof=doh doh3 dot doq os legacy"`
Endpoint string `mapstructure:"endpoint" toml:"endpoint,omitempty" validate:"required_unless=Type os"` Endpoint string `mapstructure:"endpoint" toml:"endpoint,omitempty" validate:"required_unless=Type os"`
BootstrapIP string `mapstructure:"bootstrap_ip" toml:"bootstrap_ip,omitempty"` BootstrapIP string `mapstructure:"bootstrap_ip" toml:"bootstrap_ip,omitempty"`
Domain string `mapstructure:"-" toml:"-"` Domain string `mapstructure:"-" toml:"-"`
Timeout int `mapstructure:"timeout" toml:"timeout,omitempty" validate:"gte=0"` IPStack string `mapstructure:"ip_stack" toml:"ip_stack,omitempty" validate:"ipstack"`
transport *http.Transport `mapstructure:"-" toml:"-"` Timeout int `mapstructure:"timeout" toml:"timeout,omitempty" validate:"gte=0"`
http3RoundTripper *http3.RoundTripper `mapstructure:"-" toml:"-"` // The caller should not access this field directly.
// Use UpstreamSendClientInfo instead.
SendClientInfo *bool `mapstructure:"send_client_info" toml:"send_client_info,omitempty"`
g singleflight.Group
mu sync.Mutex
bootstrapIPs []string
bootstrapIPs4 []string
bootstrapIPs6 []string
transport *http.Transport
transport4 *http.Transport
transport6 *http.Transport
http3RoundTripper http.RoundTripper
http3RoundTripper4 http.RoundTripper
http3RoundTripper6 http.RoundTripper
certPool *x509.CertPool
u *url.URL
} }
// ListenerConfig specifies the networks configuration that ctrld will run on. // ListenerConfig specifies the networks configuration that ctrld will run on.
type ListenerConfig struct { type ListenerConfig struct {
IP string `mapstructure:"ip" toml:"ip,omitempty" validate:"ip"` IP string `mapstructure:"ip" toml:"ip,omitempty" validate:"iporempty"`
Port int `mapstructure:"port" toml:"port,omitempty" validate:"gt=0"` Port int `mapstructure:"port" toml:"port,omitempty" validate:"gte=0"`
Restricted bool `mapstructure:"restricted" toml:"restricted,omitempty"` Restricted bool `mapstructure:"restricted" toml:"restricted,omitempty"`
Policy *ListenerPolicyConfig `mapstructure:"policy" toml:"policy,omitempty"` Policy *ListenerPolicyConfig `mapstructure:"policy" toml:"policy,omitempty"`
} }
@@ -131,19 +193,129 @@ type Rule map[string][]string
func (uc *UpstreamConfig) Init() { func (uc *UpstreamConfig) Init() {
if u, err := url.Parse(uc.Endpoint); err == nil { if u, err := url.Parse(uc.Endpoint); err == nil {
uc.Domain = u.Host uc.Domain = u.Host
switch uc.Type {
case ResolverTypeDOH, ResolverTypeDOH3:
uc.u = u
}
} }
if uc.Domain != "" { if uc.Domain == "" {
if !strings.Contains(uc.Endpoint, ":") {
uc.Domain = uc.Endpoint
uc.Endpoint = net.JoinHostPort(uc.Endpoint, defaultPortFor(uc.Type))
}
host, _, _ := net.SplitHostPort(uc.Endpoint)
uc.Domain = host
if net.ParseIP(uc.Domain) != nil {
uc.BootstrapIP = uc.Domain
}
}
if uc.IPStack == "" {
if uc.isControlD() {
uc.IPStack = IpStackSplit
} else {
uc.IPStack = IpStackBoth
}
}
}
// VerifyDomain returns the domain name that could be resolved by the upstream endpoint.
// It returns empty for non-ControlD upstream endpoint.
func (uc *UpstreamConfig) VerifyDomain() string {
domain := uc.Domain
if domain == "" {
if u, err := url.Parse(uc.Endpoint); err == nil {
domain = u.Hostname()
}
}
for _, parent := range controldParentDomains {
if dns.IsSubDomain(parent, domain) {
return controldVerifiedDomain[parent]
}
}
return ""
}
// UpstreamSendClientInfo reports whether the upstream is
// configured to send client info to Control D DNS server.
//
// Client info includes:
// - MAC
// - Lan IP
// - Hostname
func (uc *UpstreamConfig) UpstreamSendClientInfo() bool {
if uc.SendClientInfo != nil && !(*uc.SendClientInfo) {
return false
}
if uc.SendClientInfo == nil {
return true
}
switch uc.Type {
case ResolverTypeDOH, ResolverTypeDOH3:
if uc.isControlD() {
return true
}
}
return false
}
// BootstrapIPs returns the bootstrap IPs list of upstreams.
func (uc *UpstreamConfig) BootstrapIPs() []string {
return uc.bootstrapIPs
}
// SetCertPool sets the system cert pool used for TLS connections.
func (uc *UpstreamConfig) SetCertPool(cp *x509.CertPool) {
uc.certPool = cp
}
// SetupBootstrapIP manually find all available IPs of the upstream.
// The first usable IP will be used as bootstrap IP of the upstream.
func (uc *UpstreamConfig) SetupBootstrapIP() {
uc.setupBootstrapIP(true)
}
// SetupBootstrapIP manually find all available IPs of the upstream.
// The first usable IP will be used as bootstrap IP of the upstream.
func (uc *UpstreamConfig) setupBootstrapIP(withBootstrapDNS bool) {
b := backoff.NewBackoff("setupBootstrapIP", func(format string, args ...any) {}, 2*time.Second)
for {
uc.bootstrapIPs = lookupIP(uc.Domain, uc.Timeout, withBootstrapDNS)
if len(uc.bootstrapIPs) > 0 {
break
}
ProxyLog.Warn().Msg("could not resolve bootstrap IPs, retrying...")
b.BackOff(context.Background(), errors.New("no bootstrap IPs"))
}
for _, ip := range uc.bootstrapIPs {
if ctrldnet.IsIPv6(ip) {
uc.bootstrapIPs6 = append(uc.bootstrapIPs6, ip)
} else {
uc.bootstrapIPs4 = append(uc.bootstrapIPs4, ip)
}
}
ProxyLog.Debug().Msgf("Bootstrap IPs: %v", uc.bootstrapIPs)
}
// ReBootstrap re-setup the bootstrap IP and the transport.
func (uc *UpstreamConfig) ReBootstrap() {
switch uc.Type {
case ResolverTypeDOH, ResolverTypeDOH3:
default:
return return
} }
_, _, _ = uc.g.Do("ReBootstrap", func() (any, error) {
ProxyLog.Debug().Msg("re-bootstrapping upstream ip")
uc.setupTransportWithoutPingUpstream()
return true, nil
})
}
if !strings.Contains(uc.Endpoint, ":") { func (uc *UpstreamConfig) setupTransportWithoutPingUpstream() {
uc.Domain = uc.Endpoint switch uc.Type {
uc.Endpoint = net.JoinHostPort(uc.Endpoint, defaultPortFor(uc.Type)) case ResolverTypeDOH:
} uc.setupDOHTransportWithoutPingUpstream()
host, _, _ := net.SplitHostPort(uc.Endpoint) case ResolverTypeDOH3:
uc.Domain = host uc.setupDOH3TransportWithoutPingUpstream()
if net.ParseIP(uc.Domain) != nil {
uc.BootstrapIP = uc.Domain
} }
} }
@@ -159,51 +331,65 @@ func (uc *UpstreamConfig) SetupTransport() {
} }
func (uc *UpstreamConfig) setupDOHTransport() { func (uc *UpstreamConfig) setupDOHTransport() {
uc.transport = http.DefaultTransport.(*http.Transport).Clone() uc.setupDOHTransportWithoutPingUpstream()
uc.transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { go uc.pingUpstream()
dialer := &net.Dialer{
Timeout: 10 * time.Second,
KeepAlive: 10 * time.Second,
}
Log(ctx, ProxyLog.Debug(), "debug dial context %s - %s - %s", addr, network, bootstrapDNS)
// if we have a bootstrap ip set, use it to avoid DNS lookup
if uc.BootstrapIP != "" {
if _, port, _ := net.SplitHostPort(addr); port != "" {
addr = net.JoinHostPort(uc.BootstrapIP, port)
}
Log(ctx, ProxyLog.Debug(), "sending doh request to: %s", addr)
}
return dialer.DialContext(ctx, network, addr)
}
uc.pingUpstream()
} }
func (uc *UpstreamConfig) setupDOH3Transport() { func (uc *UpstreamConfig) newDOHTransport(addrs []string) *http.Transport {
uc.http3RoundTripper = &http3.RoundTripper{} transport := http.DefaultTransport.(*http.Transport).Clone()
uc.http3RoundTripper.Dial = func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) { transport.IdleConnTimeout = 5 * time.Second
host := addr transport.TLSClientConfig = &tls.Config{RootCAs: uc.certPool}
ProxyLog.Debug().Msgf("debug dial context D0H3 %s - %s", addr, bootstrapDNS)
// if we have a bootstrap ip set, use it to avoid DNS lookup
if uc.BootstrapIP != "" {
if _, port, _ := net.SplitHostPort(addr); port != "" {
addr = net.JoinHostPort(uc.BootstrapIP, port)
}
ProxyLog.Debug().Msgf("sending doh3 request to: %s", addr)
}
remoteAddr, err := net.ResolveUDPAddr("udp", addr)
if err != nil {
return nil, err
}
udpConn, err := net.ListenUDP("udp", nil) dialerTimeoutMs := 2000
if err != nil { if uc.Timeout > 0 && uc.Timeout < dialerTimeoutMs {
return nil, err dialerTimeoutMs = uc.Timeout
}
return quic.DialEarlyContext(ctx, udpConn, remoteAddr, host, tlsCfg, cfg)
} }
dialerTimeout := time.Duration(dialerTimeoutMs) * time.Millisecond
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
_, port, _ := net.SplitHostPort(addr)
if uc.BootstrapIP != "" {
dialer := net.Dialer{Timeout: dialerTimeout, KeepAlive: dialerTimeout}
addr := net.JoinHostPort(uc.BootstrapIP, port)
Log(ctx, ProxyLog.Debug(), "sending doh request to: %s", addr)
return dialer.DialContext(ctx, network, addr)
}
pd := &ctrldnet.ParallelDialer{}
pd.Timeout = dialerTimeout
pd.KeepAlive = dialerTimeout
dialAddrs := make([]string, len(addrs))
for i := range addrs {
dialAddrs[i] = net.JoinHostPort(addrs[i], port)
}
conn, err := pd.DialContext(ctx, network, dialAddrs)
if err != nil {
return nil, err
}
Log(ctx, ProxyLog.Debug(), "sending doh request to: %s", conn.RemoteAddr())
return conn, nil
}
return transport
}
uc.pingUpstream() func (uc *UpstreamConfig) setupDOHTransportWithoutPingUpstream() {
uc.mu.Lock()
defer uc.mu.Unlock()
switch uc.IPStack {
case IpStackBoth, "":
uc.transport = uc.newDOHTransport(uc.bootstrapIPs)
case IpStackV4:
uc.transport = uc.newDOHTransport(uc.bootstrapIPs4)
case IpStackV6:
uc.transport = uc.newDOHTransport(uc.bootstrapIPs6)
case IpStackSplit:
uc.transport4 = uc.newDOHTransport(uc.bootstrapIPs4)
if hasIPv6() {
uc.transport6 = uc.newDOHTransport(uc.bootstrapIPs6)
} else {
uc.transport6 = uc.transport4
}
uc.transport = uc.newDOHTransport(uc.bootstrapIPs)
}
} }
func (uc *UpstreamConfig) pingUpstream() { func (uc *UpstreamConfig) pingUpstream() {
@@ -221,6 +407,82 @@ func (uc *UpstreamConfig) pingUpstream() {
_, _ = dnsResolver.Resolve(ctx, msg) _, _ = dnsResolver.Resolve(ctx, msg)
} }
func (uc *UpstreamConfig) isControlD() bool {
domain := uc.Domain
if domain == "" {
if u, err := url.Parse(uc.Endpoint); err == nil {
domain = u.Hostname()
}
}
for _, parent := range controldParentDomains {
if dns.IsSubDomain(parent, domain) {
return true
}
}
return false
}
func (uc *UpstreamConfig) dohTransport(dnsType uint16) http.RoundTripper {
uc.mu.Lock()
defer uc.mu.Unlock()
switch uc.IPStack {
case IpStackBoth, IpStackV4, IpStackV6:
return uc.transport
case IpStackSplit:
switch dnsType {
case dns.TypeA:
return uc.transport4
default:
return uc.transport6
}
}
return uc.transport
}
func (uc *UpstreamConfig) bootstrapIPForDNSType(dnsType uint16) string {
switch uc.IPStack {
case IpStackBoth:
return pick(uc.bootstrapIPs)
case IpStackV4:
return pick(uc.bootstrapIPs4)
case IpStackV6:
return pick(uc.bootstrapIPs6)
case IpStackSplit:
switch dnsType {
case dns.TypeA:
return pick(uc.bootstrapIPs4)
default:
if hasIPv6() {
return pick(uc.bootstrapIPs6)
}
return pick(uc.bootstrapIPs4)
}
}
return pick(uc.bootstrapIPs)
}
func (uc *UpstreamConfig) netForDNSType(dnsType uint16) (string, string) {
switch uc.IPStack {
case IpStackBoth:
return "tcp-tls", "udp"
case IpStackV4:
return "tcp4-tls", "udp4"
case IpStackV6:
return "tcp6-tls", "udp6"
case IpStackSplit:
switch dnsType {
case dns.TypeA:
return "tcp4-tls", "udp4"
default:
if hasIPv6() {
return "tcp6-tls", "udp6"
}
return "tcp4-tls", "udp4"
}
}
return "tcp-tls", "udp"
}
// Init initialized necessary values for an ListenerConfig. // Init initialized necessary values for an ListenerConfig.
func (lc *ListenerConfig) Init() { func (lc *ListenerConfig) Init() {
if lc.Policy != nil { if lc.Policy != nil {
@@ -234,6 +496,8 @@ func (lc *ListenerConfig) Init() {
// ValidateConfig validates the given config. // ValidateConfig validates the given config.
func ValidateConfig(validate *validator.Validate, cfg *Config) error { func ValidateConfig(validate *validator.Validate, cfg *Config) error {
_ = validate.RegisterValidation("dnsrcode", validateDnsRcode) _ = validate.RegisterValidation("dnsrcode", validateDnsRcode)
_ = validate.RegisterValidation("ipstack", validateIpStack)
_ = validate.RegisterValidation("iporempty", validateIpOrEmpty)
return validate.Struct(cfg) return validate.Struct(cfg)
} }
@@ -241,6 +505,23 @@ func validateDnsRcode(fl validator.FieldLevel) bool {
return dnsrcode.FromString(fl.Field().String()) != -1 return dnsrcode.FromString(fl.Field().String()) != -1
} }
func validateIpStack(fl validator.FieldLevel) bool {
switch fl.Field().String() {
case IpStackBoth, IpStackV4, IpStackV6, IpStackSplit, "":
return true
default:
return false
}
}
func validateIpOrEmpty(fl validator.FieldLevel) bool {
val := fl.Field().String()
if val == "" {
return true
}
return net.ParseIP(val) != nil
}
func defaultPortFor(typ string) string { func defaultPortFor(typ string) string {
switch typ { switch typ {
case ResolverTypeDOH, ResolverTypeDOH3: case ResolverTypeDOH, ResolverTypeDOH3:
@@ -252,3 +533,31 @@ func defaultPortFor(typ string) string {
} }
return "53" return "53"
} }
// ResolverTypeFromEndpoint tries guessing the resolver type with a given endpoint
// using following rules:
//
// - If endpoint is an IP address -> ResolverTypeLegacy
// - If endpoint starts with "https://" -> ResolverTypeDOH
// - If endpoint starts with "quic://" -> ResolverTypeDOQ
// - For anything else -> ResolverTypeDOT
func ResolverTypeFromEndpoint(endpoint string) string {
switch {
case strings.HasPrefix(endpoint, "https://"):
return ResolverTypeDOH
case strings.HasPrefix(endpoint, "quic://"):
return ResolverTypeDOQ
}
host := endpoint
if strings.Contains(endpoint, ":") {
host, _, _ = net.SplitHostPort(host)
}
if ip := net.ParseIP(host); ip != nil {
return ResolverTypeLegacy
}
return ResolverTypeDOT
}
func pick(s []string) string {
return s[rand.Intn(len(s))]
}
+228
View File
@@ -0,0 +1,228 @@
package ctrld
import (
"net/url"
"testing"
"github.com/stretchr/testify/assert"
)
func TestUpstreamConfig_SetupBootstrapIP(t *testing.T) {
uc := &UpstreamConfig{
Name: "test",
Type: ResolverTypeDOH,
Endpoint: "https://freedns.controld.com/p2",
Timeout: 5000,
}
uc.Init()
uc.setupBootstrapIP(false)
if len(uc.bootstrapIPs) == 0 {
t.Log(nameservers())
t.Fatal("could not bootstrap ip without bootstrap DNS")
}
t.Log(uc)
}
func TestUpstreamConfig_Init(t *testing.T) {
u1, _ := url.Parse("https://example.com")
u2, _ := url.Parse("https://example.com?k=v")
tests := []struct {
name string
uc *UpstreamConfig
expected *UpstreamConfig
}{
{
"doh+doh3",
&UpstreamConfig{
Name: "doh",
Type: "doh",
Endpoint: "https://example.com",
BootstrapIP: "",
Domain: "",
Timeout: 0,
},
&UpstreamConfig{
Name: "doh",
Type: "doh",
Endpoint: "https://example.com",
BootstrapIP: "",
Domain: "example.com",
Timeout: 0,
IPStack: IpStackBoth,
u: u1,
},
},
{
"doh+doh3 with query param",
&UpstreamConfig{
Name: "doh",
Type: "doh",
Endpoint: "https://example.com?k=v",
BootstrapIP: "",
Domain: "",
Timeout: 0,
},
&UpstreamConfig{
Name: "doh",
Type: "doh",
Endpoint: "https://example.com?k=v",
BootstrapIP: "",
Domain: "example.com",
Timeout: 0,
IPStack: IpStackBoth,
u: u2,
},
},
{
"dot+doq",
&UpstreamConfig{
Name: "dot",
Type: "dot",
Endpoint: "freedns.controld.com:8853",
BootstrapIP: "",
Domain: "",
Timeout: 0,
},
&UpstreamConfig{
Name: "dot",
Type: "dot",
Endpoint: "freedns.controld.com:8853",
BootstrapIP: "",
Domain: "freedns.controld.com",
Timeout: 0,
IPStack: IpStackSplit,
},
},
{
"dot+doq without port",
&UpstreamConfig{
Name: "dot",
Type: "dot",
Endpoint: "freedns.controld.com",
BootstrapIP: "",
Domain: "",
Timeout: 0,
IPStack: IpStackSplit,
},
&UpstreamConfig{
Name: "dot",
Type: "dot",
Endpoint: "freedns.controld.com:853",
BootstrapIP: "",
Domain: "freedns.controld.com",
Timeout: 0,
IPStack: IpStackSplit,
},
},
{
"legacy",
&UpstreamConfig{
Name: "legacy",
Type: "legacy",
Endpoint: "1.2.3.4:53",
BootstrapIP: "",
Domain: "",
Timeout: 0,
},
&UpstreamConfig{
Name: "legacy",
Type: "legacy",
Endpoint: "1.2.3.4:53",
BootstrapIP: "1.2.3.4",
Domain: "1.2.3.4",
Timeout: 0,
IPStack: IpStackBoth,
},
},
{
"legacy without port",
&UpstreamConfig{
Name: "legacy",
Type: "legacy",
Endpoint: "1.2.3.4",
BootstrapIP: "",
Domain: "",
Timeout: 0,
},
&UpstreamConfig{
Name: "legacy",
Type: "legacy",
Endpoint: "1.2.3.4:53",
BootstrapIP: "1.2.3.4",
Domain: "1.2.3.4",
Timeout: 0,
IPStack: IpStackBoth,
},
},
{
"doh+doh3 with send client info set",
&UpstreamConfig{
Name: "doh",
Type: "doh",
Endpoint: "https://example.com?k=v",
BootstrapIP: "",
Domain: "",
Timeout: 0,
SendClientInfo: ptrBool(false),
IPStack: IpStackBoth,
},
&UpstreamConfig{
Name: "doh",
Type: "doh",
Endpoint: "https://example.com?k=v",
BootstrapIP: "",
Domain: "example.com",
Timeout: 0,
SendClientInfo: ptrBool(false),
IPStack: IpStackBoth,
u: u2,
},
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
tc.uc.Init()
assert.Equal(t, tc.expected, tc.uc)
})
}
}
func TestUpstreamConfig_VerifyDomain(t *testing.T) {
tests := []struct {
name string
uc *UpstreamConfig
verifyDomain string
}{
{
controlDComDomain,
&UpstreamConfig{Endpoint: "https://freedns.controld.com/p2"},
controldVerifiedDomain[controlDComDomain],
},
{
controlDDevDomain,
&UpstreamConfig{Endpoint: "https://freedns.controld.dev/p2"},
controldVerifiedDomain[controlDDevDomain],
},
{
"non-ControlD upstream",
&UpstreamConfig{Endpoint: "https://dns.google/dns-query"},
"",
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := tc.uc.VerifyDomain(); got != tc.verifyDomain {
t.Errorf("unexpected verify domain, want: %q, got: %q", tc.verifyDomain, got)
}
})
}
}
func ptrBool(b bool) *bool {
return &b
}
+157
View File
@@ -0,0 +1,157 @@
//go:build !qf
package ctrld
import (
"context"
"crypto/tls"
"errors"
"net"
"net/http"
"sync"
"time"
"github.com/miekg/dns"
"github.com/quic-go/quic-go"
"github.com/quic-go/quic-go/http3"
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
)
func (uc *UpstreamConfig) setupDOH3Transport() {
uc.setupDOH3TransportWithoutPingUpstream()
go uc.pingUpstream()
}
func (uc *UpstreamConfig) newDOH3Transport(addrs []string) http.RoundTripper {
rt := &http3.RoundTripper{}
rt.TLSClientConfig = &tls.Config{RootCAs: uc.certPool}
rt.Dial = func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
domain := addr
_, port, _ := net.SplitHostPort(addr)
// if we have a bootstrap ip set, use it to avoid DNS lookup
if uc.BootstrapIP != "" {
addr = net.JoinHostPort(uc.BootstrapIP, port)
ProxyLog.Debug().Msgf("sending doh3 request to: %s", addr)
udpConn, err := net.ListenUDP("udp", nil)
if err != nil {
return nil, err
}
remoteAddr, err := net.ResolveUDPAddr("udp", addr)
if err != nil {
return nil, err
}
return quic.DialEarlyContext(ctx, udpConn, remoteAddr, domain, tlsCfg, cfg)
}
dialAddrs := make([]string, len(addrs))
for i := range addrs {
dialAddrs[i] = net.JoinHostPort(addrs[i], port)
}
pd := &quicParallelDialer{}
conn, err := pd.Dial(ctx, domain, dialAddrs, tlsCfg, cfg)
if err != nil {
return nil, err
}
ProxyLog.Debug().Msgf("sending doh3 request to: %s", conn.RemoteAddr())
return conn, err
}
return rt
}
func (uc *UpstreamConfig) setupDOH3TransportWithoutPingUpstream() {
uc.mu.Lock()
defer uc.mu.Unlock()
switch uc.IPStack {
case IpStackBoth, "":
uc.http3RoundTripper = uc.newDOH3Transport(uc.bootstrapIPs)
case IpStackV4:
uc.http3RoundTripper = uc.newDOH3Transport(uc.bootstrapIPs4)
case IpStackV6:
uc.http3RoundTripper = uc.newDOH3Transport(uc.bootstrapIPs6)
case IpStackSplit:
uc.http3RoundTripper4 = uc.newDOH3Transport(uc.bootstrapIPs4)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if ctrldnet.IPv6Available(ctx) {
uc.http3RoundTripper6 = uc.newDOH3Transport(uc.bootstrapIPs6)
} else {
uc.http3RoundTripper6 = uc.http3RoundTripper4
}
uc.http3RoundTripper = uc.newDOH3Transport(uc.bootstrapIPs)
}
}
func (uc *UpstreamConfig) doh3Transport(dnsType uint16) http.RoundTripper {
uc.mu.Lock()
defer uc.mu.Unlock()
switch uc.IPStack {
case IpStackBoth, IpStackV4, IpStackV6:
return uc.http3RoundTripper
case IpStackSplit:
switch dnsType {
case dns.TypeA:
return uc.http3RoundTripper4
default:
return uc.http3RoundTripper6
}
}
return uc.http3RoundTripper
}
// Putting the code for quic parallel dialer here:
//
// - quic dialer is different with net.Dialer
// - simplification for quic free version
type parallelDialerResult struct {
conn quic.EarlyConnection
err error
}
type quicParallelDialer struct{}
// Dial performs parallel dialing to the given address list.
func (d *quicParallelDialer) Dial(ctx context.Context, domain string, addrs []string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
if len(addrs) == 0 {
return nil, errors.New("empty addresses")
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
ch := make(chan *parallelDialerResult, len(addrs))
var wg sync.WaitGroup
wg.Add(len(addrs))
go func() {
wg.Wait()
close(ch)
}()
udpConn, err := net.ListenUDP("udp", nil)
if err != nil {
return nil, err
}
for _, addr := range addrs {
go func(addr string) {
defer wg.Done()
remoteAddr, err := net.ResolveUDPAddr("udp", addr)
if err != nil {
ch <- &parallelDialerResult{conn: nil, err: err}
return
}
conn, err := quic.DialEarlyContext(ctx, udpConn, remoteAddr, domain, tlsCfg, cfg)
ch <- &parallelDialerResult{conn: conn, err: err}
}(addr)
}
errs := make([]error, 0, len(addrs))
for res := range ch {
if res.err == nil {
cancel()
return res.conn, res.err
}
errs = append(errs, res.err)
}
return nil, errors.Join(errs...)
}
+10
View File
@@ -0,0 +1,10 @@
//go:build qf
package ctrld
import "net/http"
func (uc *UpstreamConfig) setupDOH3Transport() {}
func (uc *UpstreamConfig) setupDOH3TransportWithoutPingUpstream() {}
func (uc *UpstreamConfig) doh3Transport(dnsType uint16) http.RoundTripper { return nil }
+13 -115
View File
@@ -24,10 +24,12 @@ func TestLoadConfig(t *testing.T) {
assert.Contains(t, cfg.Network, "0") assert.Contains(t, cfg.Network, "0")
assert.Contains(t, cfg.Network, "1") assert.Contains(t, cfg.Network, "1")
assert.Len(t, cfg.Upstream, 3) assert.Len(t, cfg.Upstream, 4)
assert.Contains(t, cfg.Upstream, "0") assert.Contains(t, cfg.Upstream, "0")
assert.Contains(t, cfg.Upstream, "1") assert.Contains(t, cfg.Upstream, "1")
assert.Contains(t, cfg.Upstream, "2") assert.Contains(t, cfg.Upstream, "2")
assert.Contains(t, cfg.Upstream, "3")
assert.NotNil(t, cfg.Upstream["3"].SendClientInfo)
assert.Len(t, cfg.Listener, 2) assert.Len(t, cfg.Listener, 2)
assert.Contains(t, cfg.Listener, "0") assert.Contains(t, cfg.Listener, "0")
@@ -42,6 +44,8 @@ func TestLoadConfig(t *testing.T) {
assert.Len(t, cfg.Listener["0"].Policy.Rules, 2) assert.Len(t, cfg.Listener["0"].Policy.Rules, 2)
assert.Contains(t, cfg.Listener["0"].Policy.Rules[0], "*.ru") assert.Contains(t, cfg.Listener["0"].Policy.Rules[0], "*.ru")
assert.Contains(t, cfg.Listener["0"].Policy.Rules[1], "*.local.host") assert.Contains(t, cfg.Listener["0"].Policy.Rules[1], "*.local.host")
assert.True(t, cfg.HasUpstreamSendClientInfo())
} }
func TestLoadDefaultConfig(t *testing.T) { func TestLoadDefaultConfig(t *testing.T) {
@@ -61,6 +65,7 @@ func TestConfigValidation(t *testing.T) {
{"invalid Config", &ctrld.Config{}, true}, {"invalid Config", &ctrld.Config{}, true},
{"default Config", defaultConfig(t), false}, {"default Config", defaultConfig(t), false},
{"sample Config", testhelper.SampleConfig(t), false}, {"sample Config", testhelper.SampleConfig(t), false},
{"empty listener IP", emptyListenerIP(t), false},
{"invalid cidr", invalidNetworkConfig(t), true}, {"invalid cidr", invalidNetworkConfig(t), true},
{"invalid upstream type", invalidUpstreamType(t), true}, {"invalid upstream type", invalidUpstreamType(t), true},
{"invalid upstream timeout", invalidUpstreamTimeout(t), true}, {"invalid upstream timeout", invalidUpstreamTimeout(t), true},
@@ -130,9 +135,15 @@ func invalidListenerIP(t *testing.T) *ctrld.Config {
return cfg return cfg
} }
func emptyListenerIP(t *testing.T) *ctrld.Config {
cfg := defaultConfig(t)
cfg.Listener["0"].IP = ""
return cfg
}
func invalidListenerPort(t *testing.T) *ctrld.Config { func invalidListenerPort(t *testing.T) *ctrld.Config {
cfg := defaultConfig(t) cfg := defaultConfig(t)
cfg.Listener["0"].Port = 0 cfg.Listener["0"].Port = -1
return cfg return cfg
} }
@@ -165,116 +176,3 @@ func configWithInvalidRcodes(t *testing.T) *ctrld.Config {
} }
return cfg return cfg
} }
func TestUpstreamConfig_Init(t *testing.T) {
tests := []struct {
name string
uc *ctrld.UpstreamConfig
expected *ctrld.UpstreamConfig
}{
{
"doh+doh3",
&ctrld.UpstreamConfig{
Name: "doh",
Type: "doh",
Endpoint: "https://example.com",
BootstrapIP: "",
Domain: "",
Timeout: 0,
},
&ctrld.UpstreamConfig{
Name: "doh",
Type: "doh",
Endpoint: "https://example.com",
BootstrapIP: "",
Domain: "example.com",
Timeout: 0,
},
},
{
"dot+doq",
&ctrld.UpstreamConfig{
Name: "dot",
Type: "dot",
Endpoint: "freedns.controld.com:8853",
BootstrapIP: "",
Domain: "",
Timeout: 0,
},
&ctrld.UpstreamConfig{
Name: "dot",
Type: "dot",
Endpoint: "freedns.controld.com:8853",
BootstrapIP: "",
Domain: "freedns.controld.com",
Timeout: 0,
},
},
{
"dot+doq without port",
&ctrld.UpstreamConfig{
Name: "dot",
Type: "dot",
Endpoint: "freedns.controld.com",
BootstrapIP: "",
Domain: "",
Timeout: 0,
},
&ctrld.UpstreamConfig{
Name: "dot",
Type: "dot",
Endpoint: "freedns.controld.com:853",
BootstrapIP: "",
Domain: "freedns.controld.com",
Timeout: 0,
},
},
{
"legacy",
&ctrld.UpstreamConfig{
Name: "legacy",
Type: "legacy",
Endpoint: "1.2.3.4:53",
BootstrapIP: "",
Domain: "",
Timeout: 0,
},
&ctrld.UpstreamConfig{
Name: "legacy",
Type: "legacy",
Endpoint: "1.2.3.4:53",
BootstrapIP: "1.2.3.4",
Domain: "1.2.3.4",
Timeout: 0,
},
},
{
"legacy without port",
&ctrld.UpstreamConfig{
Name: "legacy",
Type: "legacy",
Endpoint: "1.2.3.4",
BootstrapIP: "",
Domain: "",
Timeout: 0,
},
&ctrld.UpstreamConfig{
Name: "legacy",
Type: "legacy",
Endpoint: "1.2.3.4:53",
BootstrapIP: "1.2.3.4",
Domain: "1.2.3.4",
Timeout: 0,
},
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
tc.uc.Init()
assert.Equal(t, tc.expected, tc.uc)
})
}
}
+74 -27
View File
@@ -14,10 +14,15 @@ The config file allows for advanced configuration of the `ctrld` utility to cove
## Config Location ## Config Location
`ctrld` uses [TOML](toml_link) format for its configuration file. Default configuration file is `config.toml` found in following order: `ctrld` uses [TOML](toml_link) format for its configuration file. Default configuration file is `ctrld.toml` found in following order:
- `$HOME/.ctrld` - `/etc/controld` on *nix.
- Current directory - User's home directory on Windows.
- Same directory with `ctrld` binary on these routers:
- `ddwrt`
- `merlin`
- `freshtomato`
- Current directory.
The user can choose to override default value using command line `--config` or `-c`: The user can choose to override default value using command line `--config` or `-c`:
@@ -38,6 +43,8 @@ if it's existed.
log_path = "" log_path = ""
cache_enable = true cache_enable = true
cache_size = 4096 cache_size = 4096
cache_ttl_override = 60
cache_serve_stale = true
[network.0] [network.0]
cidrs = ["0.0.0.0/0"] cidrs = ["0.0.0.0/0"]
@@ -53,6 +60,7 @@ if it's existed.
name = "Control D - Anti-Malware" name = "Control D - Anti-Malware"
timeout = 5000 timeout = 5000
type = "doh" type = "doh"
ip_stack = "both"
[upstream.1] [upstream.1]
bootstrap_ip = "76.76.2.11" bootstrap_ip = "76.76.2.11"
@@ -60,6 +68,7 @@ if it's existed.
name = "Control D - No Ads" name = "Control D - No Ads"
timeout = 5000 timeout = 5000
type = "doq" type = "doq"
ip_stack = "split"
[upstream.2] [upstream.2]
bootstrap_ip = "76.76.2.22" bootstrap_ip = "76.76.2.22"
@@ -67,6 +76,7 @@ if it's existed.
name = "Control D - Private" name = "Control D - Private"
timeout = 5000 timeout = 5000
type = "dot" type = "dot"
ip_stack = "v4"
[listener.0] [listener.0]
ip = "127.0.0.1" ip = "127.0.0.1"
@@ -104,8 +114,8 @@ Logging level you wish to enable.
- Type: string - Type: string
- Required: no - Required: no
- Valid values: `debug`, `info`, `warn`, `error`, `fatal`, `panic` - Valid values: `debug`, `info`, `warn`, `notice`, `error`, `fatal`, `panic`
- Default: `info` - Default: `notice`
### log_path ### log_path
@@ -113,12 +123,14 @@ Relative or absolute path of the log file.
- Type: string - Type: string
- Required: no - Required: no
- Default: ""
### cache_enable ### cache_enable
When `cache_enable = true`, all resolved DNS query responses will be cached for duration of the upstream record TTLs. When `cache_enable = true`, all resolved DNS query responses will be cached for duration of the upstream record TTLs.
- Type: boolean - Type: boolean
- Required: no - Required: no
- Default: false
### cache_size ### cache_size
The number of cached records, must be a positive integer. Tweaking this value with care depends on your available RAM. The number of cached records, must be a positive integer. Tweaking this value with care depends on your available RAM.
@@ -128,29 +140,22 @@ An invalid `cache_size` value will disable the cache, regardless of `cache_enabl
- Type: int - Type: int
- Required: no - Required: no
- Default: 4096
### cache_ttl_override ### cache_ttl_override
When `cache_ttl_override` is set to a positive value (in seconds), TTLs are overridden to this value and cached for this long. When `cache_ttl_override` is set to a positive value (in seconds), TTLs are overridden to this value and cached for this long.
- Type: int - Type: int
- Required: no - Required: no
- Default: 0
### cache_serve_stale ### cache_serve_stale
When `cache_serve_stale = true`, in cases of upstream failures (upstreams not reachable), `ctrld` will keep serving When `cache_serve_stale = true`, in cases of upstream failures (upstreams not reachable), `ctrld` will keep serving
stale cached records (regardless of their TTLs) until upstream comes online. stale cached records (regardless of their TTLs) until upstream comes online.
The above config will look like this at query time. - Type: boolean
- Required: no
``` - Default: false
2022-11-14T22:18:53.808 INF Setting bootstrap IP for upstream.0 bootstrap_ip=76.76.2.11
2022-11-14T22:18:53.808 INF Starting DNS server on listener.0: 127.0.0.1:53
2022-11-14T22:18:56.381 DBG [9fd5d3] 127.0.0.1:53978 -> listener.0: 127.0.0.1:53: received query: verify.controld.com
2022-11-14T22:18:56.381 INF [9fd5d3] no policy, no network, no rule -> [upstream.0]
2022-11-14T22:18:56.381 DBG [9fd5d3] sending query to upstream.0: Control D - DOH Free
2022-11-14T22:18:56.381 DBG [9fd5d3] debug dial context freedns.controld.com:443 - tcp - 76.76.2.0
2022-11-14T22:18:56.381 DBG [9fd5d3] sending doh request to: 76.76.2.11:443
2022-11-14T22:18:56.420 DBG [9fd5d3] received response of 118 bytes in 39.662597ms
```
## Upstream ## Upstream
The `[upstream]` section specifies the DNS upstream servers that `ctrld` will forward DNS requests to. The `[upstream]` section specifies the DNS upstream servers that `ctrld` will forward DNS requests to.
@@ -162,6 +167,7 @@ The `[upstream]` section specifies the DNS upstream servers that `ctrld` will fo
name = "Control D - DOH" name = "Control D - DOH"
timeout = 5000 timeout = 5000
type = "doh" type = "doh"
ip_stack = "split"
[upstream.1] [upstream.1]
bootstrap_ip = "" bootstrap_ip = ""
@@ -169,6 +175,7 @@ The `[upstream]` section specifies the DNS upstream servers that `ctrld` will fo
name = "Control D - DOH3" name = "Control D - DOH3"
timeout = 5000 timeout = 5000
type = "doh3" type = "doh3"
ip_stack = "both"
[upstream.2] [upstream.2]
bootstrap_ip = "" bootstrap_ip = ""
@@ -176,6 +183,7 @@ The `[upstream]` section specifies the DNS upstream servers that `ctrld` will fo
name = "Controld D - DOT" name = "Controld D - DOT"
timeout = 5000 timeout = 5000
type = "dot" type = "dot"
ip_stack = "v4"
[upstream.3] [upstream.3]
bootstrap_ip = "" bootstrap_ip = ""
@@ -183,6 +191,7 @@ The `[upstream]` section specifies the DNS upstream servers that `ctrld` will fo
name = "Controld D - DOT" name = "Controld D - DOT"
timeout = 5000 timeout = 5000
type = "doq" type = "doq"
ip_stack = "v6"
[upstream.4] [upstream.4]
bootstrap_ip = "" bootstrap_ip = ""
@@ -190,6 +199,7 @@ The `[upstream]` section specifies the DNS upstream servers that `ctrld` will fo
name = "Control D - Ad Blocking" name = "Control D - Ad Blocking"
timeout = 5000 timeout = 5000
type = "legacy" type = "legacy"
ip_stack = "both"
``` ```
### bootstrap_ip ### bootstrap_ip
@@ -200,6 +210,7 @@ If `bootstrap_ip` is empty, `ctrld` will resolve this itself using its own boots
- type: ip address string - type: ip address string
- required: no - required: no
- Default: ""
### endpoint ### endpoint
IP address, hostname or URL of upstream DNS. Used together with `Type` of the endpoint. IP address, hostname or URL of upstream DNS. Used together with `Type` of the endpoint.
@@ -214,6 +225,7 @@ Human-readable name of the upstream.
- Type: string - Type: string
- Required: no - Required: no
- Default: ""
### timeout ### timeout
Timeout in milliseconds before request failsover to the next upstream (if defined). Timeout in milliseconds before request failsover to the next upstream (if defined).
@@ -221,15 +233,34 @@ Timeout in milliseconds before request failsover to the next upstream (if define
Value `0` means no timeout. Value `0` means no timeout.
- Type: number - Type: number
- required: no - Required: no
- Default: 0
### type ### type
The protocol that `ctrld` will use to send DNS requests to upstream. The protocol that `ctrld` will use to send DNS requests to upstream.
- Type: string - Type: string
- required: yes - Required: yes
- Valid values: `doh`, `doh3`, `dot`, `doq`, `legacy`, `os` - Valid values: `doh`, `doh3`, `dot`, `doq`, `legacy`, `os`
### ip_stack
Specifying what kind of ip stack that `ctrld` will use to connect to upstream.
- Type: string
- Required: no
- Valid values:
- `both`: using either ipv4 or ipv6.
- `v4`: only dial upstream via IPv4, never dial IPv6.
- `v6`: only dial upstream via IPv6, never dial IPv4.
- `split`:
- If `A` record is requested -> dial via ipv4.
- If `AAAA` or any other record is requested -> dial ipv6 (if available, otherwise ipv4)
If `ip_stack` is empty, or undefined:
- Default value is `both` for non-Control D resolvers.
- Default value is `split` for Control D resolvers.
## Network ## Network
The `[network]` section defines networks from which DNS queries can originate from. These are used in policies. You can define multiple networks, and each one can have multiple cidrs. The `[network]` section defines networks from which DNS queries can originate from. These are used in policies. You can define multiple networks, and each one can have multiple cidrs.
@@ -248,12 +279,14 @@ Name of the network.
- Type: string - Type: string
- Required: no - Required: no
- Default: ""
### cidrs ### cidrs
Specifies the network addresses that the `listener` will accept requests from. You will see more details in the listener policy section. Specifies the network addresses that the `listener` will accept requests from. You will see more details in the listener policy section.
- Type: array of network CIDR string - Type: array of network CIDR string
- Required: no - Required: no
- Default: []
## listener ## listener
@@ -271,22 +304,25 @@ The `[listener]` section specifies the ip and port of the local DNS server. You
``` ```
### ip ### ip
IP address that serves the incoming requests. IP address that serves the incoming requests. If `ip` is empty, ctrld will listen on all available addresses.
- Type: string - Type: ip address string
- Required: yes - Required: no
- Default: ""
### port ### port
Port number that the listener will listen on for incoming requests. Port number that the listener will listen on for incoming requests. If `port` is `0`, a random available port will be chosen.
- Type: number - Type: number
- Required: yes - Required: no
- Default: 0
### restricted ### restricted
If set to `true` makes the listener `REFUSE` DNS queries from all source IP addresses that are not explicitly defined in the policy using a `network`. If set to `true` makes the listener `REFUSE` DNS queries from all source IP addresses that are not explicitly defined in the policy using a `network`.
- Type: bool - Type: bool
- Required: no - Required: no
- Default: false
### policy ### policy
Allows `ctrld` to set policy rules to determine which upstreams the requests will be forwarded to. Allows `ctrld` to set policy rules to determine which upstreams the requests will be forwarded to.
@@ -330,19 +366,30 @@ rules = [
- Type: string - Type: string
- Required: no - Required: no
- Default: ""
### networks: ### networks:
`networks` is the list of network rules of the policy. `networks` is the list of network rules of the policy.
- type: array of networks - Type: array of networks
- Required: no
- Default: []
### rules: ### rules:
`rules` is the list of domain rules within the policy. Domain can be either FQDN or wildcard domain. `rules` is the list of domain rules within the policy. Domain can be either FQDN or wildcard domain.
- type: array of rule - Type: array of rule
- Required: no
- Default: []
### failover_rcodes ### failover_rcodes
For non success response, `failover_rcodes` allows the request to be forwarded to next upstream, if the response `RCODE` matches any value defined in `failover_rcodes`. For example: For non success response, `failover_rcodes` allows the request to be forwarded to next upstream, if the response `RCODE` matches any value defined in `failover_rcodes`.
- Type: array of string
- Required: no
- Default: []
-
For example:
```toml ```toml
[listener.0.policy] [listener.0.policy]
+60 -15
View File
@@ -3,29 +3,39 @@ package ctrld
import ( import (
"context" "context"
"encoding/base64" "encoding/base64"
"errors"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"net/url"
"github.com/lucas-clemente/quic-go/http3"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
const (
dohMacHeader = "x-cd-mac"
dohIPHeader = "x-cd-ip"
dohHostHeader = "x-cd-host"
headerApplicationDNS = "application/dns-message"
)
func newDohResolver(uc *UpstreamConfig) *dohResolver { func newDohResolver(uc *UpstreamConfig) *dohResolver {
r := &dohResolver{ r := &dohResolver{
endpoint: uc.Endpoint, endpoint: uc.u,
isDoH3: uc.Type == ResolverTypeDOH3, isDoH3: uc.Type == ResolverTypeDOH3,
transport: uc.transport,
http3RoundTripper: uc.http3RoundTripper, http3RoundTripper: uc.http3RoundTripper,
sendClientInfo: uc.UpstreamSendClientInfo(),
uc: uc,
} }
return r return r
} }
type dohResolver struct { type dohResolver struct {
endpoint string uc *UpstreamConfig
endpoint *url.URL
isDoH3 bool isDoH3 bool
transport *http.Transport http3RoundTripper http.RoundTripper
http3RoundTripper *http3.RoundTripper sendClientInfo bool
} }
func (r *dohResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) { func (r *dohResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) {
@@ -33,23 +43,36 @@ func (r *dohResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, erro
if err != nil { if err != nil {
return nil, err return nil, err
} }
enc := base64.RawURLEncoding.EncodeToString(data) enc := base64.RawURLEncoding.EncodeToString(data)
url := fmt.Sprintf("%s?dns=%s", r.endpoint, enc) query := r.endpoint.Query()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) query.Add("dns", enc)
endpoint := *r.endpoint
endpoint.RawQuery = query.Encode()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not create request: %w", err) return nil, fmt.Errorf("could not create request: %w", err)
} }
req.Header.Set("Content-Type", "application/dns-message") addHeader(ctx, req, r.sendClientInfo)
req.Header.Set("Accept", "application/dns-message") dnsTyp := uint16(0)
if len(msg.Question) > 0 {
c := http.Client{Transport: r.transport} dnsTyp = msg.Question[0].Qtype
}
c := http.Client{Transport: r.uc.dohTransport(dnsTyp)}
if r.isDoH3 { if r.isDoH3 {
c.Transport = r.http3RoundTripper transport := r.uc.doh3Transport(dnsTyp)
if transport == nil {
return nil, errors.New("DoH3 is not supported")
}
c.Transport = transport
} }
resp, err := c.Do(req) resp, err := c.Do(req)
if err != nil { if err != nil {
if r.isDoH3 { if r.isDoH3 {
r.http3RoundTripper.Close() if closer, ok := c.Transport.(io.Closer); ok {
closer.Close()
}
} }
return nil, fmt.Errorf("could not perform request: %w", err) return nil, fmt.Errorf("could not perform request: %w", err)
} }
@@ -65,5 +88,27 @@ func (r *dohResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, erro
} }
answer := new(dns.Msg) answer := new(dns.Msg)
return answer, answer.Unpack(buf) if err := answer.Unpack(buf); err != nil {
return nil, fmt.Errorf("answer.Unpack: %w", err)
}
return answer, nil
}
func addHeader(ctx context.Context, req *http.Request, sendClientInfo bool) {
req.Header.Set("Content-Type", headerApplicationDNS)
req.Header.Set("Accept", headerApplicationDNS)
if sendClientInfo {
if ci, ok := ctx.Value(ClientInfoCtxKey{}).(*ClientInfo); ok && ci != nil {
if ci.Mac != "" {
req.Header.Set(dohMacHeader, ci.Mac)
}
if ci.IP != "" {
req.Header.Set(dohIPHeader, ci.IP)
}
if ci.Hostname != "" {
req.Header.Set(dohHostHeader, ci.Hostname)
}
}
}
Log(ctx, ProxyLog.Debug().Interface("header", req.Header), "sending request header")
} }
+13 -5
View File
@@ -1,3 +1,5 @@
//go:build !qf
package ctrld package ctrld
import ( import (
@@ -7,8 +9,8 @@ import (
"net" "net"
"time" "time"
"github.com/lucas-clemente/quic-go"
"github.com/miekg/dns" "github.com/miekg/dns"
"github.com/quic-go/quic-go"
) )
type doqResolver struct { type doqResolver struct {
@@ -18,11 +20,17 @@ type doqResolver struct {
func (r *doqResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) { func (r *doqResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) {
endpoint := r.uc.Endpoint endpoint := r.uc.Endpoint
tlsConfig := &tls.Config{NextProtos: []string{"doq"}} tlsConfig := &tls.Config{NextProtos: []string{"doq"}}
if r.uc.BootstrapIP != "" { ip := r.uc.BootstrapIP
tlsConfig.ServerName = r.uc.Domain if ip == "" {
_, port, _ := net.SplitHostPort(endpoint) dnsTyp := uint16(0)
endpoint = net.JoinHostPort(r.uc.BootstrapIP, port) if msg != nil && len(msg.Question) > 0 {
dnsTyp = msg.Question[0].Qtype
}
ip = r.uc.bootstrapIPForDNSType(dnsTyp)
} }
tlsConfig.ServerName = r.uc.Domain
_, port, _ := net.SplitHostPort(endpoint)
endpoint = net.JoinHostPort(ip, port)
return resolve(ctx, msg, endpoint, tlsConfig) return resolve(ctx, msg, endpoint, tlsConfig)
} }
+18
View File
@@ -0,0 +1,18 @@
//go:build qf
package ctrld
import (
"context"
"errors"
"github.com/miekg/dns"
)
type doqResolver struct {
uc *UpstreamConfig
}
func (r *doqResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) {
return nil, errors.New("DoQ is not supported")
}
+12 -4
View File
@@ -14,18 +14,26 @@ type dotResolver struct {
func (r *dotResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) { func (r *dotResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) {
// The dialer is used to prevent bootstrapping cycle. // The dialer is used to prevent bootstrapping cycle.
// If r.endpoing is set to dns.controld.dev, we need to resolve // If r.endpoint is set to dns.controld.dev, we need to resolve
// dns.controld.dev first. By using a dialer with custom resolver, // dns.controld.dev first. By using a dialer with custom resolver,
// we ensure that we can always resolve the bootstrap domain // we ensure that we can always resolve the bootstrap domain
// regardless of the machine DNS status. // regardless of the machine DNS status.
dialer := newDialer(net.JoinHostPort(bootstrapDNS, "53")) dialer := newDialer(net.JoinHostPort(bootstrapDNS, "53"))
dnsTyp := uint16(0)
if msg != nil && len(msg.Question) > 0 {
dnsTyp = msg.Question[0].Qtype
}
tcpNet, _ := r.uc.netForDNSType(dnsTyp)
dnsClient := &dns.Client{ dnsClient := &dns.Client{
Net: "tcp-tls", Net: tcpNet,
Dialer: dialer, Dialer: dialer,
TLSConfig: &tls.Config{RootCAs: r.uc.certPool},
} }
endpoint := r.uc.Endpoint endpoint := r.uc.Endpoint
if r.uc.BootstrapIP != "" { if r.uc.BootstrapIP != "" {
dnsClient.TLSConfig = &tls.Config{ServerName: r.uc.Domain} dnsClient.TLSConfig.ServerName = r.uc.Domain
dnsClient.Net = "tcp-tls"
_, port, _ := net.SplitHostPort(endpoint) _, port, _ := net.SplitHostPort(endpoint)
endpoint = net.JoinHostPort(r.uc.BootstrapIP, port) endpoint = net.JoinHostPort(r.uc.BootstrapIP, port)
} }
+31 -24
View File
@@ -1,76 +1,83 @@
module github.com/Control-D-Inc/ctrld module github.com/Control-D-Inc/ctrld
go 1.19 go 1.20
require ( require (
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534 github.com/coreos/go-systemd/v22 v22.5.0
github.com/cuonglm/osinfo v0.0.0-20230329055532-c513f836da19
github.com/frankban/quicktest v1.14.3 github.com/frankban/quicktest v1.14.3
github.com/fsnotify/fsnotify v1.6.0
github.com/go-playground/validator/v10 v10.11.1 github.com/go-playground/validator/v10 v10.11.1
github.com/godbus/dbus/v5 v5.0.6 github.com/godbus/dbus/v5 v5.0.6
github.com/hashicorp/golang-lru/v2 v2.0.1 github.com/hashicorp/golang-lru/v2 v2.0.1
github.com/illarion/gonotify v1.0.1 github.com/illarion/gonotify v1.0.1
github.com/insomniacslk/dhcp v0.0.0-20211209223715-7d93572ebe8e github.com/insomniacslk/dhcp v0.0.0-20221215072855-de60144f33f8
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86
github.com/kardianos/service v1.2.1 github.com/kardianos/service v1.2.1
github.com/lucas-clemente/quic-go v0.29.1
github.com/miekg/dns v1.1.50 github.com/miekg/dns v1.1.50
github.com/pelletier/go-toml/v2 v2.0.6 github.com/pelletier/go-toml/v2 v2.0.6
github.com/quic-go/quic-go v0.32.0
github.com/rs/zerolog v1.28.0 github.com/rs/zerolog v1.28.0
github.com/spf13/cobra v1.4.0 github.com/spf13/cobra v1.4.0
github.com/spf13/viper v1.14.0 github.com/spf13/viper v1.14.0
github.com/stretchr/testify v1.8.1 github.com/stretchr/testify v1.8.1
golang.org/x/sys v0.4.0 github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54
golang.org/x/net v0.7.0
golang.org/x/sync v0.1.0
golang.org/x/sys v0.5.0
golang.zx2c4.com/wireguard/windows v0.5.3 golang.zx2c4.com/wireguard/windows v0.5.3
tailscale.com v1.34.1 tailscale.com v1.38.3
) )
require ( require (
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 // indirect github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-playground/locales v0.14.0 // indirect github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
github.com/golang/mock v1.6.0 // indirect github.com/golang/mock v1.6.0 // indirect
github.com/google/go-cmp v0.5.9 // indirect github.com/google/go-cmp v0.5.9 // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/josharian/native v1.0.0 // indirect
github.com/jsimonetti/rtnetlink v1.1.2-0.20220408201609-d380b505068b // indirect github.com/jsimonetti/rtnetlink v1.1.2-0.20220408201609-d380b505068b // indirect
github.com/kr/pretty v0.3.0 // indirect github.com/kr/pretty v0.3.0 // indirect
github.com/kr/text v0.2.0 // indirect github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.2.1 // indirect github.com/leodido/go-urn v1.2.1 // indirect
github.com/magiconair/properties v1.8.7 // indirect github.com/magiconair/properties v1.8.7 // indirect
github.com/marten-seemann/qpack v0.2.1 // indirect
github.com/marten-seemann/qtls-go1-18 v0.1.2 // indirect
github.com/marten-seemann/qtls-go1-19 v0.1.0 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 // indirect github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 // indirect
github.com/mdlayher/netlink v1.6.0 // indirect github.com/mdlayher/netlink v1.7.1 // indirect
github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 // indirect github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 // indirect
github.com/mdlayher/socket v0.2.3 // indirect github.com/mdlayher/socket v0.4.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/nxadm/tail v1.4.8 // indirect github.com/onsi/ginkgo/v2 v2.2.0 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/qtls-go1-18 v0.2.0 // indirect
github.com/quic-go/qtls-go1-19 v0.2.0 // indirect
github.com/quic-go/qtls-go1-20 v0.1.0 // indirect
github.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4 // indirect github.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4 // indirect
github.com/spf13/afero v1.9.3 // indirect github.com/spf13/afero v1.9.3 // indirect
github.com/spf13/cast v1.5.0 // indirect github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.4.1 // indirect github.com/subosito/gotenv v1.4.1 // indirect
github.com/u-root/uio v0.0.0-20220204230159-dac05f7d2cb4 // indirect github.com/u-root/uio v0.0.0-20221213070652-c3537552635f // indirect
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect
go4.org/mem v0.0.0-20210711025021-927187094b94 // indirect go4.org/mem v0.0.0-20210711025021-927187094b94 // indirect
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 // indirect golang.org/x/crypto v0.6.0 // indirect
golang.org/x/exp v0.0.0-20220827204233-334a2380cb91 // indirect golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect golang.org/x/mod v0.7.0 // indirect
golang.org/x/net v0.5.1-0.20230105164244-f8411da775a6 // indirect golang.org/x/text v0.7.0 // indirect
golang.org/x/sync v0.1.0 // indirect golang.org/x/tools v0.4.1-0.20221208213631-3f74d914ae6d // indirect
golang.org/x/text v0.6.0 // indirect
golang.org/x/tools v0.1.12 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )
replace github.com/mr-karan/doggo => github.com/Windscribe/doggo v0.0.0-20220919152748-2c118fc391f8
replace github.com/rs/zerolog => github.com/Windscribe/zerolog v0.0.0-20230503170159-e6aa153233be
+57 -66
View File
@@ -38,6 +38,8 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Windscribe/zerolog v0.0.0-20230503170159-e6aa153233be h1:qBKVRi7Mom5heOkyZ+NCIu9HZBiNCsRqrRe5t9pooik=
github.com/Windscribe/zerolog v0.0.0-20230503170159-e6aa153233be/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w=
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA= github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA=
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -50,10 +52,12 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534 h1:rtAn27wIbmOGUs7RIbVgPEjb31ehTVniDwPGXyMxm5U= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cuonglm/osinfo v0.0.0-20230329055532-c513f836da19 h1:7P/f19Mr0oa3ug8BYt4JuRe/Zq3dF4Mrr4m8+Kw+Hcs=
github.com/cuonglm/osinfo v0.0.0-20230329055532-c513f836da19/go.mod h1:G45410zMgmnSjLVKCq4f6GpbYAzoP2plX9rPwgx6C24=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -67,8 +71,6 @@ github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72/go.mod h1:Pjfxu
github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og= github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og=
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
@@ -143,6 +145,8 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
@@ -155,7 +159,6 @@ github.com/hashicorp/golang-lru/v2 v2.0.1 h1:5pv5N1lT1fjLg2VQ5KWc7kmucp2x/kvFOnx
github.com/hashicorp/golang-lru/v2 v2.0.1/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/golang-lru/v2 v2.0.1/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 h1:/jC7qQFrv8CrSJVmaolDVOxTfS9kc36uB6H40kdbQq8= github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 h1:/jC7qQFrv8CrSJVmaolDVOxTfS9kc36uB6H40kdbQq8=
github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis= github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
@@ -164,10 +167,12 @@ github.com/illarion/gonotify v1.0.1 h1:F1d+0Fgbq/sDWjj/r66ekjDG+IDeecQKUFH4wNwso
github.com/illarion/gonotify v1.0.1/go.mod h1:zt5pmDofZpU1f8aqlK0+95eQhoEAn/d4G4B/FjVW4jE= github.com/illarion/gonotify v1.0.1/go.mod h1:zt5pmDofZpU1f8aqlK0+95eQhoEAn/d4G4B/FjVW4jE=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/insomniacslk/dhcp v0.0.0-20211209223715-7d93572ebe8e h1:IQpunlq7T+NiJJMO7ODYV2YWBiv/KnObR3gofX0mWOo= github.com/insomniacslk/dhcp v0.0.0-20221215072855-de60144f33f8 h1:Z72DOke2yOK0Ms4Z2LK1E1OrRJXOxSj5DllTz2FYTRg=
github.com/insomniacslk/dhcp v0.0.0-20211209223715-7d93572ebe8e/go.mod h1:h+MxyHxRg9NH3terB1nfRIUaQEcI0XOVkdR9LNBlp8E= github.com/insomniacslk/dhcp v0.0.0-20221215072855-de60144f33f8/go.mod h1:m5WMe03WCvWcXjRnhvaAbAAXdCnu20J5P+mmH44ZzpE=
github.com/josharian/native v1.0.0 h1:Ts/E8zCSEsG17dUqv7joXJFybuMLjQfWE04tsBODTxk=
github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 h1:elKwZS1OcdQ0WwEDBeqxKwb7WB62QX8bvZ/FJnVXIfk=
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86/go.mod h1:aFAMtuldEgx/4q7iSGazk22+IcgvtiC+HIimFO9XlS8=
github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw= github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw=
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ= github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ=
github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok= github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok=
@@ -191,16 +196,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/lucas-clemente/quic-go v0.29.1 h1:Z+WMJ++qMLhvpFkRZA+jl3BTxUjm415YBmWanXB8zP0=
github.com/lucas-clemente/quic-go v0.29.1/go.mod h1:CTcNfLYJS2UuRNB+zcNlgvkjBhxX6Hm3WUxxAQx2mgE=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/marten-seemann/qpack v0.2.1 h1:jvTsT/HpCn2UZJdP+UUB53FfUUgeOyG5K1ns0OJOGVs=
github.com/marten-seemann/qpack v0.2.1/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc=
github.com/marten-seemann/qtls-go1-18 v0.1.2 h1:JH6jmzbduz0ITVQ7ShevK10Av5+jBEKAHMntXmIV7kM=
github.com/marten-seemann/qtls-go1-18 v0.1.2/go.mod h1:mJttiymBAByA49mhlNZZGrH5u1uXYZJ+RW28Py7f4m4=
github.com/marten-seemann/qtls-go1-19 v0.1.0 h1:rLFKD/9mp/uq1SYGYuVZhm83wkmU95pK5df3GufyYYU=
github.com/marten-seemann/qtls-go1-19 v0.1.0/go.mod h1:5HTDWtVudo/WFsHKRNuOhWlbdjrfs5JHrYb0wIJqGpI=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
@@ -211,28 +208,21 @@ github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE
github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M= github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M=
github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY= github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY=
github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o= github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o=
github.com/mdlayher/netlink v1.6.0 h1:rOHX5yl7qnlpiVkFWoqccueppMtXzeziFjWAjLg6sz0=
github.com/mdlayher/netlink v1.6.0/go.mod h1:0o3PlBmGst1xve7wQ7j/hwpNaFaH4qCRyWCdcZk8/vA= github.com/mdlayher/netlink v1.6.0/go.mod h1:0o3PlBmGst1xve7wQ7j/hwpNaFaH4qCRyWCdcZk8/vA=
github.com/mdlayher/netlink v1.7.1 h1:FdUaT/e33HjEXagwELR8R3/KL1Fq5x3G5jgHLp/BTmg=
github.com/mdlayher/netlink v1.7.1/go.mod h1:nKO5CSjE/DJjVhk/TNp6vCE1ktVxEA8VEh8drhZzxsQ=
github.com/mdlayher/raw v0.0.0-20190606142536-fef19f00fc18/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= github.com/mdlayher/raw v0.0.0-20190606142536-fef19f00fc18/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg=
github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 h1:aFkJ6lx4FPip+S+Uw4aTegFMct9shDvP+79PsSxpm3w= github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 h1:aFkJ6lx4FPip+S+Uw4aTegFMct9shDvP+79PsSxpm3w=
github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg=
github.com/mdlayher/socket v0.1.1/go.mod h1:mYV5YIZAfHh4dzDVzI8x8tWLWCliuX8Mon5Awbj+qDs= github.com/mdlayher/socket v0.1.1/go.mod h1:mYV5YIZAfHh4dzDVzI8x8tWLWCliuX8Mon5Awbj+qDs=
github.com/mdlayher/socket v0.2.3 h1:XZA2X2TjdOwNoNPVPclRCURoX/hokBY8nkTmRZFEheM= github.com/mdlayher/socket v0.4.0 h1:280wsy40IC9M9q1uPGcLBwXpcTQDtoGwVt+BNoITxIw=
github.com/mdlayher/socket v0.2.3/go.mod h1:bz12/FozYNH/VbvC3q7TRIK/Y6dH1kCKsXaUeXi/FmY= github.com/mdlayher/socket v0.4.0/go.mod h1:xxFqz5GRCUN3UEOm9CZqEJsAbe1C8OwSK46NlmWuVoc=
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/onsi/ginkgo/v2 v2.2.0 h1:3ZNA3L1c5FYDFTTxbFeVGGD8jYvjYauHD30YgLxVsNI=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/onsi/ginkgo/v2 v2.2.0/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q= github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
@@ -244,14 +234,22 @@ github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qR
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
github.com/quic-go/qtls-go1-18 v0.2.0 h1:5ViXqBZ90wpUcZS0ge79rf029yx0dYB0McyPJwqqj7U=
github.com/quic-go/qtls-go1-18 v0.2.0/go.mod h1:moGulGHK7o6O8lSPSZNoOwcLvJKJ85vVNc7oJFD65bc=
github.com/quic-go/qtls-go1-19 v0.2.0 h1:Cvn2WdhyViFUHoOqK52i51k4nDX8EwIh5VJiVM4nttk=
github.com/quic-go/qtls-go1-19 v0.2.0/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI=
github.com/quic-go/qtls-go1-20 v0.1.0 h1:d1PK3ErFy9t7zxKsG3NXBJXZjp/kMLoIb3y/kV54oAI=
github.com/quic-go/qtls-go1-20 v0.1.0/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM=
github.com/quic-go/quic-go v0.32.0 h1:lY02md31s1JgPiiyfqJijpu/UX/Iun304FI3yUqX7tA=
github.com/quic-go/quic-go v0.32.0/go.mod h1:/fCsKANhQIeD5l76c2JFU+07gVE3KaA0FP+0zMWwfwo=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4 h1:Ha8xCaq6ln1a+R91Km45Oq6lPXj2Mla6CRJYcuV2h1w= github.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4 h1:Ha8xCaq6ln1a+R91Km45Oq6lPXj2Mla6CRJYcuV2h1w=
github.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY=
github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
@@ -282,9 +280,13 @@ github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKs
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs=
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/u-root/uio v0.0.0-20210528114334-82958018845c/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA= github.com/u-root/uio v0.0.0-20221213070652-c3537552635f h1:dpx1PHxYqAnXzbryJrWP1NQLzEjwcVgFLhkknuFQ7ww=
github.com/u-root/uio v0.0.0-20220204230159-dac05f7d2cb4 h1:hl6sK6aFgTLISijk6xIzeqnPzQcsLqqvL6vEfTPinME= github.com/u-root/uio v0.0.0-20221213070652-c3537552635f/go.mod h1:IogEAUBXDEwX7oR/BMmCctShYs80ql4hF0ySdzGxf7E=
github.com/u-root/uio v0.0.0-20220204230159-dac05f7d2cb4/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA= github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54 h1:8mhqcHPqTMhSPoslhGYihEgSfc77+7La1P6kiB6+9So=
github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695APm9hlsSMoOoE65U4/TcqNj90mc69Rlg=
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -306,8 +308,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM= golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -318,8 +320,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20220827204233-334a2380cb91 h1:tnebWN09GYg9OLPss1KXj8txwZc6X6uMr6VFdcGNbHw= golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o=
golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -344,11 +346,10 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -372,7 +373,6 @@ golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
@@ -389,8 +389,8 @@ golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.5.1-0.20230105164244-f8411da775a6 h1:pKt/LWZC6+FwNujj5E7DdVyWcbtQvKqPuN0GPKWMyB8= golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.5.1-0.20230105164244-f8411da775a6/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -414,7 +414,6 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -427,17 +426,15 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -445,8 +442,8 @@ golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -456,14 +453,12 @@ golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -471,9 +466,11 @@ golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -484,8 +481,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -534,14 +531,13 @@ golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82u
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= golang.org/x/tools v0.4.1-0.20221208213631-3f74d914ae6d h1:9ZNWAi4CYhNv60mXGgAncgq7SGc5qa7C8VZV8Tg7Ggs=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.4.1-0.20221208213631-3f74d914ae6d/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -642,14 +638,9 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 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/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 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.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@@ -666,5 +657,5 @@ honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
tailscale.com v1.34.1 h1:tqm9Ww4ltyYp3IPe7vCGch6tT6j5G/WXPQ6BrVZ6pdI= tailscale.com v1.38.3 h1:2aX3+u0Re8QcN6nq7zf9Aa4ZCR2Nf6Imv3isqdQrb58=
tailscale.com v1.34.1/go.mod h1:ZsBP7rjzzB2rp+UCOumr9DAe0EQ6OPivwSXcz/BrekQ= tailscale.com v1.38.3/go.mod h1:UWLQxcd8dz+lds2I+HpfXSruHrvXM1j4zd4zdx86t7w=
File diff suppressed because it is too large Load Diff
+22
View File
@@ -0,0 +1,22 @@
package certs
import (
"crypto/x509"
_ "embed"
"sync"
)
var (
//go:embed cacert.pem
caRoots []byte
caCertPoolOnce sync.Once
caCertPool *x509.CertPool
)
func CACertPool() *x509.CertPool {
caCertPoolOnce.Do(func() {
caCertPool = x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caRoots)
})
return caCertPool
}
+27
View File
@@ -0,0 +1,27 @@
package certs
import (
"crypto/tls"
"net/http"
"testing"
"time"
)
func TestCACertPool(t *testing.T) {
c := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: CACertPool(),
},
},
Timeout: 2 * time.Second,
}
resp, err := c.Get("https://freedns.controld.com/p1")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if !resp.TLS.HandshakeComplete {
t.Error("TLS handshake is not complete")
}
}
+43 -20
View File
@@ -3,35 +3,33 @@ package controld
import ( import (
"bytes" "bytes"
"context" "context"
"crypto/tls"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net" "net"
"net/http" "net/http"
"time" "time"
"github.com/Control-D-Inc/ctrld"
"github.com/Control-D-Inc/ctrld/internal/certs"
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
"github.com/Control-D-Inc/ctrld/internal/router"
) )
const ( const (
resolverDataURL = "https://api.controld.com/utility" apiDomainCom = "api.controld.com"
InvalidConfigCode = 40401 apiDomainDev = "api.controld.dev"
resolverDataURLCom = "https://api.controld.com/utility"
resolverDataURLDev = "https://api.controld.dev/utility"
InvalidConfigCode = 40401
) )
const bootstrapDNS = "76.76.2.0:53"
var Dialer = &net.Dialer{
Resolver: &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
d := net.Dialer{
Timeout: 10 * time.Second,
}
return d.DialContext(ctx, "udp", bootstrapDNS)
},
},
}
// ResolverConfig represents Control D resolver data. // ResolverConfig represents Control D resolver data.
type ResolverConfig struct { type ResolverConfig struct {
DOH string `json:"doh"` DOH string `json:"doh"`
Ctrld struct {
CustomConfig string `json:"custom_config"`
} `json:"ctrld"`
Exclude []string `json:"exclude"` Exclude []string `json:"exclude"`
} }
@@ -58,19 +56,44 @@ type utilityRequest struct {
} }
// FetchResolverConfig fetch Control D config for given uid. // FetchResolverConfig fetch Control D config for given uid.
func FetchResolverConfig(uid string) (*ResolverConfig, error) { func FetchResolverConfig(uid, version string, cdDev bool) (*ResolverConfig, error) {
body, _ := json.Marshal(utilityRequest{UID: uid}) body, _ := json.Marshal(utilityRequest{UID: uid})
req, err := http.NewRequest("POST", resolverDataURL, bytes.NewReader(body)) apiUrl := resolverDataURLCom
if cdDev {
apiUrl = resolverDataURLDev
}
req, err := http.NewRequest("POST", apiUrl, bytes.NewReader(body))
if err != nil { if err != nil {
return nil, fmt.Errorf("http.NewRequest: %w", err) return nil, fmt.Errorf("http.NewRequest: %w", err)
} }
q := req.URL.Query() q := req.URL.Query()
q.Set("platform", "ctrld") q.Set("platform", "ctrld")
q.Set("version", version)
req.URL.RawQuery = q.Encode() req.URL.RawQuery = q.Encode()
req.Header.Add("Content-Type", "application/json") req.Header.Add("Content-Type", "application/json")
transport := http.DefaultTransport.(*http.Transport).Clone() transport := http.DefaultTransport.(*http.Transport).Clone()
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
return Dialer.DialContext(ctx, network, addr) apiDomain := apiDomainCom
if cdDev {
apiDomain = apiDomainDev
}
ips := ctrld.LookupIP(apiDomain)
if len(ips) == 0 {
ctrld.ProxyLog.Warn().Msgf("No IPs found for %s, connecting to %s", apiDomain, addr)
return ctrldnet.Dialer.DialContext(ctx, network, addr)
}
ctrld.ProxyLog.Debug().Msgf("API IPs: %v", ips)
_, port, _ := net.SplitHostPort(addr)
addrs := make([]string, len(ips))
for i := range ips {
addrs[i] = net.JoinHostPort(ips[i], port)
}
d := &ctrldnet.ParallelDialer{}
return d.DialContext(ctx, network, addrs)
}
if router.Name() == router.DDWrt {
transport.TLSClientConfig = &tls.Config{RootCAs: certs.CACertPool()}
} }
client := http.Client{ client := http.Client{
Timeout: 10 * time.Second, Timeout: 10 * time.Second,
+7 -6
View File
@@ -6,25 +6,26 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
const utilityURL = "https://api.controld.com/utility"
func TestFetchResolverConfig(t *testing.T) { func TestFetchResolverConfig(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
uid string uid string
dev bool
wantErr bool wantErr bool
}{ }{
{"valid", "p2", false}, {"valid com", "p2", false, false},
{"invalid uid", "abcd1234", true}, {"valid dev", "p2", true, false},
{"invalid uid", "abcd1234", false, true},
} }
for _, tc := range tests { for _, tc := range tests {
tc := tc tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel() t.Parallel()
got, err := FetchResolverConfig(tc.uid) got, err := FetchResolverConfig(tc.uid, "dev-test", tc.dev)
assert.False(t, (err != nil) != tc.wantErr) require.False(t, (err != nil) != tc.wantErr, err)
if !tc.wantErr { if !tc.wantErr {
assert.NotEmpty(t, got.DOH) assert.NotEmpty(t, got.DOH)
} }
+153
View File
@@ -0,0 +1,153 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build linux || freebsd || openbsd
package dns
import (
"bytes"
_ "embed"
"fmt"
"os"
"os/exec"
"path/filepath"
"tailscale.com/atomicfile"
"tailscale.com/types/logger"
)
//go:embed resolvconf-workaround.sh
var workaroundScript []byte
// resolvconfConfigName is the name of the config submitted to
// resolvconf.
// The name starts with 'tun' in order to match the hardcoded
// interface order in debian resolvconf, which will place this
// configuration ahead of regular network links. In theory, this
// doesn't matter because we then fix things up to ensure our config
// is the only one in use, but in case that fails, this will make our
// configuration slightly preferred.
// The 'inet' suffix has no specific meaning, but conventionally
// resolvconf implementations encourage adding a suffix roughly
// indicating where the config came from, and "inet" is the "none of
// the above" value (rather than, say, "ppp" or "dhcp").
const resolvconfConfigName = "ctrld.inet"
// resolvconfLibcHookPath is the directory containing libc update
// scripts, which are run by Debian resolvconf when /etc/resolv.conf
// has been updated.
const resolvconfLibcHookPath = "/etc/resolvconf/update-libc.d"
// resolvconfHookPath is the name of the libc hook script we install
// to force Ctrld's DNS config to take effect.
var resolvconfHookPath = filepath.Join(resolvconfLibcHookPath, "ctrld")
// resolvconfManager manages DNS configuration using the Debian
// implementation of the `resolvconf` program, written by Thomas Hood.
type resolvconfManager struct {
logf logger.Logf
listRecordsPath string
interfacesDir string
scriptInstalled bool // libc update script has been installed
}
var _ OSConfigurator = (*resolvconfManager)(nil)
func newDebianResolvconfManager(logf logger.Logf) (*resolvconfManager, error) {
ret := &resolvconfManager{
logf: logf,
listRecordsPath: "/lib/resolvconf/list-records",
interfacesDir: "/etc/resolvconf/run/interface", // panic fallback if nothing seems to work
}
if _, err := os.Stat(ret.listRecordsPath); os.IsNotExist(err) {
// This might be a Debian system from before the big /usr
// merge, try /usr instead.
ret.listRecordsPath = "/usr" + ret.listRecordsPath
}
// The runtime directory is currently (2020-04) canonically
// /etc/resolvconf/run, but the manpage is making noise about
// switching to /run/resolvconf and dropping the /etc path. So,
// let's probe the possible directories and use the first one
// that works.
for _, path := range []string{
"/etc/resolvconf/run/interface",
"/run/resolvconf/interface",
"/var/run/resolvconf/interface",
} {
if _, err := os.Stat(path); err == nil {
ret.interfacesDir = path
break
}
}
if ret.interfacesDir == "" {
// None of the paths seem to work, use the canonical location
// that the current manpage says to use.
ret.interfacesDir = "/etc/resolvconf/run/interfaces"
}
return ret, nil
}
func (m *resolvconfManager) deleteCtrldConfig() error {
cmd := exec.Command("resolvconf", "-d", resolvconfConfigName)
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("running %s: %s", cmd, out)
}
return nil
}
func (m *resolvconfManager) SetDNS(config OSConfig) error {
if !m.scriptInstalled {
m.logf("injecting resolvconf workaround script")
if err := os.MkdirAll(resolvconfLibcHookPath, 0755); err != nil {
return err
}
if err := atomicfile.WriteFile(resolvconfHookPath, workaroundScript, 0755); err != nil {
return err
}
m.scriptInstalled = true
}
if config.IsZero() {
if err := m.deleteCtrldConfig(); err != nil {
return err
}
} else {
stdin := new(bytes.Buffer)
writeResolvConf(stdin, config.Nameservers, config.SearchDomains) // dns_direct.go
// This resolvconf implementation doesn't support exclusive
// mode or interface priorities, so it will end up blending
// our configuration with other sources. However, this will
// get fixed up by the script we injected above.
cmd := exec.Command("resolvconf", "-a", resolvconfConfigName)
cmd.Stdin = stdin
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("running %s: %s", cmd, out)
}
}
return nil
}
func (m *resolvconfManager) Close() error {
if err := m.deleteCtrldConfig(); err != nil {
return err
}
if m.scriptInstalled {
m.logf("removing resolvconf workaround script")
os.Remove(resolvconfHookPath) // Best-effort
}
return nil
}
func (m *resolvconfManager) Mode() string {
return "resolvconf"
}
+4
View File
@@ -144,6 +144,10 @@ type directManager struct {
lastWarnContents []byte // last resolv.conf contents that we warned about lastWarnContents []byte // last resolv.conf contents that we warned about
} }
func newDirectManager(logf logger.Logf) *directManager {
return newDirectManagerOnFS(logf, directFS{})
}
func newDirectManagerOnFS(logf logger.Logf, fs wholeFileFS) *directManager { func newDirectManagerOnFS(logf logger.Logf, fs wholeFileFS) *directManager {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
m := &directManager{ m := &directManager{
+39
View File
@@ -0,0 +1,39 @@
// Copyright (c) 2020 Tailscale Inc & 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 dns
import (
"fmt"
"os"
"tailscale.com/types/logger"
)
func NewOSConfigurator(logf logger.Logf, _ string) (OSConfigurator, error) {
bs, err := os.ReadFile("/etc/resolv.conf")
if os.IsNotExist(err) {
return newDirectManager(logf), nil
}
if err != nil {
return nil, fmt.Errorf("reading /etc/resolv.conf: %w", err)
}
switch resolvOwner(bs) {
case "resolvconf":
switch resolvconfStyle() {
case "":
return newDirectManager(logf), nil
case "debian":
return newDebianResolvconfManager(logf)
case "openresolv":
return newOpenresolvManager()
default:
logf("[unexpected] got unknown flavor of resolvconf %q, falling back to direct manager", resolvconfStyle())
return newDirectManager(logf), nil
}
default:
return newDirectManager(logf), nil
}
}
+4
View File
@@ -62,6 +62,10 @@ func NewOSConfigurator(logf logger.Logf, interfaceName string) (ret OSConfigurat
return newResolvedManager(logf, interfaceName) return newResolvedManager(logf, interfaceName)
case "network-manager": case "network-manager":
return newNMManager(interfaceName) return newNMManager(interfaceName)
case "debian-resolvconf":
return newDebianResolvconfManager(logf)
case "openresolv":
return newOpenresolvManager()
default: default:
logf("[unexpected] detected unknown DNS mode %q, using direct manager as last resort", mode) logf("[unexpected] detected unknown DNS mode %q, using direct manager as last resort", mode)
return newDirectManagerOnFS(logf, env.fs), nil return newDirectManagerOnFS(logf, env.fs), nil
+4 -2
View File
@@ -14,8 +14,8 @@ import (
"time" "time"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
"github.com/josharian/native"
"tailscale.com/util/dnsname" "tailscale.com/util/dnsname"
"tailscale.com/util/endian"
) )
const ( const (
@@ -31,6 +31,8 @@ type nmManager struct {
dnsManager dbus.BusObject dnsManager dbus.BusObject
} }
var _ OSConfigurator = (*nmManager)(nil)
func newNMManager(interfaceName string) (*nmManager, error) { func newNMManager(interfaceName string) (*nmManager, error) {
conn, err := dbus.SystemBus() conn, err := dbus.SystemBus()
if err != nil { if err != nil {
@@ -129,7 +131,7 @@ func (m *nmManager) trySet(ctx context.Context, config OSConfig) error {
for _, ip := range config.Nameservers { for _, ip := range config.Nameservers {
b := ip.As16() b := ip.As16()
if ip.Is4() { if ip.Is4() {
dnsv4 = append(dnsv4, endian.Native.Uint32(b[12:])) dnsv4 = append(dnsv4, native.Endian.Uint32(b[12:]))
} else { } else {
dnsv6 = append(dnsv6, b[:]) dnsv6 = append(dnsv6, b[:])
} }
+57
View File
@@ -0,0 +1,57 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build linux || freebsd || openbsd
package dns
import (
"bytes"
"fmt"
"os/exec"
)
// openresolvManager manages DNS configuration using the openresolv
// implementation of the `resolvconf` program.
type openresolvManager struct{}
var _ OSConfigurator = (*openresolvManager)(nil)
func newOpenresolvManager() (openresolvManager, error) {
return openresolvManager{}, nil
}
func (m openresolvManager) deleteTailscaleConfig() error {
cmd := exec.Command("resolvconf", "-f", "-d", "ctrld")
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("running %s: %s", cmd, out)
}
return nil
}
func (m openresolvManager) SetDNS(config OSConfig) error {
if config.IsZero() {
return m.deleteTailscaleConfig()
}
var stdin bytes.Buffer
writeResolvConf(&stdin, config.Nameservers, config.SearchDomains)
cmd := exec.Command("resolvconf", "-m", "0", "-x", "-a", "ctrld")
cmd.Stdin = &stdin
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("running %s: %s", cmd, out)
}
return nil
}
func (m openresolvManager) Close() error {
return m.deleteTailscaleConfig()
}
func (m openresolvManager) Mode() string {
return "resolvconf"
}
+2
View File
@@ -13,6 +13,8 @@ import (
"tailscale.com/util/dnsname" "tailscale.com/util/dnsname"
) )
var _ OSConfigurator = (*directManager)(nil)
// An OSConfigurator applies DNS settings to the operating system. // An OSConfigurator applies DNS settings to the operating system.
type OSConfigurator interface { type OSConfigurator interface {
// SetDNS updates the OS's DNS configuration to match cfg. // SetDNS updates the OS's DNS configuration to match cfg.
+63
View File
@@ -0,0 +1,63 @@
#!/bin/sh
# Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file.
#
# This script is a workaround for a vpn-unfriendly behavior of the
# original resolvconf by Thomas Hood. Unlike the `openresolv`
# implementation (whose binary is also called resolvconf,
# confusingly), the original resolvconf lacks a way to specify
# "exclusive mode" for a provider configuration. In practice, this
# means that if Ctrld wants to install a DNS configuration, that
# config will get "blended" with the configs from other sources,
# rather than override those other sources.
#
# This script gets installed at /etc/resolvconf/update-libc.d, which
# is a directory of hook scripts that get run after resolvconf's libc
# helper has finished rewriting /etc/resolv.conf. It's meant to notify
# consumers of resolv.conf of a new configuration.
#
# Instead, we use that hook mechanism to reach into resolvconf's
# stuff, and rewrite the libc-generated resolv.conf to exclusively
# contain Ctrld's configuration - effectively implementing
# exclusive mode ourselves in post-production.
set -e
if [ -n "$CTRLD_RESOLVCONF_HOOK_LOOP" ]; then
# Hook script being invoked by itself, skip.
exit 0
fi
if [ ! -f ctrld.inet ]; then
# Ctrld isn't trying to manage DNS, do nothing.
exit 0
fi
if ! grep resolvconf /etc/resolv.conf >/dev/null; then
# resolvconf isn't managing /etc/resolv.conf, do nothing.
exit 0
fi
# Write out a modified /etc/resolv.conf containing just our config.
(
if [ -f /etc/resolvconf/resolv.conf.d/head ]; then
cat /etc/resolvconf/resolv.conf.d/head
fi
echo "# Ctrld workaround applied to set exclusive DNS configuration."
cat tun-tailscale.inet
if [ -f /etc/resolvconf/resolv.conf.d/base ]; then
# Keep options and sortlist, discard other base things since
# they're the things we're trying to override.
grep -e 'sortlist ' -e 'options ' /etc/resolvconf/resolv.conf.d/base || true
fi
if [ -f /etc/resolvconf/resolv.conf.d/tail ]; then
cat /etc/resolvconf/resolv.conf.d/tail
fi
) >/etc/resolv.conf
if [ -d /etc/resolvconf/update-libc.d ] ; then
# Re-notify libc watchers that we've changed resolv.conf again.
export CTRLD_RESOLVCONF_HOOK_LOOP=1
exec run-parts /etc/resolvconf/update-libc.d
fi
@@ -15,7 +15,6 @@ import (
"strings" "strings"
"tailscale.com/util/dnsname" "tailscale.com/util/dnsname"
"tailscale.com/util/strs"
) )
// Path is the canonical location of resolv.conf. // Path is the canonical location of resolv.conf.
@@ -63,7 +62,7 @@ func Parse(r io.Reader) (*Config, error) {
line, _, _ = strings.Cut(line, "#") // remove any comments line, _, _ = strings.Cut(line, "#") // remove any comments
line = strings.TrimSpace(line) line = strings.TrimSpace(line)
if s, ok := strs.CutPrefix(line, "nameserver"); ok { if s, ok := strings.CutPrefix(line, "nameserver"); ok {
nameserver := strings.TrimSpace(s) nameserver := strings.TrimSpace(s)
if len(nameserver) == len(s) { if len(nameserver) == len(s) {
return nil, fmt.Errorf("missing space after \"nameserver\" in %q", line) return nil, fmt.Errorf("missing space after \"nameserver\" in %q", line)
@@ -76,7 +75,7 @@ func Parse(r io.Reader) (*Config, error) {
continue continue
} }
if s, ok := strs.CutPrefix(line, "search"); ok { if s, ok := strings.CutPrefix(line, "search"); ok {
domains := strings.TrimSpace(s) domains := strings.TrimSpace(s)
if len(domains) == len(s) { if len(domains) == len(s) {
// No leading space?! // No leading space?!
+90 -20
View File
@@ -37,14 +37,45 @@ const reconfigTimeout = time.Second
// Clients connect to the bus and walk that same hierarchy to invoke // Clients connect to the bus and walk that same hierarchy to invoke
// RPCs, get/set properties, or listen for signals. // RPCs, get/set properties, or listen for signals.
const ( const (
dbusResolvedObject = "org.freedesktop.resolve1" dbusResolvedObject = "org.freedesktop.resolve1"
dbusResolvedPath dbus.ObjectPath = "/org/freedesktop/resolve1" dbusNetworkdObject = "org.freedesktop.network1"
dbusResolvedInterface = "org.freedesktop.resolve1.Manager" dbusResolvedPath dbus.ObjectPath = "/org/freedesktop/resolve1"
dbusPath dbus.ObjectPath = "/org/freedesktop/DBus" dbusNetworkdPath dbus.ObjectPath = "/org/freedesktop/network1"
dbusInterface = "org.freedesktop.DBus" dbusResolvedInterface = "org.freedesktop.resolve1.Manager"
dbusOwnerSignal = "NameOwnerChanged" // broadcast when a well-known name's owning process changes. dbusNetworkdInterface = "org.freedesktop.network1.Manager"
dbusPath dbus.ObjectPath = "/org/freedesktop/DBus"
dbusInterface = "org.freedesktop.DBus"
dbusOwnerSignal = "NameOwnerChanged" // broadcast when a well-known name's owning process changes.
dbusResolvedErrorLinkBusy = "org.freedesktop.resolve1.LinkBusy"
) )
var (
dbusSetLinkDNS string
dbusSetLinkDomains string
dbusSetLinkDefaultRoute string
dbusSetLinkLLMNR string
dbusSetLinkMulticastDNS string
dbusSetLinkDNSSEC string
dbusSetLinkDNSOverTLS string
dbusFlushCaches string
dbusRevertLink string
)
func setDbusMethods(dbusInterface string) {
dbusSetLinkDNS = dbusInterface + ".SetLinkDNS"
dbusSetLinkDomains = dbusInterface + ".SetLinkDomains"
dbusSetLinkDefaultRoute = dbusInterface + ".SetLinkDefaultRoute"
dbusSetLinkLLMNR = dbusInterface + ".SetLinkLLMNR"
dbusSetLinkMulticastDNS = dbusInterface + ".SetLinkMulticastDNS"
dbusSetLinkDNSSEC = dbusInterface + ".SetLinkDNSSEC"
dbusSetLinkDNSOverTLS = dbusInterface + ".SetLinkDNSOverTLS"
dbusFlushCaches = dbusInterface + ".FlushCaches"
dbusRevertLink = dbusInterface + ".RevertLink"
if dbusInterface == dbusNetworkdInterface {
dbusRevertLink = dbusInterface + ".RevertLinkDNS"
}
}
type resolvedLinkNameserver struct { type resolvedLinkNameserver struct {
Family int32 Family int32
Address []byte Address []byte
@@ -70,8 +101,11 @@ type resolvedManager struct {
ifidx int ifidx int
configCR chan changeRequest // tracks OSConfigs changes and error responses configCR chan changeRequest // tracks OSConfigs changes and error responses
revertCh chan struct{}
} }
var _ OSConfigurator = (*resolvedManager)(nil)
func newResolvedManager(logf logger.Logf, interfaceName string) (*resolvedManager, error) { func newResolvedManager(logf logger.Logf, interfaceName string) (*resolvedManager, error) {
iface, err := net.InterfaceByName(interfaceName) iface, err := net.InterfaceByName(interfaceName)
if err != nil { if err != nil {
@@ -89,6 +123,7 @@ func newResolvedManager(logf logger.Logf, interfaceName string) (*resolvedManage
ifidx: iface.Index, ifidx: iface.Index,
configCR: make(chan changeRequest), configCR: make(chan changeRequest),
revertCh: make(chan struct{}),
} }
go mgr.run(ctx) go mgr.run(ctx)
@@ -117,6 +152,16 @@ func (m *resolvedManager) SetDNS(config OSConfig) error {
} }
} }
func newResolvedObject(conn *dbus.Conn) dbus.BusObject {
setDbusMethods(dbusResolvedInterface)
return conn.Object(dbusResolvedObject, dbusResolvedPath)
}
func newNetworkdObject(conn *dbus.Conn) dbus.BusObject {
setDbusMethods(dbusNetworkdInterface)
return conn.Object(dbusNetworkdObject, dbusNetworkdPath)
}
func (m *resolvedManager) run(ctx context.Context) { func (m *resolvedManager) run(ctx context.Context) {
var ( var (
conn *dbus.Conn conn *dbus.Conn
@@ -131,6 +176,22 @@ func (m *resolvedManager) run(ctx context.Context) {
} }
}() }()
newManager := newResolvedObject
func() {
conn, err := dbus.SystemBus()
if err != nil {
m.logf("dbus connection error: %v", err)
return
}
rManager = newManager(conn)
if call := rManager.CallWithContext(ctx, dbusRevertLink, 0, m.ifidx); call.Err != nil {
if dbusErr, ok := call.Err.(dbus.Error); ok && dbusErr.Name == dbusResolvedErrorLinkBusy {
m.logf("[v1] Using %s as manager", dbusNetworkdObject)
newManager = newNetworkdObject
}
}
}()
// Reconnect the systemBus if disconnected. // Reconnect the systemBus if disconnected.
reconnect := func() error { reconnect := func() error {
var err error var err error
@@ -151,7 +212,7 @@ func (m *resolvedManager) run(ctx context.Context) {
return err return err
} }
rManager = conn.Object(dbusResolvedObject, dbus.ObjectPath(dbusResolvedPath)) rManager = newManager(conn)
// Only receive the DBus signals we need to resync our config on // Only receive the DBus signals we need to resync our config on
// resolved restart. Failure to set filters isn't a fatal error, // resolved restart. Failure to set filters isn't a fatal error,
@@ -160,6 +221,9 @@ func (m *resolvedManager) run(ctx context.Context) {
if err = conn.AddMatchSignal(dbus.WithMatchObjectPath(dbusPath), dbus.WithMatchInterface(dbusInterface), dbus.WithMatchMember(dbusOwnerSignal), dbus.WithMatchArg(0, dbusResolvedObject)); err != nil { if err = conn.AddMatchSignal(dbus.WithMatchObjectPath(dbusPath), dbus.WithMatchInterface(dbusInterface), dbus.WithMatchMember(dbusOwnerSignal), dbus.WithMatchArg(0, dbusResolvedObject)); err != nil {
m.logf("[v1] Setting DBus signal filter failed: %v", err) m.logf("[v1] Setting DBus signal filter failed: %v", err)
} }
if err = conn.AddMatchSignal(dbus.WithMatchObjectPath(dbusPath), dbus.WithMatchInterface(dbusInterface), dbus.WithMatchMember(dbusOwnerSignal), dbus.WithMatchArg(0, dbusNetworkdObject)); err != nil {
m.logf("[v1] Setting DBus signal filter failed: %v", err)
}
conn.Signal(signals) conn.Signal(signals)
// Reset backoff and SetNSOSHealth after successful on reconnect. // Reset backoff and SetNSOSHealth after successful on reconnect.
@@ -179,13 +243,15 @@ func (m *resolvedManager) run(ctx context.Context) {
if rManager == nil { if rManager == nil {
return return
} }
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
// RevertLink resets all per-interface settings on systemd-resolved to defaults. // RevertLink resets all per-interface settings on systemd-resolved to defaults.
// When ctx goes away systemd-resolved auto reverts. // When ctx goes away systemd-resolved auto reverts.
// Keeping for potential use in future refactor. // Keeping for potential use in future refactor.
if call := rManager.CallWithContext(ctx, dbusResolvedInterface+".RevertLink", 0, m.ifidx); call.Err != nil { if call := rManager.CallWithContext(ctx, dbusRevertLink, 0, m.ifidx); call.Err != nil {
m.logf("[v1] RevertLink: %v", call.Err) m.logf("[v1] RevertLink: %v", call.Err)
return
} }
cancel()
close(m.revertCh)
return return
case configCR := <-m.configCR: case configCR := <-m.configCR:
// Track and update sync with latest config change. // Track and update sync with latest config change.
@@ -223,7 +289,7 @@ func (m *resolvedManager) run(ctx context.Context) {
if len(signal.Body) != 3 { if len(signal.Body) != 3 {
m.logf("[unexpected] DBus NameOwnerChanged len(Body) = %d, want 3") m.logf("[unexpected] DBus NameOwnerChanged len(Body) = %d, want 3")
} }
if name, ok := signal.Body[0].(string); !ok || name != dbusResolvedObject { if name, ok := signal.Body[0].(string); !ok || (name != dbusResolvedObject && name != dbusNetworkdObject) {
continue continue
} }
newOwner, ok := signal.Body[2].(string) newOwner, ok := signal.Body[2].(string)
@@ -271,7 +337,7 @@ func (m *resolvedManager) setConfigOverDBus(ctx context.Context, rManager dbus.B
} }
} }
err := rManager.CallWithContext( err := rManager.CallWithContext(
ctx, dbusResolvedInterface+".SetLinkDNS", 0, ctx, dbusSetLinkDNS, 0,
m.ifidx, linkNameservers, m.ifidx, linkNameservers,
).Store() ).Store()
if err != nil { if err != nil {
@@ -311,14 +377,14 @@ func (m *resolvedManager) setConfigOverDBus(ctx context.Context, rManager dbus.B
} }
err = rManager.CallWithContext( err = rManager.CallWithContext(
ctx, dbusResolvedInterface+".SetLinkDomains", 0, ctx, dbusSetLinkDomains, 0,
m.ifidx, linkDomains, m.ifidx, linkDomains,
).Store() ).Store()
if err != nil && err.Error() == "Argument list too long" { // TODO: better error match if err != nil && err.Error() == "Argument list too long" { // TODO: better error match
// Issue 3188: older systemd-resolved had argument length limits. // Issue 3188: older systemd-resolved had argument length limits.
// Trim out the *.arpa. entries and try again. // Trim out the *.arpa. entries and try again.
err = rManager.CallWithContext( err = rManager.CallWithContext(
ctx, dbusResolvedInterface+".SetLinkDomains", 0, ctx, dbusSetLinkDomains, 0,
m.ifidx, linkDomainsWithoutReverseDNS(linkDomains), m.ifidx, linkDomainsWithoutReverseDNS(linkDomains),
).Store() ).Store()
} }
@@ -326,7 +392,7 @@ func (m *resolvedManager) setConfigOverDBus(ctx context.Context, rManager dbus.B
return fmt.Errorf("setLinkDomains: %w", err) return fmt.Errorf("setLinkDomains: %w", err)
} }
if call := rManager.CallWithContext(ctx, dbusResolvedInterface+".SetLinkDefaultRoute", 0, m.ifidx, len(config.MatchDomains) == 0); call.Err != nil { if call := rManager.CallWithContext(ctx, dbusSetLinkDefaultRoute, 0, m.ifidx, len(config.MatchDomains) == 0); call.Err != nil {
if dbusErr, ok := call.Err.(dbus.Error); ok && dbusErr.Name == dbus.ErrMsgUnknownMethod.Name { if dbusErr, ok := call.Err.(dbus.Error); ok && dbusErr.Name == dbus.ErrMsgUnknownMethod.Name {
// on some older systems like Kubuntu 18.04.6 with systemd 237 method SetLinkDefaultRoute is absent, // on some older systems like Kubuntu 18.04.6 with systemd 237 method SetLinkDefaultRoute is absent,
// but otherwise it's working good // but otherwise it's working good
@@ -341,33 +407,37 @@ func (m *resolvedManager) setConfigOverDBus(ctx context.Context, rManager dbus.B
// or something). // or something).
// Disable LLMNR, we don't do multicast. // Disable LLMNR, we don't do multicast.
if call := rManager.CallWithContext(ctx, dbusResolvedInterface+".SetLinkLLMNR", 0, m.ifidx, "no"); call.Err != nil { if call := rManager.CallWithContext(ctx, dbusSetLinkLLMNR, 0, m.ifidx, "no"); call.Err != nil {
m.logf("[v1] failed to disable LLMNR: %v", call.Err) m.logf("[v1] failed to disable LLMNR: %v", call.Err)
} }
// Disable mdns. // Disable mdns.
if call := rManager.CallWithContext(ctx, dbusResolvedInterface+".SetLinkMulticastDNS", 0, m.ifidx, "no"); call.Err != nil { if call := rManager.CallWithContext(ctx, dbusSetLinkMulticastDNS, 0, m.ifidx, "no"); call.Err != nil {
m.logf("[v1] failed to disable mdns: %v", call.Err) m.logf("[v1] failed to disable mdns: %v", call.Err)
} }
// We don't support dnssec consistently right now, force it off to // We don't support dnssec consistently right now, force it off to
// avoid partial failures when we split DNS internally. // avoid partial failures when we split DNS internally.
if call := rManager.CallWithContext(ctx, dbusResolvedInterface+".SetLinkDNSSEC", 0, m.ifidx, "no"); call.Err != nil { if call := rManager.CallWithContext(ctx, dbusSetLinkDNSSEC, 0, m.ifidx, "no"); call.Err != nil {
m.logf("[v1] failed to disable DNSSEC: %v", call.Err) m.logf("[v1] failed to disable DNSSEC: %v", call.Err)
} }
if call := rManager.CallWithContext(ctx, dbusResolvedInterface+".SetLinkDNSOverTLS", 0, m.ifidx, "no"); call.Err != nil { if call := rManager.CallWithContext(ctx, dbusSetLinkDNSOverTLS, 0, m.ifidx, "no"); call.Err != nil {
m.logf("[v1] failed to disable DoT: %v", call.Err) m.logf("[v1] failed to disable DoT: %v", call.Err)
} }
if call := rManager.CallWithContext(ctx, dbusResolvedInterface+".FlushCaches", 0); call.Err != nil { if rManager.Path() == dbusResolvedPath {
m.logf("failed to flush resolved DNS cache: %v", call.Err) if call := rManager.CallWithContext(ctx, dbusFlushCaches, 0); call.Err != nil {
m.logf("failed to flush resolved DNS cache: %v", call.Err)
}
} }
return nil return nil
} }
func (m *resolvedManager) Close() error { func (m *resolvedManager) Close() error {
m.cancel() // stops the 'run' method goroutine m.cancel() // stops the 'run' method goroutine
<-m.revertCh
return nil return nil
} }
+139
View File
@@ -0,0 +1,139 @@
package net
import (
"context"
"errors"
"net"
"sync"
"sync/atomic"
"time"
"tailscale.com/logtail/backoff"
)
const (
controldIPv6Test = "ipv6.controld.io"
bootstrapDNS = "76.76.2.0:53"
)
var Dialer = &net.Dialer{
Resolver: &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
d := net.Dialer{
Timeout: 10 * time.Second,
}
return d.DialContext(ctx, "udp", bootstrapDNS)
},
},
}
const probeStackTimeout = 2 * time.Second
var probeStackDialer = &net.Dialer{
Resolver: Dialer.Resolver,
Timeout: probeStackTimeout,
}
var (
stackOnce atomic.Pointer[sync.Once]
canListenIPv6Local bool
hasNetworkUp bool
)
func init() {
stackOnce.Store(new(sync.Once))
}
func supportIPv6(ctx context.Context) bool {
_, err := probeStackDialer.DialContext(ctx, "tcp6", net.JoinHostPort(controldIPv6Test, "443"))
return err == nil
}
func supportListenIPv6Local() bool {
if ln, err := net.Listen("tcp6", "[::1]:0"); err == nil {
ln.Close()
return true
}
return false
}
func probeStack() {
b := backoff.NewBackoff("probeStack", func(format string, args ...any) {}, 5*time.Second)
for {
if _, err := probeStackDialer.Dial("udp", bootstrapDNS); err == nil {
hasNetworkUp = true
break
} else {
b.BackOff(context.Background(), err)
}
}
canListenIPv6Local = supportListenIPv6Local()
}
func Up() bool {
stackOnce.Load().Do(probeStack)
return hasNetworkUp
}
func SupportsIPv6ListenLocal() bool {
stackOnce.Load().Do(probeStack)
return canListenIPv6Local
}
// IPv6Available is like SupportsIPv6, but always do the check without caching.
func IPv6Available(ctx context.Context) bool {
return supportIPv6(ctx)
}
// IsIPv6 checks if the provided IP is v6.
//
//lint:ignore U1000 use in os_windows.go
func IsIPv6(ip string) bool {
parsedIP := net.ParseIP(ip)
return parsedIP != nil && parsedIP.To4() == nil && parsedIP.To16() != nil
}
type parallelDialerResult struct {
conn net.Conn
err error
}
type ParallelDialer struct {
net.Dialer
}
func (d *ParallelDialer) DialContext(ctx context.Context, network string, addrs []string) (net.Conn, error) {
if len(addrs) == 0 {
return nil, errors.New("empty addresses")
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
ch := make(chan *parallelDialerResult, len(addrs))
var wg sync.WaitGroup
wg.Add(len(addrs))
go func() {
wg.Wait()
close(ch)
}()
for _, addr := range addrs {
go func(addr string) {
defer wg.Done()
conn, err := d.Dialer.DialContext(ctx, network, addr)
ch <- &parallelDialerResult{conn: conn, err: err}
}(addr)
}
errs := make([]error, 0, len(addrs))
for res := range ch {
if res.err == nil {
cancel()
return res.conn, res.err
}
errs = append(errs, res.err)
}
return nil, errors.Join(errs...)
}
+24
View File
@@ -0,0 +1,24 @@
package net
import (
"context"
"testing"
"time"
)
func TestProbeStackTimeout(t *testing.T) {
done := make(chan struct{})
started := make(chan struct{})
go func() {
defer close(done)
close(started)
supportIPv6(context.Background())
}()
<-started
select {
case <-time.After(probeStackTimeout + time.Second):
t.Error("probeStack timeout is not enforce")
case <-done:
}
}
+193
View File
@@ -0,0 +1,193 @@
package router
import (
"bufio"
"bytes"
"io"
"log"
"net"
"os"
"strings"
"time"
"github.com/fsnotify/fsnotify"
"tailscale.com/util/lineread"
"github.com/Control-D-Inc/ctrld"
)
// readClientInfoFunc represents the function for reading client info.
type readClientInfoFunc func(name string) error
// clientInfoFiles specifies client info files and how to read them on supported platforms.
var clientInfoFiles = map[string]readClientInfoFunc{
"/tmp/dnsmasq.leases": dnsmasqReadClientInfoFile, // ddwrt
"/tmp/dhcp.leases": dnsmasqReadClientInfoFile, // openwrt
"/var/lib/misc/dnsmasq.leases": dnsmasqReadClientInfoFile, // merlin
"/mnt/data/udapi-config/dnsmasq.lease": dnsmasqReadClientInfoFile, // UDM Pro
"/data/udapi-config/dnsmasq.lease": dnsmasqReadClientInfoFile, // UDR
"/etc/dhcpd/dhcpd-leases.log": dnsmasqReadClientInfoFile, // Synology
"/tmp/var/lib/misc/dnsmasq.leases": dnsmasqReadClientInfoFile, // Tomato
"/run/dnsmasq-dhcp.leases": dnsmasqReadClientInfoFile, // EdgeOS
"/run/dhcpd.leases": iscDHCPReadClientInfoFile, // EdgeOS
"/var/dhcpd/var/db/dhcpd.leases": iscDHCPReadClientInfoFile, // Pfsense
}
// watchClientInfoTable watches changes happens in dnsmasq/dhcpd
// lease files, perform updating to mac table if necessary.
func (r *router) watchClientInfoTable() {
if r.watcher == nil {
return
}
timer := time.NewTicker(time.Minute * 5)
for {
select {
case <-timer.C:
for _, name := range r.watcher.WatchList() {
_ = clientInfoFiles[name](name)
}
case event, ok := <-r.watcher.Events:
if !ok {
return
}
if event.Has(fsnotify.Write) {
readFunc := clientInfoFiles[event.Name]
if readFunc == nil {
log.Println("unknown file format:", event.Name)
continue
}
if err := readFunc(event.Name); err != nil && !os.IsNotExist(err) {
log.Println("could not read client info file:", err)
}
}
case err, ok := <-r.watcher.Errors:
if !ok {
return
}
log.Println("error:", err)
}
}
}
// Stop performs tasks need to be done before the router stopped.
func Stop() error {
if Name() == "" {
return nil
}
r := routerPlatform.Load()
if r.watcher != nil {
if err := r.watcher.Close(); err != nil {
return err
}
}
return nil
}
// GetClientInfoByMac returns ClientInfo for the client associated with the given mac.
func GetClientInfoByMac(mac string) *ctrld.ClientInfo {
if mac == "" {
return nil
}
_ = Name()
r := routerPlatform.Load()
val, ok := r.mac.Load(mac)
if !ok {
return nil
}
return val.(*ctrld.ClientInfo)
}
// dnsmasqReadClientInfoFile populates mac table with client info reading from dnsmasq lease file.
func dnsmasqReadClientInfoFile(name string) error {
f, err := os.Open(name)
if err != nil {
return err
}
defer f.Close()
return dnsmasqReadClientInfoReader(f)
}
// dnsmasqReadClientInfoReader likes dnsmasqReadClientInfoFile, but reading from an io.Reader instead of file.
func dnsmasqReadClientInfoReader(reader io.Reader) error {
r := routerPlatform.Load()
return lineread.Reader(reader, func(line []byte) error {
fields := bytes.Fields(line)
if len(fields) < 4 {
return nil
}
mac := string(fields[1])
if _, err := net.ParseMAC(mac); err != nil {
// The second field is not a mac, skip.
return nil
}
ip := normalizeIP(string(fields[2]))
if net.ParseIP(ip) == nil {
log.Printf("invalid ip address entry: %q", ip)
ip = ""
}
hostname := string(fields[3])
r.mac.Store(mac, &ctrld.ClientInfo{Mac: mac, IP: ip, Hostname: hostname})
return nil
})
}
// iscDHCPReadClientInfoFile populates mac table with client info reading from isc-dhcpd lease file.
func iscDHCPReadClientInfoFile(name string) error {
f, err := os.Open(name)
if err != nil {
return err
}
defer f.Close()
return iscDHCPReadClientInfoReader(f)
}
// iscDHCPReadClientInfoReader likes iscDHCPReadClientInfoFile, but reading from an io.Reader instead of file.
func iscDHCPReadClientInfoReader(reader io.Reader) error {
r := routerPlatform.Load()
s := bufio.NewScanner(reader)
var ip, mac, hostname string
for s.Scan() {
line := s.Text()
if strings.HasPrefix(line, "}") {
if mac != "" {
r.mac.Store(mac, &ctrld.ClientInfo{Mac: mac, IP: ip, Hostname: hostname})
ip, mac, hostname = "", "", ""
}
continue
}
fields := strings.Fields(line)
if len(fields) < 2 {
continue
}
switch fields[0] {
case "lease":
ip = normalizeIP(strings.ToLower(fields[1]))
if net.ParseIP(ip) == nil {
log.Printf("invalid ip address entry: %q", ip)
ip = ""
}
case "hardware":
if len(fields) >= 3 {
mac = strings.ToLower(strings.TrimRight(fields[2], ";"))
if _, err := net.ParseMAC(mac); err != nil {
// Invalid mac, skip.
mac = ""
}
}
case "client-hostname":
hostname = strings.Trim(fields[1], `";`)
}
}
return nil
}
// normalizeIP normalizes the ip parsed from dnsmasq/dhcpd lease file.
func normalizeIP(in string) string {
// dnsmasq may put ip with interface index in lease file, strip it here.
ip, _, found := strings.Cut(in, "%")
if found {
return ip
}
return in
}
+107
View File
@@ -0,0 +1,107 @@
package router
import (
"io"
"strings"
"testing"
"github.com/Control-D-Inc/ctrld"
)
func Test_normalizeIP(t *testing.T) {
tests := []struct {
name string
in string
want string
}{
{"v4", "127.0.0.1", "127.0.0.1"},
{"v4 with index", "127.0.0.1%lo", "127.0.0.1"},
{"v6", "fe80::1", "fe80::1"},
{"v6 with index", "fe80::1%22002", "fe80::1"},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := normalizeIP(tc.in); got != tc.want {
t.Errorf("normalizeIP() = %v, want %v", got, tc.want)
}
})
}
}
func Test_readClientInfoReader(t *testing.T) {
tests := []struct {
name string
in string
readFunc func(r io.Reader) error
mac string
}{
{
"good dnsmasq",
`1683329857 e6:20:59:b8:c1:6d 192.168.1.186 * 01:e6:20:59:b8:c1:6d
`,
dnsmasqReadClientInfoReader,
"e6:20:59:b8:c1:6d",
},
{
"bad dnsmasq seen on UDMdream machine",
`1683329857 e6:20:59:b8:c1:6e 192.168.1.111 * 01:e6:20:59:b8:c1:6e
duid 00:01:00:01:2b:e4:2e:2c:52:52:14:26:dc:1c
1683322985 117442354 2600:4040:b0e6:b700::111 ASDASD 00:01:00:01:2a:d0:b9:81:00:07:32:4c:1c:07
`,
dnsmasqReadClientInfoReader,
"e6:20:59:b8:c1:6e",
},
{
"isc-dhcpd good",
`lease 192.168.1.1 {
hardware ethernet 00:00:00:00:00:01;
client-hostname "host-1";
}
`,
iscDHCPReadClientInfoReader,
"00:00:00:00:00:01",
},
{
"isc-dhcpd bad mac",
`lease 192.168.1.1 {
hardware ethernet invalid-mac;
client-hostname "host-1";
}
lease 192.168.1.2 {
hardware ethernet 00:00:00:00:00:02;
client-hostname "host-2";
}
`,
iscDHCPReadClientInfoReader,
"00:00:00:00:00:02",
},
{
"",
`1685794060 00:00:00:00:00:04 192.168.0.209 cuonglm-ThinkPad-X1-Carbon-Gen-9 00:00:00:00:00:04 9`,
dnsmasqReadClientInfoReader,
"00:00:00:00:00:04",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
r := routerPlatform.Load()
r.mac.Delete(tc.mac)
if err := tc.readFunc(strings.NewReader(tc.in)); err != nil {
t.Errorf("readClientInfoReader() error = %v", err)
}
info, existed := r.mac.Load(tc.mac)
if !existed {
t.Error("client info missing")
}
if ci, ok := info.(*ctrld.ClientInfo); ok && existed && ci.Mac != tc.mac {
t.Errorf("mac mismatched, got: %q, want: %q", ci.Mac, tc.mac)
} else {
t.Log(ci)
}
})
}
}
+72
View File
@@ -0,0 +1,72 @@
package router
import (
"errors"
"fmt"
"os/exec"
)
const (
nvramCtrldKeyPrefix = "ctrld_"
nvramCtrldSetupKey = "ctrld_setup"
nvramCtrldInstallKey = "ctrld_install"
nvramRCStartupKey = "rc_startup"
)
//lint:ignore ST1005 This error is for human.
var errDdwrtJffs2NotEnabled = errors.New(`could not install service without jffs, follow this guide to enable:
https://wiki.dd-wrt.com/wiki/index.php/Journalling_Flash_File_System
`)
func setupDDWrt() error {
// Already setup.
if val, _ := nvram("get", nvramCtrldSetupKey); val == "1" {
return nil
}
data, err := dnsMasqConf()
if err != nil {
return err
}
nvramKvMap := nvramSetupKV()
nvramKvMap["dnsmasq_options"] = data
if err := nvramSetKV(nvramKvMap, nvramCtrldSetupKey); err != nil {
return err
}
// Restart dnsmasq service.
if err := restartDNSMasq(); err != nil {
return err
}
return nil
}
func cleanupDDWrt() error {
// Restore old configs.
if err := nvramRestore(nvramSetupKV(), nvramCtrldSetupKey); err != nil {
return err
}
// Restart dnsmasq service.
if err := restartDNSMasq(); err != nil {
return err
}
return nil
}
func postInstallDDWrt() error {
return nil
}
func ddwrtRestartDNSMasq() error {
if out, err := exec.Command("restart_dns").CombinedOutput(); err != nil {
return fmt.Errorf("restart_dns: %s, %w", string(out), err)
}
return nil
}
func ddwrtJff2Enabled() bool {
out, _ := nvram("get", "enable_jffs2")
return out == "1"
}
+87
View File
@@ -0,0 +1,87 @@
package router
import (
"strings"
"text/template"
)
const dnsMasqConfigContentTmpl = `# GENERATED BY ctrld - DO NOT MODIFY
no-resolv
server=127.0.0.1#5354
{{- if .SendClientInfo}}
add-mac
{{- end}}
`
const merlinDNSMasqPostConfPath = "/jffs/scripts/dnsmasq.postconf"
const merlinDNSMasqPostConfMarker = `# GENERATED BY ctrld - EOF`
const merlinDNSMasqPostConfTmpl = `# GENERATED BY ctrld - DO NOT MODIFY
#!/bin/sh
config_file="$1"
. /usr/sbin/helper.sh
pid=$(cat /tmp/ctrld.pid 2>/dev/null)
if [ -n "$pid" ] && [ -f "/proc/${pid}/cmdline" ]; then
pc_delete "servers-file" "$config_file" # no WAN DNS settings
pc_append "no-resolv" "$config_file" # do not read /etc/resolv.conf
pc_append "server=127.0.0.1#5354" "$config_file" # use ctrld as upstream
{{- if .SendClientInfo}}
pc_append "add-mac" "$config_file" # add client mac
{{- end}}
pc_delete "dnssec" "$config_file" # disable DNSSEC
pc_delete "trust-anchor=" "$config_file" # disable DNSSEC
# For John fork
pc_delete "resolv-file" "$config_file" # no WAN DNS settings
# Change /etc/resolv.conf, which may be changed by WAN DNS setup
pc_delete "nameserver" /etc/resolv.conf
pc_append "nameserver 127.0.0.1" /etc/resolv.conf
exit 0
fi
`
func dnsMasqConf() (string, error) {
var sb strings.Builder
var tmplText string
switch Name() {
case EdgeOS, DDWrt, OpenWrt, Ubios, Synology, Tomato:
tmplText = dnsMasqConfigContentTmpl
case Merlin:
tmplText = merlinDNSMasqPostConfTmpl
}
tmpl := template.Must(template.New("").Parse(tmplText))
var to = &struct {
SendClientInfo bool
}{
routerPlatform.Load().sendClientInfo,
}
if err := tmpl.Execute(&sb, to); err != nil {
return "", err
}
return sb.String(), nil
}
func restartDNSMasq() error {
switch Name() {
case EdgeOS:
return edgeOSRestartDNSMasq()
case DDWrt:
return ddwrtRestartDNSMasq()
case Merlin:
return merlinRestartDNSMasq()
case OpenWrt:
return openwrtRestartDNSMasq()
case Ubios:
return ubiosRestartDNSMasq()
case Synology:
return synologyRestartDNSMasq()
case Tomato:
return tomatoRestartService(tomatoDNSMasqSvcName)
}
panic("not supported platform")
}
+56
View File
@@ -0,0 +1,56 @@
package router
import (
"fmt"
"os"
"os/exec"
)
const edgeOSDNSMasqConfigPath = "/etc/dnsmasq.d/dnsmasq-zzz-ctrld.conf"
func setupEdgeOS() error {
// Disable dnsmasq as DNS server.
dnsMasqConfigContent, err := dnsMasqConf()
if err != nil {
return err
}
if err := os.WriteFile(edgeOSDNSMasqConfigPath, []byte(dnsMasqConfigContent), 0600); err != nil {
return err
}
// Restart dnsmasq service.
if err := restartDNSMasq(); err != nil {
return err
}
return nil
}
func cleanupEdgeOS() error {
// Remove the custom dnsmasq config
if err := os.Remove(edgeOSDNSMasqConfigPath); err != nil {
return err
}
// Restart dnsmasq service.
if err := restartDNSMasq(); err != nil {
return err
}
return nil
}
func postInstallEdgeOS() error {
// If "Content Filtering" is enabled, UniFi OS will create firewall rules to intercept all DNS queries
// from outside, and route those queries to separated interfaces (e.g: dnsfilter-2@if79) created by UniFi OS.
// Thus, those queries will never reach ctrld listener. UniFi OS does not provide any mechanism to toggle this
// feature via command line, so there's nothing ctrld can do to disable this feature. For now, reporting an
// error and guiding users to disable the feature using UniFi OS web UI.
if contentFilteringEnabled() {
return errContentFilteringEnabled
}
return nil
}
func edgeOSRestartDNSMasq() error {
if out, err := exec.Command("/etc/init.d/dnsmasq", "restart").CombinedOutput(); err != nil {
return fmt.Errorf("edgeosRestartDNSMasq: %s, %w", string(out), err)
}
return nil
}
+89
View File
@@ -0,0 +1,89 @@
package router
import (
"bytes"
"fmt"
"os"
"os/exec"
"strings"
"unicode"
)
func setupMerlin() error {
buf, err := os.ReadFile(merlinDNSMasqPostConfPath)
// Already setup.
if bytes.Contains(buf, []byte(merlinDNSMasqPostConfMarker)) {
return nil
}
if err != nil && !os.IsNotExist(err) {
return err
}
merlinDNSMasqPostConf, err := dnsMasqConf()
if err != nil {
return err
}
data := strings.Join([]string{
merlinDNSMasqPostConf,
"\n",
merlinDNSMasqPostConfMarker,
"\n",
string(buf),
}, "\n")
// Write dnsmasq post conf file.
if err := os.WriteFile(merlinDNSMasqPostConfPath, []byte(data), 0750); err != nil {
return err
}
// Restart dnsmasq service.
if err := restartDNSMasq(); err != nil {
return err
}
if err := nvramSetKV(nvramSetupKV(), nvramCtrldSetupKey); err != nil {
return err
}
return nil
}
func cleanupMerlin() error {
// Restore old configs.
if err := nvramRestore(nvramSetupKV(), nvramCtrldSetupKey); err != nil {
return err
}
buf, err := os.ReadFile(merlinDNSMasqPostConfPath)
if err != nil && !os.IsNotExist(err) {
return err
}
// Restore dnsmasq post conf file.
if err := os.WriteFile(merlinDNSMasqPostConfPath, merlinParsePostConf(buf), 0750); err != nil {
return err
}
// Restart dnsmasq service.
if err := restartDNSMasq(); err != nil {
return err
}
return nil
}
func postInstallMerlin() error {
return nil
}
func merlinRestartDNSMasq() error {
if out, err := exec.Command("service", "restart_dnsmasq").CombinedOutput(); err != nil {
return fmt.Errorf("restart_dnsmasq: %s, %w", string(out), err)
}
return nil
}
func merlinParsePostConf(buf []byte) []byte {
if len(buf) == 0 {
return nil
}
parts := bytes.Split(buf, []byte(merlinDNSMasqPostConfMarker))
if len(parts) != 1 {
return bytes.TrimLeftFunc(parts[1], unicode.IsSpace)
}
return buf
}
+38
View File
@@ -0,0 +1,38 @@
package router
import (
"bytes"
"strings"
"testing"
)
func Test_merlinParsePostConf(t *testing.T) {
origContent := "# foo"
data := strings.Join([]string{
merlinDNSMasqPostConfTmpl,
"\n",
merlinDNSMasqPostConfMarker,
"\n",
}, "\n")
tests := []struct {
name string
data string
expected string
}{
{"empty", "", ""},
{"no ctrld", origContent, origContent},
{"ctrld with data", data + origContent, origContent},
{"ctrld without data", data, ""},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
//t.Parallel()
if got := merlinParsePostConf([]byte(tc.data)); !bytes.Equal(got, []byte(tc.expected)) {
t.Errorf("unexpected result, want: %q, got: %q", tc.expected, string(got))
}
})
}
}
+110
View File
@@ -0,0 +1,110 @@
package router
import (
"bytes"
"fmt"
"os/exec"
"strings"
)
func nvram(args ...string) (string, error) {
cmd := exec.Command("nvram", args...)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("%s:%w", stderr.String(), err)
}
return strings.TrimSpace(stdout.String()), nil
}
/*
NOTE:
- For Openwrt, DNSSEC is not included in default dnsmasq (require dnsmasq-full).
- For Merlin, DNSSEC is configured during postconf script (see merlinDNSMasqPostConfTmpl).
- For Ubios UDM Pro/Dream Machine, DNSSEC is not included in their dnsmasq package:
+https://community.ui.com/questions/Implement-DNSSEC-into-UniFi/951c72b0-4d88-4c86-9174-45417bd2f9ca
+https://community.ui.com/questions/Enable-DNSSEC-for-Unifi-Dream-Machine-FW-updates/e68e367c-d09b-4459-9444-18908f7c1ea1
*/
func nvramSetupKV() map[string]string {
switch Name() {
case DDWrt:
return map[string]string{
"dns_dnsmasq": "1", // Make dnsmasq running but disable DNS ability, ctrld will replace it.
"dnsmasq_options": "", // Configuration of dnsmasq set by ctrld, filled by setupDDWrt.
"dns_crypt": "0", // Disable DNSCrypt.
"dnssec": "0", // Disable DNSSEC.
}
case Merlin:
return map[string]string{
"dnspriv_enable": "0", // Ensure Merlin native DoT disabled.
}
case Tomato:
return map[string]string{
"dnsmasq_custom": "", // Configuration of dnsmasq set by ctrld, filled by setupTomato.
"dnscrypt_proxy": "0", // Disable DNSCrypt.
"dnssec_enable": "0", // Disable DNSSEC.
"stubby_proxy": "0", // Disable Stubby
}
}
return nil
}
func nvramInstallKV() map[string]string {
switch Name() {
case Tomato:
return map[string]string{
tomatoNvramScriptWanupKey: "", // script to start ctrld, filled by tomatoSvc.Install method.
}
}
return nil
}
func nvramSetKV(m map[string]string, setupKey string) error {
// Backup current value, store ctrld's configs.
for key, value := range m {
old, err := nvram("get", key)
if err != nil {
return fmt.Errorf("%s: %w", old, err)
}
if out, err := nvram("set", nvramCtrldKeyPrefix+key+"="+old); err != nil {
return fmt.Errorf("%s: %w", out, err)
}
if out, err := nvram("set", key+"="+value); err != nil {
return fmt.Errorf("%s: %w", out, err)
}
}
if out, err := nvram("set", setupKey+"=1"); err != nil {
return fmt.Errorf("%s: %w", out, err)
}
// Commit.
if out, err := nvram("commit"); err != nil {
return fmt.Errorf("%s: %w", out, err)
}
return nil
}
func nvramRestore(m map[string]string, setupKey string) error {
// Restore old configs.
for key := range m {
ctrldKey := nvramCtrldKeyPrefix + key
old, err := nvram("get", ctrldKey)
if err != nil {
return fmt.Errorf("%s: %w", old, err)
}
_, _ = nvram("unset", ctrldKey)
if out, err := nvram("set", key+"="+old); err != nil {
return fmt.Errorf("%s: %w", out, err)
}
}
if out, err := nvram("unset", setupKey); err != nil {
return fmt.Errorf("%s: %w", out, err)
}
// Commit.
if out, err := nvram("commit"); err != nil {
return fmt.Errorf("%s: %w", out, err)
}
return nil
}
+94
View File
@@ -0,0 +1,94 @@
package router
import (
"bytes"
"errors"
"fmt"
"os"
"os/exec"
"strings"
)
var errUCIEntryNotFound = errors.New("uci: Entry not found")
const openwrtDNSMasqConfigPath = "/tmp/dnsmasq.d/ctrld.conf"
// IsGLiNet reports whether the router is an GL.iNet router.
func IsGLiNet() bool {
if Name() != OpenWrt {
return false
}
buf, _ := os.ReadFile("/proc/version")
// The output of /proc/version contains "(glinet@glinet)".
return bytes.Contains(buf, []byte(" (glinet"))
}
// IsOldOpenwrt reports whether the router is an "old" version of Openwrt,
// aka versions which don't have "service" command.
func IsOldOpenwrt() bool {
if Name() != OpenWrt {
return false
}
cmd, _ := exec.LookPath("service")
return cmd == ""
}
func setupOpenWrt() error {
// Delete dnsmasq port if set.
if _, err := uci("delete", "dhcp.@dnsmasq[0].port"); err != nil && !errors.Is(err, errUCIEntryNotFound) {
return err
}
dnsMasqConfigContent, err := dnsMasqConf()
if err != nil {
return err
}
if err := os.WriteFile(openwrtDNSMasqConfigPath, []byte(dnsMasqConfigContent), 0600); err != nil {
return err
}
// Commit.
if _, err := uci("commit"); err != nil {
return err
}
// Restart dnsmasq service.
if err := restartDNSMasq(); err != nil {
return err
}
return nil
}
func cleanupOpenWrt() error {
// Remove the custom dnsmasq config
if err := os.Remove(openwrtDNSMasqConfigPath); err != nil {
return err
}
// Restart dnsmasq service.
if err := restartDNSMasq(); err != nil {
return err
}
return nil
}
func postInstallOpenWrt() error {
return exec.Command("/etc/init.d/ctrld", "enable").Run()
}
func uci(args ...string) (string, error) {
cmd := exec.Command("uci", args...)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
if strings.HasPrefix(stderr.String(), errUCIEntryNotFound.Error()) {
return "", errUCIEntryNotFound
}
return "", fmt.Errorf("%s:%w", stderr.String(), err)
}
return strings.TrimSpace(stdout.String()), nil
}
func openwrtRestartDNSMasq() error {
if out, err := exec.Command("/etc/init.d/dnsmasq", "restart").CombinedOutput(); err != nil {
return fmt.Errorf("%s: %w", string(out), err)
}
return nil
}
+66
View File
@@ -0,0 +1,66 @@
package router
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"github.com/kardianos/service"
)
const (
rcPath = "/usr/local/etc/rc.d"
unboundRcPath = rcPath + "/unbound"
dnsmasqRcPath = rcPath + "/dnsmasq"
)
func setupPfsense() error {
// If Pfsense is in DNS Resolver mode, ensure no unbound processes running.
_ = exec.Command("killall", "unbound").Run()
// If Pfsense is in DNS Forwarder mode, ensure no dnsmasq processes running.
_ = exec.Command("killall", "dnsmasq").Run()
return nil
}
func cleanupPfsense(svc *service.Config) error {
if err := os.Remove(filepath.Join(rcPath, svc.Name+".sh")); err != nil {
return fmt.Errorf("os.Remove: %w", err)
}
_ = exec.Command(unboundRcPath, "onerestart").Run()
_ = exec.Command(dnsmasqRcPath, "onerestart").Run()
return nil
}
func postInstallPfsense(svc *service.Config) error {
// pfsense need ".sh" extension for script to be run at boot.
// See: https://docs.netgate.com/pfsense/en/latest/development/boot-commands.html#shell-script-option
oldname := filepath.Join(rcPath, svc.Name)
newname := filepath.Join(rcPath, svc.Name+".sh")
_ = os.Remove(newname)
if err := os.Symlink(oldname, newname); err != nil {
return fmt.Errorf("os.Symlink: %w", err)
}
return nil
}
const pfsenseInitScript = `#!/bin/sh
# PROVIDE: {{.Name}}
# REQUIRE: SERVERS
# REQUIRE: unbound dnsmasq securelevel
# KEYWORD: shutdown
. /etc/rc.subr
name="{{.Name}}"
{{.Name}}_env="IS_DAEMON=1"
pidfile="/var/run/${name}.pid"
command="/usr/sbin/daemon"
daemon_args="-P ${pidfile} -r -t \"${name}: daemon\"{{if .WorkingDirectory}} -c {{.WorkingDirectory}}{{end}}"
command_args="${daemon_args} {{.Path}}{{range .Arguments}} {{.}}{{end}}"
run_rc_command "$1"
`
+24
View File
@@ -0,0 +1,24 @@
package router
const openWrtScript = `#!/bin/sh /etc/rc.common
USE_PROCD=1
# After network starts
START=21
# Before network stops
STOP=89
cmd="{{.Path}}{{range .Arguments}} {{.|cmd}}{{end}}"
name="{{.Name}}"
pid_file="/var/run/${name}.pid"
start_service() {
echo "Starting ${name}"
procd_open_instance
procd_set_param command ${cmd}
procd_set_param respawn # respawn automatically if something died
procd_set_param stdout 1 # forward stdout of the command to logd
procd_set_param stderr 1 # same for stderr
procd_set_param pidfile ${pid_file} # write a pid file on instance start and remove it on stop
procd_close_instance
echo "${name} has been started"
}
`
+255
View File
@@ -0,0 +1,255 @@
package router
import (
"bytes"
"context"
"errors"
"fmt"
"os"
"os/exec"
"sync"
"sync/atomic"
"time"
"github.com/fsnotify/fsnotify"
"github.com/kardianos/service"
"tailscale.com/logtail/backoff"
"github.com/Control-D-Inc/ctrld"
)
const (
OpenWrt = "openwrt"
DDWrt = "ddwrt"
Merlin = "merlin"
Ubios = "ubios"
Synology = "synology"
Tomato = "tomato"
EdgeOS = "edgeos"
Pfsense = "pfsense"
)
// ErrNotSupported reports the current router is not supported error.
var ErrNotSupported = errors.New("unsupported platform")
var routerPlatform atomic.Pointer[router]
type router struct {
name string
sendClientInfo bool
mac sync.Map
watcher *fsnotify.Watcher
}
// IsSupported reports whether the given platform is supported by ctrld.
func IsSupported(platform string) bool {
switch platform {
case EdgeOS, DDWrt, Merlin, OpenWrt, Pfsense, Synology, Tomato, Ubios:
return true
}
return false
}
// SupportedPlatforms return all platforms that can be configured to run with ctrld.
func SupportedPlatforms() []string {
return []string{EdgeOS, DDWrt, Merlin, OpenWrt, Pfsense, Synology, Tomato, Ubios}
}
var configureFunc = map[string]func() error{
EdgeOS: setupEdgeOS,
DDWrt: setupDDWrt,
Merlin: setupMerlin,
OpenWrt: setupOpenWrt,
Pfsense: setupPfsense,
Synology: setupSynology,
Tomato: setupTomato,
Ubios: setupUbiOS,
}
// Configure configures things for running ctrld on the router.
func Configure(c *ctrld.Config) error {
name := Name()
switch name {
case EdgeOS, DDWrt, Merlin, OpenWrt, Pfsense, Synology, Tomato, Ubios:
if c.HasUpstreamSendClientInfo() {
r := routerPlatform.Load()
r.sendClientInfo = true
watcher, err := fsnotify.NewWatcher()
if err != nil {
return err
}
r.watcher = watcher
go r.watchClientInfoTable()
for file, readClienInfoFunc := range clientInfoFiles {
_ = readClienInfoFunc(file)
_ = r.watcher.Add(file)
}
}
configure := configureFunc[name]
if err := configure(); err != nil {
return err
}
return nil
default:
return ErrNotSupported
}
}
// ConfigureService performs necessary setup for running ctrld as a service on router.
func ConfigureService(sc *service.Config) error {
name := Name()
switch name {
case DDWrt:
if !ddwrtJff2Enabled() {
return errDdwrtJffs2NotEnabled
}
case OpenWrt:
sc.Option["SysvScript"] = openWrtScript
case Pfsense:
sc.Option["SysvScript"] = pfsenseInitScript
case EdgeOS, Merlin, Synology, Tomato, Ubios:
}
return nil
}
// PreRun blocks until the router is ready for running ctrld.
func PreRun() (err error) {
// On some routers, NTP may out of sync, so waiting for it to be ready.
switch Name() {
case Merlin, Tomato:
// Wait until `ntp_ready=1` set.
b := backoff.NewBackoff("PreStart", func(format string, args ...any) {}, 10*time.Second)
for {
out, err := nvram("get", "ntp_ready")
if err != nil {
return fmt.Errorf("PreStart: nvram: %w", err)
}
if out == "1" {
return nil
}
b.BackOff(context.Background(), errors.New("ntp not ready"))
}
default:
return nil
}
}
// PostInstall performs task after installing ctrld on router.
func PostInstall(svc *service.Config) error {
name := Name()
switch name {
case EdgeOS:
return postInstallEdgeOS()
case DDWrt:
return postInstallDDWrt()
case Merlin:
return postInstallMerlin()
case OpenWrt:
return postInstallOpenWrt()
case Pfsense:
return postInstallPfsense(svc)
case Synology:
return postInstallSynology()
case Tomato:
return postInstallTomato()
case Ubios:
return postInstallUbiOS()
}
return nil
}
// Cleanup cleans ctrld setup on the router.
func Cleanup(svc *service.Config) error {
name := Name()
switch name {
case EdgeOS:
return cleanupEdgeOS()
case DDWrt:
return cleanupDDWrt()
case Merlin:
return cleanupMerlin()
case OpenWrt:
return cleanupOpenWrt()
case Pfsense:
return cleanupPfsense(svc)
case Synology:
return cleanupSynology()
case Tomato:
return cleanupTomato()
case Ubios:
return cleanupUbiOS()
}
return nil
}
// ListenAddress returns the listener address of ctrld on router.
func ListenAddress() string {
name := Name()
switch name {
case EdgeOS, DDWrt, Merlin, OpenWrt, Synology, Tomato, Ubios:
return "127.0.0.1:5354"
case Pfsense:
// On pfsense, we run ctrld as DNS resolver.
}
return ""
}
// Name returns name of the router platform.
func Name() string {
if r := routerPlatform.Load(); r != nil {
return r.name
}
r := &router{}
r.name = distroName()
routerPlatform.Store(r)
return r.name
}
func distroName() string {
switch {
case bytes.HasPrefix(unameO(), []byte("DD-WRT")):
return DDWrt
case bytes.HasPrefix(unameO(), []byte("ASUSWRT-Merlin")):
return Merlin
case haveFile("/etc/openwrt_version"):
return OpenWrt
case haveDir("/data/unifi"):
return Ubios
case bytes.HasPrefix(unameU(), []byte("synology")):
return Synology
case bytes.HasPrefix(unameO(), []byte("Tomato")):
return Tomato
case haveDir("/config/scripts/post-config.d"):
return EdgeOS
case haveFile("/etc/ubnt/init/vyatta-router"):
return EdgeOS // For 2.x
case isPfsense():
return Pfsense
}
return ""
}
func haveFile(file string) bool {
_, err := os.Stat(file)
return err == nil
}
func haveDir(dir string) bool {
fi, _ := os.Stat(dir)
return fi != nil && fi.IsDir()
}
func unameO() []byte {
out, _ := exec.Command("uname", "-o").Output()
return out
}
func unameU() []byte {
out, _ := exec.Command("uname", "-u").Output()
return out
}
func isPfsense() bool {
b, err := os.ReadFile("/etc/platform")
return err == nil && bytes.HasPrefix(b, []byte("pfSense"))
}
+91
View File
@@ -0,0 +1,91 @@
package router
import (
"bytes"
"os"
"os/exec"
"github.com/kardianos/service"
)
func init() {
systems := []service.System{
&linuxSystemService{
name: "ddwrt",
detect: func() bool { return Name() == DDWrt },
interactive: func() bool {
is, _ := isInteractive()
return is
},
new: newddwrtService,
},
&linuxSystemService{
name: "merlin",
detect: func() bool { return Name() == Merlin },
interactive: func() bool {
is, _ := isInteractive()
return is
},
new: newMerlinService,
},
&linuxSystemService{
name: "ubios",
detect: func() bool {
if Name() != Ubios {
return false
}
out, err := exec.Command("ubnt-device-info", "firmware").CombinedOutput()
if err == nil {
// For v2/v3, UbiOS use a Debian base with systemd, so it is not
// necessary to use custom implementation for supporting init system.
return bytes.HasPrefix(out, []byte("1."))
}
return true
},
interactive: func() bool {
is, _ := isInteractive()
return is
},
new: newUbiosService,
},
&linuxSystemService{
name: "tomato",
detect: func() bool { return Name() == Tomato },
interactive: func() bool {
is, _ := isInteractive()
return is
},
new: newTomatoService,
},
}
systems = append(systems, service.AvailableSystems()...)
service.ChooseSystem(systems...)
}
type linuxSystemService struct {
name string
detect func() bool
interactive func() bool
new func(i service.Interface, platform string, c *service.Config) (service.Service, error)
}
func (sc linuxSystemService) String() string {
return sc.name
}
func (sc linuxSystemService) Detect() bool {
return sc.detect()
}
func (sc linuxSystemService) Interactive() bool {
return sc.interactive()
}
func (sc linuxSystemService) New(i service.Interface, c *service.Config) (service.Service, error) {
return sc.new(i, sc.String(), c)
}
func isInteractive() (bool, error) {
ppid := os.Getppid()
if ppid == 1 {
return false, nil
}
return true, nil
}
+292
View File
@@ -0,0 +1,292 @@
package router
import (
"bytes"
"errors"
"fmt"
"os"
"os/exec"
"os/signal"
"strings"
"syscall"
"text/template"
"github.com/kardianos/service"
)
type ddwrtSvc struct {
i service.Interface
platform string
*service.Config
rcStartup string
}
func newddwrtService(i service.Interface, platform string, c *service.Config) (service.Service, error) {
s := &ddwrtSvc{
i: i,
platform: platform,
Config: c,
}
if err := os.MkdirAll("/jffs/etc/config", 0644); err != nil {
return nil, err
}
return s, nil
}
func (s *ddwrtSvc) String() string {
if len(s.DisplayName) > 0 {
return s.DisplayName
}
return s.Name
}
func (s *ddwrtSvc) Platform() string {
return s.platform
}
func (s *ddwrtSvc) configPath() string {
return fmt.Sprintf("/jffs/etc/config/%s.startup", s.Config.Name)
}
func (s *ddwrtSvc) template() *template.Template {
return template.Must(template.New("").Parse(ddwrtSvcScript))
}
func (s *ddwrtSvc) Install() error {
confPath := s.configPath()
if _, err := os.Stat(confPath); err == nil {
return fmt.Errorf("already installed: %s", confPath)
}
path, err := os.Executable()
if err != nil {
return err
}
if !strings.HasPrefix(path, "/jffs/") {
return errors.New("could not install service outside /jffs")
}
var to = &struct {
*service.Config
Path string
}{
s.Config,
path,
}
f, err := os.Create(confPath)
if err != nil {
return err
}
defer f.Close()
if err := s.template().Execute(f, to); err != nil {
return err
}
if err = os.Chmod(confPath, 0755); err != nil {
return err
}
var sb strings.Builder
if err := template.Must(template.New("").Parse(ddwrtStartupCmd)).Execute(&sb, to); err != nil {
return err
}
s.rcStartup = sb.String()
curVal, err := nvram("get", nvramRCStartupKey)
if err != nil {
return err
}
if _, err := nvram("set", nvramCtrldKeyPrefix+nvramRCStartupKey+"="+curVal); err != nil {
return err
}
val := strings.Join([]string{curVal, s.rcStartup + " &", fmt.Sprintf(`echo $! > "/tmp/%s.pid"`, s.Config.Name)}, "\n")
if _, err := nvram("set", nvramRCStartupKey+"="+val); err != nil {
return err
}
if out, err := nvram("commit"); err != nil {
return fmt.Errorf("%s: %w", out, err)
}
return nil
}
func (s *ddwrtSvc) Uninstall() error {
if err := os.Remove(s.configPath()); err != nil {
return err
}
ctrldStartupKey := nvramCtrldKeyPrefix + nvramRCStartupKey
rcStartup, err := nvram("get", ctrldStartupKey)
if err != nil {
return err
}
_, _ = nvram("unset", ctrldStartupKey)
if _, err := nvram("set", nvramRCStartupKey+"="+rcStartup); err != nil {
return err
}
if out, err := nvram("commit"); err != nil {
return fmt.Errorf("%s: %w", out, err)
}
return nil
}
func (s *ddwrtSvc) Logger(errs chan<- error) (service.Logger, error) {
if service.Interactive() {
return service.ConsoleLogger, nil
}
return s.SystemLogger(errs)
}
func (s *ddwrtSvc) SystemLogger(errs chan<- error) (service.Logger, error) {
// TODO(cuonglm): detect syslog enable and return proper logger?
// this at least works with default configuration.
if service.Interactive() {
return service.ConsoleLogger, nil
}
return &noopLogger{}, nil
}
func (s *ddwrtSvc) Run() (err error) {
err = s.i.Start(s)
if err != nil {
return err
}
if interactice, _ := isInteractive(); !interactice {
signal.Ignore(syscall.SIGHUP)
}
var sigChan = make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGTERM, os.Interrupt)
<-sigChan
return s.i.Stop(s)
}
func (s *ddwrtSvc) Status() (service.Status, error) {
if _, err := os.Stat(s.configPath()); os.IsNotExist(err) {
return service.StatusUnknown, service.ErrNotInstalled
}
out, err := exec.Command(s.configPath(), "status").CombinedOutput()
if err != nil {
return service.StatusUnknown, err
}
switch string(bytes.TrimSpace(out)) {
case "running":
return service.StatusRunning, nil
default:
return service.StatusStopped, nil
}
}
func (s *ddwrtSvc) Start() error {
return exec.Command(s.configPath(), "start").Run()
}
func (s *ddwrtSvc) Stop() error {
return exec.Command(s.configPath(), "stop").Run()
}
func (s *ddwrtSvc) Restart() error {
err := s.Stop()
if err != nil {
return err
}
return s.Start()
}
type noopLogger struct {
}
func (c noopLogger) Error(v ...interface{}) error {
return nil
}
func (c noopLogger) Warning(v ...interface{}) error {
return nil
}
func (c noopLogger) Info(v ...interface{}) error {
return nil
}
func (c noopLogger) Errorf(format string, a ...interface{}) error {
return nil
}
func (c noopLogger) Warningf(format string, a ...interface{}) error {
return nil
}
func (c noopLogger) Infof(format string, a ...interface{}) error {
return nil
}
const ddwrtStartupCmd = `{{.Path}}{{range .Arguments}} {{.}}{{end}}`
const ddwrtSvcScript = `#!/bin/sh
name="{{.Name}}"
cmd="{{.Path}}{{range .Arguments}} {{.}}{{end}}"
pid_file="/tmp/$name.pid"
get_pid() {
cat "$pid_file"
}
is_running() {
[ -f "$pid_file" ] && ps | grep -q "^ *$(get_pid) "
}
case "$1" in
start)
if is_running; then
echo "Already started"
else
echo "Starting $name"
$cmd &
echo $! > "$pid_file"
chmod 600 "$pid_file"
if ! is_running; then
echo "Failed to start $name"
exit 1
fi
fi
;;
stop)
if is_running; then
echo -n "Stopping $name..."
kill "$(get_pid)"
for _ in 1 2 3 4 5; do
if ! is_running; then
echo "stopped"
if [ -f "$pid_file" ]; then
rm "$pid_file"
fi
exit 0
fi
printf "."
sleep 2
done
echo "failed to stop $name"
exit 1
fi
exit 1
;;
restart)
$0 stop
$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
`
+354
View File
@@ -0,0 +1,354 @@
package router
import (
"bytes"
"errors"
"fmt"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strings"
"syscall"
"text/template"
"github.com/kardianos/service"
)
const (
merlinJFFSScriptPath = "/jffs/scripts/services-start"
merlinJFFSServiceEventScriptPath = "/jffs/scripts/service-event"
)
type merlinSvc struct {
i service.Interface
platform string
*service.Config
}
func newMerlinService(i service.Interface, platform string, c *service.Config) (service.Service, error) {
s := &merlinSvc{
i: i,
platform: platform,
Config: c,
}
return s, nil
}
func (s *merlinSvc) String() string {
if len(s.DisplayName) > 0 {
return s.DisplayName
}
return s.Name
}
func (s *merlinSvc) Platform() string {
return s.platform
}
func (s *merlinSvc) configPath() string {
path, err := os.Executable()
if err != nil {
return ""
}
return path + ".startup"
}
func (s *merlinSvc) template() *template.Template {
return template.Must(template.New("").Parse(merlinSvcScript))
}
func (s *merlinSvc) Install() error {
exePath, err := os.Executable()
if err != nil {
return err
}
if !strings.HasPrefix(exePath, "/jffs/") {
return errors.New("could not install service outside /jffs")
}
if _, err := nvram("set", "jffs2_scripts=1"); err != nil {
return err
}
if _, err := nvram("commit"); err != nil {
return err
}
confPath := s.configPath()
if _, err := os.Stat(confPath); err == nil {
return fmt.Errorf("already installed: %s", confPath)
}
var to = &struct {
*service.Config
Path string
}{
s.Config,
exePath,
}
f, err := os.Create(confPath)
if err != nil {
return fmt.Errorf("os.Create: %w", err)
}
defer f.Close()
if err := s.template().Execute(f, to); err != nil {
return fmt.Errorf("s.template.Execute: %w", err)
}
if err = os.Chmod(confPath, 0755); err != nil {
return fmt.Errorf("os.Chmod: startup script: %w", err)
}
if err := os.MkdirAll(filepath.Dir(merlinJFFSScriptPath), 0755); err != nil {
return fmt.Errorf("os.MkdirAll: %w", err)
}
tmpScript, err := os.CreateTemp("", "ctrld_install")
if err != nil {
return fmt.Errorf("os.CreateTemp: %w", err)
}
defer os.Remove(tmpScript.Name())
defer tmpScript.Close()
if _, err := tmpScript.WriteString(merlinAddLineToScript); err != nil {
return fmt.Errorf("tmpScript.WriteString: %w", err)
}
if err := tmpScript.Close(); err != nil {
return fmt.Errorf("tmpScript.Close: %w", err)
}
addLineToScript := func(line, script string) error {
if _, err := os.Stat(script); os.IsNotExist(err) {
if err := os.WriteFile(script, []byte("#!/bin/sh\n"), 0755); err != nil {
return err
}
}
if err := os.Chmod(script, 0755); err != nil {
return fmt.Errorf("os.Chmod: jffs script: %w", err)
}
if err := exec.Command("sh", tmpScript.Name(), line, script).Run(); err != nil {
return fmt.Errorf("exec.Command: add startup script: %w", err)
}
return nil
}
for script, line := range map[string]string{
merlinJFFSScriptPath: s.configPath() + " start",
merlinJFFSServiceEventScriptPath: s.configPath() + ` service_event "$1" "$2"`,
} {
if err := addLineToScript(line, script); err != nil {
return err
}
}
return nil
}
func (s *merlinSvc) Uninstall() error {
if err := os.Remove(s.configPath()); err != nil {
return fmt.Errorf("os.Remove: %w", err)
}
tmpScript, err := os.CreateTemp("", "ctrld_uninstall")
if err != nil {
return fmt.Errorf("os.CreateTemp: %w", err)
}
defer os.Remove(tmpScript.Name())
defer tmpScript.Close()
if _, err := tmpScript.WriteString(merlinRemoveLineFromScript); err != nil {
return fmt.Errorf("tmpScript.WriteString: %w", err)
}
if err := tmpScript.Close(); err != nil {
return fmt.Errorf("tmpScript.Close: %w", err)
}
removeLineFromScript := func(line, script string) error {
if _, err := os.Stat(script); os.IsNotExist(err) {
if err := os.WriteFile(script, []byte("#!/bin/sh\n"), 0755); err != nil {
return err
}
}
if err := os.Chmod(script, 0755); err != nil {
return fmt.Errorf("os.Chmod: jffs script: %w", err)
}
if err := exec.Command("sh", tmpScript.Name(), line, script).Run(); err != nil {
return fmt.Errorf("exec.Command: add startup script: %w", err)
}
return nil
}
for script, line := range map[string]string{
merlinJFFSScriptPath: s.configPath() + " start",
merlinJFFSServiceEventScriptPath: s.configPath() + ` service_event "$1" "$2"`,
} {
if err := removeLineFromScript(line, script); err != nil {
return err
}
}
return nil
}
func (s *merlinSvc) Logger(errs chan<- error) (service.Logger, error) {
if service.Interactive() {
return service.ConsoleLogger, nil
}
return s.SystemLogger(errs)
}
func (s *merlinSvc) SystemLogger(errs chan<- error) (service.Logger, error) {
return newSysLogger(s.Name, errs)
}
func (s *merlinSvc) Run() (err error) {
err = s.i.Start(s)
if err != nil {
return err
}
if interactice, _ := isInteractive(); !interactice {
signal.Ignore(syscall.SIGHUP)
}
var sigChan = make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGTERM, os.Interrupt)
<-sigChan
return s.i.Stop(s)
}
func (s *merlinSvc) Status() (service.Status, error) {
if _, err := os.Stat(s.configPath()); os.IsNotExist(err) {
return service.StatusUnknown, service.ErrNotInstalled
}
out, err := exec.Command(s.configPath(), "status").CombinedOutput()
if err != nil {
return service.StatusUnknown, err
}
switch string(bytes.TrimSpace(out)) {
case "running":
return service.StatusRunning, nil
default:
return service.StatusStopped, nil
}
}
func (s *merlinSvc) Start() error {
return exec.Command(s.configPath(), "start").Run()
}
func (s *merlinSvc) Stop() error {
return exec.Command(s.configPath(), "stop").Run()
}
func (s *merlinSvc) Restart() error {
err := s.Stop()
if err != nil {
return err
}
return s.Start()
}
const merlinSvcScript = `#!/bin/sh
name="{{.Name}}"
cmd="{{.Path}}{{range .Arguments}} {{.}}{{end}}"
pid_file="/tmp/$name.pid"
get_pid() {
cat "$pid_file"
}
is_running() {
[ -f "$pid_file" ] && ps | grep -q "^ *$(get_pid) "
}
case "$1" in
start)
if is_running; then
logger -c "Already started"
else
logger -c "Starting $name"
if [ -f /rom/ca-bundle.crt ]; then
# For Johns fork
export SSL_CERT_FILE=/rom/ca-bundle.crt
fi
$cmd &
echo $! > "$pid_file"
chmod 600 "$pid_file"
if ! is_running; then
logger -c "Failed to start $name"
exit 1
fi
fi
;;
stop)
if is_running; then
logger -c "Stopping $name..."
kill "$(get_pid)"
for _ in 1 2 3 4 5; do
if ! is_running; then
logger -c "stopped"
if [ -f "$pid_file" ]; then
rm "$pid_file"
fi
exit 0
fi
printf "."
sleep 2
done
logger -c "failed to stop $name"
exit 1
fi
exit 1
;;
restart)
$0 stop
$0 start
;;
status)
if is_running; then
echo "running"
else
echo "stopped"
exit 1
fi
;;
service_event)
event=$2
svc=$3
dnsmasq_pid_file=$(sed -n '/pid-file=/s///p' /etc/dnsmasq.conf)
if [ "$event" = "restart" ] && [ "$svc" = "diskmon" ]; then
kill "$(cat "$dnsmasq_pid_file")" >/dev/null 2>&1
fi
;;
*)
echo "Usage: $0 {start|stop|restart|status}"
exit 1
;;
esac
exit 0
`
const merlinAddLineToScript = `#!/bin/sh
line=$1
file=$2
. /usr/sbin/helper.sh
pc_append "$line" "$file"
`
const merlinRemoveLineFromScript = `#!/bin/sh
line=$1
file=$2
. /usr/sbin/helper.sh
pc_delete "$line" "$file"
`
+278
View File
@@ -0,0 +1,278 @@
package router
import (
"bytes"
"errors"
"fmt"
"os"
"os/exec"
"os/signal"
"strings"
"syscall"
"text/template"
"github.com/kardianos/service"
)
const tomatoNvramScriptWanupKey = "script_wanup"
type tomatoSvc struct {
i service.Interface
platform string
*service.Config
}
func newTomatoService(i service.Interface, platform string, c *service.Config) (service.Service, error) {
s := &tomatoSvc{
i: i,
platform: platform,
Config: c,
}
return s, nil
}
func (s *tomatoSvc) String() string {
if len(s.DisplayName) > 0 {
return s.DisplayName
}
return s.Name
}
func (s *tomatoSvc) Platform() string {
return s.platform
}
func (s *tomatoSvc) configPath() string {
path, err := os.Executable()
if err != nil {
return ""
}
return path + ".startup"
}
func (s *tomatoSvc) template() *template.Template {
return template.Must(template.New("").Parse(tomatoSvcScript))
}
func (s *tomatoSvc) Install() error {
exePath, err := os.Executable()
if err != nil {
return err
}
if !strings.HasPrefix(exePath, "/jffs/") {
return errors.New("could not install service outside /jffs")
}
if _, err := nvram("set", "jffs2_on=1"); err != nil {
return err
}
if _, err := nvram("commit"); err != nil {
return err
}
confPath := s.configPath()
if _, err := os.Stat(confPath); err == nil {
return fmt.Errorf("already installed: %s", confPath)
}
var to = &struct {
*service.Config
Path string
}{
s.Config,
exePath,
}
f, err := os.Create(confPath)
if err != nil {
return fmt.Errorf("os.Create: %w", err)
}
defer f.Close()
if err := s.template().Execute(f, to); err != nil {
return fmt.Errorf("s.template.Execute: %w", err)
}
if err = os.Chmod(confPath, 0755); err != nil {
return fmt.Errorf("os.Chmod: startup script: %w", err)
}
nvramKvMap := nvramInstallKV()
old, err := nvram("get", tomatoNvramScriptWanupKey)
if err != nil {
return fmt.Errorf("nvram: %w", err)
}
nvramKvMap[tomatoNvramScriptWanupKey] = strings.Join([]string{old, s.configPath() + " start"}, "\n")
if err := nvramSetKV(nvramKvMap, nvramCtrldInstallKey); err != nil {
return err
}
return nil
}
func (s *tomatoSvc) Uninstall() error {
if err := os.Remove(s.configPath()); err != nil {
return fmt.Errorf("os.Remove: %w", err)
}
// Restore old configs.
if err := nvramRestore(nvramInstallKV(), nvramCtrldInstallKey); err != nil {
return err
}
return nil
}
func (s *tomatoSvc) Logger(errs chan<- error) (service.Logger, error) {
if service.Interactive() {
return service.ConsoleLogger, nil
}
return s.SystemLogger(errs)
}
func (s *tomatoSvc) SystemLogger(errs chan<- error) (service.Logger, error) {
return newSysLogger(s.Name, errs)
}
func (s *tomatoSvc) Run() (err error) {
err = s.i.Start(s)
if err != nil {
return err
}
if interactice, _ := isInteractive(); !interactice {
signal.Ignore(syscall.SIGHUP)
}
var sigChan = make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGTERM, os.Interrupt)
<-sigChan
return s.i.Stop(s)
}
func (s *tomatoSvc) Status() (service.Status, error) {
if _, err := os.Stat(s.configPath()); os.IsNotExist(err) {
return service.StatusUnknown, service.ErrNotInstalled
}
out, err := exec.Command(s.configPath(), "status").CombinedOutput()
if err != nil {
return service.StatusUnknown, err
}
switch string(bytes.TrimSpace(out)) {
case "running":
return service.StatusRunning, nil
default:
return service.StatusStopped, nil
}
}
func (s *tomatoSvc) Start() error {
return exec.Command(s.configPath(), "start").Run()
}
func (s *tomatoSvc) Stop() error {
return exec.Command(s.configPath(), "stop").Run()
}
func (s *tomatoSvc) Restart() error {
return exec.Command(s.configPath(), "restart").Run()
}
// https://wiki.freshtomato.org/doku.php/freshtomato_zerotier?s[]=%2Aservice%2A
const tomatoSvcScript = `#!/bin/sh
NAME="{{.Name}}"
CMD="{{.Path}}{{range .Arguments}} {{.}}{{end}}"
LOG_FILE="/var/log/${NAME}.log"
PID_FILE="/tmp/$NAME.pid"
alias elog="logger -t $NAME -s"
COND=$1
[ $# -eq 0 ] && COND="start"
get_pid() {
cat "$PID_FILE"
}
is_running() {
[ -f "$PID_FILE" ] && ps | grep -q "^ *$(get_pid) "
}
start() {
if is_running; then
elog "$NAME is already running."
exit 1
fi
elog "Starting $NAME Services: "
$CMD &
echo $! > "$PID_FILE"
chmod 600 "$PID_FILE"
if is_running; then
elog "succeeded."
else
elog "failed."
fi
}
stop() {
if ! is_running; then
elog "$NAME is not running."
exit 1
fi
elog "Shutting down $NAME Services: "
kill -SIGTERM "$(get_pid)"
for _ in 1 2 3 4 5; do
if ! is_running; then
if [ -f "$pid_file" ]; then
rm "$pid_file"
fi
return 0
fi
printf "."
sleep 2
done
if ! is_running; then
elog "succeeded."
else
elog "failed."
fi
}
do_restart() {
stop
start
}
do_status() {
if ! is_running; then
echo "stopped"
else
echo "running"
fi
}
case "$COND" in
start)
start
;;
stop)
stop
;;
restart)
do_restart
;;
status)
do_status
;;
*)
elog "Usage: $0 (start|stop|restart|status)"
;;
esac
exit 0
`
+336
View File
@@ -0,0 +1,336 @@
package router
import (
"bytes"
"fmt"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strings"
"syscall"
"text/template"
"time"
"github.com/kardianos/service"
)
// This is a copy of https://github.com/kardianos/service/blob/v1.2.1/service_sysv_linux.go,
// with modification for supporting ubios v1 init system.
type ubiosSvc struct {
i service.Interface
platform string
*service.Config
}
func newUbiosService(i service.Interface, platform string, c *service.Config) (service.Service, error) {
s := &ubiosSvc{
i: i,
platform: platform,
Config: c,
}
return s, nil
}
func (s *ubiosSvc) String() string {
if len(s.DisplayName) > 0 {
return s.DisplayName
}
return s.Name
}
func (s *ubiosSvc) Platform() string {
return s.platform
}
func (s *ubiosSvc) configPath() string {
return "/etc/init.d/" + s.Config.Name
}
func (s *ubiosSvc) execPath() (string, error) {
if len(s.Executable) != 0 {
return filepath.Abs(s.Executable)
}
return os.Executable()
}
func (s *ubiosSvc) template() *template.Template {
return template.Must(template.New("").Funcs(tf).Parse(ubiosSvcScript))
}
func (s *ubiosSvc) Install() error {
confPath := s.configPath()
if _, err := os.Stat(confPath); err == nil {
return fmt.Errorf("init already exists: %s", confPath)
}
f, err := os.Create(confPath)
if err != nil {
return fmt.Errorf("failed to create config path: %w", err)
}
defer f.Close()
path, err := s.execPath()
if err != nil {
return fmt.Errorf("failed to get exec path: %w", err)
}
var to = &struct {
*service.Config
Path string
DnsMasqConfPath string
}{
s.Config,
path,
ubiosDNSMasqConfigPath,
}
if err := s.template().Execute(f, to); err != nil {
return fmt.Errorf("failed to create init script: %w", err)
}
if err := f.Close(); err != nil {
return fmt.Errorf("failed to save init script: %w", err)
}
if err = os.Chmod(confPath, 0755); err != nil {
return fmt.Errorf("failed to set init script executable: %w", err)
}
// Enable on boot
script, err := os.CreateTemp("", "ctrld_boot.service")
if err != nil {
return fmt.Errorf("failed to create boot service tmp file: %w", err)
}
defer script.Close()
svcConfig := *to.Config
svcConfig.Arguments = os.Args[1:]
to.Config = &svcConfig
if err := template.Must(template.New("").Funcs(tf).Parse(ubiosBootSystemdService)).Execute(script, &to); err != nil {
return fmt.Errorf("failed to create boot service file: %w", err)
}
if err := script.Close(); err != nil {
return fmt.Errorf("failed to save boot service file: %w", err)
}
// Copy the boot script to container and start.
cmd := exec.Command("podman", "cp", "--pause=false", script.Name(), "unifi-os:/lib/systemd/system/ctrld-boot.service")
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to copy boot script, out: %s, err: %v", string(out), err)
}
cmd = exec.Command("podman", "exec", "unifi-os", "systemctl", "enable", "--now", "ctrld-boot.service")
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to start ctrld boot script, out: %s, err: %v", string(out), err)
}
return nil
}
func (s *ubiosSvc) Uninstall() error {
if err := os.Remove(s.configPath()); err != nil {
return err
}
// Remove ctrld-boot service inside unifi-os container.
cmd := exec.Command("podman", "exec", "unifi-os", "systemctl", "disable", "ctrld-boot.service")
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to disable ctrld-boot service, out: %s, err: %v", string(out), err)
}
cmd = exec.Command("podman", "exec", "unifi-os", "rm", "/lib/systemd/system/ctrld-boot.service")
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to remove ctrld-boot service file, out: %s, err: %v", string(out), err)
}
cmd = exec.Command("podman", "exec", "unifi-os", "systemctl", "daemon-reload")
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to reload systemd service, out: %s, err: %v", string(out), err)
}
cmd = exec.Command("podman", "exec", "unifi-os", "systemctl", "reset-failed")
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to reset-failed systemd service, out: %s, err: %v", string(out), err)
}
return nil
}
func (s *ubiosSvc) Logger(errs chan<- error) (service.Logger, error) {
if service.Interactive() {
return service.ConsoleLogger, nil
}
return s.SystemLogger(errs)
}
func (s *ubiosSvc) SystemLogger(errs chan<- error) (service.Logger, error) {
return newSysLogger(s.Name, errs)
}
func (s *ubiosSvc) Run() (err error) {
err = s.i.Start(s)
if err != nil {
return err
}
if interactice, _ := isInteractive(); !interactice {
signal.Ignore(syscall.SIGHUP)
}
var sigChan = make(chan os.Signal, 3)
signal.Notify(sigChan, syscall.SIGTERM, os.Interrupt)
<-sigChan
return s.i.Stop(s)
}
func (s *ubiosSvc) Status() (service.Status, error) {
if _, err := os.Stat(s.configPath()); os.IsNotExist(err) {
return service.StatusUnknown, service.ErrNotInstalled
}
out, err := exec.Command(s.configPath(), "status").CombinedOutput()
if err != nil {
return service.StatusUnknown, err
}
switch string(bytes.TrimSpace(out)) {
case "Running":
return service.StatusRunning, nil
default:
return service.StatusStopped, nil
}
}
func (s *ubiosSvc) Start() error {
return exec.Command(s.configPath(), "start").Run()
}
func (s *ubiosSvc) Stop() error {
return exec.Command(s.configPath(), "stop").Run()
}
func (s *ubiosSvc) Restart() error {
err := s.Stop()
if err != nil {
return err
}
time.Sleep(50 * time.Millisecond)
return s.Start()
}
const ubiosBootSystemdService = `[Unit]
Description=Run ctrld On Startup UDM
Wants=network-online.target
After=network-online.target
StartLimitIntervalSec=500
StartLimitBurst=5
[Service]
Restart=on-failure
RestartSec=5s
ExecStart=/sbin/ssh-proxy '[ -f "{{.DnsMasqConfPath}}" ] || {{.Path}}{{range .Arguments}} {{.|cmd}}{{end}}'
RemainAfterExit=true
[Install]
WantedBy=multi-user.target
`
const ubiosSvcScript = `#!/bin/sh
# For RedHat and cousins:
# chkconfig: - 99 01
# description: {{.Description}}
# processname: {{.Path}}
### BEGIN INIT INFO
# Provides: {{.Path}}
# Required-Start:
# Required-Stop:
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: {{.DisplayName}}
# Description: {{.Description}}
### END INIT INFO
cmd="{{.Path}}{{range .Arguments}} {{.|cmd}}{{end}}"
name=$(basename $(readlink -f $0))
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" ] && cat /proc/$(get_pid)/stat > /dev/null 2>&1
}
case "$1" in
start)
if is_running; then
echo "Already started"
else
echo "Starting $name"
{{if .WorkingDirectory}}cd '{{.WorkingDirectory}}'{{end}}
$cmd >> "$stdout_log" 2>> "$stderr_log" &
echo $! > "$pid_file"
if ! is_running; then
echo "Unable to start, see $stdout_log and $stderr_log"
exit 1
fi
fi
;;
stop)
if is_running; then
echo -n "Stopping $name.."
kill $(get_pid)
for i in $(seq 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 tf = map[string]interface{}{
"cmd": func(s string) string {
return `"` + strings.Replace(s, `"`, `\"`, -1) + `"`
},
"cmdEscape": func(s string) string {
return strings.Replace(s, " ", `\x20`, -1)
},
}
+55
View File
@@ -0,0 +1,55 @@
package router
import (
"fmt"
"os"
"os/exec"
)
const (
synologyDNSMasqConfigPath = "/etc/dhcpd/dhcpd-zzz-ctrld.conf"
synologyDhcpdInfoPath = "/etc/dhcpd/dhcpd-zzz-ctrld.info"
)
func setupSynology() error {
dnsMasqConfigContent, err := dnsMasqConf()
if err != nil {
return err
}
if err := os.WriteFile(synologyDNSMasqConfigPath, []byte(dnsMasqConfigContent), 0600); err != nil {
return err
}
if err := os.WriteFile(synologyDhcpdInfoPath, []byte(`enable="yes"`), 0600); err != nil {
return err
}
if err := restartDNSMasq(); err != nil {
return err
}
return nil
}
func cleanupSynology() error {
// Remove the custom config files.
for _, f := range []string{synologyDNSMasqConfigPath, synologyDhcpdInfoPath} {
if err := os.Remove(f); err != nil {
return err
}
}
// Restart dnsmasq service.
if err := restartDNSMasq(); err != nil {
return err
}
return nil
}
func postInstallSynology() error {
return nil
}
func synologyRestartDNSMasq() error {
if out, err := exec.Command("/etc/rc.network", "nat-restart-dhcp").CombinedOutput(); err != nil {
return fmt.Errorf("synologyRestartDNSMasq: %s - %w", string(out), err)
}
return nil
}
+49
View File
@@ -0,0 +1,49 @@
//go:build linux || darwin || freebsd
package router
import (
"fmt"
"log/syslog"
"github.com/kardianos/service"
)
func newSysLogger(name string, errs chan<- error) (service.Logger, error) {
w, err := syslog.New(syslog.LOG_INFO, name)
if err != nil {
return nil, err
}
return sysLogger{w, errs}, nil
}
type sysLogger struct {
*syslog.Writer
errs chan<- error
}
func (s sysLogger) send(err error) error {
if err != nil && s.errs != nil {
s.errs <- err
}
return err
}
func (s sysLogger) Error(v ...interface{}) error {
return s.send(s.Writer.Err(fmt.Sprint(v...)))
}
func (s sysLogger) Warning(v ...interface{}) error {
return s.send(s.Writer.Warning(fmt.Sprint(v...)))
}
func (s sysLogger) Info(v ...interface{}) error {
return s.send(s.Writer.Info(fmt.Sprint(v...)))
}
func (s sysLogger) Errorf(format string, a ...interface{}) error {
return s.send(s.Writer.Err(fmt.Sprintf(format, a...)))
}
func (s sysLogger) Warningf(format string, a ...interface{}) error {
return s.send(s.Writer.Warning(fmt.Sprintf(format, a...)))
}
func (s sysLogger) Infof(format string, a ...interface{}) error {
return s.send(s.Writer.Info(fmt.Sprintf(format, a...)))
}
+7
View File
@@ -0,0 +1,7 @@
package router
import "github.com/kardianos/service"
func newSysLogger(name string, errs chan<- error) (service.Logger, error) {
return service.ConsoleLogger, nil
}
+82
View File
@@ -0,0 +1,82 @@
package router
import (
"fmt"
"os/exec"
)
const (
tomatoDnsCryptProxySvcName = "dnscrypt-proxy"
tomatoStubbySvcName = "stubby"
tomatoDNSMasqSvcName = "dnsmasq"
)
func setupTomato() error {
// Already setup.
if val, _ := nvram("get", nvramCtrldSetupKey); val == "1" {
return nil
}
data, err := dnsMasqConf()
if err != nil {
return err
}
nvramKvMap := nvramSetupKV()
nvramKvMap["dnsmasq_custom"] = data
if err := nvramSetKV(nvramKvMap, nvramCtrldSetupKey); err != nil {
return err
}
// Restart dnscrypt-proxy service.
if err := tomatoRestartServiceWithKill(tomatoDnsCryptProxySvcName, true); err != nil {
return err
}
// Restart stubby service.
if err := tomatoRestartService(tomatoStubbySvcName); err != nil {
return err
}
// Restart dnsmasq service.
if err := restartDNSMasq(); err != nil {
return err
}
return nil
}
func postInstallTomato() error {
return nil
}
func cleanupTomato() error {
// Restore old configs.
if err := nvramRestore(nvramSetupKV(), nvramCtrldSetupKey); err != nil {
return err
}
// Restart dnscrypt-proxy service.
if err := tomatoRestartServiceWithKill(tomatoDnsCryptProxySvcName, true); err != nil {
return err
}
// Restart stubby service.
if err := tomatoRestartService(tomatoStubbySvcName); err != nil {
return err
}
// Restart dnsmasq service.
if err := restartDNSMasq(); err != nil {
return err
}
return nil
}
func tomatoRestartService(name string) error {
return tomatoRestartServiceWithKill(name, false)
}
func tomatoRestartServiceWithKill(name string, killBeforeRestart bool) error {
if killBeforeRestart {
_, _ = exec.Command("killall", name).CombinedOutput()
}
if out, err := exec.Command("service", name, "restart").CombinedOutput(); err != nil {
return fmt.Errorf("service restart %s: %s, %w", name, string(out), err)
}
return nil
}
+73
View File
@@ -0,0 +1,73 @@
package router
import (
"bytes"
"fmt"
"os"
"strconv"
)
var errContentFilteringEnabled = fmt.Errorf(`the "Content Filtering" feature" is enabled, which is conflicted with ctrld.\n
To disable it, folowing instruction here: %s`, toggleContentFilteringLink)
const (
ubiosDNSMasqConfigPath = "/run/dnsmasq.conf.d/zzzctrld.conf"
toggleContentFilteringLink = "https://community.ui.com/questions/UDM-Pro-disable-enable-DNS-filtering/e2cc4060-e56a-4139-b200-62d7f773ff8f"
)
func setupUbiOS() error {
// Disable dnsmasq as DNS server.
dnsMasqConfigContent, err := dnsMasqConf()
if err != nil {
return err
}
if err := os.WriteFile(ubiosDNSMasqConfigPath, []byte(dnsMasqConfigContent), 0600); err != nil {
return err
}
// Restart dnsmasq service.
if err := restartDNSMasq(); err != nil {
return err
}
return nil
}
func cleanupUbiOS() error {
// Remove the custom dnsmasq config
if err := os.Remove(ubiosDNSMasqConfigPath); err != nil {
return err
}
// Restart dnsmasq service.
if err := restartDNSMasq(); err != nil {
return err
}
return nil
}
func postInstallUbiOS() error {
// See comment in postInstallEdgeOS.
if contentFilteringEnabled() {
return errContentFilteringEnabled
}
return nil
}
func ubiosRestartDNSMasq() error {
buf, err := os.ReadFile("/run/dnsmasq.pid")
if err != nil {
return err
}
pid, err := strconv.ParseUint(string(bytes.TrimSpace(buf)), 10, 64)
if err != nil {
return err
}
proc, err := os.FindProcess(int(pid))
if err != nil {
return err
}
return proc.Kill()
}
func contentFilteringEnabled() bool {
st, err := os.Stat("/run/dnsfilter/dnsfilter")
return err == nil && !st.IsDir()
}
+29
View File
@@ -0,0 +1,29 @@
package ctrld
import "net"
type dnsFn func() []string
func nameservers() []string {
var dns []string
seen := make(map[string]bool)
ch := make(chan []string)
fns := dnsFns()
for _, fn := range fns {
go func(fn dnsFn) {
ch <- fn()
}(fn)
}
for range fns {
for _, ns := range <-ch {
if seen[ns] {
continue
}
seen[ns] = true
dns = append(dns, net.JoinHostPort(ns, "53"))
}
}
return dns
}
+75
View File
@@ -0,0 +1,75 @@
//go:build darwin || dragonfly || freebsd || netbsd || openbsd
package ctrld
import (
"net"
"os/exec"
"runtime"
"strings"
"syscall"
"golang.org/x/net/route"
)
func dnsFns() []dnsFn {
return []dnsFn{dnsFromRIB, dnsFromIPConfig}
}
func dnsFromRIB() []string {
var dns []string
rib, err := route.FetchRIB(syscall.AF_UNSPEC, route.RIBTypeRoute, 0)
if err != nil {
return nil
}
messages, err := route.ParseRIB(route.RIBTypeRoute, rib)
if err != nil {
return nil
}
for _, message := range messages {
message, ok := message.(*route.RouteMessage)
if !ok {
continue
}
addresses := message.Addrs
if len(addresses) < 2 {
continue
}
dst, gw := toNetIP(addresses[0]), toNetIP(addresses[1])
if dst == nil || gw == nil {
continue
}
if gw.IsLoopback() {
continue
}
if dst.Equal(net.IPv4zero) || dst.Equal(net.IPv6zero) {
dns = append(dns, gw.String())
}
}
return dns
}
func dnsFromIPConfig() []string {
if runtime.GOOS != "darwin" {
return nil
}
cmd := exec.Command("ipconfig", "getoption", "", "domain_name_server")
out, _ := cmd.Output()
if ip := net.ParseIP(strings.TrimSpace(string(out))); ip != nil {
return []string{ip.String()}
}
return nil
}
func toNetIP(addr route.Addr) net.IP {
switch t := addr.(type) {
case *route.Inet4Addr:
return net.IPv4(t.IP[0], t.IP[1], t.IP[2], t.IP[3])
case *route.Inet6Addr:
ip := make(net.IP, net.IPv6len)
copy(ip, t.IP[:])
return ip
default:
return nil
}
}
+97
View File
@@ -0,0 +1,97 @@
package ctrld
import (
"bufio"
"bytes"
"encoding/hex"
"net"
"os"
"github.com/Control-D-Inc/ctrld/internal/dns/resolvconffile"
)
const (
v4RouteFile = "/proc/net/route"
v6RouteFile = "/proc/net/ipv6_route"
)
func dnsFns() []dnsFn {
return []dnsFn{dns4, dns6, dnsFromSystemdResolver}
}
func dns4() []string {
f, err := os.Open(v4RouteFile)
if err != nil {
return nil
}
defer f.Close()
var dns []string
seen := make(map[string]bool)
s := bufio.NewScanner(f)
first := true
for s.Scan() {
if first {
first = false
continue
}
fields := bytes.Fields(s.Bytes())
if len(fields) < 2 {
continue
}
gw := make([]byte, net.IPv4len)
// Third fields is gateway.
if _, err := hex.Decode(gw, fields[2]); err != nil {
continue
}
ip := net.IPv4(gw[3], gw[2], gw[1], gw[0])
if ip.Equal(net.IPv4zero) || seen[ip.String()] {
continue
}
seen[ip.String()] = true
dns = append(dns, ip.String())
}
return dns
}
func dns6() []string {
f, err := os.Open(v6RouteFile)
if err != nil {
return nil
}
defer f.Close()
var dns []string
s := bufio.NewScanner(f)
for s.Scan() {
fields := bytes.Fields(s.Bytes())
if len(fields) < 4 {
continue
}
gw := make([]byte, net.IPv6len)
// Fifth fields is gateway.
if _, err := hex.Decode(gw, fields[4]); err != nil {
continue
}
ip := net.IP(gw)
if ip.Equal(net.IPv6zero) {
continue
}
dns = append(dns, ip.String())
}
return dns
}
func dnsFromSystemdResolver() []string {
c, err := resolvconffile.ParseFile("/run/systemd/resolve/resolv.conf")
if err != nil {
return nil
}
ns := make([]string, 0, len(c.Nameservers))
for _, nameserver := range c.Nameservers {
ns = append(ns, nameserver.String())
}
return ns
}
+11
View File
@@ -0,0 +1,11 @@
package ctrld
import "testing"
func TestNameservers(t *testing.T) {
ns := nameservers()
if len(ns) == 0 {
t.Fatal("failed to get nameservers")
}
t.Log(ns)
}
-11
View File
@@ -1,11 +0,0 @@
//go:build !js && !windows
package ctrld
import (
"github.com/Control-D-Inc/ctrld/internal/resolvconffile"
)
func nameservers() []string {
return resolvconffile.NameServersWithPort()
}
+43 -54
View File
@@ -2,70 +2,59 @@ package ctrld
import ( import (
"net" "net"
"os"
"syscall" "syscall"
"unsafe"
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
"golang.org/x/sys/windows" "golang.org/x/sys/windows"
) )
func nameservers() []string { func dnsFns() []dnsFn {
aas, err := adapterAddresses() return []dnsFn{dnsFromAdapter}
}
func dnsFromAdapter() []string {
aas, err := winipcfg.GetAdaptersAddresses(syscall.AF_UNSPEC, winipcfg.GAAFlagIncludeGateways|winipcfg.GAAFlagIncludePrefix)
if err != nil { if err != nil {
return nil return nil
} }
ns := make([]string, 0, len(aas)) ns := make([]string, 0, len(aas)*2)
seen := make(map[string]bool)
do := func(addr windows.SocketAddress) {
sa, err := addr.Sockaddr.Sockaddr()
if err != nil {
return
}
var ip net.IP
switch sa := sa.(type) {
case *syscall.SockaddrInet4:
ip = net.IPv4(sa.Addr[0], sa.Addr[1], sa.Addr[2], sa.Addr[3])
case *syscall.SockaddrInet6:
ip = make(net.IP, net.IPv6len)
copy(ip, sa.Addr[:])
if ip[0] == 0xfe && ip[1] == 0xc0 {
// Ignore these fec0/10 ones. Windows seems to
// populate them as defaults on its misc rando
// interfaces.
return
}
default:
return
}
if ip.IsLoopback() || seen[ip.String()] {
return
}
seen[ip.String()] = true
ns = append(ns, ip.String())
}
for _, aa := range aas { for _, aa := range aas {
for dns := aa.FirstDnsServerAddress; dns != nil; dns = dns.Next { for dns := aa.FirstDNSServerAddress; dns != nil; dns = dns.Next {
sa, err := dns.Address.Sockaddr.Sockaddr() do(dns.Address)
if err != nil { }
continue for gw := aa.FirstGatewayAddress; gw != nil; gw = gw.Next {
} do(gw.Address)
var ip net.IP
switch sa := sa.(type) {
case *syscall.SockaddrInet4:
ip = net.IPv4(sa.Addr[0], sa.Addr[1], sa.Addr[2], sa.Addr[3])
case *syscall.SockaddrInet6:
ip = make(net.IP, net.IPv6len)
copy(ip, sa.Addr[:])
if ip[0] == 0xfe && ip[1] == 0xc0 {
// Ignore these fec0/10 ones. Windows seems to
// populate them as defaults on its misc rando
// interfaces.
continue
}
default:
// Unexpected type.
continue
}
ns = append(ns, net.JoinHostPort(ip.String(), "53"))
} }
} }
return ns return ns
} }
func adapterAddresses() ([]*windows.IpAdapterAddresses, error) {
var b []byte
l := uint32(15000) // recommended initial size
for {
b = make([]byte, l)
err := windows.GetAdaptersAddresses(syscall.AF_UNSPEC, windows.GAA_FLAG_INCLUDE_PREFIX, 0, (*windows.IpAdapterAddresses)(unsafe.Pointer(&b[0])), &l)
if err == nil {
if l == 0 {
return nil, nil
}
break
}
if err.(syscall.Errno) != syscall.ERROR_BUFFER_OVERFLOW {
return nil, os.NewSyscallError("getadaptersaddresses", err)
}
if l <= uint32(len(b)) {
return nil, os.NewSyscallError("getadaptersaddresses", err)
}
}
var aas []*windows.IpAdapterAddresses
for aa := (*windows.IpAdapterAddresses)(unsafe.Pointer(&b[0])); aa != nil; aa = aa.Next {
aas = append(aas, aa)
}
return aas, nil
}
+46
View File
@@ -0,0 +1,46 @@
package ctrld
import (
"context"
"errors"
"sync"
"sync/atomic"
"time"
"tailscale.com/logtail/backoff"
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
)
var (
hasIPv6Once sync.Once
ipv6Available atomic.Bool
)
func hasIPv6() bool {
hasIPv6Once.Do(func() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
val := ctrldnet.IPv6Available(ctx)
ipv6Available.Store(val)
go probingIPv6(val)
})
return ipv6Available.Load()
}
// TODO(cuonglm): doing poll check natively for supported platforms.
func probingIPv6(old bool) {
b := backoff.NewBackoff("probingIPv6", func(format string, args ...any) {}, 30*time.Second)
bCtx := context.Background()
for {
func() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
cur := ctrldnet.IPv6Available(ctx)
if ipv6Available.CompareAndSwap(old, cur) {
old = cur
}
}()
b.BackOff(bCtx, errors.New("no change"))
}
}
+144 -18
View File
@@ -5,17 +5,24 @@ import (
"errors" "errors"
"fmt" "fmt"
"net" "net"
"sync/atomic" "sync"
"time"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
const ( const (
ResolverTypeDOH = "doh" // ResolverTypeDOH specifies DoH resolver.
ResolverTypeDOH3 = "doh3" ResolverTypeDOH = "doh"
ResolverTypeDOT = "dot" // ResolverTypeDOH3 specifies DoH3 resolver.
ResolverTypeDOQ = "doq" ResolverTypeDOH3 = "doh3"
ResolverTypeOS = "os" // ResolverTypeDOT specifies DoT resolver.
ResolverTypeDOT = "dot"
// ResolverTypeDOQ specifies DoQ resolver.
ResolverTypeDOQ = "doq"
// ResolverTypeOS specifies OS resolver.
ResolverTypeOS = "os"
// ResolverTypeLegacy specifies legacy resolver.
ResolverTypeLegacy = "legacy" ResolverTypeLegacy = "legacy"
) )
@@ -33,7 +40,7 @@ var errUnknownResolver = errors.New("unknown resolver")
// NewResolver creates a Resolver based on the given upstream config. // NewResolver creates a Resolver based on the given upstream config.
func NewResolver(uc *UpstreamConfig) (Resolver, error) { func NewResolver(uc *UpstreamConfig) (Resolver, error) {
typ, endpoint := uc.Type, uc.Endpoint typ := uc.Type
switch typ { switch typ {
case ResolverTypeDOH, ResolverTypeDOH3: case ResolverTypeDOH, ResolverTypeDOH3:
return newDohResolver(uc), nil return newDohResolver(uc), nil
@@ -44,29 +51,56 @@ func NewResolver(uc *UpstreamConfig) (Resolver, error) {
case ResolverTypeOS: case ResolverTypeOS:
return or, nil return or, nil
case ResolverTypeLegacy: case ResolverTypeLegacy:
return &legacyResolver{endpoint: endpoint}, nil return &legacyResolver{uc: uc}, nil
} }
return nil, fmt.Errorf("%w: %s", errUnknownResolver, typ) return nil, fmt.Errorf("%w: %s", errUnknownResolver, typ)
} }
type osResolver struct { type osResolver struct {
nameservers []string nameservers []string
next atomic.Uint32 }
type osResolverResult struct {
answer *dns.Msg
err error
} }
// Resolve performs DNS resolvers using OS default nameservers. Nameserver is chosen from // Resolve performs DNS resolvers using OS default nameservers. Nameserver is chosen from
// available nameservers with a roundrobin algorithm. // available nameservers with a roundrobin algorithm.
func (o *osResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) { func (o *osResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) {
numServers := uint32(len(o.nameservers)) numServers := len(o.nameservers)
if numServers == 0 { if numServers == 0 {
return nil, errors.New("no nameservers available") return nil, errors.New("no nameservers available")
} }
next := o.next.Add(1) ctx, cancel := context.WithCancel(ctx)
server := o.nameservers[(next-1)%numServers] defer cancel()
dnsClient := &dns.Client{Net: "udp"}
answer, _, err := dnsClient.ExchangeContext(ctx, msg, server)
return answer, err dnsClient := &dns.Client{Net: "udp"}
ch := make(chan *osResolverResult, numServers)
var wg sync.WaitGroup
wg.Add(len(o.nameservers))
go func() {
wg.Wait()
close(ch)
}()
for _, server := range o.nameservers {
go func(server string) {
defer wg.Done()
answer, _, err := dnsClient.ExchangeContext(ctx, msg.Copy(), server)
ch <- &osResolverResult{answer: answer, err: err}
}(server)
}
errs := make([]error, 0, numServers)
for res := range ch {
if res.err == nil {
cancel()
return res.answer, res.err
}
errs = append(errs, res.err)
}
return nil, errors.Join(errs...)
} }
func newDialer(dnsAddress string) *net.Dialer { func newDialer(dnsAddress string) *net.Dialer {
@@ -82,16 +116,108 @@ func newDialer(dnsAddress string) *net.Dialer {
} }
type legacyResolver struct { type legacyResolver struct {
endpoint string uc *UpstreamConfig
} }
func (r *legacyResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) { func (r *legacyResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) {
// See comment in (*dotResolver).resolve method. // See comment in (*dotResolver).resolve method.
dialer := newDialer(net.JoinHostPort(bootstrapDNS, "53")) dialer := newDialer(net.JoinHostPort(bootstrapDNS, "53"))
dnsTyp := uint16(0)
if msg != nil && len(msg.Question) > 0 {
dnsTyp = msg.Question[0].Qtype
}
_, udpNet := r.uc.netForDNSType(dnsTyp)
dnsClient := &dns.Client{ dnsClient := &dns.Client{
Net: "udp", Net: udpNet,
Dialer: dialer, Dialer: dialer,
} }
answer, _, err := dnsClient.ExchangeContext(ctx, msg, r.endpoint) endpoint := r.uc.Endpoint
if r.uc.BootstrapIP != "" {
dnsClient.Net = "udp"
_, port, _ := net.SplitHostPort(endpoint)
endpoint = net.JoinHostPort(r.uc.BootstrapIP, port)
}
answer, _, err := dnsClient.ExchangeContext(ctx, msg, endpoint)
return answer, err return answer, err
} }
// LookupIP looks up host using OS resolver.
// It returns a slice of that host's IPv4 and IPv6 addresses.
func LookupIP(domain string) []string {
return lookupIP(domain, -1, true)
}
func lookupIP(domain string, timeout int, withBootstrapDNS bool) (ips []string) {
resolver := &osResolver{nameservers: nameservers()}
if withBootstrapDNS {
resolver.nameservers = append([]string{net.JoinHostPort(bootstrapDNS, "53")}, resolver.nameservers...)
}
ProxyLog.Debug().Msgf("Resolving %q using bootstrap DNS %q", domain, resolver.nameservers)
timeoutMs := 2000
if timeout > 0 && timeout < timeoutMs {
timeoutMs = timeout
}
questionDomain := dns.Fqdn(domain)
ipFromRecord := func(record dns.RR) string {
switch ar := record.(type) {
case *dns.A:
if ar.Hdr.Name != questionDomain {
return ""
}
return ar.A.String()
case *dns.AAAA:
if ar.Hdr.Name != questionDomain {
return ""
}
return ar.AAAA.String()
}
return ""
}
lookup := func(dnsType uint16) {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutMs)*time.Millisecond)
defer cancel()
m := new(dns.Msg)
m.SetQuestion(questionDomain, dnsType)
m.RecursionDesired = true
r, err := resolver.Resolve(ctx, m)
if err != nil {
ProxyLog.Error().Err(err).Msgf("could not lookup %q record for domain %q", dns.TypeToString[dnsType], domain)
return
}
if r.Rcode != dns.RcodeSuccess {
ProxyLog.Error().Msgf("could not resolve domain %q, return code: %s", domain, dns.RcodeToString[r.Rcode])
return
}
if len(r.Answer) == 0 {
ProxyLog.Error().Msg("no answer from OS resolver")
return
}
for _, a := range r.Answer {
if ip := ipFromRecord(a); ip != "" {
ips = append(ips, ip)
}
}
}
// Find all A, AAAA records of the domain.
for _, dnsType := range []uint16{dns.TypeAAAA, dns.TypeA} {
lookup(dnsType)
}
return ips
}
// NewBootstrapResolver returns an OS resolver, which use following nameservers:
//
// - ControlD bootstrap DNS server.
// - Gateway IP address (depends on OS).
// - Input servers.
func NewBootstrapResolver(servers ...string) Resolver {
resolver := &osResolver{nameservers: nameservers()}
resolver.nameservers = append([]string{net.JoinHostPort(bootstrapDNS, "53")}, resolver.nameservers...)
for _, ns := range servers {
resolver.nameservers = append([]string{net.JoinHostPort(ns, "53")}, resolver.nameservers...)
}
return resolver
}
+53
View File
@@ -0,0 +1,53 @@
package ctrld
import (
"context"
"testing"
"time"
"github.com/miekg/dns"
)
func Test_osResolver_Resolve(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
defer cancel()
resolver := &osResolver{nameservers: []string{"127.0.0.127:5353"}}
m := new(dns.Msg)
m.SetQuestion("controld.com.", dns.TypeA)
m.RecursionDesired = true
_, _ = resolver.Resolve(context.Background(), m)
}()
select {
case <-time.After(10 * time.Second):
t.Error("os resolver hangs")
case <-ctx.Done():
}
}
func Test_upstreamTypeFromEndpoint(t *testing.T) {
tests := []struct {
name string
endpoint string
resolverType string
}{
{"doh", "https://freedns.controld.com/p2", ResolverTypeDOH},
{"doq", "quic://p2.freedns.controld.com", ResolverTypeDOQ},
{"dot", "p2.freedns.controld.com", ResolverTypeDOT},
{"legacy", "8.8.8.8:53", ResolverTypeLegacy},
{"legacy ipv6", "[2404:6800:4005:809::200e]:53", ResolverTypeLegacy},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if rt := ResolverTypeFromEndpoint(tc.endpoint); rt != tc.resolverType {
t.Errorf("mismatch, want: %s, got: %s", tc.resolverType, rt)
}
})
}
}
Executable
+27
View File
@@ -0,0 +1,27 @@
#!/bin/sh
set -ex
binary=$1
if [ -z "$binary" ]; then
echo >&2 "Usage: $0 <binary>"
exit 1
fi
case "$binary" in
*_freebsd_*)
echo >&2 "upx does not work with freebsd binary yet"
exit 0
;;
*_windows_arm*)
echo >&2 "upx does not work with windows arm/arm64 binary yet"
exit 0
;;
*_darwin_*)
echo >&2 "upx claims to work with darwin binary, but testing show that it is broken"
exit 0
;;
esac
upx -- "$binary"
+7
View File
@@ -50,6 +50,13 @@ type = "legacy"
endpoint = "8.8.8.8" endpoint = "8.8.8.8"
timeout = 5 timeout = 5
[upstream.3]
name = "DOH with client info"
type = "doh"
endpoint = "https://dns.controld.com/client_info_upstream/main-device"
timeout = 5
send_client_info = false
[listener.0] [listener.0]
ip = "127.0.0.1" ip = "127.0.0.1"
port = 53 port = 53