Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Get IP from a specific ethernet interface #713

Closed
favonia opened this issue Mar 14, 2024 · 23 comments · Fixed by #941
Closed

Get IP from a specific ethernet interface #713

favonia opened this issue Mar 14, 2024 · 23 comments · Fixed by #941
Labels
enhancement New feature or request
Milestone

Comments

@favonia
Copy link
Owner

favonia commented Mar 14, 2024

Inspired by TimothyYe/godns#238: maybe there are still cases where manual specification of the interface makes sense.

@favonia favonia added enhancement New feature or request design? The next step is to reflect upon the information and come up with a good design labels Mar 14, 2024
@favonia favonia added this to the 1.13.0 milestone Jun 29, 2024
@favonia favonia modified the milestones: 1.13.0, 1.14.0, far future Jul 16, 2024
@favonia favonia added the blocked This issue or pull request is blocked by something outside the authors' control label Jul 19, 2024
@favonia
Copy link
Owner Author

favonia commented Jul 19, 2024

The Go standard library does not provide an easy way to specify the interface for the current detection algorithm. 🙃

@tchar
Copy link

tchar commented Sep 3, 2024

@favonia Hello. I am interested in specifying a custom interface.

I have little experience with go, but shouldn't something like this https://gist.github.com/schwarzeni/f25031a3123f895ff3785970921e962c work? I tried it and it looks like it works (I have not tried it on Windows).

What I ended up doing is run cloudflare-ddns in a bridged network and create a small function to get the interface on the host machine. I spinned up a server listening on a docker IP like 172.xx.0.1:9000 and then specify the provider as url like url:http://172.xx.0.1:9000/ip

I came across a python version here https://stackoverflow.com/a/24196955. One can use flask to serve. There is also a node version here https://stackoverflow.com/a/8440736 which can be served with something like express.

It is a bit dirty but this should work even with custom commands to get an IP that is not directly related to an interface.

@favonia
Copy link
Owner Author

favonia commented Sep 3, 2024

What I ended up doing is run cloudflare-ddns in a bridged network and create a small function to get the interface on the host machine. I spinned up a server listening on a docker IP like 172.xx.0.1:9000 and then specify the provider as url like url:http://172.xx.0.1:9000/ip

This is way too complicated... you deserve a much better solution!!!

The code you showed will choose the first IP of the specified interface in the expected IP family, while the current local provider will use one of the IPs (if more than one) for connecting to the internet. Ideally, the implementation will inspect the routing table etc. to find out the path to the internet via a specific interface. Ideally, there should be a way to hack the standard library to force the interface while trying to connect to the internet. It's just that I don't know how to do it yet. 🙃

@favonia favonia added help wanted Extra attention is needed and removed blocked This issue or pull request is blocked by something outside the authors' control labels Sep 10, 2024
@favonia
Copy link
Owner Author

favonia commented Sep 13, 2024

@tchar Hi, I think more about this and feel the design is a bit unclear to me. So I'd like to just ask you directly: if you specify an interface and it magically has many seemingly valid IP addresses, what do you want the updater to do?

@tchar
Copy link

tchar commented Sep 13, 2024

Hey @favonia indeed it seems like you would have to compromise, although I'm curious how often does something like this is the case.

One possible solution is to allow the user to select family (ipv4 vs ipv6) and index (0, 1, ...).

Now the family is clear, but the question is: Is it possible for the code to run two consecutive times and give a different order for the IPs?

If the answer is no, or unlikely, you could do it like in the following scenario.

Say machine has three IPs on interface eth1:
10.0.0.2 and 10.0.0.3 and some ipv6.

Specifying something like

local:ipv4:eth1/0 would give 10.0.0.2, and
local:ipv4:eth1/1 would give 10.0.0.3
local:ipv4:eth1/2 would give empty
local:eth1/2 would give the ipv6

@favonia
Copy link
Owner Author

favonia commented Sep 13, 2024

@tchar The ipv4/6 would not be needed because provider is set separately for IP4/6_PROVIDER 😉. BTW, by any change, are you using url:http://172.xx.0.1:9000/ip to transfer an IPv6 address? If so, it might stop working in the upcoming version...

