mirror of
https://github.com/mudler/LocalAI.git
synced 2026-10-28 17:18:49 -04:00
* test(cli): use a globally routable IPv6 address in the public bind spec 2001:db8::/32 is the IPv6 documentation prefix and is now classed as non-public, so the bind is trusted and the spec failed. Use a public address from 2606:4700:4700::/48 instead. * fix(addons): rename installFlow.js so it no longer clashes with InstallFlow.jsx installFlow.js and InstallFlow.jsx differ only by case. On a case-insensitive filesystem the bundler can resolve `import FlowStatus from './InstallFlow'` to the .js module, which has no default export, and the macOS build fails with MISSING_EXPORT. Rename the module to installAndTurnOn.js, after its main export, and update its two importers. No code changes. Assisted-by: Claude Code:claude-sonnet-5-5 * fix(addons): keep Reload on the page of an add-on that is turned off The clearer empty states for a stopped add-on dropped the Reload button from the unavailable page, so a user who turned the add-on back on had no way to mount it again without leaving the page. Render the button in the actions of the off state, next to Turn on. * test(ui): serve the Voice add-on in the diarization profiles spec The remembered voices link now opens the page of the Voice add-on, which the core build no longer draws. Install the built Voice bundle next to Studio, as the other specs do for their add-ons, and build it in the global setup. --------- Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
155 lines
5.8 KiB
Go
155 lines
5.8 KiB
Go
package cli
|
|
|
|
import (
|
|
"errors"
|
|
"net"
|
|
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
// withInterfaceAddrs swaps interfaceAddrsFn for the duration of one spec.
|
|
// Ginkgo's DeferCleanup restores the original after the spec finishes, so
|
|
// concurrent specs can each pretend to be running on a different host.
|
|
func withInterfaceAddrs(cidrs ...string) {
|
|
original := interfaceAddrsFn
|
|
interfaceAddrsFn = func() ([]net.Addr, error) {
|
|
out := make([]net.Addr, 0, len(cidrs))
|
|
for _, c := range cidrs {
|
|
ip, ipnet, err := net.ParseCIDR(c)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
out = append(out, &net.IPNet{IP: ip, Mask: ipnet.Mask})
|
|
}
|
|
return out, nil
|
|
}
|
|
DeferCleanup(func() { interfaceAddrsFn = original })
|
|
}
|
|
|
|
func withInterfaceAddrsErr(err error) {
|
|
original := interfaceAddrsFn
|
|
interfaceAddrsFn = func() ([]net.Addr, error) { return nil, err }
|
|
DeferCleanup(func() { interfaceAddrsFn = original })
|
|
}
|
|
|
|
var _ = Describe("requireAuthOrTrustedBind", func() {
|
|
BeforeEach(func() {
|
|
// Default to "host has only loopback" — the literal-IP cases below
|
|
// don't touch interfaceAddrsFn but the wildcard cases do, and a
|
|
// loopback-only host is the safest default for those.
|
|
withInterfaceAddrs("127.0.0.1/8", "::1/128")
|
|
})
|
|
|
|
It("permits any bind when auth is configured", func() {
|
|
Expect(requireAuthOrTrustedBind("0.0.0.0:8080", true, false)).To(Succeed())
|
|
Expect(requireAuthOrTrustedBind("203.0.113.5:8080", true, false)).To(Succeed())
|
|
})
|
|
|
|
It("permits any bind when --allow-insecure-public-bind is set", func() {
|
|
Expect(requireAuthOrTrustedBind("0.0.0.0:8080", false, true)).To(Succeed())
|
|
Expect(requireAuthOrTrustedBind("203.0.113.5:8080", false, true)).To(Succeed())
|
|
})
|
|
|
|
Context("literal IP binds", func() {
|
|
It("permits loopback", func() {
|
|
Expect(requireAuthOrTrustedBind("127.0.0.1:8080", false, false)).To(Succeed())
|
|
Expect(requireAuthOrTrustedBind("[::1]:8080", false, false)).To(Succeed())
|
|
Expect(requireAuthOrTrustedBind("127.5.4.3:8080", false, false)).To(Succeed())
|
|
})
|
|
|
|
DescribeTable("permits private LAN ranges",
|
|
func(addr string) {
|
|
Expect(requireAuthOrTrustedBind(addr, false, false)).To(Succeed())
|
|
},
|
|
Entry("RFC 1918 — 10/8", "10.0.0.5:8080"),
|
|
Entry("RFC 1918 — 172.16/12", "172.16.5.5:8080"),
|
|
Entry("RFC 1918 — 192.168/16", "192.168.1.5:8080"),
|
|
Entry("IPv6 ULA — fc00::/7", "[fc00::1]:8080"),
|
|
Entry("IPv6 ULA — fd00::/8", "[fd12:3456:789a::1]:8080"),
|
|
)
|
|
|
|
It("permits link-local addresses", func() {
|
|
Expect(requireAuthOrTrustedBind("169.254.10.10:8080", false, false)).To(Succeed())
|
|
Expect(requireAuthOrTrustedBind("[fe80::1]:8080", false, false)).To(Succeed())
|
|
})
|
|
|
|
It("permits CGNAT (Tailscale default)", func() {
|
|
Expect(requireAuthOrTrustedBind("100.64.0.5:8080", false, false)).To(Succeed())
|
|
Expect(requireAuthOrTrustedBind("100.127.255.1:8080", false, false)).To(Succeed())
|
|
})
|
|
|
|
It("rejects boundary addresses just outside CGNAT", func() {
|
|
Expect(requireAuthOrTrustedBind("100.63.255.255:8080", false, false)).To(HaveOccurred())
|
|
Expect(requireAuthOrTrustedBind("100.128.0.0:8080", false, false)).To(HaveOccurred())
|
|
})
|
|
|
|
It("rejects public IPv4", func() {
|
|
Expect(requireAuthOrTrustedBind("8.8.8.8:8080", false, false)).To(HaveOccurred())
|
|
Expect(requireAuthOrTrustedBind("203.0.113.5:8080", false, false)).To(HaveOccurred())
|
|
})
|
|
|
|
It("rejects public IPv6", func() {
|
|
Expect(requireAuthOrTrustedBind("[2606:4700:4700::1111]:8080", false, false)).To(HaveOccurred())
|
|
})
|
|
})
|
|
|
|
Context("wildcard bind (`:port`, 0.0.0.0, ::)", func() {
|
|
It("permits when every interface is private/loopback", func() {
|
|
withInterfaceAddrs("127.0.0.1/8", "::1/128", "10.0.0.5/24", "fc00::1/64")
|
|
Expect(requireAuthOrTrustedBind(":8080", false, false)).To(Succeed())
|
|
Expect(requireAuthOrTrustedBind("0.0.0.0:8080", false, false)).To(Succeed())
|
|
Expect(requireAuthOrTrustedBind("[::]:8080", false, false)).To(Succeed())
|
|
})
|
|
|
|
It("permits when interfaces are loopback + Tailscale CGNAT", func() {
|
|
withInterfaceAddrs("127.0.0.1/8", "::1/128", "100.65.10.20/32")
|
|
Expect(requireAuthOrTrustedBind("0.0.0.0:8080", false, false)).To(Succeed())
|
|
})
|
|
|
|
It("rejects when ANY interface has a public IP", func() {
|
|
withInterfaceAddrs("127.0.0.1/8", "::1/128", "10.0.0.5/24", "203.0.113.42/24")
|
|
Expect(requireAuthOrTrustedBind(":8080", false, false)).To(HaveOccurred())
|
|
Expect(requireAuthOrTrustedBind("0.0.0.0:8080", false, false)).To(HaveOccurred())
|
|
Expect(requireAuthOrTrustedBind("[::]:8080", false, false)).To(HaveOccurred())
|
|
})
|
|
|
|
It("fails closed when interface enumeration errors", func() {
|
|
withInterfaceAddrsErr(errors.New("enumeration disabled"))
|
|
Expect(requireAuthOrTrustedBind("0.0.0.0:8080", false, false)).To(HaveOccurred())
|
|
})
|
|
|
|
It("fails closed when the host has no addresses at all", func() {
|
|
withInterfaceAddrs()
|
|
Expect(requireAuthOrTrustedBind("0.0.0.0:8080", false, false)).To(HaveOccurred())
|
|
})
|
|
})
|
|
|
|
Context("hostname binds", func() {
|
|
It("permits 'localhost' (resolves to loopback)", func() {
|
|
Expect(requireAuthOrTrustedBind("localhost:8080", false, false)).To(Succeed())
|
|
})
|
|
})
|
|
|
|
Context("malformed input", func() {
|
|
It("rejects an address with no port", func() {
|
|
Expect(requireAuthOrTrustedBind("8080", false, false)).To(HaveOccurred())
|
|
})
|
|
|
|
It("rejects an empty address", func() {
|
|
Expect(requireAuthOrTrustedBind("", false, false)).To(HaveOccurred())
|
|
})
|
|
})
|
|
|
|
Context("error message", func() {
|
|
It("guides the operator with all four escape hatches", func() {
|
|
err := requireAuthOrTrustedBind("203.0.113.5:8080", false, false)
|
|
Expect(err).To(HaveOccurred())
|
|
msg := err.Error()
|
|
Expect(msg).To(ContainSubstring("--auth"))
|
|
Expect(msg).To(ContainSubstring("--api-keys"))
|
|
Expect(msg).To(ContainSubstring("--allow-insecure-public-bind"))
|
|
Expect(msg).To(ContainSubstring("LAN"))
|
|
Expect(msg).To(ContainSubstring("VPN"))
|
|
})
|
|
})
|
|
})
|