irpas技术客

多家庭共用IPTV——记“白嫖”父母家的IPTV

irpas 108

马上要过春节了,家里总得有电视直播作为背景音乐吧?

虽然现在可以找到各种各样的第三方软件可以观看电视直播,

甚至央视频App官方也拥有九十多个频道可以一键投屏到电视上观看。

但总觉得是差点意思。总不能为了春节几天,去办一个IPTV吧?

突然想到,父母家其实一直都有IPTV,那么是否能“啃老”,白嫖父母家的IPTV呢?

那我们就来想想办法吧

过程中甚至发现了有可能是运营商提供的“福利”:非IPTV的用户,可能也可以观看单播直播源。

在文章的最后,也会将整理出来的单播源和组播源分享给大家,同地区的朋友可以尝试一下。

基本情况

【设备】

光猫:ZXHN F610EV9(光猫无WiFi,本身就是桥接模式,3个千兆口+1个iTV口)
主路由:Redmi AX3000(官方系统,WAN口接光猫千兆口,PPPoE模式,大内网)
交换机:H3C Mini S2G(接光猫的iTV口,为了保证不影响IPTV盒子的正常使用)
旁路由:友善R2S(OpenWrt系统,旁路由模式,WAN口接交换机,LAN口接主路由的LAN口)
IPTV盒子:运营商送的电视盒子(接交换机)

image-20240206105012882

简单展示一下最终方案的网络拓补图,其实就是利用OpenWrt将IPTV的组播转单播,然后通过两地之间的私有网络进行传输数据。

这种方案虽然稍微有些复杂,但好在能保证既不影响原有的IPTV盒子的使用(与IPTV认证方式有关),也不会对原有网络产生什么影响。

但在实际操作的时候,也有不少细节和值得注意的地方,在此也将一一记录。

从整体上来说,总归是如下几步:

IPTV认证方式

直播源抓取

组播转换

私有网络搭建

IPTV认证方式

搜索网上的资料,大部分提到的都是PPPoE认证,但还需要根据自己家IPTV的实际情况来看。

主要的方式,就是查看运营商给的IPTV盒子里的设置。

image-20240206123601698

比如我这个IPTV盒子的以太网设置里没有选择PPPoE,就只是动态IP

根据这个方式,所以我在光猫的iTV口下面又加了一个交换机,从理论上来说这样就可以依旧正常使用原来的IPTV盒子了。

经过测试之后,IPTV盒子依旧可以正常使用。

基于此,我们可以再继续折腾。

抓取直播源

在上一步中,我们知道了这个IPTV是不需要PPPoE认证的,那是否还有其他方式的认证呢?那只能通过抓包来查看了。

在这一步中主要有两种方式:

直接在网上搜索别人分享的源

自己抓包

方式一就不过多介绍了,毕竟我在的这个是小地方,其他人分享的源未必适合我这里,所以这里主要说方式二。

通过查资料,发现网上大部分提供的抓包方案都是通过 Wireshark 来进行,主要就是因为 Wireshark 是可以抓包UDP的,而IPTV的直播流也都是通过UDP协议来传输的。

网上提供的抓包方案大概有两种:

使用具有端口镜像的交换机,将IPTV盒子发送的数据镜像给电脑的网卡,从而抓包。

在电脑上安装两个网口,分别接IPTV盒子和光猫iTV接口,并设置网络桥接,从而抓包。

但现在尴尬的是,现在手上既没有双网口的PC,也没有USB网卡,也没有具备端口镜像的交换机,只有一个刷了OpenWrt系统的R2S,但它也只有两个网口,那该怎么办呢?

从逻辑上来讲,UDP基本上只用于传输直播流,那么其他时候呢?比如电视盒子开机获取频道信息、获取推荐页面的信息流等信息呢?大概率使用的是http/https来进行传输的。

那如果是这样,我们就可以通过经常使用的 Charles 来进行抓包了,既然理论存在,我们就试试。

将电脑接在光猫iTV口下的交换机上,使电脑和IPTV盒子在同一个网段内

并在IPTV盒子的网络设置中设置代理,填写电脑上 Charles 的IP端口

image-20240206131140924