@tchar
Copy link

tchar commented Sep 13, 2024

@tchar The ipv4/6 would not be needed because provider is set separately for IP4/6_PROVIDER 😉. BTW, by any change, are you using url:http://172.xx.0.1:9000/ip to transfer an IPv6 address? If so, it might stop working in the upcoming version...

@favonia Yes, I am using this, but for ipv4, not ipv6.

If you would put the correct number to xx I would be concerned haha. You nailed the port and almost the path.

@favonia
Copy link
Owner Author

favonia commented Sep 13, 2024

Well I'm only using the public information you provided in the issue. @tchar

@favonia
Copy link
Owner Author

favonia commented Sep 21, 2024

@tchar Alright, I did some more homework, and have some good and bad news.

  • The bad news is that not being able to specify the interface directly is a known weakness of the POSIX socket API. The Go standard library cannot do much here. There's no way to say "use eth0 to connect to 1.1.1.1". (The only exception is IPv6 link-local addresses, but let's ignore them for now.)
  • The semi-good news is that even the Linux kernel seems to be choosing the "first" IP address that "makes senses" if existing hints did not sufficiently ping down the address. So, "choosing the first one" is expected. There are many criteria we could use to determine whether an address is "reasonable":
    • Obviously, for IPv4 you ignore IPv6 addresses and vice versa. (However, should IPv4-mapped IPv6 addresses treated as IPv4 ones or just rejected?)
    • Next, we probably want to skip IPv6 link-local addresses because an IPv6 interface would likely have one.
    • Maybe we should also ignore multicast IP addresses.
    • IPv4-mapped IPv6 addresses would be suspicious...

May I have your confirmation that your address will be some global unicast (public) IPv4 address? (That is, would the most conservative implementation that rejects all "weird" addresses work for you?) Also, may I learn more about your network setup, especially why the current automatic selection fails?

If you are not sure whether a particular address is global unicast, you can change the ip = ... line in the following GO program and run it: https://go.dev/play/p/fR2wL6aKDAI

@tchar
Copy link

tchar commented Sep 21, 2024

@favonia I am not sure what is considered to be a weird address. Unless you mean local and reserved addresses.

In my case I am using a VPN tunnel for VLAN.

The IPv4 is a valid one, but any service can be accessed only from those in the VPN VLAN network.

I hope this makes more sense.

@favonia
Copy link
Owner Author

favonia commented Sep 21, 2024

@tchar Thanks. I have three questions:

  1. Are your private IPs within the standard ranges? If so, they are okay.
    • 10.0.0.0/8
    • 172.16.0.0/12
    • 192.168.0.0/16
  2. I suppose using the VPN is the reason why connecting to some public website (e.g., 1.1.1.1) does not work, right? That is, the current detection code for "local" would give you a non-VPN IP.
  3. If this is a VPN totally under your control, perhaps you could assign the same IP to your machine every time? (That is, maybe you do not need DDNS at all.)

@favonia favonia changed the title Specify the ethernet interface Specify the ethernet interface for the local provider Sep 21, 2024
@favonia favonia removed help wanted Extra attention is needed design? The next step is to reflect upon the information and come up with a good design labels Sep 21, 2024
@favonia favonia modified the milestones: far future, 1.15.0 Sep 21, 2024
@tchar
Copy link

tchar commented Sep 21, 2024

@favonia

Here is an example of imaginary interfaces to help you understand a bit how the system works.
eth0: Normal interface
tun0: Interface for VPN
tun1: Interface for VPN VLAN
docker0-xx: Docker interfaces

  1. The IPs I see do not belong in these ranges. I am not sure if they will ever be in these ranges, but I think not since the machine has to be discoverable outside of its private network.
  2. The thing is that the machine has a ton of interfaces. Some belong to docker, some belong to VPN tunnels (not the VPN VLAN), and there is one for the VPN VLAN. I am not sure what the local algorithm does, but since I cannot pick an interface, I am using the aforementioned solution. Keep in mind that when the machine wants to connect to the internet, eth0 or tun0 may be used, but not tun1, unless the traffic is intended for one of the other machines in the VPN VLAN.
  3. I cannot assign the same IP, and I am not sure it is the best practice to do so. If I could/would, I guess I would not use DDNS at all.

