mholt / caddy-dynamicdns

Caddy app that keeps your DNS records (A/AAAA) pointed at itself.
Apache License 2.0
250 stars 25 forks source link

[Feature Request] Get IP from Fritz!Box #37

Closed mietzen closed 1 year ago

mietzen commented 1 year ago

Hi,

I have a similiar Issue as https://github.com/mholt/caddy-dynamicdns/issues/31.

My setup looks like this: Fritz!Box -> VPN-GW -> Clients

To obtain the "ISP-IP" and not the "VPN-IP" I use DDClient with the following script: https://github.com/ddclient/ddclient/blob/master/sample-get-ip-from-fritzbox

Now I would like to do the same with caddy.

I generated some working go code with ChatGPT, but I'm not sure how to integrate it.

package main

import (
    "encoding/xml"
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    "strings"
)

type SoapEnvelope struct {
    XMLName xml.Name `xml:"Envelope"`
    Body    SoapBody
}

type SoapBody struct {
    XMLName               xml.Name `xml:"Body"`
    GetExternalIPAddressResponse GetExternalIPAddressResponse
}

type GetExternalIPAddressResponse struct {
    XMLName              xml.Name `xml:"GetExternalIPAddressResponse"`
    NewExternalIPAddress string   `xml:"NewExternalIPAddress"`
}

func getExternalIPAddress(fritzBoxHostname string) (string, error) {
    xml := `<?xml version="1.0" encoding="utf-8"?>
    <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
        <s:Body>
            <u:GetExternalIPAddress xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1" />
        </s:Body>
    </s:Envelope>`

    url := fmt.Sprintf("http://%s:49000/igdupnp/control/WANIPConn1", fritzBoxHostname)

    req, err := http.NewRequest("POST", url, strings.NewReader(xml))
    if err != nil {
        return "", err
    }

    req.Header.Set("Content-Type", "text/xml; charset=\"utf-8\"")
    req.Header.Set("SOAPAction", "urn:schemas-upnp-org:service:WANIPConnection:1#GetExternalIPAddress")

    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()

    bodyBytes, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return "", err
    }

    return string(bodyBytes), nil
}

func main() {
    fritzBoxHostname := "192.168.178.1" 

    respXml, err := getExternalIPAddress(fritzBoxHostname)
    if err != nil {
        log.Fatal(err)
    }

    var envelope SoapEnvelope
    err = xml.Unmarshal([]byte(respXml), &envelope)
    if err != nil {
        log.Fatal(err)
    }

    ip := envelope.Body.GetExternalIPAddressResponse.NewExternalIPAddress
    fmt.Println("External IP address:", ip)
}
francislavoie commented 1 year ago

You would implement a plugin like this: https://caddyserver.com/docs/extending-caddy

Take a look at the SimpleHTTP module: https://github.com/mholt/caddy-dynamicdns/blob/master/ipsource.go

Your plugin would look almost the same as SimpleHTTP except have a different module name.

francislavoie commented 1 year ago

To clarify, I don't think that support for this would be a fit for this project, it makes more sense as a separate package/repo. It's a very niche usecase (i.e. only if you own a specific piece of hardware from a specific provider).

mietzen commented 1 year ago

How would you feel about a option to obtain IPs from a external program, with e.g. exec.Command This would be a more generalist approach, which would cover all niche use cases.

francislavoie commented 1 year ago

Yeah, that could be interesting. WDYT @mholt

My concern with exec.Command generally is that it would allow a bad actor to escalate access (or just do bad things in general) if they manage to change your Caddy config. It is a bit of a paranoid concern, but it is a theoretical risk that wouldn't otherwise exist if the plugin didn't have support for running arbitrary commands.

mietzen commented 1 year ago

Yes of course, but wouldn't one have to have write access to either your caddy config (script path) or the script already? If we only give root write access to the script and the caddy user only read + execute permissions, I would say this should be "fine". Or am I missing something?

Edit: but yes if your caddy process runs as root and you do stupid stuff in your script, bad things will happen 😉

francislavoie commented 1 year ago

Since Caddy's admin API is accessible at localhost:2019 by default, "all that's needed" is the ability to make arbitrary HTTP requests from the server to itself (POST with a payload to update the config, GET with output to potentially get secrets from the config), then the config can be changed to escalate. Also root isn't necessary to do bad things, e.g. an attacker could exfiltrate your cert's private key to impersonate your domain.

Either way, yes this is paranoid and it does require other things to go wrong before it's a real problem, but it's still a vector for escalation that users need to be aware of.

mietzen commented 1 year ago

I tried my best to Implement it, but I only played a little bit with a over a year ago (Hence the draft PR). How do build and test this again.

francislavoie commented 1 year ago
xcaddy build --with github.com/mholt/caddy-dynamicdns=github.com/mietzen/caddy-dynamicdns@master
mholt commented 1 year ago

Thanks for opening an issue!

I think a module to get the IP address from a command is a decent idea, but until we get a sense of how secure people's environments are I think I still want to keep it an external/separate plugin.

mietzen commented 1 year ago

I made a separate module: https://github.com/mietzen/caddy-dynamicdns-cmd-source

mholt commented 1 year ago

Awesome. Thanks for understanding -- we look forward to it being used!

Feel free to add it to the Caddy website if you haven't already (just log in to your account and register the package).