先不安装证书,碰碰运气,看看它用的是http还是https。

重启下 IPTV盒子。

image-20240206131611085

紧接着,我们就能在Charles中看到了大量的请求。

大概看一下,里面包含了注册OTT设备获取频道信息广告信息等内容。

基本上能了解到,设备通过内置的useridmac等信息,让服务器识别是用户身份的,但我们先不管这个。

最值得关注的是 http://yepg.99tv.com.cn:99/pic/channel/list/channel_1.js 这条请求,这是一个只包含JSON字符串的静态文件,里面包含了我这里IPTV全部的129个频道的信息。

image-20240206132748976

那就有意思了,那就相当于我们一下子抓取到了全部频道的播放地址,看样子这里不仅包括了组播源,还包括了单播源,直接用电脑上的VLC打开网络串流验证一下,rtp://239.33.3.2:22580。

image-20240206132953278

经过测试,我们发现在抓取的每个频道的流地址中,"hw" 中的单播源 和 "multi_HW" 中的组播源都是可以使用的。(可能是机房使用华为提供的服务?)

值得注意的是,我们现在测试直播源的PC,只是和IPTV盒子在同一网段下,并没有携带任何认证信息,但依旧可以播放抓取到的流地址。从而我们推断,也许服务器并不会对此进行鉴权。

现在我们也抓取到了直播源,也测试成功了,那么后面的一切都变得简单了。

因为 Charles 无法抓包UDP数据包,所以我们无从得知是否还有隐藏的认证方式,但通过测试是可以正常播放抓取到的组播源的,那就暂时先不管那么多了。

现在想想,如果只有R2S的话,也是可以使用OpenWrt创建网桥设备,再通过tcpdump来导出数据包来实现抓包UDP数据的。

还有一点有意思的是,在后续的测试中发现,抓取到的 "hw" 键值对中的单播源,即使是在光猫的非iTV接口下,也是可以播放的。难道说,即使我不办理IPTV只办理这个运营商的宽带,也是可以观看单播源的?难道这个是运营商给的福利?那么问题来了,如果不办理IPTV,只把设备接到iTV口上,是否可以播放组播源呢?(当然,现在这个IPTV也是套餐里送的……)

组播转换

既然上一步已经获取到了单播源,我们还需要再进行组播转换吗?

我认为其实还是有必要的,经过直播画面整点报时和实际时间对比,抓取到的单播源的直播延迟大概在50-60秒而组播源的直播延迟只有10秒左右。所以,我们还是有必要自己进行一次组播转换。

这时候就要请出我们的友善R2S了,写入万能的OpenWrt系统。

具体的固件我一直都使用的 kiddin9 大佬提供的在线编译定制服务 https://openwrt.ai/

image-20240206140125966

可以根据自己的需要,直接按需求定制固件。比如这里就可以直接勾选旁路由模式,并设置好LAN口的IP和主路由的网关,这样刷写好固件后,直接插上网线,不需要进行设置就能直接使用。

如果是手动设置旁路由模式的话,也主要就是设置好LAN口的IP地址和网关就可以。

需要注意的是,在旁路由模式中,是将LAN口接在主路由的LAN口上哦,而不是将WAN口接在主路由的LAN上。在此案例中,我们关闭LAN口的DHCP服务

image-20240206141839780

接着,我们将WAN口设置为DHCP客户端,顺手也将主机名改一下,伪装一下 :P

对于WAN口的其他设置,这个主要是看各自IPTV的实际情况,我这里其他设置保持默认就可以。

image-20240206141759437

一切都准备好后,按照最开始提到的网络拓补图中的方式,将R2S接入到网络中。

image-20240206141624837

此时已经可以看到WAN口已经正常获取到了动态的IP地址。

接着我们先配置一下OpenWrt防火墙的通信规则(网络 → 防火墙 → 通信规则)。

点击下方的 “添加” 按钮,我们新建一条规则,用来 允许所有组播网段的流量

image-20240206141202169

组播转换目前大家用的多的主要是两个插件,一个是 udpxy(luci-app-udpxy),一个是 msd_lite(luci-app-msd_lite),据说后者在低配置的设备上性能更好一些。

插件的配置也很简单,主要就是配置好 源接口 和 绑定地址 即可,其他设置都可以保持默认。

msd_lite设置

image-20240206142624566

udpxy设置

image-20240206142651204

udpxy 和 msd_lite 启用一个即可,我这里只是做一个演示。

接下来我们来打开直播源,来测试下插件启动是否成功,直播源的地址我们也需要加工一下。变为:http://192.168.31.2:7088/rtp/239.33.3.2:22580

WX20240203-145446@2x

如果一切正常的话,此时我们就能看到电视的直播画面了。

经过不严谨的测试发现,在我当前的设备上,msd_lite在转发的时候cpu的使用率大概在4%左右udpxy大概在10%左右。所以后续都将使用 msd_lite

image-20240206143454665

image-20240206143523460

经过如上设置,我们已经可以在家庭A的网络中观看电视直播了

私有网络搭建

接下来就是如何让家庭B也能观看电视直播,其实方案就是大家都知道的VPN,这里使用的是OpenVPNOpenVPN相对来说复杂一些,所以这里也只介绍该案例中用到的一些东西。

因为家庭A没有公网IP家庭B有公网IP。所以将在 家庭B 的OpenWrt上架设服务端,而 家庭A R2S上的OpenWrt便作为客户端

除此之外,还需要在家庭B的OpenWrt上配置ddns服务,以方便让客户端可以找到服务端,在此便不多介绍。

OpenVPN服务端

在这里使用的是 “luci-app-openvpn” 插件,因为固件已经内置了插件并且创建好了tun0网络设备,所以这里就跳过这些设置(如果需要,可参考客户端中“配置网络接口”章节),只分享证书生成以及服务端配置文件。

生成证书文件

证书认证是OpenVPN的主要认证方式,其中包括ca证书、dh密钥交换参数、服务器证书、服务器密钥、tls认证key、客户端证书和客户端密钥

这里使用 easy-rsa 来生成所有的证书,既可以在路由器中生成,也可以在本地中生成,方法都一样。

修改easy-rsa目录下 vars 配置(原名 vars.example ,复制重命名为 vars)

<em class="CopyMyCode" style="overflow-wrap: break-word; cursor: pointer; font-size: 12px; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(51, 102, 153) !important;"> 复制代码</em><em class="hideCode" style="overflow-wrap: break-word; cursor: pointer; font-size: 12px; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(51, 102, 153) !important;"> 隐藏代码<br style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important;">
</em><code class="hljs language-perl" style="overflow-wrap: break-word; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace; background-color: rgb(0, 0, 0); color: rgb(248, 248, 248); padding: 1em; overflow-x: auto; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important;"><span class="hljs-comment" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(124, 124, 124);"># 删掉注释符号,根据实际情况填写即可</span>
set_var EASYRSA_REQ_COUNTRY <span class="hljs-string" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(168, 255, 96);">"US"</span>
set_var EASYRSA_REQ_PROVINCE    <span class="hljs-string" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(168, 255, 96);">"California"</span>
set_var EASYRSA_REQ_CITY    <span class="hljs-string" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(168, 255, 96);">"San Francisco"</span>
set_var EASYRSA_REQ_ORG <span class="hljs-string" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(168, 255, 96);">"Copyleft Certificate Co"</span>
set_var EASYRSA_REQ_EMAIL   <span class="hljs-string" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(168, 255, 96);">"me@example.net"</span>
set_var EASYRSA_REQ_OU      <span class="hljs-string" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(168, 255, 96);">"My Organizational Unit"</span>

<span class="hljs-comment" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(124, 124, 124);">#设置证书的有效期</span>
set_var EASYRSA_CA_EXPIRE   <span class="hljs-number" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(255, 115, 253);">3650</span>
set_var EASYRSA_CERT_EXPIRE <span class="hljs-number" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(255, 115, 253);">3650</span></code>

初始化目录

<em class="CopyMyCode" style="overflow-wrap: break-word; cursor: pointer; font-size: 12px; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(51, 102, 153) !important;"> 复制代码</em><em class="hideCode" style="overflow-wrap: break-word; cursor: pointer; font-size: 12px; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(51, 102, 153) !important;"> 隐藏代码<br style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important;">
</em><code class="hljs language-bash" style="overflow-wrap: break-word; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace; background-color: rgb(0, 0, 0); color: rgb(248, 248, 248); padding: 1em; overflow-x: auto; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important;">./easyrsa init-pki</code>

生成ca证书

<em class="CopyMyCode" style="overflow-wrap: break-word; cursor: pointer; font-size: 12px; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(51, 102, 153) !important;"> 复制代码</em><em class="hideCode" style="overflow-wrap: break-word; cursor: pointer; font-size: 12px; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(51, 102, 153) !important;"> 隐藏代码<br style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important;">
</em><code class="hljs language-bash" style="overflow-wrap: break-word; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace; background-color: rgb(0, 0, 0); color: rgb(248, 248, 248); padding: 1em; overflow-x: auto; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important;">./easyrsa build-ca  <span class="hljs-comment" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(124, 124, 124);"># 如果不需要密码则增加 nopass 参数</span></code>

过程中设置密码Common Name,生成位置: ./pki/ca.crt

生成服务器密钥

<em class="CopyMyCode" style="overflow-wrap: break-word; cursor: pointer; font-size: 12px; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(51, 102, 153) !important;"> 复制代码</em><em class="hideCode" style="overflow-wrap: break-word; cursor: pointer; font-size: 12px; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(51, 102, 153) !important;"> 隐藏代码<br style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important;">
</em><code class="hljs language-bash" style="overflow-wrap: break-word; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace; background-color: rgb(0, 0, 0); color: rgb(248, 248, 248); padding: 1em; overflow-x: auto; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important;">./easyrsa gen-req server nopass</code>

设置Common Name后,会在 ./pki/private/ 目录下生成名为 server.key 的私钥

生成服务器证书

<em class="CopyMyCode" style="overflow-wrap: break-word; cursor: pointer; font-size: 12px; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(51, 102, 153) !important;"> 复制代码</em><em class="hideCode" style="overflow-wrap: break-word; cursor: pointer; font-size: 12px; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(51, 102, 153) !important;"> 隐藏代码<br style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important;">
</em><code class="hljs language-bash" style="overflow-wrap: break-word; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace; background-color: rgb(0, 0, 0); color: rgb(248, 248, 248); padding: 1em; overflow-x: auto; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important;">./easyrsa sign server server</code>

过程中输入“yes” 确认信息,如果生成ca证书时设置了密码,还需要验证ca证书的密码。

会在./pki/issued/目录下生成名为 server.crt 的证书

生成dh密钥交换参数

<em class="CopyMyCode" style="overflow-wrap: break-word; cursor: pointer; font-size: 12px; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(51, 102, 153) !important;"> 复制代码</em><em class="hideCode" style="overflow-wrap: break-word; cursor: pointer; font-size: 12px; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(51, 102, 153) !important;"> 隐藏代码<br style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important;">
</em><code class="hljs language-bash" style="overflow-wrap: break-word; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace; background-color: rgb(0, 0, 0); color: rgb(248, 248, 248); padding: 1em; overflow-x: auto; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important;">./easyrsa gen-dh</code>

生成位置:./pki/dh.pem

(可选)生成tls认证key

<em class="CopyMyCode" style="overflow-wrap: break-word; cursor: pointer; font-size: 12px; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(51, 102, 153) !important;"> 复制代码</em><em class="hideCode" style="overflow-wrap: break-word; cursor: pointer; font-size: 12px; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(51, 102, 153) !important;"> 隐藏代码<br style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important;">
</em><code class="hljs language-css" style="overflow-wrap: break-word; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace; background-color: rgb(0, 0, 0); color: rgb(248, 248, 248); padding: 1em; overflow-x: auto; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important;">openvpn <span class="hljs-attr" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important;">--genkey</span> <span class="hljs-attr" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important;">--secret</span> /etc/openvpn/ta<span class="hljs-selector-class" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important;">.key</span></code>

此步骤非必须,如若使用tls认证key,则需要分别在客户端和服务端中增加相应的配置

生成客户端密钥

<em class="CopyMyCode" style="overflow-wrap: break-word; cursor: pointer; font-size: 12px; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(51, 102, 153) !important;"> 复制代码</em><em class="hideCode" style="overflow-wrap: break-word; cursor: pointer; font-size: 12px; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(51, 102, 153) !important;"> 隐藏代码<br style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important;">
</em><code class="hljs language-bash" style="overflow-wrap: break-word; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace; background-color: rgb(0, 0, 0); color: rgb(248, 248, 248); padding: 1em; overflow-x: auto; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important;">./easyrsa gen-req homeA nopass</code>

过程中会设置Common Name,建议记住这个名称以方便后续进行管理,名称区分大小写。

会在./pki/private/目录下生成名为 homeA.key 的文件

生成客户端证书

<em class="CopyMyCode" style="overflow-wrap: break-word; cursor: pointer; font-size: 12px; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(51, 102, 153) !important;"> 复制代码</em><em class="hideCode" style="overflow-wrap: break-word; cursor: pointer; font-size: 12px; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(51, 102, 153) !important;"> 隐藏代码<br style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important;">
</em><code class="hljs language-bash" style="overflow-wrap: break-word; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace; background-color: rgb(0, 0, 0); color: rgb(248, 248, 248); padding: 1em; overflow-x: auto; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important;">./easyrsa sign-req client homeA</code>

过程中输入“yes” 确认信息,如果生成ca证书时设置了密码,还需要验证ca证书的密码。

会在./pki/issued/目录下生成名为 homeA.crt 的证书

整理证书文件

上述过程中3、4、5、6四步中的证书,需要保存在服务器3、8、9三步的证书,需要保存在客户端

如果服务端中使用了 7,那么客户端中也得有 7 。如果后续要增加新的客户端证书,重复8、9两步即可。

我们将证书分别保存在方便找到的位置,服务器中可保存在 /etc/openvpn 文件夹下。

配置OpenVPN服务端

image-20240206161320886

打开OpenVPN插件页面,我们可以使用自带的模板来进行配置。

添加完成后,来编辑配置,默认会进入基本配置,我们点击左上角的高级配置

可以点击左下角的选项来添加选项,如果要删除选项,直接删掉文本框里的内容就行,或者选择 remove 选项。如果还不行,可以直接编辑 /etc/config/openvpn 文件。

要注意的是,编辑完每一页都需要保存一下。

image-20240206161926878

服务设置保持默认即可,可以添加log来设置log输出用来排错

image-20240206162137026

网络设置中,需要要把 topology 设置为 subnet 。这样在未来,我们才能为客户端绑定固定IP。有一点需要注意的失, topology 选项只有添加了 dev_type 选项并且设置 tun 后,才会出现在左下角的下拉框中

image-20240206162647069

VPN设置中,主要就是添加 client_config_dir 设置,并且新建好目录,方便后面为客户端设置固定IP。

在push设置中,添加“route 10.10.10.0 255.255.255.0”以让客户端可以访问到家庭B的路由器网段。

其他的配置保持默认或根据实际情况调整即可。

image-20240206162952317

加密算法设置中,分别设置好前面生成好的几个证书的位置,如果填写了 tls_auth ,那么在服务端一定要将 key_direction 设置为 0 ,而在客户端该项配置则为 1 。

image-20240206164114202

其他都设置好后,我们回到基本配置中,添加 proto 选项,并设置为 tcp-server

(不知道为啥,我的这个选项有时候就自己消失了……)

一切都设置好保存之后,可以返回页面,启用并启动服务。

如果启动失败,则可以根据log中的报错,来进行排查。

image-20240206163252627

正常来说,如此设置之后,所有家庭B网络下的设备都可以和OpenVPN连接进来的客户端互相访问。在OpenWrt的状态 → 路由下,也能看到 tun0 10.8.0.0/24 的路由信息。

OpenVPN客户端

我们再回到家庭AR2S上,这里要作为OpenVPN的客户端,同样使用的是 “luci-app-openvpn” 插件。

制作OpenVPN客户端配置文件

在OpenVPN中,我们可以把客户端的证书以及配置都整合在一个后缀名为 ovpn 的文本文件中,方便传输到客户端中。

<em class="CopyMyCode" style="overflow-wrap: break-word; cursor: pointer; font-size: 12px; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(51, 102, 153) !important;"> 复制代码</em><em class="hideCode" style="overflow-wrap: break-word; cursor: pointer; font-size: 12px; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(51, 102, 153) !important;"> 隐藏代码<br style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important;">
</em><code class="hljs language-bash" style="overflow-wrap: break-word; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace; background-color: rgb(0, 0, 0); color: rgb(248, 248, 248); padding: 1em; overflow-x: auto; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important;">client
dev tun
proto tcp-client
remote vpn.server.com 1194
resolv-retry infinite
nobind
persist-key
persist-tun
verb 3
<ca>
<span class="hljs-comment" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(124, 124, 124);"># ca证书文件内容</span>
</ca>
<cert>
<span class="hljs-comment" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(124, 124, 124);"># 客户端证书文件内容</span>
</cert>
<key>
<span class="hljs-comment" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(124, 124, 124);"># 客户端密钥文件内容</span>
</key>
<tls-auth>
<span class="hljs-comment" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(124, 124, 124);"># tls认证key文件内容</span>
</tls-auth>
key-direction 1
comp-lzo
</code>

将配置文件中 remote 后面修改为自己的域名或IP地址,如果服务端使用了 tls_auth ,那么客户端也需要添加 tls-auth 块,并将 key-direction 设置为 1 。检查配置没错后,我们保存为 HomeA.ovpn 。

配置网络接口

虽然安装了OpenVPN插件,但似乎并没有自动添加相关的网络接口,这时就算客户端显示正在运行,但其实并没有连接到服务端中。

这里我们就来手动添加网络接口

image-20240206165510107

在 网络 → 接口 中,点击“添加新接口”

image-20240206165636003

为接口起一个名字 vpn,协议选择“不配置协议”,在设备中选择 tun0 ,如果没有则在下拉框的“自定义”框中,填写 tun0。

image-20240206165904661

接着,在接口的“防火墙设置”中,创建一个名为 vpn 的防火墙区域。

之后,来到网络 → 防火墙中,将常规设置中的数据都改为“接受”(有可能会影响到后面访问客户端)

image-20240206170704181

在下方,找到刚新建的vpn区域,点击编辑

image-20240206170223183

常规设置中,接受所有数据,并允许lan区域的转发。

image-20240206170304338

高级设置中,涵盖的设备选择 tun0,如果没有则在自定义框中添加。

都设置好之后,建议重启一下路由器,确保配置生效。

这时,在 网络接口 中,我们会看到新建的vpn接口提示“设备不存在”,但没关系。

配置OpenVPN客户端

之前已经制作好了客户端的配置文件,这时我们只要在OpenVPN插件页面将客户端文件上传即可

image-20240206171919120

image-20240206172105075

如果没有意外的话,启用配置文件,就可以正常运行了。

image-20240206172320893

这时候,我们看 网络 → 接口 中的vpn接口已经有接收和发送的数据了。

如果这时候还是没有vpn接口的数据,那可以尝试重启路由器、删掉OpenVPN配置文件重新添加。

image-20240206172616562

在终端中,输入 ip addr ,即可看到 tun0 设备中获取到的IP地址。

此时,我们使用家庭B的网络已经能通过 http://10.8.0.2 访问家庭A中的R2S了。

同样地,在家庭B网络下的电脑中的VLC,也可以顺利访问 http://10.8.0.2:7088/rtp/239.33.3.2:22580 观看直播了。

OpenVPN客户端绑定IP

现在两地之间的网络已经互通了,但现在R2S在vpn中的IP还是动态的,接下来我们给客户端绑定IP。

还记得在配置服务端时的VPN设置中的 client_config_dir 吗?

还记得在生成客户端证书时设置的 Common Name 吗?

这时,我们在 client_config_dir 配置的文件夹下,新建一个名为客户端Common Name的文件,这里就是 HomeA。(如果忘记了Common Name,可以在证书文件中查看,一定要注意大小写!)在文件中写入下面的配置,意为:将名为HomeA客户端的IP设置为10.8.0.200

<em class="CopyMyCode" style="overflow-wrap: break-word; cursor: pointer; font-size: 12px; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(51, 102, 153) !important;"> 复制代码</em><em class="hideCode" style="overflow-wrap: break-word; cursor: pointer; font-size: 12px; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(51, 102, 153) !important;"> 隐藏代码<br style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important;">
</em><code class="hljs language-armasm" style="overflow-wrap: break-word; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace; background-color: rgb(0, 0, 0); color: rgb(248, 248, 248); padding: 1em; overflow-x: auto; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important;"><span class="hljs-symbol" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(198, 197, 254);">ifconfig</span>-<span class="hljs-keyword" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(150, 203, 254);">push</span> <span class="hljs-number" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(255, 115, 253);">10</span>.<span class="hljs-number" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(255, 115, 253);">8</span>.<span class="hljs-number" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(255, 115, 253);">0</span>.<span class="hljs-number" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(255, 115, 253);">200</span> <span class="hljs-number" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(255, 115, 253);">255</span>.<span class="hljs-number" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(255, 115, 253);">255</span>.<span class="hljs-number" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(255, 115, 253);">255</span>.<span class="hljs-number" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(255, 115, 253);">0</span></code>

分别重启服务端和客户端后,R2S获取的vpn的IP地址已经变为了绑定的IP地址。

image-20240206174618362

如果在连接的时候报错,请检查服务端网络设置中 topology 是否设置为 subnet 。

或者使用同样的客户端配置文件,在PC端或者手机端连接OpenVPN进行排错。

其他配置

通过Watchcat检查vpn连通性

因为服务端还是客户端都是自己家里的设备,无法保证一直在线。一旦作为服务端的OpenWrt重启后,客户端的vpn断开后便不会自动重连

所以,使用Watchcat插件间隔检查与服务端的连通性,若ping不通时,则自动重启,以保证R2S随时在线。

image-20240206180202444

用脚本整理所有的直播源

用一个简单的脚本,将抓取到的直播源进行整理

<em class="CopyMyCode" style="overflow-wrap: break-word; cursor: pointer; font-size: 12px; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(51, 102, 153) !important;"> 复制代码</em><em class="hideCode" style="overflow-wrap: break-word; cursor: pointer; font-size: 12px; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(51, 102, 153) !important;"> 隐藏代码<br style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important;">
</em><code class="python hljs language-python" style="overflow-wrap: break-word; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace; background-color: rgb(0, 0, 0); color: rgb(248, 248, 248); padding: 1em; overflow-x: auto; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important;"><span class="hljs-comment" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(124, 124, 124);"># iptv_m3u8.j2</span>
<span class="hljs-comment" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(124, 124, 124);">#EXTM3U</span>
{%- <span class="hljs-keyword" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(150, 203, 254);">for</span> ch <span class="hljs-keyword" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(150, 203, 254);">in</span> channel_list %}
<span class="hljs-comment" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(124, 124, 124);">#EXTINF:-1,{{ ch.get('name') }}</span>
{{ ch.get(<span class="hljs-string" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(168, 255, 96);">'playurl'</span>) }}
{%- endfor -%}

<span class="hljs-comment" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(124, 124, 124);"># -----------------------------------------</span>
<span class="hljs-comment" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(124, 124, 124);"># main.py</span>
<span class="hljs-keyword" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(150, 203, 254);">import</span> json, os, time
<span class="hljs-keyword" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(150, 203, 254);">from</span> jinja2 <span class="hljs-keyword" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(150, 203, 254);">import</span> Environment, select_autoescape, FileSystemLoader

PROXYURL = <span class="hljs-string" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(168, 255, 96);">'http://10.8.0.200:7088/'</span>
SOURCE_FILE_PATH = <span class="hljs-string" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(168, 255, 96);">'channel_1.js'</span>

<span class="hljs-keyword" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(150, 203, 254);">with</span> <span class="hljs-built_in" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important;">open</span>(SOURCE_FILE_PATH, <span class="hljs-string" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(168, 255, 96);">'r+'</span>) <span class="hljs-keyword" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(150, 203, 254);">as</span> f:
    json_data = json.loads(f.read())

channels_source = json_data[<span class="hljs-number" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(255, 115, 253);">0</span>].get(<span class="hljs-string" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(168, 255, 96);">'channelList'</span>)

env = Environment(
    loader=FileSystemLoader(os.getcwd()), 
    autoescape=select_autoescape()
)
template = env.get_template(<span class="hljs-string" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(168, 255, 96);">'iptv_m3u8.j2'</span>)

channel_list = []
channel_list2 = []

<span class="hljs-keyword" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(150, 203, 254);">for</span> channel <span class="hljs-keyword" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(150, 203, 254);">in</span> channels_source:
    name = channel.get(<span class="hljs-string" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(168, 255, 96);">'name'</span>)
    hw = channel.get(<span class="hljs-string" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(168, 255, 96);">'multi_HW'</span>)
    hw = hw.replace(<span class="hljs-string" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(168, 255, 96);">'rtp://'</span>, <span class="hljs-string" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(168, 255, 96);">'rtp/'</span>)
    play_url = <span class="hljs-string" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(168, 255, 96);">f'<span class="hljs-subst" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(218, 239, 163);">{PROXYURL}</span><span class="hljs-subst" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(218, 239, 163);">{hw}</span>'</span>
    _info = {
        <span class="hljs-string" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(168, 255, 96);">'name'</span>: name,
        <span class="hljs-string" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(168, 255, 96);">'playurl'</span>: play_url
    }

    _info2 = {
        <span class="hljs-string" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(168, 255, 96);">'name'</span>: name,
        <span class="hljs-string" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(168, 255, 96);">'playurl'</span>: channel.get(<span class="hljs-string" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(168, 255, 96);">'hw'</span>)
    }
    channel_list.append(_info)
    channel_list2.append(_info2)

m3u8 = template.render(channel_list=channel_list)
m3u82 = template.render(channel_list=channel_list2)

<span class="hljs-keyword" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(150, 203, 254);">with</span> <span class="hljs-built_in" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important;">open</span>(<span class="hljs-string" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(168, 255, 96);">f'iptv_<span class="hljs-subst" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(218, 239, 163);">{<span class="hljs-built_in" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important;">int</span>(time.time())}</span>.m3u8'</span>, <span class="hljs-string" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(168, 255, 96);">'w+'</span>) <span class="hljs-keyword" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(150, 203, 254);">as</span> f:
    f.write(m3u8)
<span class="hljs-keyword" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(150, 203, 254);">with</span> <span class="hljs-built_in" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important;">open</span>(<span class="hljs-string" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(168, 255, 96);">f'iptv2_<span class="hljs-subst" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(218, 239, 163);">{<span class="hljs-built_in" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important;">int</span>(time.time())}</span>.m3u8'</span>, <span class="hljs-string" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(168, 255, 96);">'w+'</span>) <span class="hljs-keyword" style="overflow-wrap: break-word; text-shadow: transparent 0px 0px 0px, rgba(0, 0, 0, 0.68) 0px 0px 0px !important; color: rgb(150, 203, 254);">as</span> f:
    f.write(m3u82)</code>

生成带台标及EPG的直播源

使用 http://epg.51zmt.top:8000/ 上传整理好的直播源,网站会自动匹配每个频道的台标及EPG

但可能个别频道识别有误,导致EPG没那么匹配,自己检查一下即可。

image-20240206182123397

至此,我们已经可以在家庭B中观看家庭A中的IPTV了,并且很稳定。

经过测试,1080P清晰度的频道需要的带宽大概是 1MB/s ,4K清晰度的频道需要的带宽大概是 3-4MB/s ,所以在带宽上也都不是问题。

整理后的组播源和单播源已分享至GitHub:https://github.com/WaterCalm/ct_ln_dl_iptv


1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,会注明原创字样,如未注明都非原创,如有侵权请联系删除!;3.作者投稿可能会经我们编辑修改或补充;4.本站不提供任何储存功能只提供收集或者投稿人的网盘链接。

上一篇 风靡亚洲的家居收纳整理课

下一篇没有了