Let me know if you need any other information.

@favonia
Copy link
Owner Author

favonia commented Sep 21, 2024

@tchar Thank you! Is the (first) IP assigned to tun1 the IP you want to use to update DNS records? (I am just checking that the algorithm in my mind would work.)

@tchar
Copy link

tchar commented Sep 21, 2024

Yes, there is only one IPv4 under that interface.

@favonia
Copy link
Owner Author

favonia commented Sep 22, 2024

@tchar BTW, the Go code you mentioned earlier (which is based on the code on stackoverflow) is not correct. It's assuming that the Go standard library always returns something of type *net.IPNet for interface addresses. I just checked the source code and it could be *net.IPAddr on Darwin, FreeBSD, and Windows. So, yeah, if you try it on Windows I guess it would fail.

@favonia
Copy link
Owner Author

favonia commented Sep 23, 2024

@tchar You may try out the specify-iface branch (PR #941) the edge Docker tag with the IP provider local:tun1. I still need to fine-tune the messages and testing. I have two questions for you:

  1. Is the syntax local:tun1 "obvious, stupid, crystal clear without reading the manual" to you?
  2. If you have tried out the feature, does it work for you?

@favonia favonia reopened this Sep 24, 2024
@tchar
Copy link

tchar commented Sep 26, 2024

@favonia I tested it out, and it works. I only had to put it in host mode instead of bridge.

Regarding the syntax, it makes sense, so there is no problem. If I were you, I would prefix the interface with something like if. For example. local:if/tun1 since / is an invalid name for interfaces.

You could even have a simple mode (local:tun1) and a verbose mode (local:if/tun1) in case you want to reserve local for something more than interfaces in the future.

Alternatively, you could use a keyword that is different from local (like interface).

This is up to you; either works and makes sense.

@favonia
Copy link
Owner Author

favonia commented Sep 27, 2024

@tchar Wow, that's a great suggestion! I propose using

IP6_PROVIDER=local.iface:eth0

This leverages the existing cloudflare.doh and cloudflare.trace providers, though I admit the syntax might be a bit confusing: it reads more like local.(iface:eth0) instead of the intended (local.iface):eth0. I considered alternatives like local.if:eth0 and local.interface:eth0, but if might be misunderstood as a condition, and ironically, interface feels less clear than the abbreviation iface to me.

Once we establish local.iface, we could potentially introduce local.route:host and rebrand local as

IP6_PROVIDER=local.route:api.cloudflare.com

I wonder what you think about these changes? I am making the changes now, but it is easy to adjust the syntax before the release.

@favonia favonia changed the title Specify the ethernet interface for the local provider Get IP from a specific ethernet interface Oct 1, 2024
@favonia favonia modified the milestones: 1.15.0, 1.16.0 Oct 1, 2024
@favonia
Copy link
Owner Author

favonia commented Oct 1, 2024

@tchar I released 1.15.0 with the syntax local.iface:<iface>, but clearly documented that the syntax will not be finalized until 1.16.0. We have lots of time to pin down the design. 😁

@tchar
Copy link

tchar commented Oct 3, 2024

@favonia That's great. I will pin the version to 1.15.0 so I don't get any surprises for now.

Thanks

@favonia
Copy link
Owner Author

favonia commented Oct 3, 2024

@tchar Thanks. It sounds like you don't have any preference between local.iface:tun2 and local:iface/tun2? I'm very happy with the actual code, so the format is the only remaining issue 😉

@tchar
Copy link

tchar commented Oct 3, 2024

@favonia As a user, I find these formats identical in usability. As a dev, it's your project; you should decide which design is best for the project's life. You know it inside out.

Both formats are better than local: because they are more verbose. Other than that. I am happy.

If I were you, I would probably think about what other use cases the settings can have and see what the "key" would look like.

  1. <main>.<sub>:<value>
  2. <main>:<sub>/<value>

I feel like 1 (which is your current implementation feels better)

@favonia
Copy link
Owner Author

favonia commented Oct 4, 2024

Closing this as there's nothing to do for now.

@favonia favonia closed this as completed Oct 4, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants